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