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