mirror of
https://github.com/paradigmxyz/reth.git
synced 2026-01-10 07:48:19 -05:00
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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)>,
|
||||
|
||||
Reference in New Issue
Block a user