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