From 8529e3443658d47478886353ab31c8273d1081ae Mon Sep 17 00:00:00 2001 From: figtracer <1gusredo@gmail.com> Date: Fri, 5 Dec 2025 13:38:37 +0000 Subject: [PATCH 01/13] implement scenariotype::highernonce --- .../transaction-pool/src/test_utils/pool.rs | 53 +++++++++++++++---- 1 file changed, 44 insertions(+), 9 deletions(-) diff --git a/crates/transaction-pool/src/test_utils/pool.rs b/crates/transaction-pool/src/test_utils/pool.rs index ab7bebae2f5..c4fe31e0d17 100644 --- a/crates/transaction-pool/src/test_utils/pool.rs +++ b/crates/transaction-pool/src/test_utils/pool.rs @@ -3,7 +3,7 @@ #![allow(dead_code)] use crate::{ - pool::{txpool::TxPool, AddedTransaction}, + pool::{state::SubPool, txpool::TxPool, AddedTransaction}, test_utils::{MockOrdering, MockTransactionDistribution, MockTransactionFactory}, TransactionOrdering, }; @@ -125,23 +125,58 @@ impl MockTransactionSimulator { let res = pool.add_transaction(valid_tx, on_chain_balance, on_chain_nonce, None).unwrap(); - // TODO(mattsse): need a way expect based on the current state of the pool and tx - // settings - match res { AddedTransaction::Pending(_) => {} AddedTransaction::Parked { .. } => { panic!("expected pending") } } - - // TODO(mattsse): check subpools + self.executed + .entry(sender) + .or_insert_with(|| ExecutedScenarios { sender, scenarios: vec![] }) // in the case of a new sender + .scenarios + .push(ExecutedScenario { + balance: on_chain_balance, + nonce: on_chain_nonce, + scenario: Scenario::OnchainNonce { nonce: on_chain_nonce }, + }); } - ScenarioType::HigherNonce { .. } => { - unimplemented!() + + ScenarioType::HigherNonce { skip } => { + let higher_nonce = on_chain_nonce + skip; + let tx = + self.tx_generator.tx(higher_nonce, &mut self.rng).with_gas_price(self.base_fee); + let valid_tx = self.validator.validated(tx); + + let res = + pool.add_transaction(valid_tx, on_chain_balance, on_chain_nonce, None).unwrap(); + + match res { + AddedTransaction::Pending(_) => { + panic!("expected parked") + } + AddedTransaction::Parked { subpool, .. } => { + assert_eq!( + subpool, + SubPool::Queued, + "expected to be moved to queued subpool" + ); + } + } + self.executed + .entry(sender) + .or_insert_with(|| ExecutedScenarios { sender, scenarios: vec![] }) // in the case of a new sender + .scenarios + .push(ExecutedScenario { + balance: on_chain_balance, + nonce: on_chain_nonce, + scenario: Scenario::HigherNonce { + onchain: on_chain_nonce, + nonce: higher_nonce, + }, + }); } } - // make sure everything is set pool.enforce_invariants() } From 7655b9f60a10b88d0f64f49bad977e32ae6b48cb Mon Sep 17 00:00:00 2001 From: figtracer <1gusredo@gmail.com> Date: Fri, 5 Dec 2025 13:40:47 +0000 Subject: [PATCH 02/13] add Scenario::HigherNonce unit test --- .../transaction-pool/src/test_utils/pool.rs | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/crates/transaction-pool/src/test_utils/pool.rs b/crates/transaction-pool/src/test_utils/pool.rs index c4fe31e0d17..05005bba3cf 100644 --- a/crates/transaction-pool/src/test_utils/pool.rs +++ b/crates/transaction-pool/src/test_utils/pool.rs @@ -283,5 +283,44 @@ mod tests { let mut pool = MockPool::default(); simulator.next(&mut pool); + assert_eq!(pool.pending().len(), 1); + assert_eq!(pool.queued().len(), 0); + assert_eq!(pool.base_fee().len(), 0); + } + + #[test] + fn test_higher_nonce_scenario() { + let transaction_ratio = MockTransactionRatio { + legacy_pct: 30, + dynamic_fee_pct: 70, + access_list_pct: 0, + blob_pct: 0, + }; + + let fee_ranges = MockFeeRange { + gas_price: (10u128..100).try_into().unwrap(), + priority_fee: (10u128..100).try_into().unwrap(), + max_fee: (100u128..110).try_into().unwrap(), + max_fee_blob: (1u128..100).try_into().unwrap(), + }; + + let config = MockSimulatorConfig { + num_senders: 10, + scenarios: vec![ScenarioType::HigherNonce { skip: 1 }], + base_fee: 10, + tx_generator: MockTransactionDistribution::new( + transaction_ratio, + fee_ranges, + 10..100, + 10..100, + ), + }; + let mut simulator = MockTransactionSimulator::new(rand::rng(), config); + let mut pool = MockPool::default(); + + simulator.next(&mut pool); + assert_eq!(pool.pending().len(), 0); + assert_eq!(pool.queued().len(), 1); + assert_eq!(pool.base_fee().len(), 0); } } From b1dfc24e36a53be0c379bc816fd10ee79c88f28b Mon Sep 17 00:00:00 2001 From: figtracer <1gusredo@gmail.com> Date: Fri, 5 Dec 2025 14:23:43 +0000 Subject: [PATCH 03/13] add test for random scenarios --- .../transaction-pool/src/test_utils/pool.rs | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/crates/transaction-pool/src/test_utils/pool.rs b/crates/transaction-pool/src/test_utils/pool.rs index 05005bba3cf..0f204503ea2 100644 --- a/crates/transaction-pool/src/test_utils/pool.rs +++ b/crates/transaction-pool/src/test_utils/pool.rs @@ -323,4 +323,40 @@ mod tests { assert_eq!(pool.queued().len(), 1); assert_eq!(pool.base_fee().len(), 0); } + + #[test] + fn test_many_random_scenarios() { + let transaction_ratio = MockTransactionRatio { + legacy_pct: 30, + dynamic_fee_pct: 70, + access_list_pct: 0, + blob_pct: 0, + }; + + let fee_ranges = MockFeeRange { + gas_price: (10u128..100).try_into().unwrap(), + priority_fee: (10u128..100).try_into().unwrap(), + max_fee: (100u128..110).try_into().unwrap(), + max_fee_blob: (1u128..100).try_into().unwrap(), + }; + + let config = MockSimulatorConfig { + num_senders: 10, + scenarios: vec![ScenarioType::OnchainNonce, ScenarioType::HigherNonce { skip: 1 }, ScenarioType::HigherNonce { skip: 5 }], + base_fee: 10, + tx_generator: MockTransactionDistribution::new( + transaction_ratio, + fee_ranges, + 10..100, + 10..100, + ), + }; + + let mut simulator = MockTransactionSimulator::new(rand::rng(), config); + let mut pool = MockPool::default(); + + for _ in 0..1000 { + simulator.next(&mut pool); + } + } } From d4cd6c2d49d2a659bd7aebb03ee9823865f430d4 Mon Sep 17 00:00:00 2001 From: figtracer <1gusredo@gmail.com> Date: Fri, 5 Dec 2025 14:30:36 +0000 Subject: [PATCH 04/13] add belowbasefee scenario type --- .../transaction-pool/src/test_utils/pool.rs | 38 ++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/crates/transaction-pool/src/test_utils/pool.rs b/crates/transaction-pool/src/test_utils/pool.rs index 0f204503ea2..e060f044ac6 100644 --- a/crates/transaction-pool/src/test_utils/pool.rs +++ b/crates/transaction-pool/src/test_utils/pool.rs @@ -176,6 +176,34 @@ impl MockTransactionSimulator { }, }); } + + ScenarioType::BelowBaseFee { fee } => { + let tx = self.tx_generator.tx(on_chain_nonce, &mut self.rng).with_gas_price(fee); + let valid_tx = self.validator.validated(tx); + + let res = + pool.add_transaction(valid_tx, on_chain_balance, on_chain_nonce, None).unwrap(); + + match res { + AddedTransaction::Pending(_) => panic!("expected parked"), + AddedTransaction::Parked { subpool, .. } => { + assert_eq!( + subpool, + SubPool::BaseFee, + "expected to be moved to base fee subpool" + ); + } + } + self.executed + .entry(sender) + .or_insert_with(|| ExecutedScenarios { sender, scenarios: vec![] }) // in the case of a new sender + .scenarios + .push(ExecutedScenario { + balance: on_chain_balance, + nonce: on_chain_nonce, + scenario: Scenario::BelowBaseFee { fee }, + }); + } } // make sure everything is set pool.enforce_invariants() @@ -207,6 +235,7 @@ impl MockSimulatorConfig { pub(crate) enum ScenarioType { OnchainNonce, HigherNonce { skip: u64 }, + BelowBaseFee { fee: u128 }, } /// The actual scenario, ready to be executed @@ -221,6 +250,9 @@ pub(crate) enum Scenario { OnchainNonce { nonce: u64 }, /// Send a tx with a higher nonce that what the sender has on chain HigherNonce { onchain: u64, nonce: u64 }, + /// Send a tx with a base fee below the base fee of the pool + BelowBaseFee { fee: u128 }, + Multi { // Execute multiple test scenarios scenario: Vec, @@ -342,7 +374,11 @@ mod tests { let config = MockSimulatorConfig { num_senders: 10, - scenarios: vec![ScenarioType::OnchainNonce, ScenarioType::HigherNonce { skip: 1 }, ScenarioType::HigherNonce { skip: 5 }], + scenarios: vec![ + ScenarioType::OnchainNonce, + ScenarioType::HigherNonce { skip: 1 }, + ScenarioType::HigherNonce { skip: 5 }, + ], base_fee: 10, tx_generator: MockTransactionDistribution::new( transaction_ratio, From 0fd7a6c5d8a13bc055223d2450ffe947cedf4f62 Mon Sep 17 00:00:00 2001 From: figtracer <1gusredo@gmail.com> Date: Fri, 5 Dec 2025 15:00:55 +0000 Subject: [PATCH 05/13] added create_pool() method --- .../transaction-pool/src/test_utils/pool.rs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/crates/transaction-pool/src/test_utils/pool.rs b/crates/transaction-pool/src/test_utils/pool.rs index e060f044ac6..6136948adb8 100644 --- a/crates/transaction-pool/src/test_utils/pool.rs +++ b/crates/transaction-pool/src/test_utils/pool.rs @@ -5,6 +5,7 @@ use crate::{ pool::{state::SubPool, txpool::TxPool, AddedTransaction}, test_utils::{MockOrdering, MockTransactionDistribution, MockTransactionFactory}, + traits::BlockInfo, TransactionOrdering, }; use alloy_primitives::{Address, U256}; @@ -95,6 +96,18 @@ impl MockTransactionSimulator { } } + /// Creates a pool configured for this simulator + /// + /// This is needed because `MockPool::default()` sets `pending_basefee` to 7, but we might want + /// to use different values + pub(crate) fn create_pool(&self) -> MockPool { + let mut pool = MockPool::default(); + let mut info = pool.block_info(); + info.pending_basefee = self.base_fee as u64; + pool.set_block_info(info); + pool + } + /// Returns a random address from the senders set fn rng_address(&mut self) -> Address { let idx = self.rng.random_range(0..self.senders.len()); @@ -312,7 +325,7 @@ mod tests { ), }; let mut simulator = MockTransactionSimulator::new(rand::rng(), config); - let mut pool = MockPool::default(); + let mut pool = simulator.create_pool(); simulator.next(&mut pool); assert_eq!(pool.pending().len(), 1); @@ -348,7 +361,7 @@ mod tests { ), }; let mut simulator = MockTransactionSimulator::new(rand::rng(), config); - let mut pool = MockPool::default(); + let mut pool = simulator.create_pool(); simulator.next(&mut pool); assert_eq!(pool.pending().len(), 0); @@ -389,7 +402,7 @@ mod tests { }; let mut simulator = MockTransactionSimulator::new(rand::rng(), config); - let mut pool = MockPool::default(); + let mut pool = simulator.create_pool(); for _ in 0..1000 { simulator.next(&mut pool); From 9f6d4a7e5c82a94ad2acceb7490078a296c441b7 Mon Sep 17 00:00:00 2001 From: figtracer <1gusredo@gmail.com> Date: Fri, 5 Dec 2025 16:16:47 +0000 Subject: [PATCH 06/13] cleaned up --- .../transaction-pool/src/test_utils/pool.rs | 90 +++++++++++++------ 1 file changed, 65 insertions(+), 25 deletions(-) diff --git a/crates/transaction-pool/src/test_utils/pool.rs b/crates/transaction-pool/src/test_utils/pool.rs index 6136948adb8..5ccd406fb1e 100644 --- a/crates/transaction-pool/src/test_utils/pool.rs +++ b/crates/transaction-pool/src/test_utils/pool.rs @@ -5,7 +5,6 @@ use crate::{ pool::{state::SubPool, txpool::TxPool, AddedTransaction}, test_utils::{MockOrdering, MockTransactionDistribution, MockTransactionFactory}, - traits::BlockInfo, TransactionOrdering, }; use alloy_primitives::{Address, U256}; @@ -129,10 +128,8 @@ impl MockTransactionSimulator { match scenario { ScenarioType::OnchainNonce => { - let tx = self - .tx_generator - .tx(on_chain_nonce, &mut self.rng) - .with_gas_price(self.base_fee); + // uses fee from fee_ranges + let tx = self.tx_generator.tx(on_chain_nonce, &mut self.rng); let valid_tx = self.validator.validated(tx); let res = @@ -157,8 +154,9 @@ impl MockTransactionSimulator { ScenarioType::HigherNonce { skip } => { let higher_nonce = on_chain_nonce + skip; - let tx = - self.tx_generator.tx(higher_nonce, &mut self.rng).with_gas_price(self.base_fee); + + // uses fee from fee_ranges + let tx = self.tx_generator.tx(higher_nonce, &mut self.rng); let valid_tx = self.validator.validated(tx); let res = @@ -191,6 +189,7 @@ impl MockTransactionSimulator { } ScenarioType::BelowBaseFee { fee } => { + // fee should be in [MIN_PROTOCOL_BASE_FEE, base_fee) let tx = self.tx_generator.tx(on_chain_nonce, &mut self.rng).with_gas_price(fee); let valid_tx = self.validator.validated(tx); @@ -265,11 +264,8 @@ pub(crate) enum Scenario { HigherNonce { onchain: u64, nonce: u64 }, /// Send a tx with a base fee below the base fee of the pool BelowBaseFee { fee: u128 }, - - Multi { - // Execute multiple test scenarios - scenario: Vec, - }, + /// Execute multiple test scenarios + Multi { scenario: Vec }, } /// Represents an executed scenario @@ -306,17 +302,18 @@ mod tests { blob_pct: 0, }; + let base_fee = 10u128; let fee_ranges = MockFeeRange { - gas_price: (10u128..100).try_into().unwrap(), - priority_fee: (10u128..100).try_into().unwrap(), - max_fee: (100u128..110).try_into().unwrap(), + gas_price: (base_fee..100).try_into().unwrap(), + priority_fee: (1u128..10).try_into().unwrap(), + max_fee: (base_fee..110).try_into().unwrap(), max_fee_blob: (1u128..100).try_into().unwrap(), }; let config = MockSimulatorConfig { num_senders: 10, scenarios: vec![ScenarioType::OnchainNonce], - base_fee: 10, + base_fee, tx_generator: MockTransactionDistribution::new( transaction_ratio, fee_ranges, @@ -342,17 +339,18 @@ mod tests { blob_pct: 0, }; + let base_fee = 10u128; let fee_ranges = MockFeeRange { - gas_price: (10u128..100).try_into().unwrap(), - priority_fee: (10u128..100).try_into().unwrap(), - max_fee: (100u128..110).try_into().unwrap(), + gas_price: (base_fee..100).try_into().unwrap(), + priority_fee: (1u128..10).try_into().unwrap(), + max_fee: (base_fee..110).try_into().unwrap(), max_fee_blob: (1u128..100).try_into().unwrap(), }; let config = MockSimulatorConfig { num_senders: 10, scenarios: vec![ScenarioType::HigherNonce { skip: 1 }], - base_fee: 10, + base_fee, tx_generator: MockTransactionDistribution::new( transaction_ratio, fee_ranges, @@ -369,6 +367,45 @@ mod tests { assert_eq!(pool.base_fee().len(), 0); } + #[test] + fn test_below_base_fee_scenario() { + let transaction_ratio = MockTransactionRatio { + legacy_pct: 30, + dynamic_fee_pct: 70, + access_list_pct: 0, + blob_pct: 0, + }; + + let base_fee = 10u128; + let fee_ranges = MockFeeRange { + gas_price: (base_fee..100).try_into().unwrap(), + priority_fee: (1u128..10).try_into().unwrap(), + max_fee: (base_fee..110).try_into().unwrap(), + max_fee_blob: (1u128..100).try_into().unwrap(), + }; + + let config = MockSimulatorConfig { + num_senders: 10, + scenarios: vec![ScenarioType::BelowBaseFee { fee: 8 }], /* fee should be in + * [MIN_PROTOCOL_BASE_FEE, + * base_fee) */ + base_fee, + tx_generator: MockTransactionDistribution::new( + transaction_ratio, + fee_ranges, + 10..100, + 10..100, + ), + }; + let mut simulator = MockTransactionSimulator::new(rand::rng(), config); + let mut pool = simulator.create_pool(); + + simulator.next(&mut pool); + assert_eq!(pool.pending().len(), 0); + assert_eq!(pool.queued().len(), 0); + assert_eq!(pool.base_fee().len(), 1); + } + #[test] fn test_many_random_scenarios() { let transaction_ratio = MockTransactionRatio { @@ -378,10 +415,11 @@ mod tests { blob_pct: 0, }; + let base_fee = 10u128; let fee_ranges = MockFeeRange { - gas_price: (10u128..100).try_into().unwrap(), - priority_fee: (10u128..100).try_into().unwrap(), - max_fee: (100u128..110).try_into().unwrap(), + gas_price: (base_fee..100).try_into().unwrap(), + priority_fee: (1u128..10).try_into().unwrap(), + max_fee: (base_fee..110).try_into().unwrap(), max_fee_blob: (1u128..100).try_into().unwrap(), }; @@ -390,9 +428,9 @@ mod tests { scenarios: vec![ ScenarioType::OnchainNonce, ScenarioType::HigherNonce { skip: 1 }, - ScenarioType::HigherNonce { skip: 5 }, + ScenarioType::BelowBaseFee { fee: 8 }, ], - base_fee: 10, + base_fee, tx_generator: MockTransactionDistribution::new( transaction_ratio, fee_ranges, @@ -407,5 +445,7 @@ mod tests { for _ in 0..1000 { simulator.next(&mut pool); } + + // todo: add assertions that the pool is in a valid state } } From f14ee588a91055e6d6e61b87e0874c0e83658010 Mon Sep 17 00:00:00 2001 From: figtracer <1gusredo@gmail.com> Date: Fri, 5 Dec 2025 16:43:27 +0000 Subject: [PATCH 07/13] add todo comment --- crates/transaction-pool/src/test_utils/pool.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/transaction-pool/src/test_utils/pool.rs b/crates/transaction-pool/src/test_utils/pool.rs index 5ccd406fb1e..27be00783ea 100644 --- a/crates/transaction-pool/src/test_utils/pool.rs +++ b/crates/transaction-pool/src/test_utils/pool.rs @@ -446,6 +446,6 @@ mod tests { simulator.next(&mut pool); } - // todo: add assertions that the pool is in a valid state + // todo: this is not really a good test, we should use a more deterministic approach.. } } From 91a44275964c21c355f85fcd479b4a2ea4623aaf Mon Sep 17 00:00:00 2001 From: figtracer <1gusredo@gmail.com> Date: Fri, 5 Dec 2025 16:57:17 +0000 Subject: [PATCH 08/13] cleanup --- .../transaction-pool/src/test_utils/pool.rs | 40 +------------------ 1 file changed, 1 insertion(+), 39 deletions(-) diff --git a/crates/transaction-pool/src/test_utils/pool.rs b/crates/transaction-pool/src/test_utils/pool.rs index 27be00783ea..615fb9ec664 100644 --- a/crates/transaction-pool/src/test_utils/pool.rs +++ b/crates/transaction-pool/src/test_utils/pool.rs @@ -408,44 +408,6 @@ mod tests { #[test] fn test_many_random_scenarios() { - let transaction_ratio = MockTransactionRatio { - legacy_pct: 30, - dynamic_fee_pct: 70, - access_list_pct: 0, - blob_pct: 0, - }; - - let base_fee = 10u128; - let fee_ranges = MockFeeRange { - gas_price: (base_fee..100).try_into().unwrap(), - priority_fee: (1u128..10).try_into().unwrap(), - max_fee: (base_fee..110).try_into().unwrap(), - max_fee_blob: (1u128..100).try_into().unwrap(), - }; - - let config = MockSimulatorConfig { - num_senders: 10, - scenarios: vec![ - ScenarioType::OnchainNonce, - ScenarioType::HigherNonce { skip: 1 }, - ScenarioType::BelowBaseFee { fee: 8 }, - ], - base_fee, - tx_generator: MockTransactionDistribution::new( - transaction_ratio, - fee_ranges, - 10..100, - 10..100, - ), - }; - - let mut simulator = MockTransactionSimulator::new(rand::rng(), config); - let mut pool = simulator.create_pool(); - - for _ in 0..1000 { - simulator.next(&mut pool); - } - - // todo: this is not really a good test, we should use a more deterministic approach.. + // todo: we should use a more deterministic approach to test this } } From 26c4ceecd51dc2b8c59efdca738d3cada98ed829 Mon Sep 17 00:00:00 2001 From: figtracer <1gusredo@gmail.com> Date: Mon, 8 Dec 2025 11:36:52 +0000 Subject: [PATCH 09/13] add fillnoncegap scenario --- .../transaction-pool/src/test_utils/pool.rs | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/crates/transaction-pool/src/test_utils/pool.rs b/crates/transaction-pool/src/test_utils/pool.rs index 615fb9ec664..f875c1c51ae 100644 --- a/crates/transaction-pool/src/test_utils/pool.rs +++ b/crates/transaction-pool/src/test_utils/pool.rs @@ -74,6 +74,8 @@ pub(crate) struct MockTransactionSimulator { executed: HashMap, /// "Validates" generated transactions. validator: MockTransactionFactory, + /// Represents the gaps in nonces for each sender. + nonce_gaps: HashMap, /// The rng instance used to select senders and scenarios. rng: R, } @@ -91,6 +93,7 @@ impl MockTransactionSimulator { tx_generator: config.tx_generator, executed: Default::default(), validator: Default::default(), + nonce_gaps: Default::default(), rng, } } @@ -174,6 +177,9 @@ impl MockTransactionSimulator { ); } } + // Track the nonce gap for potential filling later + self.nonce_gaps.insert(sender, higher_nonce); + self.executed .entry(sender) .or_insert_with(|| ExecutedScenarios { sender, scenarios: vec![] }) // in the case of a new sender @@ -216,6 +222,61 @@ impl MockTransactionSimulator { scenario: Scenario::BelowBaseFee { fee }, }); } + + ScenarioType::FillNonceGap => { + // Get a random sender that has a nonce gap + if self.nonce_gaps.is_empty() { + // No gaps to fill, skip this scenario + return; + } + + let gap_senders: Vec
= self.nonce_gaps.keys().copied().collect(); + let idx = self.rng.random_range(0..gap_senders.len()); + let gap_sender = gap_senders[idx]; + let queued_nonce = self.nonce_gaps[&gap_sender]; + + let sender_onchain_nonce = self.nonces[&gap_sender]; + let sender_balance = self.balances[&gap_sender]; + + // Fill all nonces from on_chain_nonce to queued_nonce - 1 + for fill_nonce in sender_onchain_nonce..queued_nonce { + let tx = self.tx_generator.tx(fill_nonce, &mut self.rng); + let valid_tx = self.validator.validated(tx); + + let res = pool + .add_transaction(valid_tx, sender_balance, sender_onchain_nonce, None) + .unwrap(); + + match res { + AddedTransaction::Pending(_) => {} + AddedTransaction::Parked { .. } => { + panic!("expected pending when filling gap") + } + } + + self.executed + .entry(gap_sender) + .or_insert_with(|| ExecutedScenarios { + sender: gap_sender, + scenarios: vec![], + }) + .scenarios + .push(ExecutedScenario { + balance: sender_balance, + nonce: fill_nonce, + scenario: Scenario::FillNonceGap { + filled_nonce: fill_nonce, + promoted_nonce: queued_nonce, + }, + }); + } + + // Update on-chain nonce + self.nonces.insert(gap_sender, queued_nonce); + + // Remove from gaps since it's now filled + self.nonce_gaps.remove(&gap_sender); + } } // make sure everything is set pool.enforce_invariants() @@ -248,6 +309,7 @@ pub(crate) enum ScenarioType { OnchainNonce, HigherNonce { skip: u64 }, BelowBaseFee { fee: u128 }, + FillNonceGap, } /// The actual scenario, ready to be executed @@ -264,6 +326,8 @@ pub(crate) enum Scenario { HigherNonce { onchain: u64, nonce: u64 }, /// Send a tx with a base fee below the base fee of the pool BelowBaseFee { fee: u128 }, + /// Fill a nonce gap to promote queued transactions + FillNonceGap { filled_nonce: u64, promoted_nonce: u64 }, /// Execute multiple test scenarios Multi { scenario: Vec }, } From 98a5e14907788a22ad512a7b33af3e777739eba6 Mon Sep 17 00:00:00 2001 From: figtracer <1gusredo@gmail.com> Date: Mon, 8 Dec 2025 12:49:14 +0000 Subject: [PATCH 10/13] add FillNonceGap tests --- .../transaction-pool/src/test_utils/pool.rs | 88 +++++++++++++++---- 1 file changed, 71 insertions(+), 17 deletions(-) diff --git a/crates/transaction-pool/src/test_utils/pool.rs b/crates/transaction-pool/src/test_utils/pool.rs index f875c1c51ae..068f3a7d3b5 100644 --- a/crates/transaction-pool/src/test_utils/pool.rs +++ b/crates/transaction-pool/src/test_utils/pool.rs @@ -11,6 +11,7 @@ use alloy_primitives::{Address, U256}; use rand::Rng; use std::{ collections::HashMap, + io::SeekFrom, ops::{Deref, DerefMut}, }; @@ -132,7 +133,7 @@ impl MockTransactionSimulator { match scenario { ScenarioType::OnchainNonce => { // uses fee from fee_ranges - let tx = self.tx_generator.tx(on_chain_nonce, &mut self.rng); + let tx = self.tx_generator.tx(on_chain_nonce, &mut self.rng).with_sender(sender); let valid_tx = self.validator.validated(tx); let res = @@ -144,6 +145,7 @@ impl MockTransactionSimulator { panic!("expected pending") } } + self.executed .entry(sender) .or_insert_with(|| ExecutedScenarios { sender, scenarios: vec![] }) // in the case of a new sender @@ -153,13 +155,20 @@ impl MockTransactionSimulator { nonce: on_chain_nonce, scenario: Scenario::OnchainNonce { nonce: on_chain_nonce }, }); + + self.nonces.insert(sender, on_chain_nonce + 1); } ScenarioType::HigherNonce { skip } => { + // if this sender already has a nonce gap, skip + if self.nonce_gaps.contains_key(&sender) { + return; + } + let higher_nonce = on_chain_nonce + skip; // uses fee from fee_ranges - let tx = self.tx_generator.tx(higher_nonce, &mut self.rng); + let tx = self.tx_generator.tx(higher_nonce, &mut self.rng).with_sender(sender); let valid_tx = self.validator.validated(tx); let res = @@ -177,8 +186,6 @@ impl MockTransactionSimulator { ); } } - // Track the nonce gap for potential filling later - self.nonce_gaps.insert(sender, higher_nonce); self.executed .entry(sender) @@ -192,11 +199,16 @@ impl MockTransactionSimulator { nonce: higher_nonce, }, }); + self.nonce_gaps.insert(sender, higher_nonce); } ScenarioType::BelowBaseFee { fee } => { // fee should be in [MIN_PROTOCOL_BASE_FEE, base_fee) - let tx = self.tx_generator.tx(on_chain_nonce, &mut self.rng).with_gas_price(fee); + let tx = self + .tx_generator + .tx(on_chain_nonce, &mut self.rng) + .with_sender(sender) + .with_gas_price(fee); let valid_tx = self.validator.validated(tx); let res = @@ -224,9 +236,7 @@ impl MockTransactionSimulator { } ScenarioType::FillNonceGap => { - // Get a random sender that has a nonce gap if self.nonce_gaps.is_empty() { - // No gaps to fill, skip this scenario return; } @@ -238,9 +248,9 @@ impl MockTransactionSimulator { let sender_onchain_nonce = self.nonces[&gap_sender]; let sender_balance = self.balances[&gap_sender]; - // Fill all nonces from on_chain_nonce to queued_nonce - 1 for fill_nonce in sender_onchain_nonce..queued_nonce { - let tx = self.tx_generator.tx(fill_nonce, &mut self.rng); + let tx = + self.tx_generator.tx(fill_nonce, &mut self.rng).with_sender(gap_sender); let valid_tx = self.validator.validated(tx); let res = pool @@ -270,16 +280,12 @@ impl MockTransactionSimulator { }, }); } - - // Update on-chain nonce - self.nonces.insert(gap_sender, queued_nonce); - - // Remove from gaps since it's now filled + self.nonces.insert(gap_sender, queued_nonce + 1); self.nonce_gaps.remove(&gap_sender); } } // make sure everything is set - pool.enforce_invariants() + pool.enforce_invariants(); } } @@ -471,7 +477,55 @@ mod tests { } #[test] - fn test_many_random_scenarios() { - // todo: we should use a more deterministic approach to test this + fn test_fill_nonce_gap_scenario() { + let transaction_ratio = MockTransactionRatio { + legacy_pct: 30, + dynamic_fee_pct: 70, + access_list_pct: 0, + blob_pct: 0, + }; + + let base_fee = 10u128; + let fee_ranges = MockFeeRange { + gas_price: (base_fee..100).try_into().unwrap(), + priority_fee: (1u128..10).try_into().unwrap(), + max_fee: (base_fee..110).try_into().unwrap(), + max_fee_blob: (1u128..100).try_into().unwrap(), + }; + + let config = MockSimulatorConfig { + num_senders: 5, + scenarios: vec![ScenarioType::HigherNonce { skip: 5 }], + base_fee, + tx_generator: MockTransactionDistribution::new( + transaction_ratio, + fee_ranges, + 10..100, + 10..100, + ), + }; + let mut simulator = MockTransactionSimulator::new(rand::rng(), config); + let mut pool = simulator.create_pool(); + + // create some nonce gaps + for _ in 0..10 { + simulator.next(&mut pool); + } + + let num_gaps = simulator.nonce_gaps.len(); + + assert_eq!(pool.pending().len(), 0); + assert_eq!(pool.queued().len(), num_gaps); + assert_eq!(pool.base_fee().len(), 0); + + simulator.scenarios = vec![ScenarioType::FillNonceGap]; + for _ in 0..num_gaps { + simulator.next(&mut pool); + } + + let expected_pending = num_gaps * 6; + assert_eq!(pool.pending().len(), expected_pending); + assert_eq!(pool.queued().len(), 0); + assert_eq!(pool.base_fee().len(), 0); } } From 7537b90dbda2351d8c6aae08990778a416fedaf3 Mon Sep 17 00:00:00 2001 From: figtracer <1gusredo@gmail.com> Date: Mon, 8 Dec 2025 12:52:58 +0000 Subject: [PATCH 11/13] remove added import --- crates/transaction-pool/src/test_utils/pool.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/transaction-pool/src/test_utils/pool.rs b/crates/transaction-pool/src/test_utils/pool.rs index 068f3a7d3b5..17fd8fb3774 100644 --- a/crates/transaction-pool/src/test_utils/pool.rs +++ b/crates/transaction-pool/src/test_utils/pool.rs @@ -11,7 +11,6 @@ use alloy_primitives::{Address, U256}; use rand::Rng; use std::{ collections::HashMap, - io::SeekFrom, ops::{Deref, DerefMut}, }; From b8bdf702c524bfb4aa8eade5b7143d7525a1d14e Mon Sep 17 00:00:00 2001 From: figtracer <1gusredo@gmail.com> Date: Mon, 8 Dec 2025 13:27:42 +0000 Subject: [PATCH 12/13] add test_random_scenarios --- .../transaction-pool/src/test_utils/pool.rs | 89 +++++++++++++++++-- 1 file changed, 83 insertions(+), 6 deletions(-) diff --git a/crates/transaction-pool/src/test_utils/pool.rs b/crates/transaction-pool/src/test_utils/pool.rs index 17fd8fb3774..18058b2a943 100644 --- a/crates/transaction-pool/src/test_utils/pool.rs +++ b/crates/transaction-pool/src/test_utils/pool.rs @@ -3,6 +3,7 @@ #![allow(dead_code)] use crate::{ + error::PoolErrorKind, pool::{state::SubPool, txpool::TxPool, AddedTransaction}, test_utils::{MockOrdering, MockTransactionDistribution, MockTransactionFactory}, TransactionOrdering, @@ -136,7 +137,15 @@ impl MockTransactionSimulator { let valid_tx = self.validator.validated(tx); let res = - pool.add_transaction(valid_tx, on_chain_balance, on_chain_nonce, None).unwrap(); + match pool.add_transaction(valid_tx, on_chain_balance, on_chain_nonce, None) { + Ok(res) => res, + Err(e) => match e.kind { + // skip pool capacity/replacement errors (not relevant) + PoolErrorKind::SpammerExceededCapacity(_) + | PoolErrorKind::ReplacementUnderpriced => return, + _ => panic!("unexpected error: {e:?}"), + }, + }; match res { AddedTransaction::Pending(_) => {} @@ -171,7 +180,15 @@ impl MockTransactionSimulator { let valid_tx = self.validator.validated(tx); let res = - pool.add_transaction(valid_tx, on_chain_balance, on_chain_nonce, None).unwrap(); + match pool.add_transaction(valid_tx, on_chain_balance, on_chain_nonce, None) { + Ok(res) => res, + Err(e) => match e.kind { + // skip pool capacity/replacement errors (not relevant) + PoolErrorKind::SpammerExceededCapacity(_) + | PoolErrorKind::ReplacementUnderpriced => return, + _ => panic!("unexpected error: {e:?}"), + }, + }; match res { AddedTransaction::Pending(_) => { @@ -211,7 +228,15 @@ impl MockTransactionSimulator { let valid_tx = self.validator.validated(tx); let res = - pool.add_transaction(valid_tx, on_chain_balance, on_chain_nonce, None).unwrap(); + match pool.add_transaction(valid_tx, on_chain_balance, on_chain_nonce, None) { + Ok(res) => res, + Err(e) => match e.kind { + // skip pool capacity/replacement errors (not relevant) + PoolErrorKind::SpammerExceededCapacity(_) + | PoolErrorKind::ReplacementUnderpriced => return, + _ => panic!("unexpected error: {e:?}"), + }, + }; match res { AddedTransaction::Pending(_) => panic!("expected parked"), @@ -252,9 +277,20 @@ impl MockTransactionSimulator { self.tx_generator.tx(fill_nonce, &mut self.rng).with_sender(gap_sender); let valid_tx = self.validator.validated(tx); - let res = pool - .add_transaction(valid_tx, sender_balance, sender_onchain_nonce, None) - .unwrap(); + let res = match pool.add_transaction( + valid_tx, + sender_balance, + sender_onchain_nonce, + None, + ) { + Ok(res) => res, + Err(e) => match e.kind { + // skip pool capacity/replacement errors (not relevant) + PoolErrorKind::SpammerExceededCapacity(_) + | PoolErrorKind::ReplacementUnderpriced => return, + _ => panic!("unexpected error: {e:?}"), + }, + }; match res { AddedTransaction::Pending(_) => {} @@ -527,4 +563,45 @@ mod tests { assert_eq!(pool.queued().len(), 0); assert_eq!(pool.base_fee().len(), 0); } + + #[test] + fn test_random_scenarios() { + let transaction_ratio = MockTransactionRatio { + legacy_pct: 30, + dynamic_fee_pct: 70, + access_list_pct: 0, + blob_pct: 0, + }; + + let base_fee = 10u128; + let fee_ranges = MockFeeRange { + gas_price: (base_fee..100).try_into().unwrap(), + priority_fee: (1u128..10).try_into().unwrap(), + max_fee: (base_fee..110).try_into().unwrap(), + max_fee_blob: (1u128..100).try_into().unwrap(), + }; + + let config = MockSimulatorConfig { + num_senders: 10, + scenarios: vec![ + ScenarioType::OnchainNonce, + ScenarioType::HigherNonce { skip: 2 }, + ScenarioType::BelowBaseFee { fee: 8 }, + ScenarioType::FillNonceGap, + ], + base_fee, + tx_generator: MockTransactionDistribution::new( + transaction_ratio, + fee_ranges, + 10..100, + 10..100, + ), + }; + let mut simulator = MockTransactionSimulator::new(rand::rng(), config); + let mut pool = simulator.create_pool(); + + for _ in 0..1000 { + simulator.next(&mut pool); + } + } } From 87cb42fe32156e1a53cc43ca4d63ddfc4e024fa4 Mon Sep 17 00:00:00 2001 From: figtracer <1gusredo@gmail.com> Date: Mon, 8 Dec 2025 13:32:06 +0000 Subject: [PATCH 13/13] fmt --- crates/transaction-pool/src/test_utils/pool.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/transaction-pool/src/test_utils/pool.rs b/crates/transaction-pool/src/test_utils/pool.rs index 18058b2a943..90d66b47fae 100644 --- a/crates/transaction-pool/src/test_utils/pool.rs +++ b/crates/transaction-pool/src/test_utils/pool.rs @@ -141,8 +141,8 @@ impl MockTransactionSimulator { Ok(res) => res, Err(e) => match e.kind { // skip pool capacity/replacement errors (not relevant) - PoolErrorKind::SpammerExceededCapacity(_) - | PoolErrorKind::ReplacementUnderpriced => return, + PoolErrorKind::SpammerExceededCapacity(_) | + PoolErrorKind::ReplacementUnderpriced => return, _ => panic!("unexpected error: {e:?}"), }, }; @@ -184,8 +184,8 @@ impl MockTransactionSimulator { Ok(res) => res, Err(e) => match e.kind { // skip pool capacity/replacement errors (not relevant) - PoolErrorKind::SpammerExceededCapacity(_) - | PoolErrorKind::ReplacementUnderpriced => return, + PoolErrorKind::SpammerExceededCapacity(_) | + PoolErrorKind::ReplacementUnderpriced => return, _ => panic!("unexpected error: {e:?}"), }, }; @@ -232,8 +232,8 @@ impl MockTransactionSimulator { Ok(res) => res, Err(e) => match e.kind { // skip pool capacity/replacement errors (not relevant) - PoolErrorKind::SpammerExceededCapacity(_) - | PoolErrorKind::ReplacementUnderpriced => return, + PoolErrorKind::SpammerExceededCapacity(_) | + PoolErrorKind::ReplacementUnderpriced => return, _ => panic!("unexpected error: {e:?}"), }, }; @@ -286,8 +286,8 @@ impl MockTransactionSimulator { Ok(res) => res, Err(e) => match e.kind { // skip pool capacity/replacement errors (not relevant) - PoolErrorKind::SpammerExceededCapacity(_) - | PoolErrorKind::ReplacementUnderpriced => return, + PoolErrorKind::SpammerExceededCapacity(_) | + PoolErrorKind::ReplacementUnderpriced => return, _ => panic!("unexpected error: {e:?}"), }, };