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, 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 + 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 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 let mut db = executor.into_state();
159 let bundle_state = db.take_bundle();
160
161 let mut state_preimages = Vec::default();
163
164 let codes = db
166 .cache
167 .contracts
168 .values()
169 .map(|code| code.original_bytes())
170 .chain(
171 bundle_state.contracts.values().map(|code| code.original_bytes()),
176 )
177 .collect();
178
179 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 let state_provider = db.database.into_inner();
211 let state = state_provider.witness(Default::default(), hashed_state.clone())?;
212
213 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 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 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 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 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 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 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 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 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}