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