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, 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")]
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)]
61    num_tasks: Option<u64>,
62
63    /// Number of blocks each worker processes before grabbing the next chunk.
64    #[arg(long, default_value = "5000")]
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                            .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                    // Atomically grab the next chunk of blocks.
177                    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                        // Reset DB once in a while to avoid OOM or read tx timeouts
286                        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                    // Full verification at chunk end for remaining unverified blocks
305                    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
398/// Verifies reverts against database changesets.
399///
400/// For each block, reverts must match changeset entries exactly. No extra slots/accounts
401/// in reverts for non-destroyed accounts. Destroyed accounts may have extra changeset slots
402/// (from DB storage wipe) absent from reverts.
403fn 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    // Verify reverts against changesets per block
412    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            // Verify account info
428            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            // Verify storage slots — remove matched changeset entries as we go
457            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                    // When a contract is selfdestructed and re-created at the same address
463                    // within the same block, revm marks slots touched by the new contract
464                    // as `Destroyed` and never reads the original DB value, so
465                    // `to_previous_value()` would resolve to zero, which might be wrong.
466                    (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            // Any remaining cs_storage slots for this address must be from a destroyed account
480            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        // Any remaining cs_accounts entries had no corresponding revert
490        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}