How Neo's MemoryPool Handles Transaction Verification and Fee Estimation

Neo’s MemoryPool verifies transactions through state-dependent checks in TryAdd, prioritizes them by fee-per-byte in a sorted set, and estimates network fees based on transaction size and policy rates before eviction of low-fee transactions when capacity is exceeded.

The MemoryPool in the neo-project/neo repository serves as the temporary holding area for transactions that have passed initial validation but await inclusion in a block. Understanding how Neo's MemoryPool handles transaction verification and fee estimation is critical for developers building wallets, RPC services, or consensus mechanisms on the Neo N3 blockchain.

Transaction Verification Flow in Neo's MemoryPool

State-Dependent Verification via TryAdd

When a transaction enters the pool, the MemoryPool.TryAdd method orchestrates the verification pipeline. Located in src/Neo/Ledger/MemoryPool.cs (lines 13–71), this method first checks for an optional NewTransaction policy handler that can reject transactions based on custom rules.

The core verification occurs through tx.VerifyStateDependent(settings, snapshot, VerificationContext, conflicts). Unlike state-independent checks (signatures, format) that happen earlier in Transaction.Verify(), state-dependent verification ensures the sender has sufficient balance, valid claims, and contract-specific requirements against the current blockchain snapshot.

Conflict Detection and Fee Comparison

Before state verification, CheckConflicts (MemoryPool.cs lines 81–110) ensures the transaction does not conflict with already-pooled transactions. If conflicts exist, the new transaction must offer a higher network fee than the sum of all conflicting transactions' fees to replace them.

This mechanism prevents spam and ensures that higher-paying transactions can bump lower-paying ones when they share inputs or conflicts.

TransactionVerificationContext for Balance Tracking

The TransactionVerificationContext class (located in src/Neo/Ledger/TransactionVerificationContext.cs) tracks accumulated fees per sender across multiple pooled transactions. When AddTransaction(tx) is called (lines 45–48), it aggregates the sender's SystemFee + NetworkFee to prevent double-spending scenarios where a user might submit multiple transactions that collectively exceed their balance.

Fee Estimation and Prioritization Mechanisms

Calculating Network Fees Before Pool Entry

Neo calculates the network fee before a transaction reaches the MemoryPool using the formula implemented in WalletHelper.CalculateNetworkFee (src/Neo/Wallets/Helper.cs, lines 95–115):


NetworkFee = size * Policy.GetFeePerByte(snapshot) + sum(attribute fees) + excess

  • Size: Transaction.Size determines the byte-length cost
  • FeePerByte: Retrieved from native Policy contract
  • Attribute fees: Special attributes like Conflicts or NotaryAssisted add additional costs via their CalculateNetworkFee implementations

This calculation ensures users pay for the computational and storage resources their transactions consume.

PoolItem Comparison and Fee-per-Byte Ordering

Once in the pool, transactions are wrapped in PoolItem objects (src/Neo/Ledger/PoolItem.cs). The CompareTo method (lines 47–60) implements a multi-level sorting strategy:

  1. High-priority flag (transactions with this flag come first)
  2. Fee-per-byte (NetworkFee / Size) - higher is better
  3. Network fee (absolute amount) as tie-breaker (lines 55–57)
  4. Transaction hash (for deterministic ordering)

This sorting ensures that miners and consensus nodes select the most profitable transactions when building blocks.

Capacity Management and Low-Fee Eviction

The MemoryPool enforces a Capacity limit. When TryAdd would exceed this limit, RemoveOverCapacity (MemoryPool.cs lines 16–34) evicts the lowest-fee transaction retrieved via GetLowestFeeTransaction.

This eviction mechanism uses the same PoolItem comparison logic, meaning transactions with the lowest fee-per-byte are removed first, maintaining the pool's economic efficiency.

Re-verification After Block Persistence

Invalidating Verified Transactions

After a block is persisted to the blockchain, the MemoryPool must re-verify all transactions because the state snapshot has changed (balances, nonces, contract storage). The InvalidateVerifiedTransactions method (MemoryPool.cs lines 87–100) moves all transactions from the verified sets (_unsortedTransactions, _sortedTransactions) to the _unverifiedTransactions dictionary.

Bounded Re-verification Process

The ReverifyTransactions method (MemoryPool.cs lines 83–115) processes unverified transactions in batches constrained by MaxMillisecondsToReverifyTx or MaxMillisecondsToReverifyTxPerIdle. This prevents the node from freezing during large re-verification tasks.

Each candidate transaction undergoes VerifyStateDependent again. Successful transactions are promoted back to the verified containers, and their fees are re-added to the VerificationContext. Failed transactions are permanently removed from the pool.

Code Examples for MemoryPool Operations

Adding a Transaction to the Memory Pool

var system = NeoSystem.Create(settings, logger);
var pool   = system.MemPool;                // MemoryPool instance
var snapshot = system.Store.GetSnapshot();   // DataCache snapshot
var tx = new Transaction { /* fill fields */ };

var result = pool.TryAdd(tx, snapshot);
if (result == VerifyResult.Succeed)
    Console.WriteLine("Transaction accepted into mempool");
else
    Console.WriteLine($"Rejected: {result}");

TryAdd runs the policy handler, conflict checks, and VerifyStateDependent.

Checking Whether a Transaction Fits in the Pool

bool canFit = pool.CanTransactionFitInPool(tx);
Console.WriteLine(canFit ? "Fits" : "Would overflow pool");

Uses the lowest-fee transaction as a benchmark (GetLowestFeeTransaction).

Retrieving the Highest-Fee Verified Transactions

Transaction[] topTxes = pool.GetSortedVerifiedTransactions(count: 10);
foreach (var t in topTxes)
    Console.WriteLine($"{t.Hash} – fee per byte: {t.FeePerByte}");

The sorted set already orders by FeePerByte then NetworkFee.

Re-verifying Stale Transactions After a Block

// After block persistence
pool.UpdatePoolForBlockPersisted(persistedBlock, snapshot);
// The pool will automatically re-verify eligible unverified transactions.

Summary

  • Neo’s MemoryPool stores transactions that have passed state-dependent verification in sorted containers (_sortedTransactions), ordered by fee-per-byte and absolute network fee.
  • Verification occurs in MemoryPool.TryAdd through CheckConflicts (ensuring higher fees replace conflicting transactions) and Transaction.VerifyStateDependent (validating balances against the current snapshot).
  • Fee estimation happens before pool entry via WalletHelper.CalculateNetworkFee, computing costs based on transaction size, per-byte policy rates, and attribute-specific fees.
  • Capacity management evicts the lowest fee-per-byte transactions when the pool exceeds its limit, using PoolItem.CompareTo to identify economically inefficient entries.
  • Re-verification after block persistence invalidates all verified transactions and re-checks them against the new state snapshot in time-bounded batches.

Frequently Asked Questions

How does Neo's MemoryPool prevent double-spending across multiple pending transactions?

The TransactionVerificationContext class tracks the cumulative SystemFee and NetworkFee for each sender across all transactions currently in the pool. When AddTransaction is called (TransactionVerificationContext.cs lines 45–48), it aggregates these fees and validates that the sender's balance covers the total amount. If a new transaction would cause the sender to overspend, VerifyStateDependent returns a failure result and the transaction is rejected from the mempool.

What determines the order in which transactions are selected for block inclusion?

Transactions are ordered by the PoolItem.CompareTo method (PoolItem.cs lines 47–60) using a hierarchical comparison: high-priority transactions come first, followed by those with higher fee-per-byte (NetworkFee / Size), then higher absolute network fee as a tie-breaker, and finally by transaction hash for deterministic ordering. This ensures miners select the most economically valuable transactions when building blocks.

How does the MemoryPool handle transactions that become invalid after a block is persisted?

After block persistence, InvalidateVerifiedTransactions (MemoryPool.cs lines 87–100) moves all previously verified transactions to an unverified collection. The ReverifyTransactions method (MemoryPool.cs lines 83–115) then processes these in time-bounded batches (controlled by MaxMillisecondsToReverifyTx), re-running VerifyStateDependent against the updated blockchain snapshot. Transactions that fail re-verification are permanently removed, while successful ones return to the verified sorted set.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →