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 pub storage: BTreeMap<U256, StorageSlot>,
40 pub status: AccountStatus,
42}
43
44#[derive(Debug, PartialEq, Eq)]
45struct BundleStateSorted {
46 pub state: BTreeMap<Address, BundleAccountSorted>,
48 pub contracts: BTreeMap<B256, Bytecode>,
50 pub reverts: Vec<Vec<(Address, AccountRevertSorted)>>,
56 pub state_size: usize,
58 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#[derive(Debug)]
115pub struct InvalidBlockWitnessHook<P, E> {
116 provider: P,
118 executor: E,
120 output_directory: PathBuf,
123 healthy_node_client: Option<jsonrpsee::http_client::HttpClient>,
125}
126
127impl<P, E> InvalidBlockWitnessHook<P, E> {
128 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 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 let mut db = executor.into_state();
169 let mut bundle_state = db.take_bundle();
170
171 let mut state_preimages = Vec::default();
173
174 let codes = db
176 .cache
177 .contracts
178 .values()
179 .map(|code| code.original_bytes())
180 .chain(
181 bundle_state.contracts.values().map(|code| code.original_bytes()),
186 )
187 .collect();
188
189 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 let state_provider = db.database.into_inner();
221 let state = state_provider.witness(Default::default(), hashed_state.clone())?;
222
223 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 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 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 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 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 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 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 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 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 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}