Skip to content

Commit bb1dfc9

Browse files
yongkangcmattsse
andauthored
perf(txpool): eliminate allocations in basefee enforcement (#18218)
Co-authored-by: Matthias Seitz <[email protected]>
1 parent a11655b commit bb1dfc9

File tree

2 files changed

+219
-18
lines changed

2 files changed

+219
-18
lines changed

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

Lines changed: 94 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -295,18 +295,43 @@ impl<T: PoolTransaction> ParkedPool<BasefeeOrd<T>> {
295295
transactions
296296
}
297297

298-
/// Removes all transactions and their dependent transaction from the subpool that no longer
299-
/// satisfy the given basefee.
298+
/// Removes all transactions from this subpool that can afford the given basefee,
299+
/// invoking the provided handler for each transaction as it is removed.
300+
///
301+
/// This method enforces the basefee constraint by identifying transactions that now
302+
/// satisfy the basefee requirement (typically after a basefee decrease) and processing
303+
/// them via the provided transaction handler closure.
304+
///
305+
/// Respects per-sender nonce ordering: if the lowest-nonce transaction for a sender
306+
/// still cannot afford the basefee, higher-nonce transactions from that sender are skipped.
300307
///
301308
/// Note: the transactions are not returned in a particular order.
302-
pub(crate) fn enforce_basefee(&mut self, basefee: u64) -> Vec<Arc<ValidPoolTransaction<T>>> {
309+
pub(crate) fn enforce_basefee_with<F>(&mut self, basefee: u64, mut tx_handler: F)
310+
where
311+
F: FnMut(Arc<ValidPoolTransaction<T>>),
312+
{
303313
let to_remove = self.satisfy_base_fee_ids(basefee as u128);
304314

305-
let mut removed = Vec::with_capacity(to_remove.len());
306315
for id in to_remove {
307-
removed.push(self.remove_transaction(&id).expect("transaction exists"));
316+
if let Some(tx) = self.remove_transaction(&id) {
317+
tx_handler(tx);
318+
}
308319
}
320+
}
309321

322+
/// Removes all transactions and their dependent transaction from the subpool that no longer
323+
/// satisfy the given basefee.
324+
///
325+
/// Legacy method maintained for compatibility with read-only queries.
326+
/// For basefee enforcement, prefer `enforce_basefee_with` for better performance.
327+
///
328+
/// Note: the transactions are not returned in a particular order.
329+
#[cfg(test)]
330+
pub(crate) fn enforce_basefee(&mut self, basefee: u64) -> Vec<Arc<ValidPoolTransaction<T>>> {
331+
let mut removed = Vec::new();
332+
self.enforce_basefee_with(basefee, |tx| {
333+
removed.push(tx);
334+
});
310335
removed
311336
}
312337
}
@@ -1039,4 +1064,68 @@ mod tests {
10391064
assert!(removed.is_some());
10401065
assert!(!pool.contains(&tx_id));
10411066
}
1067+
1068+
#[test]
1069+
fn test_enforce_basefee_with_handler_zero_allocation() {
1070+
let mut f = MockTransactionFactory::default();
1071+
let mut pool = ParkedPool::<BasefeeOrd<_>>::default();
1072+
1073+
// Add multiple transactions across different fee ranges
1074+
let sender_a = address!("0x000000000000000000000000000000000000000a");
1075+
let sender_b = address!("0x000000000000000000000000000000000000000b");
1076+
1077+
// Add transactions where nonce ordering allows proper processing:
1078+
// Sender A: both transactions can afford basefee (500 >= 400, 600 >= 400)
1079+
// Sender B: transaction cannot afford basefee (300 < 400)
1080+
let txs = vec![
1081+
f.validated_arc(
1082+
MockTransaction::eip1559()
1083+
.set_sender(sender_a)
1084+
.set_nonce(0)
1085+
.set_max_fee(500)
1086+
.clone(),
1087+
),
1088+
f.validated_arc(
1089+
MockTransaction::eip1559()
1090+
.set_sender(sender_a)
1091+
.set_nonce(1)
1092+
.set_max_fee(600)
1093+
.clone(),
1094+
),
1095+
f.validated_arc(
1096+
MockTransaction::eip1559()
1097+
.set_sender(sender_b)
1098+
.set_nonce(0)
1099+
.set_max_fee(300)
1100+
.clone(),
1101+
),
1102+
];
1103+
1104+
let expected_affordable = vec![txs[0].clone(), txs[1].clone()]; // Both sender A txs
1105+
for tx in txs {
1106+
pool.add_transaction(tx);
1107+
}
1108+
1109+
// Test the handler approach with zero allocations
1110+
let mut processed_txs = Vec::new();
1111+
let mut handler_call_count = 0;
1112+
1113+
pool.enforce_basefee_with(400, |tx| {
1114+
processed_txs.push(tx);
1115+
handler_call_count += 1;
1116+
});
1117+
1118+
// Verify correct number of transactions processed
1119+
assert_eq!(handler_call_count, 2);
1120+
assert_eq!(processed_txs.len(), 2);
1121+
1122+
// Verify the correct transactions were processed (those with fee >= 400)
1123+
let processed_ids: Vec<_> = processed_txs.iter().map(|tx| *tx.id()).collect();
1124+
for expected_tx in expected_affordable {
1125+
assert!(processed_ids.contains(expected_tx.id()));
1126+
}
1127+
1128+
// Verify transactions were removed from pool
1129+
assert_eq!(pool.len(), 1); // Only the 300 fee tx should remain
1130+
}
10421131
}

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

Lines changed: 125 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ use std::{
4040
ops::Bound::{Excluded, Unbounded},
4141
sync::Arc,
4242
};
43-
use tracing::trace;
43+
use tracing::{trace, warn};
4444

4545
#[cfg_attr(doc, aquamarine::aquamarine)]
4646
// TODO: Inlined diagram due to a bug in aquamarine library, should become an include when it's
@@ -293,19 +293,40 @@ impl<T: TransactionOrdering> TxPool<T> {
293293
Ordering::Greater
294294
}
295295
Ordering::Less => {
296-
// decreased base fee: recheck basefee pool and promote all that are now valid
297-
let removed =
298-
self.basefee_pool.enforce_basefee(self.all_transactions.pending_fees.base_fee);
299-
for tx in removed {
300-
let to = {
301-
let tx =
296+
// Base fee decreased: recheck BaseFee and promote.
297+
// Invariants:
298+
// - BaseFee contains only non-blob txs (blob txs live in Blob) and they already
299+
// have ENOUGH_BLOB_FEE_CAP_BLOCK.
300+
// - PENDING_POOL_BITS = BASE_FEE_POOL_BITS | ENOUGH_FEE_CAP_BLOCK |
301+
// ENOUGH_BLOB_FEE_CAP_BLOCK.
302+
// With the lower base fee they gain ENOUGH_FEE_CAP_BLOCK, so we can set the bit and
303+
// insert directly into Pending (skip generic routing).
304+
self.basefee_pool.enforce_basefee_with(
305+
self.all_transactions.pending_fees.base_fee,
306+
|tx| {
307+
// Update transaction state — guaranteed Pending by the invariants above
308+
let meta =
302309
self.all_transactions.txs.get_mut(tx.id()).expect("tx exists in set");
303-
tx.state.insert(TxState::ENOUGH_FEE_CAP_BLOCK);
304-
tx.subpool = tx.state.into();
305-
tx.subpool
306-
};
307-
self.add_transaction_to_subpool(to, tx);
308-
}
310+
meta.state.insert(TxState::ENOUGH_FEE_CAP_BLOCK);
311+
meta.subpool = meta.state.into();
312+
313+
trace!(target: "txpool", hash=%tx.transaction.hash(), pool=?meta.subpool, "Adding transaction to a subpool");
314+
match meta.subpool {
315+
SubPool::Queued => self.queued_pool.add_transaction(tx),
316+
SubPool::Pending => {
317+
self.pending_pool.add_transaction(tx, self.all_transactions.pending_fees.base_fee);
318+
}
319+
SubPool::Blob => {
320+
self.blob_pool.add_transaction(tx);
321+
}
322+
SubPool::BaseFee => {
323+
// This should be unreachable as transactions from BaseFee pool with
324+
// decreased basefee are guaranteed to become Pending
325+
warn!( target: "txpool", "BaseFee transactions should become Pending after basefee decrease");
326+
}
327+
}
328+
},
329+
);
309330

310331
Ordering::Less
311332
}
@@ -2954,6 +2975,97 @@ mod tests {
29542975
assert_eq!(pool.all_transactions.txs.get(&id).unwrap().subpool, SubPool::BaseFee)
29552976
}
29562977

2978+
#[test]
2979+
fn basefee_decrease_promotes_affordable_and_keeps_unaffordable() {
2980+
use alloy_primitives::address;
2981+
let mut f = MockTransactionFactory::default();
2982+
let mut pool = TxPool::new(MockOrdering::default(), Default::default());
2983+
2984+
// Create transactions that will be in basefee pool (can't afford initial high fee)
2985+
// Use different senders to avoid nonce gap issues
2986+
let sender_a = address!("0x000000000000000000000000000000000000000a");
2987+
let sender_b = address!("0x000000000000000000000000000000000000000b");
2988+
let sender_c = address!("0x000000000000000000000000000000000000000c");
2989+
2990+
let tx1 = MockTransaction::eip1559()
2991+
.set_sender(sender_a)
2992+
.set_nonce(0)
2993+
.set_max_fee(500)
2994+
.inc_limit();
2995+
let tx2 = MockTransaction::eip1559()
2996+
.set_sender(sender_b)
2997+
.set_nonce(0)
2998+
.set_max_fee(600)
2999+
.inc_limit();
3000+
let tx3 = MockTransaction::eip1559()
3001+
.set_sender(sender_c)
3002+
.set_nonce(0)
3003+
.set_max_fee(400)
3004+
.inc_limit();
3005+
3006+
// Set high initial basefee so transactions go to basefee pool
3007+
let mut block_info = pool.block_info();
3008+
block_info.pending_basefee = 700;
3009+
pool.set_block_info(block_info);
3010+
3011+
let validated1 = f.validated(tx1);
3012+
let validated2 = f.validated(tx2);
3013+
let validated3 = f.validated(tx3);
3014+
let id1 = *validated1.id();
3015+
let id2 = *validated2.id();
3016+
let id3 = *validated3.id();
3017+
3018+
// Add transactions - they should go to basefee pool due to high basefee
3019+
// All transactions have nonce 0 from different senders, so on_chain_nonce should be 0 for
3020+
// all
3021+
pool.add_transaction(validated1, U256::from(10_000), 0, None).unwrap();
3022+
pool.add_transaction(validated2, U256::from(10_000), 0, None).unwrap();
3023+
pool.add_transaction(validated3, U256::from(10_000), 0, None).unwrap();
3024+
3025+
// Debug: Check where transactions ended up
3026+
println!("Basefee pool len: {}", pool.basefee_pool.len());
3027+
println!("Pending pool len: {}", pool.pending_pool.len());
3028+
println!("tx1 subpool: {:?}", pool.all_transactions.txs.get(&id1).unwrap().subpool);
3029+
println!("tx2 subpool: {:?}", pool.all_transactions.txs.get(&id2).unwrap().subpool);
3030+
println!("tx3 subpool: {:?}", pool.all_transactions.txs.get(&id3).unwrap().subpool);
3031+
3032+
// Verify they're in basefee pool
3033+
assert_eq!(pool.basefee_pool.len(), 3);
3034+
assert_eq!(pool.pending_pool.len(), 0);
3035+
assert_eq!(pool.all_transactions.txs.get(&id1).unwrap().subpool, SubPool::BaseFee);
3036+
assert_eq!(pool.all_transactions.txs.get(&id2).unwrap().subpool, SubPool::BaseFee);
3037+
assert_eq!(pool.all_transactions.txs.get(&id3).unwrap().subpool, SubPool::BaseFee);
3038+
3039+
// Now decrease basefee to trigger the zero-allocation optimization
3040+
let mut block_info = pool.block_info();
3041+
block_info.pending_basefee = 450; // tx1 (500) and tx2 (600) can now afford it, tx3 (400) cannot
3042+
pool.set_block_info(block_info);
3043+
3044+
// Verify the optimization worked correctly:
3045+
// - tx1 and tx2 should be promoted to pending (mathematical certainty)
3046+
// - tx3 should remain in basefee pool
3047+
// - All state transitions should be correct
3048+
assert_eq!(pool.basefee_pool.len(), 1);
3049+
assert_eq!(pool.pending_pool.len(), 2);
3050+
3051+
// tx3 should still be in basefee pool (fee 400 < basefee 450)
3052+
assert_eq!(pool.all_transactions.txs.get(&id3).unwrap().subpool, SubPool::BaseFee);
3053+
3054+
// tx1 and tx2 should be in pending pool with correct state bits
3055+
let tx1_meta = pool.all_transactions.txs.get(&id1).unwrap();
3056+
let tx2_meta = pool.all_transactions.txs.get(&id2).unwrap();
3057+
assert_eq!(tx1_meta.subpool, SubPool::Pending);
3058+
assert_eq!(tx2_meta.subpool, SubPool::Pending);
3059+
assert!(tx1_meta.state.contains(TxState::ENOUGH_FEE_CAP_BLOCK));
3060+
assert!(tx2_meta.state.contains(TxState::ENOUGH_FEE_CAP_BLOCK));
3061+
3062+
// Verify that best_transactions returns the promoted transactions
3063+
let best: Vec<_> = pool.best_transactions().take(3).collect();
3064+
assert_eq!(best.len(), 2); // Only tx1 and tx2 should be returned
3065+
assert!(best.iter().any(|tx| tx.id() == &id1));
3066+
assert!(best.iter().any(|tx| tx.id() == &id2));
3067+
}
3068+
29573069
#[test]
29583070
fn get_highest_transaction_by_sender_and_nonce() {
29593071
// Set up a mock transaction factory and a new transaction pool.

0 commit comments

Comments
 (0)