Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 56 additions & 7 deletions crates/transaction-pool/src/pool/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 = added.transaction_state();

// transaction was successfully inserted into the pool
if let Some(sidecar) = maybe_sidecar {
Expand Down Expand Up @@ -1160,6 +1157,8 @@ pub enum AddedTransaction<T: PoolTransaction> {
replaced: Option<Arc<ValidPoolTransaction<T>>>,
/// The subpool it was moved to.
subpool: SubPool,
/// The specific reason why the transaction is queued (if applicable).
queued_reason: Option<QueuedReason>,
},
}

Expand Down Expand Up @@ -1229,27 +1228,77 @@ impl<T: PoolTransaction> AddedTransaction<T> {
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(),
}
}

/// 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)
#[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
Expand Down
52 changes: 52 additions & 0 deletions crates/transaction-pool/src/pool/state.rs
Original file line number Diff line number Diff line change
@@ -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.
///
Expand Down Expand Up @@ -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<QueuedReason> {
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
Expand Down
37 changes: 9 additions & 28 deletions crates/transaction-pool/src/pool/txpool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -641,31 +641,6 @@ impl<T: TransactionOrdering> TxPool<T> {
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`.
pub(crate) fn add_transaction(
&mut self,
tx: ValidPoolTransaction<T::Transaction>,
Expand All @@ -686,7 +661,7 @@ impl<T: TransactionOrdering> TxPool<T> {
.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
Expand All @@ -704,7 +679,14 @@ impl<T: TransactionOrdering> TxPool<T> {
replaced,
})
} else {
AddedTransaction::Parked { transaction, subpool: move_to, replaced }
// Determine the specific queued reason based on the transaction state
let queued_reason = state.determine_queued_reason(move_to);
AddedTransaction::Parked {
transaction,
subpool: move_to,
replaced,
queued_reason,
}
};

// Update size metrics after adding and potentially moving transactions.
Expand Down Expand Up @@ -2128,7 +2110,6 @@ pub(crate) struct InsertOk<T: PoolTransaction> {
/// 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<ValidPoolTransaction<T>>, SubPool)>,
Expand Down
Loading