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