Skip to main content

reth_chain_state/
test_utils.rs

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