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(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, &output_state_sorted, &bundle_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, &original_root, &re_executed_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, &block.state_root(), &re_executed_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(Box::new(state_provider) as StateProviderBox))
534            .with_bundle_update()
535            .build();
536
537        // Insert contracts from the fixture into the state cache
538        for (code_hash, bytecode) in &bundle_state.contracts {
539            state.cache.contracts.insert(*code_hash, bytecode.clone());
540        }
541
542        // Manually set the bundle state in the state object
543        state.bundle_state = bundle_state;
544
545        // Call the collect function
546        let result = collect_execution_data(state);
547        // Verify the function returns successfully
548        assert!(result.is_ok());
549
550        let (codes, _preimages, _hashed_state, returned_bundle_state) = result.unwrap();
551
552        // Verify that the returned data contains expected values
553        // Since we used the fixture data, we should have some codes and state
554        assert!(!codes.is_empty(), "Expected some bytecode entries");
555        assert!(!returned_bundle_state.state.is_empty(), "Expected some state entries");
556
557        // Verify the bundle state structure matches our fixture
558        assert_eq!(returned_bundle_state.state.len(), 3, "Expected 3 accounts from fixture");
559        assert_eq!(returned_bundle_state.contracts.len(), 3, "Expected 3 contracts from fixture");
560    }
561
562    #[test]
563    fn test_re_execute_block() {
564        // Create hook instance
565        let (hook, _output_directory, _temp_dir) = create_test_hook();
566
567        // Setup to call re_execute_block
568        let mut rng = generators::rng();
569        let parent_header = generators::random_header(&mut rng, 1, None);
570
571        // Create a random block that inherits from the parent header
572        let recovered_block = random_block(
573            &mut rng,
574            2, // block number
575            BlockParams {
576                parent: Some(parent_header.hash()),
577                tx_count: Some(0),
578                ..Default::default()
579            },
580        )
581        .try_recover()
582        .unwrap();
583
584        let result = hook.re_execute_block(&parent_header, &recovered_block);
585
586        // Verify the function behavior with mock data
587        assert!(result.is_ok(), "re_execute_block should return Ok");
588    }
589
590    /// Creates test `InvalidBlockWitnessHook` with temporary directory
591    fn create_test_hook() -> (
592        InvalidBlockWitnessHook<MockEthProvider<EthPrimitives, ChainSpec>, EthEvmConfig>,
593        PathBuf,
594        TempDir,
595    ) {
596        let temp_dir = TempDir::new().expect("Failed to create temp dir");
597        let output_directory = temp_dir.path().to_path_buf();
598
599        let provider = MockEthProvider::<EthPrimitives, ChainSpec>::default();
600        let evm_config = EthEvmConfig::mainnet();
601
602        let hook =
603            InvalidBlockWitnessHook::new(provider, evm_config, output_directory.clone(), None);
604
605        (hook, output_directory, temp_dir)
606    }
607
608    #[test]
609    fn test_handle_witness_operations_with_healthy_client_mock() {
610        // Create hook instance with mock healthy client
611        let (hook, output_directory, _temp_dir) = create_test_hook();
612
613        // Create sample ExecutionWitness with correct types
614        let witness = ExecutionWitness {
615            state: vec![Bytes::from("state_data")],
616            codes: vec![Bytes::from("code_data")],
617            keys: vec![Bytes::from("key_data")],
618            ..Default::default()
619        };
620
621        // Call handle_witness_operations
622        let result = hook.handle_witness_operations(&witness, "test_block_healthy", 67890);
623
624        // Should succeed
625        assert!(result.is_ok());
626
627        // Check that witness file was created
628        let witness_file = output_directory.join("test_block_healthy.witness.re_executed.json");
629        assert!(witness_file.exists());
630    }
631
632    #[test]
633    fn test_handle_witness_operations_file_creation() {
634        // Test file creation and content validation
635        let (hook, output_directory, _temp_dir) = create_test_hook();
636
637        let witness = ExecutionWitness {
638            state: vec![Bytes::from("test_state")],
639            codes: vec![Bytes::from("test_code")],
640            keys: vec![Bytes::from("test_key")],
641            ..Default::default()
642        };
643
644        let block_prefix = "file_test_block";
645        let block_number = 11111;
646
647        // Call handle_witness_operations
648        let result = hook.handle_witness_operations(&witness, block_prefix, block_number);
649        assert!(result.is_ok());
650
651        // Verify file was created with correct name
652        let expected_file =
653            output_directory.join(format!("{}.witness.re_executed.json", block_prefix));
654        assert!(expected_file.exists());
655
656        // Read and verify file content is valid JSON and contains witness structure
657        let file_content = std::fs::read_to_string(&expected_file).expect("Failed to read file");
658        let parsed_witness: serde_json::Value =
659            serde_json::from_str(&file_content).expect("File should contain valid JSON");
660
661        // Verify the JSON structure contains expected fields
662        assert!(parsed_witness.get("state").is_some(), "JSON should contain 'state' field");
663        assert!(parsed_witness.get("codes").is_some(), "JSON should contain 'codes' field");
664        assert!(parsed_witness.get("keys").is_some(), "JSON should contain 'keys' field");
665    }
666
667    #[test]
668    fn test_proof_generator_generate() {
669        // Use existing MockEthProvider
670        let mock_provider = MockEthProvider::default();
671        let state_provider: Box<dyn StateProvider> = Box::new(mock_provider);
672
673        // Mock Data
674        let mut codes = BTreeMap::new();
675        codes.insert(B256::from([1u8; 32]), Bytes::from("contract_code_1"));
676        codes.insert(B256::from([2u8; 32]), Bytes::from("contract_code_2"));
677
678        let mut preimages = BTreeMap::new();
679        preimages.insert(B256::from([3u8; 32]), Bytes::from("preimage_1"));
680        preimages.insert(B256::from([4u8; 32]), Bytes::from("preimage_2"));
681
682        let hashed_state = reth_trie::HashedPostState::default();
683
684        // Call generate function
685        let result = generate(codes.clone(), preimages.clone(), hashed_state, state_provider);
686
687        // Verify result
688        assert!(result.is_ok(), "generate function should succeed");
689        let execution_witness = result.unwrap();
690
691        assert!(execution_witness.state.is_empty(), "State should be empty from MockEthProvider");
692
693        let expected_codes: Vec<Bytes> = codes.into_values().collect();
694        assert_eq!(
695            execution_witness.codes.len(),
696            expected_codes.len(),
697            "Codes length should match"
698        );
699        for code in &expected_codes {
700            assert!(
701                execution_witness.codes.contains(code),
702                "Codes should contain expected bytecode"
703            );
704        }
705
706        let expected_keys: Vec<Bytes> = preimages.into_values().collect();
707        assert_eq!(execution_witness.keys.len(), expected_keys.len(), "Keys length should match");
708        for key in &expected_keys {
709            assert!(execution_witness.keys.contains(key), "Keys should contain expected preimage");
710        }
711    }
712
713    #[test]
714    fn test_validate_bundle_state_matching() {
715        let (hook, _output_dir, _temp_dir) = create_test_hook();
716        let bundle_state = create_bundle_state();
717        let block_prefix = "test_block_123";
718
719        // Test with identical states - should not produce any warnings or files
720        let result = hook.validate_bundle_state(&bundle_state, &bundle_state, block_prefix);
721        assert!(result.is_ok());
722    }
723
724    #[test]
725    fn test_validate_bundle_state_mismatch() {
726        let (hook, output_dir, _temp_dir) = create_test_hook();
727        let original_state = create_bundle_state();
728        let mut modified_state = create_bundle_state();
729
730        // Modify the state to create a mismatch
731        let addr = Address::from([1u8; 20]);
732        if let Some(account) = modified_state.state.get_mut(&addr) &&
733            let Some(ref mut info) = account.info
734        {
735            info.balance = U256::from(999);
736        }
737
738        let block_prefix = "test_block_mismatch";
739
740        // Test with different states - should save files and log warning
741        let result = hook.validate_bundle_state(&modified_state, &original_state, block_prefix);
742        assert!(result.is_ok());
743
744        // Verify that files were created
745        let original_file = output_dir.join(format!("{}.bundle_state.original.json", block_prefix));
746        let re_executed_file =
747            output_dir.join(format!("{}.bundle_state.re_executed.json", block_prefix));
748        let diff_file = output_dir.join(format!("{}.bundle_state.diff", block_prefix));
749
750        assert!(original_file.exists(), "Original bundle state file should be created");
751        assert!(re_executed_file.exists(), "Re-executed bundle state file should be created");
752        assert!(diff_file.exists(), "Diff file should be created");
753    }
754
755    /// Creates test `TrieUpdates` with account nodes and removed nodes
756    fn create_test_trie_updates() -> TrieUpdates {
757        use alloy_primitives::map::HashMap;
758        use reth_trie::{updates::TrieUpdates, BranchNodeCompact, Nibbles};
759        use std::collections::HashSet;
760
761        let mut account_nodes = HashMap::default();
762        let nibbles = Nibbles::from_nibbles_unchecked([0x1, 0x2, 0x3]);
763        let branch_node = BranchNodeCompact::new(
764            0b1010,                      // state_mask
765            0b1010,                      // tree_mask - must be subset of state_mask
766            0b1000,                      // hash_mask
767            vec![B256::from([1u8; 32])], // hashes
768            None,                        // root_hash
769        );
770        account_nodes.insert(nibbles, branch_node);
771
772        let mut removed_nodes = HashSet::default();
773        removed_nodes.insert(Nibbles::from_nibbles_unchecked([0x4, 0x5, 0x6]));
774
775        TrieUpdates { account_nodes, removed_nodes, storage_tries: HashMap::default() }
776    }
777
778    #[test]
779    fn test_validate_state_root_and_trie_with_trie_updates() {
780        let (hook, _output_dir, _temp_dir) = create_test_hook();
781        let bundle_state = create_bundle_state();
782
783        // Generate test data
784        let mut rng = generators::rng();
785        let parent_header = generators::random_header(&mut rng, 1, None);
786        let recovered_block = random_block(
787            &mut rng,
788            2,
789            BlockParams {
790                parent: Some(parent_header.hash()),
791                tx_count: Some(0),
792                ..Default::default()
793            },
794        )
795        .try_recover()
796        .unwrap();
797
798        let trie_updates = create_test_trie_updates();
799        let original_root = B256::from([2u8; 32]); // Different from what will be computed
800        let block_prefix = "test_state_root_with_trie";
801
802        // Test with trie updates - this will likely produce warnings due to mock data
803        let result = hook.validate_state_root_and_trie(
804            &parent_header,
805            &recovered_block,
806            &bundle_state,
807            Some((&trie_updates, original_root)),
808            block_prefix,
809        );
810        assert!(result.is_ok());
811    }
812
813    #[test]
814    fn test_on_invalid_block_calls_all_validation_methods() {
815        let (hook, output_dir, _temp_dir) = create_test_hook();
816        let bundle_state = create_bundle_state();
817
818        // Generate test data
819        let mut rng = generators::rng();
820        let parent_header = generators::random_header(&mut rng, 1, None);
821        let recovered_block = random_block(
822            &mut rng,
823            2,
824            BlockParams {
825                parent: Some(parent_header.hash()),
826                tx_count: Some(0),
827                ..Default::default()
828            },
829        )
830        .try_recover()
831        .unwrap();
832
833        // Create mock BlockExecutionOutput
834        let output = BlockExecutionOutput {
835            state: bundle_state,
836            result: reth_provider::BlockExecutionResult {
837                receipts: vec![],
838                requests: Requests::default(),
839                gas_used: 0,
840                blob_gas_used: 0,
841            },
842        };
843
844        // Create test trie updates
845        let trie_updates = create_test_trie_updates();
846        let state_root = B256::random();
847
848        // Test that on_invalid_block attempts to call all its internal methods
849        // by checking that it doesn't panic and tries to create files
850        let files_before = output_dir.read_dir().unwrap().count();
851
852        let _result = hook.on_invalid_block(
853            &parent_header,
854            &recovered_block,
855            &output,
856            Some((&trie_updates, state_root)),
857        );
858
859        // Verify that the function attempted to process the block:
860        // Either it succeeded, or it created some output files during processing
861        let files_after = output_dir.read_dir().unwrap().count();
862
863        // The function should attempt to execute its workflow
864        assert!(
865            files_after >= files_before,
866            "on_invalid_block should attempt to create output files during processing"
867        );
868    }
869
870    #[test]
871    fn test_handle_witness_operations_with_empty_witness() {
872        let (hook, _output_dir, _temp_dir) = create_test_hook();
873        let witness = ExecutionWitness::default();
874        let block_prefix = "empty_witness_test";
875        let block_number = 12345;
876
877        let result = hook.handle_witness_operations(&witness, block_prefix, block_number);
878        assert!(result.is_ok());
879    }
880
881    #[test]
882    fn test_handle_witness_operations_with_zero_block_number() {
883        let (hook, _output_dir, _temp_dir) = create_test_hook();
884        let witness = ExecutionWitness {
885            state: vec![Bytes::from("test_state")],
886            codes: vec![Bytes::from("test_code")],
887            keys: vec![Bytes::from("test_key")],
888            ..Default::default()
889        };
890        let block_prefix = "zero_block_test";
891        let block_number = 0;
892
893        let result = hook.handle_witness_operations(&witness, block_prefix, block_number);
894        assert!(result.is_ok());
895    }
896
897    #[test]
898    fn test_handle_witness_operations_with_large_witness_data() {
899        let (hook, _output_dir, _temp_dir) = create_test_hook();
900        let large_data = vec![0u8; 10000]; // 10KB of data
901        let witness = ExecutionWitness {
902            state: vec![Bytes::from(large_data.clone())],
903            codes: vec![Bytes::from(large_data.clone())],
904            keys: vec![Bytes::from(large_data)],
905            ..Default::default()
906        };
907        let block_prefix = "large_witness_test";
908        let block_number = 999999;
909
910        let result = hook.handle_witness_operations(&witness, block_prefix, block_number);
911        assert!(result.is_ok());
912    }
913
914    #[test]
915    fn test_validate_bundle_state_with_empty_states() {
916        let (hook, _output_dir, _temp_dir) = create_test_hook();
917        let empty_state = BundleState::default();
918        let block_prefix = "empty_states_test";
919
920        let result = hook.validate_bundle_state(&empty_state, &empty_state, block_prefix);
921        assert!(result.is_ok());
922    }
923
924    #[test]
925    fn test_validate_bundle_state_with_different_contract_counts() {
926        let (hook, output_dir, _temp_dir) = create_test_hook();
927        let state1 = create_bundle_state();
928        let mut state2 = create_bundle_state();
929
930        // Add extra contract to state2
931        let extra_contract_hash = B256::random();
932        state2.contracts.insert(
933            extra_contract_hash,
934            Bytecode::new_raw(Bytes::from(vec![0x60, 0x00, 0x60, 0x00, 0xfd])), // REVERT opcode
935        );
936
937        let block_prefix = "different_contracts_test";
938        let result = hook.validate_bundle_state(&state1, &state2, block_prefix);
939        assert!(result.is_ok());
940
941        // Verify diff files were created
942        let diff_file = output_dir.join(format!("{}.bundle_state.diff", block_prefix));
943        assert!(diff_file.exists());
944    }
945
946    #[test]
947    fn test_save_diff_with_identical_values() {
948        let (hook, output_dir, _temp_dir) = create_test_hook();
949        let value1 = "identical_value";
950        let value2 = "identical_value";
951        let filename = "identical_diff_test".to_string();
952
953        let result = hook.save_diff(filename.clone(), &value1, &value2);
954        assert!(result.is_ok());
955
956        let diff_file = output_dir.join(filename);
957        assert!(diff_file.exists());
958    }
959
960    #[test]
961    fn test_validate_state_root_and_trie_without_trie_updates() {
962        let (hook, _output_dir, _temp_dir) = create_test_hook();
963        let bundle_state = create_bundle_state();
964
965        let mut rng = generators::rng();
966        let parent_header = generators::random_header(&mut rng, 1, None);
967        let recovered_block = random_block(
968            &mut rng,
969            2,
970            BlockParams {
971                parent: Some(parent_header.hash()),
972                tx_count: Some(0),
973                ..Default::default()
974            },
975        )
976        .try_recover()
977        .unwrap();
978
979        let block_prefix = "no_trie_updates_test";
980
981        // Test without trie updates (None case)
982        let result = hook.validate_state_root_and_trie(
983            &parent_header,
984            &recovered_block,
985            &bundle_state,
986            None,
987            block_prefix,
988        );
989        assert!(result.is_ok());
990    }
991
992    #[test]
993    fn test_complete_invalid_block_workflow() {
994        let (hook, _output_dir, _temp_dir) = create_test_hook();
995        let mut rng = generators::rng();
996
997        // Create a realistic block scenario
998        let parent_header = generators::random_header(&mut rng, 100, None);
999        let invalid_block = random_block(
1000            &mut rng,
1001            101,
1002            BlockParams {
1003                parent: Some(parent_header.hash()),
1004                tx_count: Some(3),
1005                ..Default::default()
1006            },
1007        )
1008        .try_recover()
1009        .unwrap();
1010
1011        let bundle_state = create_bundle_state();
1012        let trie_updates = create_test_trie_updates();
1013
1014        // Test validation methods
1015        let validation_result =
1016            hook.validate_bundle_state(&bundle_state, &bundle_state, "integration_test");
1017        assert!(validation_result.is_ok(), "Bundle state validation should succeed");
1018
1019        let state_root_result = hook.validate_state_root_and_trie(
1020            &parent_header,
1021            &invalid_block,
1022            &bundle_state,
1023            Some((&trie_updates, B256::random())),
1024            "integration_test",
1025        );
1026        assert!(state_root_result.is_ok(), "State root validation should succeed");
1027    }
1028
1029    #[test]
1030    fn test_integration_workflow_components() {
1031        let (hook, _output_dir, _temp_dir) = create_test_hook();
1032        let mut rng = generators::rng();
1033
1034        // Create test data
1035        let parent_header = generators::random_header(&mut rng, 50, None);
1036        let _invalid_block = random_block(
1037            &mut rng,
1038            51,
1039            BlockParams {
1040                parent: Some(parent_header.hash()),
1041                tx_count: Some(2),
1042                ..Default::default()
1043            },
1044        )
1045        .try_recover()
1046        .unwrap();
1047
1048        let bundle_state = create_bundle_state();
1049        let _trie_updates = create_test_trie_updates();
1050
1051        // Test individual components that would be part of the complete flow
1052        let validation_result =
1053            hook.validate_bundle_state(&bundle_state, &bundle_state, "integration_component_test");
1054        assert!(validation_result.is_ok(), "Component validation should succeed");
1055    }
1056}