Skip to main content

reth_invalid_block_hooks/
witness.rs

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/// Serializable version of `BundleState` for deterministic comparison
31#[derive(Debug, PartialEq, Eq)]
32struct BundleStateSorted {
33    /// Account state
34    pub state: BTreeMap<Address, BundleAccountSorted>,
35    /// All created contracts in this block.
36    pub contracts: BTreeMap<B256, Bytecode>,
37    /// Changes to revert
38    ///
39    /// **Note**: Inside vector is *not* sorted by address.
40    ///
41    /// But it is unique by address.
42    pub reverts: Vec<Vec<(Address, AccountRevertSorted)>>,
43    /// The size of the plain state in the bundle state
44    pub state_size: usize,
45    /// The size of reverts in the bundle state
46    pub reverts_size: usize,
47}
48
49/// Serializable version of `BundleAccount`
50#[derive(Debug, PartialEq, Eq)]
51struct BundleAccountSorted {
52    pub info: Option<AccountInfo>,
53    pub original_info: Option<AccountInfo>,
54    /// Contains both original and present state.
55    /// When extracting changeset we compare if original value is different from present value.
56    /// If it is different we add it to changeset.
57    /// If Account was destroyed we ignore original value and compare present state with
58    /// `U256::ZERO`.
59    pub storage: BTreeMap<U256, StorageSlot>,
60    /// Account status.
61    pub status: AccountStatus,
62}
63
64/// Serializable version of `AccountRevert`
65#[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
73/// Converts bundle state to sorted format for deterministic comparison
74fn 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
117/// Extracts execution data including codes, preimages, and hashed state from database
118fn 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    // Collect codes
127    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    // Collect preimages
133    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
158/// Generates execution witness from collected codes, preimages, and hashed state
159fn 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/// Hook for generating execution witnesses when invalid blocks are detected.
179///
180/// This hook captures the execution state and generates witness data that can be used
181/// for debugging and analysis of invalid block execution.
182#[derive(Debug)]
183pub struct InvalidBlockWitnessHook<P, E> {
184    /// The provider to read the historical state and do the EVM execution.
185    provider: P,
186    /// The EVM configuration to use for the execution.
187    evm_config: E,
188    /// The directory to write the witness to. Additionally, diff files will be written to this
189    /// directory in case of failed sanity checks.
190    output_directory: PathBuf,
191    /// The healthy node client to compare the witness against.
192    healthy_node_client: Option<jsonrpsee::http_client::HttpClient>,
193}
194
195impl<P, E> InvalidBlockWitnessHook<P, E> {
196    /// Creates a new witness hook.
197    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    /// Re-executes the block and collects execution data
214    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    /// Handles witness generation, saving, and comparison with healthy node
234    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    /// Validates that the bundle state after re-execution matches the original
272    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            // Convert bundle state to sorted format for deterministic comparison
285            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    /// Validates state root and trie updates after re-execution
302    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        // TODO(alexey): unify with `DebugApi::debug_execution_witness`
356        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    /// Serializes and saves a value to a JSON file in the output directory
375    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    /// Compares two values and saves their diff to a file in the output directory
383    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    /// Creates a test `BundleState` with realistic accounts, contracts, and reverts
433    fn create_bundle_state() -> BundleState {
434        let mut rng = generators::rng();
435        let mut bundle_state = BundleState::default();
436
437        // Generate realistic EOA accounts using generators
438        let accounts = random_eoa_accounts(&mut rng, 3);
439
440        for (i, (addr, account)) in accounts.into_iter().enumerate() {
441            // Create storage entries for each account
442            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        // Generate realistic contract bytecode using generators
475        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]), // Simple contract
479                1 => Bytes::from(vec![0x61, 0x81, 0x60, 0x00, 0x39]), // Another contract
480                _ => Bytes::from(vec![0x60, 0x00, 0x60, 0x00, 0xfd]), // REVERT contract
481            };
482            bundle_state.contracts.insert(*hash, Bytecode::new_raw(bytecode));
483        }
484
485        // Add reverts for multiple blocks using different accounts
486        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, // First account has storage wiped
490                ..AccountRevert::default()
491            };
492            bundle_state.reverts.push(vec![(*addr, revert)]);
493        }
494
495        // Set realistic sizes
496        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        // Use the fixture function to create test data
504        let bundle_state = create_bundle_state();
505
506        // Call the function under test
507        let sorted = sort_bundle_state_for_comparison(&bundle_state);
508
509        // Verify state_size and reverts_size values match the fixture
510        assert_eq!(sorted.state_size, 3);
511        assert_eq!(sorted.reverts_size, 2);
512
513        // Verify state contains our mock accounts
514        assert_eq!(sorted.state.len(), 3); // We added 3 accounts
515
516        // Verify contracts contains our mock contracts
517        assert_eq!(sorted.contracts.len(), 3); // We added 3 contracts
518
519        // Verify reverts is an array with multiple blocks of reverts
520        let reverts = &sorted.reverts;
521        assert_eq!(reverts.len(), 2); // Fixture has two blocks of reverts
522
523        // Verify that the state accounts have the expected structure
524        for account_data in sorted.state.values() {
525            // BundleAccountSorted has info, original_info, storage, and status fields
526            // Just verify the structure exists by accessing the fields
527            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        // Create test data using the fixture function
537        let bundle_state = create_bundle_state();
538
539        // Create a State with StateProviderTest
540        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        // Insert contracts from the fixture into the state cache
547        for (code_hash, bytecode) in &bundle_state.contracts {
548            state.cache.contracts.insert(*code_hash, bytecode.clone());
549        }
550
551        // Manually set the bundle state in the state object
552        state.bundle_state = bundle_state;
553
554        // Call the collect function
555        let result = collect_execution_data(state);
556        // Verify the function returns successfully
557        assert!(result.is_ok());
558
559        let (codes, _preimages, _hashed_state, returned_bundle_state) = result.unwrap();
560
561        // Verify that the returned data contains expected values
562        // Since we used the fixture data, we should have some codes and state
563        assert!(!codes.is_empty(), "Expected some bytecode entries");
564        assert!(!returned_bundle_state.state.is_empty(), "Expected some state entries");
565
566        // Verify the bundle state structure matches our fixture
567        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        // Create hook instance
574        let (hook, _output_directory, _temp_dir) = create_test_hook();
575
576        // Setup to call re_execute_block
577        let mut rng = generators::rng();
578        let parent_header = generators::random_header(&mut rng, 1, None);
579
580        // Create a random block that inherits from the parent header
581        let recovered_block = random_block(
582            &mut rng,
583            2, // block number
584            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        // Verify the function behavior with mock data
596        assert!(result.is_ok(), "re_execute_block should return Ok");
597    }
598
599    /// Creates test `InvalidBlockWitnessHook` with temporary directory
600    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        // Create hook instance with mock healthy client
620        let (hook, output_directory, _temp_dir) = create_test_hook();
621
622        // Create sample ExecutionWitness with correct types
623        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        // Call handle_witness_operations
631        let result = hook.handle_witness_operations(&witness, "test_block_healthy", 67890);
632
633        // Should succeed
634        assert!(result.is_ok());
635
636        // Check that witness file was created
637        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        // Test file creation and content validation
644        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        // Call handle_witness_operations
657        let result = hook.handle_witness_operations(&witness, block_prefix, block_number);
658        assert!(result.is_ok());
659
660        // Verify file was created with correct name
661        let expected_file =
662            output_directory.join(format!("{}.witness.re_executed.json", block_prefix));
663        assert!(expected_file.exists());
664
665        // Read and verify file content is valid JSON and contains witness structure
666        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        // Verify the JSON structure contains expected fields
671        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        // Use existing MockEthProvider
679        let mock_provider = MockEthProvider::default();
680        let state_provider: Box<dyn StateProvider> = Box::new(mock_provider);
681
682        // Mock Data
683        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        // Call generate function
694        let result = generate(codes.clone(), preimages.clone(), hashed_state, state_provider);
695
696        // Verify result
697        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        // Test with identical states - should not produce any warnings or files
729        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        // Modify the state to create a mismatch
740        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        // Test with different states - should save files and log warning
750        let result = hook.validate_bundle_state(&modified_state, &original_state, block_prefix);
751        assert!(result.is_ok());
752
753        // Verify that files were created
754        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    /// Creates test `TrieUpdates` with account nodes and removed nodes
765    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,                      // state_mask
774            0b1010,                      // tree_mask - must be subset of state_mask
775            0b1000,                      // hash_mask
776            vec![B256::from([1u8; 32])], // hashes
777            None,                        // root_hash
778        );
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        // Generate test data
793        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]); // Different from what will be computed
809        let block_prefix = "test_state_root_with_trie";
810
811        // Test with trie updates - this will likely produce warnings due to mock data
812        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        // Generate test data
828        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        // Create mock BlockExecutionOutput
843        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        // Create test trie updates
854        let trie_updates = create_test_trie_updates();
855        let state_root = B256::random();
856
857        // Test that on_invalid_block attempts to call all its internal methods
858        // by checking that it doesn't panic and tries to create files
859        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        // Verify that the function attempted to process the block:
869        // Either it succeeded, or it created some output files during processing
870        let files_after = output_dir.read_dir().unwrap().count();
871
872        // The function should attempt to execute its workflow
873        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]; // 10KB of data
910        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        // Add extra contract to state2
940        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])), // REVERT opcode
944        );
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        // Verify diff files were created
951        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        // Test without trie updates (None case)
991        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        // Create a realistic block scenario
1007        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        // Test validation methods
1024        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        // Create test data
1044        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        // Test individual components that would be part of the complete flow
1061        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}