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;
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::new([0xAA; 20]);
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 Default::default(),
244 Default::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 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::from(1));
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 block_hash = recovered.hash();
314 let executed = ExecutedBlock::new(
315 Arc::new(recovered),
316 Arc::new(BlockExecutionOutput {
317 result: BlockExecutionResult {
318 receipts: block_receipts,
319 requests: Default::default(),
320 gas_used: num_txs * MIN_TRANSACTION_GAS,
321 blob_gas_used: 0,
322 },
323 state: bundle,
324 }),
325 Arc::new(hashed_state),
326 Default::default(),
327 );
328
329 self.post_block_state.insert(block_hash, (post_info, new_slot_value));
330
331 executed
332 }
333
334 pub fn get_executed_block_with_receipts(
336 &mut self,
337 receipts: Vec<Vec<Receipt>>,
338 parent_hash: B256,
339 ) -> ExecutedBlock {
340 let number = rand::rng().random::<u64>();
341 self.get_executed_block(number, receipts, parent_hash)
342 }
343
344 pub fn get_executed_block_with_number(
346 &mut self,
347 block_number: BlockNumber,
348 parent_hash: B256,
349 ) -> ExecutedBlock {
350 self.get_executed_block(block_number, vec![vec![]], parent_hash)
351 }
352
353 pub fn get_executed_blocks(
355 &mut self,
356 range: Range<u64>,
357 ) -> impl Iterator<Item = ExecutedBlock> + '_ {
358 let mut parent_hash = B256::default();
359 range.map(move |number| {
360 let current_parent_hash = parent_hash;
361 let block = self.get_executed_block_with_number(number, current_parent_hash);
362 parent_hash = block.recovered_block().hash();
363 block
364 })
365 }
366
367 pub fn get_execution_outcome(
371 &mut self,
372 block: RecoveredBlock<reth_ethereum_primitives::Block>,
373 ) -> ExecutionOutcome {
374 let num_txs = block.body().transactions.len() as u64;
375 let single_cost = Self::single_tx_cost();
376
377 let mut final_balance = self.signer_execute_account_info.balance;
378 for _ in 0..num_txs {
379 final_balance -= single_cost;
380 }
381
382 let final_nonce = self.signer_execute_account_info.nonce + num_txs;
383
384 let receipts = block
385 .body()
386 .transactions
387 .iter()
388 .enumerate()
389 .map(|(idx, tx)| Receipt {
390 tx_type: tx.tx_type(),
391 success: true,
392 cumulative_gas_used: (idx as u64 + 1) * MIN_TRANSACTION_GAS,
393 ..Default::default()
394 })
395 .collect::<Vec<_>>();
396
397 let bundle_state = BundleState::builder(block.number..=block.number)
398 .state_present_account_info(
399 self.signer,
400 AccountInfo { nonce: final_nonce, balance: final_balance, ..Default::default() },
401 )
402 .build();
403
404 self.signer_execute_account_info.balance = final_balance;
405 self.signer_execute_account_info.nonce = final_nonce;
406
407 let execution_outcome =
408 ExecutionOutcome::new(bundle_state, vec![vec![]], block.number, Vec::new());
409
410 execution_outcome.with_receipts(vec![receipts])
411 }
412}
413
414impl TestBlockBuilder {
415 pub fn eth() -> Self {
417 Self::default()
418 }
419}
420#[derive(Clone, Debug, Default)]
422pub struct TestCanonStateSubscriptions<N: NodePrimitives = reth_ethereum_primitives::EthPrimitives>
423{
424 canon_notif_tx: Arc<Mutex<Vec<Sender<CanonStateNotification<N>>>>>,
425}
426
427impl TestCanonStateSubscriptions {
428 pub fn add_next_commit(&self, new: Arc<Chain>) {
431 let event = CanonStateNotification::Commit { new };
432 self.canon_notif_tx.lock().as_mut().unwrap().retain(|tx| tx.send(event.clone()).is_ok())
433 }
434
435 pub fn add_next_reorg(&self, old: Arc<Chain>, new: Arc<Chain>) {
438 let event = CanonStateNotification::Reorg { old, new };
439 self.canon_notif_tx.lock().as_mut().unwrap().retain(|tx| tx.send(event.clone()).is_ok())
440 }
441}
442
443impl CanonStateSubscriptions for TestCanonStateSubscriptions {
444 type Primitives = EthPrimitives;
445
446 fn subscribe_to_canonical_state(&self) -> CanonStateNotifications {
448 let (canon_notif_tx, canon_notif_rx) = broadcast::channel(100);
449 self.canon_notif_tx.lock().as_mut().unwrap().push(canon_notif_tx);
450
451 canon_notif_rx
452 }
453}
454
455#[cfg(test)]
456mod tests {
457 use super::*;
458
459 #[test]
460 fn test_revert_after_empty_parent() {
461 let mut builder = TestBlockBuilder::eth().with_state();
462 let parent_hash = B256::repeat_byte(1);
463 let initial_info = AccountInfo::from_balance(U256::from(10).pow(U256::from(18)));
464
465 builder.post_block_state.insert(parent_hash, (initial_info.clone(), U256::from(2)));
468
469 for (parent_hash, expected_account) in
470 [(parent_hash, Some(initial_info)), (B256::repeat_byte(2), None)]
471 {
472 let block = builder.get_executed_block_with_number(2, parent_hash);
473 let mut state = block.execution_outcome().state.clone();
474 state.revert(1);
475
476 assert_eq!(
477 state.state.get(&builder.signer).and_then(|account| account.info.clone()),
478 expected_account,
479 "reverting must restore the signer's existence in the specified parent"
480 );
481 }
482 }
483}