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    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/// `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                // Reused across blocks for BAL hash encoding.
154                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                    // Atomically grab the next chunk of blocks.
172                    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                        // Reset DB once in a while to avoid OOM or read tx timeouts
281                        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                    // Full verification at chunk end for remaining unverified blocks
300                    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
393/// Verifies reverts against database changesets.
394///
395/// For each block, reverts must match changeset entries exactly. No extra slots/accounts
396/// in reverts for non-destroyed accounts. Destroyed accounts may have extra changeset slots
397/// (from DB storage wipe) absent from reverts.
398fn 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    // Verify reverts against changesets per block
407    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            // Verify account info
423            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            // Verify storage slots — remove matched changeset entries as we go
452            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                    // When a contract is selfdestructed and re-created at the same address
458                    // within the same block, revm marks slots touched by the new contract
459                    // as `Destroyed` and never reads the original DB value, so
460                    // `to_previous_value()` would resolve to zero, which might be wrong.
461                    (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            // Any remaining cs_storage slots for this address must be from a destroyed account
475            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        // Any remaining cs_accounts entries had no corresponding revert
485        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}