reth_invalid_block_hooks/
witness.rs

1use alloy_consensus::BlockHeader;
2use alloy_primitives::{keccak256, B256, U256};
3use alloy_rpc_types_debug::ExecutionWitness;
4use eyre::OptionExt;
5use pretty_assertions::Comparison;
6use reth_chainspec::{EthChainSpec, EthereumHardforks};
7use reth_engine_primitives::InvalidBlockHook;
8use reth_evm::{
9    state_change::post_block_balance_increments, system_calls::SystemCaller, ConfigureEvm,
10};
11use reth_primitives::{NodePrimitives, SealedBlockWithSenders, SealedHeader};
12use reth_primitives_traits::SignedTransaction;
13use reth_provider::{BlockExecutionOutput, ChainSpecProvider, StateProviderFactory};
14use reth_revm::{
15    database::StateProviderDatabase, db::states::bundle_state::BundleRetention,
16    primitives::EnvWithHandlerCfg, DatabaseCommit, StateBuilder,
17};
18use reth_rpc_api::DebugApiClient;
19use reth_tracing::tracing::warn;
20use reth_trie::{updates::TrieUpdates, HashedStorage};
21use serde::Serialize;
22use std::{collections::HashMap, fmt::Debug, fs::File, io::Write, path::PathBuf};
23
24/// Generates a witness for the given block and saves it to a file.
25#[derive(Debug)]
26pub struct InvalidBlockWitnessHook<P, EvmConfig> {
27    /// The provider to read the historical state and do the EVM execution.
28    provider: P,
29    /// The EVM configuration to use for the execution.
30    evm_config: EvmConfig,
31    /// The directory to write the witness to. Additionally, diff files will be written to this
32    /// directory in case of failed sanity checks.
33    output_directory: PathBuf,
34    /// The healthy node client to compare the witness against.
35    healthy_node_client: Option<jsonrpsee::http_client::HttpClient>,
36}
37
38impl<P, EvmConfig> InvalidBlockWitnessHook<P, EvmConfig> {
39    /// Creates a new witness hook.
40    pub const fn new(
41        provider: P,
42        evm_config: EvmConfig,
43        output_directory: PathBuf,
44        healthy_node_client: Option<jsonrpsee::http_client::HttpClient>,
45    ) -> Self {
46        Self { provider, evm_config, output_directory, healthy_node_client }
47    }
48}
49
50impl<P, EvmConfig> InvalidBlockWitnessHook<P, EvmConfig>
51where
52    P: StateProviderFactory
53        + ChainSpecProvider<ChainSpec: EthChainSpec + EthereumHardforks>
54        + Send
55        + Sync
56        + 'static,
57{
58    fn on_invalid_block<N>(
59        &self,
60        parent_header: &SealedHeader<N::BlockHeader>,
61        block: &SealedBlockWithSenders<N::Block>,
62        output: &BlockExecutionOutput<N::Receipt>,
63        trie_updates: Option<(&TrieUpdates, B256)>,
64    ) -> eyre::Result<()>
65    where
66        N: NodePrimitives,
67        EvmConfig: ConfigureEvm<Header = N::BlockHeader, Transaction = N::SignedTx>,
68    {
69        // TODO(alexey): unify with `DebugApi::debug_execution_witness`
70
71        // Setup database.
72        let mut db = StateBuilder::new()
73            .with_database(StateProviderDatabase::new(
74                self.provider.state_by_block_hash(parent_header.hash())?,
75            ))
76            .with_bundle_update()
77            .build();
78
79        // Setup environment for the execution.
80        let (cfg, block_env) = self.evm_config.cfg_and_block_env(block.header(), U256::MAX);
81
82        // Setup EVM
83        let mut evm = self.evm_config.evm_with_env(
84            &mut db,
85            EnvWithHandlerCfg::new_with_cfg_env(cfg, block_env, Default::default()),
86        );
87
88        let mut system_caller =
89            SystemCaller::new(self.evm_config.clone(), self.provider.chain_spec());
90
91        // Apply pre-block system contract calls.
92        system_caller.apply_pre_execution_changes(&block.clone().unseal().block, &mut evm)?;
93
94        // Re-execute all of the transactions in the block to load all touched accounts into
95        // the cache DB.
96        for tx in block.transactions() {
97            self.evm_config
98                .fill_tx_env(
99                    evm.tx_mut(),
100                    tx,
101                    tx.recover_signer().ok_or_eyre("failed to recover sender")?,
102                )
103                .map_err(|err| eyre::eyre!("failed to fill tx env: {:?}", err))?;
104            let result = evm.transact()?;
105            evm.db_mut().commit(result.state);
106        }
107
108        drop(evm);
109
110        // use U256::MAX here for difficulty, because fetching it is annoying
111        // NOTE: This is not mut because we are not doing the DAO irregular state change here
112        let balance_increments = post_block_balance_increments(
113            self.provider.chain_spec().as_ref(),
114            &block.clone().unseal().block,
115            U256::MAX,
116        );
117
118        // increment balances
119        db.increment_balances(balance_increments)?;
120
121        // Merge all state transitions
122        db.merge_transitions(BundleRetention::Reverts);
123
124        // Take the bundle state
125        let mut bundle_state = db.take_bundle();
126
127        // Initialize a map of preimages.
128        let mut state_preimages = HashMap::default();
129
130        // Grab all account proofs for the data accessed during block execution.
131        //
132        // Note: We grab *all* accounts in the cache here, as the `BundleState` prunes
133        // referenced accounts + storage slots.
134        let mut hashed_state = db.database.hashed_post_state(&bundle_state);
135        for (address, account) in db.cache.accounts {
136            let hashed_address = keccak256(address);
137            hashed_state
138                .accounts
139                .insert(hashed_address, account.account.as_ref().map(|a| a.info.clone().into()));
140
141            let storage = hashed_state
142                .storages
143                .entry(hashed_address)
144                .or_insert_with(|| HashedStorage::new(account.status.was_destroyed()));
145
146            if let Some(account) = account.account {
147                state_preimages.insert(hashed_address, alloy_rlp::encode(address).into());
148
149                for (slot, value) in account.storage {
150                    let slot = B256::from(slot);
151                    let hashed_slot = keccak256(slot);
152                    storage.storage.insert(hashed_slot, value);
153
154                    state_preimages.insert(hashed_slot, alloy_rlp::encode(slot).into());
155                }
156            }
157        }
158
159        // Generate an execution witness for the aggregated state of accessed accounts.
160        // Destruct the cache database to retrieve the state provider.
161        let state_provider = db.database.into_inner();
162        let state = state_provider.witness(Default::default(), hashed_state.clone())?;
163
164        // Write the witness to the output directory.
165        let response = ExecutionWitness {
166            state: HashMap::from_iter(state),
167            codes: Default::default(),
168            keys: state_preimages,
169        };
170        let re_executed_witness_path = self.save_file(
171            format!("{}_{}.witness.re_executed.json", block.number(), block.hash()),
172            &response,
173        )?;
174        if let Some(healthy_node_client) = &self.healthy_node_client {
175            // Compare the witness against the healthy node.
176            let healthy_node_witness = futures::executor::block_on(async move {
177                DebugApiClient::debug_execution_witness(healthy_node_client, block.number().into())
178                    .await
179            })?;
180
181            let healthy_path = self.save_file(
182                format!("{}_{}.witness.healthy.json", block.number(), block.hash()),
183                &healthy_node_witness,
184            )?;
185
186            // If the witnesses are different, write the diff to the output directory.
187            if response != healthy_node_witness {
188                let filename = format!("{}_{}.witness.diff", block.number(), block.hash());
189                let diff_path = self.save_diff(filename, &response, &healthy_node_witness)?;
190                warn!(
191                    target: "engine::invalid_block_hooks::witness",
192                    diff_path = %diff_path.display(),
193                    re_executed_path = %re_executed_witness_path.display(),
194                    healthy_path = %healthy_path.display(),
195                    "Witness mismatch against healthy node"
196                );
197            }
198        }
199
200        // The bundle state after re-execution should match the original one.
201        //
202        // NOTE: This should not be needed if `Reverts` had a comparison method that sorted first,
203        // or otherwise did not care about order.
204        //
205        // See: https://github.com/bluealloy/revm/issues/1813
206        let mut output = output.clone();
207        for reverts in output.state.reverts.iter_mut() {
208            reverts.sort_by(|left, right| left.0.cmp(&right.0));
209        }
210
211        // We also have to sort the `bundle_state` reverts
212        for reverts in bundle_state.reverts.iter_mut() {
213            reverts.sort_by(|left, right| left.0.cmp(&right.0));
214        }
215
216        if bundle_state != output.state {
217            let original_path = self.save_file(
218                format!("{}_{}.bundle_state.original.json", block.number(), block.hash()),
219                &output.state,
220            )?;
221            let re_executed_path = self.save_file(
222                format!("{}_{}.bundle_state.re_executed.json", block.number(), block.hash()),
223                &bundle_state,
224            )?;
225
226            let filename = format!("{}_{}.bundle_state.diff", block.number(), block.hash());
227            let diff_path = self.save_diff(filename, &bundle_state, &output.state)?;
228
229            warn!(
230                target: "engine::invalid_block_hooks::witness",
231                diff_path = %diff_path.display(),
232                original_path = %original_path.display(),
233                re_executed_path = %re_executed_path.display(),
234                "Bundle state mismatch after re-execution"
235            );
236        }
237
238        // Calculate the state root and trie updates after re-execution. They should match
239        // the original ones.
240        let (re_executed_root, trie_output) =
241            state_provider.state_root_with_updates(hashed_state)?;
242        if let Some((original_updates, original_root)) = trie_updates {
243            if re_executed_root != original_root {
244                let filename = format!("{}_{}.state_root.diff", block.number(), block.hash());
245                let diff_path = self.save_diff(filename, &re_executed_root, &original_root)?;
246                warn!(target: "engine::invalid_block_hooks::witness", ?original_root, ?re_executed_root, diff_path = %diff_path.display(), "State root mismatch after re-execution");
247            }
248
249            // If the re-executed state root does not match the _header_ state root, also log that.
250            if re_executed_root != block.state_root() {
251                let filename =
252                    format!("{}_{}.header_state_root.diff", block.number(), block.hash());
253                let diff_path = self.save_diff(filename, &re_executed_root, &block.state_root())?;
254                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");
255            }
256
257            if &trie_output != original_updates {
258                // Trie updates are too big to diff, so we just save the original and re-executed
259                let original_path = self.save_file(
260                    format!("{}_{}.trie_updates.original.json", block.number(), block.hash()),
261                    original_updates,
262                )?;
263                let re_executed_path = self.save_file(
264                    format!("{}_{}.trie_updates.re_executed.json", block.number(), block.hash()),
265                    &trie_output,
266                )?;
267                warn!(
268                    target: "engine::invalid_block_hooks::witness",
269                    original_path = %original_path.display(),
270                    re_executed_path = %re_executed_path.display(),
271                    "Trie updates mismatch after re-execution"
272                );
273            }
274        }
275
276        Ok(())
277    }
278
279    /// Saves the diff of two values into a file with the given name in the output directory.
280    fn save_diff<T: PartialEq + Debug>(
281        &self,
282        filename: String,
283        original: &T,
284        new: &T,
285    ) -> eyre::Result<PathBuf> {
286        let path = self.output_directory.join(filename);
287        let diff = Comparison::new(original, new);
288        File::create(&path)?.write_all(diff.to_string().as_bytes())?;
289
290        Ok(path)
291    }
292
293    fn save_file<T: Serialize>(&self, filename: String, value: &T) -> eyre::Result<PathBuf> {
294        let path = self.output_directory.join(filename);
295        File::create(&path)?.write_all(serde_json::to_string(value)?.as_bytes())?;
296
297        Ok(path)
298    }
299}
300
301impl<P, EvmConfig, N> InvalidBlockHook<N> for InvalidBlockWitnessHook<P, EvmConfig>
302where
303    N: NodePrimitives,
304    P: StateProviderFactory
305        + ChainSpecProvider<ChainSpec: EthChainSpec + EthereumHardforks>
306        + Send
307        + Sync
308        + 'static,
309    EvmConfig: ConfigureEvm<Header = N::BlockHeader, Transaction = N::SignedTx>,
310{
311    fn on_invalid_block(
312        &self,
313        parent_header: &SealedHeader<N::BlockHeader>,
314        block: &SealedBlockWithSenders<N::Block>,
315        output: &BlockExecutionOutput<N::Receipt>,
316        trie_updates: Option<(&TrieUpdates, B256)>,
317    ) {
318        if let Err(err) = self.on_invalid_block::<N>(parent_header, block, output, trie_updates) {
319            warn!(target: "engine::invalid_block_hooks::witness", %err, "Failed to invoke hook");
320        }
321    }
322}