reth_transaction_pool/test_utils/
pool.rs

1//! Test helpers for mocking an entire pool.
2
3#![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
18/// A wrapped `TxPool` with additional helpers for testing
19pub(crate) struct MockPool<T: TransactionOrdering = MockOrdering> {
20    // The wrapped pool.
21    pool: TxPool<T>,
22}
23
24impl MockPool {
25    /// The total size of all subpools
26    fn total_subpool_size(&self) -> usize {
27        self.pool.pending().len() + self.pool.base_fee().len() + self.pool.queued().len()
28    }
29
30    /// Checks that all pool invariants hold.
31    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
60/// Simulates transaction execution.
61pub(crate) struct MockTransactionSimulator<R: Rng> {
62    /// The pending base fee
63    base_fee: u128,
64    /// Generator for transactions
65    tx_generator: MockTransactionDistribution,
66    /// represents the on chain balance of a sender.
67    balances: HashMap<Address, U256>,
68    /// represents the on chain nonce of a sender.
69    nonces: HashMap<Address, u64>,
70    /// A set of addresses to use as senders.
71    senders: Vec<Address>,
72    /// What scenarios to execute.
73    scenarios: Vec<ScenarioType>,
74    /// All previous scenarios executed by a sender.
75    executed: HashMap<Address, ExecutedScenarios>,
76    /// "Validates" generated transactions.
77    validator: MockTransactionFactory,
78    /// Represents the gaps in nonces for each sender.
79    nonce_gaps: HashMap<Address, u64>,
80    /// The rng instance used to select senders and scenarios.
81    rng: R,
82}
83
84impl<R: Rng> MockTransactionSimulator<R> {
85    /// Returns a new mock instance
86    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    /// Creates a pool configured for this simulator
103    ///
104    /// This is needed because `MockPool::default()` sets `pending_basefee` to 7, but we might want
105    /// to use different values
106    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    /// Returns a random address from the senders set
115    fn rng_address(&mut self) -> Address {
116        let idx = self.rng.random_range(0..self.senders.len());
117        self.senders[idx]
118    }
119
120    /// Returns a random scenario from the scenario set
121    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    /// Executes the next scenario and applies it to the pool
127    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                // uses fee from fee_ranges
136                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                            // skip pool capacity/replacement errors (not relevant)
144                            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![] }) // in the case of a new sender
160                    .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 this sender already has a nonce gap, skip
172                if self.nonce_gaps.contains_key(&sender) {
173                    return;
174                }
175
176                let higher_nonce = on_chain_nonce + skip;
177
178                // uses fee from fee_ranges
179                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                            // skip pool capacity/replacement errors (not relevant)
187                            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![] }) // in the case of a new sender
209                    .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                // fee should be in [MIN_PROTOCOL_BASE_FEE, base_fee)
223                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                            // skip pool capacity/replacement errors (not relevant)
235                            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![] }) // in the case of a new sender
254                    .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                            // skip pool capacity/replacement errors (not relevant)
289                            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        // make sure everything is set
323        pool.enforce_invariants();
324    }
325}
326
327/// How to configure a new mock transaction stream
328pub(crate) struct MockSimulatorConfig {
329    /// How many senders to generate.
330    pub(crate) num_senders: usize,
331    /// Scenarios to test
332    pub(crate) scenarios: Vec<ScenarioType>,
333    /// The start base fee
334    pub(crate) base_fee: u128,
335    /// generator for transactions
336    pub(crate) tx_generator: MockTransactionDistribution,
337}
338
339impl MockSimulatorConfig {
340    /// Generates a set of random addresses
341    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/// Represents the different types of test scenarios.
347#[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/// The actual scenario, ready to be executed
357///
358/// A scenario produces one or more transactions and expects a certain Outcome.
359///
360/// An executed scenario can affect previous executed transactions
361#[derive(Debug, Clone)]
362#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
363pub(crate) enum Scenario {
364    /// Send a tx with the same nonce as on chain.
365    OnchainNonce { nonce: u64 },
366    /// Send a tx with a higher nonce that what the sender has on chain
367    HigherNonce { onchain: u64, nonce: u64 },
368    /// Send a tx with a base fee below the base fee of the pool
369    BelowBaseFee { fee: u128 },
370    /// Fill a nonce gap to promote queued transactions
371    FillNonceGap { filled_nonce: u64, promoted_nonce: u64 },
372    /// Execute multiple test scenarios
373    Multi { scenario: Vec<Self> },
374}
375
376/// Represents an executed scenario
377#[derive(Debug, Clone)]
378#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
379pub(crate) struct ExecutedScenario {
380    /// balance at the time of execution
381    balance: U256,
382    /// nonce at the time of execution
383    nonce: u64,
384    /// The executed scenario
385    scenario: Scenario,
386}
387
388/// All executed scenarios by a sender
389#[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 }], /* fee should be in
495                                                                     * [MIN_PROTOCOL_BASE_FEE,
496                                                                     * base_fee) */
497            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        // create some nonce gaps
546        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}