reth_invalid_block_hooks/
witness.rs1use 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#[derive(Debug)]
26pub struct InvalidBlockWitnessHook<P, EvmConfig> {
27 provider: P,
29 evm_config: EvmConfig,
31 output_directory: PathBuf,
34 healthy_node_client: Option<jsonrpsee::http_client::HttpClient>,
36}
37
38impl<P, EvmConfig> InvalidBlockWitnessHook<P, EvmConfig> {
39 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 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 let (cfg, block_env) = self.evm_config.cfg_and_block_env(block.header(), U256::MAX);
81
82 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 system_caller.apply_pre_execution_changes(&block.clone().unseal().block, &mut evm)?;
93
94 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 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 db.increment_balances(balance_increments)?;
120
121 db.merge_transitions(BundleRetention::Reverts);
123
124 let mut bundle_state = db.take_bundle();
126
127 let mut state_preimages = HashMap::default();
129
130 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 let state_provider = db.database.into_inner();
162 let state = state_provider.witness(Default::default(), hashed_state.clone())?;
163
164 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 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 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 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 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 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 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 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 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}