From c54ef7b3e1560b481cd9d85482b583728ef580d0 Mon Sep 17 00:00:00 2001 From: Tyler <48813565+technicallyty@users.noreply.github.com> Date: Wed, 11 Mar 2026 10:36:48 -0700 Subject: [PATCH 1/7] mempool checktx handler updates --- evmd/mempool.go | 2 +- mempool/check_tx.go | 57 +++-- mempool/check_tx_test.go | 343 ++++++++++++++++++++++++++++++ mempool/mempool_test.go | 44 +++- mempool/tx_store.go | 38 +++- server/config/config.go | 16 +- server/config/config_test.go | 1 + server/config/toml.go | 3 + server/flags/flags.go | 4 + server/server_app_options.go | 22 ++ server/server_app_options_test.go | 60 ++++++ server/start.go | 1 + 12 files changed, 545 insertions(+), 46 deletions(-) create mode 100644 mempool/check_tx_test.go diff --git a/evmd/mempool.go b/evmd/mempool.go index 1046f0b3b..c2526b96c 100644 --- a/evmd/mempool.go +++ b/evmd/mempool.go @@ -57,7 +57,7 @@ func (app *EVMD) configureEVMMempool(appOpts servertypes.AppOptions, logger log. ) app.EVMMempool = evmMempool app.SetMempool(evmMempool) - checkTxHandler := evmmempool.NewCheckTxHandler(evmMempool) + checkTxHandler := evmmempool.NewCheckTxHandler(evmMempool, app.Trace(), server.GetCheckTxTimeout(appOpts, logger)) app.SetCheckTxHandler(checkTxHandler) app.SetInsertTxHandler(app.NewInsertTxHandler(evmMempool)) app.SetReapTxsHandler(app.NewReapTxsHandler(evmMempool)) diff --git a/mempool/check_tx.go b/mempool/check_tx.go index 410aae520..21b5bbf5e 100644 --- a/mempool/check_tx.go +++ b/mempool/check_tx.go @@ -1,49 +1,42 @@ package mempool import ( + "context" "errors" + "time" abci "github.com/cometbft/cometbft/abci/types" - "github.com/cosmos/evm/mempool/txpool" - "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" ) // NewCheckTxHandler creates a CheckTx handler that integrates with the EVM mempool for transaction validation. -// It wraps the standard transaction execution flow to handle EVM-specific nonce gap errors by routing -// transactions with higher tx sequence numbers to the mempool for potential future execution. -// Returns a handler function that processes ABCI CheckTx requests and manages EVM transaction sequencing. -func NewCheckTxHandler(mempool *ExperimentalEVMMempool) types.CheckTxHandler { - return func(runTx types.RunTx, request *abci.RequestCheckTx) (*abci.ResponseCheckTx, error) { - gInfo, result, anteEvents, err := runTx(request.Tx, nil) - if err != nil { - // detect if there is a nonce gap error (only returned for EVM transactions) - if errors.Is(err, ErrNonceGap) || errors.Is(err, ErrNonceLow) { - // send it to the mempool for further triage - err := mempool.InsertInvalidNonce(request.Tx) - if err != nil { - return sdkerrors.ResponseCheckTxWithEvents(err, gInfo.GasWanted, gInfo.GasUsed, anteEvents, false), nil - } - } - // If its already known, this can mean the the tx was promoted from nonce gap to valid - // and by allowing ErrAlreadyKnown to be silent, we allow re-gossiping of such txs - // this also covers the case of re-submission of the same tx enforcing overpricing for replacement - if errors.Is(err, txpool.ErrAlreadyKnown) { - return sdkerrors.ResponseCheckTxWithEvents(nil, gInfo.GasWanted, gInfo.GasUsed, anteEvents, false), nil - } +// It routes new CheckTx requests through the same async insert worker path used by +// the app-side mempool and waits for the insert result. +func NewCheckTxHandler(mempool *ExperimentalEVMMempool, debug bool, timeout time.Duration) types.CheckTxHandler { + if timeout <= 0 { + panic("invalid timeout CheckTxHandler timeout value") + } + return func(_ types.RunTx, request *abci.RequestCheckTx) (*abci.ResponseCheckTx, error) { + // TODO: do we even do recheck anymore? + if request.Type == abci.CheckTxType_Recheck { + return &abci.ResponseCheckTx{Code: abci.CodeTypeOK}, nil + } - // anything else, return regular error - return sdkerrors.ResponseCheckTxWithEvents(err, gInfo.GasWanted, gInfo.GasUsed, anteEvents, false), nil + tx, err := mempool.txConfig.TxDecoder()(request.Tx) + if err != nil { + return sdkerrors.ResponseCheckTxWithEvents(err, 0, 0, nil, debug), nil } - return &abci.ResponseCheckTx{ - GasWanted: int64(gInfo.GasWanted), // #nosec G115 -- this is copied from the Cosmos SDK - GasUsed: int64(gInfo.GasUsed), // #nosec G115 -- this is copied from the Cosmos SDK - Log: result.Log, - Data: result.Data, - Events: types.MarkEventsToIndex(result.Events, nil), - }, nil + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + if err := mempool.Insert(ctx, tx); err != nil { + if errors.Is(err, context.DeadlineExceeded) { + err = ctx.Err() + } + return sdkerrors.ResponseCheckTxWithEvents(err, 0, 0, nil, debug), nil + } + return &abci.ResponseCheckTx{Code: abci.CodeTypeOK}, nil } } diff --git a/mempool/check_tx_test.go b/mempool/check_tx_test.go new file mode 100644 index 000000000..78c44959e --- /dev/null +++ b/mempool/check_tx_test.go @@ -0,0 +1,343 @@ +package mempool_test + +import ( + "math/big" + "testing" + "time" + + ethcore "github.com/ethereum/go-ethereum/core" + "github.com/stretchr/testify/suite" + + abci "github.com/cometbft/cometbft/abci/types" + + "github.com/cosmos/evm/mempool" + evmtxpool "github.com/cosmos/evm/mempool/txpool" + "github.com/cosmos/evm/mempool/txpool/legacypool" + + storetypes "cosmossdk.io/store/types" + + "github.com/cosmos/cosmos-sdk/testutil" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +type CheckTxHandlerTestSuite struct { + suite.Suite +} + +func TestCheckTxHandlerTestSuite(t *testing.T) { + suite.Run(t, new(CheckTxHandlerTestSuite)) +} + +func (s *CheckTxHandlerTestSuite) submitCheckTx(handler sdk.CheckTxHandler, txBytes []byte, txType abci.CheckTxType) *abci.ResponseCheckTx { + res, err := handler(nil, &abci.RequestCheckTx{ + Tx: txBytes, + Type: txType, + }) + s.Require().NoError(err) + return res +} + +func (s *CheckTxHandlerTestSuite) cosmosSelectContext() sdk.Context { + storeKey := storetypes.NewKVStoreKey("test") + transientKey := storetypes.NewTransientStoreKey("transient_test") + return testutil.DefaultContext(storeKey, transientKey).WithBlockHeight(2) +} + +func (s *CheckTxHandlerTestSuite) TestEVMCheckTx() { + testCases := []struct { + name string + setup func() testMempool + assert func(mp testMempool, handler sdk.CheckTxHandler) + }{ + { + name: "inserts new tx", + assert: func(mp testMempool, handler sdk.CheckTxHandler) { + tx := createMsgEthereumTx(s.T(), mp.txConfig, mp.accounts[0].key, 0, nil) + txBytes, err := mp.txConfig.TxEncoder()(tx) + s.Require().NoError(err) + + res := s.submitCheckTx(handler, txBytes, abci.CheckTxType_New) + s.Equal(abci.CodeTypeOK, res.Code) + s.Require().NoError(mp.mp.GetTxPool().Sync()) + s.Equal(1, mp.mp.CountTx()) + }, + }, + { + name: "returns insert error for duplicate tx", + assert: func(mp testMempool, handler sdk.CheckTxHandler) { + tx := createMsgEthereumTx(s.T(), mp.txConfig, mp.accounts[0].key, 0, nil) + txBytes, err := mp.txConfig.TxEncoder()(tx) + s.Require().NoError(err) + + firstRes := s.submitCheckTx(handler, txBytes, abci.CheckTxType_New) + s.Equal(abci.CodeTypeOK, firstRes.Code) + s.Require().NoError(mp.mp.GetTxPool().Sync()) + + secondRes := s.submitCheckTx(handler, txBytes, abci.CheckTxType_New) + s.NotEqual(abci.CodeTypeOK, secondRes.Code) + s.Contains(secondRes.Log, evmtxpool.ErrAlreadyKnown.Error()) + s.Require().NoError(mp.mp.GetTxPool().Sync()) + s.Equal(1, mp.mp.CountTx()) + }, + }, + { + name: "replaces with higher fee tx", + assert: func(mp testMempool, handler sdk.CheckTxHandler) { + lowFeeTx := createMsgEthereumTx(s.T(), mp.txConfig, mp.accounts[0].key, 0, big.NewInt(1000)) + highFeeTx := createMsgEthereumTx(s.T(), mp.txConfig, mp.accounts[0].key, 0, big.NewInt(2000)) + + lowFeeTxBytes, err := mp.txConfig.TxEncoder()(lowFeeTx) + s.Require().NoError(err) + highFeeTxBytes, err := mp.txConfig.TxEncoder()(highFeeTx) + s.Require().NoError(err) + + res := s.submitCheckTx(handler, lowFeeTxBytes, abci.CheckTxType_New) + s.Equal(abci.CodeTypeOK, res.Code) + s.Require().NoError(mp.mp.GetTxPool().Sync()) + + res = s.submitCheckTx(handler, highFeeTxBytes, abci.CheckTxType_New) + s.Equal(abci.CodeTypeOK, res.Code) + s.Require().NoError(mp.mp.GetTxPool().Sync()) + + legacyPool := mp.mp.GetTxPool().Subpools[0].(*legacypool.LegacyPool) + pending, queued := legacyPool.ContentFrom(mp.accounts[0].address) + s.Len(pending, 1) + s.Len(queued, 0) + s.Equal(big.NewInt(2000), pending[0].GasPrice()) + s.Equal(1, mp.mp.CountTx()) + }, + }, + { + name: "rejects underpriced replacement", + assert: func(mp testMempool, handler sdk.CheckTxHandler) { + originalTx := createMsgEthereumTx(s.T(), mp.txConfig, mp.accounts[0].key, 0, big.NewInt(1000)) + underpricedReplacementTx := createMsgEthereumTx(s.T(), mp.txConfig, mp.accounts[0].key, 0, big.NewInt(1050)) + + originalTxBytes, err := mp.txConfig.TxEncoder()(originalTx) + s.Require().NoError(err) + underpricedReplacementTxBytes, err := mp.txConfig.TxEncoder()(underpricedReplacementTx) + s.Require().NoError(err) + + res := s.submitCheckTx(handler, originalTxBytes, abci.CheckTxType_New) + s.Equal(abci.CodeTypeOK, res.Code) + s.Require().NoError(mp.mp.GetTxPool().Sync()) + + res = s.submitCheckTx(handler, underpricedReplacementTxBytes, abci.CheckTxType_New) + s.NotEqual(abci.CodeTypeOK, res.Code) + s.Contains(res.Log, evmtxpool.ErrReplaceUnderpriced.Error()) + s.Require().NoError(mp.mp.GetTxPool().Sync()) + + legacyPool := mp.mp.GetTxPool().Subpools[0].(*legacypool.LegacyPool) + pending, queued := legacyPool.ContentFrom(mp.accounts[0].address) + s.Len(pending, 1) + s.Len(queued, 0) + s.Equal(big.NewInt(1000), pending[0].GasPrice()) + s.Equal(1, mp.mp.CountTx()) + }, + }, + { + name: "accepts nonce gapped txs and promotes them when gap is filled", + assert: func(mp testMempool, handler sdk.CheckTxHandler) { + queuedTx := createMsgEthereumTx(s.T(), mp.txConfig, mp.accounts[0].key, 1, big.NewInt(1000)) + fillGapTx := createMsgEthereumTx(s.T(), mp.txConfig, mp.accounts[0].key, 0, big.NewInt(1000)) + + queuedTxBytes, err := mp.txConfig.TxEncoder()(queuedTx) + s.Require().NoError(err) + fillGapTxBytes, err := mp.txConfig.TxEncoder()(fillGapTx) + s.Require().NoError(err) + + res := s.submitCheckTx(handler, queuedTxBytes, abci.CheckTxType_New) + s.Equal(abci.CodeTypeOK, res.Code) + s.Require().NoError(mp.mp.GetTxPool().Sync()) + + legacyPool := mp.mp.GetTxPool().Subpools[0].(*legacypool.LegacyPool) + pending, queued := legacyPool.ContentFrom(mp.accounts[0].address) + s.Len(pending, 0) + s.Len(queued, 1) + s.Equal(uint64(1), queued[0].Nonce()) + s.Equal(0, mp.mp.CountTx()) + + res = s.submitCheckTx(handler, fillGapTxBytes, abci.CheckTxType_New) + s.Equal(abci.CodeTypeOK, res.Code) + s.Require().NoError(mp.mp.GetTxPool().Sync()) + + pending, queued = legacyPool.ContentFrom(mp.accounts[0].address) + s.Len(pending, 2) + s.Len(queued, 0) + s.Equal(uint64(0), pending[0].Nonce()) + s.Equal(uint64(1), pending[1].Nonce()) + s.Equal(2, mp.mp.CountTx()) + }, + }, + { + name: "replaces queued tx with higher fee", + assert: func(mp testMempool, handler sdk.CheckTxHandler) { + queuedTx := createMsgEthereumTx(s.T(), mp.txConfig, mp.accounts[0].key, 1, big.NewInt(1000)) + replacementTx := createMsgEthereumTx(s.T(), mp.txConfig, mp.accounts[0].key, 1, big.NewInt(2000)) + + queuedTxBytes, err := mp.txConfig.TxEncoder()(queuedTx) + s.Require().NoError(err) + replacementTxBytes, err := mp.txConfig.TxEncoder()(replacementTx) + s.Require().NoError(err) + + res := s.submitCheckTx(handler, queuedTxBytes, abci.CheckTxType_New) + s.Equal(abci.CodeTypeOK, res.Code) + s.Require().NoError(mp.mp.GetTxPool().Sync()) + + res = s.submitCheckTx(handler, replacementTxBytes, abci.CheckTxType_New) + s.Equal(abci.CodeTypeOK, res.Code) + s.Require().NoError(mp.mp.GetTxPool().Sync()) + + legacyPool := mp.mp.GetTxPool().Subpools[0].(*legacypool.LegacyPool) + pending, queued := legacyPool.ContentFrom(mp.accounts[0].address) + s.Len(pending, 0) + s.Len(queued, 1) + s.Equal(big.NewInt(2000), queued[0].GasPrice()) + s.Equal(0, mp.mp.CountTx()) + }, + }, + { + name: "rejects lower nonce against advanced state", + setup: func() testMempool { return setupMempoolWithAccountNonces(s.T(), []uint64{1}) }, + assert: func(mp testMempool, handler sdk.CheckTxHandler) { + tx := createMsgEthereumTx(s.T(), mp.txConfig, mp.accounts[0].key, 0, big.NewInt(1000)) + txBytes, err := mp.txConfig.TxEncoder()(tx) + s.Require().NoError(err) + + res := s.submitCheckTx(handler, txBytes, abci.CheckTxType_New) + s.NotEqual(abci.CodeTypeOK, res.Code) + s.Contains(res.Log, ethcore.ErrNonceTooLow.Error()) + s.Equal(0, mp.mp.CountTx()) + }, + }, + { + name: "returns queue full when insert queue is saturated", + setup: func() testMempool { return setupMempoolWithInsertQueueSize(s.T(), 1, 0) }, + assert: func(mp testMempool, handler sdk.CheckTxHandler) { + tx := createMsgEthereumTx(s.T(), mp.txConfig, mp.accounts[0].key, 0, big.NewInt(1000)) + txBytes, err := mp.txConfig.TxEncoder()(tx) + s.Require().NoError(err) + + res := s.submitCheckTx(handler, txBytes, abci.CheckTxType_New) + s.NotEqual(abci.CodeTypeOK, res.Code) + s.Contains(res.Log, mempool.ErrQueueFull.Error()) + s.Equal(0, mp.mp.CountTx()) + }, + }, + { + name: "rejects malformed tx bytes", + assert: func(mp testMempool, handler sdk.CheckTxHandler) { + res := s.submitCheckTx(handler, []byte("not-a-real-tx"), abci.CheckTxType_New) + s.NotEqual(abci.CodeTypeOK, res.Code) + s.NotEmpty(res.Log) + s.Equal(0, mp.mp.CountTx()) + }, + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + mp := setupMempoolWithAccounts(s.T(), 1) + if tc.setup != nil { + mp = tc.setup() + } + handler := mempool.NewCheckTxHandler(mp.mp, false, time.Minute) + tc.assert(mp, handler) + }) + } +} + +func (s *CheckTxHandlerTestSuite) TestCosmosCheckTx() { + testCases := []struct { + name string + numAccounts int + assert func(mp testMempool, handler sdk.CheckTxHandler) + }{ + { + name: "inserts cosmos tx", + numAccounts: 1, + assert: func(mp testMempool, handler sdk.CheckTxHandler) { + tx := createTestCosmosTx(s.T(), mp.txConfig, mp.accounts[0].key, 0) + txBytes, err := mp.txConfig.TxEncoder()(tx) + s.Require().NoError(err) + + res := s.submitCheckTx(handler, txBytes, abci.CheckTxType_New) + s.Equal(abci.CodeTypeOK, res.Code, res.Log) + s.Equal(1, mp.mp.CountTx()) + }, + }, + { + name: "replaces cosmos tx with higher fee", + numAccounts: 1, + assert: func(mp testMempool, handler sdk.CheckTxHandler) { + lowFeeTx := createTestCosmosTxWithFee(s.T(), mp.txConfig, mp.accounts[0].key, 0, 1000000) + highFeeTx := createTestCosmosTxWithFee(s.T(), mp.txConfig, mp.accounts[0].key, 0, 2000000) + + lowFeeTxBytes, err := mp.txConfig.TxEncoder()(lowFeeTx) + s.Require().NoError(err) + highFeeTxBytes, err := mp.txConfig.TxEncoder()(highFeeTx) + s.Require().NoError(err) + + res := s.submitCheckTx(handler, lowFeeTxBytes, abci.CheckTxType_New) + s.Equal(abci.CodeTypeOK, res.Code, res.Log) + + res = s.submitCheckTx(handler, highFeeTxBytes, abci.CheckTxType_New) + s.Equal(abci.CodeTypeOK, res.Code, res.Log) + s.Equal(1, mp.mp.CountTx()) + + iter := mp.mp.Select(s.cosmosSelectContext(), nil) + s.Require().NotNil(iter) + + feeTx, ok := iter.Tx().(sdk.FeeTx) + s.True(ok) + s.EqualValues(2000000, feeTx.GetFee()[0].Amount.Int64()) + s.Nil(iter.Next()) + }, + }, + { + name: "replaces multi signer cosmos tx with higher fee", + numAccounts: 2, + assert: func(mp testMempool, handler sdk.CheckTxHandler) { + originalTx := createTestMultiSignerCosmosTxWithFee(s.T(), mp.txConfig, 1000000, mp.accounts[0].key, mp.accounts[1].key) + replacementTx := createTestMultiSignerCosmosTxWithFee(s.T(), mp.txConfig, 2000000, mp.accounts[0].key, mp.accounts[1].key) + + originalTxBytes, err := mp.txConfig.TxEncoder()(originalTx) + s.Require().NoError(err) + replacementTxBytes, err := mp.txConfig.TxEncoder()(replacementTx) + s.Require().NoError(err) + + res := s.submitCheckTx(handler, originalTxBytes, abci.CheckTxType_New) + s.Equal(abci.CodeTypeOK, res.Code, res.Log) + + res = s.submitCheckTx(handler, replacementTxBytes, abci.CheckTxType_New) + s.Equal(abci.CodeTypeOK, res.Code, res.Log) + s.Equal(1, mp.mp.CountTx()) + + iter := mp.mp.Select(s.cosmosSelectContext(), nil) + s.Require().NotNil(iter) + + feeTx, ok := iter.Tx().(sdk.FeeTx) + s.True(ok) + s.EqualValues(2000000, feeTx.GetFee()[0].Amount.Int64()) + s.Nil(iter.Next()) + }, + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + mp := setupMempoolWithAccounts(s.T(), tc.numAccounts) + handler := mempool.NewCheckTxHandler(mp.mp, false, time.Minute) + tc.assert(mp, handler) + }) + } +} + +func (s *CheckTxHandlerTestSuite) TestRecheckIsNoOp() { + mp := setupMempoolWithAccounts(s.T(), 1) + handler := mempool.NewCheckTxHandler(mp.mp, false, time.Minute) + + res := s.submitCheckTx(handler, []byte("not-a-real-tx"), abci.CheckTxType_Recheck) + s.Equal(abci.CodeTypeOK, res.Code) + s.Equal(0, mp.mp.CountTx()) +} diff --git a/mempool/mempool_test.go b/mempool/mempool_test.go index f3af034cf..621a027ba 100644 --- a/mempool/mempool_test.go +++ b/mempool/mempool_test.go @@ -53,7 +53,7 @@ func TestMempool_Iterate(t *testing.T) { numAccs := 20 storeKey := storetypes.NewKVStoreKey("test") transientKey := storetypes.NewTransientStoreKey("transient_test") - ctx := testutil.DefaultContext(storeKey, transientKey) //nolint:staticcheck // false positive. + ctx := testutil.DefaultContext(storeKey, transientKey) s := setupMempoolWithAccounts(t, numAccs) mp, txConfig, accounts := s.mp, s.txConfig, s.accounts @@ -94,7 +94,7 @@ func TestMempool_Iterate(t *testing.T) { func TestMempool_Reserver(t *testing.T) { storeKey := storetypes.NewKVStoreKey("test") transientKey := storetypes.NewTransientStoreKey("transient_test") - ctx := testutil.DefaultContext(storeKey, transientKey) //nolint:staticcheck // false positive. + ctx := testutil.DefaultContext(storeKey, transientKey) s := setupMempoolWithAccounts(t, 3) mp, txConfig, accounts := s.mp, s.txConfig, s.accounts @@ -137,7 +137,7 @@ func TestMempool_Reserver(t *testing.T) { func TestMempool_ReserverMultiSigner(t *testing.T) { storeKey := storetypes.NewKVStoreKey("test") transientKey := storetypes.NewTransientStoreKey("transient_test") - ctx := testutil.DefaultContext(storeKey, transientKey) //nolint:staticcheck // false positive. + ctx := testutil.DefaultContext(storeKey, transientKey) s := setupMempoolWithAccounts(t, 4) mp, txConfig, accounts := s.mp, s.txConfig, s.accounts @@ -633,16 +633,35 @@ type testMempool struct { func setupMempoolWithAccounts(t *testing.T, numAccounts int) testMempool { t.Helper() + return setupMempool(t, numAccounts, nil, 1000) +} + +func setupMempoolWithAccountNonces(t *testing.T, initialNonces []uint64) testMempool { + t.Helper() + return setupMempool(t, len(initialNonces), initialNonces, 1000) +} + +func setupMempoolWithInsertQueueSize(t *testing.T, numAccounts int, insertQueueSize int) testMempool { + t.Helper() + return setupMempool(t, numAccounts, nil, insertQueueSize) +} + +func setupMempool(t *testing.T, numAccounts int, initialNonces []uint64, insertQueueSize int) testMempool { //nolint:unparam // false positive + t.Helper() // Create accounts accounts := make([]testAccount, numAccounts) for i := range numAccounts { key, err := crypto.GenerateKey() require.NoError(t, err) + var nonce uint64 + if len(initialNonces) > i { + nonce = initialNonces[i] + } accounts[i] = testAccount{ key: key, address: crypto.PubkeyToAddress(key.PublicKey), - nonce: 0, + nonce: nonce, initialBalance: 100000000000100, } } @@ -708,6 +727,7 @@ func setupMempoolWithAccounts(t *testing.T, numAccounts int) testMempool { encodingConfig := encoding.MakeConfig(constants.EighteenDecimalsChainID) // Register vm types so MsgEthereumTx can be decoded vmtypes.RegisterInterfaces(encodingConfig.InterfaceRegistry) + banktypes.RegisterInterfaces(encodingConfig.InterfaceRegistry) txConfig := encodingConfig.TxConfig // Create client context @@ -726,7 +746,7 @@ func setupMempoolWithAccounts(t *testing.T, numAccounts int) testMempool { LegacyPoolConfig: &legacyConfig, BlockGasLimit: 30000000, MinTip: uint256.NewInt(0), - InsertQueueSize: 1000, + InsertQueueSize: insertQueueSize, } // Create mempool @@ -887,6 +907,11 @@ func (mr *MockRechecker) Update(ctx sdk.Context, _ *types.Header) { // createTestCosmosTx creates a real Cosmos SDK transaction with the given signer func createTestCosmosTx(t *testing.T, txConfig client.TxConfig, key *ecdsa.PrivateKey, sequence uint64) sdk.Tx { t.Helper() + return createTestCosmosTxWithFee(t, txConfig, key, sequence, 1000000) +} + +func createTestCosmosTxWithFee(t *testing.T, txConfig client.TxConfig, key *ecdsa.PrivateKey, sequence uint64, feeAmount int64) sdk.Tx { + t.Helper() pubKeyBytes := crypto.CompressPubkey(&key.PublicKey) pubKey := ðsecp256k1.PubKey{Key: pubKeyBytes} @@ -905,7 +930,7 @@ func createTestCosmosTx(t *testing.T, txConfig client.TxConfig, key *ecdsa.Priva require.NoError(t, err) txBuilder.SetGasLimit(100000) - txBuilder.SetFeeAmount(sdk.NewCoins(sdk.NewInt64Coin("aevmos", 1000000))) + txBuilder.SetFeeAmount(sdk.NewCoins(sdk.NewInt64Coin("aevmos", feeAmount))) // Set signature with pubkey (unsigned but has signer info) sigData := &signingtypes.SingleSignatureData{ @@ -926,6 +951,11 @@ func createTestCosmosTx(t *testing.T, txConfig client.TxConfig, key *ecdsa.Priva // createTestMultiSignerCosmosTx creates a Cosmos SDK transaction with multiple signers. // Each key produces one MsgSend from that signer. func createTestMultiSignerCosmosTx(t *testing.T, txConfig client.TxConfig, keys ...*ecdsa.PrivateKey) sdk.Tx { + t.Helper() + return createTestMultiSignerCosmosTxWithFee(t, txConfig, 1000000, keys...) +} + +func createTestMultiSignerCosmosTxWithFee(t *testing.T, txConfig client.TxConfig, feeAmount int64, keys ...*ecdsa.PrivateKey) sdk.Tx { t.Helper() require.NotEmpty(t, keys, "must provide at least one key") @@ -964,7 +994,7 @@ func createTestMultiSignerCosmosTx(t *testing.T, txConfig client.TxConfig, keys require.NoError(t, err) txBuilder.SetGasLimit(100000 * uint64(len(keys))) - txBuilder.SetFeeAmount(sdk.NewCoins(sdk.NewInt64Coin("aevmos", 1000000))) + txBuilder.SetFeeAmount(sdk.NewCoins(sdk.NewInt64Coin("aevmos", feeAmount))) err = txBuilder.SetSignatures(sigs...) require.NoError(t, err) diff --git a/mempool/tx_store.go b/mempool/tx_store.go index c60e9b4cb..0bc06ae8f 100644 --- a/mempool/tx_store.go +++ b/mempool/tx_store.go @@ -1,6 +1,8 @@ package mempool import ( + "fmt" + "strings" "sync" sdk "github.com/cosmos/cosmos-sdk/types" @@ -14,6 +16,7 @@ type CosmosTxStore struct { // index maps a tx to its position in the txs slice for fast removal index map[sdk.Tx]int + keys map[string]int mu sync.RWMutex } @@ -22,15 +25,27 @@ type CosmosTxStore struct { func NewCosmosTxStore() *CosmosTxStore { return &CosmosTxStore{ index: make(map[sdk.Tx]int), + keys: make(map[string]int), } } // AddTx adds a single tx to the store. Duplicate txs (by pointer identity) -// are ignored. +// are ignored. Transactions with the same signer/nonce tuple overwrite the +// existing entry to mirror the SDK PriorityNonceMempool replacement model. func (s *CosmosTxStore) AddTx(tx sdk.Tx) { s.mu.Lock() defer s.mu.Unlock() + if key, ok := cosmosTxKey(tx); ok { + if idx, exists := s.keys[key]; exists { + delete(s.index, s.txs[idx]) + s.txs[idx] = tx + s.index[tx] = idx + return + } + s.keys[key] = len(s.txs) + } + if _, exists := s.index[tx]; exists { return } @@ -38,6 +53,27 @@ func (s *CosmosTxStore) AddTx(tx sdk.Tx) { s.txs = append(s.txs, tx) } +func cosmosTxKey(tx sdk.Tx) (string, bool) { + signerSeqs, err := extractSignerSequences(tx) + if err != nil || len(signerSeqs) == 0 { + return "", false + } + + var b strings.Builder + for i, sig := range signerSeqs { + if i > 0 { + b.WriteByte('|') + } + nonce, err := sdkmempool.ChooseNonce(sig.seq, tx) + if err != nil { + return "", false + } + fmt.Fprintf(&b, "%s/%d", sig.account, nonce) + } + + return b.String(), true +} + // Txs returns a copy of the current set of txs in the store. func (s *CosmosTxStore) Txs() []sdk.Tx { s.mu.RLock() diff --git a/server/config/config.go b/server/config/config.go index 1ce86abeb..4e0ed0a1c 100644 --- a/server/config/config.go +++ b/server/config/config.go @@ -131,6 +131,10 @@ var evmTracers = []string{"json", "markdown", "struct", "access_list"} type Config struct { config.Config `mapstructure:",squash"` + // UseIAVLX enables the IAVLX storage engine. + // todo: move to appropriate place? + UseIAVLX bool `mapstructure:"use-iavlx"` + EVM EVMConfig `mapstructure:"evm"` JSONRPC JSONRPCConfig `mapstructure:"json-rpc"` TLS TLSConfig `mapstructure:"tls"` @@ -171,6 +175,9 @@ type MempoolConfig struct { GlobalQueue uint64 `mapstructure:"global-queue"` // Lifetime is the maximum amount of time non-executable transaction are queued Lifetime time.Duration `mapstructure:"lifetime"` + // CheckTxTimeout is the maximum amount of time to wait for an async mempool + // insert result while serving CheckTx (0 disables the timeout) + CheckTxTimeout time.Duration `mapstructure:"check-tx-timeout"` // PendingTxProposalTimeout is the amount of time to spend waiting for // rechecking of the mempool to complete when creating a proposal PendingTxProposalTimeout time.Duration `mapstructure:"pending-tx-proposal-timeout"` @@ -189,6 +196,7 @@ func DefaultMempoolConfig() MempoolConfig { AccountQueue: 64, // 64 non-executable transaction slots per account GlobalQueue: 1024, // 1024 global non-executable slots Lifetime: 3 * time.Hour, // 3 hour lifetime for queued transactions + CheckTxTimeout: 30 * time.Second, // 30 seconds to wait for CheckTx insert results PendingTxProposalTimeout: 250 * time.Millisecond, // 250 milliseconds to wait for rechecks InsertQueueSize: 5_000, // 5000 txs maximum in the insert queue } @@ -217,6 +225,9 @@ func (c MempoolConfig) Validate() error { if c.Lifetime < 1 { return fmt.Errorf("lifetime must be at least 1 nanosecond, got %s", c.Lifetime) } + if c.CheckTxTimeout < 0 { + return fmt.Errorf("check tx timeout must be non-negative, got %s", c.CheckTxTimeout) + } if c.InsertQueueSize < 1 { return fmt.Errorf("insert queue size must be at least 1, got %d", c.InsertQueueSize) } @@ -460,11 +471,6 @@ func GetConfig(v *viper.Viper) (Config, error) { if err := v.Unmarshal(conf); err != nil { return Config{}, fmt.Errorf("error extracting app config: %w", err) } - sdkConf, err := config.GetConfig(v) - if err != nil { - return Config{}, err - } - conf.GRPC.HistoricalGRPCAddressBlockRange = sdkConf.GRPC.HistoricalGRPCAddressBlockRange return *conf, nil } diff --git a/server/config/config_test.go b/server/config/config_test.go index e5920a986..3db0fc610 100644 --- a/server/config/config_test.go +++ b/server/config/config_test.go @@ -17,6 +17,7 @@ func TestDefaultConfig(t *testing.T) { require.False(t, cfg.JSONRPC.Enable) require.Equal(t, cfg.JSONRPC.Address, serverconfig.DefaultJSONRPCAddress) require.Equal(t, cfg.JSONRPC.WsAddress, serverconfig.DefaultJSONRPCWsAddress) + require.Equal(t, serverconfig.DefaultMempoolConfig().CheckTxTimeout, cfg.EVM.Mempool.CheckTxTimeout) } func TestGetConfig(t *testing.T) { diff --git a/server/config/toml.go b/server/config/toml.go index 5e32ef9f9..1f2171b47 100644 --- a/server/config/toml.go +++ b/server/config/toml.go @@ -52,6 +52,9 @@ global-queue = {{ .EVM.Mempool.GlobalQueue }} # Lifetime is the maximum amount of time non-executable transaction are queued lifetime = "{{ .EVM.Mempool.Lifetime }}" +# CheckTxTimeout is the maximum amount of time to wait for async mempool admission during CheckTx (0 disables the timeout) +check-tx-timeout = "{{ .EVM.Mempool.CheckTxTimeout }}" + # PendingTxProposalTimeout is the amount of time to spend waiting for rechecking of the mempool to complete when creating a proposal pending-tx-proposal-timeout = "{{ .EVM.Mempool.PendingTxProposalTimeout }}" diff --git a/server/flags/flags.go b/server/flags/flags.go index dd5d6347a..49a073cff 100644 --- a/server/flags/flags.go +++ b/server/flags/flags.go @@ -79,10 +79,14 @@ const ( EVMMempoolAccountQueue = "evm.mempool.account-queue" EVMMempoolGlobalQueue = "evm.mempool.global-queue" EVMMempoolLifetime = "evm.mempool.lifetime" + EVMMempoolCheckTxTimeout = "evm.mempool.check-tx-timeout" EVMMempoolPendingTxProposalTimeout = "evm.mempool.pending-tx-proposal-timeout" EVMMempoolInsertQueueSize = "evm.mempool.insert-queue-size" ) +// IAVLXEnable (experimental) enables the IAVLX storage engine. +const IAVLXEnable = "iavlx-enable" + // TLS flags const ( TLSCertPath = "tls.certificate-path" diff --git a/server/server_app_options.go b/server/server_app_options.go index 38cd2787b..6acca1135 100644 --- a/server/server_app_options.go +++ b/server/server_app_options.go @@ -9,6 +9,7 @@ import ( "github.com/spf13/cast" "github.com/cosmos/evm/mempool/txpool/legacypool" + serverconfig "github.com/cosmos/evm/server/config" srvflags "github.com/cosmos/evm/server/flags" "cosmossdk.io/log/v2" @@ -154,6 +155,27 @@ func GetPendingTxProposalTimeout(appOpts servertypes.AppOptions, logger log.Logg return cast.ToDuration(appOpts.Get(srvflags.EVMMempoolPendingTxProposalTimeout)) } +func GetCheckTxTimeout(appOpts servertypes.AppOptions, logger log.Logger) time.Duration { + defaultTimeout := serverconfig.DefaultMempoolConfig().CheckTxTimeout + if appOpts == nil { + logger.Error("app options is nil, using default check tx timeout", "timeout", defaultTimeout) + return defaultTimeout + } + + value := appOpts.Get(srvflags.EVMMempoolCheckTxTimeout) + if value == nil { + return defaultTimeout + } + + timeout := cast.ToDuration(value) + if timeout < 0 { + logger.Error("invalid check tx timeout in app options, using default", "timeout", timeout, "default", defaultTimeout) + return defaultTimeout + } + + return timeout +} + func GetMempoolInsertQueueSize(appOpts servertypes.AppOptions, logger log.Logger) int { if appOpts == nil { logger.Error("app options is nil, using insert queue size of 5000") diff --git a/server/server_app_options_test.go b/server/server_app_options_test.go index 7eb756cae..e53979fd6 100644 --- a/server/server_app_options_test.go +++ b/server/server_app_options_test.go @@ -7,9 +7,13 @@ import ( "os" "path/filepath" "testing" + "time" "github.com/stretchr/testify/require" + serverconfig "github.com/cosmos/evm/server/config" + srvflags "github.com/cosmos/evm/server/flags" + "cosmossdk.io/log/v2" sdkmath "cosmossdk.io/math" @@ -230,6 +234,62 @@ func TestGetMinGasPrices(t *testing.T) { } } +func TestGetCheckTxTimeout(t *testing.T) { + t.Parallel() + + defaultTimeout := serverconfig.DefaultMempoolConfig().CheckTxTimeout + + tests := []struct { + name string + setupFn func() servertypes.AppOptions + expected time.Duration + }{ + { + name: "missing option uses default", + setupFn: func() servertypes.AppOptions { + return newMockAppOptions() + }, + expected: defaultTimeout, + }, + { + name: "configured timeout is returned", + setupFn: func() servertypes.AppOptions { + opts := newMockAppOptions() + opts.Set(srvflags.EVMMempoolCheckTxTimeout, "12s") + return opts + }, + expected: 12 * time.Second, + }, + { + name: "zero timeout disables waiting limit", + setupFn: func() servertypes.AppOptions { + opts := newMockAppOptions() + opts.Set(srvflags.EVMMempoolCheckTxTimeout, "0s") + return opts + }, + expected: 0, + }, + { + name: "negative timeout falls back to default", + setupFn: func() servertypes.AppOptions { + opts := newMockAppOptions() + opts.Set(srvflags.EVMMempoolCheckTxTimeout, "-1s") + return opts + }, + expected: defaultTimeout, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + result := GetCheckTxTimeout(tc.setupFn(), log.NewNopLogger()) + require.Equal(t, tc.expected, result) + }) + } +} + func createGenesisWithMaxGas(t *testing.T, maxGas int64) string { t.Helper() tempDir := t.TempDir() diff --git a/server/start.go b/server/start.go index a731e6417..f101a5a7d 100644 --- a/server/start.go +++ b/server/start.go @@ -231,6 +231,7 @@ which accepts a path for the resulting pprof file. cmd.Flags().Uint64(srvflags.EVMMempoolAccountQueue, cosmosevmserverconfig.DefaultMempoolConfig().AccountQueue, "the maximum number of non-executable transaction slots permitted per account") cmd.Flags().Uint64(srvflags.EVMMempoolGlobalQueue, cosmosevmserverconfig.DefaultMempoolConfig().GlobalQueue, "the maximum number of non-executable transaction slots for all accounts") cmd.Flags().Duration(srvflags.EVMMempoolLifetime, cosmosevmserverconfig.DefaultMempoolConfig().Lifetime, "the maximum amount of time non-executable transaction are queued") + cmd.Flags().Duration(srvflags.EVMMempoolCheckTxTimeout, cosmosevmserverconfig.DefaultMempoolConfig().CheckTxTimeout, "the maximum amount of time to wait for async mempool admission during CheckTx (0 disables the timeout)") cmd.Flags().Duration(srvflags.EVMMempoolPendingTxProposalTimeout, cosmosevmserverconfig.DefaultMempoolConfig().PendingTxProposalTimeout, "the maximum amount of time to spend waiting for rechecking of the mempool to complete when creating a proposal") cmd.Flags().Int(srvflags.EVMMempoolInsertQueueSize, cosmosevmserverconfig.DefaultMempoolConfig().InsertQueueSize, "the maximum number of transactions that can be in the insert queue at once") From 2a8c3e8296ae1b1585c0ad3ab2185ec72646bc0c Mon Sep 17 00:00:00 2001 From: Tyler <48813565+technicallyty@users.noreply.github.com> Date: Wed, 11 Mar 2026 10:57:59 -0700 Subject: [PATCH 2/7] Remove iavlx merge cflicts --- server/config/config.go | 4 ---- server/flags/flags.go | 3 --- 2 files changed, 7 deletions(-) diff --git a/server/config/config.go b/server/config/config.go index 4e0ed0a1c..5eae82bc5 100644 --- a/server/config/config.go +++ b/server/config/config.go @@ -131,10 +131,6 @@ var evmTracers = []string{"json", "markdown", "struct", "access_list"} type Config struct { config.Config `mapstructure:",squash"` - // UseIAVLX enables the IAVLX storage engine. - // todo: move to appropriate place? - UseIAVLX bool `mapstructure:"use-iavlx"` - EVM EVMConfig `mapstructure:"evm"` JSONRPC JSONRPCConfig `mapstructure:"json-rpc"` TLS TLSConfig `mapstructure:"tls"` diff --git a/server/flags/flags.go b/server/flags/flags.go index 49a073cff..397c25b94 100644 --- a/server/flags/flags.go +++ b/server/flags/flags.go @@ -84,9 +84,6 @@ const ( EVMMempoolInsertQueueSize = "evm.mempool.insert-queue-size" ) -// IAVLXEnable (experimental) enables the IAVLX storage engine. -const IAVLXEnable = "iavlx-enable" - // TLS flags const ( TLSCertPath = "tls.certificate-path" From 0ce99fc68d582d0bb369429e82cb5085b2563cec Mon Sep 17 00:00:00 2001 From: Tyler <48813565+technicallyty@users.noreply.github.com> Date: Wed, 11 Mar 2026 11:20:12 -0700 Subject: [PATCH 3/7] no rechecktx --- mempool/check_tx.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/mempool/check_tx.go b/mempool/check_tx.go index 21b5bbf5e..ca4523652 100644 --- a/mempool/check_tx.go +++ b/mempool/check_tx.go @@ -19,11 +19,6 @@ func NewCheckTxHandler(mempool *ExperimentalEVMMempool, debug bool, timeout time panic("invalid timeout CheckTxHandler timeout value") } return func(_ types.RunTx, request *abci.RequestCheckTx) (*abci.ResponseCheckTx, error) { - // TODO: do we even do recheck anymore? - if request.Type == abci.CheckTxType_Recheck { - return &abci.ResponseCheckTx{Code: abci.CodeTypeOK}, nil - } - tx, err := mempool.txConfig.TxDecoder()(request.Tx) if err != nil { return sdkerrors.ResponseCheckTxWithEvents(err, 0, 0, nil, debug), nil From 0169dc5fbf49c561e20a7abe70087758acb4d0e5 Mon Sep 17 00:00:00 2001 From: Tyler <48813565+technicallyty@users.noreply.github.com> Date: Thu, 12 Mar 2026 14:51:37 -0700 Subject: [PATCH 4/7] fix comments referring to 0 timeout --- server/config/config.go | 2 +- server/config/toml.go | 2 +- server/start.go | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/server/config/config.go b/server/config/config.go index 5eae82bc5..09e727dc3 100644 --- a/server/config/config.go +++ b/server/config/config.go @@ -172,7 +172,7 @@ type MempoolConfig struct { // Lifetime is the maximum amount of time non-executable transaction are queued Lifetime time.Duration `mapstructure:"lifetime"` // CheckTxTimeout is the maximum amount of time to wait for an async mempool - // insert result while serving CheckTx (0 disables the timeout) + // insert result while serving CheckTx (defaults to 30s) CheckTxTimeout time.Duration `mapstructure:"check-tx-timeout"` // PendingTxProposalTimeout is the amount of time to spend waiting for // rechecking of the mempool to complete when creating a proposal diff --git a/server/config/toml.go b/server/config/toml.go index 1f2171b47..312669d59 100644 --- a/server/config/toml.go +++ b/server/config/toml.go @@ -52,7 +52,7 @@ global-queue = {{ .EVM.Mempool.GlobalQueue }} # Lifetime is the maximum amount of time non-executable transaction are queued lifetime = "{{ .EVM.Mempool.Lifetime }}" -# CheckTxTimeout is the maximum amount of time to wait for async mempool admission during CheckTx (0 disables the timeout) +# CheckTxTimeout is the maximum amount of time to wait for async mempool admission during CheckTx (default: 30s, must be nonzero) check-tx-timeout = "{{ .EVM.Mempool.CheckTxTimeout }}" # PendingTxProposalTimeout is the amount of time to spend waiting for rechecking of the mempool to complete when creating a proposal diff --git a/server/start.go b/server/start.go index f101a5a7d..77ebaf084 100644 --- a/server/start.go +++ b/server/start.go @@ -231,10 +231,9 @@ which accepts a path for the resulting pprof file. cmd.Flags().Uint64(srvflags.EVMMempoolAccountQueue, cosmosevmserverconfig.DefaultMempoolConfig().AccountQueue, "the maximum number of non-executable transaction slots permitted per account") cmd.Flags().Uint64(srvflags.EVMMempoolGlobalQueue, cosmosevmserverconfig.DefaultMempoolConfig().GlobalQueue, "the maximum number of non-executable transaction slots for all accounts") cmd.Flags().Duration(srvflags.EVMMempoolLifetime, cosmosevmserverconfig.DefaultMempoolConfig().Lifetime, "the maximum amount of time non-executable transaction are queued") - cmd.Flags().Duration(srvflags.EVMMempoolCheckTxTimeout, cosmosevmserverconfig.DefaultMempoolConfig().CheckTxTimeout, "the maximum amount of time to wait for async mempool admission during CheckTx (0 disables the timeout)") + cmd.Flags().Duration(srvflags.EVMMempoolCheckTxTimeout, cosmosevmserverconfig.DefaultMempoolConfig().CheckTxTimeout, "the maximum amount of time to wait for async mempool admission during CheckTx (default: 30s, must be nonzero)") cmd.Flags().Duration(srvflags.EVMMempoolPendingTxProposalTimeout, cosmosevmserverconfig.DefaultMempoolConfig().PendingTxProposalTimeout, "the maximum amount of time to spend waiting for rechecking of the mempool to complete when creating a proposal") cmd.Flags().Int(srvflags.EVMMempoolInsertQueueSize, cosmosevmserverconfig.DefaultMempoolConfig().InsertQueueSize, "the maximum number of transactions that can be in the insert queue at once") - cmd.Flags().String(srvflags.TLSCertPath, "", "the cert.pem file path for the server TLS configuration") cmd.Flags().String(srvflags.TLSKeyPath, "", "the key.pem file path for the server TLS configuration") From b26a6bd1520540d47e867051001cc03d9f737334 Mon Sep 17 00:00:00 2001 From: Tyler <48813565+technicallyty@users.noreply.github.com> Date: Thu, 12 Mar 2026 14:52:53 -0700 Subject: [PATCH 5/7] fix pointless err resassignment --- mempool/check_tx.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/mempool/check_tx.go b/mempool/check_tx.go index ca4523652..1471b80ff 100644 --- a/mempool/check_tx.go +++ b/mempool/check_tx.go @@ -2,7 +2,6 @@ package mempool import ( "context" - "errors" "time" abci "github.com/cometbft/cometbft/abci/types" @@ -27,9 +26,6 @@ func NewCheckTxHandler(mempool *ExperimentalEVMMempool, debug bool, timeout time ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() if err := mempool.Insert(ctx, tx); err != nil { - if errors.Is(err, context.DeadlineExceeded) { - err = ctx.Err() - } return sdkerrors.ResponseCheckTxWithEvents(err, 0, 0, nil, debug), nil } return &abci.ResponseCheckTx{Code: abci.CodeTypeOK}, nil From 4fe146691032d31202d1d8e34578b5496498bfcc Mon Sep 17 00:00:00 2001 From: Tyler <48813565+technicallyty@users.noreply.github.com> Date: Thu, 12 Mar 2026 14:53:26 -0700 Subject: [PATCH 6/7] checktxtimeout must be nonzero --- server/config/config.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/config/config.go b/server/config/config.go index 09e727dc3..59bd9785d 100644 --- a/server/config/config.go +++ b/server/config/config.go @@ -221,8 +221,8 @@ func (c MempoolConfig) Validate() error { if c.Lifetime < 1 { return fmt.Errorf("lifetime must be at least 1 nanosecond, got %s", c.Lifetime) } - if c.CheckTxTimeout < 0 { - return fmt.Errorf("check tx timeout must be non-negative, got %s", c.CheckTxTimeout) + if c.CheckTxTimeout <= 0 { + return fmt.Errorf("check tx timeout must be non-zero, got %s", c.CheckTxTimeout) } if c.InsertQueueSize < 1 { return fmt.Errorf("insert queue size must be at least 1, got %d", c.InsertQueueSize) From 9dd5c85d0e352a2087074064001123291ca45ddd Mon Sep 17 00:00:00 2001 From: Tyler <48813565+technicallyty@users.noreply.github.com> Date: Thu, 12 Mar 2026 15:19:24 -0700 Subject: [PATCH 7/7] panic on recheck --- mempool/check_tx.go | 3 +++ mempool/check_tx_test.go | 7 ++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/mempool/check_tx.go b/mempool/check_tx.go index 1471b80ff..1dd9d59ab 100644 --- a/mempool/check_tx.go +++ b/mempool/check_tx.go @@ -18,6 +18,9 @@ func NewCheckTxHandler(mempool *ExperimentalEVMMempool, debug bool, timeout time panic("invalid timeout CheckTxHandler timeout value") } return func(_ types.RunTx, request *abci.RequestCheckTx) (*abci.ResponseCheckTx, error) { + if request.GetType() == abci.CheckTxType_Recheck { + panic("checkTx does not support recheck") + } tx, err := mempool.txConfig.TxDecoder()(request.Tx) if err != nil { return sdkerrors.ResponseCheckTxWithEvents(err, 0, 0, nil, debug), nil diff --git a/mempool/check_tx_test.go b/mempool/check_tx_test.go index 78c44959e..19bf8ae19 100644 --- a/mempool/check_tx_test.go +++ b/mempool/check_tx_test.go @@ -6,6 +6,7 @@ import ( "time" ethcore "github.com/ethereum/go-ethereum/core" + "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" abci "github.com/cometbft/cometbft/abci/types" @@ -337,7 +338,7 @@ func (s *CheckTxHandlerTestSuite) TestRecheckIsNoOp() { mp := setupMempoolWithAccounts(s.T(), 1) handler := mempool.NewCheckTxHandler(mp.mp, false, time.Minute) - res := s.submitCheckTx(handler, []byte("not-a-real-tx"), abci.CheckTxType_Recheck) - s.Equal(abci.CodeTypeOK, res.Code) - s.Equal(0, mp.mp.CountTx()) + require.Panics(s.T(), func() { + s.submitCheckTx(handler, []byte("not-a-real-tx"), abci.CheckTxType_Recheck) + }) }