diff --git a/e2e/relayer/relayer.go b/e2e/relayer/relayer.go index befcd6d127a..afa0385fb7c 100644 --- a/e2e/relayer/relayer.go +++ b/e2e/relayer/relayer.go @@ -58,3 +58,22 @@ func newCosmosRelayer(t *testing.T, tag string, logger *zap.Logger, dockerClient func newHermesRelayer() ibc.Relayer { panic("hermes relayer not yet implemented for interchaintest") } + +// RelayerMap is a mapping from test names to a relayer set for that test. +type RelayerMap map[string]map[ibc.Wallet]bool + +// AddRelayer adds the given relayer to the relayer set for the given test name. +func (r RelayerMap) AddRelayer(testName string, relayer ibc.Wallet) { + if _, ok := r[testName]; !ok { + r[testName] = make(map[ibc.Wallet]bool) + } + r[testName][relayer] = true +} + +// containsRelayer returns true if the given relayer is in the relayer set for the given test name. +func (r RelayerMap) ContainsRelayer(testName string, wallet ibc.Wallet) bool { + if relayerSet, ok := r[testName]; ok { + return relayerSet[wallet] + } + return false +} diff --git a/e2e/testsuite/testsuite.go b/e2e/testsuite/testsuite.go index 7fe7acf1322..812610086fa 100644 --- a/e2e/testsuite/testsuite.go +++ b/e2e/testsuite/testsuite.go @@ -60,6 +60,7 @@ type E2ETestSuite struct { grpcClients map[string]GRPCClients paths map[string]path + relayers relayer.RelayerMap logger *zap.Logger DockerClient *dockerclient.Client network string @@ -117,6 +118,12 @@ func (s *E2ETestSuite) GetRelayerUsers(ctx context.Context, chainOpts ...testcon chainARelayerUser := cosmos.NewWallet(ChainARelayerName, chainAAccountBytes, "", chainA.Config()) chainBRelayerUser := cosmos.NewWallet(ChainBRelayerName, chainBAccountBytes, "", chainB.Config()) + if s.relayers == nil { + s.relayers = make(relayer.RelayerMap) + } + s.relayers.AddRelayer(s.T().Name(), chainARelayerUser) + s.relayers.AddRelayer(s.T().Name(), chainBRelayerUser) + return chainARelayerUser, chainBRelayerUser } @@ -281,7 +288,18 @@ func (s *E2ETestSuite) BroadcastMessages(ctx context.Context, chain *cosmos.Cosm return factory.WithGas(DefaultGasValue) }) - resp, err := cosmos.BroadcastTx(ctx, broadcaster, user, msgs...) + // Retry the operation a few times if the user signing the transaction is a relayer. (See issue #3264) + var resp sdk.TxResponse + var err error + broadcastFunc := func() (sdk.TxResponse, error) { + return cosmos.BroadcastTx(ctx, broadcaster, user, msgs...) + } + if s.relayers.ContainsRelayer(s.T().Name(), user) { + // Retry five times, the value of 5 chosen is arbitrary. + resp, err = s.retryNtimes(broadcastFunc, 5) + } else { + resp, err = broadcastFunc() + } if err != nil { return sdk.TxResponse{}, err } @@ -611,6 +629,36 @@ func (s *E2ETestSuite) QueryGranterGrants(ctx context.Context, chain *cosmos.Cos return grants.Grants, nil } +// retryNtimes retries the provided function up to the provided number of attempts. +func (s *E2ETestSuite) retryNtimes(f func() (sdk.TxResponse, error), attempts int) (sdk.TxResponse, error) { + // Ignore account sequence mismatch errors. + retryMessages := []string{"account sequence mismatch"} + var resp sdk.TxResponse + var err error + // If the response's raw log doesn't contain any of the allowed prefixes we return, else, we retry. + for i := 0; i < attempts; i++ { + resp, err = f() + if err != nil { + return sdk.TxResponse{}, err + } + if !containsMessage(resp.RawLog, retryMessages) { + return resp, err + } + s.T().Logf("retrying tx due to non deterministic failure: %+v", resp) + } + return resp, err +} + +// containsMessages returns true if the string s contains any of the messages in the slice. +func containsMessage(s string, messages []string) bool { + for _, message := range messages { + if strings.Contains(s, message) { + return true + } + } + return false +} + // GetIBCToken returns the denomination of the full token denom sent to the receiving channel func GetIBCToken(fullTokenDenom string, portID, channelID string) transfertypes.DenomTrace { return transfertypes.ParseDenomTrace(fmt.Sprintf("%s/%s/%s", portID, channelID, fullTokenDenom))