From 76ac4e835b039c552a4b2f7bc32cf3ce7a0e056f Mon Sep 17 00:00:00 2001 From: Sarah Date: Thu, 28 Aug 2025 00:15:13 +0800 Subject: [PATCH 1/5] Implements granular transaction pool state tracking --- crates/transaction-pool/src/pool/mod.rs | 79 ++++++++++++++++++++++--- 1 file changed, 71 insertions(+), 8 deletions(-) diff --git a/crates/transaction-pool/src/pool/mod.rs b/crates/transaction-pool/src/pool/mod.rs index 415a7cfe881..3ee67811c70 100644 --- a/crates/transaction-pool/src/pool/mod.rs +++ b/crates/transaction-pool/src/pool/mod.rs @@ -75,7 +75,7 @@ use crate::{ BlobTransactionSidecarListener, PendingTransactionHashListener, PoolEventBroadcast, TransactionListener, }, - state::SubPool, + state::{SubPool, TxState}, txpool::{SenderInfo, TxPool}, update::UpdateOutcome, }, @@ -510,10 +510,7 @@ where let added = pool.add_transaction(tx, balance, state_nonce, bytecode_hash)?; let hash = *added.hash(); - let state = match added.subpool() { - SubPool::Pending => AddedTransactionState::Pending, - _ => AddedTransactionState::Queued, - }; + let state = Self::determine_transaction_state(&added, pool); // transaction was successfully inserted into the pool if let Some(sidecar) = maybe_sidecar { @@ -1055,6 +1052,47 @@ where .collect(); self.delete_blobs(blob_txs); } + + /// Determines the specific reason why a transaction was added to the pool. + /// + /// Maps the transaction's subpool assignment to a granular `AddedTransactionState` + /// that indicates the specific condition preventing it from being pending. + fn determine_transaction_state( + added: &AddedTransaction, + pool: &RwLockWriteGuard<'_, TxPool>, + ) -> AddedTransactionState { + match added.subpool() { + SubPool::Pending => AddedTransactionState::Pending, + SubPool::Queued => { + let tx_id = match added { + AddedTransaction::Pending(pending_tx) => pending_tx.transaction.id(), + AddedTransaction::Parked { transaction, .. } => transaction.id(), + }; + + if let Some(internal_tx) = pool.all().get(tx_id) { + let state = internal_tx.state; + + if !state.contains(TxState::NO_NONCE_GAPS) { + AddedTransactionState::Queued(QueuedReason::NonceGap) + } else if !state.contains(TxState::ENOUGH_BALANCE) { + AddedTransactionState::Queued(QueuedReason::InsufficientBalance) + } else if !state.contains(TxState::NO_PARKED_ANCESTORS) { + AddedTransactionState::Queued(QueuedReason::ParkedAncestors) + } else if !state.contains(TxState::NOT_TOO_MUCH_GAS) { + AddedTransactionState::Queued(QueuedReason::TooMuchGas) + } else { + // Fallback for unexpected queued state - could be due to other conditions + AddedTransactionState::Queued(QueuedReason::NonceGap) + } + } else { + // Fallback if we can't find the transaction in the pool + AddedTransactionState::Queued(QueuedReason::NonceGap) + } + } + SubPool::BaseFee => AddedTransactionState::Queued(QueuedReason::InsufficientBaseFee), + SubPool::Blob => AddedTransactionState::Queued(QueuedReason::InsufficientBlobFee), + } + } } impl fmt::Debug for PoolInner { @@ -1231,25 +1269,50 @@ impl AddedTransaction { } } +/// The specific reason why a transaction is queued (not ready for execution) +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum QueuedReason { + /// Transaction has a nonce gap - missing prior transactions + NonceGap, + /// Transaction has parked ancestors - waiting for other transactions to be mined + ParkedAncestors, + /// Sender has insufficient balance to cover the transaction cost + InsufficientBalance, + /// Transaction exceeds the block gas limit + TooMuchGas, + /// Transaction doesn't meet the base fee requirement + InsufficientBaseFee, + /// Transaction doesn't meet the blob fee requirement (EIP-4844) + InsufficientBlobFee, +} + /// The state of a transaction when is was added to the pool #[derive(Debug, Clone, PartialEq, Eq)] pub enum AddedTransactionState { /// Ready for execution Pending, - /// Not ready for execution due to a nonce gap or insufficient balance - Queued, // TODO: Break it down to missing nonce, insufficient balance, etc. + /// Not ready for execution due to a specific condition + Queued(QueuedReason), } impl AddedTransactionState { /// Returns whether the transaction was submitted as queued. pub const fn is_queued(&self) -> bool { - matches!(self, Self::Queued) + matches!(self, Self::Queued(_)) } /// Returns whether the transaction was submitted as pending. pub const fn is_pending(&self) -> bool { matches!(self, Self::Pending) } + + /// Returns the specific queued reason if the transaction is queued. + pub const fn queued_reason(&self) -> Option<&QueuedReason> { + match self { + Self::Queued(reason) => Some(reason), + Self::Pending => None, + } + } } /// The outcome of a successful transaction addition From 528cf0c9b80525215228e2edbd3b8ef5a1e69d67 Mon Sep 17 00:00:00 2001 From: Sarah Date: Wed, 10 Sep 2025 10:52:24 +0800 Subject: [PATCH 2/5] add 'queued_reason' to 'AddedTransaction' --- crates/transaction-pool/src/pool/mod.rs | 43 +++++++++------------- crates/transaction-pool/src/pool/txpool.rs | 33 +++++++++++++++-- 2 files changed, 47 insertions(+), 29 deletions(-) diff --git a/crates/transaction-pool/src/pool/mod.rs b/crates/transaction-pool/src/pool/mod.rs index 3ee67811c70..94fa9c9d86a 100644 --- a/crates/transaction-pool/src/pool/mod.rs +++ b/crates/transaction-pool/src/pool/mod.rs @@ -75,7 +75,7 @@ use crate::{ BlobTransactionSidecarListener, PendingTransactionHashListener, PoolEventBroadcast, TransactionListener, }, - state::{SubPool, TxState}, + state::SubPool, txpool::{SenderInfo, TxPool}, update::UpdateOutcome, }, @@ -1059,38 +1059,19 @@ where /// that indicates the specific condition preventing it from being pending. fn determine_transaction_state( added: &AddedTransaction, - pool: &RwLockWriteGuard<'_, TxPool>, + _pool: &RwLockWriteGuard<'_, TxPool>, ) -> AddedTransactionState { match added.subpool() { SubPool::Pending => AddedTransactionState::Pending, - SubPool::Queued => { - let tx_id = match added { - AddedTransaction::Pending(pending_tx) => pending_tx.transaction.id(), - AddedTransaction::Parked { transaction, .. } => transaction.id(), - }; - - if let Some(internal_tx) = pool.all().get(tx_id) { - let state = internal_tx.state; - - if !state.contains(TxState::NO_NONCE_GAPS) { - AddedTransactionState::Queued(QueuedReason::NonceGap) - } else if !state.contains(TxState::ENOUGH_BALANCE) { - AddedTransactionState::Queued(QueuedReason::InsufficientBalance) - } else if !state.contains(TxState::NO_PARKED_ANCESTORS) { - AddedTransactionState::Queued(QueuedReason::ParkedAncestors) - } else if !state.contains(TxState::NOT_TOO_MUCH_GAS) { - AddedTransactionState::Queued(QueuedReason::TooMuchGas) - } else { - // Fallback for unexpected queued state - could be due to other conditions - AddedTransactionState::Queued(QueuedReason::NonceGap) - } + _ => { + // For non-pending transactions, use the queued reason directly from the AddedTransaction + if let Some(reason) = added.queued_reason() { + AddedTransactionState::Queued(reason.clone()) } else { - // Fallback if we can't find the transaction in the pool + // Fallback - this shouldn't happen with the new implementation AddedTransactionState::Queued(QueuedReason::NonceGap) } } - SubPool::BaseFee => AddedTransactionState::Queued(QueuedReason::InsufficientBaseFee), - SubPool::Blob => AddedTransactionState::Queued(QueuedReason::InsufficientBlobFee), } } } @@ -1198,6 +1179,8 @@ pub enum AddedTransaction { replaced: Option>>, /// The subpool it was moved to. subpool: SubPool, + /// The specific reason why the transaction is queued (if applicable). + queued_reason: Option, }, } @@ -1267,6 +1250,14 @@ impl AddedTransaction { Self::Parked { transaction, .. } => transaction.id(), } } + + /// Returns the queued reason if the transaction is parked with a queued reason. + pub(crate) const fn queued_reason(&self) -> Option<&QueuedReason> { + match self { + Self::Pending(_) => None, + Self::Parked { queued_reason, .. } => queued_reason.as_ref(), + } + } } /// The specific reason why a transaction is queued (not ready for execution) diff --git a/crates/transaction-pool/src/pool/txpool.rs b/crates/transaction-pool/src/pool/txpool.rs index ad56c2ba78b..f247edcaee5 100644 --- a/crates/transaction-pool/src/pool/txpool.rs +++ b/crates/transaction-pool/src/pool/txpool.rs @@ -15,7 +15,7 @@ use crate::{ pending::PendingPool, state::{SubPool, TxState}, update::{Destination, PoolUpdate, UpdateOutcome}, - AddedPendingTransaction, AddedTransaction, OnNewCanonicalStateOutcome, + AddedPendingTransaction, AddedTransaction, OnNewCanonicalStateOutcome, QueuedReason, }, traits::{BestTransactionsAttributes, BlockInfo, PoolSize}, PoolConfig, PoolResult, PoolTransaction, PoolUpdateKind, PriceBumpConfig, TransactionOrdering, @@ -654,6 +654,31 @@ impl TxPool { /// requirement, or blob fee requirement. Transactions become executable only if the /// transaction `feeCap` is greater than the block's `baseFee` and the `maxBlobFee` is greater /// than the block's `blobFee`. + + /// Determines the specific reason why a transaction is queued based on its subpool and state. + fn determine_queued_reason(subpool: SubPool, state: TxState) -> Option { + match subpool { + SubPool::Pending => None, // Not queued + SubPool::Queued => { + // Check state flags to determine specific reason + if !state.contains(TxState::NO_NONCE_GAPS) { + Some(QueuedReason::NonceGap) + } else if !state.contains(TxState::ENOUGH_BALANCE) { + Some(QueuedReason::InsufficientBalance) + } else if !state.contains(TxState::NO_PARKED_ANCESTORS) { + Some(QueuedReason::ParkedAncestors) + } else if !state.contains(TxState::NOT_TOO_MUCH_GAS) { + Some(QueuedReason::TooMuchGas) + } else { + // Fallback for unexpected queued state + Some(QueuedReason::NonceGap) + } + } + SubPool::BaseFee => Some(QueuedReason::InsufficientBaseFee), + SubPool::Blob => Some(QueuedReason::InsufficientBlobFee), + } + } + pub(crate) fn add_transaction( &mut self, tx: ValidPoolTransaction, @@ -674,7 +699,7 @@ impl TxPool { .update(on_chain_nonce, on_chain_balance); match self.all_transactions.insert_tx(tx, on_chain_balance, on_chain_nonce) { - Ok(InsertOk { transaction, move_to, replaced_tx, updates, .. }) => { + Ok(InsertOk { transaction, move_to, replaced_tx, updates, state }) => { // replace the new tx and remove the replaced in the subpool(s) self.add_new_transaction(transaction.clone(), replaced_tx.clone(), move_to); // Update inserted transactions metric @@ -692,7 +717,9 @@ impl TxPool { replaced, }) } else { - AddedTransaction::Parked { transaction, subpool: move_to, replaced } + // Determine the specific queued reason based on the transaction state + let queued_reason = Self::determine_queued_reason(move_to, state); + AddedTransaction::Parked { transaction, subpool: move_to, replaced, queued_reason } }; // Update size metrics after adding and potentially moving transactions. From 8d29f04934599d35740bbd9f808ac9cd076a71d9 Mon Sep 17 00:00:00 2001 From: Sarah Date: Wed, 10 Sep 2025 20:27:27 +0800 Subject: [PATCH 3/5] make clippy happy --- crates/transaction-pool/src/pool/mod.rs | 3 ++- crates/transaction-pool/src/pool/txpool.rs | 12 ++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/crates/transaction-pool/src/pool/mod.rs b/crates/transaction-pool/src/pool/mod.rs index 94fa9c9d86a..d5ddb3820b4 100644 --- a/crates/transaction-pool/src/pool/mod.rs +++ b/crates/transaction-pool/src/pool/mod.rs @@ -1064,7 +1064,8 @@ where match added.subpool() { SubPool::Pending => AddedTransactionState::Pending, _ => { - // For non-pending transactions, use the queued reason directly from the AddedTransaction + // For non-pending transactions, use the queued reason directly from the + // AddedTransaction if let Some(reason) = added.queued_reason() { AddedTransactionState::Queued(reason.clone()) } else { diff --git a/crates/transaction-pool/src/pool/txpool.rs b/crates/transaction-pool/src/pool/txpool.rs index f247edcaee5..43666536658 100644 --- a/crates/transaction-pool/src/pool/txpool.rs +++ b/crates/transaction-pool/src/pool/txpool.rs @@ -654,9 +654,9 @@ impl TxPool { /// requirement, or blob fee requirement. Transactions become executable only if the /// transaction `feeCap` is greater than the block's `baseFee` and the `maxBlobFee` is greater /// than the block's `blobFee`. - + /// /// Determines the specific reason why a transaction is queued based on its subpool and state. - fn determine_queued_reason(subpool: SubPool, state: TxState) -> Option { + const fn determine_queued_reason(subpool: SubPool, state: TxState) -> Option { match subpool { SubPool::Pending => None, // Not queued SubPool::Queued => { @@ -719,7 +719,12 @@ impl TxPool { } else { // Determine the specific queued reason based on the transaction state let queued_reason = Self::determine_queued_reason(move_to, state); - AddedTransaction::Parked { transaction, subpool: move_to, replaced, queued_reason } + AddedTransaction::Parked { + transaction, + subpool: move_to, + replaced, + queued_reason, + } }; // Update size metrics after adding and potentially moving transactions. @@ -2128,7 +2133,6 @@ pub(crate) struct InsertOk { /// Where to move the transaction to. move_to: SubPool, /// Current state of the inserted tx. - #[cfg_attr(not(test), expect(dead_code))] state: TxState, /// The transaction that was replaced by this. replaced_tx: Option<(Arc>, SubPool)>, From 852a0c88e4a805682d10c3e95a8ce0351bcae523 Mon Sep 17 00:00:00 2001 From: Sarah Date: Wed, 10 Sep 2025 22:53:48 +0800 Subject: [PATCH 4/5] Refactor: move transaction state logic to 'AddedTransaction::transaction_state' for clarity --- crates/transaction-pool/src/pool/mod.rs | 42 +++++++++++-------------- 1 file changed, 18 insertions(+), 24 deletions(-) diff --git a/crates/transaction-pool/src/pool/mod.rs b/crates/transaction-pool/src/pool/mod.rs index d5ddb3820b4..ce308cf9867 100644 --- a/crates/transaction-pool/src/pool/mod.rs +++ b/crates/transaction-pool/src/pool/mod.rs @@ -510,7 +510,7 @@ where let added = pool.add_transaction(tx, balance, state_nonce, bytecode_hash)?; let hash = *added.hash(); - let state = Self::determine_transaction_state(&added, pool); + let state = added.transaction_state(); // transaction was successfully inserted into the pool if let Some(sidecar) = maybe_sidecar { @@ -1052,29 +1052,6 @@ where .collect(); self.delete_blobs(blob_txs); } - - /// Determines the specific reason why a transaction was added to the pool. - /// - /// Maps the transaction's subpool assignment to a granular `AddedTransactionState` - /// that indicates the specific condition preventing it from being pending. - fn determine_transaction_state( - added: &AddedTransaction, - _pool: &RwLockWriteGuard<'_, TxPool>, - ) -> AddedTransactionState { - match added.subpool() { - SubPool::Pending => AddedTransactionState::Pending, - _ => { - // For non-pending transactions, use the queued reason directly from the - // AddedTransaction - if let Some(reason) = added.queued_reason() { - AddedTransactionState::Queued(reason.clone()) - } else { - // Fallback - this shouldn't happen with the new implementation - AddedTransactionState::Queued(QueuedReason::NonceGap) - } - } - } - } } impl fmt::Debug for PoolInner { @@ -1259,6 +1236,23 @@ impl AddedTransaction { Self::Parked { queued_reason, .. } => queued_reason.as_ref(), } } + + /// Returns the transaction state based on the subpool and queued reason. + pub(crate) fn transaction_state(&self) -> AddedTransactionState { + match self.subpool() { + SubPool::Pending => AddedTransactionState::Pending, + _ => { + // For non-pending transactions, use the queued reason directly from the + // AddedTransaction + if let Some(reason) = self.queued_reason() { + AddedTransactionState::Queued(reason.clone()) + } else { + // Fallback - this shouldn't happen with the new implementation + AddedTransactionState::Queued(QueuedReason::NonceGap) + } + } + } + } } /// The specific reason why a transaction is queued (not ready for execution) From 75ed0e21ce6523938a191e99d6ea33a0b7ac6fdd Mon Sep 17 00:00:00 2001 From: Matthias Seitz Date: Fri, 12 Sep 2025 14:48:14 +0200 Subject: [PATCH 5/5] touchup --- crates/transaction-pool/src/pool/state.rs | 52 +++++++++++++++++++++ crates/transaction-pool/src/pool/txpool.rs | 54 +--------------------- 2 files changed, 54 insertions(+), 52 deletions(-) diff --git a/crates/transaction-pool/src/pool/state.rs b/crates/transaction-pool/src/pool/state.rs index e04b463343e..187d472f5ae 100644 --- a/crates/transaction-pool/src/pool/state.rs +++ b/crates/transaction-pool/src/pool/state.rs @@ -1,3 +1,5 @@ +use crate::pool::QueuedReason; + bitflags::bitflags! { /// Marker to represents the current state of a transaction in the pool and from which the corresponding sub-pool is derived, depending on what bits are set. /// @@ -68,6 +70,56 @@ impl TxState { pub(crate) const fn has_nonce_gap(&self) -> bool { !self.intersects(Self::NO_NONCE_GAPS) } + + /// Adds the transaction into the pool. + /// + /// This pool consists of four sub-pools: `Queued`, `Pending`, `BaseFee`, and `Blob`. + /// + /// The `Queued` pool contains transactions with gaps in its dependency tree: It requires + /// additional transactions that are note yet present in the pool. And transactions that the + /// sender can not afford with the current balance. + /// + /// The `Pending` pool contains all transactions that have no nonce gaps, and can be afforded by + /// the sender. It only contains transactions that are ready to be included in the pending + /// block. The pending pool contains all transactions that could be listed currently, but not + /// necessarily independently. However, this pool never contains transactions with nonce gaps. A + /// transaction is considered `ready` when it has the lowest nonce of all transactions from the + /// same sender. Which is equals to the chain nonce of the sender in the pending pool. + /// + /// The `BaseFee` pool contains transactions that currently can't satisfy the dynamic fee + /// requirement. With EIP-1559, transactions can become executable or not without any changes to + /// the sender's balance or nonce and instead their `feeCap` determines whether the + /// transaction is _currently_ (on the current state) ready or needs to be parked until the + /// `feeCap` satisfies the block's `baseFee`. + /// + /// The `Blob` pool contains _blob_ transactions that currently can't satisfy the dynamic fee + /// requirement, or blob fee requirement. Transactions become executable only if the + /// transaction `feeCap` is greater than the block's `baseFee` and the `maxBlobFee` is greater + /// than the block's `blobFee`. + /// + /// Determines the specific reason why a transaction is queued based on its subpool and state. + pub(crate) const fn determine_queued_reason(&self, subpool: SubPool) -> Option { + match subpool { + SubPool::Pending => None, // Not queued + SubPool::Queued => { + // Check state flags to determine specific reason + if !self.contains(Self::NO_NONCE_GAPS) { + Some(QueuedReason::NonceGap) + } else if !self.contains(Self::ENOUGH_BALANCE) { + Some(QueuedReason::InsufficientBalance) + } else if !self.contains(Self::NO_PARKED_ANCESTORS) { + Some(QueuedReason::ParkedAncestors) + } else if !self.contains(Self::NOT_TOO_MUCH_GAS) { + Some(QueuedReason::TooMuchGas) + } else { + // Fallback for unexpected queued state + Some(QueuedReason::NonceGap) + } + } + SubPool::BaseFee => Some(QueuedReason::InsufficientBaseFee), + SubPool::Blob => Some(QueuedReason::InsufficientBlobFee), + } + } } /// Identifier for the transaction Sub-pool diff --git a/crates/transaction-pool/src/pool/txpool.rs b/crates/transaction-pool/src/pool/txpool.rs index 69c73a00dfa..a25dc9b2919 100644 --- a/crates/transaction-pool/src/pool/txpool.rs +++ b/crates/transaction-pool/src/pool/txpool.rs @@ -15,7 +15,7 @@ use crate::{ pending::PendingPool, state::{SubPool, TxState}, update::{Destination, PoolUpdate, UpdateOutcome}, - AddedPendingTransaction, AddedTransaction, OnNewCanonicalStateOutcome, QueuedReason, + AddedPendingTransaction, AddedTransaction, OnNewCanonicalStateOutcome, }, traits::{BestTransactionsAttributes, BlockInfo, PoolSize}, PoolConfig, PoolResult, PoolTransaction, PoolUpdateKind, PriceBumpConfig, TransactionOrdering, @@ -641,56 +641,6 @@ impl TxPool { self.metrics.total_eip7702_transactions.set(eip7702_count as f64); } - /// Adds the transaction into the pool. - /// - /// This pool consists of four sub-pools: `Queued`, `Pending`, `BaseFee`, and `Blob`. - /// - /// The `Queued` pool contains transactions with gaps in its dependency tree: It requires - /// additional transactions that are note yet present in the pool. And transactions that the - /// sender can not afford with the current balance. - /// - /// The `Pending` pool contains all transactions that have no nonce gaps, and can be afforded by - /// the sender. It only contains transactions that are ready to be included in the pending - /// block. The pending pool contains all transactions that could be listed currently, but not - /// necessarily independently. However, this pool never contains transactions with nonce gaps. A - /// transaction is considered `ready` when it has the lowest nonce of all transactions from the - /// same sender. Which is equals to the chain nonce of the sender in the pending pool. - /// - /// The `BaseFee` pool contains transactions that currently can't satisfy the dynamic fee - /// requirement. With EIP-1559, transactions can become executable or not without any changes to - /// the sender's balance or nonce and instead their `feeCap` determines whether the - /// transaction is _currently_ (on the current state) ready or needs to be parked until the - /// `feeCap` satisfies the block's `baseFee`. - /// - /// The `Blob` pool contains _blob_ transactions that currently can't satisfy the dynamic fee - /// requirement, or blob fee requirement. Transactions become executable only if the - /// transaction `feeCap` is greater than the block's `baseFee` and the `maxBlobFee` is greater - /// than the block's `blobFee`. - /// - /// Determines the specific reason why a transaction is queued based on its subpool and state. - const fn determine_queued_reason(subpool: SubPool, state: TxState) -> Option { - match subpool { - SubPool::Pending => None, // Not queued - SubPool::Queued => { - // Check state flags to determine specific reason - if !state.contains(TxState::NO_NONCE_GAPS) { - Some(QueuedReason::NonceGap) - } else if !state.contains(TxState::ENOUGH_BALANCE) { - Some(QueuedReason::InsufficientBalance) - } else if !state.contains(TxState::NO_PARKED_ANCESTORS) { - Some(QueuedReason::ParkedAncestors) - } else if !state.contains(TxState::NOT_TOO_MUCH_GAS) { - Some(QueuedReason::TooMuchGas) - } else { - // Fallback for unexpected queued state - Some(QueuedReason::NonceGap) - } - } - SubPool::BaseFee => Some(QueuedReason::InsufficientBaseFee), - SubPool::Blob => Some(QueuedReason::InsufficientBlobFee), - } - } - pub(crate) fn add_transaction( &mut self, tx: ValidPoolTransaction, @@ -730,7 +680,7 @@ impl TxPool { }) } else { // Determine the specific queued reason based on the transaction state - let queued_reason = Self::determine_queued_reason(move_to, state); + let queued_reason = state.determine_queued_reason(move_to); AddedTransaction::Parked { transaction, subpool: move_to,