diff --git a/changelog.md b/changelog.md index 791af078fe..beee985ee1 100644 --- a/changelog.md +++ b/changelog.md @@ -11,6 +11,7 @@ * [3205](https://github.com/zeta-chain/node/issues/3205) - move Bitcoin revert address test to advanced group to avoid upgrade test failure * [3254](https://github.com/zeta-chain/node/pull/3254) - rename v2 E2E tests as evm tests and rename old evm tests as legacy * [3095](https://github.com/zeta-chain/node/pull/3095) - initialize simulation tests for custom zetachain modules +* [3207](https://github.com/zeta-chain/node/pull/3207) - add simulation test operations for all messages in crosschain and observer module ## Refactor diff --git a/pkg/chains/chain_filters_test.go b/pkg/chains/chain_filters_test.go index 6254600452..fe936e5110 100644 --- a/pkg/chains/chain_filters_test.go +++ b/pkg/chains/chain_filters_test.go @@ -48,6 +48,21 @@ func TestFilterChains(t *testing.T) { return chains.ChainListByConsensus(chains.Consensus_solana_consensus, []chains.Chain{}) }, }, + { + name: "Filter vm evm chains", + filters: []chains.ChainFilter{ + chains.FilterByVM(chains.Vm_evm), + }, + expected: func() []chains.Chain { + var chainList []chains.Chain + for _, chain := range chains.ExternalChainList([]chains.Chain{}) { + if chain.Vm == chains.Vm_evm { + chainList = append(chainList, chain) + } + } + return chainList + }, + }, { name: "Apply multiple filters external chains and gateway observer", filters: []chains.ChainFilter{ diff --git a/pkg/coin/coint_test.go b/pkg/coin/coint_test.go new file mode 100644 index 0000000000..98599ebb74 --- /dev/null +++ b/pkg/coin/coint_test.go @@ -0,0 +1,28 @@ +package coin_test + +import ( + "testing" + + "github.com/zeta-chain/node/pkg/coin" +) + +func TestCoinType_SupportsRefund(t *testing.T) { + tests := []struct { + name string + c coin.CoinType + want bool + }{ + {"ERC20", coin.CoinType_ERC20, true}, + {"Gas", coin.CoinType_Gas, true}, + {"Zeta", coin.CoinType_Zeta, true}, + {"Cmd", coin.CoinType_Cmd, false}, + {"Unknown", coin.CoinType(100), false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.c.SupportsRefund(); got != tt.want { + t.Errorf("CoinType.SupportsRefund() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/simulation/simulation_test.go b/simulation/simulation_test.go index 3f72d08690..c12b241654 100644 --- a/simulation/simulation_test.go +++ b/simulation/simulation_test.go @@ -183,7 +183,10 @@ func TestAppStateDeterminism(t *testing.T) { } // TestFullAppSimulation runs a full simApp simulation with the provided configuration. -// This is the basic test which just runs the simulation and checks for any errors +// This test does the following +// 1. It runs a full simulation with the provided configuration +// 2. It exports the state and validators +// 3. Verifies that the run and export were successful func TestFullAppSimulation(t *testing.T) { config := zetasimulation.NewConfigFromFlags() @@ -586,62 +589,3 @@ func TestAppSimulationAfterImport(t *testing.T) { ) require.NoError(t, err) } - -// DiffKVStores compares two KVstores and returns all the key/value pairs -// that differ from one another. It also skips value comparison for a set of provided prefixes. -func CountKVStores(a sdk.KVStore, b sdk.KVStore, _ [][]byte) (int, int) { - iterA := a.Iterator(nil, nil) - - defer iterA.Close() - - iterB := b.Iterator(nil, nil) - - defer iterB.Close() - - countA := 0 - countB := 0 - - for iterA.Valid() { - countA++ - iterA.Next() - } - - for iterB.Valid() { - countB++ - iterB.Next() - } - return countA, countB -} - -func FindDiffKeys(a sdk.KVStore, b sdk.KVStore) { - - keysA := map[string]bool{} - keysB := map[string]bool{} - iterA := a.Iterator(nil, nil) - - defer iterA.Close() - - iterB := b.Iterator(nil, nil) - - defer iterB.Close() - - for iterA.Valid() { - k := string(iterA.Key()) - iterA.Next() - keysA[k] = true - - } - - for iterB.Valid() { - kb := string(iterB.Key()) - iterB.Next() - keysB[kb] = true - } - - for k := range keysA { - if _, ok := keysB[k]; !ok { - fmt.Println("Key in A not in B", k) - } - } - -} diff --git a/simulation/state.go b/simulation/state.go index 91a4a980e2..87e0860a31 100644 --- a/simulation/state.go +++ b/simulation/state.go @@ -3,17 +3,12 @@ package simulation import ( "encoding/json" "fmt" - "io" "math/rand" - "os" "testing" "time" "cosmossdk.io/math" - cmtjson "github.com/cometbft/cometbft/libs/json" - tmtypes "github.com/cometbft/cometbft/types" "github.com/cosmos/cosmos-sdk/codec" - "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" simtypes "github.com/cosmos/cosmos-sdk/types/simulation" @@ -23,7 +18,6 @@ import ( "github.com/stretchr/testify/require" evmtypes "github.com/zeta-chain/ethermint/x/evm/types" - zetaapp "github.com/zeta-chain/node/app" zetachains "github.com/zeta-chain/node/pkg/chains" "github.com/zeta-chain/node/pkg/coin" "github.com/zeta-chain/node/pkg/crypto" @@ -40,6 +34,9 @@ const ( InitiallyBondedValidators = "initially_bonded_validators" ) +// updateBankState updates the bank genesis state. +// It adds the following +// - The not bonded balance for the not bonded pool func updateBankState( t *testing.T, rawState map[string]json.RawMessage, @@ -71,6 +68,8 @@ func updateBankState( return bankState } +// updateEVMState updates the evm genesis state. +// It replaces the EvmDenom with BondDenom func updateEVMState( t *testing.T, rawState map[string]json.RawMessage, @@ -89,6 +88,10 @@ func updateEVMState( return evmState } +// updateStakingState updates the staking genesis state. +// It adds the following +// - The not bonded balance for the not bonded pool +// It additionally returns the non-bonded coins as well func updateStakingState( t *testing.T, rawState map[string]json.RawMessage, @@ -116,6 +119,15 @@ func updateStakingState( return stakingState, notBondedCoins } +// updateObserverState updates the observer genesis state. +// It adds the following +// - A random observer set which is a subset of the current validator set +// - A randomised node account for each observer +// - A random TSS +// - A TSS history for the TSS created +// - Chain nonces for each chain +// - Pending nonces for each chain +// - Crosschain flags, inbound and outbound enabled func updateObserverState( t *testing.T, rawState map[string]json.RawMessage, @@ -158,7 +170,6 @@ func updateObserverState( GranteePubkey: &crypto.PubKeySet{}, NodeStatus: observertypes.NodeStatus_Active, } - //fmt.Println(nodeAccounts[i].GranteePubkey) } // Create a random tss tss, err := sample.TSSFromRand(r) @@ -200,6 +211,9 @@ func updateObserverState( return observerState } +// updateAuthorityState updates the authority genesis state. +// It adds the following +// - A policy for each policy type, the address is a random account address selected from the simulation accounts list func updateAuthorityState( t *testing.T, rawState map[string]json.RawMessage, @@ -235,6 +249,9 @@ func updateAuthorityState( return authorityState } +// updateCrossChainState updates the crosschain genesis state. +// It adds the following +// - A gas price list for each chain func updateCrossChainState( t *testing.T, rawState map[string]json.RawMessage, @@ -247,11 +264,11 @@ func updateCrossChainState( crossChainState := new(crosschaintypes.GenesisState) cdc.MustUnmarshalJSON(crossChainStateBz, crossChainState) - var gasPriceList []*crosschaintypes.GasPrice - + // Add a gasprice for each chain chains := zetachains.DefaultChainsList() - for _, chain := range chains { - gasPriceList = append(gasPriceList, sample.GasPriceFromRand(r, chain.ChainId)) + gasPriceList := make([]*crosschaintypes.GasPrice, len(chains)) + for i, chain := range chains { + gasPriceList[i] = sample.GasPriceFromRand(r, chain.ChainId) } crossChainState.GasPriceList = gasPriceList @@ -259,6 +276,12 @@ func updateCrossChainState( return crossChainState } +// updateFungibleState updates the fungible genesis state. +// It adds the following +// - A random system contract address +// - A random connector zevm address +// - A random gateway address +// - A foreign coin for each chain under the default chain list. func updateFungibleState( t *testing.T, rawState map[string]json.RawMessage, @@ -296,6 +319,8 @@ func updateFungibleState( return fungibleState } +// updateRawState updates the raw genesis state for the application. +// This is used to inject values needed to run the simulation tests. func updateRawState( t *testing.T, rawState map[string]json.RawMessage, @@ -340,7 +365,7 @@ func AppStateFn( chainID = config.ChainID - // if exported state is provided then use it + // if exported state is provided, then use it if exportedState != nil { return exportedState, accs, chainID, genesisTimestamp } @@ -431,58 +456,3 @@ func AppStateRandomizedFn( } return appState, accs } - -// AppStateFromGenesisFileFn util function to generate the genesis AppState -// from a genesis.json file. -func AppStateFromGenesisFileFn( - r io.Reader, - cdc codec.JSONCodec, - genesisFile string, -) (tmtypes.GenesisDoc, []simtypes.Account, error) { - bytes, err := os.ReadFile(genesisFile) // #nosec G304 -- genesisFile value is controlled - if err != nil { - panic(err) - } - - var genesis tmtypes.GenesisDoc - // NOTE: Comet uses a custom JSON decoder for GenesisDoc - err = cmtjson.Unmarshal(bytes, &genesis) - if err != nil { - panic(err) - } - - var appState zetaapp.GenesisState - err = json.Unmarshal(genesis.AppState, &appState) - if err != nil { - panic(err) - } - - var authGenesis authtypes.GenesisState - if appState[authtypes.ModuleName] != nil { - cdc.MustUnmarshalJSON(appState[authtypes.ModuleName], &authGenesis) - } - - newAccs := make([]simtypes.Account, len(authGenesis.Accounts)) - for i, acc := range authGenesis.Accounts { - // Pick a random private key, since we don't know the actual key - // This should be fine as it's only used for mock Tendermint validators - // and these keys are never actually used to sign by mock Tendermint. - privkeySeed := make([]byte, 15) - if _, err := r.Read(privkeySeed); err != nil { - panic(err) - } - - privKey := secp256k1.GenPrivKeyFromSecret(privkeySeed) - - a, ok := acc.GetCachedValue().(authtypes.AccountI) - if !ok { - return genesis, nil, fmt.Errorf("expected account") - } - - // create simulator accounts - simAcc := simtypes.Account{PrivKey: privKey, PubKey: privKey.PubKey(), Address: a.GetAddress()} - newAccs[i] = simAcc - } - - return genesis, newAccs, nil -} diff --git a/testutil/sample/crosschain.go b/testutil/sample/crosschain.go index 3634e6e1cc..293dbff61f 100644 --- a/testutil/sample/crosschain.go +++ b/testutil/sample/crosschain.go @@ -357,7 +357,6 @@ func InboundVote(coinType coin.CoinType, from, to int64) types.MsgVoteInbound { } } -// InboundVoteFromRand creates a simulated inbound vote message. This function uses the provided source of randomness to generate the vot // InboundVoteFromRand creates a simulated inbound vote message. This function uses the provided source of randomness to generate the vote func InboundVoteFromRand(from, to int64, r *rand.Rand, asset string) types.MsgVoteInbound { coinType := CoinTypeFromRand(r) diff --git a/testutil/sample/crypto.go b/testutil/sample/crypto.go index 8b5d68fda3..4f62878d0f 100644 --- a/testutil/sample/crypto.go +++ b/testutil/sample/crypto.go @@ -129,14 +129,6 @@ func SolanaAddress(t *testing.T) string { return privKey.PublicKey().String() } -func SolAddressFromRand(r *rand.Rand) string { - privKey, err := solana.NewRandomPrivateKey() - if err != nil { - panic(err) - } - return privKey.PublicKey().String() -} - // SolanaSignature returns a sample solana signature func SolanaSignature(t *testing.T) solana.Signature { // Generate a random keypair diff --git a/x/crosschain/genesis.go b/x/crosschain/genesis.go index 1054482b02..0f6856d559 100644 --- a/x/crosschain/genesis.go +++ b/x/crosschain/genesis.go @@ -1,10 +1,8 @@ package crosschain import ( - sdkmath "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/zeta-chain/node/pkg/coin" "github.com/zeta-chain/node/x/crosschain/keeper" "github.com/zeta-chain/node/x/crosschain/types" ) @@ -12,9 +10,8 @@ import ( // InitGenesis initializes the crosschain module's state from a provided genesis // state. func InitGenesis(ctx sdk.Context, k keeper.Keeper, genState types.GenesisState) { - // Always set the zeta accounting to zero at genesis. - // ZetaAccounting value is build by iterating through all the cctxs and adding the amount to the zeta accounting. - k.SetZetaAccounting(ctx, types.ZetaAccounting{AbortedZetaAmount: sdkmath.ZeroUint()}) + k.SetZetaAccounting(ctx, genState.ZetaAccounting) + // Set all the outbound tracker for _, elem := range genState.OutboundTrackerList { k.SetOutboundTracker(ctx, elem) @@ -37,8 +34,6 @@ func InitGenesis(ctx sdk.Context, k keeper.Keeper, genState types.GenesisState) } } - // Set all the chain nonces - // Set all the last block heights for _, elem := range genState.LastBlockHeightList { if elem != nil { @@ -46,36 +41,15 @@ func InitGenesis(ctx sdk.Context, k keeper.Keeper, genState types.GenesisState) } } - // Set all the cross-chain txs - //tss, found := k.GetObserverKeeper().GetTSS(ctx) - //if found { + // Set the cross-chain transactions only, + // We don't need to call SaveCCTXUpdate as the other fields are being set already for _, elem := range genState.CrossChainTxs { if elem != nil { cctx := *elem k.SetCrossChainTx(ctx, cctx) - // set mapping inboundHash -> cctxIndex - //in, _ := k.GetInboundHashToCctx(ctx, cctx.InboundParams.ObservedHash) - //in.InboundHash = cctx.InboundParams.ObservedHash - //found := false - //for _, cctxIndex := range in.CctxIndex { - // if cctxIndex == cctx.Index { - // found = true - // break - // } - //} - //if !found { - // in.CctxIndex = append(in.CctxIndex, cctx.Index) - //} - //k.SetInboundHashToCctx(ctx, in) - - if cctx.CctxStatus.Status == types.CctxStatus_Aborted && - cctx.InboundParams.CoinType == coin.CoinType_Zeta && - cctx.CctxStatus.IsAbortRefunded == false { - k.AddZetaAbortedAmount(ctx, keeper.GetAbortedAmount(cctx)) - } } } - //} + for _, elem := range genState.FinalizedInbounds { k.SetFinalizedInbound(ctx, elem) } diff --git a/x/crosschain/keeper/refund.go b/x/crosschain/keeper/refund.go index fd12b832e4..8d1e2da230 100644 --- a/x/crosschain/keeper/refund.go +++ b/x/crosschain/keeper/refund.go @@ -47,7 +47,6 @@ func (k Keeper) RefundAmountOnZetaChainGas( // get the zrc20 contract address fcSenderChain, found := k.fungibleKeeper.GetGasCoinForForeignCoin(ctx, chainID) if !found { - fmt.Println("chainID", chainID, "RefundAmountOnZetaChainGas") return types.ErrForeignCoinNotFound } zrc20 := ethcommon.HexToAddress(fcSenderChain.Zrc20ContractAddress) diff --git a/x/crosschain/simulation/genesis.go b/x/crosschain/simulation/genesis.go deleted file mode 100644 index 1e1cab4fcd..0000000000 --- a/x/crosschain/simulation/genesis.go +++ /dev/null @@ -1,14 +0,0 @@ -package simulation - -import ( - "github.com/cosmos/cosmos-sdk/types/module" - - "github.com/zeta-chain/node/x/crosschain/types" -) - -func RandomizedGenState(simState *module.SimulationState) { - // Randomization is primarily done for params present in the application state - // We do not need to randomize the genesis state for the crosschain module for now. - crosschainGenesis := types.DefaultGenesis() - simState.GenState[types.ModuleName] = simState.Cdc.MustMarshalJSON(crosschainGenesis) -} diff --git a/x/crosschain/types/outbound_tracker_test.go b/x/crosschain/types/outbound_tracker_test.go new file mode 100644 index 0000000000..4cd8a52fd7 --- /dev/null +++ b/x/crosschain/types/outbound_tracker_test.go @@ -0,0 +1,48 @@ +package types_test + +import ( + "testing" + + "github.com/zeta-chain/node/x/crosschain/types" +) + +func TestOutboundTracker_IsMaxed(t *testing.T) { + tests := []struct { + name string + tracker types.OutboundTracker + want bool + }{ + {"Not maxed", types.OutboundTracker{HashList: []*types.TxHash{ + {TxHash: "hash1", TxSigner: "signer1"}, + {TxHash: "hash2", TxSigner: "signer2"}, + {TxHash: "hash3", TxSigner: "signer3"}, + }}, + false}, + + {"Maxed", types.OutboundTracker{HashList: []*types.TxHash{ + {TxHash: "hash1", TxSigner: "signer1"}, + {TxHash: "hash2", TxSigner: "signer2"}, + {TxHash: "hash3", TxSigner: "signer3"}, + {TxHash: "hash4", TxSigner: "signer4"}, + {TxHash: "hash5", TxSigner: "signer5"}, + }}, + true}, + {"More than Maxed", types.OutboundTracker{HashList: []*types.TxHash{ + {TxHash: "hash1", TxSigner: "signer1"}, + {TxHash: "hash2", TxSigner: "signer2"}, + {TxHash: "hash3", TxSigner: "signer3"}, + {TxHash: "hash4", TxSigner: "signer4"}, + {TxHash: "hash5", TxSigner: "signer5"}, + {TxHash: "hash6", TxSigner: "signer6"}, + {TxHash: "hash7", TxSigner: "signer7"}, + }}, + true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.tracker.IsMaxed(); got != tt.want { + t.Errorf("OutboundTracker.IsMaxed() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/x/fungible/simulation/genesis.go b/x/fungible/simulation/genesis.go deleted file mode 100644 index 044fe27ae1..0000000000 --- a/x/fungible/simulation/genesis.go +++ /dev/null @@ -1,13 +0,0 @@ -package simulation - -import ( - "github.com/cosmos/cosmos-sdk/types/module" - - "github.com/zeta-chain/node/x/fungible/types" -) - -func RandomizedGenState(simState *module.SimulationState) { - // We do not have any params that we need to randomize for this module - fungibleGenesis := types.DefaultGenesis() - simState.GenState[types.ModuleName] = simState.Cdc.MustMarshalJSON(fungibleGenesis) -} diff --git a/x/observer/keeper/msg_server_vote_tss.go b/x/observer/keeper/msg_server_vote_tss.go index 9c796ba5bb..fc0156ffdd 100644 --- a/x/observer/keeper/msg_server_vote_tss.go +++ b/x/observer/keeper/msg_server_vote_tss.go @@ -79,12 +79,6 @@ func (k msgServer) VoteTSS(goCtx context.Context, msg *types.MsgVoteTSS) (*types return &types.MsgVoteTSSResponse{}, errorsmod.Wrap(err, voteTSSid) } - //if ctx.BlockHeight() == 3 || ctx.BlockHeight() == 4 { - // fmt.Println("Vote added", ctx.BlockHeight(), msg.TssPubkey) - // fmt.Println("Votes", ballot.Votes) - // fmt.Println("VoterList Length", len(ballot.VoterList)) - //} - ballot, isFinalized := k.CheckIfFinalizingVote(ctx, ballot) if !isFinalized { return &types.MsgVoteTSSResponse{ @@ -94,8 +88,6 @@ func (k msgServer) VoteTSS(goCtx context.Context, msg *types.MsgVoteTSS) (*types }, nil } - //fmt.Println("Ballot finalized", ballot.BallotStatus) - // The ballot is finalized, we check if this is the correct ballot for updating the TSS // The requirements are // 1. The keygen is still pending diff --git a/x/observer/simulation/genesis.go b/x/observer/simulation/genesis.go deleted file mode 100644 index f9dc10ff1c..0000000000 --- a/x/observer/simulation/genesis.go +++ /dev/null @@ -1,13 +0,0 @@ -package simulation - -import ( - "github.com/cosmos/cosmos-sdk/types/module" - - "github.com/zeta-chain/node/x/observer/types" -) - -func RandomizedGenState(simState *module.SimulationState) { - // We do not have any params that we need to randomize for this module - observerGenesis := types.DefaultGenesis() - simState.GenState[types.ModuleName] = simState.Cdc.MustMarshalJSON(observerGenesis) -} diff --git a/x/observer/simulation/operations.go b/x/observer/simulation/operations.go index 93d436ca78..d0acc1d020 100644 --- a/x/observer/simulation/operations.go +++ b/x/observer/simulation/operations.go @@ -28,17 +28,16 @@ import ( // 100 seems to the max weight used,and we should use relative weights // to signify the number of each operation in a block. const ( - // #nosec G101 not a hardcoded credential - OpWeightMsgTypeMsgEnableCCTX = "op_weight_msg_enable_crosschain_flags" - OpWeightMsgTypeMsgDisableCCTX = "op_weight_msg_disable_crosschain_flags" - OpWeightMsgTypeMsgVoteTSS = "op_weight_msg_vote_tss" - OpWeightMsgTypeMsgUpdateKeygen = "op_weight_msg_update_keygen" - OpWeightMsgTypeMsgUpdateObserver = "op_weight_msg_update_observer" - OpWeightMsgTypeMsgUpdateChainParams = "op_weight_msg_update_chain_params" - OpWeightMsgTypeMsgRemoveChainParams = "op_weight_msg_remove_chain_params" - OpWeightMsgTypeMsgResetChainNonces = "op_weight_msg_reset_chain_nonces" - OpWeightMsgTypeMsgUpdateGasPriceIncreaseFlags = "op_weight_msg_update_gas_price_increase_flags" - OpWeightMsgTypeMsgAddObserver = "op_weight_msg_add_observer" + OpWeightMsgTypeMsgEnableCCTX = "op_weight_msg_enable_crosschain_flags" // #nosec G101 not a hardcoded credential + OpWeightMsgTypeMsgDisableCCTX = "op_weight_msg_disable_crosschain_flags" // #nosec G101 not a hardcoded credential + OpWeightMsgTypeMsgVoteTSS = "op_weight_msg_vote_tss" // #nosec G101 not a hardcoded credential + OpWeightMsgTypeMsgUpdateKeygen = "op_weight_msg_update_keygen" // #nosec G101 not a hardcoded credential + OpWeightMsgTypeMsgUpdateObserver = "op_weight_msg_update_observer" // #nosec G101 not a hardcoded credential + OpWeightMsgTypeMsgUpdateChainParams = "op_weight_msg_update_chain_params" // #nosec G101 not a hardcoded credential + OpWeightMsgTypeMsgRemoveChainParams = "op_weight_msg_remove_chain_params" // #nosec G101 not a hardcoded credential + OpWeightMsgTypeMsgResetChainNonces = "op_weight_msg_reset_chain_nonces" // #nosec G101 not a hardcoded credential + OpWeightMsgTypeMsgUpdateGasPriceIncreaseFlags = "op_weight_msg_update_gas_price_increase_flags" // #nosec G101 not a hardcoded credential + OpWeightMsgTypeMsgAddObserver = "op_weight_msg_add_observer" // #nosec G101 not a hardcoded credential // DefaultWeightMsgTypeMsgEnableCCTX We use a high weight for this operation // to ensure that it is present in the block more number of times than any operation that changes the validator set diff --git a/zetaclient/chains/evm/observer/outbound.go b/zetaclient/chains/evm/observer/outbound.go index 6d7fab944f..fc67d5c82f 100644 --- a/zetaclient/chains/evm/observer/outbound.go +++ b/zetaclient/chains/evm/observer/outbound.go @@ -125,7 +125,7 @@ func (ob *Observer) ProcessOutboundTrackers(ctx context.Context) error { // should not happen. We can't tell which txHash is true. It might happen (e.g. bug, glitchy/hacked endpoint) ob.Logger().Outbound.Error().Msgf("WatchOutbound: confirmed multiple (%d) outbound for chain %d nonce %d", txCount, chainID, nonce) } else { - if len(tracker.HashList) == crosschaintypes.MaxOutboundTrackerHashes { + if tracker.IsMaxed() { ob.Logger().Outbound.Error().Msgf("WatchOutbound: outbound tracker is full of hashes for chain %d nonce %d", chainID, nonce) } }