Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion evmd/mempool.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
53 changes: 20 additions & 33 deletions mempool/check_tx.go
Original file line number Diff line number Diff line change
@@ -1,49 +1,36 @@
package mempool

import (
"errors"
"context"
"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)
// 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) {
if request.GetType() == abci.CheckTxType_Recheck {
panic("checkTx does not support recheck")
}
tx, err := mempool.txConfig.TxDecoder()(request.Tx)
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

were the anteEvents also always nil here? we are missing out on events now as well with this? should we modify the response of mempool.Insert to return some of this info?

Copy link
Contributor Author

@technicallyty technicallyty Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i checked on v0.53.x and main, calling broadcast tx sync never returned anything other than code, tx hash, and a log if it failed.

you can see comet stripping down the response here: https://github.com/cometbft/cometbft/blob/1bb8b386fc366bc4655dede0535e16d1ad669c7d/rpc/core/mempool.go#L58-L64

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

got it, tested on gaia as well earlier and this was the case too

}
}
// 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
}

// anything else, return regular error
return sdkerrors.ResponseCheckTxWithEvents(err, gInfo.GasWanted, gInfo.GasUsed, anteEvents, false), 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 {
return sdkerrors.ResponseCheckTxWithEvents(err, 0, 0, nil, debug), nil
}
return &abci.ResponseCheckTx{Code: abci.CodeTypeOK}, nil
}
Comment on lines +20 to 35
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Recheck requests not short-circuited — breaks TestRecheckIsNoOp and risks tx eviction

The handler does not inspect request.Type, so abci.CheckTxType_Recheck requests take the same code path as new transactions.

Two concrete failure modes follow:

  1. TestRecheckIsNoOp will fail. The test submits []byte("not-a-real-tx") with CheckTxType_Recheck and asserts CodeTypeOK. With the current code the decoder will return an error and sdkerrors.ResponseCheckTxWithEvents will produce a non-CodeTypeOK response.

  2. Valid in-mempool transactions get evicted during recheck. When CometBFT fires a recheck for a tx that is already in the pool, mempool.Insert will return ErrAlreadyKnown (non-OK). CometBFT interprets a non-OK recheck response as the transaction having become invalid and removes it from its tracking — silently draining the mempool.

Since the app sets OperateExclusively = true, CometBFT delegates full mempool management to the application layer. Recheck is therefore a no-op from CometBFT's perspective and should always return CodeTypeOK without hitting the insert worker:

return func(_ types.RunTx, request *abci.RequestCheckTx) (*abci.ResponseCheckTx, error) {
    if request.Type == abci.CheckTxType_Recheck {
        return &abci.ResponseCheckTx{Code: abci.CodeTypeOK}, nil
    }
    tx, err := mempool.txConfig.TxDecoder()(request.Tx)
    ...
}

}
Loading
Loading