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 evm2::evm::AccountInfo;
12use rand::Rng;
13use reth_chainspec::{ChainSpec, EthereumHardfork, MIN_TRANSACTION_GAS};
14use reth_ethereum_primitives::{
15 Block, BlockBody, EthPrimitives, Receipt, Transaction, TransactionSigned,
16};
17use reth_execution_types::{BlockExecutionOutput, BlockExecutionResult, Chain, ExecutionOutcome};
18use reth_primitives_traits::{
19 proofs::{calculate_receipt_root, calculate_transaction_root, calculate_withdrawals_root},
20 Account, NodePrimitives, Recovered, RecoveredBlock, SealedBlock, SealedHeader,
21 SignedTransaction,
22};
23use reth_trie::{bundle_state::BundleState, root::state_root_unhashed, SortedTrieData};
24use std::{
25 ops::Range,
26 sync::{Arc, Mutex},
27};
28use tokio::sync::broadcast::{self, Sender};
29
30const TEST_STORAGE_ADDRESS: Address = Address::repeat_byte(0xAA);
32
33const TEST_STORAGE_SLOT: U256 = U256::from_limbs([1, 0, 0, 0]);
35
36#[derive(Debug)]
39pub struct TestBlockBuilder<N: NodePrimitives = EthPrimitives> {
40 pub signer: Address,
42 pub signer_pk: PrivateKeySigner,
44 pub signer_execute_account_info: AccountInfo,
47 pub signer_build_account_info: AccountInfo,
50 pub chain_spec: ChainSpec,
52 pub post_block_state: B256HashMap<(AccountInfo, U256)>,
55 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 =
64 AccountInfo::default().with_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 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 pub fn with_chain_spec(mut self, chain_spec: ChainSpec) -> Self {
91 self.chain_spec = chain_spec;
92 self
93 }
94
95 pub const fn with_state(mut self) -> Self {
98 self.with_state = true;
99 self
100 }
101
102 pub fn single_tx_cost() -> U256 {
104 U256::from(INITIAL_BASE_FEE * MIN_TRANSACTION_GAS)
105 }
106
107 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 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 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 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 SortedTrieData::default(),
245 );
246 return executed;
247 }
248
249 let initial_info = AccountInfo::default().with_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 let (pre_info, old_slot_value) =
255 self.post_block_state.get(&parent_hash).cloned().unwrap_or((initial_info, U256::ZERO));
256
257 let mut final_balance = pre_info.balance;
258 for _ in 0..num_txs {
259 final_balance -= single_cost;
260 }
261 let final_nonce = pre_info.nonce + num_txs;
262 let post_info =
263 AccountInfo { nonce: final_nonce, balance: final_balance, ..Default::default() };
264
265 let account_revert = if self.post_block_state.contains_key(&parent_hash) {
267 Some(Some(pre_info))
268 } else {
269 Some(None)
270 };
271
272 let new_slot_value = U256::from(block_number).wrapping_add(U256::ONE);
273
274 let bundle = BundleState::builder(block_number..=block_number)
275 .state_present_account_info(self.signer, post_info.clone())
276 .revert_account_info(block_number, self.signer, account_revert)
277 .state_storage(
278 TEST_STORAGE_ADDRESS,
279 alloy_primitives::map::HashMap::from_iter([(
280 TEST_STORAGE_SLOT,
281 (old_slot_value, new_slot_value),
282 )]),
283 )
284 .revert_storage(
285 block_number,
286 TEST_STORAGE_ADDRESS,
287 vec![(TEST_STORAGE_SLOT, old_slot_value)],
288 )
289 .build();
290
291 let hashed_state = reth_trie::HashedPostState::from_bundle_state::<
292 reth_trie::KeccakKeyHasher,
293 >(bundle.state.iter())
294 .into_sorted();
295
296 let block_receipts = if receipts.is_empty() {
297 recovered
298 .body()
299 .transactions
300 .iter()
301 .enumerate()
302 .map(|(idx, tx)| Receipt {
303 tx_type: tx.tx_type(),
304 success: true,
305 cumulative_gas_used: (idx as u64 + 1) * MIN_TRANSACTION_GAS,
306 ..Default::default()
307 })
308 .collect()
309 } else {
310 receipts.into_iter().flatten().collect()
311 };
312
313 let trie_data =
314 SortedTrieData { hashed_state: Arc::new(hashed_state), ..Default::default() };
315
316 let block_hash = recovered.hash();
317 let executed = ExecutedBlock::new(
318 Arc::new(recovered),
319 Arc::new(BlockExecutionOutput {
320 result: BlockExecutionResult {
321 receipts: block_receipts,
322 requests: Default::default(),
323 gas_used: num_txs * MIN_TRANSACTION_GAS,
324 blob_gas_used: 0,
325 },
326 state: bundle,
327 }),
328 trie_data,
329 );
330
331 self.post_block_state.insert(block_hash, (post_info, new_slot_value));
332
333 executed
334 }
335
336 pub fn get_executed_block_with_receipts(
338 &mut self,
339 receipts: Vec<Vec<Receipt>>,
340 parent_hash: B256,
341 ) -> ExecutedBlock {
342 let number = rand::rng().random::<u64>();
343 self.get_executed_block(number, receipts, parent_hash)
344 }
345
346 pub fn get_executed_block_with_number(
348 &mut self,
349 block_number: BlockNumber,
350 parent_hash: B256,
351 ) -> ExecutedBlock {
352 self.get_executed_block(block_number, vec![vec![]], parent_hash)
353 }
354
355 pub fn get_executed_blocks(
357 &mut self,
358 range: Range<u64>,
359 ) -> impl Iterator<Item = ExecutedBlock> + '_ {
360 let mut parent_hash = B256::default();
361 range.map(move |number| {
362 let current_parent_hash = parent_hash;
363 let block = self.get_executed_block_with_number(number, current_parent_hash);
364 parent_hash = block.recovered_block().hash();
365 block
366 })
367 }
368
369 pub fn get_execution_outcome(
373 &mut self,
374 block: RecoveredBlock<reth_ethereum_primitives::Block>,
375 ) -> ExecutionOutcome {
376 let num_txs = block.body().transactions.len() as u64;
377 let single_cost = Self::single_tx_cost();
378
379 let mut final_balance = self.signer_execute_account_info.balance;
380 for _ in 0..num_txs {
381 final_balance -= single_cost;
382 }
383
384 let final_nonce = self.signer_execute_account_info.nonce + num_txs;
385
386 let receipts = block
387 .body()
388 .transactions
389 .iter()
390 .enumerate()
391 .map(|(idx, tx)| Receipt {
392 tx_type: tx.tx_type(),
393 success: true,
394 cumulative_gas_used: (idx as u64 + 1) * MIN_TRANSACTION_GAS,
395 ..Default::default()
396 })
397 .collect::<Vec<_>>();
398
399 let bundle_state = BundleState::builder(block.number..=block.number)
400 .state_present_account_info(
401 self.signer,
402 AccountInfo { nonce: final_nonce, balance: final_balance, ..Default::default() },
403 )
404 .build();
405
406 self.signer_execute_account_info.balance = final_balance;
407 self.signer_execute_account_info.nonce = final_nonce;
408
409 let execution_outcome =
410 ExecutionOutcome::new(bundle_state, vec![vec![]], block.number, Vec::new());
411
412 execution_outcome.with_receipts(vec![receipts])
413 }
414}
415
416impl TestBlockBuilder {
417 pub fn eth() -> Self {
419 Self::default()
420 }
421}
422#[derive(Clone, Debug, Default)]
424pub struct TestCanonStateSubscriptions<N: NodePrimitives = reth_ethereum_primitives::EthPrimitives>
425{
426 canon_notif_tx: Arc<Mutex<Vec<Sender<CanonStateNotification<N>>>>>,
427}
428
429impl TestCanonStateSubscriptions {
430 pub fn add_next_commit(&self, new: Arc<Chain>) {
433 let event = CanonStateNotification::Commit { new };
434 self.canon_notif_tx.lock().as_mut().unwrap().retain(|tx| tx.send(event.clone()).is_ok())
435 }
436
437 pub fn add_next_reorg(&self, old: Arc<Chain>, new: Arc<Chain>) {
440 let event = CanonStateNotification::Reorg { old, new };
441 self.canon_notif_tx.lock().as_mut().unwrap().retain(|tx| tx.send(event.clone()).is_ok())
442 }
443}
444
445impl CanonStateSubscriptions for TestCanonStateSubscriptions {
446 type Primitives = EthPrimitives;
447
448 fn subscribe_to_canonical_state(&self) -> CanonStateNotifications {
450 let (canon_notif_tx, canon_notif_rx) = broadcast::channel(100);
451 self.canon_notif_tx.lock().as_mut().unwrap().push(canon_notif_tx);
452
453 canon_notif_rx
454 }
455}
456
457#[cfg(test)]
458mod tests {
459 use super::*;
460
461 #[test]
462 fn test_revert_after_empty_parent() {
463 let mut builder = TestBlockBuilder::eth().with_state();
464 let parent_hash = B256::repeat_byte(1);
465 let initial_info = AccountInfo::default().with_balance(U256::from(10).pow(U256::from(18)));
466
467 builder.post_block_state.insert(parent_hash, (initial_info.clone(), U256::from(2)));
470
471 for (parent_hash, expected_account) in
472 [(parent_hash, Some(initial_info)), (B256::repeat_byte(2), None)]
473 {
474 let block = builder.get_executed_block_with_number(2, parent_hash);
475 let mut state = block.execution_outcome().state.clone();
476 state.revert(1);
477
478 assert_eq!(
479 state.state.get(&builder.signer).and_then(|account| account.info.clone()),
480 expected_account,
481 "reverting must restore the signer's existence in the specified parent"
482 );
483 }
484 }
485}