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 providers::BlockchainProvider, BlockHashReader, BlockNumReader, BlockReader, ChainSpecProvider,
21 DatabaseProviderFactory, ReceiptProvider, 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 state_provider_factory = BlockchainProvider::new(provider_factory.clone())?;
154 let mut bal_buf = Vec::new();
156
157 let db_at = {
158 |block_number: u64| {
159 let provider = provider_factory
160 .database_provider_ro()
161 .unwrap()
162 .disable_long_read_transaction_safety();
163 let hash = provider.block_hash(block_number).unwrap().unwrap();
164 StateProviderDatabase(
165 state_provider_factory
166 .state_provider_from_database(provider, hash),
167 )
168 }
169 };
170
171 loop {
172 if cancellation.is_cancelled() {
173 break;
174 }
175
176 let chunk_start =
178 next_block.fetch_add(blocks_per_chunk, Ordering::Relaxed);
179 if chunk_start >= max_block {
180 break;
181 }
182 let chunk_end = (chunk_start + blocks_per_chunk).min(max_block);
183
184 let mut executor = evm_config.batch_executor(db_at(chunk_start - 1));
185 let mut executor_created = Instant::now();
186
187 'blocks: for block in chunk_start..chunk_end {
188 if cancellation.is_cancelled() {
189 break;
190 }
191
192 let block = provider_factory
193 .recovered_block(block.into(), TransactionVariant::NoHash)?
194 .unwrap();
195
196 let result = match executor.execute_one(&block) {
197 Ok(result) => result,
198 Err(err) => {
199 if skip_invalid_blocks {
200 executor =
201 evm_config.batch_executor(db_at(block.number()));
202 let _ =
203 info_tx.send((block, eyre::Report::new(err)));
204 continue
205 }
206 return Err(err.into())
207 }
208 };
209
210 let bal_hash = executor
211 .take_bal()
212 .map(|bal| Bal::from(bal).compute_hash_with_buf(&mut bal_buf));
213
214 if let Err(err) = consensus
215 .validate_block_post_execution(&block, &result, None, bal_hash)
216 .wrap_err_with(|| {
217 format!(
218 "Failed to validate block {} {}",
219 block.number(),
220 block.hash()
221 )
222 })
223 {
224 let correct_receipts = provider_factory
225 .receipts_by_block(block.number().into())?
226 .unwrap();
227
228 for (i, (receipt, correct_receipt)) in
229 result.receipts.iter().zip(correct_receipts.iter()).enumerate()
230 {
231 if receipt != correct_receipt {
232 let tx_hash =
233 block.body().transactions()[i].tx_hash();
234 error!(
235 ?receipt,
236 ?correct_receipt,
237 index = i,
238 ?tx_hash,
239 "Invalid receipt"
240 );
241 let expected_gas_used =
242 correct_receipt.cumulative_gas_used() -
243 if i == 0 {
244 0
245 } else {
246 correct_receipts[i - 1]
247 .cumulative_gas_used()
248 };
249 let got_gas_used = receipt.cumulative_gas_used() -
250 if i == 0 {
251 0
252 } else {
253 result.receipts[i - 1].cumulative_gas_used()
254 };
255 if got_gas_used != expected_gas_used {
256 let mismatch = GotExpected {
257 expected: expected_gas_used,
258 got: got_gas_used,
259 };
260
261 error!(number=?block.number(), ?mismatch, "Gas usage mismatch");
262 if skip_invalid_blocks {
263 executor = evm_config
264 .batch_executor(db_at(block.number()));
265 let _ = info_tx.send((block, err));
266 continue 'blocks;
267 }
268 return Err(err);
269 }
270 } else {
271 continue;
272 }
273 }
274
275 if skip_invalid_blocks {
276 executor =
277 evm_config.batch_executor(db_at(block.number()));
278 let _ = info_tx.send((block, err));
279 continue 'blocks;
280 }
281 return Err(err);
282 }
283 let _ = stats_tx.send((block.number(), block.gas_used()));
284
285 if executor.size_hint() > 5_000_000 ||
287 executor_created.elapsed() > executor_lifetime
288 {
289 let last_block = block.number();
290 let old_executor = std::mem::replace(
291 &mut executor,
292 evm_config.batch_executor(db_at(last_block)),
293 );
294 let bundle = old_executor.into_state().take_bundle();
295 verify_bundle_against_changesets(
296 &provider,
297 &bundle,
298 last_block,
299 )?;
300 executor_created = Instant::now();
301 }
302 }
303
304 let bundle = executor.into_state().take_bundle();
306 verify_bundle_against_changesets(
307 &provider,
308 &bundle,
309 chunk_end - 1,
310 )?;
311 }
312
313 eyre::Ok(())
314 });
315 }
316
317 let instant = Instant::now();
318 let mut total_executed_blocks = 0;
319 let mut total_executed_gas = 0;
320 let mut latest_executed_block = None;
321
322 let mut last_logged_gas = 0;
323 let mut last_logged_blocks = 0;
324 let mut last_logged_time = Instant::now();
325 let mut invalid_blocks = Vec::new();
326
327 let mut interval = tokio::time::interval(Duration::from_secs(10));
328
329 loop {
330 tokio::select! {
331 Some((block_number, gas_used)) = stats_rx.recv() => {
332 total_executed_blocks += 1;
333 total_executed_gas += gas_used;
334 latest_executed_block =
335 Some(latest_executed_block.unwrap_or(block_number).max(block_number));
336 }
337 Some((block, err)) = info_rx.recv() => {
338 error!(?err, block=?block.num_hash(), "Invalid block");
339 invalid_blocks.push(block.num_hash());
340 }
341 result = tasks.join_next() => {
342 if let Some(result) = result {
343 if matches!(result, Err(_) | Ok(Err(_))) {
344 error!(?result);
345 return Err(eyre::eyre!("Re-execution failed: {result:?}"));
346 }
347 } else {
348 break;
349 }
350 }
351 _ = interval.tick() => {
352 let blocks_executed = total_executed_blocks - last_logged_blocks;
353 let gas_executed = total_executed_gas - last_logged_gas;
354
355 if blocks_executed > 0 {
356 let progress = 100.0 * total_executed_gas as f64 / total_gas as f64;
357 info!(
358 throughput=?format_gas_throughput(gas_executed, last_logged_time.elapsed()),
359 progress=format!("{progress:.2}%"),
360 ?latest_executed_block,
361 "Executed {blocks_executed} blocks"
362 );
363 }
364
365 last_logged_blocks = total_executed_blocks;
366 last_logged_gas = total_executed_gas;
367 last_logged_time = Instant::now();
368 }
369 }
370 }
371
372 if invalid_blocks.is_empty() {
373 info!(
374 start_block = min_block,
375 end_block = max_block,
376 %total_executed_blocks,
377 ?latest_executed_block,
378 throughput=?format_gas_throughput(total_executed_gas, instant.elapsed()),
379 "Re-executed successfully"
380 );
381 } else {
382 info!(
383 start_block = min_block,
384 end_block = max_block,
385 %total_executed_blocks,
386 ?latest_executed_block,
387 invalid_block_count = invalid_blocks.len(),
388 ?invalid_blocks,
389 throughput=?format_gas_throughput(total_executed_gas, instant.elapsed()),
390 "Re-executed with invalid blocks"
391 );
392 }
393
394 Ok(())
395 }
396}
397
398fn verify_bundle_against_changesets<P>(
404 provider: &P,
405 bundle: &BundleState,
406 last_block: u64,
407) -> eyre::Result<()>
408where
409 P: ChangeSetReader + StorageChangeSetReader,
410{
411 for (i, block_reverts) in bundle.reverts.iter().rev().enumerate() {
413 let block_number = last_block - i as u64;
414
415 let mut cs_accounts: HashMap<Address, Option<Account>> = provider
416 .account_block_changeset(block_number)?
417 .into_iter()
418 .map(|cs| (cs.address, cs.info))
419 .collect();
420
421 let mut cs_storage: HashMap<Address, HashMap<B256, U256>> = HashMap::new();
422 for (bna, entry) in provider.storage_changeset(block_number)? {
423 cs_storage.entry(bna.address()).or_default().insert(entry.key, entry.value);
424 }
425
426 for (addr, revert) in block_reverts {
427 match &revert.account {
429 AccountInfoRevert::DoNothing => {
430 eyre::ensure!(
431 !cs_accounts.contains_key(addr),
432 "Block {block_number}: account {addr} in changeset but revert is DoNothing",
433 );
434 }
435 AccountInfoRevert::DeleteIt => {
436 let cs_info = cs_accounts.remove(addr).ok_or_else(|| {
437 eyre::eyre!("Block {block_number}: account {addr} revert is DeleteIt but not in changeset")
438 })?;
439 eyre::ensure!(
440 cs_info.is_none(),
441 "Block {block_number}: account {addr} revert is DeleteIt but changeset has {cs_info:?}",
442 );
443 }
444 AccountInfoRevert::RevertTo(info) => {
445 let cs_info = cs_accounts.remove(addr).ok_or_else(|| {
446 eyre::eyre!("Block {block_number}: account {addr} revert is RevertTo but not in changeset")
447 })?;
448 let revert_acct = Some(Account::from(info));
449 eyre::ensure!(
450 revert_acct == cs_info,
451 "Block {block_number}: account {addr} info mismatch: revert={revert_acct:?} cs={cs_info:?}",
452 );
453 }
454 }
455
456 let mut cs_slots = cs_storage.get_mut(addr);
458 for (slot_key, revert_slot) in &revert.storage {
459 let b256_key = B256::from(*slot_key);
460 let cs_value = cs_slots.as_mut().and_then(|s| s.remove(&b256_key));
461 match (revert_slot, cs_value) {
462 (RevertToSlot::Destroyed, _) => {}
467 (RevertToSlot::Some(prev), Some(cs_value)) => eyre::ensure!(
468 *prev == cs_value,
469 "Block {block_number}: {addr} slot {b256_key} mismatch: \
470 revert={prev} cs={cs_value}",
471 ),
472 (RevertToSlot::Some(_), None) => eyre::ensure!(
473 revert.wipe_storage,
474 "Block {block_number}: {addr} slot {b256_key} in reverts but not in changeset",
475 ),
476 }
477 }
478
479 if let Some(remaining) = cs_slots.filter(|s| !s.is_empty()) {
481 eyre::ensure!(
482 revert.wipe_storage,
483 "Block {block_number}: {addr} has {} unmatched storage slots in changeset",
484 remaining.len(),
485 );
486 }
487 }
488
489 if let Some(addr) = cs_accounts.keys().next() {
491 eyre::bail!("Block {block_number}: account {addr} in changeset but not in reverts");
492 }
493 }
494
495 Ok(())
496}