Skip to main content

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