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::{database::StateProviderDatabase, 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_stages::stages::calculate_gas_used_from_headers;
24use reth_storage_api::{ChangeSetReader, DBProvider, StateProvider, StorageChangeSetReader};
25use reth_trie::bundle_state::{AccountInfoRevert, BundleState, RevertToSlot};
26use std::{
27    collections::HashMap,
28    sync::{
29        atomic::{AtomicU64, Ordering},
30        Arc,
31    },
32    time::{Duration, Instant},
33};
34use tokio::{sync::mpsc, task::JoinSet};
35use tracing::*;
36
37/// `reth re-execute` command
38///
39/// Re-execute blocks in parallel to verify historical sync correctness.
40#[derive(Debug, Parser)]
41pub struct Command<C: ChainSpecParser> {
42    #[command(flatten)]
43    env: EnvironmentArgs<C>,
44
45    /// The height to start at.
46    #[arg(long, default_value = "1", value_parser = clap::value_parser!(u64).range(1..))]
47    from: u64,
48
49    /// The height to end at. Defaults to the latest block.
50    #[arg(long)]
51    to: Option<u64>,
52
53    /// Number of tasks to run in parallel. Defaults to the number of available CPUs.
54    #[arg(long, value_parser = clap::value_parser!(u64).range(1..))]
55    num_tasks: Option<u64>,
56
57    /// Number of blocks each worker processes before grabbing the next chunk.
58    #[arg(long, default_value = "5000", value_parser = clap::value_parser!(u64).range(1..))]
59    blocks_per_chunk: u64,
60
61    /// Continues with execution when an invalid block is encountered and collects these blocks.
62    #[arg(long)]
63    skip_invalid_blocks: bool,
64
65    #[command(flatten)]
66    pub jit: JitArgs,
67}
68
69impl<C: ChainSpecParser> Command<C> {
70    /// Returns the underlying chain being used to run this command
71    pub fn chain_spec(&self) -> Option<&Arc<C::ChainSpec>> {
72        Some(&self.env.chain)
73    }
74}
75
76impl<C: ChainSpecParser<ChainSpec: EthChainSpec + Hardforks + EthereumHardforks>> Command<C> {
77    /// Execute `re-execute` command
78    pub async fn execute<N>(
79        mut self,
80        components: impl CliComponentsBuilder<N>,
81        runtime: reth_tasks::Runtime,
82    ) -> eyre::Result<()>
83    where
84        N: CliNodeTypes<ChainSpec = C::ChainSpec>,
85    {
86        // Default to 4GB RocksDB block cache for re-execute unless explicitly set.
87        if self.env.db.rocksdb_block_cache_size.is_none() {
88            self.env.db.rocksdb_block_cache_size = Some(4 << 30);
89        }
90
91        let Environment { provider_factory, .. } = self.env.init::<N>(AccessRights::RO, runtime)?;
92
93        let components = components(provider_factory.chain_spec());
94
95        let min_block = self.from;
96        let best_block = DatabaseProviderFactory::database_provider_ro(&provider_factory)?
97            .best_block_number()?;
98        let mut max_block = best_block;
99        if let Some(to) = self.to {
100            if to > best_block {
101                warn!(
102                    requested = to,
103                    best_block,
104                    "Requested --to is beyond available chain head; clamping to best block"
105                );
106            } else {
107                max_block = to;
108            }
109        };
110
111        if min_block > max_block {
112            eyre::bail!("--from ({min_block}) is beyond --to ({max_block}), nothing to re-execute");
113        }
114
115        let num_tasks = self.num_tasks.unwrap_or_else(|| {
116            std::thread::available_parallelism().map(|n| n.get() as u64).unwrap_or(10)
117        });
118
119        let total_gas = calculate_gas_used_from_headers(
120            &provider_factory.static_file_provider(),
121            min_block..=max_block,
122        )?;
123
124        let skip_invalid_blocks = self.skip_invalid_blocks;
125        let blocks_per_chunk = self.blocks_per_chunk;
126        let (stats_tx, mut stats_rx) = mpsc::unbounded_channel();
127        let (info_tx, mut info_rx) = mpsc::unbounded_channel();
128        let cancellation = CancellationToken::new();
129        let _guard = cancellation.drop_guard();
130
131        // Shared counter for work stealing: workers atomically grab the next chunk of blocks.
132        let next_block = Arc::new(AtomicU64::new(min_block));
133
134        let mut tasks = JoinSet::new();
135        for _ in 0..num_tasks {
136            let provider_factory = provider_factory.clone();
137            let evm_config = components.evm_config().clone();
138            let consensus = components.consensus().clone();
139            let stats_tx = stats_tx.clone();
140            let info_tx = info_tx.clone();
141            let cancellation = cancellation.clone();
142            let next_block = Arc::clone(&next_block);
143            tasks.spawn_blocking(move || {
144                let evm_config = evm_config.with_jit_support();
145                let executor_lifetime = Duration::from_secs(600);
146                let provider = provider_factory.database_provider_ro()?.disable_long_read_transaction_safety();
147                let state_provider_factory = BlockchainProvider::new(provider_factory.clone())?;
148                // Reused across blocks for BAL hash encoding.
149                let mut bal_buf = Vec::new();
150
151                let db_at = {
152                    |block_number: u64| {
153                        let provider = provider_factory
154                            .database_provider_ro()?
155                            .disable_long_read_transaction_safety();
156                        let hash = provider.block_hash(block_number)?.ok_or_else(|| {
157                            eyre::eyre!("hash for block {block_number} not found in the database")
158                        })?;
159                        eyre::Ok(StateProviderDatabase(
160                            state_provider_factory
161                                .state_provider_from_database(provider, hash)
162                                .into_evm_state_provider(),
163                        ))
164                    }
165                };
166
167                loop {
168                    if cancellation.is_cancelled() {
169                        break;
170                    }
171
172                    // Atomically grab the next chunk of blocks.
173                    let chunk_start =
174                        next_block.fetch_add(blocks_per_chunk, Ordering::Relaxed);
175                    if chunk_start > max_block {
176                        break;
177                    }
178                    let chunk_end = (chunk_start + blocks_per_chunk).min(max_block + 1);
179
180                    let mut executor = evm_config.batch_executor(db_at(chunk_start - 1)?);
181                    let mut executor_created = Instant::now();
182
183                    'blocks: for block in chunk_start..chunk_end {
184                        if cancellation.is_cancelled() {
185                            break;
186                        }
187
188                        let block = provider_factory
189                            .recovered_block(block.into(), TransactionVariant::NoHash)?
190                            .ok_or_else(|| eyre::eyre!("block {block} not found in the database"))?;
191
192                        let result = match executor.execute_one(&block) {
193                            Ok(result) => result,
194                            Err(err) => {
195                                if skip_invalid_blocks {
196                                    executor =
197                                        evm_config.batch_executor(db_at(block.number())?);
198                                    let _ =
199                                        info_tx.send((block, eyre::Report::new(err)));
200                                    continue
201                                }
202                                return Err(err.into())
203                            }
204                        };
205
206                        let bal_hash = executor
207                            .take_bal()
208                            .map(|bal| Bal::from(bal).compute_hash_with_buf(&mut bal_buf));
209
210                        if let Err(err) = consensus
211                            .validate_block_post_execution(&block, &result, None, bal_hash)
212                            .wrap_err_with(|| {
213                                format!(
214                                    "Failed to validate block {} {}",
215                                    block.number(),
216                                    block.hash()
217                                )
218                            })
219                        {
220                            let correct_receipts = provider_factory
221                                .receipts_by_block(block.number().into())?
222                                .ok_or_else(|| {
223                                    eyre::eyre!(
224                                        "receipts for block {} not found in the database",
225                                        block.number()
226                                    )
227                                })?;
228
229                            for (i, (receipt, correct_receipt)) in
230                                result.receipts.iter().zip(correct_receipts.iter()).enumerate()
231                            {
232                                if receipt != correct_receipt {
233                                    let tx_hash =
234                                        block.body().transactions()[i].tx_hash();
235                                    error!(
236                                        ?receipt,
237                                        ?correct_receipt,
238                                        index = i,
239                                        ?tx_hash,
240                                        "Invalid receipt"
241                                    );
242                                    let expected_gas_used =
243                                        correct_receipt.cumulative_gas_used() -
244                                            if i == 0 {
245                                                0
246                                            } else {
247                                                correct_receipts[i - 1]
248                                                    .cumulative_gas_used()
249                                            };
250                                    let got_gas_used = receipt.cumulative_gas_used() -
251                                        if i == 0 {
252                                            0
253                                        } else {
254                                            result.receipts[i - 1].cumulative_gas_used()
255                                        };
256                                    if got_gas_used != expected_gas_used {
257                                        let mismatch = GotExpected {
258                                            expected: expected_gas_used,
259                                            got: got_gas_used,
260                                        };
261
262                                        error!(number=?block.number(), ?mismatch, "Gas usage mismatch");
263                                        if skip_invalid_blocks {
264                                            executor = evm_config
265                                                .batch_executor(db_at(block.number())?);
266                                            let _ = info_tx.send((block, err));
267                                            continue 'blocks;
268                                        }
269                                        return Err(err);
270                                    }
271                                } else {
272                                    continue;
273                                }
274                            }
275
276                            if skip_invalid_blocks {
277                                executor =
278                                    evm_config.batch_executor(db_at(block.number())?);
279                                let _ = info_tx.send((block, err));
280                                continue 'blocks;
281                            }
282                            return Err(err);
283                        }
284                        let _ = stats_tx.send((block.number(), block.gas_used()));
285
286                        // Reset DB once in a while to avoid OOM or read tx timeouts
287                        if executor.size_hint() > 5_000_000 ||
288                            executor_created.elapsed() > executor_lifetime
289                        {
290                            let last_block = block.number();
291                            let old_executor = std::mem::replace(
292                                &mut executor,
293                                evm_config.batch_executor(db_at(last_block)?),
294                            );
295                            let bundle = old_executor.into_state();
296                            verify_bundle_against_changesets(
297                                &provider,
298                                &bundle,
299                                last_block,
300                            )?;
301                            executor_created = Instant::now();
302                        }
303                    }
304
305                    // Full verification at chunk end for remaining unverified blocks
306                    let bundle = executor.into_state();
307                    verify_bundle_against_changesets(
308                        &provider,
309                        &bundle,
310                        chunk_end - 1,
311                    )?;
312                }
313
314                eyre::Ok(())
315            });
316        }
317
318        let instant = Instant::now();
319        let mut total_executed_blocks = 0;
320        let mut total_executed_gas = 0;
321        let mut latest_executed_block = None;
322
323        let mut last_logged_gas = 0;
324        let mut last_logged_blocks = 0;
325        let mut last_logged_time = Instant::now();
326        let mut invalid_blocks = Vec::new();
327
328        let mut interval = tokio::time::interval(Duration::from_secs(10));
329
330        loop {
331            tokio::select! {
332                Some((block_number, gas_used)) = stats_rx.recv() => {
333                    total_executed_blocks += 1;
334                    total_executed_gas += gas_used;
335                    latest_executed_block =
336                        Some(latest_executed_block.unwrap_or(block_number).max(block_number));
337                }
338                Some((block, err)) = info_rx.recv() => {
339                    error!(?err, block=?block.num_hash(), "Invalid block");
340                    invalid_blocks.push(block.num_hash());
341                }
342                result = tasks.join_next() => {
343                    let Some(result) = result else { break };
344                    match result {
345                        Ok(Ok(())) => {}
346                        Ok(Err(err)) => return Err(err.wrap_err("Re-execution failed")),
347                        Err(err) => {
348                            return Err(eyre::Report::new(err).wrap_err("Re-execution failed"))
349                        }
350                    }
351                }
352                _ = interval.tick() => {
353                    let blocks_executed = total_executed_blocks - last_logged_blocks;
354                    let gas_executed = total_executed_gas - last_logged_gas;
355
356                    if blocks_executed > 0 {
357                        let progress = 100.0 * total_executed_gas as f64 / total_gas as f64;
358                        info!(
359                            throughput=?format_gas_throughput(gas_executed, last_logged_time.elapsed()),
360                            progress=format!("{progress:.2}%"),
361                            ?latest_executed_block,
362                            "Executed {blocks_executed} blocks"
363                        );
364                    }
365
366                    last_logged_blocks = total_executed_blocks;
367                    last_logged_gas = total_executed_gas;
368                    last_logged_time = Instant::now();
369                }
370            }
371        }
372
373        if invalid_blocks.is_empty() {
374            info!(
375                start_block = min_block,
376                end_block = max_block,
377                %total_executed_blocks,
378                ?latest_executed_block,
379                throughput=?format_gas_throughput(total_executed_gas, instant.elapsed()),
380                "Re-executed successfully"
381            );
382        } else {
383            info!(
384                start_block = min_block,
385                end_block = max_block,
386                %total_executed_blocks,
387                ?latest_executed_block,
388                invalid_block_count = invalid_blocks.len(),
389                ?invalid_blocks,
390                throughput=?format_gas_throughput(total_executed_gas, instant.elapsed()),
391                "Re-executed with invalid blocks"
392            );
393        }
394
395        Ok(())
396    }
397}
398
399/// Verifies reverts against database changesets.
400///
401/// For each block, reverts must match changeset entries exactly. No extra slots/accounts
402/// in reverts for non-destroyed accounts. Destroyed accounts may have extra changeset slots
403/// (from DB storage wipe) absent from reverts.
404fn verify_bundle_against_changesets<P>(
405    provider: &P,
406    bundle: &BundleState,
407    last_block: u64,
408) -> eyre::Result<()>
409where
410    P: ChangeSetReader + StorageChangeSetReader,
411{
412    // Verify reverts against changesets per block
413    for (i, block_reverts) in bundle.reverts.iter().rev().enumerate() {
414        let block_number = last_block - i as u64;
415
416        let mut cs_accounts: HashMap<Address, Option<Account>> = provider
417            .account_block_changeset(block_number)?
418            .into_iter()
419            .map(|cs| (cs.address, cs.info))
420            .collect();
421
422        let mut cs_storage: HashMap<Address, HashMap<B256, U256>> = HashMap::new();
423        for (bna, entry) in provider.storage_changeset(block_number)? {
424            cs_storage.entry(bna.address()).or_default().insert(entry.key, entry.value);
425        }
426
427        for (addr, revert) in block_reverts {
428            // Verify account info
429            match &revert.account {
430                AccountInfoRevert::DoNothing => {
431                    eyre::ensure!(
432                        !cs_accounts.contains_key(addr),
433                        "Block {block_number}: account {addr} in changeset but revert is DoNothing",
434                    );
435                }
436                AccountInfoRevert::DeleteIt => {
437                    let cs_info = cs_accounts.remove(addr).ok_or_else(|| {
438                        eyre::eyre!("Block {block_number}: account {addr} revert is DeleteIt but not in changeset")
439                    })?;
440                    eyre::ensure!(
441                        cs_info.is_none(),
442                        "Block {block_number}: account {addr} revert is DeleteIt but changeset has {cs_info:?}",
443                    );
444                }
445                AccountInfoRevert::RevertTo(info) => {
446                    let cs_info = cs_accounts.remove(addr).ok_or_else(|| {
447                        eyre::eyre!("Block {block_number}: account {addr} revert is RevertTo but not in changeset")
448                    })?;
449                    let revert_acct = Some(Account::from(info));
450                    eyre::ensure!(
451                        revert_acct == cs_info,
452                        "Block {block_number}: account {addr} info mismatch: revert={revert_acct:?} cs={cs_info:?}",
453                    );
454                }
455            }
456
457            // Verify storage slots — remove matched changeset entries as we go
458            let mut cs_slots = cs_storage.get_mut(addr);
459            for (slot_key, revert_slot) in &revert.storage {
460                let b256_key = B256::from(*slot_key);
461                let cs_value = cs_slots.as_mut().and_then(|s| s.remove(&b256_key));
462                match (revert_slot, cs_value) {
463                    // When a contract is selfdestructed and re-created at the same address
464                    // within the same block, revm marks slots touched by the new contract
465                    // as `Destroyed` and never reads the original DB value, so
466                    // `to_previous_value()` would resolve to zero, which might be wrong.
467                    (RevertToSlot::Destroyed, _) => {}
468                    (RevertToSlot::Some(prev), Some(cs_value)) => eyre::ensure!(
469                        *prev == cs_value,
470                        "Block {block_number}: {addr} slot {b256_key} mismatch: \
471                         revert={prev} cs={cs_value}",
472                    ),
473                    (RevertToSlot::Some(_), None) => eyre::ensure!(
474                        revert.wipe_storage,
475                        "Block {block_number}: {addr} slot {b256_key} in reverts but not in changeset",
476                    ),
477                }
478            }
479
480            // Any remaining cs_storage slots for this address must be from a destroyed account
481            if let Some(remaining) = cs_slots.filter(|s| !s.is_empty()) {
482                eyre::ensure!(
483                    revert.wipe_storage,
484                    "Block {block_number}: {addr} has {} unmatched storage slots in changeset",
485                    remaining.len(),
486                );
487            }
488        }
489
490        // Any remaining cs_accounts entries had no corresponding revert
491        if let Some(addr) = cs_accounts.keys().next() {
492            eyre::bail!("Block {block_number}: account {addr} in changeset but not in reverts");
493        }
494    }
495
496    Ok(())
497}