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
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 = 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 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 pub fn with_chain_spec(mut self, chain_spec: ChainSpec) -> Self {
90 self.chain_spec = chain_spec;
91 self
92 }
93
94 pub const fn with_state(mut self) -> Self {
97 self.with_state = true;
98 self
99 }
100
101 pub fn single_tx_cost() -> U256 {
103 U256::from(INITIAL_BASE_FEE * MIN_TRANSACTION_GAS)
104 }
105
106 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 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 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 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 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 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 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 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 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 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 pub fn eth() -> Self {
418 Self::default()
419 }
420}
421#[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 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 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 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 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}