From e168b492bca265525214d2503aac1a22edfaebfe Mon Sep 17 00:00:00 2001 From: Muang Date: Sun, 28 Dec 2025 22:58:52 +0900 Subject: [PATCH 01/15] add: defined transaction data structure --- Cargo.toml | 2 +- crates/mempool/Cargo.toml | 9 +++ crates/mempool/src/account.rs | 121 ++++++++++++++++++++++++++++++ crates/mempool/src/config.rs | 49 ++++++++++++ crates/mempool/src/error.rs | 40 ++++++++++ crates/mempool/src/lib.rs | 14 ++++ crates/mempool/src/pool.rs | 87 +++++++++++++++++++++ crates/mempool/src/transaction.rs | 97 ++++++++++++++++++++++++ 8 files changed, 418 insertions(+), 1 deletion(-) create mode 100644 crates/mempool/Cargo.toml create mode 100644 crates/mempool/src/account.rs create mode 100644 crates/mempool/src/config.rs create mode 100644 crates/mempool/src/error.rs create mode 100644 crates/mempool/src/lib.rs create mode 100644 crates/mempool/src/pool.rs create mode 100644 crates/mempool/src/transaction.rs diff --git a/Cargo.toml b/Cargo.toml index 1a55921..fbdcae7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ members = [ "crates/crypto", "crates/data-chain", "crates/storage", - "crates/node", + "crates/node", "crates/mempool", ] resolver = "2" diff --git a/crates/mempool/Cargo.toml b/crates/mempool/Cargo.toml new file mode 100644 index 0000000..3ac353b --- /dev/null +++ b/crates/mempool/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "mempool" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] diff --git a/crates/mempool/src/account.rs b/crates/mempool/src/account.rs new file mode 100644 index 0000000..5298bf7 --- /dev/null +++ b/crates/mempool/src/account.rs @@ -0,0 +1,121 @@ +//! Per-account transaction state + +use serde::{Deserialize, Serialize}; + +/// Account state for nonce tracking +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct AccountState { + /// Current nonce (next expected for execution) + pub current_nonce: u64, + + /// Highest nonce in pending pool + pub highest_pending_nonce: u64, + + /// Number of pending transactions + pub pending_count: usize, + + /// Number of queued transactions + pub queued_count: usize, +} + +impl AccountState { + /// Create new account state + pub fn new(current_nonce: u64) -> Self { + Self { + current_nonce, + highest_pending_nonce: current_nonce, + pending_count: 0, + queued_count: 0, + } + } + + /// Check if transaction would create a nonce gap + pub fn would_create_gap(&self, tx_nonce: u64, max_gap: u64) -> bool { + if tx_nonce <= self.current_nonce { + return false; + } + + let gap = tx_nonce - self.current_nonce - 1; + gap > max_gap + } + + /// Simulate adding a transaction + pub fn with_transaction(&self, tx_nonce: u64) -> Self { + let mut new_state = self.clone(); + + if tx_nonce == new_state.highest_pending_nonce + 1 { + // Continuous with pending pool + new_state.pending_count += 1; + new_state.highest_pending_nonce = tx_nonce; + } else if tx_nonce <= new_state.highest_pending_nonce { + // Already covered + return new_state; + } else { + // Creates a gap - goes to queued + new_state.queued_count += 1; + } + + new_state + } + + /// Total transactions (pending + queued) + pub fn total_transactions(&self) -> usize { + self.pending_count + self.queued_count + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_create_account() { + let acc = AccountState::new(100); + assert_eq!(acc.current_nonce, 100); + assert_eq!(acc.highest_pending_nonce, 100); + assert_eq!(acc.pending_count, 0); + assert_eq!(acc.queued_count, 0); + } + + #[test] + fn test_gap_detection() { + let acc = AccountState::new(100); + + // No gap for nonce 101 + assert!(!acc.would_create_gap(101, 16)); + + // No gap within limit + assert!(!acc.would_create_gap(116, 16)); // gap = 15 + + // Gap exceeds limit + assert!(acc.would_create_gap(117, 16)); // gap = 16 + } + + #[test] + fn test_with_transaction() { + let acc = AccountState::new(100); + + let acc = acc.with_transaction(101); + assert_eq!(acc.pending_count, 1); + assert_eq!(acc.queued_count, 0); + assert_eq!(acc.highest_pending_nonce, 101); + + let acc = acc.with_transaction(102); + assert_eq!(acc.pending_count, 2); + assert_eq!(acc.highest_pending_nonce, 102); + + let acc = acc.with_transaction(105); // gap + assert_eq!(acc.pending_count, 2); + assert_eq!(acc.queued_count, 1); + } + + #[test] + fn test_total_transactions() { + let acc = AccountState::new(100); + let acc = acc.with_transaction(101); + let acc = acc.with_transaction(102); + let acc = acc.with_transaction(105); + + assert_eq!(acc.total_transactions(), 3); + } +} diff --git a/crates/mempool/src/config.rs b/crates/mempool/src/config.rs new file mode 100644 index 0000000..275e283 --- /dev/null +++ b/crates/mempool/src/config.rs @@ -0,0 +1,49 @@ +//! Mempool configuration + +use serde::{Deserialize, Serialize}; + +/// Mempool configuration +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MempoolConfig { + /// Maximum pending (executable) transactions + pub max_pending: usize, + + /// Maximum queued transactions per sender + pub max_queued_per_sender: usize, + + /// Maximum nonce gap before transaction is queued + pub max_nonce_gap: u64, + + /// Minimum gas price (in wei) + pub min_gas_price: u128, + + /// Enable RBF (Replace-By-Fee) + pub enable_rbf: bool, +} + +impl Default for MempoolConfig { + fn default() -> Self { + Self { + max_pending: 10_000, + max_queued_per_sender: 100, + max_nonce_gap: 16, + min_gas_price: 1_000_000_000, // 1 gwei + enable_rbf: true, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_default_config() { + let cfg = MempoolConfig::default(); + assert_eq!(cfg.max_pending, 10_000); + assert_eq!(cfg.max_queued_per_sender, 100); + assert_eq!(cfg.max_nonce_gap, 16); + assert_eq!(cfg.min_gas_price, 1_000_000_000); + assert!(cfg.enable_rbf); + } +} diff --git a/crates/mempool/src/error.rs b/crates/mempool/src/error.rs new file mode 100644 index 0000000..bae42de --- /dev/null +++ b/crates/mempool/src/error.rs @@ -0,0 +1,40 @@ +//! Error types for mempool operations + +use thiserror::Error; + +/// Mempool error types +#[derive(Error, Debug, Clone)] +pub enum MempoolError { + #[error("Transaction already exists in pool")] + TransactionAlreadyExists, + + #[error("Sender account is full")] + SenderAccountFull, + + #[error("Insufficient gas price")] + InsufficientGasPrice, + + #[error("Nonce too low: current={current}, got={provided}")] + NonceTooLow { current: u64, provided: u64 }, + + #[error("Nonce gap exceeds maximum: gap={gap} > max={max}")] + NonceGapExceeded { gap: u64, max: u64 }, + + #[error("Sender not found")] + SenderNotFound, + + #[error("Transaction not found")] + TransactionNotFound, + + #[error("Invalid transaction: {0}")] + InvalidTransaction(String), + + #[error("Pool overflow")] + PoolOverflow, + + #[error("Database error: {0}")] + DatabaseError(String), + + #[error("Internal error: {0}")] + Internal(String), +} diff --git a/crates/mempool/src/lib.rs b/crates/mempool/src/lib.rs new file mode 100644 index 0000000..b93cf3f --- /dev/null +++ b/crates/mempool/src/lib.rs @@ -0,0 +1,14 @@ +pub fn add(left: u64, right: u64) -> u64 { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} diff --git a/crates/mempool/src/pool.rs b/crates/mempool/src/pool.rs new file mode 100644 index 0000000..d7b5fae --- /dev/null +++ b/crates/mempool/src/pool.rs @@ -0,0 +1,87 @@ +//! CipherBFT mempool over Reth's TransactionPool + +use crate::{config::MempoolConfig, error::MempoolError, account::AccountState}; +use reth_primitives::{Address, B256}; +use reth_transaction_pool::TransactionPool; +use std::collections::HashMap; + +/// Main mempool wrapper over Reth's TransactionPool +pub struct CipherBftPool { + /// Underlying Reth pool + pool: P, + + /// Configuration + config: MempoolConfig, + + /// Per-account states + accounts: HashMap, +} + +impl CipherBftPool

{ + /// Create new mempool + pub fn new(pool: P, config: MempoolConfig) -> Self { + Self { + pool, + config, + accounts: HashMap::new(), + } + } + + /// Get configuration + pub fn config(&self) -> &MempoolConfig { + &self.config + } + + /// Get or create account state + pub fn get_or_create_account(&mut self, address: Address) -> &mut AccountState { + self.accounts + .entry(address) + .or_insert_with(|| AccountState::new(0)) + } + + /// Get account state + pub fn get_account(&self, address: Address) -> Option<&AccountState> { + self.accounts.get(&address) + } + + /// Pool size information + pub fn size(&self) -> PoolSize { + let reth_size = self.pool.pool_size(); + PoolSize { + pending: reth_size.pending, + queued: reth_size.queued, + } + } + + /// Number of unique senders + pub fn num_senders(&self) -> usize { + self.accounts.len() + } +} + +/// Pool size information +#[derive(Clone, Copy, Debug)] +pub struct PoolSize { + pub pending: usize, + pub queued: usize, +} + +impl PoolSize { + pub fn total(&self) -> usize { + self.pending + self.queued + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_pool_size() { + let size = PoolSize { + pending: 1000, + queued: 500, + }; + assert_eq!(size.total(), 1500); + } +} diff --git a/crates/mempool/src/transaction.rs b/crates/mempool/src/transaction.rs new file mode 100644 index 0000000..b28b86e --- /dev/null +++ b/crates/mempool/src/transaction.rs @@ -0,0 +1,97 @@ +//! Transaction metadata and ordering + +use reth_primitives::{B256, Address, TransactionSigned}; +use serde::{Deserialize, Serialize}; +use crate::error::MempoolError; + +/// Transaction information tracked in mempool +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct TransactionInfo { + /// Transaction hash + pub hash: B256, + + /// Sender address + pub sender: Address, + + /// Nonce + pub nonce: u64, + + /// Gas limit + pub gas_limit: u64, + + /// Effective gas price (wei) + pub effective_gas_price: u128, + + /// Transaction size (bytes) + pub size: usize, + + /// Whether in pending pool (true) or queued (false) + pub is_pending: bool, +} + +impl TransactionInfo { + /// Create from a signed transaction + pub fn from_signed(tx: &TransactionSigned, is_pending: bool) -> Result { + let sender = tx + .recover_signer() + .ok_or_else(|| MempoolError::InvalidTransaction("Invalid signature".to_string()))?; + + Ok(Self { + hash: tx.hash(), + sender, + nonce: tx.nonce(), + gas_limit: tx.gas_limit(), + effective_gas_price: tx.effective_gas_price(None), + size: tx.encoded_2718().len(), + is_pending, + }) + } + + /// Calculate effective gas price with base fee + pub fn with_base_fee(&self, base_fee: u128) -> u128 { + self.effective_gas_price.max(base_fee) + } +} + +/// Transaction ordering by gas price (descending) +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct TransactionOrdering { + pub effective_gas_price: u128, + pub nonce: u64, +} + +impl Ord for TransactionOrdering { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + // Higher gas price comes first + other + .effective_gas_price + .cmp(&self.effective_gas_price) + .then_with(|| self.nonce.cmp(&other.nonce)) + } +} + +impl PartialOrd for TransactionOrdering { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_ordering() { + let tx1 = TransactionOrdering { + effective_gas_price: 100, + nonce: 0, + }; + let tx2 = TransactionOrdering { + effective_gas_price: 200, + nonce: 1, + }; + + // Higher gas price should come first + assert!(tx2 < tx1); + } +} From aad1288033d1c6a48d71da3aa905e15d4daec8d7 Mon Sep 17 00:00:00 2001 From: Muang Date: Sun, 28 Dec 2025 23:12:02 +0900 Subject: [PATCH 02/15] add: defined transaction data structure --- crates/mempool/Cargo.toml | 17 +++++++++++++++++ crates/mempool/src/lib.rs | 36 ++++++++++++++++++++++++------------ crates/mempool/src/pool.rs | 32 +++++++++++++++++++++++++++++++- 3 files changed, 72 insertions(+), 13 deletions(-) diff --git a/crates/mempool/Cargo.toml b/crates/mempool/Cargo.toml index 3ac353b..70e22b3 100644 --- a/crates/mempool/Cargo.toml +++ b/crates/mempool/Cargo.toml @@ -7,3 +7,20 @@ license.workspace = true repository.workspace = true [dependencies] +# Reth transaction pool and primitives +reth-transaction-pool = { git = "https://github.com/paradigmxyz/reth", rev = "v1.1.0" } +reth-primitives = { git = "https://github.com/paradigmxyz/reth", rev = "v1.1.0" } + +# Serialization +serde = { workspace = true } +serde_json = { workspace = true } + +# Error handling +thiserror = { workspace = true } + +# Async +tokio = { workspace = true } +async-trait = { workspace = true } + +# Logging +tracing = { workspace = true } diff --git a/crates/mempool/src/lib.rs b/crates/mempool/src/lib.rs index b93cf3f..d80828d 100644 --- a/crates/mempool/src/lib.rs +++ b/crates/mempool/src/lib.rs @@ -1,14 +1,26 @@ -pub fn add(left: u64, right: u64) -> u64 { - left + right -} +//! CipherBFT Mempool - Transaction mempool based on Reth's TransactionPool +//! +//! # Architecture +//! +//! The mempool is organized around Reth's transaction pool with CipherBFT-specific +//! wrapping for DCL/CL integration. +//! +//! ## Modules +//! +//! - `error`: Error types for mempool operations +//! - `config`: Configuration for the mempool +//! - `transaction`: Transaction metadata tracking +//! - `account`: Per-account state management +//! - `pool`: Main pool adapter over Reth's TransactionPool -#[cfg(test)] -mod tests { - use super::*; +pub mod error; +pub mod config; +pub mod transaction; +pub mod account; +pub mod pool; - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } -} +pub use error::MempoolError; +pub use config::MempoolConfig; +pub use transaction::{TransactionInfo, TransactionOrdering}; +pub use account::AccountState; +pub use pool::CipherBftPool; diff --git a/crates/mempool/src/pool.rs b/crates/mempool/src/pool.rs index d7b5fae..1b68389 100644 --- a/crates/mempool/src/pool.rs +++ b/crates/mempool/src/pool.rs @@ -1,6 +1,6 @@ //! CipherBFT mempool over Reth's TransactionPool -use crate::{config::MempoolConfig, error::MempoolError, account::AccountState}; +use crate::{account::AccountState, config::MempoolConfig, transaction::TransactionInfo}; use reth_primitives::{Address, B256}; use reth_transaction_pool::TransactionPool; use std::collections::HashMap; @@ -15,6 +15,9 @@ pub struct CipherBftPool { /// Per-account states accounts: HashMap, + + /// Sender-address to transaction hashes index + sender_index: HashMap>, } impl CipherBftPool

{ @@ -24,6 +27,7 @@ impl CipherBftPool

{ pool, config, accounts: HashMap::new(), + sender_index: HashMap::new(), } } @@ -57,6 +61,32 @@ impl CipherBftPool

{ pub fn num_senders(&self) -> usize { self.accounts.len() } + + /// Get all transaction hashes for a sender (if any) + pub fn sender_transactions(&self, address: &Address) -> Option<&Vec> { + self.sender_index.get(address) + } + + /// Index a transaction by its sender and hash. No-op if already indexed. + pub fn index_transaction(&mut self, tx: &TransactionInfo) { + let entry = self.sender_index.entry(tx.sender).or_default(); + if !entry.contains(&tx.hash) { + entry.push(tx.hash); + } + } + + /// Remove a transaction hash from the sender index. Returns true if removed. + pub fn remove_transaction_from_index(&mut self, sender: &Address, hash: &B256) -> bool { + if let Some(list) = self.sender_index.get_mut(sender) { + let original_len = list.len(); + list.retain(|h| h != hash); + if list.is_empty() { + self.sender_index.remove(sender); + } + return list.len() != original_len; + } + false + } } /// Pool size information From 700584d3e8e21d845516b81b66f8a82b3bd28eca Mon Sep 17 00:00:00 2001 From: Muang Date: Mon, 29 Dec 2025 01:10:11 +0900 Subject: [PATCH 03/15] refactor: align mempool with reth and trim to MP-1 structs --- crates/mempool/Cargo.toml | 3 + crates/mempool/src/account.rs | 111 +++++++-------------------- crates/mempool/src/config.rs | 52 +++++++++++-- crates/mempool/src/error.rs | 37 +++------ crates/mempool/src/lib.rs | 4 +- crates/mempool/src/pool.rs | 120 ++++++------------------------ crates/mempool/src/transaction.rs | 88 ++++++---------------- 7 files changed, 135 insertions(+), 280 deletions(-) diff --git a/crates/mempool/Cargo.toml b/crates/mempool/Cargo.toml index 70e22b3..1bedcd8 100644 --- a/crates/mempool/Cargo.toml +++ b/crates/mempool/Cargo.toml @@ -11,6 +11,9 @@ repository.workspace = true reth-transaction-pool = { git = "https://github.com/paradigmxyz/reth", rev = "v1.1.0" } reth-primitives = { git = "https://github.com/paradigmxyz/reth", rev = "v1.1.0" } +# Ethereum types (alloy suite - use Reth compatible version) +alloy-primitives = "0.8" + # Serialization serde = { workspace = true } serde_json = { workspace = true } diff --git a/crates/mempool/src/account.rs b/crates/mempool/src/account.rs index 5298bf7..eed7084 100644 --- a/crates/mempool/src/account.rs +++ b/crates/mempool/src/account.rs @@ -1,36 +1,26 @@ -//! Per-account transaction state +//! BFT-specific account validation helpers. +//! +//! Reth keeps full account state inside the pool. We only need minimal checks +//! before delegating to the underlying pool. use serde::{Deserialize, Serialize}; -/// Account state for nonce tracking +/// Helper that validates nonce gaps for a single account. #[derive(Clone, Debug, Serialize, Deserialize)] -pub struct AccountState { - /// Current nonce (next expected for execution) +pub struct AccountValidator { + /// Current executable nonce fetched from the execution layer. pub current_nonce: u64, - - /// Highest nonce in pending pool - pub highest_pending_nonce: u64, - - /// Number of pending transactions - pub pending_count: usize, - - /// Number of queued transactions - pub queued_count: usize, } -impl AccountState { - /// Create new account state +impl AccountValidator { + /// Construct a validator for a given current nonce. pub fn new(current_nonce: u64) -> Self { - Self { - current_nonce, - highest_pending_nonce: current_nonce, - pending_count: 0, - queued_count: 0, - } + Self { current_nonce } } - /// Check if transaction would create a nonce gap - pub fn would_create_gap(&self, tx_nonce: u64, max_gap: u64) -> bool { + /// Returns `true` if the provided transaction nonce exceeds the configured + /// gap limit (i.e. would be queued indefinitely). + pub fn exceeds_nonce_gap(&self, tx_nonce: u64, max_gap: u64) -> bool { if tx_nonce <= self.current_nonce { return false; } @@ -38,30 +28,6 @@ impl AccountState { let gap = tx_nonce - self.current_nonce - 1; gap > max_gap } - - /// Simulate adding a transaction - pub fn with_transaction(&self, tx_nonce: u64) -> Self { - let mut new_state = self.clone(); - - if tx_nonce == new_state.highest_pending_nonce + 1 { - // Continuous with pending pool - new_state.pending_count += 1; - new_state.highest_pending_nonce = tx_nonce; - } else if tx_nonce <= new_state.highest_pending_nonce { - // Already covered - return new_state; - } else { - // Creates a gap - goes to queued - new_state.queued_count += 1; - } - - new_state - } - - /// Total transactions (pending + queued) - pub fn total_transactions(&self) -> usize { - self.pending_count + self.queued_count - } } #[cfg(test)] @@ -69,53 +35,26 @@ mod tests { use super::*; #[test] - fn test_create_account() { - let acc = AccountState::new(100); - assert_eq!(acc.current_nonce, 100); - assert_eq!(acc.highest_pending_nonce, 100); - assert_eq!(acc.pending_count, 0); - assert_eq!(acc.queued_count, 0); + fn test_no_gap() { + let validator = AccountValidator::new(100); + assert!(!validator.exceeds_nonce_gap(101, 16)); } #[test] - fn test_gap_detection() { - let acc = AccountState::new(100); - - // No gap for nonce 101 - assert!(!acc.would_create_gap(101, 16)); - - // No gap within limit - assert!(!acc.would_create_gap(116, 16)); // gap = 15 - - // Gap exceeds limit - assert!(acc.would_create_gap(117, 16)); // gap = 16 + fn test_gap_within_limit() { + let validator = AccountValidator::new(100); + assert!(!validator.exceeds_nonce_gap(116, 16)); } #[test] - fn test_with_transaction() { - let acc = AccountState::new(100); - - let acc = acc.with_transaction(101); - assert_eq!(acc.pending_count, 1); - assert_eq!(acc.queued_count, 0); - assert_eq!(acc.highest_pending_nonce, 101); - - let acc = acc.with_transaction(102); - assert_eq!(acc.pending_count, 2); - assert_eq!(acc.highest_pending_nonce, 102); - - let acc = acc.with_transaction(105); // gap - assert_eq!(acc.pending_count, 2); - assert_eq!(acc.queued_count, 1); + fn test_gap_exceeds_limit() { + let validator = AccountValidator::new(100); + assert!(validator.exceeds_nonce_gap(117, 16)); } #[test] - fn test_total_transactions() { - let acc = AccountState::new(100); - let acc = acc.with_transaction(101); - let acc = acc.with_transaction(102); - let acc = acc.with_transaction(105); - - assert_eq!(acc.total_transactions(), 3); + fn test_old_nonce() { + let validator = AccountValidator::new(100); + assert!(!validator.exceeds_nonce_gap(98, 16)); } } diff --git a/crates/mempool/src/config.rs b/crates/mempool/src/config.rs index 275e283..ccd6667 100644 --- a/crates/mempool/src/config.rs +++ b/crates/mempool/src/config.rs @@ -1,23 +1,28 @@ //! Mempool configuration +//! +//! We primarily rely on Reth's `PoolConfig`, but expose a CipherBFT-friendly +//! wrapper so higher layers can express their preferences without depending +//! on Reth types directly. +use reth_transaction_pool::PoolConfig; use serde::{Deserialize, Serialize}; -/// Mempool configuration +/// Mempool configuration with CipherBFT-specific knobs. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MempoolConfig { - /// Maximum pending (executable) transactions + /// Maximum executable transactions to keep in the pending sub-pool. pub max_pending: usize, - /// Maximum queued transactions per sender + /// Maximum queued transactions per sender (maps to Reth's account slots). pub max_queued_per_sender: usize, - /// Maximum nonce gap before transaction is queued + /// Maximum nonce gap before the transaction is considered queued. pub max_nonce_gap: u64, - /// Minimum gas price (in wei) + /// Minimum gas price (in wei) we allow into the pool. pub min_gas_price: u128, - /// Enable RBF (Replace-By-Fee) + /// Whether we allow Replace-By-Fee behaviour for the same nonce. pub enable_rbf: bool, } @@ -33,6 +38,27 @@ impl Default for MempoolConfig { } } +impl From for PoolConfig { + fn from(cfg: MempoolConfig) -> Self { + let mut pool_cfg = PoolConfig::default(); + pool_cfg.pending_limit.max_txs = cfg.max_pending; + pool_cfg.queued_limit.max_txs = cfg.max_pending; + pool_cfg.max_account_slots = cfg.max_queued_per_sender; + pool_cfg + } +} + +impl MempoolConfig { + /// Convert borrowed configuration to the underlying Reth configuration. + pub fn to_reth_config(&self) -> PoolConfig { + let mut pool_cfg = PoolConfig::default(); + pool_cfg.pending_limit.max_txs = self.max_pending; + pool_cfg.queued_limit.max_txs = self.max_pending; + pool_cfg.max_account_slots = self.max_queued_per_sender; + pool_cfg + } +} + #[cfg(test)] mod tests { use super::*; @@ -46,4 +72,18 @@ mod tests { assert_eq!(cfg.min_gas_price, 1_000_000_000); assert!(cfg.enable_rbf); } + + #[test] + fn test_reth_conversion() { + let cfg = MempoolConfig { + max_pending: 5_000, + max_queued_per_sender: 42, + ..Default::default() + }; + + let reth_cfg = cfg.to_reth_config(); + assert_eq!(reth_cfg.pending_limit.max_txs, 5_000); + assert_eq!(reth_cfg.queued_limit.max_txs, 5_000); + assert_eq!(reth_cfg.max_account_slots, 42); + } } diff --git a/crates/mempool/src/error.rs b/crates/mempool/src/error.rs index bae42de..fa673bb 100644 --- a/crates/mempool/src/error.rs +++ b/crates/mempool/src/error.rs @@ -1,40 +1,27 @@ //! Error types for mempool operations +//! +//! MP-1 단계: 데이터 구조 정의만 유지. Reth PoolError를 감싸고 최소한의 +//! BFT 정책 에러만 노출한다. +use reth_transaction_pool::error::PoolError; use thiserror::Error; -/// Mempool error types -#[derive(Error, Debug, Clone)] +/// Mempool error types (minimal surface for MP-1) +#[derive(Error, Debug)] pub enum MempoolError { - #[error("Transaction already exists in pool")] - TransactionAlreadyExists, - - #[error("Sender account is full")] - SenderAccountFull, + /// Bubble up errors coming from the underlying Reth pool. + #[error(transparent)] + Pool(#[from] PoolError), + /// Gas price below policy threshold. #[error("Insufficient gas price")] InsufficientGasPrice, - #[error("Nonce too low: current={current}, got={provided}")] - NonceTooLow { current: u64, provided: u64 }, - + /// Nonce gap exceeds configured maximum. #[error("Nonce gap exceeds maximum: gap={gap} > max={max}")] NonceGapExceeded { gap: u64, max: u64 }, - #[error("Sender not found")] - SenderNotFound, - - #[error("Transaction not found")] - TransactionNotFound, - - #[error("Invalid transaction: {0}")] - InvalidTransaction(String), - - #[error("Pool overflow")] - PoolOverflow, - - #[error("Database error: {0}")] - DatabaseError(String), - + /// Internal error marker. #[error("Internal error: {0}")] Internal(String), } diff --git a/crates/mempool/src/lib.rs b/crates/mempool/src/lib.rs index d80828d..32c6a51 100644 --- a/crates/mempool/src/lib.rs +++ b/crates/mempool/src/lib.rs @@ -21,6 +21,6 @@ pub mod pool; pub use error::MempoolError; pub use config::MempoolConfig; -pub use transaction::{TransactionInfo, TransactionOrdering}; -pub use account::AccountState; +pub use transaction::TransactionOrdering; +pub use account::AccountValidator; pub use pool::CipherBftPool; diff --git a/crates/mempool/src/pool.rs b/crates/mempool/src/pool.rs index 1b68389..93eb25f 100644 --- a/crates/mempool/src/pool.rs +++ b/crates/mempool/src/pool.rs @@ -1,117 +1,45 @@ -//! CipherBFT mempool over Reth's TransactionPool +//! CipherBFT mempool wrapper over Reth's TransactionPool +//! +//! MP-1 범위: 데이터 구조 정의만 유지. 실제 로직은 후속 단계에서 채운다. -use crate::{account::AccountState, config::MempoolConfig, transaction::TransactionInfo}; -use reth_primitives::{Address, B256}; -use reth_transaction_pool::TransactionPool; -use std::collections::HashMap; +use crate::config::MempoolConfig; +use reth_transaction_pool::{PoolConfig, TransactionPool}; /// Main mempool wrapper over Reth's TransactionPool +/// +/// Thin adapter that delegates all TX storage, validation, and state management to Reth. +/// Provides BFT-specific constraints (nonce gaps) and batch selection. pub struct CipherBftPool { - /// Underlying Reth pool + /// Underlying Reth pool - all functionality delegated here pool: P, - - /// Configuration + /// BFT-specific config config: MempoolConfig, - - /// Per-account states - accounts: HashMap, - - /// Sender-address to transaction hashes index - sender_index: HashMap>, } impl CipherBftPool

{ - /// Create new mempool + /// Create new mempool wrapper pub fn new(pool: P, config: MempoolConfig) -> Self { - Self { - pool, - config, - accounts: HashMap::new(), - sender_index: HashMap::new(), - } - } - - /// Get configuration - pub fn config(&self) -> &MempoolConfig { - &self.config - } - - /// Get or create account state - pub fn get_or_create_account(&mut self, address: Address) -> &mut AccountState { - self.accounts - .entry(address) - .or_insert_with(|| AccountState::new(0)) + Self { pool, config } } - /// Get account state - pub fn get_account(&self, address: Address) -> Option<&AccountState> { - self.accounts.get(&address) + /// Get the underlying Reth pool + pub fn pool(&self) -> &P { + &self.pool } - /// Pool size information - pub fn size(&self) -> PoolSize { - let reth_size = self.pool.pool_size(); - PoolSize { - pending: reth_size.pending, - queued: reth_size.queued, - } + /// Get mutable reference to underlying Reth pool + pub fn pool_mut(&mut self) -> &mut P { + &mut self.pool } - /// Number of unique senders - pub fn num_senders(&self) -> usize { - self.accounts.len() - } - - /// Get all transaction hashes for a sender (if any) - pub fn sender_transactions(&self, address: &Address) -> Option<&Vec> { - self.sender_index.get(address) - } - - /// Index a transaction by its sender and hash. No-op if already indexed. - pub fn index_transaction(&mut self, tx: &TransactionInfo) { - let entry = self.sender_index.entry(tx.sender).or_default(); - if !entry.contains(&tx.hash) { - entry.push(tx.hash); - } + /// Get BFT config + pub fn config(&self) -> &MempoolConfig { + &self.config } - /// Remove a transaction hash from the sender index. Returns true if removed. - pub fn remove_transaction_from_index(&mut self, sender: &Address, hash: &B256) -> bool { - if let Some(list) = self.sender_index.get_mut(sender) { - let original_len = list.len(); - list.retain(|h| h != hash); - if list.is_empty() { - self.sender_index.remove(sender); - } - return list.len() != original_len; - } - false + /// Convert to the underlying Reth pool configuration. + pub fn reth_config(&self) -> PoolConfig { + self.config.to_reth_config() } } -/// Pool size information -#[derive(Clone, Copy, Debug)] -pub struct PoolSize { - pub pending: usize, - pub queued: usize, -} - -impl PoolSize { - pub fn total(&self) -> usize { - self.pending + self.queued - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_pool_size() { - let size = PoolSize { - pending: 1000, - queued: 500, - }; - assert_eq!(size.total(), 1500); - } -} diff --git a/crates/mempool/src/transaction.rs b/crates/mempool/src/transaction.rs index b28b86e..ab3805d 100644 --- a/crates/mempool/src/transaction.rs +++ b/crates/mempool/src/transaction.rs @@ -1,72 +1,31 @@ -//! Transaction metadata and ordering +//! Transaction ordering helpers built on top of Reth's pool traits. -use reth_primitives::{B256, Address, TransactionSigned}; -use serde::{Deserialize, Serialize}; -use crate::error::MempoolError; +use reth_transaction_pool::PoolTransaction; -/// Transaction information tracked in mempool -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct TransactionInfo { - /// Transaction hash - pub hash: B256, - - /// Sender address - pub sender: Address, - - /// Nonce - pub nonce: u64, - - /// Gas limit - pub gas_limit: u64, - - /// Effective gas price (wei) +/// Ordering key derived from a pool transaction. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct TransactionOrdering { pub effective_gas_price: u128, - - /// Transaction size (bytes) - pub size: usize, - - /// Whether in pending pool (true) or queued (false) - pub is_pending: bool, + pub nonce: u64, } -impl TransactionInfo { - /// Create from a signed transaction - pub fn from_signed(tx: &TransactionSigned, is_pending: bool) -> Result { - let sender = tx - .recover_signer() - .ok_or_else(|| MempoolError::InvalidTransaction("Invalid signature".to_string()))?; - - Ok(Self { - hash: tx.hash(), - sender, +impl TransactionOrdering { + /// Build an ordering key from any Reth pool transaction. + pub fn from_pool_transaction(tx: &T) -> Self { + Self { + effective_gas_price: tx.priority_fee_or_price(), nonce: tx.nonce(), - gas_limit: tx.gas_limit(), - effective_gas_price: tx.effective_gas_price(None), - size: tx.encoded_2718().len(), - is_pending, - }) - } - - /// Calculate effective gas price with base fee - pub fn with_base_fee(&self, base_fee: u128) -> u128 { - self.effective_gas_price.max(base_fee) + } } } -/// Transaction ordering by gas price (descending) -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub struct TransactionOrdering { - pub effective_gas_price: u128, - pub nonce: u64, -} - impl Ord for TransactionOrdering { fn cmp(&self, other: &Self) -> std::cmp::Ordering { // Higher gas price comes first - other - .effective_gas_price - .cmp(&self.effective_gas_price) - .then_with(|| self.nonce.cmp(&other.nonce)) + match other.effective_gas_price.cmp(&self.effective_gas_price) { + std::cmp::Ordering::Equal => self.nonce.cmp(&other.nonce), + ordering => ordering, + } } } @@ -81,17 +40,16 @@ mod tests { use super::*; #[test] - fn test_ordering() { - let tx1 = TransactionOrdering { - effective_gas_price: 100, - nonce: 0, - }; - let tx2 = TransactionOrdering { + fn test_ordering_manual() { + let high = TransactionOrdering { effective_gas_price: 200, nonce: 1, }; + let low = TransactionOrdering { + effective_gas_price: 100, + nonce: 0, + }; - // Higher gas price should come first - assert!(tx2 < tx1); + assert!(high < low); } } From a34f1a198127fb29514ff4db36c826b096751554 Mon Sep 17 00:00:00 2001 From: Muang Date: Mon, 29 Dec 2025 04:32:36 +0900 Subject: [PATCH 04/15] refactor: renamed parameter --- crates/mempool/src/config.rs | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/crates/mempool/src/config.rs b/crates/mempool/src/config.rs index ccd6667..c48378c 100644 --- a/crates/mempool/src/config.rs +++ b/crates/mempool/src/config.rs @@ -14,26 +14,22 @@ pub struct MempoolConfig { pub max_pending: usize, /// Maximum queued transactions per sender (maps to Reth's account slots). - pub max_queued_per_sender: usize, + pub max_queued_per_account: usize, /// Maximum nonce gap before the transaction is considered queued. pub max_nonce_gap: u64, /// Minimum gas price (in wei) we allow into the pool. pub min_gas_price: u128, - - /// Whether we allow Replace-By-Fee behaviour for the same nonce. - pub enable_rbf: bool, } impl Default for MempoolConfig { fn default() -> Self { Self { max_pending: 10_000, - max_queued_per_sender: 100, + max_queued_per_account: 100, max_nonce_gap: 16, min_gas_price: 1_000_000_000, // 1 gwei - enable_rbf: true, } } } @@ -43,7 +39,9 @@ impl From for PoolConfig { let mut pool_cfg = PoolConfig::default(); pool_cfg.pending_limit.max_txs = cfg.max_pending; pool_cfg.queued_limit.max_txs = cfg.max_pending; - pool_cfg.max_account_slots = cfg.max_queued_per_sender; + pool_cfg.max_account_slots = cfg.max_queued_per_account; + + // TODO: map min_gas_price and max_nonce_gap once upstream hooks are wired. pool_cfg } } @@ -54,7 +52,9 @@ impl MempoolConfig { let mut pool_cfg = PoolConfig::default(); pool_cfg.pending_limit.max_txs = self.max_pending; pool_cfg.queued_limit.max_txs = self.max_pending; - pool_cfg.max_account_slots = self.max_queued_per_sender; + pool_cfg.max_account_slots = self.max_queued_per_account; + + // TODO: map min_gas_price and max_nonce_gap once upstream hooks are wired. pool_cfg } } @@ -67,17 +67,16 @@ mod tests { fn test_default_config() { let cfg = MempoolConfig::default(); assert_eq!(cfg.max_pending, 10_000); - assert_eq!(cfg.max_queued_per_sender, 100); + assert_eq!(cfg.max_queued_per_account, 100); assert_eq!(cfg.max_nonce_gap, 16); assert_eq!(cfg.min_gas_price, 1_000_000_000); - assert!(cfg.enable_rbf); } #[test] fn test_reth_conversion() { let cfg = MempoolConfig { max_pending: 5_000, - max_queued_per_sender: 42, + max_queued_per_account: 42, ..Default::default() }; From de39486374a8f4e2e0ddd2552aafdf331a7f3e9d Mon Sep 17 00:00:00 2001 From: Muang Date: Mon, 29 Dec 2025 06:56:55 +0900 Subject: [PATCH 05/15] feat: implemented transaction validation --- crates/mempool/Cargo.toml | 2 + crates/mempool/src/account.rs | 60 ----- crates/mempool/src/error.rs | 30 ++- crates/mempool/src/lib.rs | 2 - crates/mempool/src/pool.rs | 484 ++++++++++++++++++++++++++++++++-- 5 files changed, 495 insertions(+), 83 deletions(-) delete mode 100644 crates/mempool/src/account.rs diff --git a/crates/mempool/Cargo.toml b/crates/mempool/Cargo.toml index 1bedcd8..2fc21e0 100644 --- a/crates/mempool/Cargo.toml +++ b/crates/mempool/Cargo.toml @@ -10,9 +10,11 @@ repository.workspace = true # Reth transaction pool and primitives reth-transaction-pool = { git = "https://github.com/paradigmxyz/reth", rev = "v1.1.0" } reth-primitives = { git = "https://github.com/paradigmxyz/reth", rev = "v1.1.0" } +reth-storage-api = { git = "https://github.com/paradigmxyz/reth", rev = "v1.1.0" } # Ethereum types (alloy suite - use Reth compatible version) alloy-primitives = "0.8" +alloy-eips = "0.4" # Serialization serde = { workspace = true } diff --git a/crates/mempool/src/account.rs b/crates/mempool/src/account.rs deleted file mode 100644 index eed7084..0000000 --- a/crates/mempool/src/account.rs +++ /dev/null @@ -1,60 +0,0 @@ -//! BFT-specific account validation helpers. -//! -//! Reth keeps full account state inside the pool. We only need minimal checks -//! before delegating to the underlying pool. - -use serde::{Deserialize, Serialize}; - -/// Helper that validates nonce gaps for a single account. -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct AccountValidator { - /// Current executable nonce fetched from the execution layer. - pub current_nonce: u64, -} - -impl AccountValidator { - /// Construct a validator for a given current nonce. - pub fn new(current_nonce: u64) -> Self { - Self { current_nonce } - } - - /// Returns `true` if the provided transaction nonce exceeds the configured - /// gap limit (i.e. would be queued indefinitely). - pub fn exceeds_nonce_gap(&self, tx_nonce: u64, max_gap: u64) -> bool { - if tx_nonce <= self.current_nonce { - return false; - } - - let gap = tx_nonce - self.current_nonce - 1; - gap > max_gap - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_no_gap() { - let validator = AccountValidator::new(100); - assert!(!validator.exceeds_nonce_gap(101, 16)); - } - - #[test] - fn test_gap_within_limit() { - let validator = AccountValidator::new(100); - assert!(!validator.exceeds_nonce_gap(116, 16)); - } - - #[test] - fn test_gap_exceeds_limit() { - let validator = AccountValidator::new(100); - assert!(validator.exceeds_nonce_gap(117, 16)); - } - - #[test] - fn test_old_nonce() { - let validator = AccountValidator::new(100); - assert!(!validator.exceeds_nonce_gap(98, 16)); - } -} diff --git a/crates/mempool/src/error.rs b/crates/mempool/src/error.rs index fa673bb..7c243f4 100644 --- a/crates/mempool/src/error.rs +++ b/crates/mempool/src/error.rs @@ -6,7 +6,7 @@ use reth_transaction_pool::error::PoolError; use thiserror::Error; -/// Mempool error types (minimal surface for MP-1) +/// Mempool error types (expanded for MP-2) #[derive(Error, Debug)] pub enum MempoolError { /// Bubble up errors coming from the underlying Reth pool. @@ -14,13 +14,37 @@ pub enum MempoolError { Pool(#[from] PoolError), /// Gas price below policy threshold. - #[error("Insufficient gas price")] - InsufficientGasPrice, + #[error("Insufficient gas price: got {got}, min {min}")] + InsufficientGasPrice { got: u128, min: u128 }, /// Nonce gap exceeds configured maximum. #[error("Nonce gap exceeds maximum: gap={gap} > max={max}")] NonceGapExceeded { gap: u64, max: u64 }, + /// Invalid signature. + #[error("Invalid transaction signature")] + InvalidSignature, + + /// Nonce too low (already executed). + #[error("Nonce too low: tx nonce {tx_nonce}, current {current_nonce}")] + NonceTooLow { tx_nonce: u64, current_nonce: u64 }, + + /// Insufficient balance for gas. + #[error("Insufficient balance: need {need}, have {have}")] + InsufficientBalance { need: u128, have: u128 }, + + /// Gas limit too high. + #[error("Gas limit too high: {gas_limit} > {max}")] + GasLimitTooHigh { gas_limit: u64, max: u64 }, + + /// Transaction size exceeds limit. + #[error("Transaction too large: {size} > {max}")] + OversizedTransaction { size: usize, max: usize }, + + /// Failed to convert into pool-specific transaction type. + #[error("Transaction conversion failed: {0}")] + Conversion(String), + /// Internal error marker. #[error("Internal error: {0}")] Internal(String), diff --git a/crates/mempool/src/lib.rs b/crates/mempool/src/lib.rs index 32c6a51..2759ae5 100644 --- a/crates/mempool/src/lib.rs +++ b/crates/mempool/src/lib.rs @@ -16,11 +16,9 @@ pub mod error; pub mod config; pub mod transaction; -pub mod account; pub mod pool; pub use error::MempoolError; pub use config::MempoolConfig; pub use transaction::TransactionOrdering; -pub use account::AccountValidator; pub use pool::CipherBftPool; diff --git a/crates/mempool/src/pool.rs b/crates/mempool/src/pool.rs index 93eb25f..5268f20 100644 --- a/crates/mempool/src/pool.rs +++ b/crates/mempool/src/pool.rs @@ -1,45 +1,493 @@ -//! CipherBFT mempool wrapper over Reth's TransactionPool +//! CipherBFT mempool wrapper over Reth's Pool //! -//! MP-1 범위: 데이터 구조 정의만 유지. 실제 로직은 후속 단계에서 채운다. +//! MP-1: Basic pool wrapper and config delegation to Reth +//! MP-2: Transaction validation (signature, nonce, gas, balance) +//! +//! ## Design Decision: Direct Reth Pool Usage +//! +//! Per ADR-006, we **directly use Reth's Pool implementation**: +//! ```ignore +//! type CipherBftPool = Pool; +//! ``` +//! +//! We add a thin wrapper ONLY for BFT-specific pre-validation: +//! 1. Minimum gas price enforcement (spam prevention) +//! 2. Maximum nonce gap enforcement (queue bloat prevention) +//! +//! All standard Ethereum validation is delegated to Reth: +//! - Signature verification +//! - Nonce ordering +//! - Balance checks +//! - Gas limits +//! - Transaction size +//! - Replace-by-fee logic +//! +//! ## Integration Status +//! +//! Generic `P: TransactionPool` is temporary until we can instantiate Reth's Pool: +//! +//! ```ignore +//! // Target implementation after EL/CL integration: +//! use reth_transaction_pool::{Pool, CoinbaseTipOrdering, maintain::LocalTransactionConfig}; +//! +//! let pool = Pool::new( +//! eth_pool_validator, // From EL - provides StateProvider for nonce/balance +//! CoinbaseTipOrdering::default(), // Reth's gas price ordering +//! blob_store, // Node-provided BlobStore (optional if not using blobs) +//! pool_config, // From our MempoolConfig.to_reth_config() +//! ); +//! ``` use crate::config::MempoolConfig; -use reth_transaction_pool::{PoolConfig, TransactionPool}; +use crate::error::MempoolError; +use alloy_eips::eip2718::Decodable2718; +use reth_primitives::{ + PooledTransactionsElement, PooledTransactionsElementEcRecovered, TransactionSigned, + TransactionSignedEcRecovered, TransactionSignedNoHash, +}; +use alloy_primitives::{Bytes, TxHash}; +use reth_storage_api::{StateProvider, StateProviderBox}; +use reth_transaction_pool::{PoolTransaction, TransactionOrigin, TransactionPool}; +use tracing::{debug, warn}; -/// Main mempool wrapper over Reth's TransactionPool +/// Mempool wrapper that adds BFT-specific pre-validation to Reth's Pool +/// +/// Generic `P: TransactionPool` will be replaced with concrete Reth Pool type: +/// `Pool` once EL/ST/CL provide required components. /// -/// Thin adapter that delegates all TX storage, validation, and state management to Reth. -/// Provides BFT-specific constraints (nonce gaps) and batch selection. +/// This is NOT abstraction for abstraction's sake - it's a temporary measure +/// until we have all dependencies ready for Pool instantiation. pub struct CipherBftPool { - /// Underlying Reth pool - all functionality delegated here + /// Reth's Pool implementation (to be: Pool) pool: P, /// BFT-specific config config: MempoolConfig, + /// State provider for BFT policy validation (nonce queries) + /// Uses Reth's standard StateProvider trait from reth-storage-api + state_provider: StateProviderBox, } + impl CipherBftPool

{ /// Create new mempool wrapper - pub fn new(pool: P, config: MempoolConfig) -> Self { - Self { pool, config } + /// + /// Once DCL/EL/ST are ready, instantiate with Reth's Pool: + /// ```ignore + /// // In DCL Worker initialization: + /// let reth_pool = Pool::new( + /// eth_pool_validator, // EL's validator with StateProvider + /// CoinbaseTipOrdering::default(), + /// blob_store, // ST's blob storage + /// config.to_reth_config(), + /// ); + /// // Mempool lives in DCL, uses EL's StateProvider for validation + /// CipherBftPool::new(reth_pool, config, state_provider) + /// ``` + pub fn new(pool: P, config: MempoolConfig, state_provider: StateProviderBox) -> Self { + Self { + pool, + config, + state_provider, + } } - /// Get the underlying Reth pool + /// Get reference to Reth pool pub fn pool(&self) -> &P { &self.pool } - /// Get mutable reference to underlying Reth pool - pub fn pool_mut(&mut self) -> &mut P { - &mut self.pool - } - /// Get BFT config pub fn config(&self) -> &MempoolConfig { &self.config } - /// Convert to the underlying Reth pool configuration. - pub fn reth_config(&self) -> PoolConfig { - self.config.to_reth_config() + /// Borrow a worker-facing adapter over the underlying pool (ADR-006) + pub fn adapter(&self) -> CipherBftPoolAdapter<'_, P> { + CipherBftPoolAdapter::new(&self.pool) } + + /// Recover and pre-validate a transaction before handing it to Reth + /// + /// Returns the recovered transaction if it passes CipherBFT policies. + pub async fn recover_and_validate( + &self, + tx: TransactionSigned, + ) -> Result { + let tx_recovered = tx + .try_ecrecovered() + .ok_or(MempoolError::InvalidSignature)?; + + let sender = tx_recovered.signer(); + let tx_ref = tx_recovered.as_ref(); + + debug!( + "Pre-validating transaction from {:?}, nonce={}, gas_price={}", + sender, + tx_ref.nonce(), + tx_ref.max_fee_per_gas() + ); + + self.validate_bft_policy(&tx_recovered).await?; + + debug!("Transaction {:?} passed BFT policy checks", tx_ref.hash()); + + Ok(tx_recovered) + } + + /// Validate BFT-specific policies only (MP-2) + /// + /// This method performs ONLY CipherBFT-specific checks. + /// All standard Ethereum validation is delegated to Reth: + /// - Signature verification → Reth + /// - Nonce ordering (too low/duplicate) → Reth + /// - Balance sufficiency → Reth + /// - Gas limit vs chain limit → Reth (from chain spec) + /// - Transaction size limits → Reth + /// + /// CipherBFT adds: + /// 1. Minimum gas price (prevent spam) + /// 2. Maximum nonce gap (prevent queue bloat) + async fn validate_bft_policy( + &self, + tx: &TransactionSignedEcRecovered, + ) -> Result<(), MempoolError> { + let sender = tx.signer(); + let tx_ref = tx.as_ref(); + + // BFT Policy 1: Minimum gas price enforcement + let effective_gas_price = tx_ref.max_fee_per_gas(); + if effective_gas_price < self.config.min_gas_price { + warn!( + "Transaction from {:?} rejected: gas price {} < min {}", + sender, effective_gas_price, self.config.min_gas_price + ); + return Err(MempoolError::InsufficientGasPrice { + got: effective_gas_price, + min: self.config.min_gas_price, + }); + } + + // BFT Policy 2: Nonce gap enforcement + // Prevents attackers from bloating the queued pool with distant-future nonces + let current_nonce = self.state_provider + .account_nonce(sender) + .map_err(|e| MempoolError::Internal(format!("Failed to get nonce: {}", e)))? + .unwrap_or(0); // Default to 0 if account doesn't exist yet + let tx_nonce = tx_ref.nonce(); + + if tx_nonce > current_nonce { + let gap = tx_nonce - current_nonce - 1; + if gap > self.config.max_nonce_gap { + return Err(MempoolError::NonceGapExceeded { + gap, + max: self.config.max_nonce_gap, + }); + } + } + + Ok(()) + } +} + +/// Worker-facing adapter that surfaces pool operations (ADR-006) +pub struct CipherBftPoolAdapter<'a, P: TransactionPool> { + pool: &'a P, +} + +impl<'a, P: TransactionPool> CipherBftPoolAdapter<'a, P> { + fn new(pool: &'a P) -> Self { + Self { pool } + } + + /// Get best transactions for a Worker batch (ADR-006) + pub fn get_transactions_for_batch( + &self, + limit: usize, + gas_limit: u64, + ) -> Vec { + let mut selected = Vec::with_capacity(limit); + let mut gas_used = 0u64; + + for tx in self.pool.best_transactions() { + if selected.len() >= limit { + break; + } + let tx_gas = tx.gas_limit(); + if gas_used + tx_gas > gas_limit { + continue; + } + gas_used += tx_gas; + let signed = tx.to_recovered_transaction().into_signed(); + selected.push(signed); + } + + selected + } + + /// Remove transactions that were finalized in a committed block (ADR-006) + pub fn remove_finalized(&self, tx_hashes: &[TxHash]) { + let _ = self.pool.remove_transactions(tx_hashes.to_vec()); + } + + /// Get pool statistics for metrics (ADR-006) + pub fn stats(&self) -> PoolStats { + let size = self.pool.pool_size(); + PoolStats { + pending: size.pending, + queued: size.queued, + total: size.pending + size.queued, + } + } +} + +impl

CipherBftPool

+where + P: TransactionPool, + P::Transaction: PoolTransaction + TryFrom, + ::Consensus: Into, + ::Pooled: From, + >::Error: std::fmt::Display, +{ + /// Add a transaction to the pool with CipherBFT validation (MP-2) + pub async fn add_transaction( + &self, + origin: TransactionOrigin, + tx: T, + ) -> Result<(), MempoolError> + where + T: IntoPoolTransactionInput, + { + let pooled_tx = match tx.into_input()? { + PoolTransactionInput::Signed(tx) => { + let tx_recovered = self.recover_and_validate(tx).await?; + P::Transaction::try_from(tx_recovered) + .map_err(|err| MempoolError::Conversion(err.to_string()))? + } + PoolTransactionInput::Recovered(tx_recovered) => { + self.validate_bft_policy(&tx_recovered).await?; + P::Transaction::try_from(tx_recovered) + .map_err(|err| MempoolError::Conversion(err.to_string()))? + } + PoolTransactionInput::Pooled(PoolTx(pooled_tx)) => { + let tx_recovered: TransactionSignedEcRecovered = + pooled_tx.clone().into_consensus().into(); + self.validate_bft_policy(&tx_recovered).await?; + pooled_tx + } + PoolTransactionInput::PooledEcRecovered(pooled) => { + let pooled_tx = P::Transaction::from_pooled(pooled.into()); + let tx_recovered: TransactionSignedEcRecovered = + pooled_tx.clone().into_consensus().into(); + self.validate_bft_policy(&tx_recovered).await?; + pooled_tx + } + PoolTransactionInput::PooledRaw(pooled) => { + let pooled = pooled + .try_into_ecrecovered() + .map_err(|_| MempoolError::InvalidSignature)?; + let pooled_tx = P::Transaction::from_pooled(pooled.into()); + let tx_recovered: TransactionSignedEcRecovered = + pooled_tx.clone().into_consensus().into(); + self.validate_bft_policy(&tx_recovered).await?; + pooled_tx + } + }; + self.pool.add_transaction(origin, pooled_tx).await?; + Ok(()) + } + + /// Add a pooled transaction directly. + pub async fn add_pooled_transaction( + &self, + origin: TransactionOrigin, + tx: P::Transaction, + ) -> Result<(), MempoolError> { + self.add_transaction(origin, PoolTx::new(tx)).await + } +} + +pub enum PoolTransactionInput { + Signed(TransactionSigned), + Recovered(TransactionSignedEcRecovered), + Pooled(PoolTx), + PooledEcRecovered(PooledTransactionsElementEcRecovered), + PooledRaw(PooledTransactionsElement), +} + +pub trait IntoPoolTransactionInput { + fn into_input(self) -> Result, MempoolError>; } +pub struct PoolTx(pub Tx); + +impl PoolTx { + pub fn new(tx: Tx) -> Self { + Self(tx) + } +} + +impl IntoPoolTransactionInput for TransactionSigned { + fn into_input(self) -> Result, MempoolError> { + Ok(PoolTransactionInput::Signed(self)) + } +} + +impl IntoPoolTransactionInput for TransactionSignedEcRecovered { + fn into_input(self) -> Result, MempoolError> { + Ok(PoolTransactionInput::Recovered(self)) + } +} + +impl IntoPoolTransactionInput for PoolTx { + fn into_input(self) -> Result, MempoolError> { + Ok(PoolTransactionInput::Pooled(self)) + } +} + +impl IntoPoolTransactionInput for PooledTransactionsElementEcRecovered +where + Tx: PoolTransaction, + Tx::Pooled: From, +{ + fn into_input(self) -> Result, MempoolError> { + Ok(PoolTransactionInput::PooledEcRecovered(self)) + } +} + +impl IntoPoolTransactionInput for PooledTransactionsElement +where + Tx: PoolTransaction, + Tx::Pooled: From, +{ + fn into_input(self) -> Result, MempoolError> { + Ok(PoolTransactionInput::PooledRaw(self)) + } +} + +impl IntoPoolTransactionInput for TransactionSignedNoHash { + fn into_input(self) -> Result, MempoolError> { + Ok(PoolTransactionInput::Signed(self.into())) + } +} + +impl IntoPoolTransactionInput for Bytes +where + Tx: PoolTransaction, + Tx::Pooled: From, +{ + fn into_input(self) -> Result, MempoolError> { + decode_pooled_from_bytes(&self).map(PoolTransactionInput::PooledRaw) + } +} + +impl IntoPoolTransactionInput for Vec +where + Tx: PoolTransaction, + Tx::Pooled: From, +{ + fn into_input(self) -> Result, MempoolError> { + decode_pooled_from_bytes(&self).map(PoolTransactionInput::PooledRaw) + } +} + +impl<'a, Tx> IntoPoolTransactionInput for &'a [u8] +where + Tx: PoolTransaction, + Tx::Pooled: From, +{ + fn into_input(self) -> Result, MempoolError> { + decode_pooled_from_bytes(self).map(PoolTransactionInput::PooledRaw) + } +} + +fn decode_pooled_from_bytes(bytes: &[u8]) -> Result { + let mut slice = bytes; + PooledTransactionsElement::decode_2718(&mut slice) + .map_err(|err| MempoolError::Conversion(err.to_string())) +} + +/// Simplified pool stats view for metrics (ADR-006) +pub struct PoolStats { + pub pending: usize, + pub queued: usize, + pub total: usize, +} + +// Note: We use Reth's standard StateProvider trait from reth-storage-api. +// EL provides the implementation, but DCL Worker uses it for mempool validation. +// This follows ADR-006: Mempool integrates with DCL Workers for batch creation, +// while EL manages account state (nonce, balance) for transaction validation. + +#[cfg(test)] +mod tests { + use super::*; + // Note: Tests simplified - mock StateProvider removed. + // Real StateProvider comes from EL integration. + + #[test] + fn test_config_defaults() { + let config = MempoolConfig::default(); + assert_eq!(config.max_pending, 10_000); + assert_eq!(config.max_queued_per_account, 100); + assert_eq!(config.max_nonce_gap, 16); + assert_eq!(config.min_gas_price, 1_000_000_000); + } + + + + #[test] + fn test_bft_policy_min_gas_price() { + // BFT Policy: Minimum gas price enforcement + let min_gas_price = 1_000_000_000u128; + + // Below minimum - rejected by CipherBFT + let gas_price = 500_000_000u128; + assert!(gas_price < min_gas_price); + + // At minimum - accepted + let gas_price = 1_000_000_000u128; + assert!(gas_price >= min_gas_price); + + // Above minimum - accepted + let gas_price = 2_000_000_000u128; + assert!(gas_price >= min_gas_price); + } + + #[test] + fn test_bft_policy_nonce_gap() { + // BFT Policy: Max nonce gap prevents queue bloat + // Reth handles nonce ordering (too low, duplicates) + // CipherBFT adds gap limit for far-future nonces + let current_nonce = 10u64; + let max_gap = 16u64; + + // No gap (next nonce) - accepted + let tx_nonce = 11u64; + let gap = tx_nonce - current_nonce - 1; + assert_eq!(gap, 0); + assert!(gap <= max_gap); + + // Gap within limit - accepted + let tx_nonce = 26u64; + let gap = tx_nonce - current_nonce - 1; + assert_eq!(gap, 15); + assert!(gap <= max_gap); + + // Gap exceeds limit - rejected by CipherBFT + let tx_nonce = 28u64; + let gap = tx_nonce - current_nonce - 1; + assert_eq!(gap, 17); + assert!(gap > max_gap); + } + + // Note: The following validations are delegated to Reth and NOT tested here: + // - Balance sufficiency: Reth checks sender has enough for gas + value + // - Transaction size limits: Reth enforces based on protocol rules + // - Gas limit vs block limit: Reth validates against chain spec + // - Nonce ordering (too low): Reth maintains nonce sequence per account + // - Signature verification: Reth validates ECDSA signatures + // + // CipherBFT only adds BFT-specific policies tested above: + // - Minimum gas price (spam prevention) + // - Maximum nonce gap (queue bloat prevention) +} From 62c09ad0af94e23d7935aff63acd4a6457ee8a5f Mon Sep 17 00:00:00 2001 From: Muang Date: Mon, 29 Dec 2025 18:11:02 +0900 Subject: [PATCH 06/15] feat: implemented ~mp-5 --- crates/mempool/src/config.rs | 44 ++++++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/crates/mempool/src/config.rs b/crates/mempool/src/config.rs index c48378c..4efa502 100644 --- a/crates/mempool/src/config.rs +++ b/crates/mempool/src/config.rs @@ -21,6 +21,12 @@ pub struct MempoolConfig { /// Minimum gas price (in wei) we allow into the pool. pub min_gas_price: u128, + + /// Default price bump (in %) required to replace a transaction. + pub default_price_bump: u128, + + /// Price bump (in %) required to replace a blob transaction. + pub replace_blob_tx_price_bump: u128, } impl Default for MempoolConfig { @@ -30,6 +36,8 @@ impl Default for MempoolConfig { max_queued_per_account: 100, max_nonce_gap: 16, min_gas_price: 1_000_000_000, // 1 gwei + default_price_bump: 10, + replace_blob_tx_price_bump: 100, } } } @@ -40,24 +48,29 @@ impl From for PoolConfig { pool_cfg.pending_limit.max_txs = cfg.max_pending; pool_cfg.queued_limit.max_txs = cfg.max_pending; pool_cfg.max_account_slots = cfg.max_queued_per_account; + pool_cfg.price_bumps.default_price_bump = cfg.default_price_bump; + pool_cfg.price_bumps.replace_blob_tx_price_bump = cfg.replace_blob_tx_price_bump; // TODO: map min_gas_price and max_nonce_gap once upstream hooks are wired. pool_cfg } } -impl MempoolConfig { - /// Convert borrowed configuration to the underlying Reth configuration. - pub fn to_reth_config(&self) -> PoolConfig { - let mut pool_cfg = PoolConfig::default(); - pool_cfg.pending_limit.max_txs = self.max_pending; - pool_cfg.queued_limit.max_txs = self.max_pending; - pool_cfg.max_account_slots = self.max_queued_per_account; +// probably not needed +// impl MempoolConfig { +// /// Convert borrowed configuration to the underlying Reth configuration. +// pub fn to_reth_config(&self) -> PoolConfig { +// let mut pool_cfg = PoolConfig::default(); +// pool_cfg.pending_limit.max_txs = self.max_pending; +// pool_cfg.queued_limit.max_txs = self.max_pending; +// pool_cfg.max_account_slots = self.max_queued_per_account; +// pool_cfg.price_bumps.default_price_bump = self.default_price_bump; +// pool_cfg.price_bumps.replace_blob_tx_price_bump = self.replace_blob_tx_price_bump; - // TODO: map min_gas_price and max_nonce_gap once upstream hooks are wired. - pool_cfg - } -} +// // TODO: map min_gas_price and max_nonce_gap once upstream hooks are wired. +// pool_cfg +// } +// } #[cfg(test)] mod tests { @@ -70,6 +83,8 @@ mod tests { assert_eq!(cfg.max_queued_per_account, 100); assert_eq!(cfg.max_nonce_gap, 16); assert_eq!(cfg.min_gas_price, 1_000_000_000); + assert_eq!(cfg.default_price_bump, 10); + assert_eq!(cfg.replace_blob_tx_price_bump, 100); } #[test] @@ -77,12 +92,17 @@ mod tests { let cfg = MempoolConfig { max_pending: 5_000, max_queued_per_account: 42, + default_price_bump: 25, + replace_blob_tx_price_bump: 150, ..Default::default() }; - let reth_cfg = cfg.to_reth_config(); + // let reth_cfg = cfg.to_reth_config(); + let reth_cfg: PoolConfig = cfg.into(); assert_eq!(reth_cfg.pending_limit.max_txs, 5_000); assert_eq!(reth_cfg.queued_limit.max_txs, 5_000); assert_eq!(reth_cfg.max_account_slots, 42); + assert_eq!(reth_cfg.price_bumps.default_price_bump, 25); + assert_eq!(reth_cfg.price_bumps.replace_blob_tx_price_bump, 150); } } From 022294f63e7bc2a3d235273135e3292accef54d7 Mon Sep 17 00:00:00 2001 From: Muang Date: Mon, 29 Dec 2025 18:11:16 +0900 Subject: [PATCH 07/15] feat: implemented ~mp-5 --- crates/mempool/README.md | 82 ++++++++++++++++++++++++++++++++++++++ crates/mempool/src/pool.rs | 18 +++++++++ 2 files changed, 100 insertions(+) create mode 100644 crates/mempool/README.md diff --git a/crates/mempool/README.md b/crates/mempool/README.md new file mode 100644 index 0000000..11a81ca --- /dev/null +++ b/crates/mempool/README.md @@ -0,0 +1,82 @@ +# Mempool integration notes + +This crate wraps Reth's transaction pool and adds CipherBFT-specific validation. +Use these notes when wiring the pool in the EL/worker initialization. + +## MP-1 / MP-2 behavior + +- MP-1: `CipherBftPool` is a thin wrapper over Reth's pool, delegating pool behavior/config. +- MP-2: `add_transaction` performs BFT policy checks (min gas price, nonce gap) and then hands + validated transactions to Reth for standard validation. + +## Pool creation (required for MP-3 / MP-4) + +MP-3 (priority ordering) and MP-4 (replacement logic) are enforced by the Reth pool. +To enable them, you must pass a Reth `PoolConfig` and an ordering implementation +when instantiating the pool. + +Example (shape only; actual validator/blob store wiring depends on your node setup): + +```rust +use reth_transaction_pool::{ + Pool, CoinbaseTipOrdering, TransactionValidationTaskExecutor, +}; + +let pool_config = mempool_config.into(); // or mempool_config.to_reth_config() +let ordering = CoinbaseTipOrdering::default(); + +let pool = Pool::new( + tx_validator, + ordering, + blob_store, + pool_config, +); +``` + +Notes: +- MP-3 relies on the built-in ordering (`CoinbaseTipOrdering`) and `best_transactions()`. +- MP-4 relies on `PoolConfig.price_bumps` (set via `MempoolConfig` mapping). +- MP-5 relies on Reth's pending/queued pools and promotion logic. + +## MempoolConfig mapping + +`MempoolConfig` maps into Reth's `PoolConfig`, including price bump settings: + +```rust +let pool_config: PoolConfig = mempool_config.into(); +``` + +Relevant fields: +- `default_price_bump` (percent) +- `replace_blob_tx_price_bump` (percent) + +## Inserting transactions + +`CipherBftPool::add_transaction` accepts several input types: +- `TransactionSigned` +- `TransactionSignedEcRecovered` +- `PooledTransactionsElement` +- `PooledTransactionsElementEcRecovered` +- `TransactionSignedNoHash` +- raw bytes (`Bytes`, `Vec`, `&[u8]`) decoded as EIP-2718 + +If you already have the pool's transaction type, use: + +```rust +pool.add_pooled_transaction(origin, pooled_tx).await?; +``` + +## Batch selection + +`CipherBftPoolAdapter::get_transactions_for_batch` uses +`pool.best_transactions()` and returns `TransactionSigned` values in the +order determined by the pool's ordering. + +## Pending / queued access (MP-5) + +Use the adapter helpers: + +```rust +let pending = pool.adapter().pending_transactions(); +let queued = pool.adapter().queued_transactions(); +``` diff --git a/crates/mempool/src/pool.rs b/crates/mempool/src/pool.rs index 5268f20..ca9740e 100644 --- a/crates/mempool/src/pool.rs +++ b/crates/mempool/src/pool.rs @@ -238,6 +238,24 @@ impl<'a, P: TransactionPool> CipherBftPoolAdapter<'a, P> { total: size.pending + size.queued, } } + + /// Get pending (executable) transactions in pool order (MP-5). + pub fn pending_transactions(&self) -> Vec { + self.pool + .pending_transactions() + .into_iter() + .map(|tx| tx.to_recovered_transaction().into_signed()) + .collect() + } + + /// Get queued (nonce-gap) transactions in pool order (MP-5). + pub fn queued_transactions(&self) -> Vec { + self.pool + .queued_transactions() + .into_iter() + .map(|tx| tx.to_recovered_transaction().into_signed()) + .collect() + } } impl

CipherBftPool

From b60978a76c0be0a0697f336f13d3e9307d96f7e2 Mon Sep 17 00:00:00 2001 From: Muang Date: Mon, 29 Dec 2025 18:26:21 +0900 Subject: [PATCH 08/15] test: added mempool overall tests --- crates/mempool/Cargo.toml | 4 + crates/mempool/tests/mempool_tests.rs | 119 ++++++++++++++++++++++++++ 2 files changed, 123 insertions(+) create mode 100644 crates/mempool/tests/mempool_tests.rs diff --git a/crates/mempool/Cargo.toml b/crates/mempool/Cargo.toml index 2fc21e0..a1d5987 100644 --- a/crates/mempool/Cargo.toml +++ b/crates/mempool/Cargo.toml @@ -29,3 +29,7 @@ async-trait = { workspace = true } # Logging tracing = { workspace = true } + +[dev-dependencies] +reth-transaction-pool = { git = "https://github.com/paradigmxyz/reth", rev = "v1.1.0", features = ["test-utils"] } +reth-provider = { git = "https://github.com/paradigmxyz/reth", rev = "v1.1.0", features = ["test-utils"] } diff --git a/crates/mempool/tests/mempool_tests.rs b/crates/mempool/tests/mempool_tests.rs new file mode 100644 index 0000000..12093f0 --- /dev/null +++ b/crates/mempool/tests/mempool_tests.rs @@ -0,0 +1,119 @@ +use alloy_primitives::address; +use mempool::{CipherBftPool, MempoolConfig}; +use reth_primitives::TransactionSignedEcRecovered; +use reth_provider::test_utils::NoopProvider; +use reth_storage_api::StateProviderBox; +use reth_transaction_pool::{PoolConfig, SubPoolLimit, TransactionOrigin}; +use reth_transaction_pool::test_utils::{MockTransaction, TestPoolBuilder}; + +fn noop_state_provider() -> StateProviderBox { + Box::new(NoopProvider::default()) +} + +fn recovered_tx( + sender: alloy_primitives::Address, + nonce: u64, + gas_price: u128, +) -> TransactionSignedEcRecovered { + let mock = MockTransaction::legacy() + .with_sender(sender) + .with_nonce(nonce) + .with_gas_price(gas_price); + TransactionSignedEcRecovered::from(mock) +} + +#[tokio::test] +async fn test_transaction_insertion_and_retrieval() { + let pool: reth_transaction_pool::test_utils::TestPool = TestPoolBuilder::default().into(); + let mempool: CipherBftPool = + CipherBftPool::new(pool, MempoolConfig::default(), noop_state_provider()); + + let tx = recovered_tx(address!("1000000000000000000000000000000000000001"), 0, 2_000_000_000); + let tx_hash = *tx.clone().into_signed().hash(); + + mempool.add_transaction(TransactionOrigin::External, tx).await.unwrap(); + + let pending = mempool.adapter().pending_transactions(); + assert_eq!(pending.len(), 1); + assert_eq!(*pending[0].hash(), tx_hash); +} + +#[tokio::test] +async fn test_priority_ordering_by_gas_price() { + let pool: reth_transaction_pool::test_utils::TestPool = TestPoolBuilder::default().into(); + let mempool: CipherBftPool = + CipherBftPool::new(pool, MempoolConfig::default(), noop_state_provider()); + + let low = recovered_tx(address!("1000000000000000000000000000000000000002"), 0, 1_500_000_000); + let high = recovered_tx(address!("1000000000000000000000000000000000000003"), 0, 3_000_000_000); + let high_hash = *high.clone().into_signed().hash(); + + mempool.add_transaction(TransactionOrigin::External, low).await.unwrap(); + mempool.add_transaction(TransactionOrigin::External, high).await.unwrap(); + + let batch = mempool.adapter().get_transactions_for_batch(2, 1_000_000_000); + assert_eq!(batch.len(), 2); + assert_eq!(*batch[0].hash(), high_hash); +} + +#[tokio::test] +async fn test_replacement_logic() { + let pool: reth_transaction_pool::test_utils::TestPool = TestPoolBuilder::default().into(); + let mempool: CipherBftPool = + CipherBftPool::new(pool, MempoolConfig::default(), noop_state_provider()); + + let sender = address!("1000000000000000000000000000000000000004"); + let low = recovered_tx(sender, 0, 1_000_000_000); + let high = recovered_tx(sender, 0, 2_000_000_000); + let high_hash = *high.clone().into_signed().hash(); + + mempool.add_transaction(TransactionOrigin::External, low).await.unwrap(); + mempool.add_transaction(TransactionOrigin::External, high).await.unwrap(); + + let pending = mempool.adapter().pending_transactions(); + assert_eq!(pending.len(), 1); + assert_eq!(*pending[0].hash(), high_hash); +} + +#[tokio::test] +async fn test_pending_queued_promotion() { + let pool: reth_transaction_pool::test_utils::TestPool = TestPoolBuilder::default().into(); + let mempool: CipherBftPool = + CipherBftPool::new(pool, MempoolConfig::default(), noop_state_provider()); + + let sender = address!("1000000000000000000000000000000000000005"); + let nonce_one = recovered_tx(sender, 1, 2_000_000_000); + let nonce_zero = recovered_tx(sender, 0, 2_000_000_000); + + mempool.add_transaction(TransactionOrigin::External, nonce_one).await.unwrap(); + assert_eq!(mempool.adapter().pending_transactions().len(), 0); + assert_eq!(mempool.adapter().queued_transactions().len(), 1); + + mempool.add_transaction(TransactionOrigin::External, nonce_zero).await.unwrap(); + assert_eq!(mempool.adapter().queued_transactions().len(), 0); + assert_eq!(mempool.adapter().pending_transactions().len(), 2); +} + +#[tokio::test] +async fn test_eviction_under_pressure() { + let mut config = PoolConfig::default(); + config.pending_limit = SubPoolLimit::new(1, usize::MAX); + config.basefee_limit = SubPoolLimit::new(0, usize::MAX); + config.queued_limit = SubPoolLimit::new(0, usize::MAX); + config.blob_limit = SubPoolLimit::new(0, usize::MAX); + config.max_account_slots = 1; + + let pool: reth_transaction_pool::test_utils::TestPool = + TestPoolBuilder::default().with_config(config).into(); + let mempool: CipherBftPool = + CipherBftPool::new(pool, MempoolConfig::default(), noop_state_provider()); + + let low = recovered_tx(address!("1000000000000000000000000000000000000010"), 0, 1_000_000_000); + let high = recovered_tx(address!("1000000000000000000000000000000000000011"), 0, 2_000_000_000); + + mempool.add_transaction(TransactionOrigin::External, low).await.unwrap(); + mempool.add_transaction(TransactionOrigin::External, high).await.unwrap(); + + let pending = mempool.adapter().pending_transactions(); + assert_eq!(pending.len(), 1); +} From 97ee6e9a2c757481698d974a8eeedfa8c735e5b7 Mon Sep 17 00:00:00 2001 From: Muang Date: Wed, 31 Dec 2025 18:13:22 +0900 Subject: [PATCH 09/15] feat: add tx validator wrapper --- crates/mempool/src/lib.rs | 2 + crates/mempool/src/validator.rs | 67 +++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 crates/mempool/src/validator.rs diff --git a/crates/mempool/src/lib.rs b/crates/mempool/src/lib.rs index 2759ae5..31ca5d2 100644 --- a/crates/mempool/src/lib.rs +++ b/crates/mempool/src/lib.rs @@ -17,8 +17,10 @@ pub mod error; pub mod config; pub mod transaction; pub mod pool; +pub mod validator; pub use error::MempoolError; pub use config::MempoolConfig; pub use transaction::TransactionOrdering; pub use pool::CipherBftPool; +pub use validator::CipherBftValidator; diff --git a/crates/mempool/src/validator.rs b/crates/mempool/src/validator.rs new file mode 100644 index 0000000..6cdcff0 --- /dev/null +++ b/crates/mempool/src/validator.rs @@ -0,0 +1,67 @@ +//! CipherBFT transaction validator wrapper for Reth's pool. + +use reth_primitives::InvalidTransactionError; +use reth_transaction_pool::{ + error::InvalidPoolTransactionError, PoolTransaction, TransactionOrigin, + TransactionValidationOutcome, TransactionValidator, +}; + +/// CipherBFT-specific validation that wraps a Reth `TransactionValidator`. +#[derive(Debug, Clone)] +pub struct CipherBftValidator { + inner: V, + chain_id: u64, +} + +impl CipherBftValidator { + /// Create a new wrapper around the given validator. + pub fn new(inner: V, chain_id: u64) -> Self { + Self { inner, chain_id } + } +} +// to add custom validation logic, modify the validate_transaction method +impl TransactionValidator for CipherBftValidator { + type Transaction = V::Transaction; + + async fn validate_transaction( + &self, + origin: TransactionOrigin, + transaction: Self::Transaction, + ) -> TransactionValidationOutcome { + + self.inner.validate_transaction(origin, transaction).await + } +} + +#[cfg(test)] +mod tests { + use super::*; + use reth_transaction_pool::noop::MockTransactionValidator; + use reth_transaction_pool::test_utils::MockTransaction; + + #[tokio::test] + async fn test_chain_id_mismatch_invalid() { + let inner = MockTransactionValidator::::default(); + let validator = CipherBftValidator::new(inner, 2); + let tx = MockTransaction::legacy(); + + let outcome = validator + .validate_transaction(TransactionOrigin::External, tx) + .await; + + assert!(outcome.is_invalid()); + } + + #[tokio::test] + async fn test_chain_id_match_valid() { + let inner = MockTransactionValidator::::default(); + let validator = CipherBftValidator::new(inner, 1); + let tx = MockTransaction::legacy(); + + let outcome = validator + .validate_transaction(TransactionOrigin::External, tx) + .await; + + assert!(outcome.is_valid()); + } +} From 81c889ca29080a5e8e3bcd9597df9fe685ceb083 Mon Sep 17 00:00:00 2001 From: Muang Date: Sat, 3 Jan 2026 02:30:51 +0900 Subject: [PATCH 10/15] fix: resolve clippy warnings and update cargo-deny ignores --- crates/mempool/src/lib.rs | 8 +-- crates/mempool/src/pool.rs | 28 ++++----- crates/mempool/src/validator.rs | 6 +- crates/mempool/tests/mempool_tests.rs | 85 +++++++++++++++++++++------ deny.toml | 9 ++- 5 files changed, 93 insertions(+), 43 deletions(-) diff --git a/crates/mempool/src/lib.rs b/crates/mempool/src/lib.rs index 31ca5d2..0a09584 100644 --- a/crates/mempool/src/lib.rs +++ b/crates/mempool/src/lib.rs @@ -13,14 +13,14 @@ //! - `account`: Per-account state management //! - `pool`: Main pool adapter over Reth's TransactionPool -pub mod error; pub mod config; -pub mod transaction; +pub mod error; pub mod pool; +pub mod transaction; pub mod validator; -pub use error::MempoolError; pub use config::MempoolConfig; -pub use transaction::TransactionOrdering; +pub use error::MempoolError; pub use pool::CipherBftPool; +pub use transaction::TransactionOrdering; pub use validator::CipherBftValidator; diff --git a/crates/mempool/src/pool.rs b/crates/mempool/src/pool.rs index ca9740e..dd4442b 100644 --- a/crates/mempool/src/pool.rs +++ b/crates/mempool/src/pool.rs @@ -41,11 +41,11 @@ use crate::config::MempoolConfig; use crate::error::MempoolError; use alloy_eips::eip2718::Decodable2718; +use alloy_primitives::{Bytes, TxHash}; use reth_primitives::{ PooledTransactionsElement, PooledTransactionsElementEcRecovered, TransactionSigned, TransactionSignedEcRecovered, TransactionSignedNoHash, }; -use alloy_primitives::{Bytes, TxHash}; use reth_storage_api::{StateProvider, StateProviderBox}; use reth_transaction_pool::{PoolTransaction, TransactionOrigin, TransactionPool}; use tracing::{debug, warn}; @@ -67,7 +67,6 @@ pub struct CipherBftPool { state_provider: StateProviderBox, } - impl CipherBftPool

{ /// Create new mempool wrapper /// @@ -113,9 +112,7 @@ impl CipherBftPool

{ &self, tx: TransactionSigned, ) -> Result { - let tx_recovered = tx - .try_ecrecovered() - .ok_or(MempoolError::InvalidSignature)?; + let tx_recovered = tx.try_ecrecovered().ok_or(MempoolError::InvalidSignature)?; let sender = tx_recovered.signer(); let tx_ref = tx_recovered.as_ref(); @@ -169,10 +166,11 @@ impl CipherBftPool

{ // BFT Policy 2: Nonce gap enforcement // Prevents attackers from bloating the queued pool with distant-future nonces - let current_nonce = self.state_provider + let current_nonce = self + .state_provider .account_nonce(sender) .map_err(|e| MempoolError::Internal(format!("Failed to get nonce: {}", e)))? - .unwrap_or(0); // Default to 0 if account doesn't exist yet + .unwrap_or(0); // Default to 0 if account doesn't exist yet let tx_nonce = tx_ref.nonce(); if tx_nonce > current_nonce { @@ -408,7 +406,7 @@ where } } -impl<'a, Tx> IntoPoolTransactionInput for &'a [u8] +impl IntoPoolTransactionInput for &[u8] where Tx: PoolTransaction, Tx::Pooled: From, @@ -451,21 +449,19 @@ mod tests { assert_eq!(config.min_gas_price, 1_000_000_000); } - - #[test] fn test_bft_policy_min_gas_price() { // BFT Policy: Minimum gas price enforcement let min_gas_price = 1_000_000_000u128; - + // Below minimum - rejected by CipherBFT let gas_price = 500_000_000u128; assert!(gas_price < min_gas_price); - + // At minimum - accepted let gas_price = 1_000_000_000u128; assert!(gas_price >= min_gas_price); - + // Above minimum - accepted let gas_price = 2_000_000_000u128; assert!(gas_price >= min_gas_price); @@ -478,19 +474,19 @@ mod tests { // CipherBFT adds gap limit for far-future nonces let current_nonce = 10u64; let max_gap = 16u64; - + // No gap (next nonce) - accepted let tx_nonce = 11u64; let gap = tx_nonce - current_nonce - 1; assert_eq!(gap, 0); assert!(gap <= max_gap); - + // Gap within limit - accepted let tx_nonce = 26u64; let gap = tx_nonce - current_nonce - 1; assert_eq!(gap, 15); assert!(gap <= max_gap); - + // Gap exceeds limit - rejected by CipherBFT let tx_nonce = 28u64; let gap = tx_nonce - current_nonce - 1; diff --git a/crates/mempool/src/validator.rs b/crates/mempool/src/validator.rs index 6cdcff0..ca30018 100644 --- a/crates/mempool/src/validator.rs +++ b/crates/mempool/src/validator.rs @@ -1,9 +1,7 @@ //! CipherBFT transaction validator wrapper for Reth's pool. -use reth_primitives::InvalidTransactionError; use reth_transaction_pool::{ - error::InvalidPoolTransactionError, PoolTransaction, TransactionOrigin, - TransactionValidationOutcome, TransactionValidator, + TransactionOrigin, TransactionValidationOutcome, TransactionValidator, }; /// CipherBFT-specific validation that wraps a Reth `TransactionValidator`. @@ -28,7 +26,7 @@ impl TransactionValidator for CipherBftValidator { origin: TransactionOrigin, transaction: Self::Transaction, ) -> TransactionValidationOutcome { - + let _ = self.chain_id; self.inner.validate_transaction(origin, transaction).await } } diff --git a/crates/mempool/tests/mempool_tests.rs b/crates/mempool/tests/mempool_tests.rs index 12093f0..ea34971 100644 --- a/crates/mempool/tests/mempool_tests.rs +++ b/crates/mempool/tests/mempool_tests.rs @@ -3,8 +3,8 @@ use mempool::{CipherBftPool, MempoolConfig}; use reth_primitives::TransactionSignedEcRecovered; use reth_provider::test_utils::NoopProvider; use reth_storage_api::StateProviderBox; -use reth_transaction_pool::{PoolConfig, SubPoolLimit, TransactionOrigin}; use reth_transaction_pool::test_utils::{MockTransaction, TestPoolBuilder}; +use reth_transaction_pool::{PoolConfig, SubPoolLimit, TransactionOrigin}; fn noop_state_provider() -> StateProviderBox { Box::new(NoopProvider::default()) @@ -28,10 +28,17 @@ async fn test_transaction_insertion_and_retrieval() { let mempool: CipherBftPool = CipherBftPool::new(pool, MempoolConfig::default(), noop_state_provider()); - let tx = recovered_tx(address!("1000000000000000000000000000000000000001"), 0, 2_000_000_000); + let tx = recovered_tx( + address!("1000000000000000000000000000000000000001"), + 0, + 2_000_000_000, + ); let tx_hash = *tx.clone().into_signed().hash(); - mempool.add_transaction(TransactionOrigin::External, tx).await.unwrap(); + mempool + .add_transaction(TransactionOrigin::External, tx) + .await + .unwrap(); let pending = mempool.adapter().pending_transactions(); assert_eq!(pending.len(), 1); @@ -44,14 +51,30 @@ async fn test_priority_ordering_by_gas_price() { let mempool: CipherBftPool = CipherBftPool::new(pool, MempoolConfig::default(), noop_state_provider()); - let low = recovered_tx(address!("1000000000000000000000000000000000000002"), 0, 1_500_000_000); - let high = recovered_tx(address!("1000000000000000000000000000000000000003"), 0, 3_000_000_000); + let low = recovered_tx( + address!("1000000000000000000000000000000000000002"), + 0, + 1_500_000_000, + ); + let high = recovered_tx( + address!("1000000000000000000000000000000000000003"), + 0, + 3_000_000_000, + ); let high_hash = *high.clone().into_signed().hash(); - mempool.add_transaction(TransactionOrigin::External, low).await.unwrap(); - mempool.add_transaction(TransactionOrigin::External, high).await.unwrap(); - - let batch = mempool.adapter().get_transactions_for_batch(2, 1_000_000_000); + mempool + .add_transaction(TransactionOrigin::External, low) + .await + .unwrap(); + mempool + .add_transaction(TransactionOrigin::External, high) + .await + .unwrap(); + + let batch = mempool + .adapter() + .get_transactions_for_batch(2, 1_000_000_000); assert_eq!(batch.len(), 2); assert_eq!(*batch[0].hash(), high_hash); } @@ -67,8 +90,14 @@ async fn test_replacement_logic() { let high = recovered_tx(sender, 0, 2_000_000_000); let high_hash = *high.clone().into_signed().hash(); - mempool.add_transaction(TransactionOrigin::External, low).await.unwrap(); - mempool.add_transaction(TransactionOrigin::External, high).await.unwrap(); + mempool + .add_transaction(TransactionOrigin::External, low) + .await + .unwrap(); + mempool + .add_transaction(TransactionOrigin::External, high) + .await + .unwrap(); let pending = mempool.adapter().pending_transactions(); assert_eq!(pending.len(), 1); @@ -85,11 +114,17 @@ async fn test_pending_queued_promotion() { let nonce_one = recovered_tx(sender, 1, 2_000_000_000); let nonce_zero = recovered_tx(sender, 0, 2_000_000_000); - mempool.add_transaction(TransactionOrigin::External, nonce_one).await.unwrap(); + mempool + .add_transaction(TransactionOrigin::External, nonce_one) + .await + .unwrap(); assert_eq!(mempool.adapter().pending_transactions().len(), 0); assert_eq!(mempool.adapter().queued_transactions().len(), 1); - mempool.add_transaction(TransactionOrigin::External, nonce_zero).await.unwrap(); + mempool + .add_transaction(TransactionOrigin::External, nonce_zero) + .await + .unwrap(); assert_eq!(mempool.adapter().queued_transactions().len(), 0); assert_eq!(mempool.adapter().pending_transactions().len(), 2); } @@ -108,11 +143,25 @@ async fn test_eviction_under_pressure() { let mempool: CipherBftPool = CipherBftPool::new(pool, MempoolConfig::default(), noop_state_provider()); - let low = recovered_tx(address!("1000000000000000000000000000000000000010"), 0, 1_000_000_000); - let high = recovered_tx(address!("1000000000000000000000000000000000000011"), 0, 2_000_000_000); - - mempool.add_transaction(TransactionOrigin::External, low).await.unwrap(); - mempool.add_transaction(TransactionOrigin::External, high).await.unwrap(); + let low = recovered_tx( + address!("1000000000000000000000000000000000000010"), + 0, + 1_000_000_000, + ); + let high = recovered_tx( + address!("1000000000000000000000000000000000000011"), + 0, + 2_000_000_000, + ); + + mempool + .add_transaction(TransactionOrigin::External, low) + .await + .unwrap(); + mempool + .add_transaction(TransactionOrigin::External, high) + .await + .unwrap(); let pending = mempool.adapter().pending_transactions(); assert_eq!(pending.len(), 1); diff --git a/deny.toml b/deny.toml index df5f7b6..ca94615 100644 --- a/deny.toml +++ b/deny.toml @@ -8,7 +8,14 @@ all-features = true [advisories] version = 2 db-path = "~/.cargo/advisory-db" -ignore = [] +ignore = [ + # paste crate unmaintained - transitive dependency from alloy-primitives 1.x + # Used as proc-macro only, not a runtime security concern + "RUSTSEC-2024-0436", + # proc-macro-error unmaintained - transitive dependency via reth tooling + # Used as proc-macro only, not a runtime security concern + "RUSTSEC-2024-0370", +] [licenses] version = 2 From 1a47768707c3de5a54d34af048524eef9014e60e Mon Sep 17 00:00:00 2001 From: Muang Date: Sat, 3 Jan 2026 02:35:43 +0900 Subject: [PATCH 11/15] fix: fixed test suite --- crates/mempool/tests/mempool_tests.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/crates/mempool/tests/mempool_tests.rs b/crates/mempool/tests/mempool_tests.rs index ea34971..771390e 100644 --- a/crates/mempool/tests/mempool_tests.rs +++ b/crates/mempool/tests/mempool_tests.rs @@ -131,12 +131,14 @@ async fn test_pending_queued_promotion() { #[tokio::test] async fn test_eviction_under_pressure() { - let mut config = PoolConfig::default(); - config.pending_limit = SubPoolLimit::new(1, usize::MAX); - config.basefee_limit = SubPoolLimit::new(0, usize::MAX); - config.queued_limit = SubPoolLimit::new(0, usize::MAX); - config.blob_limit = SubPoolLimit::new(0, usize::MAX); - config.max_account_slots = 1; + let config = PoolConfig { + pending_limit: SubPoolLimit::new(1, usize::MAX), + basefee_limit: SubPoolLimit::new(0, usize::MAX), + queued_limit: SubPoolLimit::new(0, usize::MAX), + blob_limit: SubPoolLimit::new(0, usize::MAX), + max_account_slots: 1, + ..Default::default() + }; let pool: reth_transaction_pool::test_utils::TestPool = TestPoolBuilder::default().with_config(config).into(); From caca252e4601af5209d5ac0aba3bd5cc5cf817db Mon Sep 17 00:00:00 2001 From: Muang Date: Sat, 3 Jan 2026 03:20:01 +0900 Subject: [PATCH 12/15] refactor: standardize mempool validator and pool constructors --- crates/mempool/Cargo.toml | 1 + crates/mempool/src/pool.rs | 43 +++++++++++++-- crates/mempool/src/validator.rs | 75 +++++++++++++++++++++++---- crates/mempool/tests/mempool_tests.rs | 10 ++-- 4 files changed, 110 insertions(+), 19 deletions(-) diff --git a/crates/mempool/Cargo.toml b/crates/mempool/Cargo.toml index a1d5987..f4e1082 100644 --- a/crates/mempool/Cargo.toml +++ b/crates/mempool/Cargo.toml @@ -11,6 +11,7 @@ repository.workspace = true reth-transaction-pool = { git = "https://github.com/paradigmxyz/reth", rev = "v1.1.0" } reth-primitives = { git = "https://github.com/paradigmxyz/reth", rev = "v1.1.0" } reth-storage-api = { git = "https://github.com/paradigmxyz/reth", rev = "v1.1.0" } +reth-chainspec = { git = "https://github.com/paradigmxyz/reth", rev = "v1.1.0" } # Ethereum types (alloy suite - use Reth compatible version) alloy-primitives = "0.8" diff --git a/crates/mempool/src/pool.rs b/crates/mempool/src/pool.rs index dd4442b..f33953a 100644 --- a/crates/mempool/src/pool.rs +++ b/crates/mempool/src/pool.rs @@ -40,14 +40,19 @@ use crate::config::MempoolConfig; use crate::error::MempoolError; +use crate::validator::CipherBftValidator; use alloy_eips::eip2718::Decodable2718; use alloy_primitives::{Bytes, TxHash}; use reth_primitives::{ PooledTransactionsElement, PooledTransactionsElementEcRecovered, TransactionSigned, TransactionSignedEcRecovered, TransactionSignedNoHash, }; -use reth_storage_api::{StateProvider, StateProviderBox}; -use reth_transaction_pool::{PoolTransaction, TransactionOrigin, TransactionPool}; +use reth_storage_api::{StateProvider, StateProviderBox, StateProviderFactory}; +use reth_transaction_pool::{ + blobstore::BlobStore, validate::EthTransactionValidator, CoinbaseTipOrdering, + EthPooledTransaction, Pool, PoolTransaction, TransactionOrigin, TransactionPool, +}; +use std::sync::Arc; use tracing::{debug, warn}; /// Mempool wrapper that adds BFT-specific pre-validation to Reth's Pool @@ -67,6 +72,13 @@ pub struct CipherBftPool { state_provider: StateProviderBox, } +/// Concrete Reth pool type used by CipherBFT. +pub type CipherBftRethPool = Pool< + CipherBftValidator>, + CoinbaseTipOrdering, + S, +>; + impl CipherBftPool

{ /// Create new mempool wrapper /// @@ -80,9 +92,9 @@ impl CipherBftPool

{ /// config.to_reth_config(), /// ); /// // Mempool lives in DCL, uses EL's StateProvider for validation - /// CipherBftPool::new(reth_pool, config, state_provider) + /// CipherBftPool::wrap(reth_pool, config, state_provider) /// ``` - pub fn new(pool: P, config: MempoolConfig, state_provider: StateProviderBox) -> Self { + pub fn wrap(pool: P, config: MempoolConfig, state_provider: StateProviderBox) -> Self { Self { pool, config, @@ -187,6 +199,29 @@ impl CipherBftPool

{ } } +impl CipherBftPool> +where + Client: StateProviderFactory, + S: BlobStore + Clone, +{ + /// Create a CipherBFT mempool that builds the underlying Reth pool internally. + pub fn new( + chain_spec: Arc, + client: Client, + blob_store: S, + chain_id: u64, + config: MempoolConfig, + ) -> Result { + let state_provider = client + .latest() + .map_err(|err| MempoolError::Internal(format!("Failed to get state provider: {err}")))?; + let validator = CipherBftValidator::new(chain_spec, client, blob_store.clone(), chain_id); + let pool_config: reth_transaction_pool::PoolConfig = config.clone().into(); + let pool = Pool::new(validator, CoinbaseTipOrdering::default(), blob_store, pool_config); + Ok(Self::wrap(pool, config, state_provider)) + } +} + /// Worker-facing adapter that surfaces pool operations (ADR-006) pub struct CipherBftPoolAdapter<'a, P: TransactionPool> { pool: &'a P, diff --git a/crates/mempool/src/validator.rs b/crates/mempool/src/validator.rs index ca30018..ab7dec8 100644 --- a/crates/mempool/src/validator.rs +++ b/crates/mempool/src/validator.rs @@ -1,8 +1,12 @@ //! CipherBFT transaction validator wrapper for Reth's pool. +use reth_storage_api::StateProviderFactory; use reth_transaction_pool::{ - TransactionOrigin, TransactionValidationOutcome, TransactionValidator, + blobstore::BlobStore, + validate::{EthTransactionValidator, EthTransactionValidatorBuilder}, + EthPoolTransaction, TransactionOrigin, TransactionValidationOutcome, TransactionValidator, }; +use std::sync::Arc; /// CipherBFT-specific validation that wraps a Reth `TransactionValidator`. #[derive(Debug, Clone)] @@ -13,10 +17,30 @@ pub struct CipherBftValidator { impl CipherBftValidator { /// Create a new wrapper around the given validator. - pub fn new(inner: V, chain_id: u64) -> Self { + pub fn wrap(inner: V, chain_id: u64) -> Self { Self { inner, chain_id } } } + +impl CipherBftValidator> +where + Client: StateProviderFactory, + Tx: EthPoolTransaction, +{ + /// Build a wrapper with a reth EthTransactionValidator. + pub fn new( + chain_spec: Arc, + client: Client, + blob_store: S, + chain_id: u64, + ) -> Self + where + S: BlobStore, + { + let validator = EthTransactionValidatorBuilder::new(chain_spec).build(client, blob_store); + Self::wrap(validator, chain_id) + } +} // to add custom validation logic, modify the validate_transaction method impl TransactionValidator for CipherBftValidator { type Transaction = V::Transaction; @@ -34,14 +58,45 @@ impl TransactionValidator for CipherBftValidator { #[cfg(test)] mod tests { use super::*; - use reth_transaction_pool::noop::MockTransactionValidator; - use reth_transaction_pool::test_utils::MockTransaction; + use alloy_primitives::{Address, U256}; + use reth_chainspec::{Chain, ChainSpecBuilder, MAINNET}; + use reth_provider::test_utils::{ExtendedAccount, MockEthProvider}; + use reth_transaction_pool::blobstore::InMemoryBlobStore; + use reth_transaction_pool::test_utils::TransactionBuilder; + use reth_transaction_pool::validate::{EthTransactionValidator, EthTransactionValidatorBuilder}; + use reth_transaction_pool::EthPooledTransaction; + use reth_transaction_pool::PoolTransaction; + use std::sync::Arc; + + fn build_test_pooled_tx(chain_id: u64) -> EthPooledTransaction { + TransactionBuilder::default() + .chain_id(chain_id) + .to(Address::ZERO) + .gas_limit(100_000) + .max_fee_per_gas(1_000_000_000) + .max_priority_fee_per_gas(1_000_000_000) + .into_eip1559() + .into_ecrecovered() + .expect("recover signed transaction") + .try_into() + .expect("convert to pooled transaction") + } + + fn build_test_validator( + chain_id: u64, + tx: &EthPooledTransaction, + ) -> CipherBftValidator> { + let chain_spec = ChainSpecBuilder::mainnet().chain(Chain::from_id(chain_id)).build(); + let provider = MockEthProvider::default(); + provider.add_account(tx.sender(), ExtendedAccount::new(tx.nonce(), U256::MAX)); + let blob_store = InMemoryBlobStore::default(); + CipherBftValidator::new(Arc::new(chain_spec), provider, blob_store, chain_id) + } #[tokio::test] async fn test_chain_id_mismatch_invalid() { - let inner = MockTransactionValidator::::default(); - let validator = CipherBftValidator::new(inner, 2); - let tx = MockTransaction::legacy(); + let tx = build_test_pooled_tx(MAINNET.chain.id()); + let validator = build_test_validator(2, &tx); let outcome = validator .validate_transaction(TransactionOrigin::External, tx) @@ -52,9 +107,9 @@ mod tests { #[tokio::test] async fn test_chain_id_match_valid() { - let inner = MockTransactionValidator::::default(); - let validator = CipherBftValidator::new(inner, 1); - let tx = MockTransaction::legacy(); + let chain_id = MAINNET.chain.id(); + let tx = build_test_pooled_tx(chain_id); + let validator = build_test_validator(chain_id, &tx); let outcome = validator .validate_transaction(TransactionOrigin::External, tx) diff --git a/crates/mempool/tests/mempool_tests.rs b/crates/mempool/tests/mempool_tests.rs index 771390e..269038d 100644 --- a/crates/mempool/tests/mempool_tests.rs +++ b/crates/mempool/tests/mempool_tests.rs @@ -26,7 +26,7 @@ fn recovered_tx( async fn test_transaction_insertion_and_retrieval() { let pool: reth_transaction_pool::test_utils::TestPool = TestPoolBuilder::default().into(); let mempool: CipherBftPool = - CipherBftPool::new(pool, MempoolConfig::default(), noop_state_provider()); + CipherBftPool::wrap(pool, MempoolConfig::default(), noop_state_provider()); let tx = recovered_tx( address!("1000000000000000000000000000000000000001"), @@ -49,7 +49,7 @@ async fn test_transaction_insertion_and_retrieval() { async fn test_priority_ordering_by_gas_price() { let pool: reth_transaction_pool::test_utils::TestPool = TestPoolBuilder::default().into(); let mempool: CipherBftPool = - CipherBftPool::new(pool, MempoolConfig::default(), noop_state_provider()); + CipherBftPool::wrap(pool, MempoolConfig::default(), noop_state_provider()); let low = recovered_tx( address!("1000000000000000000000000000000000000002"), @@ -83,7 +83,7 @@ async fn test_priority_ordering_by_gas_price() { async fn test_replacement_logic() { let pool: reth_transaction_pool::test_utils::TestPool = TestPoolBuilder::default().into(); let mempool: CipherBftPool = - CipherBftPool::new(pool, MempoolConfig::default(), noop_state_provider()); + CipherBftPool::wrap(pool, MempoolConfig::default(), noop_state_provider()); let sender = address!("1000000000000000000000000000000000000004"); let low = recovered_tx(sender, 0, 1_000_000_000); @@ -108,7 +108,7 @@ async fn test_replacement_logic() { async fn test_pending_queued_promotion() { let pool: reth_transaction_pool::test_utils::TestPool = TestPoolBuilder::default().into(); let mempool: CipherBftPool = - CipherBftPool::new(pool, MempoolConfig::default(), noop_state_provider()); + CipherBftPool::wrap(pool, MempoolConfig::default(), noop_state_provider()); let sender = address!("1000000000000000000000000000000000000005"); let nonce_one = recovered_tx(sender, 1, 2_000_000_000); @@ -143,7 +143,7 @@ async fn test_eviction_under_pressure() { let pool: reth_transaction_pool::test_utils::TestPool = TestPoolBuilder::default().with_config(config).into(); let mempool: CipherBftPool = - CipherBftPool::new(pool, MempoolConfig::default(), noop_state_provider()); + CipherBftPool::wrap(pool, MempoolConfig::default(), noop_state_provider()); let low = recovered_tx( address!("1000000000000000000000000000000000000010"), From 0a7eb81af861a3e085deb8fe1dbd1d580e8fa02a Mon Sep 17 00:00:00 2001 From: Muang Date: Sat, 3 Jan 2026 03:29:32 +0900 Subject: [PATCH 13/15] refactor: standardize mempool validator and pool constructors --- crates/mempool/src/pool.rs | 13 +++++++++---- crates/mempool/src/validator.rs | 6 ++++-- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/crates/mempool/src/pool.rs b/crates/mempool/src/pool.rs index f33953a..c3b1bef 100644 --- a/crates/mempool/src/pool.rs +++ b/crates/mempool/src/pool.rs @@ -212,12 +212,17 @@ where chain_id: u64, config: MempoolConfig, ) -> Result { - let state_provider = client - .latest() - .map_err(|err| MempoolError::Internal(format!("Failed to get state provider: {err}")))?; + let state_provider = client.latest().map_err(|err| { + MempoolError::Internal(format!("Failed to get state provider: {err}")) + })?; let validator = CipherBftValidator::new(chain_spec, client, blob_store.clone(), chain_id); let pool_config: reth_transaction_pool::PoolConfig = config.clone().into(); - let pool = Pool::new(validator, CoinbaseTipOrdering::default(), blob_store, pool_config); + let pool = Pool::new( + validator, + CoinbaseTipOrdering::default(), + blob_store, + pool_config, + ); Ok(Self::wrap(pool, config, state_provider)) } } diff --git a/crates/mempool/src/validator.rs b/crates/mempool/src/validator.rs index ab7dec8..8c73ce4 100644 --- a/crates/mempool/src/validator.rs +++ b/crates/mempool/src/validator.rs @@ -63,7 +63,7 @@ mod tests { use reth_provider::test_utils::{ExtendedAccount, MockEthProvider}; use reth_transaction_pool::blobstore::InMemoryBlobStore; use reth_transaction_pool::test_utils::TransactionBuilder; - use reth_transaction_pool::validate::{EthTransactionValidator, EthTransactionValidatorBuilder}; + use reth_transaction_pool::validate::EthTransactionValidator; use reth_transaction_pool::EthPooledTransaction; use reth_transaction_pool::PoolTransaction; use std::sync::Arc; @@ -86,7 +86,9 @@ mod tests { chain_id: u64, tx: &EthPooledTransaction, ) -> CipherBftValidator> { - let chain_spec = ChainSpecBuilder::mainnet().chain(Chain::from_id(chain_id)).build(); + let chain_spec = ChainSpecBuilder::mainnet() + .chain(Chain::from_id(chain_id)) + .build(); let provider = MockEthProvider::default(); provider.add_account(tx.sender(), ExtendedAccount::new(tx.nonce(), U256::MAX)); let blob_store = InMemoryBlobStore::default(); From a25a59f34ed694f3d0af395eaa9ba7900ec76ab0 Mon Sep 17 00:00:00 2001 From: Muang Date: Sat, 3 Jan 2026 10:38:47 +0900 Subject: [PATCH 14/15] docs: modified readme --- crates/mempool/README.md | 53 ++++++++++++++++++++++++++-------------- 1 file changed, 35 insertions(+), 18 deletions(-) diff --git a/crates/mempool/README.md b/crates/mempool/README.md index 11a81ca..6d0f415 100644 --- a/crates/mempool/README.md +++ b/crates/mempool/README.md @@ -1,7 +1,7 @@ # Mempool integration notes This crate wraps Reth's transaction pool and adds CipherBFT-specific validation. -Use these notes when wiring the pool in the EL/worker initialization. +Use these notes when wiring the pool in node/worker initialization. ## MP-1 / MP-2 behavior @@ -9,34 +9,51 @@ Use these notes when wiring the pool in the EL/worker initialization. - MP-2: `add_transaction` performs BFT policy checks (min gas price, nonce gap) and then hands validated transactions to Reth for standard validation. +## Validator creation (optional) + +You do not need to create a validator directly if you use `CipherBftPool::new(...)`. +This section is only for cases where you want to build or wrap a validator manually. + +`CipherBftValidator::new` builds and wraps a Reth `EthTransactionValidator`. +It requires: +- `ChainSpec` (for chain ID and fork rules) +- `StateProviderFactory` (EL/Storage-backed) +- `BlobStore` (in-memory or persistent) + +Example (shape only; actual types depend on your node setup): + +```rust +use reth_transaction_pool::blobstore::InMemoryBlobStore; + +let validator = CipherBftValidator::new( + chain_spec, + state_provider_factory, + InMemoryBlobStore::default(), + chain_id, +); +``` + +If you already have a validator instance, use `CipherBftValidator::wrap`. + ## Pool creation (required for MP-3 / MP-4) MP-3 (priority ordering) and MP-4 (replacement logic) are enforced by the Reth pool. To enable them, you must pass a Reth `PoolConfig` and an ordering implementation when instantiating the pool. -Example (shape only; actual validator/blob store wiring depends on your node setup): +Preferred: build the pool through `CipherBftPool::new` (it creates the Reth pool internally). ```rust -use reth_transaction_pool::{ - Pool, CoinbaseTipOrdering, TransactionValidationTaskExecutor, -}; - -let pool_config = mempool_config.into(); // or mempool_config.to_reth_config() -let ordering = CoinbaseTipOrdering::default(); - -let pool = Pool::new( - tx_validator, - ordering, +let mempool = CipherBftPool::new( + chain_spec, + state_provider_factory, blob_store, - pool_config, -); + chain_id, + mempool_config, +)?; ``` -Notes: -- MP-3 relies on the built-in ordering (`CoinbaseTipOrdering`) and `best_transactions()`. -- MP-4 relies on `PoolConfig.price_bumps` (set via `MempoolConfig` mapping). -- MP-5 relies on Reth's pending/queued pools and promotion logic. +If you already have a Reth pool instance, use `CipherBftPool::wrap(pool, config, state_provider)`. ## MempoolConfig mapping From 8c36a3c62924ddaaa057e4aa554467d790b08fe6 Mon Sep 17 00:00:00 2001 From: Muang Date: Mon, 5 Jan 2026 17:07:27 +0900 Subject: [PATCH 15/15] docs: modified readme --- crates/mempool/README.md | 126 ++++++++++++++++++--------------------- 1 file changed, 58 insertions(+), 68 deletions(-) diff --git a/crates/mempool/README.md b/crates/mempool/README.md index 6d0f415..6f11777 100644 --- a/crates/mempool/README.md +++ b/crates/mempool/README.md @@ -1,99 +1,89 @@ -# Mempool integration notes +# CipherBFT Mempool This crate wraps Reth's transaction pool and adds CipherBFT-specific validation. -Use these notes when wiring the pool in node/worker initialization. -## MP-1 / MP-2 behavior +## What is implemented -- MP-1: `CipherBftPool` is a thin wrapper over Reth's pool, delegating pool behavior/config. -- MP-2: `add_transaction` performs BFT policy checks (min gas price, nonce gap) and then hands - validated transactions to Reth for standard validation. +- `CipherBftPool

`: thin wrapper over Reth's pool (`pool.rs`) +- `CipherBftValidator`: wrapper for Reth validator (`validator.rs`) +- BFT policy checks: min gas price + nonce gap (inside `validate_bft_policy`) +- `MempoolConfig -> PoolConfig` mapping (`config.rs`) +- Worker adapter: pending/queued/batch helpers (`CipherBftPoolAdapter`) -## Validator creation (optional) +## Pool creation -You do not need to create a validator directly if you use `CipherBftPool::new(...)`. -This section is only for cases where you want to build or wrap a validator manually. +### Recommended (build internally) -`CipherBftValidator::new` builds and wraps a Reth `EthTransactionValidator`. -It requires: -- `ChainSpec` (for chain ID and fork rules) -- `StateProviderFactory` (EL/Storage-backed) -- `BlobStore` (in-memory or persistent) - -Example (shape only; actual types depend on your node setup): +`CipherBftPool::new(...)` builds the Reth pool and validator internally. ```rust +use cipherbft_mempool::{CipherBftPool, MempoolConfig}; +use reth_chainspec::ChainSpec; +use reth_provider::StateProviderFactory; use reth_transaction_pool::blobstore::InMemoryBlobStore; +use std::sync::Arc; -let validator = CipherBftValidator::new( - chain_spec, - state_provider_factory, - InMemoryBlobStore::default(), - chain_id, -); -``` +let chain_spec: Arc = Arc::new(/* ... */); +let client: impl StateProviderFactory = /* ... */; +let blob_store = InMemoryBlobStore::default(); +let chain_id = 1; +let config = MempoolConfig::default(); -If you already have a validator instance, use `CipherBftValidator::wrap`. - -## Pool creation (required for MP-3 / MP-4) +let pool = CipherBftPool::new(chain_spec, client, blob_store, chain_id, config)?; +``` -MP-3 (priority ordering) and MP-4 (replacement logic) are enforced by the Reth pool. -To enable them, you must pass a Reth `PoolConfig` and an ordering implementation -when instantiating the pool. +### Wrap an existing Reth pool -Preferred: build the pool through `CipherBftPool::new` (it creates the Reth pool internally). +Use this when you already constructed a `Pool`. ```rust -let mempool = CipherBftPool::new( - chain_spec, - state_provider_factory, +use cipherbft_mempool::{CipherBftPool, CipherBftValidator, MempoolConfig}; +use reth_transaction_pool::{Pool, CoinbaseTipOrdering, PoolConfig}; + +let state_provider = client.latest()?; +let validator = CipherBftValidator::new(chain_spec, client, blob_store.clone(), chain_id); +let pool_config: PoolConfig = mempool_config.clone().into(); +let reth_pool = Pool::new( + validator, + CoinbaseTipOrdering::default(), blob_store, - chain_id, - mempool_config, -)?; -``` + pool_config, +); -If you already have a Reth pool instance, use `CipherBftPool::wrap(pool, config, state_provider)`. +let pool = CipherBftPool::wrap(reth_pool, mempool_config, state_provider); +``` -## MempoolConfig mapping +## Transaction insertion -`MempoolConfig` maps into Reth's `PoolConfig`, including price bump settings: +`add_transaction` accepts several input types (`TransactionSigned`, recovered, pooled, raw bytes): ```rust -let pool_config: PoolConfig = mempool_config.into(); -``` +use reth_transaction_pool::TransactionOrigin; -Relevant fields: -- `default_price_bump` (percent) -- `replace_blob_tx_price_bump` (percent) - -## Inserting transactions - -`CipherBftPool::add_transaction` accepts several input types: -- `TransactionSigned` -- `TransactionSignedEcRecovered` -- `PooledTransactionsElement` -- `PooledTransactionsElementEcRecovered` -- `TransactionSignedNoHash` -- raw bytes (`Bytes`, `Vec`, `&[u8]`) decoded as EIP-2718 +pool.add_transaction(TransactionOrigin::External, tx_signed).await?; +pool.add_transaction(TransactionOrigin::External, tx_recovered).await?; +pool.add_transaction(TransactionOrigin::External, pooled).await?; +``` -If you already have the pool's transaction type, use: +Or insert pooled transactions directly: ```rust -pool.add_pooled_transaction(origin, pooled_tx).await?; +pool.add_pooled_transaction(TransactionOrigin::Local, pooled_tx).await?; ``` -## Batch selection - -`CipherBftPoolAdapter::get_transactions_for_batch` uses -`pool.best_transactions()` and returns `TransactionSigned` values in the -order determined by the pool's ordering. - -## Pending / queued access (MP-5) - -Use the adapter helpers: +## Adapter helpers (worker-facing) ```rust -let pending = pool.adapter().pending_transactions(); -let queued = pool.adapter().queued_transactions(); +let adapter = pool.adapter(); +let pending = adapter.pending_transactions(); +let queued = adapter.queued_transactions(); +let batch = adapter.get_transactions_for_batch(100, 30_000_000); +let stats = adapter.stats(); +adapter.remove_finalized(&tx_hashes); ``` + +## Notes + +- BFT policy checks are enforced before handing transactions to Reth. +- Standard Ethereum validation is delegated to Reth. +- Worker integration is not yet wired in the node.