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