feat(txpool): break down queued transaction states into specific reasons (#18106)

Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
This commit is contained in:
かとり
2025-09-12 20:03:41 +07:00
committed by GitHub
parent 51bf7e37e2
commit 72c2d1b6a0
3 changed files with 117 additions and 35 deletions

View File

@@ -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 {
@@ -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>,
},
}
@@ -1229,6 +1228,48 @@ 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
@@ -1236,20 +1277,28 @@ impl<T: PoolTransaction> AddedTransaction<T> {
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

View File

@@ -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<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

View File

@@ -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>,
@@ -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
@@ -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.
@@ -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)>,