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, 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 + 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        // TODO(alexey): unify with `DebugApi::debug_execution_witness`
150
151        let mut executor = self.evm_config.batch_executor(StateProviderDatabase::new(
152            self.provider.state_by_block_hash(parent_header.hash())?,
153        ));
154
155        executor.execute_one(block)?;
156
157        // Take the bundle state
158        let mut db = executor.into_state();
159        let bundle_state = db.take_bundle();
160
161        // Initialize a map of preimages.
162        let mut state_preimages = Vec::default();
163
164        // Get codes
165        let codes = db
166            .cache
167            .contracts
168            .values()
169            .map(|code| code.original_bytes())
170            .chain(
171                // cache state does not have all the contracts, especially when
172                // a contract is created within the block
173                // the contract only exists in bundle state, therefore we need
174                // to include them as well
175                bundle_state.contracts.values().map(|code| code.original_bytes()),
176            )
177            .collect();
178
179        // Grab all account proofs for the data accessed during block execution.
180        //
181        // Note: We grab *all* accounts in the cache here, as the `BundleState` prunes
182        // referenced accounts + storage slots.
183        let mut hashed_state = db.database.hashed_post_state(&bundle_state);
184        for (address, account) in db.cache.accounts {
185            let hashed_address = keccak256(address);
186            hashed_state
187                .accounts
188                .insert(hashed_address, account.account.as_ref().map(|a| a.info.clone().into()));
189
190            let storage = hashed_state
191                .storages
192                .entry(hashed_address)
193                .or_insert_with(|| HashedStorage::new(account.status.was_destroyed()));
194
195            if let Some(account) = account.account {
196                state_preimages.push(alloy_rlp::encode(address).into());
197
198                for (slot, value) in account.storage {
199                    let slot = B256::from(slot);
200                    let hashed_slot = keccak256(slot);
201                    storage.storage.insert(hashed_slot, value);
202
203                    state_preimages.push(alloy_rlp::encode(slot).into());
204                }
205            }
206        }
207
208        // Generate an execution witness for the aggregated state of accessed accounts.
209        // Destruct the cache database to retrieve the state provider.
210        let state_provider = db.database.into_inner();
211        let state = state_provider.witness(Default::default(), hashed_state.clone())?;
212
213        // Write the witness to the output directory.
214        let response =
215            ExecutionWitness { state, codes, keys: state_preimages, ..Default::default() };
216        let re_executed_witness_path = self.save_file(
217            format!("{}_{}.witness.re_executed.json", block.number(), block.hash()),
218            &response,
219        )?;
220        if let Some(healthy_node_client) = &self.healthy_node_client {
221            // Compare the witness against the healthy node.
222            let healthy_node_witness = futures::executor::block_on(async move {
223                DebugApiClient::<()>::debug_execution_witness(
224                    healthy_node_client,
225                    block.number().into(),
226                )
227                .await
228            })?;
229
230            let healthy_path = self.save_file(
231                format!("{}_{}.witness.healthy.json", block.number(), block.hash()),
232                &healthy_node_witness,
233            )?;
234
235            // If the witnesses are different, write the diff to the output directory.
236            if response != healthy_node_witness {
237                let filename = format!("{}_{}.witness.diff", block.number(), block.hash());
238                let diff_path = self.save_diff(filename, &response, &healthy_node_witness)?;
239                warn!(
240                    target: "engine::invalid_block_hooks::witness",
241                    diff_path = %diff_path.display(),
242                    re_executed_path = %re_executed_witness_path.display(),
243                    healthy_path = %healthy_path.display(),
244                    "Witness mismatch against healthy node"
245                );
246            }
247        }
248
249        // The bundle state after re-execution should match the original one.
250        //
251        // Reverts now supports order-independent equality, so we can compare directly without
252        // sorting the reverts vectors.
253        //
254        // See: https://github.com/bluealloy/revm/pull/1827
255        if bundle_state != output.state {
256            let original_path = self.save_file(
257                format!("{}_{}.bundle_state.original.json", block.number(), block.hash()),
258                &output.state,
259            )?;
260            let re_executed_path = self.save_file(
261                format!("{}_{}.bundle_state.re_executed.json", block.number(), block.hash()),
262                &bundle_state,
263            )?;
264
265            let filename = format!("{}_{}.bundle_state.diff", block.number(), block.hash());
266            // Convert bundle state to sorted struct which has BTreeMap instead of HashMap to
267            // have deterministic ordering
268            let bundle_state_sorted = BundleStateSorted::from_bundle_state(&bundle_state);
269            let output_state_sorted = BundleStateSorted::from_bundle_state(&output.state);
270
271            let diff_path = self.save_diff(filename, &bundle_state_sorted, &output_state_sorted)?;
272
273            warn!(
274                target: "engine::invalid_block_hooks::witness",
275                diff_path = %diff_path.display(),
276                original_path = %original_path.display(),
277                re_executed_path = %re_executed_path.display(),
278                "Bundle state mismatch after re-execution"
279            );
280        }
281
282        // Calculate the state root and trie updates after re-execution. They should match
283        // the original ones.
284        let (re_executed_root, trie_output) =
285            state_provider.state_root_with_updates(hashed_state)?;
286        if let Some((original_updates, original_root)) = trie_updates {
287            if re_executed_root != original_root {
288                let filename = format!("{}_{}.state_root.diff", block.number(), block.hash());
289                let diff_path = self.save_diff(filename, &re_executed_root, &original_root)?;
290                warn!(target: "engine::invalid_block_hooks::witness", ?original_root, ?re_executed_root, diff_path = %diff_path.display(), "State root mismatch after re-execution");
291            }
292
293            // If the re-executed state root does not match the _header_ state root, also log that.
294            if re_executed_root != block.state_root() {
295                let filename =
296                    format!("{}_{}.header_state_root.diff", block.number(), block.hash());
297                let diff_path = self.save_diff(filename, &re_executed_root, &block.state_root())?;
298                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");
299            }
300
301            if &trie_output != original_updates {
302                // Trie updates are too big to diff, so we just save the original and re-executed
303                let trie_output_sorted = &trie_output.into_sorted_ref();
304                let original_updates_sorted = &original_updates.into_sorted_ref();
305                let original_path = self.save_file(
306                    format!("{}_{}.trie_updates.original.json", block.number(), block.hash()),
307                    original_updates_sorted,
308                )?;
309                let re_executed_path = self.save_file(
310                    format!("{}_{}.trie_updates.re_executed.json", block.number(), block.hash()),
311                    trie_output_sorted,
312                )?;
313                warn!(
314                    target: "engine::invalid_block_hooks::witness",
315                    original_path = %original_path.display(),
316                    re_executed_path = %re_executed_path.display(),
317                    "Trie updates mismatch after re-execution"
318                );
319            }
320        }
321
322        Ok(())
323    }
324
325    /// Saves the diff of two values into a file with the given name in the output directory.
326    fn save_diff<T: PartialEq + Debug>(
327        &self,
328        filename: String,
329        original: &T,
330        new: &T,
331    ) -> eyre::Result<PathBuf> {
332        let path = self.output_directory.join(filename);
333        let diff = Comparison::new(original, new);
334        File::create(&path)?.write_all(diff.to_string().as_bytes())?;
335
336        Ok(path)
337    }
338
339    fn save_file<T: Serialize>(&self, filename: String, value: &T) -> eyre::Result<PathBuf> {
340        let path = self.output_directory.join(filename);
341        File::create(&path)?.write_all(serde_json::to_string(value)?.as_bytes())?;
342
343        Ok(path)
344    }
345}
346
347impl<P, E, N: NodePrimitives> InvalidBlockHook<N> for InvalidBlockWitnessHook<P, E>
348where
349    P: StateProviderFactory + Send + Sync + 'static,
350    E: ConfigureEvm<Primitives = N> + 'static,
351{
352    fn on_invalid_block(
353        &self,
354        parent_header: &SealedHeader<N::BlockHeader>,
355        block: &RecoveredBlock<N::Block>,
356        output: &BlockExecutionOutput<N::Receipt>,
357        trie_updates: Option<(&TrieUpdates, B256)>,
358    ) {
359        if let Err(err) = self.on_invalid_block(parent_header, block, output, trie_updates) {
360            warn!(target: "engine::invalid_block_hooks::witness", %err, "Failed to invoke hook");
361        }
362    }
363}