Skip to content

Commit c7b6890

Browse files
authored
fix(txpool): ensure skipped high-priority transactions are tracked in BestTransactions (#19940)
1 parent 4467bc9 commit c7b6890

File tree

1 file changed

+43
-7
lines changed
  • crates/transaction-pool/src/pool

1 file changed

+43
-7
lines changed

crates/transaction-pool/src/pool/best.rs

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ impl<T: TransactionOrdering> BestTransactions<T> {
126126
}
127127

128128
/// Non-blocking read on the new pending transactions subscription channel
129-
fn try_recv(&mut self) -> Option<PendingTransaction<T>> {
129+
fn try_recv(&mut self) -> Option<IncomingTransaction<T>> {
130130
loop {
131131
match self.new_transaction_receiver.as_mut()?.try_recv() {
132132
Ok(tx) => {
@@ -135,9 +135,9 @@ impl<T: TransactionOrdering> BestTransactions<T> {
135135
{
136136
// we skip transactions if we already yielded a transaction with lower
137137
// priority
138-
return None
138+
return Some(IncomingTransaction::Stash(tx))
139139
}
140-
return Some(tx)
140+
return Some(IncomingTransaction::Process(tx))
141141
}
142142
// note TryRecvError::Lagged can be returned here, which is an error that attempts
143143
// to correct itself on consecutive try_recv() attempts
@@ -170,18 +170,54 @@ impl<T: TransactionOrdering> BestTransactions<T> {
170170
for _ in 0..MAX_NEW_TRANSACTIONS_PER_BATCH {
171171
if let Some(pending_tx) = self.try_recv() {
172172
// same logic as PendingPool::add_transaction/PendingPool::best_with_unlocked
173-
let tx_id = *pending_tx.transaction.id();
174-
if self.ancestor(&tx_id).is_none() {
175-
self.independent.insert(pending_tx.clone());
173+
174+
match pending_tx {
175+
IncomingTransaction::Process(tx) => {
176+
let tx_id = *tx.transaction.id();
177+
if self.ancestor(&tx_id).is_none() {
178+
self.independent.insert(tx.clone());
179+
}
180+
self.all.insert(tx_id, tx);
181+
}
182+
IncomingTransaction::Stash(tx) => {
183+
let tx_id = *tx.transaction.id();
184+
self.all.insert(tx_id, tx);
185+
}
176186
}
177-
self.all.insert(tx_id, pending_tx);
178187
} else {
179188
break;
180189
}
181190
}
182191
}
183192
}
184193

194+
/// Result of attempting to receive a new transaction from the channel during iteration.
195+
///
196+
/// This enum determines how a newly received transaction should be handled based on its priority
197+
/// relative to transactions already yielded by the iterator.
198+
enum IncomingTransaction<T: TransactionOrdering> {
199+
/// Process the transaction normally: add to both `all` map and potentially to `independent`
200+
/// set (if it has no ancestor).
201+
///
202+
/// This variant is used when the transaction's priority is lower than or equal to the last
203+
/// yielded transaction, meaning it can be safely processed without breaking the descending
204+
/// priority order.
205+
Process(PendingTransaction<T>),
206+
207+
/// Stash the transaction: add only to the `all` map, but NOT to the `independent` set.
208+
///
209+
/// This variant is used when the transaction has a higher priority than the last yielded
210+
/// transaction. We cannot yield it immediately (to maintain strict priority ordering), but we
211+
/// must still track it so that:
212+
/// - Its descendants can find it via `ancestor()` lookups
213+
/// - We prevent those descendants from being incorrectly promoted to `independent`
214+
///
215+
/// Without stashing, if a child of this transaction arrives later, it would fail to find its
216+
/// parent in `all`, be marked as `independent`, and be yielded out of order (before its
217+
/// parent), causing nonce gaps.
218+
Stash(PendingTransaction<T>),
219+
}
220+
185221
impl<T: TransactionOrdering> crate::traits::BestTransactions for BestTransactions<T> {
186222
fn mark_invalid(&mut self, tx: &Self::Item, kind: InvalidPoolTransactionError) {
187223
Self::mark_invalid(self, tx, kind)

0 commit comments

Comments
 (0)