diff --git a/CHANGELOG.md b/CHANGELOG.md index e9c7986f..d57f51be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,19 @@ Ref: https://keepachangelog.com/en/1.0.0/ ## [Unreleased] +### Features + +- [#189] Add the flag `--valset-relay-mode` which allows a finer control over + how valsets will be relayed. + +### Deprecated + +- [#189] Deprecate the `--relay-valsets` flag. + +### Bug Fixes + +- [#189] Order validator set before deploying (`peggo bridge deploy-gravity`) + ## [v0.2.4](https://github.com/umee-network/peggo/releases/tag/v0.2.4) - 2022-02-16 ### Improvements diff --git a/README.md b/README.md index 03dbe382..77dfe2d0 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ export PEGGO_ETH_PK={ethereum private key} $ peggo orchestrator {gravityAddress} \ --eth-rpc=$ETH_RPC \ --relay-batches=true \ - --relay-valsets=true \ + --valset-relay-mode=minimum \ --cosmos-chain-id=... \ --cosmos-grpc="tcp://..." \ --tendermint-rpc="http://..." \ diff --git a/cmd/peggo/bridge.go b/cmd/peggo/bridge.go index 5f5654d7..0315a9bb 100644 --- a/cmd/peggo/bridge.go +++ b/cmd/peggo/bridge.go @@ -24,6 +24,7 @@ import ( "google.golang.org/grpc" "github.com/umee-network/peggo/cmd/peggo/client" + "github.com/umee-network/peggo/orchestrator/relayer" wrappers "github.com/umee-network/peggo/solwrappers/Gravity.sol" ) @@ -140,6 +141,9 @@ func deployGravityCmd() *cobra.Command { totalPower uint64 ) + // Always sort the validator set + relayer.BridgeValidators(currValset.Valset.Members).Sort() + for i, member := range currValset.Valset.Members { validators[i] = ethcmn.HexToAddress(member.EthereumAddress) powers[i] = new(big.Int).SetUint64(member.Power) diff --git a/cmd/peggo/flags.go b/cmd/peggo/flags.go index 863794c8..3f92b1c1 100644 --- a/cmd/peggo/flags.go +++ b/cmd/peggo/flags.go @@ -36,6 +36,7 @@ const ( flagEthGasLimitAdjustment = "eth-gas-limit-adjustment" flagEthAlchemyWS = "eth-alchemy-ws" flagRelayValsets = "relay-valsets" + flagValsetRelayMode = "valset-relay-mode" flagRelayBatches = "relay-batches" flagCoinGeckoAPI = "coingecko-api" flagEthGasPrice = "eth-gas-price" diff --git a/cmd/peggo/orchestrator.go b/cmd/peggo/orchestrator.go index 63962700..b8f1e8c7 100644 --- a/cmd/peggo/orchestrator.go +++ b/cmd/peggo/orchestrator.go @@ -177,11 +177,23 @@ func getOrchestratorCmd() *cobra.Command { // Here we cast the float64 to a Duration (int64); as we are dealing with ms, we'll lose as much as 1ms. relayerLoopDuration := time.Duration(ethBlockTimeF64*relayerLoopMultiplier) * time.Millisecond + relayValsets := konfig.Bool(flagRelayValsets) + valsetRelayMode, err := validateRelayValsetsMode(konfig.String(flagValsetRelayMode)) + if err != nil { + return err + } + + // If relayValsets is true then the user didn't specify a value for 'valset-relay-mode', + // so we'll default to "minimum". + if relayValsets && valsetRelayMode == relayer.ValsetRelayModeNone { + valsetRelayMode = relayer.ValsetRelayModeMinimum + } + relayer := relayer.NewGravityRelayer( logger, gravityQuerier, gravityContract, - konfig.Bool(flagRelayValsets), + valsetRelayMode, konfig.Bool(flagRelayBatches), relayerLoopDuration, konfig.Duration(flagEthPendingTXWait), @@ -244,6 +256,7 @@ func getOrchestratorCmd() *cobra.Command { } cmd.Flags().Bool(flagRelayValsets, false, "Relay validator set updates to Ethereum") + cmd.Flags().String(flagValsetRelayMode, relayer.ValsetRelayModeNone.String(), "Set an (optional) relaying mode for valset updates to Ethereum. Possible values: none, minimum, all") cmd.Flags().Bool(flagRelayBatches, false, "Relay transaction batches to Ethereum") cmd.Flags().Int64(flagEthBlocksPerLoop, 2000, "Number of Ethereum blocks to process per orchestrator loop") cmd.Flags().String(flagCoinGeckoAPI, "https://api.coingecko.com/api/v3", "Specify the coingecko API endpoint") @@ -259,6 +272,7 @@ func getOrchestratorCmd() *cobra.Command { cmd.Flags().AddFlagSet(cosmosKeyringFlagSet()) cmd.Flags().AddFlagSet(ethereumKeyOptsFlagSet()) cmd.Flags().AddFlagSet(ethereumOptsFlagSet()) + _ = cmd.Flags().MarkDeprecated(flagRelayValsets, "use --valset-relay-mode instead") return cmd } @@ -294,3 +308,16 @@ func startOrchestrator(ctx context.Context, logger zerolog.Logger, orch orchestr } } } + +func validateRelayValsetsMode(mode string) (relayer.ValsetRelayMode, error) { + switch mode { + case relayer.ValsetRelayModeNone.String(): + return relayer.ValsetRelayModeNone, nil + case relayer.ValsetRelayModeMinimum.String(): + return relayer.ValsetRelayModeMinimum, nil + case relayer.ValsetRelayModeAll.String(): + return relayer.ValsetRelayModeAll, nil + default: + return relayer.ValsetRelayModeNone, fmt.Errorf("invalid relay valsets mode: %s", mode) + } +} diff --git a/orchestrator/relayer/find_latest_valset.go b/orchestrator/relayer/find_latest_valset.go index 61edcc4c..63a64896 100644 --- a/orchestrator/relayer/find_latest_valset.go +++ b/orchestrator/relayer/find_latest_valset.go @@ -142,12 +142,9 @@ func (s *gravityRelayer) checkIfValsetsDiffer(cosmosValset, ethereumValset *type return } + // Do not check if nonces are not equal. + // We queried cosmosValset using ethereumValset.Nonce so it is not necessary. if cosmosValset.Nonce != ethereumValset.Nonce { - - s.logger.Error(). - Uint64("eth_valset_nonce", ethereumValset.Nonce). - Uint64("cosmos_valset_nonce", cosmosValset.Nonce). - Msg("cosmos does have a wrong valset nonce, differs from Ethereum chain. Possible bridge hijacking!") return } diff --git a/orchestrator/relayer/main_loop.go b/orchestrator/relayer/main_loop.go index 1f595489..1a4174aa 100644 --- a/orchestrator/relayer/main_loop.go +++ b/orchestrator/relayer/main_loop.go @@ -14,7 +14,7 @@ import ( func (s *gravityRelayer) Start(ctx context.Context) error { logger := s.logger.With().Str("loop", "RelayerMainLoop").Logger() - if s.valsetRelayEnabled { + if s.valsetRelayMode != ValsetRelayModeNone { logger.Info().Msg("valset relay enabled; starting to relay valsets to Ethereum") } @@ -44,7 +44,7 @@ func (s *gravityRelayer) Start(ctx context.Context) error { } var pg loops.ParanoidGroup - if s.valsetRelayEnabled { + if s.valsetRelayMode != ValsetRelayModeNone { pg.Go(func() error { return retry.Do(func() error { return s.RelayValsets(ctx, *currentValset) diff --git a/orchestrator/relayer/relayer.go b/orchestrator/relayer/relayer.go index 3455fabb..0be2525d 100644 --- a/orchestrator/relayer/relayer.go +++ b/orchestrator/relayer/relayer.go @@ -14,6 +14,21 @@ import ( gravitytypes "github.com/umee-network/Gravity-Bridge/module/x/gravity/types" ) +// ValsetRelayMode defines an enumerated validator set relay mode. +type ValsetRelayMode int64 + +// Allowed validator set relay modes +const ( + ValsetRelayModeNone ValsetRelayMode = iota + ValsetRelayModeMinimum + ValsetRelayModeAll +) + +// String gets the string representation of the validator set relay mode. +func (d ValsetRelayMode) String() string { + return [...]string{"none", "minimum", "all"}[d] +} + type GravityRelayer interface { Start(ctx context.Context) error @@ -33,16 +48,16 @@ type GravityRelayer interface { } type gravityRelayer struct { - logger zerolog.Logger - cosmosQueryClient gravitytypes.QueryClient - gravityContract gravity.Contract - ethProvider provider.EVMProvider - valsetRelayEnabled bool - batchRelayEnabled bool - loopDuration time.Duration - priceFeeder *coingecko.PriceFeed - pendingTxWait time.Duration - profitMultiplier float64 + logger zerolog.Logger + cosmosQueryClient gravitytypes.QueryClient + gravityContract gravity.Contract + ethProvider provider.EVMProvider + valsetRelayMode ValsetRelayMode + batchRelayEnabled bool + loopDuration time.Duration + priceFeeder *coingecko.PriceFeed + pendingTxWait time.Duration + profitMultiplier float64 // Store locally the last tx this validator made to avoid sending duplicates // or invalid txs. @@ -54,7 +69,7 @@ func NewGravityRelayer( logger zerolog.Logger, gravityQueryClient gravitytypes.QueryClient, gravityContract gravity.Contract, - valsetRelayEnabled bool, + valsetRelayMode ValsetRelayMode, batchRelayEnabled bool, loopDuration time.Duration, pendingTxWait time.Duration, @@ -62,15 +77,15 @@ func NewGravityRelayer( options ...func(GravityRelayer), ) GravityRelayer { relayer := &gravityRelayer{ - logger: logger.With().Str("module", "gravity_relayer").Logger(), - cosmosQueryClient: gravityQueryClient, - gravityContract: gravityContract, - ethProvider: gravityContract.Provider(), - valsetRelayEnabled: valsetRelayEnabled, - batchRelayEnabled: batchRelayEnabled, - loopDuration: loopDuration, - pendingTxWait: pendingTxWait, - profitMultiplier: profitMultiplier, + logger: logger.With().Str("module", "gravity_relayer").Logger(), + cosmosQueryClient: gravityQueryClient, + gravityContract: gravityContract, + ethProvider: gravityContract.Provider(), + valsetRelayMode: valsetRelayMode, + batchRelayEnabled: batchRelayEnabled, + loopDuration: loopDuration, + pendingTxWait: pendingTxWait, + profitMultiplier: profitMultiplier, } for _, option := range options { diff --git a/orchestrator/relayer/relayer_test.go b/orchestrator/relayer/relayer_test.go index a0929541..71dd8496 100644 --- a/orchestrator/relayer/relayer_test.go +++ b/orchestrator/relayer/relayer_test.go @@ -25,7 +25,7 @@ func TestNewGravityRelayer(t *testing.T) { relayer := NewGravityRelayer(logger, mockQClient, mockGravityContract, - true, + ValsetRelayModeMinimum, true, time.Minute, time.Minute, diff --git a/orchestrator/relayer/valset_relaying.go b/orchestrator/relayer/valset_relaying.go index ff71812d..e47e814a 100644 --- a/orchestrator/relayer/valset_relaying.go +++ b/orchestrator/relayer/valset_relaying.go @@ -10,8 +10,13 @@ import ( // RelayValsets checks the last validator set on Ethereum, if it's lower than our latest validator // set then we should package and submit the update as an Ethereum transaction func (s *gravityRelayer) RelayValsets(ctx context.Context, currentValset types.Valset) error { - // we should determine if we need to relay one - // to Ethereum for that we will find the latest confirmed valset and compare it to the ethereum chain + // This works by checking if the latest valset update can be sent by the current valset stored on Ethereum. + // If so, there's no need to relay anything, given that the current valset on Eth shares enough signers with the + // latest valset on Cosmos. + // If the latest valset on Cosmos (latestValsets.Valsets[0]) can't be sent by the current valset on Ethereum, + // then we need to relay the latest valid valset on Cosmos; that means the one that shares enough signers with + // the valset stored on Ethereum. + latestValsets, err := s.cosmosQueryClient.LastValsetRequests(ctx, &types.QueryLastValsetRequestsRequest{}) if err != nil { err = errors.Wrap(err, "failed to fetch latest valsets from cosmos") @@ -20,7 +25,8 @@ func (s *gravityRelayer) RelayValsets(ctx context.Context, currentValset types.V return errors.New("no valsets found") } - latestCosmosConfirmed, latestCosmosSigs, err := s.findLatestValidValset( + // latestValidValset means the latest valset that can be sent using the current valset on Ethereum. + latestValidValset, latestValidValsetSigs, err := s.findLatestValidValset( ctx, currentValset, latestValsets.Valsets[0].Nonce, @@ -31,19 +37,19 @@ func (s *gravityRelayer) RelayValsets(ctx context.Context, currentValset types.V return err } - if latestCosmosConfirmed == nil && err == nil { + if latestValidValset == nil && err == nil { s.logger.Info().Msg("no valset updates to relay") return nil } - if s.lastSentValsetNonce >= latestCosmosConfirmed.Nonce { + if s.lastSentValsetNonce >= latestValidValset.Nonce { s.logger.Debug().Msg("already relayed this valset; skipping") return nil } s.logger.Debug(). Uint64("current_eth_valset_nonce", currentValset.Nonce). - Uint64("latest_cosmos_confirmed_nonce", latestCosmosConfirmed.Nonce). + Uint64("latest_cosmos_confirmed_nonce", latestValidValset.Nonce). Msg("found latest valsets") latestEthereumValsetNonce, err := s.gravityContract.GetValsetNonce(ctx, s.gravityContract.FromAddress()) @@ -53,21 +59,27 @@ func (s *gravityRelayer) RelayValsets(ctx context.Context, currentValset types.V } // Check if latestCosmosConfirmed already submitted by other validators in mean time - if latestCosmosConfirmed.Nonce <= latestEthereumValsetNonce.Uint64() { + if latestValidValset.Nonce <= latestEthereumValsetNonce.Uint64() { // This valset update is already confirmed. return nil } + // We might not need to relay this valset update unless the user explicitly specified it. + if s.valsetRelayMode == ValsetRelayModeMinimum && latestValidValset.Nonce == latestValsets.Valsets[0].Nonce { + s.logger.Debug().Msg("not relaying because nonces match") + return nil + } + s.logger.Info(). - Uint64("latest_cosmos_confirmed_nonce", latestCosmosConfirmed.Nonce). + Uint64("latest_cosmos_confirmed_nonce", latestValidValset.Nonce). Uint64("latest_ethereum_valset_nonce", latestEthereumValsetNonce.Uint64()). Msg("detected latest cosmos valset nonce, but latest valset on Ethereum is different. Sending update to Ethereum") txData, err := s.gravityContract.EncodeValsetUpdate( ctx, currentValset, - *latestCosmosConfirmed, - latestCosmosSigs, + *latestValidValset, + latestValidValsetSigs, ) if err != nil { s.logger.Err(err).Msg("failed to encode valset update") @@ -109,7 +121,7 @@ func (s *gravityRelayer) RelayValsets(ctx context.Context, currentValset types.V s.logger.Info().Str("tx_hash", txHash.Hex()).Msg("sent Tx (Gravity updateValset)") // update our local tracker of the latest valset - s.lastSentValsetNonce = latestCosmosConfirmed.Nonce + s.lastSentValsetNonce = latestValidValset.Nonce return nil } diff --git a/test/e2e/e2e_setup_test.go b/test/e2e/e2e_setup_test.go index 63179588..e0c1ed1e 100644 --- a/test/e2e/e2e_setup_test.go +++ b/test/e2e/e2e_setup_test.go @@ -659,7 +659,7 @@ func (s *IntegrationTestSuite) runOrchestrators() { "--cosmos-from", s.chain.orchestrators[i].keyInfo.GetName(), "--relay-batches=true", - "--relay-valsets=true", + "--valset-relay-mode=minimum", "--profit-multiplier=0.0", "--relayer-loop-multiplier=1.0", "--requester-loop-multiplier=1.0",