diff --git a/CHANGELOG.md b/CHANGELOG.md index 360a843e..c5b92949 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 + ### Bug Fixes - [#209](https://github.com/umee-network/peggo/pull/209) Fix the `version` command to display correctly. - [#205](https://github.com/umee-network/peggo/pull/205) Make sure users are warned when using unencrypted non-local urls in flags. +### Deprecated + +- [#215](https://github.com/umee-network/peggo/pull/215) Deprecate the `--eth-rpc` flag. + ## [v0.2.5](https://github.com/umee-network/peggo/releases/tag/v0.2.5) - 2022-02-21 ### Features diff --git a/README.md b/README.md index 77dfe2d0..ad9dd83a 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ $ umeed tx gravity set-orchestrator-address \ ```shell export PEGGO_ETH_PK={ethereum private key} $ peggo orchestrator {gravityAddress} \ - --eth-rpc=$ETH_RPC \ + --eth-rpcs=$ETH_RPCS \ --relay-batches=true \ --valset-relay-mode=minimum \ --cosmos-chain-id=... \ diff --git a/cmd/peggo/bridge.go b/cmd/peggo/bridge.go index 93610415..e0178a20 100644 --- a/cmd/peggo/bridge.go +++ b/cmd/peggo/bridge.go @@ -15,8 +15,6 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi/bind" ethcmn "github.com/ethereum/go-ethereum/common" ethcrypto "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/ethclient" - "github.com/knadh/koanf" "github.com/pkg/errors" "github.com/spf13/cobra" rpchttp "github.com/tendermint/tendermint/rpc/client/http" @@ -64,6 +62,8 @@ func deployGravityCmd() *cobra.Command { return err } + em := NewEthRPCManager(konfig) + // COSMOS RPC cosmosChainID := konfig.String(flagCosmosChainID) @@ -119,13 +119,12 @@ func deployGravityCmd() *cobra.Command { } // ETH RPC - ethRPCEndpoint := konfig.String(flagEthRPC) - ethRPC, err := ethclient.Dial(ethRPCEndpoint) + ethRPC, err := em.GetEthClient() if err != nil { - return fmt.Errorf("failed to dial Ethereum RPC node: %w", err) + return err } - auth, err := buildTransactOpts(konfig, ethRPC) + auth, err := buildTransactOpts(em) if err != nil { return err } @@ -200,13 +199,9 @@ func deployERC20Cmd() *cobra.Command { return err } - ethRPCEndpoint := konfig.String(flagEthRPC) - ethRPC, err := ethclient.Dial(ethRPCEndpoint) - if err != nil { - return fmt.Errorf("failed to dial Ethereum RPC node: %w", err) - } + em := NewEthRPCManager(konfig) - auth, err := buildTransactOpts(konfig, ethRPC) + auth, err := buildTransactOpts(em) if err != nil { return err } @@ -261,7 +256,7 @@ func deployERC20Cmd() *cobra.Command { gravityAddr := args[0] - gravityContract, err := getGravityContract(ethRPC, gravityAddr) + gravityContract, err := getGravityContract(em, gravityAddr) if err != nil { return err } @@ -341,20 +336,16 @@ network starting.`, return err } - ethRPCEndpoint := konfig.String(flagEthRPC) - ethRPC, err := ethclient.Dial(ethRPCEndpoint) - if err != nil { - return fmt.Errorf("failed to dial Ethereum RPC node: %w", err) - } + em := NewEthRPCManager(konfig) - auth, err := buildTransactOpts(konfig, ethRPC) + auth, err := buildTransactOpts(em) if err != nil { return err } gravityAddr := args[0] - gravityContract, err := getGravityContract(ethRPC, gravityAddr) + gravityContract, err := getGravityContract(em, gravityAddr) if err != nil { return err } @@ -403,15 +394,11 @@ func sendToCosmosCmd() *cobra.Command { return err } - ethRPCEndpoint := konfig.String(flagEthRPC) - ethRPC, err := ethclient.Dial(ethRPCEndpoint) - if err != nil { - return fmt.Errorf("failed to dial Ethereum RPC node: %w", err) - } + em := NewEthRPCManager(konfig) gravityAddr := args[0] - gravityContract, err := getGravityContract(ethRPC, gravityAddr) + gravityContract, err := getGravityContract(em, gravityAddr) if err != nil { return err } @@ -420,12 +407,12 @@ func sendToCosmosCmd() *cobra.Command { tokenAddr := ethcmn.HexToAddress(tokenAddrStr) if konfig.Bool(flagAutoApprove) { - if err := approveERC20(konfig, ethRPC, tokenAddrStr, gravityAddr); err != nil { + if err := approveERC20(em, tokenAddrStr, gravityAddr); err != nil { return err } } - auth, err := buildTransactOpts(konfig, ethRPC) + auth, err := buildTransactOpts(em) if err != nil { return err } @@ -468,7 +455,8 @@ Transaction: %s return cmd } -func buildTransactOpts(konfig *koanf.Koanf, ethClient *ethclient.Client) (*bind.TransactOpts, error) { +func buildTransactOpts(em *EthRPCManager) (*bind.TransactOpts, error) { + konfig := em.konfig ethPrivKeyHexStr := konfig.String(flagEthPK) privKey, err := ethcrypto.ToECDSA(ethcmn.FromHex(ethPrivKeyHexStr)) @@ -487,7 +475,7 @@ func buildTransactOpts(konfig *koanf.Koanf, ethClient *ethclient.Client) (*bind. fromAddress := ethcrypto.PubkeyToAddress(*publicKeyECDSA) - nonce, err := ethClient.PendingNonceAt(goCtx, fromAddress) + nonce, err := em.PendingNonceAt(goCtx, fromAddress) if err != nil { return nil, err } @@ -495,7 +483,7 @@ func buildTransactOpts(konfig *koanf.Koanf, ethClient *ethclient.Client) (*bind. goCtx, cancel = context.WithTimeout(context.Background(), 15*time.Second) defer cancel() - ethChainID, err := ethClient.ChainID(goCtx) + ethChainID, err := em.ChainID(goCtx) if err != nil { return nil, fmt.Errorf("failed to get Ethereum chain ID: %w", err) } @@ -516,7 +504,7 @@ func buildTransactOpts(konfig *koanf.Koanf, ethClient *ethclient.Client) (*bind. gasPrice = big.NewInt(gasPriceInt) default: - gasPrice, err = ethClient.SuggestGasPrice(context.Background()) + gasPrice, err = em.SuggestGasPrice(context.Background()) if err != nil { return nil, fmt.Errorf("failed to get Ethereum gas estimate: %w", err) } @@ -549,7 +537,12 @@ func getGravityParams(gRPCConn *grpc.ClientConn) (*gravitytypes.Params, error) { return &gravityParamsResp.Params, nil } -func getGravityContract(ethRPC *ethclient.Client, gravityAddr string) (*wrappers.Gravity, error) { +func getGravityContract(em *EthRPCManager, gravityAddr string) (*wrappers.Gravity, error) { + ethRPC, err := em.GetEthClient() + if err != nil { + return nil, fmt.Errorf("failed to create Gravity contract instance: %w", err) + } + contract, err := wrappers.NewGravity(ethcmn.HexToAddress(gravityAddr), ethRPC) if err != nil { return nil, fmt.Errorf("failed to create Gravity contract instance: %w", err) @@ -558,13 +551,18 @@ func getGravityContract(ethRPC *ethclient.Client, gravityAddr string) (*wrappers return contract, nil } -func approveERC20(konfig *koanf.Koanf, ethRPC *ethclient.Client, erc20AddrStr, gravityAddrStr string) error { +func approveERC20(em *EthRPCManager, erc20AddrStr, gravityAddrStr string) error { + ethRPC, err := em.GetEthClient() + if err != nil { + return fmt.Errorf("failed to create ERC20 contract instance: %w", err) + } + contract, err := wrappers.NewERC20(ethcmn.HexToAddress(erc20AddrStr), ethRPC) if err != nil { return fmt.Errorf("failed to create ERC20 contract instance: %w", err) } - auth, err := buildTransactOpts(konfig, ethRPC) + auth, err := buildTransactOpts(em) if err != nil { return err } diff --git a/cmd/peggo/ethrpc_manager.go b/cmd/peggo/ethrpc_manager.go new file mode 100644 index 00000000..ada48784 --- /dev/null +++ b/cmd/peggo/ethrpc_manager.go @@ -0,0 +1,138 @@ +package peggo + +import ( + "context" + "fmt" + "math/big" + "os" + "strings" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/ethereum/go-ethereum/rpc" + "github.com/knadh/koanf" + "github.com/pkg/errors" +) + +type EthRPCManager struct { + currentEndpoint int // index (in the slice of configured RPC endpoints) of most recent endpoint used + client *rpc.Client + konfig *koanf.Koanf +} + +// creates an instance of EthRPCManager with a given konfig. +func NewEthRPCManager(konfig *koanf.Koanf) *EthRPCManager { + ethManager := &EthRPCManager{ + konfig: konfig, + } + return ethManager +} + +// closes and sets to nil the stored eth RPC client +func (em *EthRPCManager) CloseClient() { + if em.client != nil { + em.client.Close() + em.client = nil + } +} + +// closes the current client and dials configured ethereum rpc endpoints in a roundrobin fashion until one +// is connected. returns an error if no endpoints ar configured or all dials failed +func (em *EthRPCManager) DialNext() error { + if em.konfig == nil { + return errors.New("ethRPCManager konfig is nil") + } + + rpcs := strings.Split(strings.ReplaceAll(em.konfig.String(flagEthRPCs), " ", ""), ",") + + em.CloseClient() + + dialIndex := func(i int) bool { + if cli, err := rpc.Dial(rpcs[i]); err == nil { + em.currentEndpoint = i + em.client = cli + return true + } + fmt.Fprintf(os.Stderr, "Failed to dial to Ethereum RPC: %s\n", rpcs[i]) + return false + } + + // first tries all endpoints in the slice after the current index + for i := range rpcs { + if i > em.currentEndpoint && dialIndex(i) { + fmt.Fprintf(os.Stderr, "Connected to Ethereum RPC: %s\n", rpcs[i]) + return nil + } + } + + // then tries remaining endpoints from the beginning of the slice + for i := range rpcs { + if i <= em.currentEndpoint && dialIndex(i) { + fmt.Fprintf(os.Stderr, "Connected to Ethereum RPC: %s\n", rpcs[i]) + return nil + } + } + + return errors.New(fmt.Sprintf("failed to dial any of the %d Ethereum RPC endpoints configured", len(rpcs))) +} + +// returns the current eth RPC client, dialing one first if nonexistent +func (em *EthRPCManager) GetClient() (*rpc.Client, error) { + if em.client == nil { + if err := em.DialNext(); err != nil { + return nil, err + } + } + return em.client, nil +} + +// returns the current eth RPC client, dialing one first if nonexistent +func (em *EthRPCManager) GetEthClient() (*ethclient.Client, error) { + cli, err := em.GetClient() + if err != nil { + return nil, err + } + return ethclient.NewClient(cli), nil +} + +// wraps ethclient.PendingNonceAt, also closing client if PendingNonceAt returns an error +func (em *EthRPCManager) PendingNonceAt(ctx context.Context, addr common.Address) (uint64, error) { + cli, err := em.GetEthClient() + if err != nil { + return 0, err + } + nonce, err := cli.PendingNonceAt(ctx, addr) + if err != nil { + em.CloseClient() + return 0, err + } + return nonce, nil +} + +// wraps ethclient.ChainID, also closing client if ChainID returns an error +func (em *EthRPCManager) ChainID(ctx context.Context) (*big.Int, error) { + cli, err := em.GetEthClient() + if err != nil { + return nil, err + } + id, err := cli.ChainID(ctx) + if err != nil { + em.CloseClient() + return nil, err + } + return id, nil +} + +// wraps ethclient.SuggestGasPrice, also closing client if SuggestGasPrice returns an error +func (em *EthRPCManager) SuggestGasPrice(ctx context.Context) (*big.Int, error) { + cli, err := em.GetEthClient() + if err != nil { + return nil, err + } + price, err := cli.SuggestGasPrice(ctx) + if err != nil { + em.CloseClient() + return nil, err + } + return price, nil +} diff --git a/cmd/peggo/flags.go b/cmd/peggo/flags.go index 60dbbb93..e7125423 100644 --- a/cmd/peggo/flags.go +++ b/cmd/peggo/flags.go @@ -2,6 +2,7 @@ package peggo import ( + "fmt" "net/url" "strings" @@ -37,6 +38,7 @@ const ( flagEthPK = "eth-pk" flagEthUseLedger = "eth-use-ledger" flagEthRPC = "eth-rpc" + flagEthRPCs = "eth-rpcs" flagEthGasAdjustment = "eth-gas-price-adjustment" flagEthGasLimitAdjustment = "eth-gas-limit-adjustment" flagEthAlchemyWS = "eth-alchemy-ws" @@ -93,9 +95,12 @@ func ethereumOptsFlagSet() *pflag.FlagSet { fs := pflag.NewFlagSet("", pflag.ContinueOnError) fs.String(flagEthRPC, "http://localhost:8545", "Specify the RPC address of an Ethereum node") + fs.String(flagEthRPCs, "http://localhost:8545", "Specify comma-separated RPC addresses of one or more Ethereum nodes") fs.Float64(flagEthGasAdjustment, float64(1.3), "Specify a gas price adjustment for Ethereum transactions") fs.Float64(flagEthGasLimitAdjustment, float64(1.2), "Specify a gas limit adjustment for Ethereum transactions") + _ = fs.MarkDeprecated(flagEthRPC, fmt.Sprintf("Use the '%s' flag instead to provide one or more Ethereum RPC instances", flagEthRPCs)) + return fs } @@ -103,9 +108,12 @@ func bridgeFlagSet() *pflag.FlagSet { fs := pflag.NewFlagSet("", pflag.ContinueOnError) fs.String(flagEthRPC, "http://localhost:8545", "Specify the RPC address of an Ethereum node") + fs.String(flagEthRPCs, "http://localhost:8545", "Specify comma-separated RPC addresses of one or more Ethereum nodes") fs.Int64(flagEthGasPrice, 0, "The Ethereum gas price to include in the transaction; If zero, gas price will be estimated") fs.Int64(flagEthGasLimit, 6000000, "The Ethereum gas limit to include in the transaction") + _ = fs.MarkDeprecated(flagEthRPC, fmt.Sprintf("Use the '%s' flag instead to provide one or more Ethereum RPC instances", flagEthRPCs)) + return fs } diff --git a/cmd/peggo/orchestrator.go b/cmd/peggo/orchestrator.go index d86454b4..8509300a 100644 --- a/cmd/peggo/orchestrator.go +++ b/cmd/peggo/orchestrator.go @@ -11,7 +11,6 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" ethcmn "github.com/ethereum/go-ethereum/common" - ethrpc "github.com/ethereum/go-ethereum/rpc" "github.com/rs/zerolog" "github.com/spf13/cobra" rpchttp "github.com/tendermint/tendermint/rpc/client/http" @@ -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) + ethRPC, err := em.GetClient() if err != nil { - return fmt.Errorf("failed to dial Ethereum RPC node: %w", err) + return err } - fmt.Fprintf(os.Stderr, "Connected to Ethereum RPC: %s\n", ethRPCEndpoint) ethProvider := provider.NewEVMProvider(ethRPC) ethGasPriceAdjustment := konfig.Float64(flagEthGasAdjustment) diff --git a/cmd/peggo/peggo.go b/cmd/peggo/peggo.go index 58612cc8..d057fa82 100644 --- a/cmd/peggo/peggo.go +++ b/cmd/peggo/peggo.go @@ -21,7 +21,7 @@ func NewRootCmd() *cobra.Command { Long: `Peggo is a companion executable for orchestrating a Gravity validator. Inputs in the CLI commands can be provided via flags or environment variables. If -using the later, prefix the environment variable with PEGGO_ and the named of the +using the latter, prefix the environment variable with PEGGO_ and the named of the flag (e.g. PEGGO_COSMOS_PK).`, } @@ -81,7 +81,7 @@ func parseServerConfig(cmd *cobra.Command) (*koanf.Koanf, error) { konfig := koanf.New(".") // load from file first (if provided) - // TODO: Support config files if/when needed. + // TODO: Support config files if/when needed. Koanf also supports watch the config file for changes. // if configPath := ctx.String(config.ConfigPath); len(configPath) != 0 { // if err := konfig.Load(file.Provider(configPath), toml.Parser()); err != nil { // return nil, err diff --git a/test/e2e/e2e_setup_test.go b/test/e2e/e2e_setup_test.go index e0c1ed1e..9865c1f5 100644 --- a/test/e2e/e2e_setup_test.go +++ b/test/e2e/e2e_setup_test.go @@ -545,7 +545,7 @@ func (s *IntegrationTestSuite) runContractDeployment() { "peggo", "bridge", "deploy-gravity", - "--eth-rpc", + "--eth-rpcs", fmt.Sprintf("http://%s:8545", s.ethResource.Container.Name[1:]), "--cosmos-grpc", fmt.Sprintf("tcp://%s:9090", s.valResources[0].Container.Name[1:]), @@ -646,7 +646,7 @@ func (s *IntegrationTestSuite) runOrchestrators() { "peggo", "orchestrator", s.gravityContractAddr, - "--eth-rpc", + "--eth-rpcs", fmt.Sprintf("http://%s:8545", s.ethResource.Container.Name[1:]), "--cosmos-chain-id", s.chain.id, diff --git a/test/e2e/e2e_util_test.go b/test/e2e/e2e_util_test.go index ac1c625a..682e9610 100644 --- a/test/e2e/e2e_util_test.go +++ b/test/e2e/e2e_util_test.go @@ -40,7 +40,7 @@ func (s *IntegrationTestSuite) deployERC20Token(baseDenom string) string { "deploy-erc20", s.gravityContractAddr, baseDenom, - "--eth-rpc", + "--eth-rpcs", fmt.Sprintf("http://%s:8545", s.ethResource.Container.Name[1:]), "--cosmos-chain-id", s.chain.id, @@ -266,7 +266,7 @@ func (s *IntegrationTestSuite) sendFromEthToUmee(valIdx int, tokenAddr, toUmeeAd tokenAddr, toUmeeAddr, amount, - "--eth-rpc", + "--eth-rpcs", fmt.Sprintf("http://%s:8545", s.ethResource.Container.Name[1:]), "--cosmos-chain-id", s.chain.id,