Skip to main content

reth_cli_commands/
re_execute.rs

1//! Re-execute blocks from database in parallel.
2
3use 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/// `reth re-execute` command
44///
45/// Re-execute blocks in parallel to verify historical sync correctness.
46#[derive(Debug, Parser)]
47pub struct Command<C: ChainSpecParser> {
48    #[command(flatten)]
49    env: EnvironmentArgs<C>,
50
51    /// The height to start at.
52    #[arg(long, default_value = "1", value_parser = clap::value_parser!(u64).range(1..))]
53    from: u64,
54
55    /// The height to end at. Defaults to the latest block.
56    #[arg(long)]
57    to: Option<u64>,
58
59    /// Number of tasks to run in parallel. Defaults to the number of available CPUs.
60    #[arg(long, value_parser = clap::value_parser!(u64).range(1..))]
61    num_tasks: Option<u64>,
62
63    /// Number of blocks each worker processes before grabbing the next chunk.
64    #[arg(long, default_value = "5000", value_parser = clap::value_parser!(u64).range(1..))]
65    blocks_per_chunk: u64,
66
67    /// Continues with execution when an invalid block is encountered and collects these blocks.
68    #[arg(long)]
69    skip_invalid_blocks: bool,
70
71    #[command(flatten)]
72    pub jit: JitArgs,
73}
74
75impl<C: ChainSpecParser> Command<C> {
76    /// Returns the underlying chain being used to run this command
77    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    /// Execute `re-execute` command
84    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        // Default to 4GB RocksDB block cache for re-execute unless explicitly set.
93        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        // Shared counter for work stealing: workers atomically grab the next chunk of blocks.
138        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                // Reused across blocks for BAL hash encoding.
155                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                    // Atomically grab the next chunk of blocks.
179                    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                        // Reset DB once in a while to avoid OOM or read tx timeouts
293                        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                    // Full verification at chunk end for remaining unverified blocks
312                    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
405/// Verifies reverts against database changesets.
406///
407/// For each block, reverts must match changeset entries exactly. No extra slots/accounts
408/// in reverts for non-destroyed accounts. Destroyed accounts may have extra changeset slots
409/// (from DB storage wipe) absent from reverts.
410fn 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    // Verify reverts against changesets per block
419    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            // Verify account info
435            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            // Verify storage slots — remove matched changeset entries as we go
464            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                    // When a contract is selfdestructed and re-created at the same address
470                    // within the same block, revm marks slots touched by the new contract
471                    // as `Destroyed` and never reads the original DB value, so
472                    // `to_previous_value()` would resolve to zero, which might be wrong.
473                    (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            // Any remaining cs_storage slots for this address must be from a destroyed account
487            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        // Any remaining cs_accounts entries had no corresponding revert
497        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}