Skip to content

Instantly share code, notes, and snippets.

@sandybradley
Created December 4, 2025 16:07
Show Gist options
  • Select an option

  • Save sandybradley/314de0a7a973b8491d98b9f7d83030ba to your computer and use it in GitHub Desktop.

Select an option

Save sandybradley/314de0a7a973b8491d98b9f7d83030ba to your computer and use it in GitHub Desktop.

Private mempool - reth patch

Reth Private Mempool Implementation Plan

Architecture Overview

We'll extend reth's transaction pool to support a segregated private mempool with access controls, while ensuring private transactions don't leak to the public network.

Core Infrastructure

Private Transaction Pool Foundation

File: crates/transaction-pool/src/pool/mod.rs

use std::collections::{HashMap, HashSet};
use reth_primitives::{Address, TxHash, B256};
use tokio::sync::RwLock;

#[derive(Debug, Clone)]
pub struct PrivateTransaction {
    pub transaction: PooledTransaction,
    pub submitter: Address,
    pub submission_time: u64,
    pub priority_fee: u128,
}

#[derive(Debug)]
pub struct PrivateMempool {
    /// Private transactions by hash
    private_txs: HashMap<TxHash, PrivateTransaction>,
    /// Authorized addresses that can submit private transactions
    authorized_submitters: HashSet<Address>,
    /// Max private transactions to store
    max_private_txs: usize,
    /// Access control for authentication
    access_control: Arc<AccessControl>,
}

impl PrivateMempool {
    pub fn new(max_private_txs: usize) -> Self {
        Self {
            private_txs: HashMap::new(),
            authorized_submitters: HashSet::new(),
            max_private_txs,
            access_control: Arc::new(AccessControl::new()),
        }
    }

    pub fn add_private_transaction(
        &mut self,
        tx: PooledTransaction,
        submitter: Address,
    ) -> Result<TxHash, PrivateMempoolError> {
        // Validate authorization
        if !self.is_authorized(&submitter) {
            return Err(PrivateMempoolError::Unauthorized);
        }

        // Check capacity
        if self.private_txs.len() >= self.max_private_txs {
            self.evict_oldest();
        }

        let tx_hash = tx.hash();
        let private_tx = PrivateTransaction {
            transaction: tx,
            submitter,
            submission_time: SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(),
            priority_fee: tx.max_priority_fee_per_gas().unwrap_or_default(),
        };

        self.private_txs.insert(tx_hash, private_tx);
        Ok(tx_hash)
    }

    pub fn get_private_transactions_for_block(&self, limit: usize) -> Vec<PooledTransaction> {
        let mut txs: Vec<_> = self.private_txs.values().collect();
        // Sort by priority fee (descending) then by submission time (ascending)
        txs.sort_by(|a, b| {
            b.priority_fee.cmp(&a.priority_fee)
                .then_with(|| a.submission_time.cmp(&b.submission_time))
        });
        
        txs.into_iter()
            .take(limit)
            .map(|private_tx| private_tx.transaction.clone())
            .collect()
    }
}

Extend existing TxPool struct:

// In the main TxPool struct, add:
pub struct Pool<V: PoolTransaction, T: TransactionOrdering, S: PooledTransactions<Transaction = V>> {
    // ... existing fields
    /// Private mempool for authorized transactions
    private_mempool: Arc<RwLock<PrivateMempool>>,
}

Access Control Layer

Create: crates/transaction-pool/src/access_control.rs

use jsonwebtoken::{decode, DecodingKey, Validation, Algorithm};
use serde::{Deserialize, Serialize};
use std::collections::HashSet;

#[derive(Debug, Serialize, Deserialize)]
struct Claims {
    sub: String,  // submitter address
    exp: usize,   // expiration time
    iat: usize,   // issued at
    authorized_addresses: Vec<String>,
}

pub struct AccessControl {
    jwt_secret: String,
    api_keys: HashSet<String>,
    authorized_addresses: HashSet<Address>,
}

impl AccessControl {
    pub fn new() -> Self {
        Self {
            jwt_secret: std::env::var("PRIVATE_MEMPOOL_JWT_SECRET")
                .unwrap_or_else(|_| "default-secret".to_string()),
            api_keys: HashSet::new(),
            authorized_addresses: HashSet::new(),
        }
    }

    pub fn validate_jwt(&self, token: &str) -> Result<Address, AccessError> {
        let decoding_key = DecodingKey::from_secret(self.jwt_secret.as_bytes());
        let validation = Validation::new(Algorithm::HS256);
        
        match decode::<Claims>(token, &decoding_key, &validation) {
            Ok(token_data) => {
                let address = token_data.claims.sub.parse::<Address>()
                    .map_err(|_| AccessError::InvalidAddress)?;
                
                if self.authorized_addresses.contains(&address) {
                    Ok(address)
                } else {
                    Err(AccessError::Unauthorized)
                }
            }
            Err(_) => Err(AccessError::InvalidToken),
        }
    }

    pub fn validate_api_key(&self, api_key: &str, address: Address) -> Result<(), AccessError> {
        if self.api_keys.contains(api_key) && self.authorized_addresses.contains(&address) {
            Ok(())
        } else {
            Err(AccessError::Unauthorized)
        }
    }
}

RPC Endpoints

File: crates/rpc/rpc/src/eth/api/transactions.rs

// Add new RPC methods for private mempool
impl<Provider, Pool, Network, EvmConfig> EthApi<Provider, Pool, Network, EvmConfig>
where
    Provider: BlockReaderIdExt + ChainSpecProvider + StateProviderFactory + EvmEnvProvider + 'static,
    Pool: TransactionPool + 'static,
    Network: NetworkInfo + Peers + 'static,
    EvmConfig: ConfigureEvm + 'static,
{
    /// Submit a private transaction
    pub async fn send_private_transaction(
        &self,
        request: TransactionRequest,
        auth_token: String,
    ) -> Result<B256, EthApiError> {
        // Validate authentication
        let submitter = self.pool().private_mempool()
            .read()
            .await
            .access_control()
            .validate_jwt(&auth_token)?;

        // Convert request to transaction
        let transaction = self.build_transaction(request, submitter).await?;
        
        // Add to private mempool
        let tx_hash = self.pool()
            .private_mempool()
            .write()
            .await
            .add_private_transaction(transaction, submitter)?;

        Ok(tx_hash)
    }

    /// Get private mempool status for authorized users
    pub async fn get_private_mempool_status(
        &self,
        auth_token: String,
    ) -> Result<PrivateMempoolStatus, EthApiError> {
        let submitter = self.pool().private_mempool()
            .read()
            .await
            .access_control()
            .validate_jwt(&auth_token)?;

        let private_mempool = self.pool().private_mempool().read().await;
        
        Ok(PrivateMempoolStatus {
            pending_count: private_mempool.pending_count(),
            your_transactions: private_mempool.get_transactions_by_submitter(submitter),
        })
    }
}

Block Building & Network Integration

Block Building Integration

File: crates/payload/builder/src/lib.rs

// Modify the payload builder to prioritize private transactions
impl<Pool, Client> PayloadBuilder<Pool, Client>
where
    Pool: TransactionPool,
    Client: StateProviderFactory,
{
    pub fn build_payload(&self, args: BuildArguments) -> Result<BuiltPayload, PayloadBuilderError> {
        let mut transactions = Vec::new();
        
        // First, add private transactions with higher priority
        let private_mempool = self.pool.private_mempool().read().await;
        let private_txs = private_mempool.get_private_transactions_for_block(100); // Max 100 private txs per block
        
        for private_tx in private_txs {
            if self.can_fit_transaction(&private_tx, &args) {
                transactions.push(private_tx);
            }
        }
        
        // Then fill remaining space with public transactions
        let public_txs = self.pool.best_transactions_with_attributes(TransactionListingAttributes {
            // ... existing attributes
            exclude_private: true, // New field to exclude private transactions
        });
        
        for public_tx in public_txs {
            if transactions.len() >= args.max_transactions {
                break;
            }
            if self.can_fit_transaction(&public_tx, &args) {
                transactions.push(public_tx);
            }
        }
        
        self.build_block_with_transactions(transactions, args)
    }
}

Network Layer Modifications

File: crates/net/network/src/transactions.rs

// Modify transaction propagation to exclude private transactions
impl<Pool> TransactionsManager<Pool>
where
    Pool: TransactionPool,
{
    /// Modified to prevent private transaction gossip
    fn on_new_transactions(&mut self, txs: Vec<NewPooledTransaction>) {
        let mut public_txs = Vec::new();
        
        for tx in txs {
            // Only propagate if not in private mempool
            if !self.pool.is_private_transaction(&tx.hash()) {
                public_txs.push(tx);
            }
        }
        
        if !public_txs.is_empty() {
            // Existing propagation logic for public transactions only
            self.propagate_transactions(public_txs);
        }
    }
    
    /// Ensure private transactions are never included in transaction announcements
    fn get_transactions_to_propagate(&self, peer_id: PeerId) -> Vec<TxHash> {
        self.pool
            .pooled_transactions_max(MAX_TRANSACTIONS_TO_PROPAGATE)
            .filter(|tx| !self.pool.is_private_transaction(&tx.hash()))
            .map(|tx| tx.hash())
            .collect()
    }
}

Configuration & CLI

File: bin/reth/src/args/private_mempool.rs

use clap::Args;
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Args, PartialEq, Eq, Serialize, Deserialize)]
pub struct PrivateMempoolArgs {
    /// Enable private mempool functionality
    #[arg(long, default_value = "false")]
    pub enable_private_mempool: bool,

    /// Maximum number of private transactions to store
    #[arg(long, default_value = "10000")]
    pub max_private_transactions: usize,

    /// JWT secret for private transaction authentication
    #[arg(long, env = "PRIVATE_MEMPOOL_JWT_SECRET")]
    pub jwt_secret: Option<String>,

    /// File containing authorized addresses
    #[arg(long)]
    pub authorized_addresses_file: Option<PathBuf>,

    /// Private transaction TTL in seconds
    #[arg(long, default_value = "300")]
    pub private_transaction_ttl: u64,
}

Integration & Testing

Create: crates/transaction-pool/src/test/private_mempool_tests.rs

#[cfg(test)]
mod tests {
    use super::*;
    use reth_primitives::TransactionSigned;
    
    #[tokio::test]
    async fn test_private_transaction_submission() {
        let mut private_mempool = PrivateMempool::new(1000);
        
        // Add authorized submitter
        let authorized_addr = Address::random();
        private_mempool.add_authorized_address(authorized_addr);
        
        // Create test transaction
        let tx = create_test_transaction();
        
        // Should succeed for authorized address
        let result = private_mempool.add_private_transaction(tx.clone(), authorized_addr);
        assert!(result.is_ok());
        
        // Should fail for unauthorized address
        let unauthorized_addr = Address::random();
        let result = private_mempool.add_private_transaction(tx, unauthorized_addr);
        assert!(result.is_err());
    }
    
    #[tokio::test]
    async fn test_private_transaction_ordering() {
        let mut private_mempool = PrivateMempool::new(1000);
        
        // Add transactions with different priority fees
        let high_fee_tx = create_transaction_with_priority_fee(1000);
        let low_fee_tx = create_transaction_with_priority_fee(100);
        
        private_mempool.add_private_transaction(low_fee_tx, Address::random()).unwrap();
        private_mempool.add_private_transaction(high_fee_tx, Address::random()).unwrap();
        
        let ordered_txs = private_mempool.get_private_transactions_for_block(10);
        
        // High fee transaction should be first
        assert!(ordered_txs[0].max_priority_fee_per_gas() > ordered_txs[1].max_priority_fee_per_gas());
    }
}

Documentation & Deployment

Create: docs/private_mempool.md

# Private Mempool Configuration

## Setup

1. Generate JWT secret:
```bash
openssl rand -base64 32
  1. Create authorized addresses file:
[
  "0x742d35cc6C2C2f0B5e2CD6EDZ...",
  "0x8ba1f109551bD432803012645Hd..."
]
  1. Start reth with private mempool:
reth node \
  --enable-private-mempool \
  --jwt-secret="your-secret-here" \
  --authorized-addresses-file=./authorized.json \
  --max-private-transactions=5000

API Usage

Submit private transaction:

curl -X POST http://localhost:8545 \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "method": "eth_sendPrivateTransaction",
    "params": [{
      "from": "0x...",
      "to": "0x...",
      "value": "0x1000",
      "gas": "0x5208"
    }, "your-jwt-token"],
    "id": 1
  }'

## Implementation Checklist

**Week 1:**
- [ ] Private mempool data structures
- [ ] Access control with JWT/API keys
- [ ] Basic RPC endpoints
- [ ] Unit tests for core functionality

**Week 2:**
- [ ] Block building integration
- [ ] Network layer modifications (prevent gossip)
- [ ] CLI configuration
- [ ] Integration tests
- [ ] Documentation

**Optional Enhancements (Week 3+):**
- [ ] Metrics and monitoring
- [ ] Rate limiting per submitter  
- [ ] Transaction replacement policies
- [ ] MEV protection mechanisms

Metadata

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment