reth_invalid_block_hooks/
witness.rs1use 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 pub storage: BTreeMap<U256, StorageSlot>,
39 pub status: AccountStatus,
41}
42
43#[derive(Debug, PartialEq, Eq)]
44struct BundleStateSorted {
45 pub state: BTreeMap<Address, BundleAccountSorted>,
47 pub contracts: BTreeMap<B256, Bytecode>,
49 pub reverts: Vec<Vec<(Address, AccountRevertSorted)>>,
55 pub state_size: usize,
57 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#[derive(Debug)]
112pub struct InvalidBlockWitnessHook<P, E> {
113 provider: P,
115 evm_config: E,
117 output_directory: PathBuf,
120 healthy_node_client: Option<jsonrpsee::http_client::HttpClient>,
122}
123
124impl<P, E> InvalidBlockWitnessHook<P, E> {
125 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 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 let mut db = executor.into_state();
162 let bundle_state = db.take_bundle();
163
164 let mut state_preimages = Vec::default();
166
167 let codes = db
169 .cache
170 .contracts
171 .values()
172 .map(|code| code.original_bytes())
173 .chain(
174 bundle_state.contracts.values().map(|code| code.original_bytes()),
179 )
180 .collect();
181
182 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 let state_provider = db.database.into_inner();
214 let state = state_provider.witness(Default::default(), hashed_state.clone())?;
215
216 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 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 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 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 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 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 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 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 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}