Skip to main content

Overview

This guide provides step-by-step instructions for integrating the EVM mempool into your Cosmos EVM chain. For conceptual information about mempool design and architecture, see the Mempool Concepts page.

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, // or 0 to use default
    }

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

    // 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())
}
Breaking Change from v0.4.x: The global mempool registry (SetGlobalEVMMempool) has been removed. Mempool is now passed directly to the JSON-RPC server during initialization.

Configuration Options

The EVMMempoolConfig struct provides several configuration options for customizing the mempool behavior:
  • Minimal Configuration
  • Full Configuration Options
For most use cases, the minimal configuration is sufficient:
mempoolConfig := &evmmempool.EVMMempoolConfig{
    AnteHandler:   app.GetAnteHandler(),
    BlockGasLimit: 100_000_000, // or 0 to use default
}

Defaults and Fallbacks

  • If BlockGasLimit is 0, the mempool uses a fallback of 100_000_000 gas.
  • If LegacyPoolConfig is not provided, defaults from legacypool.DefaultConfig are used.
  • If CosmosPoolConfig is not provided, a default PriorityNonceMempool is created with:
    • Priority = (fee_amount / gas_limit) in the chain bond denom
    • Comparator = big-int comparison (higher is selected first)
    • MinValue = 0
  • If BroadcastTxFn is not provided, a default is created that uses the app clientCtx/txConfig to broadcast EVM transactions when they are promoted from queued → pending.
  • MinTip is optional. If unset, selection uses the effective tip from each tx (min(gas_tip_cap, gas_fee_cap - base_fee)).

v0.4.x to v0.5.0 Migration

Breaking Change: Pre-built pools replaced with configuration objects PR #496 replaced pre-built pools with configs in EVMMempoolConfig:
  • Removed: TxPool *txpool.TxPool, CosmosPool sdkmempool.ExtMempool
  • Added: LegacyPoolConfig *legacypool.Config, CosmosPoolConfig *sdkmempool.PriorityNonceMempoolConfig[math.Int]

Minimal Setups: Nothing to Change

If you use the default mempool wiring (no custom pools), your existing code continues to work:
mempoolConfig := &evmmempool.EVMMempoolConfig{
    AnteHandler:   app.GetAnteHandler(),
    BlockGasLimit: 100_000_000, // or 0 to use default
}
evmMempool := evmmempool.NewExperimentalEVMMempool(
    app.CreateQueryContext, logger, app.EVMKeeper, app.FeeMarketKeeper,
    app.txConfig, app.clientCtx, mempoolConfig
)

Advanced Setups: Migrate Your Customizations

If you built custom pools yourself, replace them with configuration objects: Before (v0.4.x):
mempoolConfig := &evmmempool.EVMMempoolConfig{
    TxPool:     customTxPool,      // ← REMOVED
    CosmosPool: customCosmosPool,  // ← REMOVED
    AnteHandler:   app.GetAnteHandler(),
    BlockGasLimit: 100_000_000,
}
After (v0.5.0):
mempoolConfig := &evmmempool.EVMMempoolConfig{
    LegacyPoolConfig: &legacyCfg,  // ← NEW (or nil for defaults)
    CosmosPoolConfig: &cosmosCfg,  // ← NEW (or nil for defaults)
    AnteHandler:      app.GetAnteHandler(),
    BlockGasLimit:    100_000_000,
}

Custom Legacy Pool Configuration

Customize EVM transaction pool parameters:
// EVM legacy txpool tuning
legacyCfg := legacypool.DefaultConfig
legacyCfg.PriceLimit = 2              // Minimum gas price (wei)
legacyCfg.PriceBump = 15              // 15% price bump to replace
legacyCfg.AccountSlots = 32           // Slots per account
legacyCfg.GlobalSlots = 10240         // Total executable slots
legacyCfg.AccountQueue = 128          // Non-executable per account
legacyCfg.GlobalQueue = 2048          // Total non-executable
legacyCfg.Lifetime = 6 * time.Hour    // Max queue time

mempoolConfig.LegacyPoolConfig = &legacyCfg

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
cosmosCfg := sdkmempool.PriorityNonceMempoolConfig[math.Int]{}
cosmosCfg.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.CosmosPoolConfig = &cosmosCfg

Custom Broadcast Function

Override the default broadcast behavior for promoted EVM transactions:
// Custom EVM broadcast (optional)
mempoolConfig.BroadcastTxFn = func(txs []*ethtypes.Transaction) error {
    // Custom logic for broadcasting promoted transactions
    return nil
}

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

Event Bus Integration

For best results, connect the mempool to CometBFT’s EventBus so it can react to finalized blocks:
// After starting the CometBFT node
if m, ok := app.GetMempool().(*evmmempool.ExperimentalEVMMempool); ok {
    m.SetEventBus(bftNode.EventBus())
}
This enables chain-head notifications so the mempool can promptly promote/evict transactions when blocks are committed.

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
I