1#![allow(dead_code)]
4
5use crate::{
6 error::PoolErrorKind,
7 pool::{state::SubPool, txpool::TxPool, AddedTransaction},
8 test_utils::{MockOrdering, MockTransactionDistribution, MockTransactionFactory},
9 TransactionOrdering,
10};
11use alloy_primitives::{Address, U256};
12use rand::Rng;
13use std::{
14 collections::HashMap,
15 ops::{Deref, DerefMut},
16};
17
18pub(crate) struct MockPool<T: TransactionOrdering = MockOrdering> {
20 pool: TxPool<T>,
22}
23
24impl MockPool {
25 fn total_subpool_size(&self) -> usize {
27 self.pool.pending().len() + self.pool.base_fee().len() + self.pool.queued().len()
28 }
29
30 fn enforce_invariants(&self) {
32 assert_eq!(
33 self.pool.len(),
34 self.total_subpool_size(),
35 "Tx in AllTransactions and sum(subpools) must match"
36 );
37 }
38}
39
40impl Default for MockPool {
41 fn default() -> Self {
42 Self { pool: TxPool::new(MockOrdering::default(), Default::default()) }
43 }
44}
45
46impl<T: TransactionOrdering> Deref for MockPool<T> {
47 type Target = TxPool<T>;
48
49 fn deref(&self) -> &Self::Target {
50 &self.pool
51 }
52}
53
54impl<T: TransactionOrdering> DerefMut for MockPool<T> {
55 fn deref_mut(&mut self) -> &mut Self::Target {
56 &mut self.pool
57 }
58}
59
60pub(crate) struct MockTransactionSimulator<R: Rng> {
62 base_fee: u128,
64 tx_generator: MockTransactionDistribution,
66 balances: HashMap<Address, U256>,
68 nonces: HashMap<Address, u64>,
70 senders: Vec<Address>,
72 scenarios: Vec<ScenarioType>,
74 executed: HashMap<Address, ExecutedScenarios>,
76 validator: MockTransactionFactory,
78 nonce_gaps: HashMap<Address, u64>,
80 rng: R,
82}
83
84impl<R: Rng> MockTransactionSimulator<R> {
85 pub(crate) fn new(mut rng: R, config: MockSimulatorConfig) -> Self {
87 let senders = config.addresses(&mut rng);
88 Self {
89 base_fee: config.base_fee,
90 balances: senders.iter().copied().map(|a| (a, rng.random())).collect(),
91 nonces: senders.iter().copied().map(|a| (a, 0)).collect(),
92 senders,
93 scenarios: config.scenarios,
94 tx_generator: config.tx_generator,
95 executed: Default::default(),
96 validator: Default::default(),
97 nonce_gaps: Default::default(),
98 rng,
99 }
100 }
101
102 pub(crate) fn create_pool(&self) -> MockPool {
107 let mut pool = MockPool::default();
108 let mut info = pool.block_info();
109 info.pending_basefee = self.base_fee as u64;
110 pool.set_block_info(info);
111 pool
112 }
113
114 fn rng_address(&mut self) -> Address {
116 let idx = self.rng.random_range(0..self.senders.len());
117 self.senders[idx]
118 }
119
120 fn rng_scenario(&mut self) -> ScenarioType {
122 let idx = self.rng.random_range(0..self.scenarios.len());
123 self.scenarios[idx].clone()
124 }
125
126 pub(crate) fn next(&mut self, pool: &mut MockPool) {
128 let sender = self.rng_address();
129 let scenario = self.rng_scenario();
130 let on_chain_nonce = self.nonces[&sender];
131 let on_chain_balance = self.balances[&sender];
132
133 match scenario {
134 ScenarioType::OnchainNonce => {
135 let tx = self.tx_generator.tx(on_chain_nonce, &mut self.rng).with_sender(sender);
137 let valid_tx = self.validator.validated(tx);
138
139 let res =
140 match pool.add_transaction(valid_tx, on_chain_balance, on_chain_nonce, None) {
141 Ok(res) => res,
142 Err(e) => match e.kind {
143 PoolErrorKind::SpammerExceededCapacity(_) |
145 PoolErrorKind::ReplacementUnderpriced => return,
146 _ => panic!("unexpected error: {e:?}"),
147 },
148 };
149
150 match res {
151 AddedTransaction::Pending(_) => {}
152 AddedTransaction::Parked { .. } => {
153 panic!("expected pending")
154 }
155 }
156
157 self.executed
158 .entry(sender)
159 .or_insert_with(|| ExecutedScenarios { sender, scenarios: vec![] }) .scenarios
161 .push(ExecutedScenario {
162 balance: on_chain_balance,
163 nonce: on_chain_nonce,
164 scenario: Scenario::OnchainNonce { nonce: on_chain_nonce },
165 });
166
167 self.nonces.insert(sender, on_chain_nonce + 1);
168 }
169
170 ScenarioType::HigherNonce { skip } => {
171 if self.nonce_gaps.contains_key(&sender) {
173 return;
174 }
175
176 let higher_nonce = on_chain_nonce + skip;
177
178 let tx = self.tx_generator.tx(higher_nonce, &mut self.rng).with_sender(sender);
180 let valid_tx = self.validator.validated(tx);
181
182 let res =
183 match pool.add_transaction(valid_tx, on_chain_balance, on_chain_nonce, None) {
184 Ok(res) => res,
185 Err(e) => match e.kind {
186 PoolErrorKind::SpammerExceededCapacity(_) |
188 PoolErrorKind::ReplacementUnderpriced => return,
189 _ => panic!("unexpected error: {e:?}"),
190 },
191 };
192
193 match res {
194 AddedTransaction::Pending(_) => {
195 panic!("expected parked")
196 }
197 AddedTransaction::Parked { subpool, .. } => {
198 assert_eq!(
199 subpool,
200 SubPool::Queued,
201 "expected to be moved to queued subpool"
202 );
203 }
204 }
205
206 self.executed
207 .entry(sender)
208 .or_insert_with(|| ExecutedScenarios { sender, scenarios: vec![] }) .scenarios
210 .push(ExecutedScenario {
211 balance: on_chain_balance,
212 nonce: on_chain_nonce,
213 scenario: Scenario::HigherNonce {
214 onchain: on_chain_nonce,
215 nonce: higher_nonce,
216 },
217 });
218 self.nonce_gaps.insert(sender, higher_nonce);
219 }
220
221 ScenarioType::BelowBaseFee { fee } => {
222 let tx = self
224 .tx_generator
225 .tx(on_chain_nonce, &mut self.rng)
226 .with_sender(sender)
227 .with_gas_price(fee);
228 let valid_tx = self.validator.validated(tx);
229
230 let res =
231 match pool.add_transaction(valid_tx, on_chain_balance, on_chain_nonce, None) {
232 Ok(res) => res,
233 Err(e) => match e.kind {
234 PoolErrorKind::SpammerExceededCapacity(_) |
236 PoolErrorKind::ReplacementUnderpriced => return,
237 _ => panic!("unexpected error: {e:?}"),
238 },
239 };
240
241 match res {
242 AddedTransaction::Pending(_) => panic!("expected parked"),
243 AddedTransaction::Parked { subpool, .. } => {
244 assert_eq!(
245 subpool,
246 SubPool::BaseFee,
247 "expected to be moved to base fee subpool"
248 );
249 }
250 }
251 self.executed
252 .entry(sender)
253 .or_insert_with(|| ExecutedScenarios { sender, scenarios: vec![] }) .scenarios
255 .push(ExecutedScenario {
256 balance: on_chain_balance,
257 nonce: on_chain_nonce,
258 scenario: Scenario::BelowBaseFee { fee },
259 });
260 }
261
262 ScenarioType::FillNonceGap => {
263 if self.nonce_gaps.is_empty() {
264 return;
265 }
266
267 let gap_senders: Vec<Address> = self.nonce_gaps.keys().copied().collect();
268 let idx = self.rng.random_range(0..gap_senders.len());
269 let gap_sender = gap_senders[idx];
270 let queued_nonce = self.nonce_gaps[&gap_sender];
271
272 let sender_onchain_nonce = self.nonces[&gap_sender];
273 let sender_balance = self.balances[&gap_sender];
274
275 for fill_nonce in sender_onchain_nonce..queued_nonce {
276 let tx =
277 self.tx_generator.tx(fill_nonce, &mut self.rng).with_sender(gap_sender);
278 let valid_tx = self.validator.validated(tx);
279
280 let res = match pool.add_transaction(
281 valid_tx,
282 sender_balance,
283 sender_onchain_nonce,
284 None,
285 ) {
286 Ok(res) => res,
287 Err(e) => match e.kind {
288 PoolErrorKind::SpammerExceededCapacity(_) |
290 PoolErrorKind::ReplacementUnderpriced => return,
291 _ => panic!("unexpected error: {e:?}"),
292 },
293 };
294
295 match res {
296 AddedTransaction::Pending(_) => {}
297 AddedTransaction::Parked { .. } => {
298 panic!("expected pending when filling gap")
299 }
300 }
301
302 self.executed
303 .entry(gap_sender)
304 .or_insert_with(|| ExecutedScenarios {
305 sender: gap_sender,
306 scenarios: vec![],
307 })
308 .scenarios
309 .push(ExecutedScenario {
310 balance: sender_balance,
311 nonce: fill_nonce,
312 scenario: Scenario::FillNonceGap {
313 filled_nonce: fill_nonce,
314 promoted_nonce: queued_nonce,
315 },
316 });
317 }
318 self.nonces.insert(gap_sender, queued_nonce + 1);
319 self.nonce_gaps.remove(&gap_sender);
320 }
321 }
322 pool.enforce_invariants();
324 }
325}
326
327pub(crate) struct MockSimulatorConfig {
329 pub(crate) num_senders: usize,
331 pub(crate) scenarios: Vec<ScenarioType>,
333 pub(crate) base_fee: u128,
335 pub(crate) tx_generator: MockTransactionDistribution,
337}
338
339impl MockSimulatorConfig {
340 pub(crate) fn addresses(&self, rng: &mut impl rand::Rng) -> Vec<Address> {
342 std::iter::repeat_with(|| Address::random_with(rng)).take(self.num_senders).collect()
343 }
344}
345
346#[derive(Debug, Clone)]
348#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
349pub(crate) enum ScenarioType {
350 OnchainNonce,
351 HigherNonce { skip: u64 },
352 BelowBaseFee { fee: u128 },
353 FillNonceGap,
354}
355
356#[derive(Debug, Clone)]
362#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
363pub(crate) enum Scenario {
364 OnchainNonce { nonce: u64 },
366 HigherNonce { onchain: u64, nonce: u64 },
368 BelowBaseFee { fee: u128 },
370 FillNonceGap { filled_nonce: u64, promoted_nonce: u64 },
372 Multi { scenario: Vec<Self> },
374}
375
376#[derive(Debug, Clone)]
378#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
379pub(crate) struct ExecutedScenario {
380 balance: U256,
382 nonce: u64,
384 scenario: Scenario,
386}
387
388#[derive(Debug, Clone)]
390#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
391pub(crate) struct ExecutedScenarios {
392 sender: Address,
393 scenarios: Vec<ExecutedScenario>,
394}
395
396#[cfg(test)]
397mod tests {
398 use super::*;
399 use crate::test_utils::{MockFeeRange, MockTransactionRatio};
400
401 #[test]
402 fn test_on_chain_nonce_scenario() {
403 let transaction_ratio = MockTransactionRatio {
404 legacy_pct: 30,
405 dynamic_fee_pct: 70,
406 access_list_pct: 0,
407 blob_pct: 0,
408 };
409
410 let base_fee = 10u128;
411 let fee_ranges = MockFeeRange {
412 gas_price: (base_fee..100).try_into().unwrap(),
413 priority_fee: (1u128..10).try_into().unwrap(),
414 max_fee: (base_fee..110).try_into().unwrap(),
415 max_fee_blob: (1u128..100).try_into().unwrap(),
416 };
417
418 let config = MockSimulatorConfig {
419 num_senders: 10,
420 scenarios: vec![ScenarioType::OnchainNonce],
421 base_fee,
422 tx_generator: MockTransactionDistribution::new(
423 transaction_ratio,
424 fee_ranges,
425 10..100,
426 10..100,
427 ),
428 };
429 let mut simulator = MockTransactionSimulator::new(rand::rng(), config);
430 let mut pool = simulator.create_pool();
431
432 simulator.next(&mut pool);
433 assert_eq!(pool.pending().len(), 1);
434 assert_eq!(pool.queued().len(), 0);
435 assert_eq!(pool.base_fee().len(), 0);
436 }
437
438 #[test]
439 fn test_higher_nonce_scenario() {
440 let transaction_ratio = MockTransactionRatio {
441 legacy_pct: 30,
442 dynamic_fee_pct: 70,
443 access_list_pct: 0,
444 blob_pct: 0,
445 };
446
447 let base_fee = 10u128;
448 let fee_ranges = MockFeeRange {
449 gas_price: (base_fee..100).try_into().unwrap(),
450 priority_fee: (1u128..10).try_into().unwrap(),
451 max_fee: (base_fee..110).try_into().unwrap(),
452 max_fee_blob: (1u128..100).try_into().unwrap(),
453 };
454
455 let config = MockSimulatorConfig {
456 num_senders: 10,
457 scenarios: vec![ScenarioType::HigherNonce { skip: 1 }],
458 base_fee,
459 tx_generator: MockTransactionDistribution::new(
460 transaction_ratio,
461 fee_ranges,
462 10..100,
463 10..100,
464 ),
465 };
466 let mut simulator = MockTransactionSimulator::new(rand::rng(), config);
467 let mut pool = simulator.create_pool();
468
469 simulator.next(&mut pool);
470 assert_eq!(pool.pending().len(), 0);
471 assert_eq!(pool.queued().len(), 1);
472 assert_eq!(pool.base_fee().len(), 0);
473 }
474
475 #[test]
476 fn test_below_base_fee_scenario() {
477 let transaction_ratio = MockTransactionRatio {
478 legacy_pct: 30,
479 dynamic_fee_pct: 70,
480 access_list_pct: 0,
481 blob_pct: 0,
482 };
483
484 let base_fee = 10u128;
485 let fee_ranges = MockFeeRange {
486 gas_price: (base_fee..100).try_into().unwrap(),
487 priority_fee: (1u128..10).try_into().unwrap(),
488 max_fee: (base_fee..110).try_into().unwrap(),
489 max_fee_blob: (1u128..100).try_into().unwrap(),
490 };
491
492 let config = MockSimulatorConfig {
493 num_senders: 10,
494 scenarios: vec![ScenarioType::BelowBaseFee { fee: 8 }], base_fee,
498 tx_generator: MockTransactionDistribution::new(
499 transaction_ratio,
500 fee_ranges,
501 10..100,
502 10..100,
503 ),
504 };
505 let mut simulator = MockTransactionSimulator::new(rand::rng(), config);
506 let mut pool = simulator.create_pool();
507
508 simulator.next(&mut pool);
509 assert_eq!(pool.pending().len(), 0);
510 assert_eq!(pool.queued().len(), 0);
511 assert_eq!(pool.base_fee().len(), 1);
512 }
513
514 #[test]
515 fn test_fill_nonce_gap_scenario() {
516 let transaction_ratio = MockTransactionRatio {
517 legacy_pct: 30,
518 dynamic_fee_pct: 70,
519 access_list_pct: 0,
520 blob_pct: 0,
521 };
522
523 let base_fee = 10u128;
524 let fee_ranges = MockFeeRange {
525 gas_price: (base_fee..100).try_into().unwrap(),
526 priority_fee: (1u128..10).try_into().unwrap(),
527 max_fee: (base_fee..110).try_into().unwrap(),
528 max_fee_blob: (1u128..100).try_into().unwrap(),
529 };
530
531 let config = MockSimulatorConfig {
532 num_senders: 5,
533 scenarios: vec![ScenarioType::HigherNonce { skip: 5 }],
534 base_fee,
535 tx_generator: MockTransactionDistribution::new(
536 transaction_ratio,
537 fee_ranges,
538 10..100,
539 10..100,
540 ),
541 };
542 let mut simulator = MockTransactionSimulator::new(rand::rng(), config);
543 let mut pool = simulator.create_pool();
544
545 for _ in 0..10 {
547 simulator.next(&mut pool);
548 }
549
550 let num_gaps = simulator.nonce_gaps.len();
551
552 assert_eq!(pool.pending().len(), 0);
553 assert_eq!(pool.queued().len(), num_gaps);
554 assert_eq!(pool.base_fee().len(), 0);
555
556 simulator.scenarios = vec![ScenarioType::FillNonceGap];
557 for _ in 0..num_gaps {
558 simulator.next(&mut pool);
559 }
560
561 let expected_pending = num_gaps * 6;
562 assert_eq!(pool.pending().len(), expected_pending);
563 assert_eq!(pool.queued().len(), 0);
564 assert_eq!(pool.base_fee().len(), 0);
565 }
566
567 #[test]
568 fn test_random_scenarios() {
569 let transaction_ratio = MockTransactionRatio {
570 legacy_pct: 30,
571 dynamic_fee_pct: 70,
572 access_list_pct: 0,
573 blob_pct: 0,
574 };
575
576 let base_fee = 10u128;
577 let fee_ranges = MockFeeRange {
578 gas_price: (base_fee..100).try_into().unwrap(),
579 priority_fee: (1u128..10).try_into().unwrap(),
580 max_fee: (base_fee..110).try_into().unwrap(),
581 max_fee_blob: (1u128..100).try_into().unwrap(),
582 };
583
584 let config = MockSimulatorConfig {
585 num_senders: 10,
586 scenarios: vec![
587 ScenarioType::OnchainNonce,
588 ScenarioType::HigherNonce { skip: 2 },
589 ScenarioType::BelowBaseFee { fee: 8 },
590 ScenarioType::FillNonceGap,
591 ],
592 base_fee,
593 tx_generator: MockTransactionDistribution::new(
594 transaction_ratio,
595 fee_ranges,
596 10..100,
597 10..100,
598 ),
599 };
600 let mut simulator = MockTransactionSimulator::new(rand::rng(), config);
601 let mut pool = simulator.create_pool();
602
603 for _ in 0..1000 {
604 simulator.next(&mut pool);
605 }
606 }
607}