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