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::{
9    BlockExecutionOutput, EvmStateProviderAdapter, StateProvider, StateProviderBox,
10    StateProviderFactory,
11};
12use reth_revm::{
13    database::StateProviderDatabase,
14    db::{BundleState, State},
15};
16use reth_rpc_api::DebugApiClient;
17use reth_tracing::tracing::warn;
18use reth_trie::updates::TrieUpdates;
19use revm::{
20    bytecode::Bytecode,
21    database::{
22        states::{reverts::AccountInfoRevert, StorageSlot},
23        AccountStatus, RevertToSlot,
24    },
25    state::AccountInfo,
26};
27use serde::Serialize;
28use std::{collections::BTreeMap, fmt::Debug, fs::File, io::Write, path::PathBuf};
29
30type CollectionResult =
31    (BTreeMap<B256, Bytes>, BTreeMap<B256, Bytes>, reth_trie::HashedPostState, BundleState);
32
33/// Serializable version of `BundleState` for deterministic comparison
34#[derive(Debug, PartialEq, Eq)]
35struct BundleStateSorted {
36    /// Account state
37    pub state: BTreeMap<Address, BundleAccountSorted>,
38    /// All created contracts in this block.
39    pub contracts: BTreeMap<B256, Bytecode>,
40    /// Changes to revert
41    ///
42    /// **Note**: Inside vector is *not* sorted by address.
43    ///
44    /// But it is unique by address.
45    pub reverts: Vec<Vec<(Address, AccountRevertSorted)>>,
46    /// The size of the plain state in the bundle state
47    pub state_size: usize,
48    /// The size of reverts in the bundle state
49    pub reverts_size: usize,
50}
51
52/// Serializable version of `BundleAccount`
53#[derive(Debug, PartialEq, Eq)]
54struct BundleAccountSorted {
55    pub info: Option<AccountInfo>,
56    pub original_info: Option<AccountInfo>,
57    /// Contains both original and present state.
58    /// When extracting changeset we compare if original value is different from present value.
59    /// If it is different we add it to changeset.
60    /// If Account was destroyed we ignore original value and compare present state with
61    /// `U256::ZERO`.
62    pub storage: BTreeMap<U256, StorageSlot>,
63    /// Account status.
64    pub status: AccountStatus,
65}
66
67/// Serializable version of `AccountRevert`
68#[derive(Debug, PartialEq, Eq)]
69struct AccountRevertSorted {
70    pub account: AccountInfoRevert,
71    pub storage: BTreeMap<U256, RevertToSlot>,
72    pub previous_status: AccountStatus,
73    pub wipe_storage: bool,
74}
75
76/// Converts bundle state to sorted format for deterministic comparison
77fn sort_bundle_state_for_comparison(bundle_state: &BundleState) -> BundleStateSorted {
78    BundleStateSorted {
79        state: bundle_state
80            .state
81            .iter()
82            .map(|(addr, acc)| {
83                (
84                    *addr,
85                    BundleAccountSorted {
86                        info: acc.info.clone(),
87                        original_info: acc.original_info.clone(),
88                        storage: acc.storage.iter().map(|(k, v)| (*k, *v)).collect(),
89                        status: acc.status,
90                    },
91                )
92            })
93            .collect(),
94        contracts: bundle_state.contracts.iter().map(|(k, v)| (*k, v.clone())).collect(),
95        reverts: bundle_state
96            .reverts
97            .iter()
98            .map(|block| {
99                block
100                    .iter()
101                    .map(|(addr, rev)| {
102                        (
103                            *addr,
104                            AccountRevertSorted {
105                                account: rev.account.clone(),
106                                storage: rev.storage.iter().map(|(k, v)| (*k, *v)).collect(),
107                                previous_status: rev.previous_status,
108                                wipe_storage: rev.wipe_storage,
109                            },
110                        )
111                    })
112                    .collect()
113            })
114            .collect(),
115        state_size: bundle_state.state_size,
116        reverts_size: bundle_state.reverts_size,
117    }
118}
119
120/// Extracts execution data including codes, preimages, and hashed state from database
121fn collect_execution_data(
122    mut db: State<StateProviderDatabase<EvmStateProviderAdapter<StateProviderBox>>>,
123) -> eyre::Result<CollectionResult> {
124    let bundle_state = db.take_bundle();
125    let mut codes = BTreeMap::new();
126    let mut preimages = BTreeMap::new();
127    let mut hashed_state = db.database.hashed_post_state(&bundle_state)?;
128
129    // Collect codes
130    db.cache.contracts.values().chain(bundle_state.contracts.values()).for_each(|code| {
131        codes.insert(code.hash_slow(), code.original_bytes());
132    });
133
134    // Collect preimages
135    for (address, account) in db.cache.accounts {
136        let hashed_address = keccak256(address);
137        hashed_state
138            .accounts
139            .insert(hashed_address, account.account.as_ref().map(|a| a.info.clone().into()));
140
141        if let Some(account_data) = account.account {
142            preimages.insert(hashed_address, alloy_rlp::encode(address).into());
143            let storage = hashed_state.storages.entry(hashed_address).or_default();
144
145            for (slot, value) in account_data.storage {
146                let slot_bytes = B256::from(slot);
147                let hashed_slot = keccak256(slot_bytes);
148                storage.storage.insert(hashed_slot, value);
149                preimages.insert(hashed_slot, alloy_rlp::encode(slot_bytes).into());
150            }
151        }
152    }
153
154    Ok((codes, preimages, hashed_state, bundle_state))
155}
156
157/// Generates execution witness from collected codes, preimages, and hashed state
158fn generate(
159    codes: BTreeMap<B256, Bytes>,
160    preimages: BTreeMap<B256, Bytes>,
161    hashed_state: reth_trie::HashedPostState,
162    state_provider: Box<dyn StateProvider>,
163) -> eyre::Result<ExecutionWitness> {
164    let state = state_provider.witness(
165        Default::default(),
166        hashed_state,
167        reth_trie::ExecutionWitnessMode::Legacy,
168    )?;
169    Ok(ExecutionWitness {
170        state,
171        codes: codes.into_values().collect(),
172        keys: preimages.into_values().collect(),
173        ..Default::default()
174    })
175}
176
177/// Hook for generating execution witnesses when invalid blocks are detected.
178///
179/// This hook captures the execution state and generates witness data that can be used
180/// for debugging and analysis of invalid block execution.
181#[derive(Debug)]
182pub struct InvalidBlockWitnessHook<P, E> {
183    /// The provider to read the historical state and do the EVM execution.
184    provider: P,
185    /// The EVM configuration to use for the execution.
186    evm_config: E,
187    /// The directory to write the witness to. Additionally, diff files will be written to this
188    /// directory in case of failed sanity checks.
189    output_directory: PathBuf,
190    /// The healthy node client to compare the witness against.
191    healthy_node_client: Option<jsonrpsee::http_client::HttpClient>,
192}
193
194impl<P, E> InvalidBlockWitnessHook<P, E> {
195    /// Creates a new witness hook.
196    pub const fn new(
197        provider: P,
198        evm_config: E,
199        output_directory: PathBuf,
200        healthy_node_client: Option<jsonrpsee::http_client::HttpClient>,
201    ) -> Self {
202        Self { provider, evm_config, output_directory, healthy_node_client }
203    }
204}
205
206impl<P, E, N> InvalidBlockWitnessHook<P, E>
207where
208    P: StateProviderFactory + Send + Sync + 'static,
209    E: ConfigureEvm<Primitives = N> + 'static,
210    N: NodePrimitives,
211{
212    /// Re-executes the block and collects execution data
213    fn re_execute_block(
214        &self,
215        parent_header: &SealedHeader<N::BlockHeader>,
216        block: &RecoveredBlock<N::Block>,
217    ) -> eyre::Result<(ExecutionWitness, BundleState)> {
218        let mut executor = self.evm_config.batch_executor(StateProviderDatabase::new(
219            self.provider.state_by_block_hash(parent_header.hash())?.into_evm_state_provider(),
220        ));
221
222        executor.execute_one(block)?;
223        let db = executor.into_state();
224        let (codes, preimages, hashed_state, bundle_state) = collect_execution_data(db)?;
225
226        let state_provider = self.provider.state_by_block_hash(parent_header.hash())?;
227        let witness = generate(codes, preimages, hashed_state, state_provider)?;
228
229        Ok((witness, bundle_state))
230    }
231
232    /// Handles witness generation, saving, and comparison with healthy node
233    fn handle_witness_operations(
234        &self,
235        witness: &ExecutionWitness,
236        block_prefix: &str,
237        block_number: u64,
238    ) -> eyre::Result<()> {
239        let filename = format!("{}.witness.re_executed.json", block_prefix);
240        let re_executed_witness_path = self.save_file(filename, witness)?;
241
242        if let Some(healthy_node_client) = &self.healthy_node_client {
243            let healthy_node_witness = futures::executor::block_on(async move {
244                DebugApiClient::<()>::debug_execution_witness(
245                    healthy_node_client,
246                    block_number.into(),
247                    None,
248                )
249                .await
250            })?;
251
252            let filename = format!("{}.witness.healthy.json", block_prefix);
253            let healthy_path = self.save_file(filename, &healthy_node_witness)?;
254
255            if witness != &healthy_node_witness {
256                let filename = format!("{}.witness.diff", block_prefix);
257                let diff_path = self.save_diff(filename, witness, &healthy_node_witness)?;
258                warn!(
259                    target: "engine::invalid_block_hooks::witness",
260                    diff_path = %diff_path.display(),
261                    re_executed_path = %re_executed_witness_path.display(),
262                    healthy_path = %healthy_path.display(),
263                    "Witness mismatch against healthy node"
264                );
265            }
266        }
267        Ok(())
268    }
269
270    /// Validates that the bundle state after re-execution matches the original
271    fn validate_bundle_state(
272        &self,
273        re_executed_state: &BundleState,
274        original_state: &BundleState,
275        block_prefix: &str,
276    ) -> eyre::Result<()> {
277        if re_executed_state != original_state {
278            let original_filename = format!("{}.bundle_state.original.json", block_prefix);
279            let original_path = self.save_file(original_filename, original_state)?;
280            let re_executed_filename = format!("{}.bundle_state.re_executed.json", block_prefix);
281            let re_executed_path = self.save_file(re_executed_filename, re_executed_state)?;
282
283            // Convert bundle state to sorted format for deterministic comparison
284            let bundle_state_sorted = sort_bundle_state_for_comparison(re_executed_state);
285            let output_state_sorted = sort_bundle_state_for_comparison(original_state);
286            let filename = format!("{}.bundle_state.diff", block_prefix);
287            let diff_path = self.save_diff(filename, &output_state_sorted, &bundle_state_sorted)?;
288
289            warn!(
290                target: "engine::invalid_block_hooks::witness",
291                diff_path = %diff_path.display(),
292                original_path = %original_path.display(),
293                re_executed_path = %re_executed_path.display(),
294                "Bundle state mismatch after re-execution"
295            );
296        }
297        Ok(())
298    }
299
300    /// Validates state root and trie updates after re-execution
301    fn validate_state_root_and_trie(
302        &self,
303        parent_header: &SealedHeader<N::BlockHeader>,
304        block: &RecoveredBlock<N::Block>,
305        bundle_state: &BundleState,
306        trie_updates: Option<(&TrieUpdates, B256)>,
307        block_prefix: &str,
308    ) -> eyre::Result<()> {
309        let state_provider = self.provider.state_by_block_hash(parent_header.hash())?;
310        let hashed_state = state_provider.hashed_post_state(bundle_state)?;
311        let (re_executed_root, trie_output) =
312            state_provider.state_root_with_updates(hashed_state)?;
313
314        if let Some((original_updates, original_root)) = trie_updates {
315            if re_executed_root != original_root {
316                let filename = format!("{}.state_root.diff", block_prefix);
317                let diff_path = self.save_diff(filename, &original_root, &re_executed_root)?;
318                warn!(target: "engine::invalid_block_hooks::witness", ?original_root, ?re_executed_root, diff_path = %diff_path.display(), "State root mismatch after re-execution");
319            }
320
321            if re_executed_root != block.state_root() {
322                let filename = format!("{}.header_state_root.diff", block_prefix);
323                let diff_path = self.save_diff(filename, &block.state_root(), &re_executed_root)?;
324                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");
325            }
326
327            if &trie_output != original_updates {
328                let original_path = self.save_file(
329                    format!("{}.trie_updates.original.json", block_prefix),
330                    &original_updates.into_sorted_ref(),
331                )?;
332                let re_executed_path = self.save_file(
333                    format!("{}.trie_updates.re_executed.json", block_prefix),
334                    &trie_output.into_sorted_ref(),
335                )?;
336                warn!(
337                    target: "engine::invalid_block_hooks::witness",
338                    original_path = %original_path.display(),
339                    re_executed_path = %re_executed_path.display(),
340                    "Trie updates mismatch after re-execution"
341                );
342            }
343        }
344        Ok(())
345    }
346
347    fn on_invalid_block(
348        &self,
349        parent_header: &SealedHeader<N::BlockHeader>,
350        block: &RecoveredBlock<N::Block>,
351        output: &BlockExecutionOutput<N::Receipt>,
352        trie_updates: Option<(&TrieUpdates, B256)>,
353    ) -> eyre::Result<()> {
354        // TODO(alexey): unify with `DebugApi::debug_execution_witness`
355        let (witness, bundle_state) = self.re_execute_block(parent_header, block)?;
356
357        let block_prefix = format!("{}_{}", block.number(), block.hash());
358        self.handle_witness_operations(&witness, &block_prefix, block.number())?;
359
360        self.validate_bundle_state(&bundle_state, &output.state, &block_prefix)?;
361
362        self.validate_state_root_and_trie(
363            parent_header,
364            block,
365            &bundle_state,
366            trie_updates,
367            &block_prefix,
368        )?;
369
370        Ok(())
371    }
372
373    /// Serializes and saves a value to a JSON file in the output directory
374    fn save_file<T: Serialize>(&self, filename: String, value: &T) -> eyre::Result<PathBuf> {
375        let path = self.output_directory.join(filename);
376        File::create(&path)?.write_all(serde_json::to_string(value)?.as_bytes())?;
377
378        Ok(path)
379    }
380
381    /// Compares two values and saves their diff to a file in the output directory
382    fn save_diff<T: PartialEq + Debug>(
383        &self,
384        filename: String,
385        original: &T,
386        new: &T,
387    ) -> eyre::Result<PathBuf> {
388        let path = self.output_directory.join(filename);
389        let diff = Comparison::new(original, new);
390        File::create(&path)?.write_all(diff.to_string().as_bytes())?;
391
392        Ok(path)
393    }
394}
395
396impl<P, E, N: NodePrimitives> InvalidBlockHook<N> for InvalidBlockWitnessHook<P, E>
397where
398    P: StateProviderFactory + Send + Sync + 'static,
399    E: ConfigureEvm<Primitives = N> + 'static,
400{
401    fn on_invalid_block(
402        &self,
403        parent_header: &SealedHeader<N::BlockHeader>,
404        block: &RecoveredBlock<N::Block>,
405        output: &BlockExecutionOutput<N::Receipt>,
406        trie_updates: Option<(&TrieUpdates, B256)>,
407    ) {
408        if let Err(err) = self.on_invalid_block(parent_header, block, output, trie_updates) {
409            warn!(target: "engine::invalid_block_hooks::witness", %err, "Failed to invoke hook");
410        }
411    }
412}
413
414#[cfg(test)]
415mod tests {
416    use super::*;
417    use alloy_eips::eip7685::Requests;
418    use alloy_primitives::{map::HashMap, Address, Bytes, B256, U256};
419    use reth_chainspec::ChainSpec;
420    use reth_ethereum_primitives::EthPrimitives;
421    use reth_evm_ethereum::EthEvmConfig;
422    use reth_provider::test_utils::MockEthProvider;
423    use reth_revm::db::{BundleAccount, BundleState};
424    use revm::database::states::reverts::AccountRevert;
425    use tempfile::TempDir;
426
427    use reth_revm::test_utils::StateProviderTest;
428    use reth_testing_utils::generators::{self, random_block, random_eoa_accounts, BlockParams};
429    use revm::bytecode::Bytecode;
430
431    /// Creates a test `BundleState` with realistic accounts, contracts, and reverts
432    fn create_bundle_state() -> BundleState {
433        let mut rng = generators::rng();
434        let mut bundle_state = BundleState::default();
435
436        // Generate realistic EOA accounts using generators
437        let accounts = random_eoa_accounts(&mut rng, 3);
438
439        for (i, (addr, account)) in accounts.into_iter().enumerate() {
440            // Create storage entries for each account
441            let mut storage = HashMap::default();
442            let storage_key = U256::from(i + 1);
443            storage.insert(
444                storage_key,
445                StorageSlot {
446                    present_value: U256::from((i + 1) * 10),
447                    previous_or_original_value: U256::from((i + 1) * 15),
448                },
449            );
450
451            let bundle_account = BundleAccount {
452                info: Some(AccountInfo {
453                    balance: account.balance,
454                    nonce: account.nonce,
455                    code_hash: account.bytecode_hash.unwrap_or_default(),
456                    code: None,
457                    ..Default::default()
458                }),
459                original_info: (i == 0).then(|| AccountInfo {
460                    balance: account.balance.checked_div(U256::from(2)).unwrap_or(U256::ZERO),
461                    code_hash: account.bytecode_hash.unwrap_or_default(),
462                    code: None,
463                    ..Default::default()
464                }),
465                storage,
466                status: AccountStatus::default(),
467            };
468
469            bundle_state.state.insert(addr, bundle_account);
470        }
471
472        // Generate realistic contract bytecode using generators
473        let contract_hashes: Vec<B256> = (0..3).map(|_| B256::random()).collect();
474        for (i, hash) in contract_hashes.iter().enumerate() {
475            let bytecode = match i {
476                0 => Bytes::from(vec![0x60, 0x80, 0x60, 0x40, 0x52]), // Simple contract
477                1 => Bytes::from(vec![0x61, 0x81, 0x60, 0x00, 0x39]), // Another contract
478                _ => Bytes::from(vec![0x60, 0x00, 0x60, 0x00, 0xfd]), // REVERT contract
479            };
480            bundle_state.contracts.insert(*hash, Bytecode::new_raw(bytecode));
481        }
482
483        // Add reverts for multiple blocks using different accounts
484        let addresses: Vec<Address> = bundle_state.state.keys().copied().collect();
485        for (i, addr) in addresses.iter().take(2).enumerate() {
486            let revert = AccountRevert {
487                wipe_storage: i == 0, // First account has storage wiped
488                ..AccountRevert::default()
489            };
490            bundle_state.reverts.push(vec![(*addr, revert)]);
491        }
492
493        // Set realistic sizes
494        bundle_state.state_size = bundle_state.state.len();
495        bundle_state.reverts_size = bundle_state.reverts.len();
496
497        bundle_state
498    }
499    #[test]
500    fn test_sort_bundle_state_for_comparison() {
501        // Use the fixture function to create test data
502        let bundle_state = create_bundle_state();
503
504        // Call the function under test
505        let sorted = sort_bundle_state_for_comparison(&bundle_state);
506
507        // Verify state_size and reverts_size values match the fixture
508        assert_eq!(sorted.state_size, 3);
509        assert_eq!(sorted.reverts_size, 2);
510
511        // Verify state contains our mock accounts
512        assert_eq!(sorted.state.len(), 3); // We added 3 accounts
513
514        // Verify contracts contains our mock contracts
515        assert_eq!(sorted.contracts.len(), 3); // We added 3 contracts
516
517        // Verify reverts is an array with multiple blocks of reverts
518        let reverts = &sorted.reverts;
519        assert_eq!(reverts.len(), 2); // Fixture has two blocks of reverts
520
521        // Verify that the state accounts have the expected structure
522        for account_data in sorted.state.values() {
523            // BundleAccountSorted has info, original_info, storage, and status fields
524            // Just verify the structure exists by accessing the fields
525            let _info = &account_data.info;
526            let _original_info = &account_data.original_info;
527            let _storage = &account_data.storage;
528            let _status = &account_data.status;
529        }
530    }
531
532    #[test]
533    fn test_data_collector_collect() {
534        // Create test data using the fixture function
535        let bundle_state = create_bundle_state();
536
537        // Create a State with StateProviderTest
538        let state_provider = StateProviderTest::default();
539        let mut state = State::builder()
540            .with_database(StateProviderDatabase::new(
541                (Box::new(state_provider) as StateProviderBox).into_evm_state_provider(),
542            ))
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}