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