Transaction Pool (Mempool)

Managing pending transactions before they're included in blocks.

Advanced⏱️ 50 min📚 Prerequisites: 1

Transaction Pool (Mempool)

The transaction pool (mempool) stores pending transactions waiting to be included in blocks. It's critical for blockchain performance.

Pool Structure

RUST
struct TransactionPool {
    pending: Vec<Transaction>,
    by_sender: HashMap<String, Vec<Transaction>>,
    by_fee: BTreeMap<u64, Vec<Transaction>>,  // Sorted by fee
    max_size: usize,
}

Pool Operations

Adding Transactions

RUST
fn add_transaction(&mut self, tx: Transaction) -> Result<(), String> {
    // Validate transaction
    if !self.validate(&tx) {
        return Err(String::from("Invalid transaction"));
    }
    
    // Check nonce ordering
    if !self.check_nonce(&tx) {
        return Err(String::from("Invalid nonce"));
    }
    
    // Add to pool
    self.pending.push(tx);
    Ok(())
}

Selecting Transactions

RUST
fn select_for_block(&mut self, max_gas: u64) -> Vec<Transaction> {
    // Select transactions with highest fees
    // Respect gas limits
    // Maintain nonce ordering
    // Return selected transactions
}

Transaction Ordering

  • Fee priority: Higher fee = higher priority
  • Nonce ordering: Must process in order per sender
  • Gas limits: Respect block gas limit

Pool Management

  • Eviction: Remove old/low-fee transactions
  • Replacement: Replace with higher fee (same nonce)
  • Cleanup: Remove included transactions
  • Validation: Re-validate on state changes

Nonce Management

RUST
struct NonceTracker {
    current_nonces: HashMap<String, u64>,
    pending_nonces: HashMap<String, HashSet<u64>>,
}

impl NonceTracker {
    fn can_add(&self, sender: &str, nonce: u64) -> bool {
        let current = self.current_nonces.get(sender).copied().unwrap_or(0);
        nonce == current || self.pending_nonces.get(sender)
            .map(|set| set.contains(&(nonce - 1)))
            .unwrap_or(false)
    }
}

Performance Considerations

  • Indexing: Fast lookups by sender, fee, etc.
  • Sorting: Keep transactions sorted by priority
  • Memory limits: Prevent pool from growing too large
  • Concurrent access: Thread-safe operations

Code Examples

Transaction Pool

Basic transaction pool implementation

RUST
use std::collections::HashMap;
struct Transaction {
    from: String,
    to: String,
    amount: u64,
    fee: u64,
    nonce: u64,
}
struct TransactionPool {
    pending: Vec<Transaction>,
    max_size: usize,
}
impl TransactionPool {
    fn new(max_size: usize) -> Self {
        TransactionPool {
            pending: Vec::new(),
            max_size,
        }
    }
    fn add(&mut self, tx: Transaction) -> Result<(), String> {
        if self.pending.len() >= self.max_size {
            return Err(String::from("Pool is full"));
        }
        self.pending.push(tx);
        Ok(())
    }
    fn select_best(&mut self, count: usize) -> Vec<Transaction> {
        self.pending.sort_by(|a, b| b.fee.cmp(&a.fee));
        self.pending.drain(..count.min(self.pending.len())).collect()
    }
    fn size(&self) -> usize {
        self.pending.len()
    }
}
fn main() {
    let mut pool = TransactionPool::new(100);
    pool.add(Transaction {
        from: String::from("Alice"),
        to: String::from("Bob"),
        amount: 100,
        fee: 10,
        nonce: 1,
    }).unwrap();
    pool.add(Transaction {
        from: String::from("Charlie"),
        to: String::from("Dave"),
        amount: 200,
        fee: 20,
        nonce: 1,
    }).unwrap();
    println!("Pool size: {}", pool.size());
    let selected = pool.select_best(1);
    println!("Selected transaction with fee: {}", selected[0].fee);
}

Explanation:

A transaction pool stores pending transactions. It selects transactions based on priority (typically fee) for inclusion in blocks.

Fee Priority

Selecting transactions by fee

RUST
struct Transaction {
    id: String,
    fee: u64,
}
fn select_by_fee(transactions: &mut Vec<Transaction>, max_count: usize) -> Vec<Transaction> {
    transactions.sort_by(|a, b| b.fee.cmp(&a.fee));
    transactions.iter()
        .take(max_count)
        .cloned()
        .collect()
}
fn main() {
    let mut txs = vec![
        Transaction { id: String::from("tx1"), fee: 5 },
        Transaction { id: String::from("tx2"), fee: 20 },
        Transaction { id: String::from("tx3"), fee: 10 },
        Transaction { id: String::from("tx4"), fee: 15 },
    ];
    let selected = select_by_fee(&mut txs, 2);
    for tx in selected {
        println!("Selected: {} (fee: {})", tx.id, tx.fee);
    }
}

Explanation:

Transactions are typically selected by fee priority. Higher fee transactions are included first, incentivizing users to pay higher fees for faster inclusion.

Exercises

Transaction Pool

Create a transaction pool that selects highest fee transactions!

Medium

Starter Code:

RUST
struct Transaction {
    fee: u64,
    data: String,
}
struct Pool {
    transactions: Vec<Transaction>,
}
fn main() {
    let mut pool = Pool::new();
    pool.add(Transaction { fee: 10, data: String::from("tx1") });
    pool.add(Transaction { fee: 30, data: String::from("tx2") });
    pool.add(Transaction { fee: 20, data: String::from("tx3") });
    let top = pool.select_top(2);
    println!("Selected {} transactions", top.len());
}