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