This mempool implementation is experimental and under active development. It is intended for testing and evaluation purposes. Use in production environments is not recommended without thorough testing and risk assessment. Please report issues and submit feedback to help improve stability.

Overview

This guide explains how to integrate the EVM mempool in your Cosmos SDK chain to enable Ethereum-compatible transaction flows, including out-of-order transactions and nonce gap handling.

Prerequisites

Before integrating the EVM mempool:
  1. EVM Module Integration: Complete the EVM module integration first
  2. FeeMarket Module: Ensure the feemarket module is properly configured for base fee calculations
  3. Compatible AnteHandler: Your ante handler must support EVM transaction validation

Quick Start

Step 1: Add EVM Mempool to App Struct

Update your app/app.go to include the EVM mempool:
type App struct {
    *baseapp.BaseApp
    // ... other keepers
    
    // Cosmos EVM keepers
    FeeMarketKeeper   feemarketkeeper.Keeper
    EVMKeeper         *evmkeeper.Keeper
    EVMMempool        *evmmempool.ExperimentalEVMMempool
}

Step 2: Configure Mempool in NewApp Constructor

The mempool must be initialized after the antehandler has been set in the app.
Add the following configuration in your NewApp constructor:
// Set the EVM priority nonce mempool
if evmtypes.GetChainConfig() != nil {
    mempoolConfig := &evmmempool.EVMMempoolConfig{
        AnteHandler:   app.GetAnteHandler(),
        BlockGasLimit: 100_000_000,
    }

    evmMempool := evmmempool.NewExperimentalEVMMempool(
        app.CreateQueryContext, 
        logger, 
        app.EVMKeeper, 
        app.FeeMarketKeeper, 
        app.txConfig, 
        app.clientCtx, 
        mempoolConfig,
    )
    app.EVMMempool = evmMempool

    // Set the global mempool for RPC access
    if err := evmmempool.SetGlobalEVMMempool(evmMempool); err != nil {
        panic(err)
    }
    
    // Replace BaseApp mempool
    app.SetMempool(evmMempool)
    
    // Set custom CheckTx handler for nonce gap support
    checkTxHandler := evmmempool.NewCheckTxHandler(evmMempool)
    app.SetCheckTxHandler(checkTxHandler)

    // Set custom PrepareProposal handler
    abciProposalHandler := baseapp.NewDefaultProposalHandler(evmMempool, app)
    abciProposalHandler.SetSignerExtractionAdapter(
        evmmempool.NewEthSignerExtractionAdapter(
            sdkmempool.NewDefaultSignerExtractionAdapter(),
        ),
    )
    app.SetPrepareProposal(abciProposalHandler.PrepareProposalHandler())
}

Configuration Options

The EVMMempoolConfig struct provides several configuration options for customizing the mempool behavior:

Minimal Configuration

mempoolConfig := &evmmempool.EVMMempoolConfig{
    AnteHandler:   app.GetAnteHandler(),
    BlockGasLimit: 100_000_000, // 100M gas limit
}

Full Configuration Options

type EVMMempoolConfig struct {
    // Required: AnteHandler for transaction validation
    AnteHandler   sdk.AnteHandler
    
    // Required: Block gas limit for transaction selection
    BlockGasLimit uint64
    
    // Optional: Custom TxPool (defaults to LegacyPool)
    TxPool        *txpool.TxPool
    
    // Optional: Custom Cosmos pool (defaults to PriorityNonceMempool)  
    CosmosPool    sdkmempool.ExtMempool
    
    // Optional: Custom broadcast function for promoted transactions
    BroadCastTxFn func(txs []*ethtypes.Transaction) error
}

Custom Cosmos Mempool Configuration

The mempool uses a PriorityNonceMempool for Cosmos transactions by default. You can customize the priority calculation:
// Define custom priority calculation for Cosmos transactions
priorityConfig := sdkmempool.PriorityNonceMempoolConfig[math.Int]{
    TxPriority: sdkmempool.TxPriority[math.Int]{
        GetTxPriority: func(goCtx context.Context, tx sdk.Tx) math.Int {
            feeTx, ok := tx.(sdk.FeeTx)
            if !ok {
                return math.ZeroInt()
            }
            
            // Get fee in bond denomination
            bondDenom := "uatom" // or your chain's bond denom
            fee := feeTx.GetFee()
            found, coin := fee.Find(bondDenom)
            if !found {
                return math.ZeroInt()
            }
            
            // Calculate gas price: fee_amount / gas_limit
            gasPrice := coin.Amount.Quo(math.NewIntFromUint64(feeTx.GetGas()))
            return gasPrice
        },
        Compare: func(a, b math.Int) int {
            return a.BigInt().Cmp(b.BigInt()) // Higher values have priority
        },
        MinValue: math.ZeroInt(),
    },
}

mempoolConfig := &evmmempool.EVMMempoolConfig{
    AnteHandler:   app.GetAnteHandler(),
    BlockGasLimit: 100_000_000,
    CosmosPool:    sdkmempool.NewPriorityMempool(priorityConfig),
}

Custom Block Gas Limit

Different chains may require different gas limits based on their capacity:
// Example: 50M gas limit for lower capacity chains
mempoolConfig := &evmmempool.EVMMempoolConfig{
    AnteHandler:   app.GetAnteHandler(),
    BlockGasLimit: 50_000_000,
}

Architecture Components

The EVM mempool consists of several key components:

ExperimentalEVMMempool

The main coordinator implementing Cosmos SDK’s ExtMempool interface (mempool/mempool.go). Key Methods:
  • Insert(ctx, tx): Routes transactions to appropriate pools
  • Select(ctx, filter): Returns unified iterator over all transactions
  • Remove(tx): Handles transaction removal with EVM-specific logic
  • InsertInvalidNonce(txBytes): Queues nonce-gapped EVM transactions locally

CheckTx Handler

Custom transaction validation that handles nonce gaps specially (mempool/check_tx.go). Special Handling: On ErrNonceGap for EVM transactions:
if errors.Is(err, ErrNonceGap) {
    // Route to local queue instead of rejecting
    err := mempool.InsertInvalidNonce(request.Tx)
    // Must intercept error and return success to EVM client
    return interceptedSuccess
}

TxPool

Direct port of Ethereum’s transaction pool managing both pending and queued transactions (mempool/txpool/). Key Features:
  • Uses vm.StateDB interface for Cosmos state compatibility
  • Implements BroadcastTxFn callback for transaction promotion
  • Cosmos-specific reset logic for instant finality

PriorityNonceMempool

Standard Cosmos SDK mempool for non-EVM transactions with fee-based prioritization. Default Priority Calculation:
// Calculate effective gas price
priority = (fee_amount / gas_limit) - base_fee

Transaction Type Routing

The mempool handles different transaction types appropriately:

Ethereum Transactions (MsgEthereumTx)

  • Tier 1 (Local): EVM TxPool handles nonce gaps and promotion
  • Tier 2 (Network): CometBFT broadcasts executable transactions

Cosmos Transactions (Bank, Staking, Gov, etc.)

  • Direct to Tier 2: Always go directly to CometBFT mempool
  • Standard Flow: Follow normal Cosmos SDK validation and broadcasting
  • Priority-Based: Use PriorityNonceMempool for fee-based ordering

Unified Transaction Selection

During block building, both transaction types compete fairly based on their effective tips:
// Simplified selection logic
func SelectTransactions() Iterator {
    evmTxs := GetPendingEVMTransactions()      // From local TxPool
    cosmosTxs := GetPendingCosmosTransactions() // From Cosmos mempool
    
    return NewUnifiedIterator(evmTxs, cosmosTxs) // Fee-based priority
}
Fee Comparison:
  • EVM: gas_tip_cap or min(gas_tip_cap, gas_fee_cap - base_fee)
  • Cosmos: (fee_amount / gas_limit) - base_fee
  • Selection: Higher effective tip gets selected first

Testing Your Integration

Verify Nonce Gap Handling

Test that transactions with nonce gaps are properly queued:
// Send transactions out of order
await wallet.sendTransaction({nonce: 100, ...}); // OK: Immediate execution
await wallet.sendTransaction({nonce: 102, ...}); // OK: Queued locally (gap)
await wallet.sendTransaction({nonce: 101, ...}); // OK: Fills gap, both execute

Test Transaction Replacement

Verify that higher-fee transactions replace lower-fee ones:
// Send initial transaction
const tx1 = await wallet.sendTransaction({
  nonce: 100,
  gasPrice: parseUnits("20", "gwei")
});

// Replace with higher fee
const tx2 = await wallet.sendTransaction({
  nonce: 100, // Same nonce
  gasPrice: parseUnits("30", "gwei") // Higher fee
});
// tx1 is replaced by tx2

Verify Batch Deployments

Test typical deployment scripts (like Uniswap) that send many transactions at once:
// Deploy multiple contracts in quick succession
const factory = await Factory.deploy();
const router = await Router.deploy(factory.address);
const multicall = await Multicall.deploy();
// All transactions should queue and execute properly

Monitoring and Debugging

Use the txpool RPC methods to monitor mempool state:
  • txpool_status: Get pending and queued transaction counts
  • txpool_content: View all transactions in the pool
  • txpool_inspect: Get human-readable transaction summaries
  • txpool_contentFrom: View transactions from specific addresses

Common Issues and Solutions

Transactions Not Being Queued

Issue: Transactions with nonce gaps are rejected instead of queued. Solution: Ensure the CheckTx handler is properly configured:
checkTxHandler := evmmempool.NewCheckTxHandler(evmMempool)
app.SetCheckTxHandler(checkTxHandler)

Mempool Not Accepting Transactions

Issue: Mempool requires block 1+ before accepting transactions. Solution: Ensure your chain has produced at least one block before testing.

RPC Methods Not Available

Issue: txpool_* methods return “method not found”. Solution: Set the global mempool for RPC access:
if err := evmmempool.SetGlobalEVMMempool(evmMempool); err != nil {
    panic(err)
}

Transaction Promotion Not Working

Issue: Queued transactions aren’t promoted when gaps are filled. Solution: Verify the BroadcastTxFn callback is configured and the promotion background process is running.