Skip to main content

reth_chain_state/
test_utils.rs

1use crate::{
2    in_memory::ExecutedBlock, CanonStateNotification, CanonStateNotifications,
3    CanonStateSubscriptions, ComputedTrieData,
4};
5use alloy_consensus::{Header, SignableTransaction, TxEip1559, TxReceipt, EMPTY_ROOT_HASH};
6use alloy_eips::eip1559::{ETHEREUM_BLOCK_GAS_LIMIT_30M, INITIAL_BASE_FEE};
7use alloy_primitives::{map::B256HashMap, Address, BlockNumber, B256, U256};
8use alloy_signer::SignerSync;
9use alloy_signer_local::PrivateKeySigner;
10use core::marker::PhantomData;
11use rand::Rng;
12use reth_chainspec::{ChainSpec, EthereumHardfork, MIN_TRANSACTION_GAS};
13use reth_ethereum_primitives::{
14    Block, BlockBody, EthPrimitives, Receipt, Transaction, TransactionSigned,
15};
16use reth_execution_types::{BlockExecutionOutput, BlockExecutionResult, Chain, ExecutionOutcome};
17use reth_primitives_traits::{
18    proofs::{calculate_receipt_root, calculate_transaction_root, calculate_withdrawals_root},
19    Account, NodePrimitives, Recovered, RecoveredBlock, SealedBlock, SealedHeader,
20    SignedTransaction,
21};
22use reth_storage_api::NodePrimitivesProvider;
23use reth_trie::root::state_root_unhashed;
24use revm_database::BundleState;
25use revm_state::AccountInfo;
26use std::{
27    ops::Range,
28    sync::{Arc, Mutex},
29};
30use tokio::sync::broadcast::{self, Sender};
31
32/// Fixed address used for storage slot writes in test blocks.
33const TEST_STORAGE_ADDRESS: Address = Address::new([0xAA; 20]);
34
35/// Fixed storage slot key used in test blocks.
36const TEST_STORAGE_SLOT: U256 = U256::from_limbs([1, 0, 0, 0]);
37
38/// Functionality to build blocks for tests and help with assertions about
39/// their execution.
40#[derive(Debug)]
41pub struct TestBlockBuilder<N: NodePrimitives = EthPrimitives> {
42    /// The account that signs all the block's transactions.
43    pub signer: Address,
44    /// Private key for signing.
45    pub signer_pk: PrivateKeySigner,
46    /// Keeps track of signer's account info after execution, will be updated in
47    /// methods related to block execution.
48    pub signer_execute_account_info: AccountInfo,
49    /// Keeps track of signer's nonce, will be updated in methods related
50    /// to block execution.
51    pub signer_build_account_info: AccountInfo,
52    /// Chain spec of the blocks generated by this builder
53    pub chain_spec: ChainSpec,
54    /// Maps block hash → post-block state (signer account info, storage slot value).
55    /// Used to construct proper reverts when building blocks on different forks.
56    pub post_block_state: B256HashMap<(AccountInfo, U256)>,
57    /// When true, generated blocks include proper `BundleState` with account/storage
58    /// changes and reverts. When false, blocks use `BundleState::default()`.
59    pub with_state: bool,
60    _prims: PhantomData<N>,
61}
62
63impl<N: NodePrimitives> Default for TestBlockBuilder<N> {
64    fn default() -> Self {
65        let initial_account_info = AccountInfo::from_balance(U256::from(10).pow(U256::from(18)));
66        let signer_pk = PrivateKeySigner::random();
67        let signer = signer_pk.address();
68        Self {
69            chain_spec: ChainSpec::default(),
70            signer,
71            signer_pk,
72            signer_execute_account_info: initial_account_info.clone(),
73            signer_build_account_info: initial_account_info,
74            post_block_state: B256HashMap::default(),
75            with_state: false,
76            _prims: PhantomData,
77        }
78    }
79}
80
81impl<N: NodePrimitives> TestBlockBuilder<N> {
82    /// Signer pk setter.
83    pub fn with_signer_pk(mut self, signer_pk: PrivateKeySigner) -> Self {
84        self.signer = signer_pk.address();
85        self.signer_pk = signer_pk;
86
87        self
88    }
89
90    /// Chainspec setter.
91    pub fn with_chain_spec(mut self, chain_spec: ChainSpec) -> Self {
92        self.chain_spec = chain_spec;
93        self
94    }
95
96    /// Enables state generation: blocks will include proper `BundleState` with
97    /// account/storage changes, reverts, and hashed state.
98    pub const fn with_state(mut self) -> Self {
99        self.with_state = true;
100        self
101    }
102
103    /// Gas cost of a single transaction generated by the block builder.
104    pub fn single_tx_cost() -> U256 {
105        U256::from(INITIAL_BASE_FEE * MIN_TRANSACTION_GAS)
106    }
107
108    /// Generates a random [`RecoveredBlock`].
109    pub fn generate_random_block(
110        &mut self,
111        number: BlockNumber,
112        parent_hash: B256,
113    ) -> SealedBlock<reth_ethereum_primitives::Block> {
114        let mut rng = rand::rng();
115
116        let mock_tx = |nonce: u64| -> Recovered<_> {
117            let tx = Transaction::Eip1559(TxEip1559 {
118                chain_id: self.chain_spec.chain.id(),
119                nonce,
120                gas_limit: MIN_TRANSACTION_GAS,
121                to: Address::random().into(),
122                max_fee_per_gas: INITIAL_BASE_FEE as u128,
123                max_priority_fee_per_gas: 1,
124                ..Default::default()
125            });
126            let signature_hash = tx.signature_hash();
127            let signature = self.signer_pk.sign_hash_sync(&signature_hash).unwrap();
128
129            TransactionSigned::new_unhashed(tx, signature).with_signer(self.signer)
130        };
131
132        let num_txs = rng.random_range(0..5);
133        let signer_balance_decrease = Self::single_tx_cost() * U256::from(num_txs);
134        let transactions: Vec<Recovered<_>> = (0..num_txs)
135            .map(|_| {
136                let tx = mock_tx(self.signer_build_account_info.nonce);
137                self.signer_build_account_info.nonce += 1;
138                self.signer_build_account_info.balance -= Self::single_tx_cost();
139                tx
140            })
141            .collect();
142
143        let receipts = transactions
144            .iter()
145            .enumerate()
146            .map(|(idx, tx)| {
147                Receipt {
148                    tx_type: tx.tx_type(),
149                    success: true,
150                    cumulative_gas_used: (idx as u64 + 1) * MIN_TRANSACTION_GAS,
151                    ..Default::default()
152                }
153                .into_with_bloom()
154            })
155            .collect::<Vec<_>>();
156
157        let initial_signer_balance = U256::from(10).pow(U256::from(18));
158
159        let header = Header {
160            number,
161            parent_hash,
162            gas_used: transactions.len() as u64 * MIN_TRANSACTION_GAS,
163            mix_hash: B256::random(),
164            gas_limit: ETHEREUM_BLOCK_GAS_LIMIT_30M,
165            base_fee_per_gas: Some(INITIAL_BASE_FEE),
166            transactions_root: calculate_transaction_root(&transactions),
167            receipts_root: calculate_receipt_root(&receipts),
168            beneficiary: Address::random(),
169            state_root: state_root_unhashed([(
170                self.signer,
171                Account {
172                    balance: initial_signer_balance - signer_balance_decrease,
173                    nonce: num_txs,
174                    ..Default::default()
175                }
176                .into_trie_account(EMPTY_ROOT_HASH),
177            )]),
178            // use the number as the timestamp so it is monotonically increasing
179            timestamp: number +
180                EthereumHardfork::Cancun.activation_timestamp(self.chain_spec.chain).unwrap(),
181            withdrawals_root: Some(calculate_withdrawals_root(&[])),
182            blob_gas_used: Some(0),
183            excess_blob_gas: Some(0),
184            parent_beacon_block_root: Some(B256::random()),
185            ..Default::default()
186        };
187
188        SealedBlock::from_sealed_parts(
189            SealedHeader::seal_slow(header),
190            BlockBody {
191                transactions: transactions.into_iter().map(|tx| tx.into_inner()).collect(),
192                ommers: Vec::new(),
193                withdrawals: Some(vec![].into()),
194            },
195        )
196    }
197
198    /// Creates a fork chain with the given base block.
199    pub fn create_fork(
200        &mut self,
201        base_block: &SealedBlock<Block>,
202        length: u64,
203    ) -> Vec<RecoveredBlock<Block>> {
204        let mut fork = Vec::with_capacity(length as usize);
205        let mut parent = base_block.clone();
206
207        for _ in 0..length {
208            let block = self.generate_random_block(parent.number + 1, parent.hash());
209            parent = block.clone();
210            let senders = vec![self.signer; block.body().transactions.len()];
211            let block = block.with_senders(senders);
212            fork.push(block);
213        }
214
215        fork
216    }
217
218    /// Gets an [`ExecutedBlock`] with [`BlockNumber`], receipts and parent hash.
219    ///
220    /// When `self.with_state` is enabled, the returned block includes a proper
221    /// [`BundleState`] with account and storage state changes plus reverts, so
222    /// that `save_blocks` writes real changesets, history indices, and hashed state.
223    fn get_executed_block(
224        &mut self,
225        block_number: BlockNumber,
226        mut receipts: Vec<Vec<Receipt>>,
227        parent_hash: B256,
228    ) -> ExecutedBlock {
229        let block = self.generate_random_block(block_number, parent_hash);
230        let senders = vec![self.signer; block.body().transactions.len()];
231        let recovered = RecoveredBlock::new_sealed(block, senders);
232
233        if !self.with_state {
234            let executed = ExecutedBlock::new(
235                Arc::new(recovered),
236                Arc::new(BlockExecutionOutput {
237                    result: BlockExecutionResult {
238                        receipts: receipts.pop().unwrap_or_default(),
239                        requests: Default::default(),
240                        gas_used: 0,
241                        blob_gas_used: 0,
242                    },
243                    state: BundleState::default(),
244                }),
245                ComputedTrieData::default(),
246            );
247            return executed;
248        }
249
250        let initial_info = AccountInfo::from_balance(U256::from(10).pow(U256::from(18)));
251        let num_txs = recovered.body().transactions.len() as u64;
252        let single_cost = Self::single_tx_cost();
253
254        // Look up parent's post-block state for correct revert construction.
255        let (pre_info, old_slot_value) = self
256            .post_block_state
257            .get(&parent_hash)
258            .cloned()
259            .unwrap_or_else(|| (initial_info.clone(), U256::ZERO));
260
261        let mut final_balance = pre_info.balance;
262        for _ in 0..num_txs {
263            final_balance -= single_cost;
264        }
265        let final_nonce = pre_info.nonce + num_txs;
266        let post_info =
267            AccountInfo { nonce: final_nonce, balance: final_balance, ..Default::default() };
268
269        let account_revert = if pre_info.balance == initial_info.balance && pre_info.nonce == 0 {
270            Some(None)
271        } else {
272            Some(Some(pre_info))
273        };
274
275        let new_slot_value = U256::from(block_number).wrapping_add(U256::from(1));
276
277        let bundle = BundleState::builder(block_number..=block_number)
278            .state_present_account_info(self.signer, post_info.clone())
279            .revert_account_info(block_number, self.signer, account_revert)
280            .state_storage(
281                TEST_STORAGE_ADDRESS,
282                alloy_primitives::map::HashMap::from_iter([(
283                    TEST_STORAGE_SLOT,
284                    (old_slot_value, new_slot_value),
285                )]),
286            )
287            .revert_storage(
288                block_number,
289                TEST_STORAGE_ADDRESS,
290                vec![(TEST_STORAGE_SLOT, old_slot_value)],
291            )
292            .build();
293
294        let hashed_state = reth_trie::HashedPostState::from_bundle_state::<
295            reth_trie::KeccakKeyHasher,
296        >(bundle.state.iter())
297        .into_sorted();
298
299        let block_receipts = if receipts.is_empty() {
300            recovered
301                .body()
302                .transactions
303                .iter()
304                .enumerate()
305                .map(|(idx, tx)| Receipt {
306                    tx_type: tx.tx_type(),
307                    success: true,
308                    cumulative_gas_used: (idx as u64 + 1) * MIN_TRANSACTION_GAS,
309                    ..Default::default()
310                })
311                .collect()
312        } else {
313            receipts.into_iter().flatten().collect()
314        };
315
316        let trie_data =
317            ComputedTrieData { hashed_state: Arc::new(hashed_state), ..Default::default() };
318
319        let block_hash = recovered.hash();
320        let executed = ExecutedBlock::new(
321            Arc::new(recovered),
322            Arc::new(BlockExecutionOutput {
323                result: BlockExecutionResult {
324                    receipts: block_receipts,
325                    requests: Default::default(),
326                    gas_used: num_txs * MIN_TRANSACTION_GAS,
327                    blob_gas_used: 0,
328                },
329                state: bundle,
330            }),
331            trie_data,
332        );
333
334        self.post_block_state.insert(block_hash, (post_info, new_slot_value));
335
336        executed
337    }
338
339    /// Generates an [`ExecutedBlock`] that includes the given receipts.
340    pub fn get_executed_block_with_receipts(
341        &mut self,
342        receipts: Vec<Vec<Receipt>>,
343        parent_hash: B256,
344    ) -> ExecutedBlock {
345        let number = rand::rng().random::<u64>();
346        self.get_executed_block(number, receipts, parent_hash)
347    }
348
349    /// Generates an [`ExecutedBlock`] with the given [`BlockNumber`].
350    pub fn get_executed_block_with_number(
351        &mut self,
352        block_number: BlockNumber,
353        parent_hash: B256,
354    ) -> ExecutedBlock {
355        self.get_executed_block(block_number, vec![vec![]], parent_hash)
356    }
357
358    /// Generates a range of executed blocks with ascending block numbers.
359    pub fn get_executed_blocks(
360        &mut self,
361        range: Range<u64>,
362    ) -> impl Iterator<Item = ExecutedBlock> + '_ {
363        let mut parent_hash = B256::default();
364        range.map(move |number| {
365            let current_parent_hash = parent_hash;
366            let block = self.get_executed_block_with_number(number, current_parent_hash);
367            parent_hash = block.recovered_block().hash();
368            block
369        })
370    }
371
372    /// Returns the execution outcome for a block created with this builder.
373    /// In order to properly include the bundle state, the signer balance is
374    /// updated.
375    pub fn get_execution_outcome(
376        &mut self,
377        block: RecoveredBlock<reth_ethereum_primitives::Block>,
378    ) -> ExecutionOutcome {
379        let num_txs = block.body().transactions.len() as u64;
380        let single_cost = Self::single_tx_cost();
381
382        let mut final_balance = self.signer_execute_account_info.balance;
383        for _ in 0..num_txs {
384            final_balance -= single_cost;
385        }
386
387        let final_nonce = self.signer_execute_account_info.nonce + num_txs;
388
389        let receipts = block
390            .body()
391            .transactions
392            .iter()
393            .enumerate()
394            .map(|(idx, tx)| Receipt {
395                tx_type: tx.tx_type(),
396                success: true,
397                cumulative_gas_used: (idx as u64 + 1) * MIN_TRANSACTION_GAS,
398                ..Default::default()
399            })
400            .collect::<Vec<_>>();
401
402        let bundle_state = BundleState::builder(block.number..=block.number)
403            .state_present_account_info(
404                self.signer,
405                AccountInfo { nonce: final_nonce, balance: final_balance, ..Default::default() },
406            )
407            .build();
408
409        self.signer_execute_account_info.balance = final_balance;
410        self.signer_execute_account_info.nonce = final_nonce;
411
412        let execution_outcome =
413            ExecutionOutcome::new(bundle_state, vec![vec![]], block.number, Vec::new());
414
415        execution_outcome.with_receipts(vec![receipts])
416    }
417}
418
419impl TestBlockBuilder {
420    /// Creates a `TestBlockBuilder` configured for Ethereum primitives.
421    pub fn eth() -> Self {
422        Self::default()
423    }
424}
425/// A test `ChainEventSubscriptions`
426#[derive(Clone, Debug, Default)]
427pub struct TestCanonStateSubscriptions<N: NodePrimitives = reth_ethereum_primitives::EthPrimitives>
428{
429    canon_notif_tx: Arc<Mutex<Vec<Sender<CanonStateNotification<N>>>>>,
430}
431
432impl TestCanonStateSubscriptions {
433    /// Adds new block commit to the queue that can be consumed with
434    /// [`TestCanonStateSubscriptions::subscribe_to_canonical_state`]
435    pub fn add_next_commit(&self, new: Arc<Chain>) {
436        let event = CanonStateNotification::Commit { new };
437        self.canon_notif_tx.lock().as_mut().unwrap().retain(|tx| tx.send(event.clone()).is_ok())
438    }
439
440    /// Adds reorg to the queue that can be consumed with
441    /// [`TestCanonStateSubscriptions::subscribe_to_canonical_state`]
442    pub fn add_next_reorg(&self, old: Arc<Chain>, new: Arc<Chain>) {
443        let event = CanonStateNotification::Reorg { old, new };
444        self.canon_notif_tx.lock().as_mut().unwrap().retain(|tx| tx.send(event.clone()).is_ok())
445    }
446}
447
448impl NodePrimitivesProvider for TestCanonStateSubscriptions {
449    type Primitives = EthPrimitives;
450}
451
452impl CanonStateSubscriptions for TestCanonStateSubscriptions {
453    /// Sets up a broadcast channel with a buffer size of 100.
454    fn subscribe_to_canonical_state(&self) -> CanonStateNotifications {
455        let (canon_notif_tx, canon_notif_rx) = broadcast::channel(100);
456        self.canon_notif_tx.lock().as_mut().unwrap().push(canon_notif_tx);
457
458        canon_notif_rx
459    }
460}