fix(txpool): prevent double-processing of tx pool tier (#18446)

Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
This commit is contained in:
YK
2025-09-19 19:10:01 +08:00
committed by GitHub
parent 5bc507bfaf
commit 8aeebe10ff

View File

@@ -221,7 +221,14 @@ impl<T: TransactionOrdering> TxPool<T> {
}
/// Updates the tracked blob fee
fn update_blob_fee(&mut self, mut pending_blob_fee: u128, base_fee_update: Ordering) {
fn update_blob_fee<F>(
&mut self,
mut pending_blob_fee: u128,
base_fee_update: Ordering,
mut on_promoted: F,
) where
F: FnMut(&Arc<ValidPoolTransaction<T::Transaction>>),
{
std::mem::swap(&mut self.all_transactions.pending_fees.blob_fee, &mut pending_blob_fee);
match (self.all_transactions.pending_fees.blob_fee.cmp(&pending_blob_fee), base_fee_update)
{
@@ -250,15 +257,20 @@ impl<T: TransactionOrdering> TxPool<T> {
let removed =
self.blob_pool.enforce_pending_fees(&self.all_transactions.pending_fees);
for tx in removed {
let to = {
let tx =
let subpool = {
let tx_meta =
self.all_transactions.txs.get_mut(tx.id()).expect("tx exists in set");
tx.state.insert(TxState::ENOUGH_BLOB_FEE_CAP_BLOCK);
tx.state.insert(TxState::ENOUGH_FEE_CAP_BLOCK);
tx.subpool = tx.state.into();
tx.subpool
tx_meta.state.insert(TxState::ENOUGH_BLOB_FEE_CAP_BLOCK);
tx_meta.state.insert(TxState::ENOUGH_FEE_CAP_BLOCK);
tx_meta.subpool = tx_meta.state.into();
tx_meta.subpool
};
self.add_transaction_to_subpool(to, tx);
if subpool == SubPool::Pending {
on_promoted(&tx);
}
self.add_transaction_to_subpool(subpool, tx);
}
}
}
@@ -268,7 +280,10 @@ impl<T: TransactionOrdering> TxPool<T> {
///
/// Depending on the change in direction of the basefee, this will promote or demote
/// transactions from the basefee pool.
fn update_basefee(&mut self, mut pending_basefee: u64) -> Ordering {
fn update_basefee<F>(&mut self, mut pending_basefee: u64, mut on_promoted: F) -> Ordering
where
F: FnMut(&Arc<ValidPoolTransaction<T::Transaction>>),
{
std::mem::swap(&mut self.all_transactions.pending_fees.base_fee, &mut pending_basefee);
match self.all_transactions.pending_fees.base_fee.cmp(&pending_basefee) {
Ordering::Equal => {
@@ -301,32 +316,37 @@ impl<T: TransactionOrdering> TxPool<T> {
// ENOUGH_BLOB_FEE_CAP_BLOCK.
// With the lower base fee they gain ENOUGH_FEE_CAP_BLOCK, so we can set the bit and
// insert directly into Pending (skip generic routing).
self.basefee_pool.enforce_basefee_with(
self.all_transactions.pending_fees.base_fee,
|tx| {
// Update transaction state — guaranteed Pending by the invariants above
let current_base_fee = self.all_transactions.pending_fees.base_fee;
self.basefee_pool.enforce_basefee_with(current_base_fee, |tx| {
// Update transaction state — guaranteed Pending by the invariants above
let subpool = {
let meta =
self.all_transactions.txs.get_mut(tx.id()).expect("tx exists in set");
meta.state.insert(TxState::ENOUGH_FEE_CAP_BLOCK);
meta.subpool = meta.state.into();
meta.subpool
};
trace!(target: "txpool", hash=%tx.transaction.hash(), pool=?meta.subpool, "Adding transaction to a subpool");
match meta.subpool {
SubPool::Queued => self.queued_pool.add_transaction(tx),
SubPool::Pending => {
self.pending_pool.add_transaction(tx, self.all_transactions.pending_fees.base_fee);
}
SubPool::Blob => {
self.blob_pool.add_transaction(tx);
}
SubPool::BaseFee => {
// This should be unreachable as transactions from BaseFee pool with
// decreased basefee are guaranteed to become Pending
warn!( target: "txpool", "BaseFee transactions should become Pending after basefee decrease");
}
if subpool == SubPool::Pending {
on_promoted(&tx);
}
trace!(target: "txpool", hash=%tx.transaction.hash(), pool=?subpool, "Adding transaction to a subpool");
match subpool {
SubPool::Queued => self.queued_pool.add_transaction(tx),
SubPool::Pending => {
self.pending_pool.add_transaction(tx, current_base_fee);
}
},
);
SubPool::Blob => {
self.blob_pool.add_transaction(tx);
}
SubPool::BaseFee => {
// This should be unreachable as transactions from BaseFee pool with decreased
// basefee are guaranteed to become Pending
warn!(target: "txpool", "BaseFee transactions should become Pending after basefee decrease");
}
}
});
Ordering::Less
}
@@ -338,9 +358,9 @@ impl<T: TransactionOrdering> TxPool<T> {
/// This will also apply updates to the pool based on the new base fee and blob fee
pub fn set_block_info(&mut self, info: BlockInfo) {
// first update the subpools based on the new values
let basefee_ordering = self.update_basefee(info.pending_basefee);
let basefee_ordering = self.update_basefee(info.pending_basefee, |_| {});
if let Some(blob_fee) = info.pending_blob_fee {
self.update_blob_fee(blob_fee, basefee_ordering)
self.update_blob_fee(blob_fee, basefee_ordering, |_| {})
}
// then update tracked values
self.all_transactions.set_block_info(info);
@@ -546,6 +566,59 @@ impl<T: TransactionOrdering> TxPool<T> {
self.all_transactions.txs_iter(sender).map(|(_, tx)| Arc::clone(&tx.transaction)).collect()
}
/// Updates only the pending fees without triggering subpool updates.
/// Returns the previous base fee and blob fee values.
const fn update_pending_fees_only(
&mut self,
mut new_base_fee: u64,
new_blob_fee: Option<u128>,
) -> (u64, u128) {
std::mem::swap(&mut self.all_transactions.pending_fees.base_fee, &mut new_base_fee);
let prev_blob_fee = if let Some(mut blob_fee) = new_blob_fee {
std::mem::swap(&mut self.all_transactions.pending_fees.blob_fee, &mut blob_fee);
blob_fee
} else {
self.all_transactions.pending_fees.blob_fee
};
(new_base_fee, prev_blob_fee)
}
/// Applies fee-based promotion updates based on the previous fees.
///
/// Records promoted transactions based on fee swings.
///
/// Caution: This expects that the fees were previously already updated via
/// [`Self::update_pending_fees_only`].
fn apply_fee_updates(
&mut self,
prev_base_fee: u64,
prev_blob_fee: u128,
outcome: &mut UpdateOutcome<T::Transaction>,
) {
let new_base_fee = self.all_transactions.pending_fees.base_fee;
let new_blob_fee = self.all_transactions.pending_fees.blob_fee;
if new_base_fee == prev_base_fee && new_blob_fee == prev_blob_fee {
// nothing to update
return;
}
// IMPORTANT:
// Restore previous fees so that the update fee functions correctly handle fee swings
self.all_transactions.pending_fees.base_fee = prev_base_fee;
self.all_transactions.pending_fees.blob_fee = prev_blob_fee;
let base_fee_ordering = self.update_basefee(new_base_fee, |tx| {
outcome.promoted.push(tx.clone());
});
self.update_blob_fee(new_blob_fee, base_fee_ordering, |tx| {
outcome.promoted.push(tx.clone());
});
}
/// Updates the transactions for the changed senders.
pub(crate) fn update_accounts(
&mut self,
@@ -577,7 +650,6 @@ impl<T: TransactionOrdering> TxPool<T> {
) -> OnNewCanonicalStateOutcome<T::Transaction> {
// update block info
let block_hash = block_info.last_seen_block_hash;
self.set_block_info(block_info);
// Remove all transaction that were included in the block
let mut removed_txs_count = 0;
@@ -590,7 +662,22 @@ impl<T: TransactionOrdering> TxPool<T> {
// Update removed transactions metric
self.metrics.removed_transactions.increment(removed_txs_count);
let UpdateOutcome { promoted, discarded } = self.update_accounts(changed_senders);
// Update fees internally first without triggering subpool updates based on fee movements
// This must happen before we update the changed so that all account updates use the new fee
// values, this way all changed accounts remain unaffected by the fee updates that are
// performed in next step and we don't collect promotions twice
let (prev_base_fee, prev_blob_fee) =
self.update_pending_fees_only(block_info.pending_basefee, block_info.pending_blob_fee);
// Now update accounts with the new fees already set
let mut outcome = self.update_accounts(changed_senders);
// Apply subpool updates based on fee changes
// This will record any additional promotions based on fee movements
self.apply_fee_updates(prev_base_fee, prev_blob_fee, &mut outcome);
// Update the rest of block info (without triggering fee updates again)
self.all_transactions.set_block_info(block_info);
self.update_transaction_type_metrics();
self.metrics.performed_state_updates.increment(1);
@@ -598,7 +685,12 @@ impl<T: TransactionOrdering> TxPool<T> {
// Update the latest update kind
self.latest_update_kind = Some(update_kind);
OnNewCanonicalStateOutcome { block_hash, mined: mined_transactions, promoted, discarded }
OnNewCanonicalStateOutcome {
block_hash,
mined: mined_transactions,
promoted: outcome.promoted,
discarded: outcome.discarded,
}
}
/// Update sub-pools size metrics.
@@ -2593,6 +2685,239 @@ mod tests {
assert!(inserted.state.intersects(expected_state));
}
#[test]
// Test that on_canonical_state_change doesn't double-process transactions
// when both fee and account updates would affect the same transaction
fn test_on_canonical_state_change_no_double_processing() {
let mut tx_factory = MockTransactionFactory::default();
let mut pool = TxPool::new(MockOrdering::default(), Default::default());
// Setup: Create a sender with a transaction in basefee pool
let tx = MockTransaction::eip1559().with_gas_price(50).with_gas_limit(30_000);
let sender = tx.sender();
// Set high base fee initially
let mut block_info = pool.block_info();
block_info.pending_basefee = 100;
pool.set_block_info(block_info);
let validated = tx_factory.validated(tx);
pool.add_transaction(validated, U256::from(10_000_000), 0, None).unwrap();
// Get sender_id after the transaction has been added
let sender_id = tx_factory.ids.sender_id(&sender).unwrap();
assert_eq!(pool.basefee_pool.len(), 1);
assert_eq!(pool.pending_pool.len(), 0);
// Now simulate a canonical state change with:
// 1. Lower base fee (would promote tx)
// 2. Account balance update (would also evaluate tx)
block_info.pending_basefee = 40;
let mut changed_senders = FxHashMap::default();
changed_senders.insert(
sender_id,
SenderInfo {
state_nonce: 0,
balance: U256::from(20_000_000), // Increased balance
},
);
let outcome = pool.on_canonical_state_change(
block_info,
vec![], // no mined transactions
changed_senders,
PoolUpdateKind::Commit,
);
// Transaction should be promoted exactly once
assert_eq!(pool.pending_pool.len(), 1, "Transaction should be in pending pool");
assert_eq!(pool.basefee_pool.len(), 0, "Transaction should not be in basefee pool");
assert_eq!(outcome.promoted.len(), 1, "Should report exactly one promotion");
}
#[test]
// Regression test: ensure we don't double-count promotions when base fee
// decreases and account is updated. This test would fail before the fix.
fn test_canonical_state_change_with_basefee_update_regression() {
let mut tx_factory = MockTransactionFactory::default();
let mut pool = TxPool::new(MockOrdering::default(), Default::default());
// Create transactions from different senders to test independently
let sender_balance = U256::from(100_000_000);
// Sender 1: tx will be promoted (gas price 60 > new base fee 50)
let tx1 =
MockTransaction::eip1559().with_gas_price(60).with_gas_limit(21_000).with_nonce(0);
let sender1 = tx1.sender();
// Sender 2: tx will be promoted (gas price 55 > new base fee 50)
let tx2 =
MockTransaction::eip1559().with_gas_price(55).with_gas_limit(21_000).with_nonce(0);
let sender2 = tx2.sender();
// Sender 3: tx will NOT be promoted (gas price 45 < new base fee 50)
let tx3 =
MockTransaction::eip1559().with_gas_price(45).with_gas_limit(21_000).with_nonce(0);
let sender3 = tx3.sender();
// Set high initial base fee (all txs will go to basefee pool)
let mut block_info = pool.block_info();
block_info.pending_basefee = 70;
pool.set_block_info(block_info);
// Add all transactions
let validated1 = tx_factory.validated(tx1);
let validated2 = tx_factory.validated(tx2);
let validated3 = tx_factory.validated(tx3);
pool.add_transaction(validated1, sender_balance, 0, None).unwrap();
pool.add_transaction(validated2, sender_balance, 0, None).unwrap();
pool.add_transaction(validated3, sender_balance, 0, None).unwrap();
let sender1_id = tx_factory.ids.sender_id(&sender1).unwrap();
let sender2_id = tx_factory.ids.sender_id(&sender2).unwrap();
let sender3_id = tx_factory.ids.sender_id(&sender3).unwrap();
// All should be in basefee pool initially
assert_eq!(pool.basefee_pool.len(), 3, "All txs should be in basefee pool");
assert_eq!(pool.pending_pool.len(), 0, "No txs should be in pending pool");
// Now decrease base fee to 50 - this should promote tx1 and tx2 (prices 60 and 55)
// but not tx3 (price 45)
block_info.pending_basefee = 50;
// Update all senders' balances (simulating account state changes)
let mut changed_senders = FxHashMap::default();
changed_senders.insert(
sender1_id,
SenderInfo { state_nonce: 0, balance: sender_balance + U256::from(1000) },
);
changed_senders.insert(
sender2_id,
SenderInfo { state_nonce: 0, balance: sender_balance + U256::from(1000) },
);
changed_senders.insert(
sender3_id,
SenderInfo { state_nonce: 0, balance: sender_balance + U256::from(1000) },
);
let outcome = pool.on_canonical_state_change(
block_info,
vec![],
changed_senders,
PoolUpdateKind::Commit,
);
// Check final state
assert_eq!(pool.pending_pool.len(), 2, "tx1 and tx2 should be promoted");
assert_eq!(pool.basefee_pool.len(), 1, "tx3 should remain in basefee");
// CRITICAL: Should report exactly 2 promotions, not 4 (which would happen with
// double-processing)
assert_eq!(
outcome.promoted.len(),
2,
"Should report exactly 2 promotions, not double-counted"
);
// Verify the correct transactions were promoted
let promoted_prices: Vec<u128> =
outcome.promoted.iter().map(|tx| tx.max_fee_per_gas()).collect();
assert!(promoted_prices.contains(&60));
assert!(promoted_prices.contains(&55));
}
#[test]
fn test_basefee_decrease_with_empty_senders() {
// Test that fee promotions still occur when basefee decreases
// even with no changed_senders
let mut tx_factory = MockTransactionFactory::default();
let mut pool = TxPool::new(MockOrdering::default(), Default::default());
// Create transaction that will be promoted when fee drops
let tx = MockTransaction::eip1559().with_gas_price(60).with_gas_limit(21_000);
// Set high initial base fee
let mut block_info = pool.block_info();
block_info.pending_basefee = 100;
pool.set_block_info(block_info);
// Add transaction - should go to basefee pool
let validated = tx_factory.validated(tx);
pool.add_transaction(validated, U256::from(10_000_000), 0, None).unwrap();
assert_eq!(pool.basefee_pool.len(), 1);
assert_eq!(pool.pending_pool.len(), 0);
// Decrease base fee with NO changed senders
block_info.pending_basefee = 50;
let outcome = pool.on_canonical_state_change(
block_info,
vec![],
FxHashMap::default(), // Empty changed_senders!
PoolUpdateKind::Commit,
);
// Transaction should still be promoted by fee-driven logic
assert_eq!(pool.pending_pool.len(), 1, "Fee decrease should promote tx");
assert_eq!(pool.basefee_pool.len(), 0);
assert_eq!(outcome.promoted.len(), 1, "Should report promotion from fee update");
}
#[test]
fn test_basefee_decrease_account_makes_unfundable() {
// Test that when basefee decreases but account update makes tx unfundable,
// we don't get transient promote-then-discard double counting
let mut tx_factory = MockTransactionFactory::default();
let mut pool = TxPool::new(MockOrdering::default(), Default::default());
let tx = MockTransaction::eip1559().with_gas_price(60).with_gas_limit(21_000);
let sender = tx.sender();
// High initial base fee
let mut block_info = pool.block_info();
block_info.pending_basefee = 100;
pool.set_block_info(block_info);
let validated = tx_factory.validated(tx);
pool.add_transaction(validated, U256::from(10_000_000), 0, None).unwrap();
let sender_id = tx_factory.ids.sender_id(&sender).unwrap();
assert_eq!(pool.basefee_pool.len(), 1);
// Decrease base fee (would normally promote) but also drain account
block_info.pending_basefee = 50;
let mut changed_senders = FxHashMap::default();
changed_senders.insert(
sender_id,
SenderInfo {
state_nonce: 0,
balance: U256::from(100), // Too low to pay for gas!
},
);
let outcome = pool.on_canonical_state_change(
block_info,
vec![],
changed_senders,
PoolUpdateKind::Commit,
);
// With insufficient balance, transaction goes to queued pool
assert_eq!(pool.pending_pool.len(), 0, "Unfunded tx should not be in pending");
assert_eq!(pool.basefee_pool.len(), 0, "Tx no longer in basefee pool");
assert_eq!(pool.queued_pool.len(), 1, "Unfunded tx should be in queued pool");
// Transaction is not removed, just moved to queued
let tx_count = pool.all_transactions.txs.len();
assert_eq!(tx_count, 1, "Transaction should still be in pool (in queued)");
assert_eq!(outcome.promoted.len(), 0, "Should not report promotion");
assert_eq!(outcome.discarded.len(), 0, "Queued tx is not reported as discarded");
}
#[test]
fn insert_already_imported() {
let on_chain_balance = U256::ZERO;
@@ -2940,7 +3265,7 @@ mod tests {
assert_eq!(pool.pending_pool.len(), 1);
pool.update_basefee((tx.max_fee_per_gas() + 1) as u64);
pool.update_basefee((tx.max_fee_per_gas() + 1) as u64, |_| {});
assert!(pool.pending_pool.is_empty());
assert_eq!(pool.basefee_pool.len(), 1);
@@ -3062,6 +3387,170 @@ mod tests {
assert!(best.iter().any(|tx| tx.id() == &id2));
}
#[test]
fn apply_fee_updates_records_promotions_after_basefee_drop() {
let mut f = MockTransactionFactory::default();
let mut pool = TxPool::new(MockOrdering::default(), Default::default());
let tx = MockTransaction::eip1559()
.with_gas_limit(21_000)
.with_max_fee(500)
.with_priority_fee(1);
let validated = f.validated(tx);
let id = *validated.id();
pool.add_transaction(validated, U256::from(1_000_000), 0, None).unwrap();
assert_eq!(pool.pending_pool.len(), 1);
// Raise base fee beyond the transaction's cap so it gets parked in BaseFee pool.
pool.update_basefee(600, |_| {});
assert!(pool.pending_pool.is_empty());
assert_eq!(pool.basefee_pool.len(), 1);
let prev_base_fee = 600;
let prev_blob_fee = pool.all_transactions.pending_fees.blob_fee;
// Simulate the canonical state path updating pending fees before applying promotions.
pool.all_transactions.pending_fees.base_fee = 400;
let mut outcome = UpdateOutcome::default();
pool.apply_fee_updates(prev_base_fee, prev_blob_fee, &mut outcome);
assert_eq!(pool.pending_pool.len(), 1);
assert!(pool.basefee_pool.is_empty());
assert_eq!(outcome.promoted.len(), 1);
assert_eq!(outcome.promoted[0].id(), &id);
assert_eq!(pool.all_transactions.pending_fees.base_fee, 400);
assert_eq!(pool.all_transactions.pending_fees.blob_fee, prev_blob_fee);
let tx_meta = pool.all_transactions.txs.get(&id).unwrap();
assert_eq!(tx_meta.subpool, SubPool::Pending);
assert!(tx_meta.state.contains(TxState::ENOUGH_FEE_CAP_BLOCK));
}
#[test]
fn apply_fee_updates_records_promotions_after_blob_fee_drop() {
let mut f = MockTransactionFactory::default();
let mut pool = TxPool::new(MockOrdering::default(), Default::default());
let initial_blob_fee = pool.all_transactions.pending_fees.blob_fee;
let tx = MockTransaction::eip4844().with_blob_fee(initial_blob_fee + 100);
let validated = f.validated(tx.clone());
let id = *validated.id();
pool.add_transaction(validated, U256::from(1_000_000), 0, None).unwrap();
assert_eq!(pool.pending_pool.len(), 1);
// Raise blob fee beyond the transaction's cap so it gets parked in Blob pool.
let increased_blob_fee = tx.max_fee_per_blob_gas().unwrap() + 200;
pool.update_blob_fee(increased_blob_fee, Ordering::Equal, |_| {});
assert!(pool.pending_pool.is_empty());
assert_eq!(pool.blob_pool.len(), 1);
let prev_base_fee = pool.all_transactions.pending_fees.base_fee;
let prev_blob_fee = pool.all_transactions.pending_fees.blob_fee;
// Simulate the canonical state path updating pending fees before applying promotions.
pool.all_transactions.pending_fees.blob_fee = tx.max_fee_per_blob_gas().unwrap();
let mut outcome = UpdateOutcome::default();
pool.apply_fee_updates(prev_base_fee, prev_blob_fee, &mut outcome);
assert_eq!(pool.pending_pool.len(), 1);
assert!(pool.blob_pool.is_empty());
assert_eq!(outcome.promoted.len(), 1);
assert_eq!(outcome.promoted[0].id(), &id);
assert_eq!(pool.all_transactions.pending_fees.base_fee, prev_base_fee);
assert_eq!(pool.all_transactions.pending_fees.blob_fee, tx.max_fee_per_blob_gas().unwrap());
let tx_meta = pool.all_transactions.txs.get(&id).unwrap();
assert_eq!(tx_meta.subpool, SubPool::Pending);
assert!(tx_meta.state.contains(TxState::ENOUGH_BLOB_FEE_CAP_BLOCK));
assert!(tx_meta.state.contains(TxState::ENOUGH_FEE_CAP_BLOCK));
}
#[test]
fn apply_fee_updates_promotes_blob_after_basefee_drop() {
let mut f = MockTransactionFactory::default();
let mut pool = TxPool::new(MockOrdering::default(), Default::default());
let initial_blob_fee = pool.all_transactions.pending_fees.blob_fee;
let tx = MockTransaction::eip4844()
.with_max_fee(500)
.with_priority_fee(1)
.with_blob_fee(initial_blob_fee + 100);
let validated = f.validated(tx);
let id = *validated.id();
pool.add_transaction(validated, U256::from(1_000_000), 0, None).unwrap();
assert_eq!(pool.pending_pool.len(), 1);
// Raise base fee beyond the transaction's cap so it gets parked in Blob pool.
let high_base_fee = 600;
pool.update_basefee(high_base_fee, |_| {});
assert!(pool.pending_pool.is_empty());
assert_eq!(pool.blob_pool.len(), 1);
let prev_base_fee = high_base_fee;
let prev_blob_fee = pool.all_transactions.pending_fees.blob_fee;
// Simulate applying a lower base fee while keeping blob fee unchanged.
pool.all_transactions.pending_fees.base_fee = 400;
let mut outcome = UpdateOutcome::default();
pool.apply_fee_updates(prev_base_fee, prev_blob_fee, &mut outcome);
assert_eq!(pool.pending_pool.len(), 1);
assert!(pool.blob_pool.is_empty());
assert_eq!(outcome.promoted.len(), 1);
assert_eq!(outcome.promoted[0].id(), &id);
assert_eq!(pool.all_transactions.pending_fees.base_fee, 400);
assert_eq!(pool.all_transactions.pending_fees.blob_fee, prev_blob_fee);
let tx_meta = pool.all_transactions.txs.get(&id).unwrap();
assert_eq!(tx_meta.subpool, SubPool::Pending);
assert!(tx_meta.state.contains(TxState::ENOUGH_BLOB_FEE_CAP_BLOCK));
assert!(tx_meta.state.contains(TxState::ENOUGH_FEE_CAP_BLOCK));
}
#[test]
fn apply_fee_updates_demotes_after_basefee_rise() {
let mut f = MockTransactionFactory::default();
let mut pool = TxPool::new(MockOrdering::default(), Default::default());
let tx = MockTransaction::eip1559()
.with_gas_limit(21_000)
.with_max_fee(400)
.with_priority_fee(1);
let validated = f.validated(tx);
let id = *validated.id();
pool.add_transaction(validated, U256::from(1_000_000), 0, None).unwrap();
assert_eq!(pool.pending_pool.len(), 1);
let prev_base_fee = pool.all_transactions.pending_fees.base_fee;
let prev_blob_fee = pool.all_transactions.pending_fees.blob_fee;
// Simulate canonical path raising the base fee beyond the transaction's cap.
let new_base_fee = prev_base_fee + 1_000;
pool.all_transactions.pending_fees.base_fee = new_base_fee;
let mut outcome = UpdateOutcome::default();
pool.apply_fee_updates(prev_base_fee, prev_blob_fee, &mut outcome);
assert!(pool.pending_pool.is_empty());
assert_eq!(pool.basefee_pool.len(), 1);
assert!(outcome.promoted.is_empty());
assert_eq!(pool.all_transactions.pending_fees.base_fee, new_base_fee);
assert_eq!(pool.all_transactions.pending_fees.blob_fee, prev_blob_fee);
let tx_meta = pool.all_transactions.txs.get(&id).unwrap();
assert_eq!(tx_meta.subpool, SubPool::BaseFee);
assert!(!tx_meta.state.contains(TxState::ENOUGH_FEE_CAP_BLOCK));
}
#[test]
fn get_highest_transaction_by_sender_and_nonce() {
// Set up a mock transaction factory and a new transaction pool.
@@ -3219,7 +3708,7 @@ mod tests {
// set the base fee of the pool
let pool_base_fee = 100;
pool.update_basefee(pool_base_fee);
pool.update_basefee(pool_base_fee, |_| {});
// 2 txs, that should put the pool over the size limit but not max txs
let a_txs = MockTransactionSet::dependent(a_sender, 0, 3, TxType::Eip1559)
@@ -4006,7 +4495,7 @@ mod tests {
.inc_limit();
// Set high basefee so transaction goes to BaseFee pool initially
pool.update_basefee(600);
pool.update_basefee(600, |_| {});
let validated = f.validated(non_4844_tx);
let tx_id = *validated.id();
@@ -4022,7 +4511,7 @@ mod tests {
// Decrease basefee - transaction should be promoted to Pending
// This is where PR #18215 bug would manifest: blob fee bit incorrectly removed
pool.update_basefee(400);
pool.update_basefee(400, |_| {});
// After basefee decrease: should be promoted to Pending with blob fee bit preserved
let tx_meta = pool.all_transactions.txs.get(&tx_id).unwrap();