reth_invalid_block_hooks/
witness.rs

1use alloy_consensus::BlockHeader;
2use alloy_primitives::{keccak256, Address, 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, ChainSpecProvider, StateProviderFactory};
9use reth_revm::{database::StateProviderDatabase, db::BundleState, state::AccountInfo};
10use reth_rpc_api::DebugApiClient;
11use reth_tracing::tracing::warn;
12use reth_trie::{updates::TrieUpdates, HashedStorage};
13use revm_bytecode::Bytecode;
14use revm_database::states::{
15    reverts::{AccountInfoRevert, RevertToSlot},
16    AccountStatus, StorageSlot,
17};
18use serde::Serialize;
19use std::{collections::BTreeMap, fmt::Debug, fs::File, io::Write, path::PathBuf};
20
21#[derive(Debug, PartialEq, Eq)]
22struct AccountRevertSorted {
23    pub account: AccountInfoRevert,
24    pub storage: BTreeMap<U256, RevertToSlot>,
25    pub previous_status: AccountStatus,
26    pub wipe_storage: bool,
27}
28
29#[derive(Debug, PartialEq, Eq)]
30struct BundleAccountSorted {
31    pub info: Option<AccountInfo>,
32    pub original_info: Option<AccountInfo>,
33    /// Contains both original and present state.
34    /// When extracting changeset we compare if original value is different from present value.
35    /// If it is different we add it to changeset.
36    /// If Account was destroyed we ignore original value and compare present state with
37    /// `U256::ZERO`.
38    pub storage: BTreeMap<U256, StorageSlot>,
39    /// Account status.
40    pub status: AccountStatus,
41}
42
43#[derive(Debug, PartialEq, Eq)]
44struct BundleStateSorted {
45    /// Account state
46    pub state: BTreeMap<Address, BundleAccountSorted>,
47    /// All created contracts in this block.
48    pub contracts: BTreeMap<B256, Bytecode>,
49    /// Changes to revert
50    ///
51    /// **Note**: Inside vector is *not* sorted by address.
52    ///
53    /// But it is unique by address.
54    pub reverts: Vec<Vec<(Address, AccountRevertSorted)>>,
55    /// The size of the plain state in the bundle state
56    pub state_size: usize,
57    /// The size of reverts in the bundle state
58    pub reverts_size: usize,
59}
60
61impl BundleStateSorted {
62    fn from_bundle_state(bundle_state: &BundleState) -> Self {
63        let state = bundle_state
64            .state
65            .clone()
66            .into_iter()
67            .map(|(address, account)| {
68                (
69                    address,
70                    BundleAccountSorted {
71                        info: account.info,
72                        original_info: account.original_info,
73                        status: account.status,
74                        storage: BTreeMap::from_iter(account.storage),
75                    },
76                )
77            })
78            .collect();
79
80        let contracts = BTreeMap::from_iter(bundle_state.contracts.clone());
81
82        let reverts = bundle_state
83            .reverts
84            .iter()
85            .map(|block| {
86                block
87                    .iter()
88                    .map(|(address, account_revert)| {
89                        (
90                            *address,
91                            AccountRevertSorted {
92                                account: account_revert.account.clone(),
93                                previous_status: account_revert.previous_status,
94                                wipe_storage: account_revert.wipe_storage,
95                                storage: BTreeMap::from_iter(account_revert.storage.clone()),
96                            },
97                        )
98                    })
99                    .collect()
100            })
101            .collect();
102
103        let state_size = bundle_state.state_size;
104        let reverts_size = bundle_state.reverts_size;
105
106        Self { state, contracts, reverts, state_size, reverts_size }
107    }
108}
109
110/// Generates a witness for the given block and saves it to a file.
111#[derive(Debug)]
112pub struct InvalidBlockWitnessHook<P, E> {
113    /// The provider to read the historical state and do the EVM execution.
114    provider: P,
115    /// The EVM configuration to use for the execution.
116    evm_config: E,
117    /// The directory to write the witness to. Additionally, diff files will be written to this
118    /// directory in case of failed sanity checks.
119    output_directory: PathBuf,
120    /// The healthy node client to compare the witness against.
121    healthy_node_client: Option<jsonrpsee::http_client::HttpClient>,
122}
123
124impl<P, E> InvalidBlockWitnessHook<P, E> {
125    /// Creates a new witness hook.
126    pub const fn new(
127        provider: P,
128        evm_config: E,
129        output_directory: PathBuf,
130        healthy_node_client: Option<jsonrpsee::http_client::HttpClient>,
131    ) -> Self {
132        Self { provider, evm_config, output_directory, healthy_node_client }
133    }
134}
135
136impl<P, E, N> InvalidBlockWitnessHook<P, E>
137where
138    P: StateProviderFactory + ChainSpecProvider + Send + Sync + 'static,
139    E: ConfigureEvm<Primitives = N> + 'static,
140    N: NodePrimitives,
141{
142    fn on_invalid_block(
143        &self,
144        parent_header: &SealedHeader<N::BlockHeader>,
145        block: &RecoveredBlock<N::Block>,
146        output: &BlockExecutionOutput<N::Receipt>,
147        trie_updates: Option<(&TrieUpdates, B256)>,
148    ) -> eyre::Result<()>
149    where
150        N: NodePrimitives,
151    {
152        // TODO(alexey): unify with `DebugApi::debug_execution_witness`
153
154        let mut executor = self.evm_config.batch_executor(StateProviderDatabase::new(
155            self.provider.state_by_block_hash(parent_header.hash())?,
156        ));
157
158        executor.execute_one(block)?;
159
160        // Take the bundle state
161        let mut db = executor.into_state();
162        let bundle_state = db.take_bundle();
163
164        // Initialize a map of preimages.
165        let mut state_preimages = Vec::default();
166
167        // Get codes
168        let codes = db
169            .cache
170            .contracts
171            .values()
172            .map(|code| code.original_bytes())
173            .chain(
174                // cache state does not have all the contracts, especially when
175                // a contract is created within the block
176                // the contract only exists in bundle state, therefore we need
177                // to include them as well
178                bundle_state.contracts.values().map(|code| code.original_bytes()),
179            )
180            .collect();
181
182        // Grab all account proofs for the data accessed during block execution.
183        //
184        // Note: We grab *all* accounts in the cache here, as the `BundleState` prunes
185        // referenced accounts + storage slots.
186        let mut hashed_state = db.database.hashed_post_state(&bundle_state);
187        for (address, account) in db.cache.accounts {
188            let hashed_address = keccak256(address);
189            hashed_state
190                .accounts
191                .insert(hashed_address, account.account.as_ref().map(|a| a.info.clone().into()));
192
193            let storage = hashed_state
194                .storages
195                .entry(hashed_address)
196                .or_insert_with(|| HashedStorage::new(account.status.was_destroyed()));
197
198            if let Some(account) = account.account {
199                state_preimages.push(alloy_rlp::encode(address).into());
200
201                for (slot, value) in account.storage {
202                    let slot = B256::from(slot);
203                    let hashed_slot = keccak256(slot);
204                    storage.storage.insert(hashed_slot, value);
205
206                    state_preimages.push(alloy_rlp::encode(slot).into());
207                }
208            }
209        }
210
211        // Generate an execution witness for the aggregated state of accessed accounts.
212        // Destruct the cache database to retrieve the state provider.
213        let state_provider = db.database.into_inner();
214        let state = state_provider.witness(Default::default(), hashed_state.clone())?;
215
216        // Write the witness to the output directory.
217        let response =
218            ExecutionWitness { state, codes, keys: state_preimages, ..Default::default() };
219        let re_executed_witness_path = self.save_file(
220            format!("{}_{}.witness.re_executed.json", block.number(), block.hash()),
221            &response,
222        )?;
223        if let Some(healthy_node_client) = &self.healthy_node_client {
224            // Compare the witness against the healthy node.
225            let healthy_node_witness = futures::executor::block_on(async move {
226                DebugApiClient::<()>::debug_execution_witness(
227                    healthy_node_client,
228                    block.number().into(),
229                )
230                .await
231            })?;
232
233            let healthy_path = self.save_file(
234                format!("{}_{}.witness.healthy.json", block.number(), block.hash()),
235                &healthy_node_witness,
236            )?;
237
238            // If the witnesses are different, write the diff to the output directory.
239            if response != healthy_node_witness {
240                let filename = format!("{}_{}.witness.diff", block.number(), block.hash());
241                let diff_path = self.save_diff(filename, &response, &healthy_node_witness)?;
242                warn!(
243                    target: "engine::invalid_block_hooks::witness",
244                    diff_path = %diff_path.display(),
245                    re_executed_path = %re_executed_witness_path.display(),
246                    healthy_path = %healthy_path.display(),
247                    "Witness mismatch against healthy node"
248                );
249            }
250        }
251
252        // The bundle state after re-execution should match the original one.
253        //
254        // Reverts now supports order-independent equality, so we can compare directly without
255        // sorting the reverts vectors.
256        //
257        // See: https://github.com/bluealloy/revm/pull/1827
258        if bundle_state != output.state {
259            let original_path = self.save_file(
260                format!("{}_{}.bundle_state.original.json", block.number(), block.hash()),
261                &output.state,
262            )?;
263            let re_executed_path = self.save_file(
264                format!("{}_{}.bundle_state.re_executed.json", block.number(), block.hash()),
265                &bundle_state,
266            )?;
267
268            let filename = format!("{}_{}.bundle_state.diff", block.number(), block.hash());
269            // Convert bundle state to sorted struct which has BTreeMap instead of HashMap to
270            // have deterministic ordering
271            let bundle_state_sorted = BundleStateSorted::from_bundle_state(&bundle_state);
272            let output_state_sorted = BundleStateSorted::from_bundle_state(&output.state);
273
274            let diff_path = self.save_diff(filename, &bundle_state_sorted, &output_state_sorted)?;
275
276            warn!(
277                target: "engine::invalid_block_hooks::witness",
278                diff_path = %diff_path.display(),
279                original_path = %original_path.display(),
280                re_executed_path = %re_executed_path.display(),
281                "Bundle state mismatch after re-execution"
282            );
283        }
284
285        // Calculate the state root and trie updates after re-execution. They should match
286        // the original ones.
287        let (re_executed_root, trie_output) =
288            state_provider.state_root_with_updates(hashed_state)?;
289        if let Some((original_updates, original_root)) = trie_updates {
290            if re_executed_root != original_root {
291                let filename = format!("{}_{}.state_root.diff", block.number(), block.hash());
292                let diff_path = self.save_diff(filename, &re_executed_root, &original_root)?;
293                warn!(target: "engine::invalid_block_hooks::witness", ?original_root, ?re_executed_root, diff_path = %diff_path.display(), "State root mismatch after re-execution");
294            }
295
296            // If the re-executed state root does not match the _header_ state root, also log that.
297            if re_executed_root != block.state_root() {
298                let filename =
299                    format!("{}_{}.header_state_root.diff", block.number(), block.hash());
300                let diff_path = self.save_diff(filename, &re_executed_root, &block.state_root())?;
301                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");
302            }
303
304            if &trie_output != original_updates {
305                // Trie updates are too big to diff, so we just save the original and re-executed
306                let trie_output_sorted = &trie_output.into_sorted_ref();
307                let original_updates_sorted = &original_updates.into_sorted_ref();
308                let original_path = self.save_file(
309                    format!("{}_{}.trie_updates.original.json", block.number(), block.hash()),
310                    original_updates_sorted,
311                )?;
312                let re_executed_path = self.save_file(
313                    format!("{}_{}.trie_updates.re_executed.json", block.number(), block.hash()),
314                    trie_output_sorted,
315                )?;
316                warn!(
317                    target: "engine::invalid_block_hooks::witness",
318                    original_path = %original_path.display(),
319                    re_executed_path = %re_executed_path.display(),
320                    "Trie updates mismatch after re-execution"
321                );
322            }
323        }
324
325        Ok(())
326    }
327
328    /// Saves the diff of two values into a file with the given name in the output directory.
329    fn save_diff<T: PartialEq + Debug>(
330        &self,
331        filename: String,
332        original: &T,
333        new: &T,
334    ) -> eyre::Result<PathBuf> {
335        let path = self.output_directory.join(filename);
336        let diff = Comparison::new(original, new);
337        File::create(&path)?.write_all(diff.to_string().as_bytes())?;
338
339        Ok(path)
340    }
341
342    fn save_file<T: Serialize>(&self, filename: String, value: &T) -> eyre::Result<PathBuf> {
343        let path = self.output_directory.join(filename);
344        File::create(&path)?.write_all(serde_json::to_string(value)?.as_bytes())?;
345
346        Ok(path)
347    }
348}
349
350impl<P, E, N: NodePrimitives> InvalidBlockHook<N> for InvalidBlockWitnessHook<P, E>
351where
352    P: StateProviderFactory + ChainSpecProvider + Send + Sync + 'static,
353    E: ConfigureEvm<Primitives = N> + 'static,
354{
355    fn on_invalid_block(
356        &self,
357        parent_header: &SealedHeader<N::BlockHeader>,
358        block: &RecoveredBlock<N::Block>,
359        output: &BlockExecutionOutput<N::Receipt>,
360        trie_updates: Option<(&TrieUpdates, B256)>,
361    ) {
362        if let Err(err) = self.on_invalid_block(parent_header, block, output, trie_updates) {
363            warn!(target: "engine::invalid_block_hooks::witness", %err, "Failed to invoke hook");
364        }
365    }
366}