Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support multiple ethereum rpcs #215

Closed
wants to merge 14 commits into from
Closed

feat: support multiple ethereum rpcs #215

wants to merge 14 commits into from

Conversation

toteki
Copy link
Member

@toteki toteki commented Feb 24, 2022

struct EthRPCManager supports multiple ethereum RPC endpoints and attempts to rotate through them when one fails

closes #196

@toteki
Copy link
Member Author

toteki commented Feb 26, 2022

All functions requiring rpc.Client or ethclient.Client have been wrapped by EthRPCManager, except the following:

cmd/bridge.go L171 in deployGravityCmd

address, tx, _, err := wrappers.DeployGravity(auth, ethRPC, gravityIDBytes32, validators, powers)

cmd/bridge.go L546 in getGravityContract

contract, err := wrappers.NewGravity(ethcmn.HexToAddress(gravityAddr), ethRPC)

cmd/bridge.go L560 in approveERC20

contract, err := wrappers.NewERC20(ethcmn.HexToAddress(erc20AddrStr), ethRPC)

cmd/orchestrator.go L126 in getOrchestratorCmd

ethProvider := provider.NewEVMProvider(ethRPC)

Those functions require structs of the types rpc.Client or ethclient.Client (both structures, not interfaces) to be passed in, so it will be more difficult to wrap them if we want to automatically redial on any rpc errors that occur after the initial client is passed in.

If the commands in bridge.go run instantly and exit on error, then they don't need redial-after-error and the EVMProviderWithRet in orchestrator.go is the only remaining issue. The provider is also used to create other structures like EthCommitter, so it would be difficult to wrap everything.

@toteki
Copy link
Member Author

toteki commented Feb 26, 2022

A note: The current behavior of EthRPCManager is as follows:

Stores the following:

type EthRPCManager struct {
	currentEndpoint int // index (in the slice of configured RPC endpoints) of most recent endpoint used
	client          *rpc.Client
	konfig          *koanf.Koanf
}

Has the following methods which return the current client if non-nil, otherwise attempting to dial each configured eth rpc endpoint in a roundrobin fashion, returning the first successfully dialed and only trying each endpoint a maximum of once per call.

// returns the current eth RPC client, dialing one first if nonexistent
func (em *EthRPCManager) GetClient() (*rpc.Client, error)

// returns the current eth RPC client, dialing one first if nonexistent
func (em *EthRPCManager) GetEthClient() (*ethclient.Client, error)

Additionally has the following methods, which call the same-name method on the current eth client, dialing one first if nil, and closing the current eth client if the wrapped function returns an error.

// wraps ethclient.PendingNonceAt, also closing client if PendingNonceAt returns an error
func (em *EthRPCManager) PendingNonceAt(ctx context.Context, addr common.Address) (uint64, error)

// wraps ethclient.ChainID, also closing client if ChainID returns an error
func (em *EthRPCManager) ChainID(ctx context.Context) (*big.Int, error)

// wraps ethclient.SuggestGasPrice, also closing client if SuggestGasPrice returns an error
func (em *EthRPCManager) SuggestGasPrice(ctx context.Context) (*big.Int, error)

@toteki
Copy link
Member Author

toteki commented Feb 26, 2022

Remaining things we might want to do:

  • difficult: figure out how to wrap EVMProviderWithRet and similar functions spawned in orchestrator.go
  • very optional: modify PendingNonceAt, ChainID, and SuggestGasPrice to retry once with a new client before returning upon receiving an error, thus increasing their reliability.

@toteki toteki marked this pull request as ready for review February 27, 2022 00:04
@facundomedica
Copy link

I like this :)
nit: cmd/peggo/grpcclient.go I think grpcclient is a typo, right? Should be something more like ethrpc_manager or any other name (I'm terrible at naming)

@alexanderbez
Copy link

alexanderbez commented Feb 28, 2022

@toteki excellent summary and I like the approach.

Regarding to the remaining (optional?) items:

difficult: figure out how to wrap EVMProviderWithRet and similar functions spawned in orchestrator.go

Can you elaborate a bit more on this? Which functions in particular? You mean calls to provider.* package? If so, I don't think there's much we can do about that since we can't control what that function accepts, unless we want to wrap all the provider.*` calls, which may or may not be overkill.

Thoughts @facundomedica ?

I'll be reviewing this PR shortly :)

@@ -43,11 +43,19 @@ Ref: https://keepachangelog.com/en/1.0.0/

## [Unreleased]

### Features

- [#215](https://github.com/umee-network/peggo/pull/215) Add the flag `--eth-rpcs` and support multiple ethereum rpc endpoints

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- [#215](https://github.com/umee-network/peggo/pull/215) Add the flag `--eth-rpcs` and support multiple ethereum rpc endpoints
- [#215](https://github.com/umee-network/peggo/pull/215) Add the flag `--eth-rpcs` and support multiple ethereum rpc endpoints.

@@ -123,13 +122,12 @@ func getOrchestratorCmd() *cobra.Command {
return fmt.Errorf("failed to initialize Ethereum account: %w", err)
}

ethRPCEndpoint := konfig.String(flagEthRPC)
ethRPC, err := ethrpc.Dial(ethRPCEndpoint)
em := NewEthRPCManager(konfig)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see what you mean @toteki. I think the biggest win for peggo would be to have ETH RPC rotation at the orchestrator level, but it seems in order to do that, we'd have to do way too much wrapping :-/

"github.com/pkg/errors"
)

type EthRPCManager struct {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: add a godoc :)

}

// creates an instance of EthRPCManager with a given konfig.
func NewEthRPCManager(konfig *koanf.Koanf) *EthRPCManager {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible for there to be concurrent use of an EthRPCManager instance?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use of the embedded clients themselves should be concurrency-safe, but concurrent redialing / closing would need to be worked on - for example if one goroutine closed the client (perhaps as a result of an error in a grpc call) while another was doing something, the latter could geta very unexpected "connection was closed" type of error.

Or if two concurrent redialing loops were active, a whole lot could happen. It should be possible to add a lock on DialNext() in particular before merging if we move forward with this.

em.client = cli
return true
}
fmt.Fprintf(os.Stderr, "Failed to dial to Ethereum RPC: %s\n", rpcs[i])

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably pass the core logger to NewEthRPCManager instead of using fmt.*.

return false
}

// first tries all endpoints in the slice after the current index

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could probably combine both these loops into one using the modulo operator.

@facundomedica
Copy link

facundomedica commented Feb 28, 2022

Just started looking deeper into this one. I'd consider this an "all or nothing" situation given that if just a single endpoint isn't covered, then we'll be back at where we started.

For example see this one: https://github.com/umee-network/peggo/blob/main/orchestrator/eth_event_watcher_test.go#L185-L192

If any of those endpoints are not covered by this, then sadly the whole effort was in vain.
I think we either wrap all these functions or add an external helper (like those nginx proxies people were sharing).

type EVMProvider interface {
	bind.ContractCaller // <- Must remove these and add their functions here too (probably)
	bind.ContractFilterer

	PendingNonceAt(ctx context.Context, account ethcmn.Address) (uint64, error)
	PendingCodeAt(ctx context.Context, account ethcmn.Address) ([]byte, error)
	EstimateGas(ctx context.Context, msg ethereum.CallMsg) (uint64, error)
	SuggestGasPrice(ctx context.Context) (*big.Int, error)
	TransactionByHash(ctx context.Context, hash ethcmn.Hash) (tx *types.Transaction, isPending bool, err error)
	TransactionReceipt(ctx context.Context, txHash ethcmn.Hash) (*types.Receipt, error)
	SendTransaction(ctx context.Context, tx *types.Transaction) error
	HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error)
	SuggestGasTipCap(ctx context.Context) (*big.Int, error)
}

We could even change EVMProvider/EVMProviderWithRet to be EthRPCManager?

Now, is it worth it? I'm not sure...

  • it gives us the best plug&play experience ever but in exchange we have a lot of work to do
  • I'm also very tempted to just provide some help with an Nginx config, which looks extremely simple (AFAIK it will handle health checks and all that stuff)

EDIT: I feel like I gave this text a harsh tone, so want to clarify that I love what you did here @toteki and it was quite eye-opening to see how hard it can be to add something that said in conversation sounds easy to do ("just add a backup lol").
I missed the "how to be polite in writing" English class, so imagine me saying this while smiling 😅

@alexanderbez
Copy link

I do agree that if we don't have RPC rotation at the orchestrator level, we don't get much of a win.

@facundomedica are you suggesting we just scrap this in favor of providing some general nginx configuration to peggo operators?

@toteki
Copy link
Member Author

toteki commented Feb 28, 2022

If nginx can do it generally without us having to go into the provider package, then I agree that would be a more effective solution then reinventing it here

@alexanderbez
Copy link

@facundomedica can you share what you have in mind re nginx?

@facundomedica
Copy link

A valoper shared this in Discord: https://github.com/stakingcare/node-tools/blob/main/eth-api-failover/default
Haven't tried it, but looks pretty neat

@toteki
Copy link
Member Author

toteki commented Mar 17, 2022

Closing for now

@toteki toteki closed this Mar 17, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Support Multiple Ethereum RPCs
3 participants