1use crate::common::{
4 AccessRights, CliComponentsBuilder, CliNodeComponents, CliNodeTypes, Environment,
5 EnvironmentArgs,
6};
7use alloy_consensus::{transaction::TxHashRef, BlockHeader, TxReceipt};
8use alloy_eip7928::bal::Bal;
9use alloy_primitives::{Address, B256, U256};
10use clap::Parser;
11use eyre::WrapErr;
12use reth_chainspec::{EthChainSpec, EthereumHardforks, Hardforks};
13use reth_cli::chainspec::ChainSpecParser;
14use reth_cli_util::cancellation::CancellationToken;
15use reth_consensus::FullConsensus;
16use reth_evm::{database::StateProviderDatabase, execute::Executor, ConfigureEvm};
17use reth_node_core::args::JitArgs;
18use reth_primitives_traits::{format_gas_throughput, Account, BlockBody, GotExpected};
19use reth_provider::{
20 providers::BlockchainProvider, BlockHashReader, BlockNumReader, BlockReader, ChainSpecProvider,
21 DatabaseProviderFactory, ReceiptProvider, StaticFileProviderFactory, TransactionVariant,
22};
23use reth_stages::stages::calculate_gas_used_from_headers;
24use reth_storage_api::{ChangeSetReader, DBProvider, StateProvider, StorageChangeSetReader};
25use reth_trie::bundle_state::{AccountInfoRevert, BundleState, RevertToSlot};
26use std::{
27 collections::HashMap,
28 sync::{
29 atomic::{AtomicU64, Ordering},
30 Arc,
31 },
32 time::{Duration, Instant},
33};
34use tokio::{sync::mpsc, task::JoinSet};
35use tracing::*;
36
37#[derive(Debug, Parser)]
41pub struct Command<C: ChainSpecParser> {
42 #[command(flatten)]
43 env: EnvironmentArgs<C>,
44
45 #[arg(long, default_value = "1", value_parser = clap::value_parser!(u64).range(1..))]
47 from: u64,
48
49 #[arg(long)]
51 to: Option<u64>,
52
53 #[arg(long, value_parser = clap::value_parser!(u64).range(1..))]
55 num_tasks: Option<u64>,
56
57 #[arg(long, default_value = "5000", value_parser = clap::value_parser!(u64).range(1..))]
59 blocks_per_chunk: u64,
60
61 #[arg(long)]
63 skip_invalid_blocks: bool,
64
65 #[command(flatten)]
66 pub jit: JitArgs,
67}
68
69impl<C: ChainSpecParser> Command<C> {
70 pub fn chain_spec(&self) -> Option<&Arc<C::ChainSpec>> {
72 Some(&self.env.chain)
73 }
74}
75
76impl<C: ChainSpecParser<ChainSpec: EthChainSpec + Hardforks + EthereumHardforks>> Command<C> {
77 pub async fn execute<N>(
79 mut self,
80 components: impl CliComponentsBuilder<N>,
81 runtime: reth_tasks::Runtime,
82 ) -> eyre::Result<()>
83 where
84 N: CliNodeTypes<ChainSpec = C::ChainSpec>,
85 {
86 if self.env.db.rocksdb_block_cache_size.is_none() {
88 self.env.db.rocksdb_block_cache_size = Some(4 << 30);
89 }
90
91 let Environment { provider_factory, .. } = self.env.init::<N>(AccessRights::RO, runtime)?;
92
93 let components = components(provider_factory.chain_spec());
94
95 let min_block = self.from;
96 let best_block = DatabaseProviderFactory::database_provider_ro(&provider_factory)?
97 .best_block_number()?;
98 let mut max_block = best_block;
99 if let Some(to) = self.to {
100 if to > best_block {
101 warn!(
102 requested = to,
103 best_block,
104 "Requested --to is beyond available chain head; clamping to best block"
105 );
106 } else {
107 max_block = to;
108 }
109 };
110
111 if min_block > max_block {
112 eyre::bail!("--from ({min_block}) is beyond --to ({max_block}), nothing to re-execute");
113 }
114
115 let num_tasks = self.num_tasks.unwrap_or_else(|| {
116 std::thread::available_parallelism().map(|n| n.get() as u64).unwrap_or(10)
117 });
118
119 let total_gas = calculate_gas_used_from_headers(
120 &provider_factory.static_file_provider(),
121 min_block..=max_block,
122 )?;
123
124 let skip_invalid_blocks = self.skip_invalid_blocks;
125 let blocks_per_chunk = self.blocks_per_chunk;
126 let (stats_tx, mut stats_rx) = mpsc::unbounded_channel();
127 let (info_tx, mut info_rx) = mpsc::unbounded_channel();
128 let cancellation = CancellationToken::new();
129 let _guard = cancellation.drop_guard();
130
131 let next_block = Arc::new(AtomicU64::new(min_block));
133
134 let mut tasks = JoinSet::new();
135 for _ in 0..num_tasks {
136 let provider_factory = provider_factory.clone();
137 let evm_config = components.evm_config().clone();
138 let consensus = components.consensus().clone();
139 let stats_tx = stats_tx.clone();
140 let info_tx = info_tx.clone();
141 let cancellation = cancellation.clone();
142 let next_block = Arc::clone(&next_block);
143 tasks.spawn_blocking(move || {
144 let evm_config = evm_config.with_jit_support();
145 let executor_lifetime = Duration::from_secs(600);
146 let provider = provider_factory.database_provider_ro()?.disable_long_read_transaction_safety();
147 let state_provider_factory = BlockchainProvider::new(provider_factory.clone())?;
148 let mut bal_buf = Vec::new();
150
151 let db_at = {
152 |block_number: u64| {
153 let provider = provider_factory
154 .database_provider_ro()?
155 .disable_long_read_transaction_safety();
156 let hash = provider.block_hash(block_number)?.ok_or_else(|| {
157 eyre::eyre!("hash for block {block_number} not found in the database")
158 })?;
159 eyre::Ok(StateProviderDatabase(
160 state_provider_factory
161 .state_provider_from_database(provider, hash)
162 .into_evm_state_provider(),
163 ))
164 }
165 };
166
167 loop {
168 if cancellation.is_cancelled() {
169 break;
170 }
171
172 let chunk_start =
174 next_block.fetch_add(blocks_per_chunk, Ordering::Relaxed);
175 if chunk_start > max_block {
176 break;
177 }
178 let chunk_end = (chunk_start + blocks_per_chunk).min(max_block + 1);
179
180 let mut executor = evm_config.batch_executor(db_at(chunk_start - 1)?);
181 let mut executor_created = Instant::now();
182
183 'blocks: for block in chunk_start..chunk_end {
184 if cancellation.is_cancelled() {
185 break;
186 }
187
188 let block = provider_factory
189 .recovered_block(block.into(), TransactionVariant::NoHash)?
190 .ok_or_else(|| eyre::eyre!("block {block} not found in the database"))?;
191
192 let result = match executor.execute_one(&block) {
193 Ok(result) => result,
194 Err(err) => {
195 if skip_invalid_blocks {
196 executor =
197 evm_config.batch_executor(db_at(block.number())?);
198 let _ =
199 info_tx.send((block, eyre::Report::new(err)));
200 continue
201 }
202 return Err(err.into())
203 }
204 };
205
206 let bal_hash = executor
207 .take_bal()
208 .map(|bal| Bal::from(bal).compute_hash_with_buf(&mut bal_buf));
209
210 if let Err(err) = consensus
211 .validate_block_post_execution(&block, &result, None, bal_hash)
212 .wrap_err_with(|| {
213 format!(
214 "Failed to validate block {} {}",
215 block.number(),
216 block.hash()
217 )
218 })
219 {
220 let correct_receipts = provider_factory
221 .receipts_by_block(block.number().into())?
222 .ok_or_else(|| {
223 eyre::eyre!(
224 "receipts for block {} not found in the database",
225 block.number()
226 )
227 })?;
228
229 for (i, (receipt, correct_receipt)) in
230 result.receipts.iter().zip(correct_receipts.iter()).enumerate()
231 {
232 if receipt != correct_receipt {
233 let tx_hash =
234 block.body().transactions()[i].tx_hash();
235 error!(
236 ?receipt,
237 ?correct_receipt,
238 index = i,
239 ?tx_hash,
240 "Invalid receipt"
241 );
242 let expected_gas_used =
243 correct_receipt.cumulative_gas_used() -
244 if i == 0 {
245 0
246 } else {
247 correct_receipts[i - 1]
248 .cumulative_gas_used()
249 };
250 let got_gas_used = receipt.cumulative_gas_used() -
251 if i == 0 {
252 0
253 } else {
254 result.receipts[i - 1].cumulative_gas_used()
255 };
256 if got_gas_used != expected_gas_used {
257 let mismatch = GotExpected {
258 expected: expected_gas_used,
259 got: got_gas_used,
260 };
261
262 error!(number=?block.number(), ?mismatch, "Gas usage mismatch");
263 if skip_invalid_blocks {
264 executor = evm_config
265 .batch_executor(db_at(block.number())?);
266 let _ = info_tx.send((block, err));
267 continue 'blocks;
268 }
269 return Err(err);
270 }
271 } else {
272 continue;
273 }
274 }
275
276 if skip_invalid_blocks {
277 executor =
278 evm_config.batch_executor(db_at(block.number())?);
279 let _ = info_tx.send((block, err));
280 continue 'blocks;
281 }
282 return Err(err);
283 }
284 let _ = stats_tx.send((block.number(), block.gas_used()));
285
286 if executor.size_hint() > 5_000_000 ||
288 executor_created.elapsed() > executor_lifetime
289 {
290 let last_block = block.number();
291 let old_executor = std::mem::replace(
292 &mut executor,
293 evm_config.batch_executor(db_at(last_block)?),
294 );
295 let bundle = old_executor.into_state();
296 verify_bundle_against_changesets(
297 &provider,
298 &bundle,
299 last_block,
300 )?;
301 executor_created = Instant::now();
302 }
303 }
304
305 let bundle = executor.into_state();
307 verify_bundle_against_changesets(
308 &provider,
309 &bundle,
310 chunk_end - 1,
311 )?;
312 }
313
314 eyre::Ok(())
315 });
316 }
317
318 let instant = Instant::now();
319 let mut total_executed_blocks = 0;
320 let mut total_executed_gas = 0;
321 let mut latest_executed_block = None;
322
323 let mut last_logged_gas = 0;
324 let mut last_logged_blocks = 0;
325 let mut last_logged_time = Instant::now();
326 let mut invalid_blocks = Vec::new();
327
328 let mut interval = tokio::time::interval(Duration::from_secs(10));
329
330 loop {
331 tokio::select! {
332 Some((block_number, gas_used)) = stats_rx.recv() => {
333 total_executed_blocks += 1;
334 total_executed_gas += gas_used;
335 latest_executed_block =
336 Some(latest_executed_block.unwrap_or(block_number).max(block_number));
337 }
338 Some((block, err)) = info_rx.recv() => {
339 error!(?err, block=?block.num_hash(), "Invalid block");
340 invalid_blocks.push(block.num_hash());
341 }
342 result = tasks.join_next() => {
343 let Some(result) = result else { break };
344 match result {
345 Ok(Ok(())) => {}
346 Ok(Err(err)) => return Err(err.wrap_err("Re-execution failed")),
347 Err(err) => {
348 return Err(eyre::Report::new(err).wrap_err("Re-execution failed"))
349 }
350 }
351 }
352 _ = interval.tick() => {
353 let blocks_executed = total_executed_blocks - last_logged_blocks;
354 let gas_executed = total_executed_gas - last_logged_gas;
355
356 if blocks_executed > 0 {
357 let progress = 100.0 * total_executed_gas as f64 / total_gas as f64;
358 info!(
359 throughput=?format_gas_throughput(gas_executed, last_logged_time.elapsed()),
360 progress=format!("{progress:.2}%"),
361 ?latest_executed_block,
362 "Executed {blocks_executed} blocks"
363 );
364 }
365
366 last_logged_blocks = total_executed_blocks;
367 last_logged_gas = total_executed_gas;
368 last_logged_time = Instant::now();
369 }
370 }
371 }
372
373 if invalid_blocks.is_empty() {
374 info!(
375 start_block = min_block,
376 end_block = max_block,
377 %total_executed_blocks,
378 ?latest_executed_block,
379 throughput=?format_gas_throughput(total_executed_gas, instant.elapsed()),
380 "Re-executed successfully"
381 );
382 } else {
383 info!(
384 start_block = min_block,
385 end_block = max_block,
386 %total_executed_blocks,
387 ?latest_executed_block,
388 invalid_block_count = invalid_blocks.len(),
389 ?invalid_blocks,
390 throughput=?format_gas_throughput(total_executed_gas, instant.elapsed()),
391 "Re-executed with invalid blocks"
392 );
393 }
394
395 Ok(())
396 }
397}
398
399fn verify_bundle_against_changesets<P>(
405 provider: &P,
406 bundle: &BundleState,
407 last_block: u64,
408) -> eyre::Result<()>
409where
410 P: ChangeSetReader + StorageChangeSetReader,
411{
412 for (i, block_reverts) in bundle.reverts.iter().rev().enumerate() {
414 let block_number = last_block - i as u64;
415
416 let mut cs_accounts: HashMap<Address, Option<Account>> = provider
417 .account_block_changeset(block_number)?
418 .into_iter()
419 .map(|cs| (cs.address, cs.info))
420 .collect();
421
422 let mut cs_storage: HashMap<Address, HashMap<B256, U256>> = HashMap::new();
423 for (bna, entry) in provider.storage_changeset(block_number)? {
424 cs_storage.entry(bna.address()).or_default().insert(entry.key, entry.value);
425 }
426
427 for (addr, revert) in block_reverts {
428 match &revert.account {
430 AccountInfoRevert::DoNothing => {
431 eyre::ensure!(
432 !cs_accounts.contains_key(addr),
433 "Block {block_number}: account {addr} in changeset but revert is DoNothing",
434 );
435 }
436 AccountInfoRevert::DeleteIt => {
437 let cs_info = cs_accounts.remove(addr).ok_or_else(|| {
438 eyre::eyre!("Block {block_number}: account {addr} revert is DeleteIt but not in changeset")
439 })?;
440 eyre::ensure!(
441 cs_info.is_none(),
442 "Block {block_number}: account {addr} revert is DeleteIt but changeset has {cs_info:?}",
443 );
444 }
445 AccountInfoRevert::RevertTo(info) => {
446 let cs_info = cs_accounts.remove(addr).ok_or_else(|| {
447 eyre::eyre!("Block {block_number}: account {addr} revert is RevertTo but not in changeset")
448 })?;
449 let revert_acct = Some(Account::from(info));
450 eyre::ensure!(
451 revert_acct == cs_info,
452 "Block {block_number}: account {addr} info mismatch: revert={revert_acct:?} cs={cs_info:?}",
453 );
454 }
455 }
456
457 let mut cs_slots = cs_storage.get_mut(addr);
459 for (slot_key, revert_slot) in &revert.storage {
460 let b256_key = B256::from(*slot_key);
461 let cs_value = cs_slots.as_mut().and_then(|s| s.remove(&b256_key));
462 match (revert_slot, cs_value) {
463 (RevertToSlot::Destroyed, _) => {}
468 (RevertToSlot::Some(prev), Some(cs_value)) => eyre::ensure!(
469 *prev == cs_value,
470 "Block {block_number}: {addr} slot {b256_key} mismatch: \
471 revert={prev} cs={cs_value}",
472 ),
473 (RevertToSlot::Some(_), None) => eyre::ensure!(
474 revert.wipe_storage,
475 "Block {block_number}: {addr} slot {b256_key} in reverts but not in changeset",
476 ),
477 }
478 }
479
480 if let Some(remaining) = cs_slots.filter(|s| !s.is_empty()) {
482 eyre::ensure!(
483 revert.wipe_storage,
484 "Block {block_number}: {addr} has {} unmatched storage slots in changeset",
485 remaining.len(),
486 );
487 }
488 }
489
490 if let Some(addr) = cs_accounts.keys().next() {
492 eyre::bail!("Block {block_number}: account {addr} in changeset but not in reverts");
493 }
494 }
495
496 Ok(())
497}