1use alloy_consensus::BlockHeader;
2use alloy_primitives::{keccak256, Address, Bytes, B256, U256};
3use alloy_rpc_types_debug::ExecutionWitness;
4use pretty_assertions::Comparison;
5use reth_engine_primitives::InvalidBlockHook;
6use reth_evm::{execute::Executor, ConfigureEvm};
7use reth_primitives_traits::{NodePrimitives, RecoveredBlock, SealedHeader};
8use reth_provider::{BlockExecutionOutput, StateProvider, StateProviderBox, StateProviderFactory};
9use reth_revm::{
10 database::StateProviderDatabase,
11 db::{BundleState, State},
12};
13use reth_rpc_api::DebugApiClient;
14use reth_tracing::tracing::warn;
15use reth_trie::updates::TrieUpdates;
16use revm::{
17 bytecode::Bytecode,
18 database::{
19 states::{reverts::AccountInfoRevert, StorageSlot},
20 AccountStatus, RevertToSlot,
21 },
22 state::AccountInfo,
23};
24use serde::Serialize;
25use std::{collections::BTreeMap, fmt::Debug, fs::File, io::Write, path::PathBuf};
26
27type CollectionResult =
28 (BTreeMap<B256, Bytes>, BTreeMap<B256, Bytes>, reth_trie::HashedPostState, BundleState);
29
30#[derive(Debug, PartialEq, Eq)]
32struct BundleStateSorted {
33 pub state: BTreeMap<Address, BundleAccountSorted>,
35 pub contracts: BTreeMap<B256, Bytecode>,
37 pub reverts: Vec<Vec<(Address, AccountRevertSorted)>>,
43 pub state_size: usize,
45 pub reverts_size: usize,
47}
48
49#[derive(Debug, PartialEq, Eq)]
51struct BundleAccountSorted {
52 pub info: Option<AccountInfo>,
53 pub original_info: Option<AccountInfo>,
54 pub storage: BTreeMap<U256, StorageSlot>,
60 pub status: AccountStatus,
62}
63
64#[derive(Debug, PartialEq, Eq)]
66struct AccountRevertSorted {
67 pub account: AccountInfoRevert,
68 pub storage: BTreeMap<U256, RevertToSlot>,
69 pub previous_status: AccountStatus,
70 pub wipe_storage: bool,
71}
72
73fn sort_bundle_state_for_comparison(bundle_state: &BundleState) -> BundleStateSorted {
75 BundleStateSorted {
76 state: bundle_state
77 .state
78 .iter()
79 .map(|(addr, acc)| {
80 (
81 *addr,
82 BundleAccountSorted {
83 info: acc.info.clone(),
84 original_info: acc.original_info.clone(),
85 storage: acc.storage.iter().map(|(k, v)| (*k, *v)).collect(),
86 status: acc.status,
87 },
88 )
89 })
90 .collect(),
91 contracts: bundle_state.contracts.iter().map(|(k, v)| (*k, v.clone())).collect(),
92 reverts: bundle_state
93 .reverts
94 .iter()
95 .map(|block| {
96 block
97 .iter()
98 .map(|(addr, rev)| {
99 (
100 *addr,
101 AccountRevertSorted {
102 account: rev.account.clone(),
103 storage: rev.storage.iter().map(|(k, v)| (*k, *v)).collect(),
104 previous_status: rev.previous_status,
105 wipe_storage: rev.wipe_storage,
106 },
107 )
108 })
109 .collect()
110 })
111 .collect(),
112 state_size: bundle_state.state_size,
113 reverts_size: bundle_state.reverts_size,
114 }
115}
116
117fn collect_execution_data(
119 mut db: State<StateProviderDatabase<StateProviderBox>>,
120) -> eyre::Result<CollectionResult> {
121 let bundle_state = db.take_bundle();
122 let mut codes = BTreeMap::new();
123 let mut preimages = BTreeMap::new();
124 let mut hashed_state = db.database.hashed_post_state(&bundle_state)?;
125
126 db.cache.contracts.values().chain(bundle_state.contracts.values()).for_each(|code| {
128 let code_bytes = code.original_bytes();
129 codes.insert(keccak256(&code_bytes), code_bytes);
130 });
131
132 for (address, account) in db.cache.accounts {
134 let hashed_address = keccak256(address);
135 hashed_state
136 .accounts
137 .insert(hashed_address, account.account.as_ref().map(|a| a.info.clone().into()));
138
139 if let Some(account_data) = account.account {
140 preimages.insert(hashed_address, alloy_rlp::encode(address).into());
141 let storage = hashed_state.storages.entry(hashed_address).or_default();
142
143 for (slot, value) in account_data.storage {
144 let slot_bytes = B256::from(slot);
145 let hashed_slot = keccak256(slot_bytes);
146 storage.storage.insert(hashed_slot, value);
147 preimages.insert(hashed_slot, alloy_rlp::encode(slot_bytes).into());
148 }
149 }
150 }
151
152 Ok((codes, preimages, hashed_state, bundle_state))
153}
154
155fn generate(
157 codes: BTreeMap<B256, Bytes>,
158 preimages: BTreeMap<B256, Bytes>,
159 hashed_state: reth_trie::HashedPostState,
160 state_provider: Box<dyn StateProvider>,
161) -> eyre::Result<ExecutionWitness> {
162 let state = state_provider.witness(
163 Default::default(),
164 hashed_state,
165 reth_trie::ExecutionWitnessMode::Legacy,
166 )?;
167 Ok(ExecutionWitness {
168 state,
169 codes: codes.into_values().collect(),
170 keys: preimages.into_values().collect(),
171 ..Default::default()
172 })
173}
174
175#[derive(Debug)]
180pub struct InvalidBlockWitnessHook<P, E> {
181 provider: P,
183 evm_config: E,
185 output_directory: PathBuf,
188 healthy_node_client: Option<jsonrpsee::http_client::HttpClient>,
190}
191
192impl<P, E> InvalidBlockWitnessHook<P, E> {
193 pub const fn new(
195 provider: P,
196 evm_config: E,
197 output_directory: PathBuf,
198 healthy_node_client: Option<jsonrpsee::http_client::HttpClient>,
199 ) -> Self {
200 Self { provider, evm_config, output_directory, healthy_node_client }
201 }
202}
203
204impl<P, E, N> InvalidBlockWitnessHook<P, E>
205where
206 P: StateProviderFactory + Send + Sync + 'static,
207 E: ConfigureEvm<Primitives = N> + 'static,
208 N: NodePrimitives,
209{
210 fn re_execute_block(
212 &self,
213 parent_header: &SealedHeader<N::BlockHeader>,
214 block: &RecoveredBlock<N::Block>,
215 ) -> eyre::Result<(ExecutionWitness, BundleState)> {
216 let mut executor = self.evm_config.batch_executor(StateProviderDatabase::new(
217 self.provider.state_by_block_hash(parent_header.hash())?,
218 ));
219
220 executor.execute_one(block)?;
221 let db = executor.into_state();
222 let (codes, preimages, hashed_state, bundle_state) = collect_execution_data(db)?;
223
224 let state_provider = self.provider.state_by_block_hash(parent_header.hash())?;
225 let witness = generate(codes, preimages, hashed_state, state_provider)?;
226
227 Ok((witness, bundle_state))
228 }
229
230 fn handle_witness_operations(
232 &self,
233 witness: &ExecutionWitness,
234 block_prefix: &str,
235 block_number: u64,
236 ) -> eyre::Result<()> {
237 let filename = format!("{}.witness.re_executed.json", block_prefix);
238 let re_executed_witness_path = self.save_file(filename, witness)?;
239
240 if let Some(healthy_node_client) = &self.healthy_node_client {
241 let healthy_node_witness = futures::executor::block_on(async move {
242 DebugApiClient::<()>::debug_execution_witness(
243 healthy_node_client,
244 block_number.into(),
245 None,
246 )
247 .await
248 })?;
249
250 let filename = format!("{}.witness.healthy.json", block_prefix);
251 let healthy_path = self.save_file(filename, &healthy_node_witness)?;
252
253 if witness != &healthy_node_witness {
254 let filename = format!("{}.witness.diff", block_prefix);
255 let diff_path = self.save_diff(filename, witness, &healthy_node_witness)?;
256 warn!(
257 target: "engine::invalid_block_hooks::witness",
258 diff_path = %diff_path.display(),
259 re_executed_path = %re_executed_witness_path.display(),
260 healthy_path = %healthy_path.display(),
261 "Witness mismatch against healthy node"
262 );
263 }
264 }
265 Ok(())
266 }
267
268 fn validate_bundle_state(
270 &self,
271 re_executed_state: &BundleState,
272 original_state: &BundleState,
273 block_prefix: &str,
274 ) -> eyre::Result<()> {
275 if re_executed_state != original_state {
276 let original_filename = format!("{}.bundle_state.original.json", block_prefix);
277 let original_path = self.save_file(original_filename, original_state)?;
278 let re_executed_filename = format!("{}.bundle_state.re_executed.json", block_prefix);
279 let re_executed_path = self.save_file(re_executed_filename, re_executed_state)?;
280
281 let bundle_state_sorted = sort_bundle_state_for_comparison(re_executed_state);
283 let output_state_sorted = sort_bundle_state_for_comparison(original_state);
284 let filename = format!("{}.bundle_state.diff", block_prefix);
285 let diff_path = self.save_diff(filename, &output_state_sorted, &bundle_state_sorted)?;
286
287 warn!(
288 target: "engine::invalid_block_hooks::witness",
289 diff_path = %diff_path.display(),
290 original_path = %original_path.display(),
291 re_executed_path = %re_executed_path.display(),
292 "Bundle state mismatch after re-execution"
293 );
294 }
295 Ok(())
296 }
297
298 fn validate_state_root_and_trie(
300 &self,
301 parent_header: &SealedHeader<N::BlockHeader>,
302 block: &RecoveredBlock<N::Block>,
303 bundle_state: &BundleState,
304 trie_updates: Option<(&TrieUpdates, B256)>,
305 block_prefix: &str,
306 ) -> eyre::Result<()> {
307 let state_provider = self.provider.state_by_block_hash(parent_header.hash())?;
308 let hashed_state = state_provider.hashed_post_state(bundle_state)?;
309 let (re_executed_root, trie_output) =
310 state_provider.state_root_with_updates(hashed_state)?;
311
312 if let Some((original_updates, original_root)) = trie_updates {
313 if re_executed_root != original_root {
314 let filename = format!("{}.state_root.diff", block_prefix);
315 let diff_path = self.save_diff(filename, &original_root, &re_executed_root)?;
316 warn!(target: "engine::invalid_block_hooks::witness", ?original_root, ?re_executed_root, diff_path = %diff_path.display(), "State root mismatch after re-execution");
317 }
318
319 if re_executed_root != block.state_root() {
320 let filename = format!("{}.header_state_root.diff", block_prefix);
321 let diff_path = self.save_diff(filename, &block.state_root(), &re_executed_root)?;
322 warn!(target: "engine::invalid_block_hooks::witness", header_state_root=?block.state_root(), ?re_executed_root, diff_path = %diff_path.display(), "Re-executed state root does not match block state root");
323 }
324
325 if &trie_output != original_updates {
326 let original_path = self.save_file(
327 format!("{}.trie_updates.original.json", block_prefix),
328 &original_updates.into_sorted_ref(),
329 )?;
330 let re_executed_path = self.save_file(
331 format!("{}.trie_updates.re_executed.json", block_prefix),
332 &trie_output.into_sorted_ref(),
333 )?;
334 warn!(
335 target: "engine::invalid_block_hooks::witness",
336 original_path = %original_path.display(),
337 re_executed_path = %re_executed_path.display(),
338 "Trie updates mismatch after re-execution"
339 );
340 }
341 }
342 Ok(())
343 }
344
345 fn on_invalid_block(
346 &self,
347 parent_header: &SealedHeader<N::BlockHeader>,
348 block: &RecoveredBlock<N::Block>,
349 output: &BlockExecutionOutput<N::Receipt>,
350 trie_updates: Option<(&TrieUpdates, B256)>,
351 ) -> eyre::Result<()> {
352 let (witness, bundle_state) = self.re_execute_block(parent_header, block)?;
354
355 let block_prefix = format!("{}_{}", block.number(), block.hash());
356 self.handle_witness_operations(&witness, &block_prefix, block.number())?;
357
358 self.validate_bundle_state(&bundle_state, &output.state, &block_prefix)?;
359
360 self.validate_state_root_and_trie(
361 parent_header,
362 block,
363 &bundle_state,
364 trie_updates,
365 &block_prefix,
366 )?;
367
368 Ok(())
369 }
370
371 fn save_file<T: Serialize>(&self, filename: String, value: &T) -> eyre::Result<PathBuf> {
373 let path = self.output_directory.join(filename);
374 File::create(&path)?.write_all(serde_json::to_string(value)?.as_bytes())?;
375
376 Ok(path)
377 }
378
379 fn save_diff<T: PartialEq + Debug>(
381 &self,
382 filename: String,
383 original: &T,
384 new: &T,
385 ) -> eyre::Result<PathBuf> {
386 let path = self.output_directory.join(filename);
387 let diff = Comparison::new(original, new);
388 File::create(&path)?.write_all(diff.to_string().as_bytes())?;
389
390 Ok(path)
391 }
392}
393
394impl<P, E, N: NodePrimitives> InvalidBlockHook<N> for InvalidBlockWitnessHook<P, E>
395where
396 P: StateProviderFactory + Send + Sync + 'static,
397 E: ConfigureEvm<Primitives = N> + 'static,
398{
399 fn on_invalid_block(
400 &self,
401 parent_header: &SealedHeader<N::BlockHeader>,
402 block: &RecoveredBlock<N::Block>,
403 output: &BlockExecutionOutput<N::Receipt>,
404 trie_updates: Option<(&TrieUpdates, B256)>,
405 ) {
406 if let Err(err) = self.on_invalid_block(parent_header, block, output, trie_updates) {
407 warn!(target: "engine::invalid_block_hooks::witness", %err, "Failed to invoke hook");
408 }
409 }
410}
411
412#[cfg(test)]
413mod tests {
414 use super::*;
415 use alloy_eips::eip7685::Requests;
416 use alloy_primitives::{map::HashMap, Address, Bytes, B256, U256};
417 use reth_chainspec::ChainSpec;
418 use reth_ethereum_primitives::EthPrimitives;
419 use reth_evm_ethereum::EthEvmConfig;
420 use reth_provider::test_utils::MockEthProvider;
421 use reth_revm::db::{BundleAccount, BundleState};
422 use revm::database::states::reverts::AccountRevert;
423 use tempfile::TempDir;
424
425 use reth_revm::test_utils::StateProviderTest;
426 use reth_testing_utils::generators::{self, random_block, random_eoa_accounts, BlockParams};
427 use revm::bytecode::Bytecode;
428
429 fn create_bundle_state() -> BundleState {
431 let mut rng = generators::rng();
432 let mut bundle_state = BundleState::default();
433
434 let accounts = random_eoa_accounts(&mut rng, 3);
436
437 for (i, (addr, account)) in accounts.into_iter().enumerate() {
438 let mut storage = HashMap::default();
440 let storage_key = U256::from(i + 1);
441 storage.insert(
442 storage_key,
443 StorageSlot {
444 present_value: U256::from((i + 1) * 10),
445 previous_or_original_value: U256::from((i + 1) * 15),
446 },
447 );
448
449 let bundle_account = BundleAccount {
450 info: Some(AccountInfo {
451 balance: account.balance,
452 nonce: account.nonce,
453 code_hash: account.bytecode_hash.unwrap_or_default(),
454 code: None,
455 account_id: None,
456 }),
457 original_info: (i == 0).then(|| AccountInfo {
458 balance: account.balance.checked_div(U256::from(2)).unwrap_or(U256::ZERO),
459 nonce: 0,
460 code_hash: account.bytecode_hash.unwrap_or_default(),
461 code: None,
462 account_id: None,
463 }),
464 storage,
465 status: AccountStatus::default(),
466 };
467
468 bundle_state.state.insert(addr, bundle_account);
469 }
470
471 let contract_hashes: Vec<B256> = (0..3).map(|_| B256::random()).collect();
473 for (i, hash) in contract_hashes.iter().enumerate() {
474 let bytecode = match i {
475 0 => Bytes::from(vec![0x60, 0x80, 0x60, 0x40, 0x52]), 1 => Bytes::from(vec![0x61, 0x81, 0x60, 0x00, 0x39]), _ => Bytes::from(vec![0x60, 0x00, 0x60, 0x00, 0xfd]), };
479 bundle_state.contracts.insert(*hash, Bytecode::new_raw(bytecode));
480 }
481
482 let addresses: Vec<Address> = bundle_state.state.keys().copied().collect();
484 for (i, addr) in addresses.iter().take(2).enumerate() {
485 let revert = AccountRevert {
486 wipe_storage: i == 0, ..AccountRevert::default()
488 };
489 bundle_state.reverts.push(vec![(*addr, revert)]);
490 }
491
492 bundle_state.state_size = bundle_state.state.len();
494 bundle_state.reverts_size = bundle_state.reverts.len();
495
496 bundle_state
497 }
498 #[test]
499 fn test_sort_bundle_state_for_comparison() {
500 let bundle_state = create_bundle_state();
502
503 let sorted = sort_bundle_state_for_comparison(&bundle_state);
505
506 assert_eq!(sorted.state_size, 3);
508 assert_eq!(sorted.reverts_size, 2);
509
510 assert_eq!(sorted.state.len(), 3); assert_eq!(sorted.contracts.len(), 3); let reverts = &sorted.reverts;
518 assert_eq!(reverts.len(), 2); for account_data in sorted.state.values() {
522 let _info = &account_data.info;
525 let _original_info = &account_data.original_info;
526 let _storage = &account_data.storage;
527 let _status = &account_data.status;
528 }
529 }
530
531 #[test]
532 fn test_data_collector_collect() {
533 let bundle_state = create_bundle_state();
535
536 let state_provider = StateProviderTest::default();
538 let mut state = State::builder()
539 .with_database(StateProviderDatabase::new(Box::new(state_provider) as StateProviderBox))
540 .with_bundle_update()
541 .build();
542
543 for (code_hash, bytecode) in &bundle_state.contracts {
545 state.cache.contracts.insert(*code_hash, bytecode.clone());
546 }
547
548 state.bundle_state = bundle_state;
550
551 let result = collect_execution_data(state);
553 assert!(result.is_ok());
555
556 let (codes, _preimages, _hashed_state, returned_bundle_state) = result.unwrap();
557
558 assert!(!codes.is_empty(), "Expected some bytecode entries");
561 assert!(!returned_bundle_state.state.is_empty(), "Expected some state entries");
562
563 assert_eq!(returned_bundle_state.state.len(), 3, "Expected 3 accounts from fixture");
565 assert_eq!(returned_bundle_state.contracts.len(), 3, "Expected 3 contracts from fixture");
566 }
567
568 #[test]
569 fn test_re_execute_block() {
570 let (hook, _output_directory, _temp_dir) = create_test_hook();
572
573 let mut rng = generators::rng();
575 let parent_header = generators::random_header(&mut rng, 1, None);
576
577 let recovered_block = random_block(
579 &mut rng,
580 2, BlockParams {
582 parent: Some(parent_header.hash()),
583 tx_count: Some(0),
584 ..Default::default()
585 },
586 )
587 .try_recover()
588 .unwrap();
589
590 let result = hook.re_execute_block(&parent_header, &recovered_block);
591
592 assert!(result.is_ok(), "re_execute_block should return Ok");
594 }
595
596 fn create_test_hook() -> (
598 InvalidBlockWitnessHook<MockEthProvider<EthPrimitives, ChainSpec>, EthEvmConfig>,
599 PathBuf,
600 TempDir,
601 ) {
602 let temp_dir = TempDir::new().expect("Failed to create temp dir");
603 let output_directory = temp_dir.path().to_path_buf();
604
605 let provider = MockEthProvider::<EthPrimitives, ChainSpec>::default();
606 let evm_config = EthEvmConfig::mainnet();
607
608 let hook =
609 InvalidBlockWitnessHook::new(provider, evm_config, output_directory.clone(), None);
610
611 (hook, output_directory, temp_dir)
612 }
613
614 #[test]
615 fn test_handle_witness_operations_with_healthy_client_mock() {
616 let (hook, output_directory, _temp_dir) = create_test_hook();
618
619 let witness = ExecutionWitness {
621 state: vec![Bytes::from("state_data")],
622 codes: vec![Bytes::from("code_data")],
623 keys: vec![Bytes::from("key_data")],
624 ..Default::default()
625 };
626
627 let result = hook.handle_witness_operations(&witness, "test_block_healthy", 67890);
629
630 assert!(result.is_ok());
632
633 let witness_file = output_directory.join("test_block_healthy.witness.re_executed.json");
635 assert!(witness_file.exists());
636 }
637
638 #[test]
639 fn test_handle_witness_operations_file_creation() {
640 let (hook, output_directory, _temp_dir) = create_test_hook();
642
643 let witness = ExecutionWitness {
644 state: vec![Bytes::from("test_state")],
645 codes: vec![Bytes::from("test_code")],
646 keys: vec![Bytes::from("test_key")],
647 ..Default::default()
648 };
649
650 let block_prefix = "file_test_block";
651 let block_number = 11111;
652
653 let result = hook.handle_witness_operations(&witness, block_prefix, block_number);
655 assert!(result.is_ok());
656
657 let expected_file =
659 output_directory.join(format!("{}.witness.re_executed.json", block_prefix));
660 assert!(expected_file.exists());
661
662 let file_content = std::fs::read_to_string(&expected_file).expect("Failed to read file");
664 let parsed_witness: serde_json::Value =
665 serde_json::from_str(&file_content).expect("File should contain valid JSON");
666
667 assert!(parsed_witness.get("state").is_some(), "JSON should contain 'state' field");
669 assert!(parsed_witness.get("codes").is_some(), "JSON should contain 'codes' field");
670 assert!(parsed_witness.get("keys").is_some(), "JSON should contain 'keys' field");
671 }
672
673 #[test]
674 fn test_proof_generator_generate() {
675 let mock_provider = MockEthProvider::default();
677 let state_provider: Box<dyn StateProvider> = Box::new(mock_provider);
678
679 let mut codes = BTreeMap::new();
681 codes.insert(B256::from([1u8; 32]), Bytes::from("contract_code_1"));
682 codes.insert(B256::from([2u8; 32]), Bytes::from("contract_code_2"));
683
684 let mut preimages = BTreeMap::new();
685 preimages.insert(B256::from([3u8; 32]), Bytes::from("preimage_1"));
686 preimages.insert(B256::from([4u8; 32]), Bytes::from("preimage_2"));
687
688 let hashed_state = reth_trie::HashedPostState::default();
689
690 let result = generate(codes.clone(), preimages.clone(), hashed_state, state_provider);
692
693 assert!(result.is_ok(), "generate function should succeed");
695 let execution_witness = result.unwrap();
696
697 assert!(execution_witness.state.is_empty(), "State should be empty from MockEthProvider");
698
699 let expected_codes: Vec<Bytes> = codes.into_values().collect();
700 assert_eq!(
701 execution_witness.codes.len(),
702 expected_codes.len(),
703 "Codes length should match"
704 );
705 for code in &expected_codes {
706 assert!(
707 execution_witness.codes.contains(code),
708 "Codes should contain expected bytecode"
709 );
710 }
711
712 let expected_keys: Vec<Bytes> = preimages.into_values().collect();
713 assert_eq!(execution_witness.keys.len(), expected_keys.len(), "Keys length should match");
714 for key in &expected_keys {
715 assert!(execution_witness.keys.contains(key), "Keys should contain expected preimage");
716 }
717 }
718
719 #[test]
720 fn test_validate_bundle_state_matching() {
721 let (hook, _output_dir, _temp_dir) = create_test_hook();
722 let bundle_state = create_bundle_state();
723 let block_prefix = "test_block_123";
724
725 let result = hook.validate_bundle_state(&bundle_state, &bundle_state, block_prefix);
727 assert!(result.is_ok());
728 }
729
730 #[test]
731 fn test_validate_bundle_state_mismatch() {
732 let (hook, output_dir, _temp_dir) = create_test_hook();
733 let original_state = create_bundle_state();
734 let mut modified_state = create_bundle_state();
735
736 let addr = Address::from([1u8; 20]);
738 if let Some(account) = modified_state.state.get_mut(&addr) &&
739 let Some(ref mut info) = account.info
740 {
741 info.balance = U256::from(999);
742 }
743
744 let block_prefix = "test_block_mismatch";
745
746 let result = hook.validate_bundle_state(&modified_state, &original_state, block_prefix);
748 assert!(result.is_ok());
749
750 let original_file = output_dir.join(format!("{}.bundle_state.original.json", block_prefix));
752 let re_executed_file =
753 output_dir.join(format!("{}.bundle_state.re_executed.json", block_prefix));
754 let diff_file = output_dir.join(format!("{}.bundle_state.diff", block_prefix));
755
756 assert!(original_file.exists(), "Original bundle state file should be created");
757 assert!(re_executed_file.exists(), "Re-executed bundle state file should be created");
758 assert!(diff_file.exists(), "Diff file should be created");
759 }
760
761 fn create_test_trie_updates() -> TrieUpdates {
763 use alloy_primitives::map::HashMap;
764 use reth_trie::{updates::TrieUpdates, BranchNodeCompact, Nibbles};
765 use std::collections::HashSet;
766
767 let mut account_nodes = HashMap::default();
768 let nibbles = Nibbles::from_nibbles_unchecked([0x1, 0x2, 0x3]);
769 let branch_node = BranchNodeCompact::new(
770 0b1010, 0b1010, 0b1000, vec![B256::from([1u8; 32])], None, );
776 account_nodes.insert(nibbles, branch_node);
777
778 let mut removed_nodes = HashSet::default();
779 removed_nodes.insert(Nibbles::from_nibbles_unchecked([0x4, 0x5, 0x6]));
780
781 TrieUpdates { account_nodes, removed_nodes, storage_tries: HashMap::default() }
782 }
783
784 #[test]
785 fn test_validate_state_root_and_trie_with_trie_updates() {
786 let (hook, _output_dir, _temp_dir) = create_test_hook();
787 let bundle_state = create_bundle_state();
788
789 let mut rng = generators::rng();
791 let parent_header = generators::random_header(&mut rng, 1, None);
792 let recovered_block = random_block(
793 &mut rng,
794 2,
795 BlockParams {
796 parent: Some(parent_header.hash()),
797 tx_count: Some(0),
798 ..Default::default()
799 },
800 )
801 .try_recover()
802 .unwrap();
803
804 let trie_updates = create_test_trie_updates();
805 let original_root = B256::from([2u8; 32]); let block_prefix = "test_state_root_with_trie";
807
808 let result = hook.validate_state_root_and_trie(
810 &parent_header,
811 &recovered_block,
812 &bundle_state,
813 Some((&trie_updates, original_root)),
814 block_prefix,
815 );
816 assert!(result.is_ok());
817 }
818
819 #[test]
820 fn test_on_invalid_block_calls_all_validation_methods() {
821 let (hook, output_dir, _temp_dir) = create_test_hook();
822 let bundle_state = create_bundle_state();
823
824 let mut rng = generators::rng();
826 let parent_header = generators::random_header(&mut rng, 1, None);
827 let recovered_block = random_block(
828 &mut rng,
829 2,
830 BlockParams {
831 parent: Some(parent_header.hash()),
832 tx_count: Some(0),
833 ..Default::default()
834 },
835 )
836 .try_recover()
837 .unwrap();
838
839 let output = BlockExecutionOutput {
841 state: bundle_state,
842 result: reth_provider::BlockExecutionResult {
843 receipts: vec![],
844 requests: Requests::default(),
845 gas_used: 0,
846 blob_gas_used: 0,
847 },
848 };
849
850 let trie_updates = create_test_trie_updates();
852 let state_root = B256::random();
853
854 let files_before = output_dir.read_dir().unwrap().count();
857
858 let _result = hook.on_invalid_block(
859 &parent_header,
860 &recovered_block,
861 &output,
862 Some((&trie_updates, state_root)),
863 );
864
865 let files_after = output_dir.read_dir().unwrap().count();
868
869 assert!(
871 files_after >= files_before,
872 "on_invalid_block should attempt to create output files during processing"
873 );
874 }
875
876 #[test]
877 fn test_handle_witness_operations_with_empty_witness() {
878 let (hook, _output_dir, _temp_dir) = create_test_hook();
879 let witness = ExecutionWitness::default();
880 let block_prefix = "empty_witness_test";
881 let block_number = 12345;
882
883 let result = hook.handle_witness_operations(&witness, block_prefix, block_number);
884 assert!(result.is_ok());
885 }
886
887 #[test]
888 fn test_handle_witness_operations_with_zero_block_number() {
889 let (hook, _output_dir, _temp_dir) = create_test_hook();
890 let witness = ExecutionWitness {
891 state: vec![Bytes::from("test_state")],
892 codes: vec![Bytes::from("test_code")],
893 keys: vec![Bytes::from("test_key")],
894 ..Default::default()
895 };
896 let block_prefix = "zero_block_test";
897 let block_number = 0;
898
899 let result = hook.handle_witness_operations(&witness, block_prefix, block_number);
900 assert!(result.is_ok());
901 }
902
903 #[test]
904 fn test_handle_witness_operations_with_large_witness_data() {
905 let (hook, _output_dir, _temp_dir) = create_test_hook();
906 let large_data = vec![0u8; 10000]; let witness = ExecutionWitness {
908 state: vec![Bytes::from(large_data.clone())],
909 codes: vec![Bytes::from(large_data.clone())],
910 keys: vec![Bytes::from(large_data)],
911 ..Default::default()
912 };
913 let block_prefix = "large_witness_test";
914 let block_number = 999999;
915
916 let result = hook.handle_witness_operations(&witness, block_prefix, block_number);
917 assert!(result.is_ok());
918 }
919
920 #[test]
921 fn test_validate_bundle_state_with_empty_states() {
922 let (hook, _output_dir, _temp_dir) = create_test_hook();
923 let empty_state = BundleState::default();
924 let block_prefix = "empty_states_test";
925
926 let result = hook.validate_bundle_state(&empty_state, &empty_state, block_prefix);
927 assert!(result.is_ok());
928 }
929
930 #[test]
931 fn test_validate_bundle_state_with_different_contract_counts() {
932 let (hook, output_dir, _temp_dir) = create_test_hook();
933 let state1 = create_bundle_state();
934 let mut state2 = create_bundle_state();
935
936 let extra_contract_hash = B256::random();
938 state2.contracts.insert(
939 extra_contract_hash,
940 Bytecode::new_raw(Bytes::from(vec![0x60, 0x00, 0x60, 0x00, 0xfd])), );
942
943 let block_prefix = "different_contracts_test";
944 let result = hook.validate_bundle_state(&state1, &state2, block_prefix);
945 assert!(result.is_ok());
946
947 let diff_file = output_dir.join(format!("{}.bundle_state.diff", block_prefix));
949 assert!(diff_file.exists());
950 }
951
952 #[test]
953 fn test_save_diff_with_identical_values() {
954 let (hook, output_dir, _temp_dir) = create_test_hook();
955 let value1 = "identical_value";
956 let value2 = "identical_value";
957 let filename = "identical_diff_test".to_string();
958
959 let result = hook.save_diff(filename.clone(), &value1, &value2);
960 assert!(result.is_ok());
961
962 let diff_file = output_dir.join(filename);
963 assert!(diff_file.exists());
964 }
965
966 #[test]
967 fn test_validate_state_root_and_trie_without_trie_updates() {
968 let (hook, _output_dir, _temp_dir) = create_test_hook();
969 let bundle_state = create_bundle_state();
970
971 let mut rng = generators::rng();
972 let parent_header = generators::random_header(&mut rng, 1, None);
973 let recovered_block = random_block(
974 &mut rng,
975 2,
976 BlockParams {
977 parent: Some(parent_header.hash()),
978 tx_count: Some(0),
979 ..Default::default()
980 },
981 )
982 .try_recover()
983 .unwrap();
984
985 let block_prefix = "no_trie_updates_test";
986
987 let result = hook.validate_state_root_and_trie(
989 &parent_header,
990 &recovered_block,
991 &bundle_state,
992 None,
993 block_prefix,
994 );
995 assert!(result.is_ok());
996 }
997
998 #[test]
999 fn test_complete_invalid_block_workflow() {
1000 let (hook, _output_dir, _temp_dir) = create_test_hook();
1001 let mut rng = generators::rng();
1002
1003 let parent_header = generators::random_header(&mut rng, 100, None);
1005 let invalid_block = random_block(
1006 &mut rng,
1007 101,
1008 BlockParams {
1009 parent: Some(parent_header.hash()),
1010 tx_count: Some(3),
1011 ..Default::default()
1012 },
1013 )
1014 .try_recover()
1015 .unwrap();
1016
1017 let bundle_state = create_bundle_state();
1018 let trie_updates = create_test_trie_updates();
1019
1020 let validation_result =
1022 hook.validate_bundle_state(&bundle_state, &bundle_state, "integration_test");
1023 assert!(validation_result.is_ok(), "Bundle state validation should succeed");
1024
1025 let state_root_result = hook.validate_state_root_and_trie(
1026 &parent_header,
1027 &invalid_block,
1028 &bundle_state,
1029 Some((&trie_updates, B256::random())),
1030 "integration_test",
1031 );
1032 assert!(state_root_result.is_ok(), "State root validation should succeed");
1033 }
1034
1035 #[test]
1036 fn test_integration_workflow_components() {
1037 let (hook, _output_dir, _temp_dir) = create_test_hook();
1038 let mut rng = generators::rng();
1039
1040 let parent_header = generators::random_header(&mut rng, 50, None);
1042 let _invalid_block = random_block(
1043 &mut rng,
1044 51,
1045 BlockParams {
1046 parent: Some(parent_header.hash()),
1047 tx_count: Some(2),
1048 ..Default::default()
1049 },
1050 )
1051 .try_recover()
1052 .unwrap();
1053
1054 let bundle_state = create_bundle_state();
1055 let _trie_updates = create_test_trie_updates();
1056
1057 let validation_result =
1059 hook.validate_bundle_state(&bundle_state, &bundle_state, "integration_component_test");
1060 assert!(validation_result.is_ok(), "Component validation should succeed");
1061 }
1062}