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::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/// Serializable version of `BundleState` for deterministic comparison
29#[derive(Debug, PartialEq, Eq)]
30struct BundleStateSorted {
31    /// Account state
32    pub state: BTreeMap<Address, BundleAccountSorted>,
33    /// All created contracts in this block.
34    pub contracts: BTreeMap<B256, Bytecode>,
35    /// Changes to revert
36    ///
37    /// **Note**: Inside vector is *not* sorted by address.
38    ///
39    /// But it is unique by address.
40    pub reverts: Vec<Vec<(Address, AccountRevertSorted)>>,
41    /// The size of the plain state in the bundle state
42    pub state_size: usize,
43    /// The size of reverts in the bundle state
44    pub reverts_size: usize,
45}
46
47/// Serializable version of `BundleAccount`
48#[derive(Debug, PartialEq, Eq)]
49struct BundleAccountSorted {
50    pub info: Option<AccountInfo>,
51    pub original_info: Option<AccountInfo>,
52    /// Contains both original and present state.
53    /// When extracting changeset we compare if original value is different from present value.
54    /// If it is different we add it to changeset.
55    /// If Account was destroyed we ignore original value and compare present state with
56    /// `U256::ZERO`.
57    pub storage: BTreeMap<U256, StorageSlot>,
58    /// Account status.
59    pub status: AccountStatus,
60}
61
62/// Serializable version of `AccountRevert`
63#[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
71/// Converts bundle state to sorted format for deterministic comparison
72fn 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
115/// Extracts execution data including codes, preimages, and hashed state from database
116fn 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    // Collect codes
125    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    // Collect preimages
131    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
156/// Generates execution witness from collected codes, preimages, and hashed state
157fn generate(
158    codes: BTreeMap<B256, Bytes>,
159    preimages: BTreeMap<B256, Bytes>,
160    hashed_state: reth_trie::HashedPostState,
161    state_provider: Box<dyn StateProvider>,
162) -> eyre::Result<ExecutionWitness> {
163    let state = state_provider.witness(
164        Default::default(),
165        hashed_state,
166        reth_trie::ExecutionWitnessMode::Legacy,
167    )?;
168    Ok(ExecutionWitness {
169        state,
170        codes: codes.into_values().collect(),
171        keys: preimages.into_values().collect(),
172        ..Default::default()
173    })
174}
175
176/// Hook for generating execution witnesses when invalid blocks are detected.
177///
178/// This hook captures the execution state and generates witness data that can be used
179/// for debugging and analysis of invalid block execution.
180#[derive(Debug)]
181pub struct InvalidBlockWitnessHook<P, E> {
182    /// The provider to read the historical state and do the EVM execution.
183    provider: P,
184    /// The EVM configuration to use for the execution.
185    evm_config: E,
186    /// The directory to write the witness to. Additionally, diff files will be written to this
187    /// directory in case of failed sanity checks.
188    output_directory: PathBuf,
189    /// The healthy node client to compare the witness against.
190    healthy_node_client: Option<jsonrpsee::http_client::HttpClient>,
191}
192
193impl<P, E> InvalidBlockWitnessHook<P, E> {
194    /// Creates a new witness hook.
195    pub const fn new(
196        provider: P,
197        evm_config: E,
198        output_directory: PathBuf,
199        healthy_node_client: Option<jsonrpsee::http_client::HttpClient>,
200    ) -> Self {
201        Self { provider, evm_config, output_directory, healthy_node_client }
202    }
203}
204
205impl<P, E, N> InvalidBlockWitnessHook<P, E>
206where
207    P: StateProviderFactory + Send + Sync + 'static,
208    E: ConfigureEvm<Primitives = N> + 'static,
209    N: NodePrimitives,
210{
211    /// Re-executes the block and collects execution data
212    fn re_execute_block(
213        &self,
214        parent_header: &SealedHeader<N::BlockHeader>,
215        block: &RecoveredBlock<N::Block>,
216    ) -> eyre::Result<(ExecutionWitness, BundleState)> {
217        let mut executor = self.evm_config.batch_executor(StateProviderDatabase::new(
218            self.provider.state_by_block_hash(parent_header.hash())?,
219        ));
220
221        executor.execute_one(block)?;
222        let db = executor.into_state();
223        let (codes, preimages, hashed_state, bundle_state) = collect_execution_data(db)?;
224
225        let state_provider = self.provider.state_by_block_hash(parent_header.hash())?;
226        let witness = generate(codes, preimages, hashed_state, state_provider)?;
227
228        Ok((witness, bundle_state))
229    }
230
231    /// Handles witness generation, saving, and comparison with healthy node
232    fn handle_witness_operations(
233        &self,
234        witness: &ExecutionWitness,
235        block_prefix: &str,
236        block_number: u64,
237    ) -> eyre::Result<()> {
238        let filename = format!("{}.witness.re_executed.json", block_prefix);
239        let re_executed_witness_path = self.save_file(filename, witness)?;
240
241        if let Some(healthy_node_client) = &self.healthy_node_client {
242            let healthy_node_witness = futures::executor::block_on(async move {
243                DebugApiClient::<()>::debug_execution_witness(
244                    healthy_node_client,
245                    block_number.into(),
246                    None,
247                )
248                .await
249            })?;
250
251            let filename = format!("{}.witness.healthy.json", block_prefix);
252            let healthy_path = self.save_file(filename, &healthy_node_witness)?;
253
254            if witness != &healthy_node_witness {
255                let filename = format!("{}.witness.diff", block_prefix);
256                let diff_path = self.save_diff(filename, witness, &healthy_node_witness)?;
257                warn!(
258                    target: "engine::invalid_block_hooks::witness",
259                    diff_path = %diff_path.display(),
260                    re_executed_path = %re_executed_witness_path.display(),
261                    healthy_path = %healthy_path.display(),
262                    "Witness mismatch against healthy node"
263                );
264            }
265        }
266        Ok(())
267    }
268
269    /// Validates that the bundle state after re-execution matches the original
270    fn validate_bundle_state(
271        &self,
272        re_executed_state: &BundleState,
273        original_state: &BundleState,
274        block_prefix: &str,
275    ) -> eyre::Result<()> {
276        if re_executed_state != original_state {
277            let original_filename = format!("{}.bundle_state.original.json", block_prefix);
278            let original_path = self.save_file(original_filename, original_state)?;
279            let re_executed_filename = format!("{}.bundle_state.re_executed.json", block_prefix);
280            let re_executed_path = self.save_file(re_executed_filename, re_executed_state)?;
281
282            // Convert bundle state to sorted format for deterministic comparison
283            let bundle_state_sorted = sort_bundle_state_for_comparison(re_executed_state);
284            let output_state_sorted = sort_bundle_state_for_comparison(original_state);
285            let filename = format!("{}.bundle_state.diff", block_prefix);
286            let diff_path = self.save_diff(filename, &output_state_sorted, &bundle_state_sorted)?;
287
288            warn!(
289                target: "engine::invalid_block_hooks::witness",
290                diff_path = %diff_path.display(),
291                original_path = %original_path.display(),
292                re_executed_path = %re_executed_path.display(),
293                "Bundle state mismatch after re-execution"
294            );
295        }
296        Ok(())
297    }
298
299    /// Validates state root and trie updates after re-execution
300    fn validate_state_root_and_trie(
301        &self,
302        parent_header: &SealedHeader<N::BlockHeader>,
303        block: &RecoveredBlock<N::Block>,
304        bundle_state: &BundleState,
305        trie_updates: Option<(&TrieUpdates, B256)>,
306        block_prefix: &str,
307    ) -> eyre::Result<()> {
308        let state_provider = self.provider.state_by_block_hash(parent_header.hash())?;
309        let hashed_state = state_provider.hashed_post_state(bundle_state);
310        let (re_executed_root, trie_output) =
311            state_provider.state_root_with_updates(hashed_state)?;
312
313        if let Some((original_updates, original_root)) = trie_updates {
314            if re_executed_root != original_root {
315                let filename = format!("{}.state_root.diff", block_prefix);
316                let diff_path = self.save_diff(filename, &original_root, &re_executed_root)?;
317                warn!(target: "engine::invalid_block_hooks::witness", ?original_root, ?re_executed_root, diff_path = %diff_path.display(), "State root mismatch after re-execution");
318            }
319
320            if re_executed_root != block.state_root() {
321                let filename = format!("{}.header_state_root.diff", block_prefix);
322                let diff_path = self.save_diff(filename, &block.state_root(), &re_executed_root)?;
323                warn!(target: "engine::invalid_block_hooks::witness", header_state_root=?block.state_root(), ?re_executed_root, diff_path = %diff_path.display(), "Re-executed state root does not match block state root");
324            }
325
326            if &trie_output != original_updates {
327                let original_path = self.save_file(
328                    format!("{}.trie_updates.original.json", block_prefix),
329                    &original_updates.into_sorted_ref(),
330                )?;
331                let re_executed_path = self.save_file(
332                    format!("{}.trie_updates.re_executed.json", block_prefix),
333                    &trie_output.into_sorted_ref(),
334                )?;
335                warn!(
336                    target: "engine::invalid_block_hooks::witness",
337                    original_path = %original_path.display(),
338                    re_executed_path = %re_executed_path.display(),
339                    "Trie updates mismatch after re-execution"
340                );
341            }
342        }
343        Ok(())
344    }
345
346    fn on_invalid_block(
347        &self,
348        parent_header: &SealedHeader<N::BlockHeader>,
349        block: &RecoveredBlock<N::Block>,
350        output: &BlockExecutionOutput<N::Receipt>,
351        trie_updates: Option<(&TrieUpdates, B256)>,
352    ) -> eyre::Result<()> {
353        // TODO(alexey): unify with `DebugApi::debug_execution_witness`
354        let (witness, bundle_state) = self.re_execute_block(parent_header, block)?;
355
356        let block_prefix = format!("{}_{}", block.number(), block.hash());
357        self.handle_witness_operations(&witness, &block_prefix, block.number())?;
358
359        self.validate_bundle_state(&bundle_state, &output.state, &block_prefix)?;
360
361        self.validate_state_root_and_trie(
362            parent_header,
363            block,
364            &bundle_state,
365            trie_updates,
366            &block_prefix,
367        )?;
368
369        Ok(())
370    }
371
372    /// Serializes and saves a value to a JSON file in the output directory
373    fn save_file<T: Serialize>(&self, filename: String, value: &T) -> eyre::Result<PathBuf> {
374        let path = self.output_directory.join(filename);
375        File::create(&path)?.write_all(serde_json::to_string(value)?.as_bytes())?;
376
377        Ok(path)
378    }
379
380    /// Compares two values and saves their diff to a file in the output directory
381    fn save_diff<T: PartialEq + Debug>(
382        &self,
383        filename: String,
384        original: &T,
385        new: &T,
386    ) -> eyre::Result<PathBuf> {
387        let path = self.output_directory.join(filename);
388        let diff = Comparison::new(original, new);
389        File::create(&path)?.write_all(diff.to_string().as_bytes())?;
390
391        Ok(path)
392    }
393}
394
395impl<P, E, N: NodePrimitives> InvalidBlockHook<N> for InvalidBlockWitnessHook<P, E>
396where
397    P: StateProviderFactory + Send + Sync + 'static,
398    E: ConfigureEvm<Primitives = N> + 'static,
399{
400    fn on_invalid_block(
401        &self,
402        parent_header: &SealedHeader<N::BlockHeader>,
403        block: &RecoveredBlock<N::Block>,
404        output: &BlockExecutionOutput<N::Receipt>,
405        trie_updates: Option<(&TrieUpdates, B256)>,
406    ) {
407        if let Err(err) = self.on_invalid_block(parent_header, block, output, trie_updates) {
408            warn!(target: "engine::invalid_block_hooks::witness", %err, "Failed to invoke hook");
409        }
410    }
411}
412
413#[cfg(test)]
414mod tests {
415    use super::*;
416    use alloy_eips::eip7685::Requests;
417    use alloy_primitives::{map::HashMap, Address, Bytes, B256, U256};
418    use reth_chainspec::ChainSpec;
419    use reth_ethereum_primitives::EthPrimitives;
420    use reth_evm_ethereum::EthEvmConfig;
421    use reth_provider::test_utils::MockEthProvider;
422    use reth_revm::db::{BundleAccount, BundleState};
423    use revm_database::states::reverts::AccountRevert;
424    use tempfile::TempDir;
425
426    use reth_revm::test_utils::StateProviderTest;
427    use reth_testing_utils::generators::{self, random_block, random_eoa_accounts, BlockParams};
428    use revm_bytecode::Bytecode;
429
430    /// Creates a test `BundleState` with realistic accounts, contracts, and reverts
431    fn create_bundle_state() -> BundleState {
432        let mut rng = generators::rng();
433        let mut bundle_state = BundleState::default();
434
435        // Generate realistic EOA accounts using generators
436        let accounts = random_eoa_accounts(&mut rng, 3);
437
438        for (i, (addr, account)) in accounts.into_iter().enumerate() {
439            // Create storage entries for each account
440            let mut storage = HashMap::default();
441            let storage_key = U256::from(i + 1);
442            storage.insert(
443                storage_key,
444                StorageSlot {
445                    present_value: U256::from((i + 1) * 10),
446                    previous_or_original_value: U256::from((i + 1) * 15),
447                },
448            );
449
450            let bundle_account = BundleAccount {
451                info: Some(AccountInfo {
452                    balance: account.balance,
453                    nonce: account.nonce,
454                    code_hash: account.bytecode_hash.unwrap_or_default(),
455                    code: None,
456                    account_id: None,
457                }),
458                original_info: (i == 0).then(|| AccountInfo {
459                    balance: account.balance.checked_div(U256::from(2)).unwrap_or(U256::ZERO),
460                    nonce: 0,
461                    code_hash: account.bytecode_hash.unwrap_or_default(),
462                    code: None,
463                    account_id: None,
464                }),
465                storage,
466                status: AccountStatus::default(),
467            };
468
469            bundle_state.state.insert(addr, bundle_account);
470        }
471
472        // 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(Box::new(state_provider) as StateProviderBox))
541            .with_bundle_update()
542            .build();
543
544        // Insert contracts from the fixture into the state cache
545        for (code_hash, bytecode) in &bundle_state.contracts {
546            state.cache.contracts.insert(*code_hash, bytecode.clone());
547        }
548
549        // Manually set the bundle state in the state object
550        state.bundle_state = bundle_state;
551
552        // Call the collect function
553        let result = collect_execution_data(state);
554        // Verify the function returns successfully
555        assert!(result.is_ok());
556
557        let (codes, _preimages, _hashed_state, returned_bundle_state) = result.unwrap();
558
559        // Verify that the returned data contains expected values
560        // Since we used the fixture data, we should have some codes and state
561        assert!(!codes.is_empty(), "Expected some bytecode entries");
562        assert!(!returned_bundle_state.state.is_empty(), "Expected some state entries");
563
564        // Verify the bundle state structure matches our fixture
565        assert_eq!(returned_bundle_state.state.len(), 3, "Expected 3 accounts from fixture");
566        assert_eq!(returned_bundle_state.contracts.len(), 3, "Expected 3 contracts from fixture");
567    }
568
569    #[test]
570    fn test_re_execute_block() {
571        // Create hook instance
572        let (hook, _output_directory, _temp_dir) = create_test_hook();
573
574        // Setup to call re_execute_block
575        let mut rng = generators::rng();
576        let parent_header = generators::random_header(&mut rng, 1, None);
577
578        // Create a random block that inherits from the parent header
579        let recovered_block = random_block(
580            &mut rng,
581            2, // block number
582            BlockParams {
583                parent: Some(parent_header.hash()),
584                tx_count: Some(0),
585                ..Default::default()
586            },
587        )
588        .try_recover()
589        .unwrap();
590
591        let result = hook.re_execute_block(&parent_header, &recovered_block);
592
593        // Verify the function behavior with mock data
594        assert!(result.is_ok(), "re_execute_block should return Ok");
595    }
596
597    /// Creates test `InvalidBlockWitnessHook` with temporary directory
598    fn create_test_hook() -> (
599        InvalidBlockWitnessHook<MockEthProvider<EthPrimitives, ChainSpec>, EthEvmConfig>,
600        PathBuf,
601        TempDir,
602    ) {
603        let temp_dir = TempDir::new().expect("Failed to create temp dir");
604        let output_directory = temp_dir.path().to_path_buf();
605
606        let provider = MockEthProvider::<EthPrimitives, ChainSpec>::default();
607        let evm_config = EthEvmConfig::mainnet();
608
609        let hook =
610            InvalidBlockWitnessHook::new(provider, evm_config, output_directory.clone(), None);
611
612        (hook, output_directory, temp_dir)
613    }
614
615    #[test]
616    fn test_handle_witness_operations_with_healthy_client_mock() {
617        // Create hook instance with mock healthy client
618        let (hook, output_directory, _temp_dir) = create_test_hook();
619
620        // Create sample ExecutionWitness with correct types
621        let witness = ExecutionWitness {
622            state: vec![Bytes::from("state_data")],
623            codes: vec![Bytes::from("code_data")],
624            keys: vec![Bytes::from("key_data")],
625            ..Default::default()
626        };
627
628        // Call handle_witness_operations
629        let result = hook.handle_witness_operations(&witness, "test_block_healthy", 67890);
630
631        // Should succeed
632        assert!(result.is_ok());
633
634        // Check that witness file was created
635        let witness_file = output_directory.join("test_block_healthy.witness.re_executed.json");
636        assert!(witness_file.exists());
637    }
638
639    #[test]
640    fn test_handle_witness_operations_file_creation() {
641        // Test file creation and content validation
642        let (hook, output_directory, _temp_dir) = create_test_hook();
643
644        let witness = ExecutionWitness {
645            state: vec![Bytes::from("test_state")],
646            codes: vec![Bytes::from("test_code")],
647            keys: vec![Bytes::from("test_key")],
648            ..Default::default()
649        };
650
651        let block_prefix = "file_test_block";
652        let block_number = 11111;
653
654        // Call handle_witness_operations
655        let result = hook.handle_witness_operations(&witness, block_prefix, block_number);
656        assert!(result.is_ok());
657
658        // Verify file was created with correct name
659        let expected_file =
660            output_directory.join(format!("{}.witness.re_executed.json", block_prefix));
661        assert!(expected_file.exists());
662
663        // Read and verify file content is valid JSON and contains witness structure
664        let file_content = std::fs::read_to_string(&expected_file).expect("Failed to read file");
665        let parsed_witness: serde_json::Value =
666            serde_json::from_str(&file_content).expect("File should contain valid JSON");
667
668        // Verify the JSON structure contains expected fields
669        assert!(parsed_witness.get("state").is_some(), "JSON should contain 'state' field");
670        assert!(parsed_witness.get("codes").is_some(), "JSON should contain 'codes' field");
671        assert!(parsed_witness.get("keys").is_some(), "JSON should contain 'keys' field");
672    }
673
674    #[test]
675    fn test_proof_generator_generate() {
676        // Use existing MockEthProvider
677        let mock_provider = MockEthProvider::default();
678        let state_provider: Box<dyn StateProvider> = Box::new(mock_provider);
679
680        // Mock Data
681        let mut codes = BTreeMap::new();
682        codes.insert(B256::from([1u8; 32]), Bytes::from("contract_code_1"));
683        codes.insert(B256::from([2u8; 32]), Bytes::from("contract_code_2"));
684
685        let mut preimages = BTreeMap::new();
686        preimages.insert(B256::from([3u8; 32]), Bytes::from("preimage_1"));
687        preimages.insert(B256::from([4u8; 32]), Bytes::from("preimage_2"));
688
689        let hashed_state = reth_trie::HashedPostState::default();
690
691        // Call generate function
692        let result = generate(codes.clone(), preimages.clone(), hashed_state, state_provider);
693
694        // Verify result
695        assert!(result.is_ok(), "generate function should succeed");
696        let execution_witness = result.unwrap();
697
698        assert!(execution_witness.state.is_empty(), "State should be empty from MockEthProvider");
699
700        let expected_codes: Vec<Bytes> = codes.into_values().collect();
701        assert_eq!(
702            execution_witness.codes.len(),
703            expected_codes.len(),
704            "Codes length should match"
705        );
706        for code in &expected_codes {
707            assert!(
708                execution_witness.codes.contains(code),
709                "Codes should contain expected bytecode"
710            );
711        }
712
713        let expected_keys: Vec<Bytes> = preimages.into_values().collect();
714        assert_eq!(execution_witness.keys.len(), expected_keys.len(), "Keys length should match");
715        for key in &expected_keys {
716            assert!(execution_witness.keys.contains(key), "Keys should contain expected preimage");
717        }
718    }
719
720    #[test]
721    fn test_validate_bundle_state_matching() {
722        let (hook, _output_dir, _temp_dir) = create_test_hook();
723        let bundle_state = create_bundle_state();
724        let block_prefix = "test_block_123";
725
726        // Test with identical states - should not produce any warnings or files
727        let result = hook.validate_bundle_state(&bundle_state, &bundle_state, block_prefix);
728        assert!(result.is_ok());
729    }
730
731    #[test]
732    fn test_validate_bundle_state_mismatch() {
733        let (hook, output_dir, _temp_dir) = create_test_hook();
734        let original_state = create_bundle_state();
735        let mut modified_state = create_bundle_state();
736
737        // Modify the state to create a mismatch
738        let addr = Address::from([1u8; 20]);
739        if let Some(account) = modified_state.state.get_mut(&addr) &&
740            let Some(ref mut info) = account.info
741        {
742            info.balance = U256::from(999);
743        }
744
745        let block_prefix = "test_block_mismatch";
746
747        // Test with different states - should save files and log warning
748        let result = hook.validate_bundle_state(&modified_state, &original_state, block_prefix);
749        assert!(result.is_ok());
750
751        // Verify that files were created
752        let original_file = output_dir.join(format!("{}.bundle_state.original.json", block_prefix));
753        let re_executed_file =
754            output_dir.join(format!("{}.bundle_state.re_executed.json", block_prefix));
755        let diff_file = output_dir.join(format!("{}.bundle_state.diff", block_prefix));
756
757        assert!(original_file.exists(), "Original bundle state file should be created");
758        assert!(re_executed_file.exists(), "Re-executed bundle state file should be created");
759        assert!(diff_file.exists(), "Diff file should be created");
760    }
761
762    /// Creates test `TrieUpdates` with account nodes and removed nodes
763    fn create_test_trie_updates() -> TrieUpdates {
764        use alloy_primitives::map::HashMap;
765        use reth_trie::{updates::TrieUpdates, BranchNodeCompact, Nibbles};
766        use std::collections::HashSet;
767
768        let mut account_nodes = HashMap::default();
769        let nibbles = Nibbles::from_nibbles_unchecked([0x1, 0x2, 0x3]);
770        let branch_node = BranchNodeCompact::new(
771            0b1010,                      // state_mask
772            0b1010,                      // tree_mask - must be subset of state_mask
773            0b1000,                      // hash_mask
774            vec![B256::from([1u8; 32])], // hashes
775            None,                        // root_hash
776        );
777        account_nodes.insert(nibbles, branch_node);
778
779        let mut removed_nodes = HashSet::default();
780        removed_nodes.insert(Nibbles::from_nibbles_unchecked([0x4, 0x5, 0x6]));
781
782        TrieUpdates { account_nodes, removed_nodes, storage_tries: HashMap::default() }
783    }
784
785    #[test]
786    fn test_validate_state_root_and_trie_with_trie_updates() {
787        let (hook, _output_dir, _temp_dir) = create_test_hook();
788        let bundle_state = create_bundle_state();
789
790        // Generate test data
791        let mut rng = generators::rng();
792        let parent_header = generators::random_header(&mut rng, 1, None);
793        let recovered_block = random_block(
794            &mut rng,
795            2,
796            BlockParams {
797                parent: Some(parent_header.hash()),
798                tx_count: Some(0),
799                ..Default::default()
800            },
801        )
802        .try_recover()
803        .unwrap();
804
805        let trie_updates = create_test_trie_updates();
806        let original_root = B256::from([2u8; 32]); // Different from what will be computed
807        let block_prefix = "test_state_root_with_trie";
808
809        // Test with trie updates - this will likely produce warnings due to mock data
810        let result = hook.validate_state_root_and_trie(
811            &parent_header,
812            &recovered_block,
813            &bundle_state,
814            Some((&trie_updates, original_root)),
815            block_prefix,
816        );
817        assert!(result.is_ok());
818    }
819
820    #[test]
821    fn test_on_invalid_block_calls_all_validation_methods() {
822        let (hook, output_dir, _temp_dir) = create_test_hook();
823        let bundle_state = create_bundle_state();
824
825        // Generate test data
826        let mut rng = generators::rng();
827        let parent_header = generators::random_header(&mut rng, 1, None);
828        let recovered_block = random_block(
829            &mut rng,
830            2,
831            BlockParams {
832                parent: Some(parent_header.hash()),
833                tx_count: Some(0),
834                ..Default::default()
835            },
836        )
837        .try_recover()
838        .unwrap();
839
840        // Create mock BlockExecutionOutput
841        let output = BlockExecutionOutput {
842            state: bundle_state,
843            result: reth_provider::BlockExecutionResult {
844                receipts: vec![],
845                requests: Requests::default(),
846                gas_used: 0,
847                blob_gas_used: 0,
848            },
849        };
850
851        // Create test trie updates
852        let trie_updates = create_test_trie_updates();
853        let state_root = B256::random();
854
855        // Test that on_invalid_block attempts to call all its internal methods
856        // by checking that it doesn't panic and tries to create files
857        let files_before = output_dir.read_dir().unwrap().count();
858
859        let _result = hook.on_invalid_block(
860            &parent_header,
861            &recovered_block,
862            &output,
863            Some((&trie_updates, state_root)),
864        );
865
866        // Verify that the function attempted to process the block:
867        // Either it succeeded, or it created some output files during processing
868        let files_after = output_dir.read_dir().unwrap().count();
869
870        // The function should attempt to execute its workflow
871        assert!(
872            files_after >= files_before,
873            "on_invalid_block should attempt to create output files during processing"
874        );
875    }
876
877    #[test]
878    fn test_handle_witness_operations_with_empty_witness() {
879        let (hook, _output_dir, _temp_dir) = create_test_hook();
880        let witness = ExecutionWitness::default();
881        let block_prefix = "empty_witness_test";
882        let block_number = 12345;
883
884        let result = hook.handle_witness_operations(&witness, block_prefix, block_number);
885        assert!(result.is_ok());
886    }
887
888    #[test]
889    fn test_handle_witness_operations_with_zero_block_number() {
890        let (hook, _output_dir, _temp_dir) = create_test_hook();
891        let witness = ExecutionWitness {
892            state: vec![Bytes::from("test_state")],
893            codes: vec![Bytes::from("test_code")],
894            keys: vec![Bytes::from("test_key")],
895            ..Default::default()
896        };
897        let block_prefix = "zero_block_test";
898        let block_number = 0;
899
900        let result = hook.handle_witness_operations(&witness, block_prefix, block_number);
901        assert!(result.is_ok());
902    }
903
904    #[test]
905    fn test_handle_witness_operations_with_large_witness_data() {
906        let (hook, _output_dir, _temp_dir) = create_test_hook();
907        let large_data = vec![0u8; 10000]; // 10KB of data
908        let witness = ExecutionWitness {
909            state: vec![Bytes::from(large_data.clone())],
910            codes: vec![Bytes::from(large_data.clone())],
911            keys: vec![Bytes::from(large_data)],
912            ..Default::default()
913        };
914        let block_prefix = "large_witness_test";
915        let block_number = 999999;
916
917        let result = hook.handle_witness_operations(&witness, block_prefix, block_number);
918        assert!(result.is_ok());
919    }
920
921    #[test]
922    fn test_validate_bundle_state_with_empty_states() {
923        let (hook, _output_dir, _temp_dir) = create_test_hook();
924        let empty_state = BundleState::default();
925        let block_prefix = "empty_states_test";
926
927        let result = hook.validate_bundle_state(&empty_state, &empty_state, block_prefix);
928        assert!(result.is_ok());
929    }
930
931    #[test]
932    fn test_validate_bundle_state_with_different_contract_counts() {
933        let (hook, output_dir, _temp_dir) = create_test_hook();
934        let state1 = create_bundle_state();
935        let mut state2 = create_bundle_state();
936
937        // Add extra contract to state2
938        let extra_contract_hash = B256::random();
939        state2.contracts.insert(
940            extra_contract_hash,
941            Bytecode::new_raw(Bytes::from(vec![0x60, 0x00, 0x60, 0x00, 0xfd])), // REVERT opcode
942        );
943
944        let block_prefix = "different_contracts_test";
945        let result = hook.validate_bundle_state(&state1, &state2, block_prefix);
946        assert!(result.is_ok());
947
948        // Verify diff files were created
949        let diff_file = output_dir.join(format!("{}.bundle_state.diff", block_prefix));
950        assert!(diff_file.exists());
951    }
952
953    #[test]
954    fn test_save_diff_with_identical_values() {
955        let (hook, output_dir, _temp_dir) = create_test_hook();
956        let value1 = "identical_value";
957        let value2 = "identical_value";
958        let filename = "identical_diff_test".to_string();
959
960        let result = hook.save_diff(filename.clone(), &value1, &value2);
961        assert!(result.is_ok());
962
963        let diff_file = output_dir.join(filename);
964        assert!(diff_file.exists());
965    }
966
967    #[test]
968    fn test_validate_state_root_and_trie_without_trie_updates() {
969        let (hook, _output_dir, _temp_dir) = create_test_hook();
970        let bundle_state = create_bundle_state();
971
972        let mut rng = generators::rng();
973        let parent_header = generators::random_header(&mut rng, 1, None);
974        let recovered_block = random_block(
975            &mut rng,
976            2,
977            BlockParams {
978                parent: Some(parent_header.hash()),
979                tx_count: Some(0),
980                ..Default::default()
981            },
982        )
983        .try_recover()
984        .unwrap();
985
986        let block_prefix = "no_trie_updates_test";
987
988        // Test without trie updates (None case)
989        let result = hook.validate_state_root_and_trie(
990            &parent_header,
991            &recovered_block,
992            &bundle_state,
993            None,
994            block_prefix,
995        );
996        assert!(result.is_ok());
997    }
998
999    #[test]
1000    fn test_complete_invalid_block_workflow() {
1001        let (hook, _output_dir, _temp_dir) = create_test_hook();
1002        let mut rng = generators::rng();
1003
1004        // Create a realistic block scenario
1005        let parent_header = generators::random_header(&mut rng, 100, None);
1006        let invalid_block = random_block(
1007            &mut rng,
1008            101,
1009            BlockParams {
1010                parent: Some(parent_header.hash()),
1011                tx_count: Some(3),
1012                ..Default::default()
1013            },
1014        )
1015        .try_recover()
1016        .unwrap();
1017
1018        let bundle_state = create_bundle_state();
1019        let trie_updates = create_test_trie_updates();
1020
1021        // Test validation methods
1022        let validation_result =
1023            hook.validate_bundle_state(&bundle_state, &bundle_state, "integration_test");
1024        assert!(validation_result.is_ok(), "Bundle state validation should succeed");
1025
1026        let state_root_result = hook.validate_state_root_and_trie(
1027            &parent_header,
1028            &invalid_block,
1029            &bundle_state,
1030            Some((&trie_updates, B256::random())),
1031            "integration_test",
1032        );
1033        assert!(state_root_result.is_ok(), "State root validation should succeed");
1034    }
1035
1036    #[test]
1037    fn test_integration_workflow_components() {
1038        let (hook, _output_dir, _temp_dir) = create_test_hook();
1039        let mut rng = generators::rng();
1040
1041        // Create test data
1042        let parent_header = generators::random_header(&mut rng, 50, None);
1043        let _invalid_block = random_block(
1044            &mut rng,
1045            51,
1046            BlockParams {
1047                parent: Some(parent_header.hash()),
1048                tx_count: Some(2),
1049                ..Default::default()
1050            },
1051        )
1052        .try_recover()
1053        .unwrap();
1054
1055        let bundle_state = create_bundle_state();
1056        let _trie_updates = create_test_trie_updates();
1057
1058        // Test individual components that would be part of the complete flow
1059        let validation_result =
1060            hook.validate_bundle_state(&bundle_state, &bundle_state, "integration_component_test");
1061        assert!(validation_result.is_ok(), "Component validation should succeed");
1062    }
1063}