Skip to main content

reth_stages/stages/execution/
mod.rs

1use crate::stages::MERKLE_STAGE_DEFAULT_INCREMENTAL_THRESHOLD;
2use alloy_consensus::BlockHeader;
3use alloy_eip7928::bal::Bal;
4use alloy_primitives::BlockNumber;
5use num_traits::Zero;
6use reth_chainspec::{ChainSpecProvider, EthereumHardforks};
7use reth_config::config::ExecutionConfig;
8use reth_consensus::FullConsensus;
9use reth_db::{static_file::HeaderMask, tables};
10use reth_evm::{execute::Executor, metrics::ExecutorMetrics, ConfigureEvm};
11use reth_execution_types::Chain;
12use reth_exex::{ExExManagerHandle, ExExNotification, ExExNotificationSource};
13use reth_primitives_traits::{format_gas_throughput, BlockBody, NodePrimitives};
14use reth_provider::{
15    providers::{StaticFileProvider, StaticFileWriter},
16    BlockReader, DBProvider, EitherWriter, ExecutionOutcome, HashedPostStateProvider,
17    HeaderProvider, LatestStateProviderRef, OriginalValuesKnown, ProviderError, StateProvider,
18    StateWriteConfig, StateWriter, StaticFileProviderFactory, StatsReader, StoragePath,
19    StorageSettingsCache, TransactionVariant,
20};
21use reth_revm::database::StateProviderDatabase;
22use reth_stages_api::{
23    BlockErrorKind, CheckpointBlockRange, EntitiesCheckpoint, ExecInput, ExecOutput,
24    ExecutionCheckpoint, ExecutionStageThresholds, Stage, StageCheckpoint, StageError, StageId,
25    UnwindInput, UnwindOutput,
26};
27use reth_static_file_types::StaticFileSegment;
28use std::{
29    cmp::{max, Ordering},
30    collections::BTreeMap,
31    ops::RangeInclusive,
32    sync::Arc,
33    task::{ready, Context, Poll},
34    time::{Duration, Instant},
35};
36use tracing::*;
37
38use super::missing_static_data_error;
39
40/// Slot-preimage database for recovering plain storage keys from hashed keys during
41/// pre-Cancun `SELFDESTRUCT` handling.
42pub mod slot_preimages;
43
44/// The execution stage executes all transactions and
45/// update history indexes.
46///
47/// Input tables:
48/// - [`tables::CanonicalHeaders`] get next block to execute.
49/// - [`tables::Headers`] get for revm environment variables.
50/// - [`tables::BlockBodyIndices`] to get tx number
51/// - [`tables::Transactions`] to execute
52///
53/// For state access [`LatestStateProviderRef`] provides us latest state and history state
54/// For latest most recent state [`LatestStateProviderRef`] would need (Used for execution Stage):
55/// - [`tables::PlainAccountState`]
56/// - [`tables::Bytecodes`]
57/// - [`tables::PlainStorageState`]
58///
59/// Tables updated after state finishes execution:
60/// - [`tables::PlainAccountState`]
61/// - [`tables::PlainStorageState`]
62/// - [`tables::Bytecodes`]
63/// - [`tables::AccountChangeSets`]
64/// - [`tables::StorageChangeSets`]
65///
66/// For unwinds we are accessing:
67/// - [`tables::BlockBodyIndices`] get tx index to know what needs to be unwinded
68/// - [`tables::AccountsHistory`] to remove change set and apply old values to
69/// - [`tables::PlainAccountState`] [`tables::StoragesHistory`] to remove change set and apply old
70///   values to [`tables::PlainStorageState`]
71// false positive, we cannot derive it if !DB: Debug.
72#[derive(Debug)]
73pub struct ExecutionStage<E>
74where
75    E: ConfigureEvm,
76{
77    /// The stage's internal block executor
78    evm_config: E,
79    /// The consensus instance for validating blocks.
80    consensus: Arc<dyn FullConsensus<E::Primitives>>,
81    /// The commit thresholds of the execution stage.
82    thresholds: ExecutionStageThresholds,
83    /// The highest threshold (in number of blocks) for switching between incremental
84    /// and full calculations across [`super::MerkleStage`], [`super::AccountHashingStage`] and
85    /// [`super::StorageHashingStage`]. This is required to figure out if can prune or not
86    /// changesets on subsequent pipeline runs.
87    external_clean_threshold: u64,
88    /// Input for the post execute commit hook.
89    /// Set after every [`ExecutionStage::execute`] and cleared after
90    /// [`ExecutionStage::post_execute_commit`].
91    post_execute_commit_input: Option<Chain<E::Primitives>>,
92    /// Input for the post unwind commit hook.
93    /// Set after every [`ExecutionStage::unwind`] and cleared after
94    /// [`ExecutionStage::post_unwind_commit`].
95    post_unwind_commit_input: Option<Chain<E::Primitives>>,
96    /// Handle to communicate with `ExEx` manager.
97    exex_manager_handle: ExExManagerHandle<E::Primitives>,
98    /// Executor metrics.
99    metrics: ExecutorMetrics,
100}
101
102impl<E> ExecutionStage<E>
103where
104    E: ConfigureEvm,
105{
106    /// Create new execution stage with specified config.
107    pub fn new(
108        evm_config: E,
109        consensus: Arc<dyn FullConsensus<E::Primitives>>,
110        thresholds: ExecutionStageThresholds,
111        external_clean_threshold: u64,
112        exex_manager_handle: ExExManagerHandle<E::Primitives>,
113    ) -> Self {
114        Self {
115            external_clean_threshold,
116            evm_config,
117            consensus,
118            thresholds,
119            post_execute_commit_input: None,
120            post_unwind_commit_input: None,
121            exex_manager_handle,
122            metrics: ExecutorMetrics::default(),
123        }
124    }
125
126    /// Create an execution stage with the provided executor.
127    ///
128    /// The commit threshold will be set to [`MERKLE_STAGE_DEFAULT_INCREMENTAL_THRESHOLD`].
129    pub fn new_with_executor(
130        evm_config: E,
131        consensus: Arc<dyn FullConsensus<E::Primitives>>,
132    ) -> Self {
133        Self::new(
134            evm_config,
135            consensus,
136            ExecutionStageThresholds::default(),
137            MERKLE_STAGE_DEFAULT_INCREMENTAL_THRESHOLD,
138            ExExManagerHandle::empty(),
139        )
140    }
141
142    /// Create new instance of [`ExecutionStage`] from configuration.
143    pub fn from_config(
144        evm_config: E,
145        consensus: Arc<dyn FullConsensus<E::Primitives>>,
146        config: ExecutionConfig,
147        external_clean_threshold: u64,
148    ) -> Self {
149        Self::new(
150            evm_config,
151            consensus,
152            config.into(),
153            external_clean_threshold,
154            ExExManagerHandle::empty(),
155        )
156    }
157
158    /// Returns whether we can perform pruning of [`tables::AccountChangeSets`] and
159    /// [`tables::StorageChangeSets`].
160    ///
161    /// This function verifies whether the [`super::MerkleStage`] or Hashing stages will run from
162    /// scratch. If at least one stage isn't starting anew, it implies that pruning of
163    /// changesets cannot occur. This is determined by checking the highest clean threshold
164    /// (`self.external_clean_threshold`) across the stages.
165    ///
166    /// Given that `start_block` changes with each checkpoint, it's necessary to inspect
167    /// [`tables::AccountsTrie`] to ensure that [`super::MerkleStage`] hasn't
168    /// been previously executed.
169    fn can_prune_changesets(
170        &self,
171        provider: impl StatsReader,
172        start_block: u64,
173        max_block: u64,
174    ) -> Result<bool, StageError> {
175        // We can only prune changesets if we're not executing MerkleStage from scratch (by
176        // threshold or first-sync)
177        Ok(max_block - start_block > self.external_clean_threshold ||
178            provider.count_entries::<tables::AccountsTrie>()?.is_zero())
179    }
180
181    /// Performs consistency check on static files.
182    ///
183    /// This function compares the highest receipt number recorded in the database with that in the
184    /// static file to detect any discrepancies due to unexpected shutdowns or database rollbacks.
185    /// **If the height in the static file is higher**, it rolls back (unwinds) the static file.
186    /// **Conversely, if the height in the database is lower**, it triggers a rollback in the
187    /// database (by returning [`StageError`]) until the heights in both the database and static
188    /// file match.
189    fn ensure_consistency<Provider>(
190        &self,
191        provider: &Provider,
192        checkpoint: u64,
193        unwind_to: Option<u64>,
194    ) -> Result<(), StageError>
195    where
196        Provider: StaticFileProviderFactory + DBProvider + BlockReader + StorageSettingsCache,
197    {
198        // On old nodes, if there's any receipts pruning configured, receipts are written directly
199        // to database and inconsistencies are expected.
200        if EitherWriter::receipts_destination(provider).is_database() {
201            return Ok(())
202        }
203
204        // Get next expected receipt number
205        let next_receipt_num =
206            provider.block_body_indices(checkpoint)?.map(|b| b.next_tx_num()).unwrap_or(0);
207
208        let static_file_provider = provider.static_file_provider();
209
210        // Get next expected receipt number in static files
211        let next_static_file_receipt_num = static_file_provider
212            .get_highest_static_file_tx(StaticFileSegment::Receipts)
213            .map(|num| num + 1)
214            .unwrap_or(0);
215
216        // Get highest block number in static files for receipts
217        let static_file_block_num = static_file_provider
218            .get_highest_static_file_block(StaticFileSegment::Receipts)
219            .unwrap_or(0);
220
221        // Check if we had any unexpected shutdown after committing to static files, but
222        // NOT committing to database.
223        match static_file_block_num.cmp(&checkpoint) {
224            // It can be equal when it's a chain of empty blocks, but we still need to update the
225            // last block in the range.
226            Ordering::Greater | Ordering::Equal => {
227                let mut static_file_producer =
228                    static_file_provider.latest_writer(StaticFileSegment::Receipts)?;
229                static_file_producer.prune_receipts(
230                    next_static_file_receipt_num.saturating_sub(next_receipt_num),
231                    checkpoint,
232                )?;
233                // Since this is a database <-> static file inconsistency, we commit the change
234                // straight away.
235                static_file_producer.commit()?;
236            }
237            Ordering::Less => {
238                // If we are already in the process of unwind, this might be fine because we will
239                // fix the inconsistency right away.
240                if let Some(unwind_to) = unwind_to &&
241                    unwind_to <= static_file_block_num
242                {
243                    return Ok(())
244                }
245
246                // Otherwise, this is a real inconsistency - database has more blocks than static
247                // files
248                return Err(missing_static_data_error(
249                    next_static_file_receipt_num.saturating_sub(1),
250                    &static_file_provider,
251                    provider,
252                    StaticFileSegment::Receipts,
253                )?)
254            }
255        }
256
257        Ok(())
258    }
259}
260
261impl<E, Provider> Stage<Provider> for ExecutionStage<E>
262where
263    E: ConfigureEvm,
264    Provider: DBProvider
265        + BlockReader<
266            Block = <E::Primitives as NodePrimitives>::Block,
267            Header = <E::Primitives as NodePrimitives>::BlockHeader,
268        > + StaticFileProviderFactory<
269            Primitives: NodePrimitives<BlockHeader: reth_db_api::table::Value>,
270        > + StatsReader
271        + StateWriter<Receipt = <E::Primitives as NodePrimitives>::Receipt>
272        + StorageSettingsCache
273        + StoragePath
274        + ChainSpecProvider<ChainSpec: EthereumHardforks>,
275{
276    /// Return the id of the stage
277    fn id(&self) -> StageId {
278        StageId::Execution
279    }
280
281    fn poll_execute_ready(
282        &mut self,
283        cx: &mut Context<'_>,
284        _: ExecInput,
285    ) -> Poll<Result<(), StageError>> {
286        ready!(self.exex_manager_handle.poll_ready(cx));
287
288        Poll::Ready(Ok(()))
289    }
290
291    /// Execute the stage
292    fn execute(&mut self, provider: &Provider, input: ExecInput) -> Result<ExecOutput, StageError> {
293        if input.target_reached() {
294            return Ok(ExecOutput::done(input.checkpoint()))
295        }
296
297        let start_block = input.next_block();
298        let max_block = input.target();
299        let static_file_provider = provider.static_file_provider();
300
301        self.ensure_consistency(provider, input.checkpoint().block_number, None)?;
302
303        let db =
304            StateProviderDatabase(LatestStateProviderRef::new(provider).into_evm_state_provider());
305        let mut executor = self.evm_config.batch_executor(db);
306
307        // Progress tracking
308        let mut stage_progress = start_block;
309        let mut stage_checkpoint = execution_checkpoint(
310            &static_file_provider,
311            start_block,
312            max_block,
313            input.checkpoint(),
314        )?;
315
316        let mut fetch_block_duration = Duration::default();
317        let mut execution_duration = Duration::default();
318
319        let mut last_block = start_block;
320        let mut last_execution_duration = Duration::default();
321        let mut last_cumulative_gas = 0;
322        let mut last_log_instant = Instant::now();
323        let log_duration = Duration::from_secs(10);
324
325        debug!(target: "sync::stages::execution", start = start_block, end = max_block, "Executing range");
326
327        // Execute block range
328        let mut cumulative_gas = 0;
329        let batch_start = Instant::now();
330
331        let mut blocks = Vec::new();
332        let mut results = Vec::new();
333        // Reused across blocks for BAL hash encoding.
334        let mut bal_buf = Vec::new();
335        for block_number in start_block..=max_block {
336            // Fetch the block
337            let fetch_block_start = Instant::now();
338
339            // we need the block's transactions but we don't need the transaction hashes
340            let block = provider
341                .recovered_block(block_number.into(), TransactionVariant::NoHash)?
342                .ok_or_else(|| ProviderError::HeaderNotFound(block_number.into()))?;
343
344            fetch_block_duration += fetch_block_start.elapsed();
345
346            cumulative_gas += block.header().gas_used();
347
348            // Configure the executor to use the current state.
349            trace!(target: "sync::stages::execution", number = block_number, txs = block.body().transactions().len(), "Executing block");
350
351            // Execute the block
352            let execute_start = Instant::now();
353
354            let result = self.metrics.metered_one(&block, |input| {
355                executor.execute_one(input).map_err(|error| StageError::Block {
356                    block: Box::new(block.block_with_parent()),
357                    error: BlockErrorKind::Execution(error),
358                })
359            })?;
360
361            let built_bal = executor.take_bal().map(Bal::from);
362            if let Some(bal) = &built_bal &&
363                let Err(err) = bal.validate_gas_limit(block.header().gas_limit())
364            {
365                return Err(StageError::Block {
366                    block: Box::new(block.block_with_parent()),
367                    error: BlockErrorKind::Validation(err.into()),
368                })
369            }
370            let bal_hash = built_bal.as_ref().map(|bal| bal.compute_hash_with_buf(&mut bal_buf));
371
372            if let Err(err) =
373                self.consensus.validate_block_post_execution(&block, &result, None, bal_hash)
374            {
375                return Err(StageError::Block {
376                    block: Box::new(block.block_with_parent()),
377                    error: BlockErrorKind::Validation(err),
378                })
379            }
380            results.push(result);
381
382            execution_duration += execute_start.elapsed();
383
384            // Log execution throughput
385            if last_log_instant.elapsed() >= log_duration {
386                info!(
387                    target: "sync::stages::execution",
388                    start = last_block,
389                    end = block_number,
390                    throughput = format_gas_throughput(cumulative_gas - last_cumulative_gas, execution_duration - last_execution_duration),
391                    "Executed block range"
392                );
393
394                last_block = block_number + 1;
395                last_execution_duration = execution_duration;
396                last_cumulative_gas = cumulative_gas;
397                last_log_instant = Instant::now();
398            }
399
400            stage_progress = block_number;
401            stage_checkpoint.progress.processed += block.header().gas_used();
402
403            // If we have ExExes we need to save the block in memory for later
404            if self.exex_manager_handle.has_exexs() {
405                blocks.push(block);
406            }
407
408            // Check if we should commit now
409            if self.thresholds.is_end_of_batch(
410                block_number - start_block,
411                executor.size_hint() as u64,
412                cumulative_gas,
413                batch_start.elapsed(),
414            ) {
415                break
416            }
417        }
418
419        // prepare execution output for writing
420        let time = Instant::now();
421        let mut state = ExecutionOutcome::from_blocks(
422            start_block,
423            executor.into_state().take_bundle(),
424            results,
425        );
426        let write_preparation_duration = time.elapsed();
427
428        // log the gas per second for the range we just executed
429        debug!(
430            target: "sync::stages::execution",
431            start = start_block,
432            end = stage_progress,
433            throughput = format_gas_throughput(cumulative_gas, execution_duration),
434            "Finished executing block range"
435        );
436
437        // The ExEx notification must carry the outcome as executed, so a copy is only taken if the
438        // state is modified below before it is written.
439        let mut exex_state = None;
440
441        let time = Instant::now();
442
443        if self.can_prune_changesets(provider, start_block, max_block)? {
444            let prune_modes = provider.prune_modes_ref();
445
446            if !blocks.is_empty() &&
447                prune_modes.account_history.is_some() &&
448                prune_modes.storage_history.is_some()
449            {
450                exex_state = Some(state.clone());
451            }
452
453            // Iterate over all reverts and clear them if pruning is configured.
454            for block_number in start_block..=max_block {
455                let Some(reverts) =
456                    state.bundle.reverts.get_mut((block_number - start_block) as usize)
457                else {
458                    break
459                };
460
461                // If both account history and storage history pruning is configured, clear reverts
462                // for this block.
463                if prune_modes
464                    .account_history
465                    .is_some_and(|m| m.should_prune(block_number, max_block)) &&
466                    prune_modes
467                        .storage_history
468                        .is_some_and(|m| m.should_prune(block_number, max_block))
469                {
470                    reverts.clear();
471                }
472            }
473        }
474
475        // When using hashed state (storage.v2), inject plain storage-slot keys into wipe
476        // reverts for self-destructed accounts. Without this, the changeset writer would only
477        // see hashed slot keys (from `HashedStorages`) which pollutes the entire codebase.
478        //
479        // SELFDESTRUCT no longer destroys storage post-Cancun, so this is only needed for
480        // pre-Cancun blocks. Post-Cancun we can remove the preimage db entirely.
481        if provider.cached_storage_settings().use_hashed_state() {
482            let start_header = provider
483                .header_by_number(start_block)?
484                .ok_or_else(|| ProviderError::HeaderNotFound(start_block.into()))?;
485
486            let path = provider.storage_path().join("preimage");
487            if !provider.chain_spec().is_cancun_active_at_timestamp(start_header.timestamp()) {
488                if !blocks.is_empty() && exex_state.is_none() {
489                    exex_state = Some(state.clone());
490                }
491                slot_preimages::inject_plain_wipe_slots(&path, provider, &mut state)?;
492            } else if path.exists() {
493                // Post-Cancun: no more self-destructs, preimage db is no longer needed.
494                let _ = std::fs::remove_dir_all(&path);
495            }
496        }
497
498        // Write output. When `use_hashed_state` is enabled, `write_state` skips writing to
499        // plain account/storage tables and only writes bytecodes and changesets. The hashed
500        // state is then written separately below.
501        provider.write_state(&state, OriginalValuesKnown::Yes, StateWriteConfig::default())?;
502
503        if provider.cached_storage_settings().use_hashed_state() {
504            let hashed_state =
505                LatestStateProviderRef::new(provider).hashed_post_state(&state.bundle)?;
506            provider.write_hashed_state(&hashed_state.into_sorted())?;
507        }
508
509        let db_write_duration = time.elapsed();
510        debug!(
511            target: "sync::stages::execution",
512            block_fetch = ?fetch_block_duration,
513            execution = ?execution_duration,
514            write_preparation = ?write_preparation_duration,
515            write = ?db_write_duration,
516            "Execution time"
517        );
518
519        // Prepare the input for post execute commit hook, where an `ExExNotification` will be sent.
520        //
521        // Note: Since we only write to `blocks` if there are any ExExes, we don't need to perform
522        // the `has_exexs` check here as well
523        if !blocks.is_empty() {
524            let previous_input = self.post_execute_commit_input.replace(Chain::new(
525                blocks,
526                exex_state.unwrap_or(state),
527                BTreeMap::new(),
528            ));
529
530            if previous_input.is_some() {
531                // Not processing the previous post execute commit input is a critical error, as it
532                // means that we didn't send the notification to ExExes
533                return Err(StageError::PostExecuteCommit(
534                    "Previous post execute commit input wasn't processed",
535                ))
536            }
537        }
538
539        let done = stage_progress == max_block;
540        Ok(ExecOutput {
541            checkpoint: StageCheckpoint::new(stage_progress)
542                .with_execution_stage_checkpoint(stage_checkpoint),
543            done,
544        })
545    }
546
547    fn post_execute_commit(&mut self) -> Result<(), StageError> {
548        let Some(chain) = self.post_execute_commit_input.take() else { return Ok(()) };
549
550        // NOTE: We can ignore the error here, since an error means that the channel is closed,
551        // which means the manager has died, which then in turn means the node is shutting down.
552        let _ = self.exex_manager_handle.send(
553            ExExNotificationSource::Pipeline,
554            ExExNotification::ChainCommitted { new: Arc::new(chain) },
555        );
556
557        Ok(())
558    }
559
560    /// Unwind the stage.
561    fn unwind(
562        &mut self,
563        provider: &Provider,
564        input: UnwindInput,
565    ) -> Result<UnwindOutput, StageError> {
566        let (range, unwind_to, _) =
567            input.unwind_block_range_with_threshold(self.thresholds.max_blocks.unwrap_or(u64::MAX));
568        if range.is_empty() {
569            return Ok(UnwindOutput {
570                checkpoint: input.checkpoint.with_block_number(input.unwind_to),
571            })
572        }
573
574        reject_cancun_boundary_unwind(provider, input.checkpoint.block_number, unwind_to)?;
575
576        self.ensure_consistency(provider, input.checkpoint.block_number, Some(unwind_to))?;
577
578        // Unwind account and storage changesets, as well as receipts.
579        //
580        // This also updates `PlainStorageState` and `PlainAccountState`.
581        let bundle_state_with_receipts = provider.take_state_above(unwind_to)?;
582
583        // Prepare the input for post unwind commit hook, where an `ExExNotification` will be sent.
584        if self.exex_manager_handle.has_exexs() {
585            // Get the blocks for the unwound range.
586            let blocks = provider.recovered_block_range(range.clone())?;
587            let previous_input = self.post_unwind_commit_input.replace(Chain::new(
588                blocks,
589                bundle_state_with_receipts,
590                BTreeMap::new(),
591            ));
592
593            debug_assert!(
594                previous_input.is_none(),
595                "Previous post unwind commit input wasn't processed"
596            );
597            if let Some(previous_input) = previous_input {
598                tracing::debug!(target: "sync::stages::execution", ?previous_input, "Previous post unwind commit input wasn't processed");
599            }
600        }
601
602        // Update the checkpoint.
603        let mut stage_checkpoint = input.checkpoint.execution_stage_checkpoint();
604        if let Some(stage_checkpoint) = stage_checkpoint.as_mut() {
605            for block_number in range {
606                stage_checkpoint.progress.processed -= provider
607                    .header_by_number(block_number)?
608                    .ok_or_else(|| ProviderError::HeaderNotFound(block_number.into()))?
609                    .gas_used();
610            }
611        }
612        let checkpoint = if let Some(stage_checkpoint) = stage_checkpoint {
613            StageCheckpoint::new(unwind_to).with_execution_stage_checkpoint(stage_checkpoint)
614        } else {
615            StageCheckpoint::new(unwind_to)
616        };
617
618        Ok(UnwindOutput { checkpoint })
619    }
620
621    fn post_unwind_commit(&mut self) -> Result<(), StageError> {
622        let Some(chain) = self.post_unwind_commit_input.take() else { return Ok(()) };
623
624        // NOTE: We can ignore the error here, since an error means that the channel is closed,
625        // which means the manager has died, which then in turn means the node is shutting down.
626        let _ = self.exex_manager_handle.send(
627            ExExNotificationSource::Pipeline,
628            ExExNotification::ChainReverted { old: Arc::new(chain) },
629        );
630
631        Ok(())
632    }
633}
634
635fn reject_cancun_boundary_unwind<Provider>(
636    provider: &Provider,
637    checkpoint_block: u64,
638    unwind_to: u64,
639) -> Result<(), StageError>
640where
641    Provider: HeaderProvider + ChainSpecProvider<ChainSpec: EthereumHardforks>,
642{
643    let checkpoint_header = provider
644        .header_by_number(checkpoint_block)?
645        .ok_or_else(|| ProviderError::HeaderNotFound(checkpoint_block.into()))?;
646    let unwind_to_header = provider
647        .header_by_number(unwind_to)?
648        .ok_or_else(|| ProviderError::HeaderNotFound(unwind_to.into()))?;
649    let checkpoint_is_cancun =
650        provider.chain_spec().is_cancun_active_at_timestamp(checkpoint_header.timestamp());
651    let unwind_to_is_cancun =
652        provider.chain_spec().is_cancun_active_at_timestamp(unwind_to_header.timestamp());
653    if checkpoint_is_cancun && !unwind_to_is_cancun {
654        return Err(StageError::Fatal(
655            std::io::Error::other(format!(
656                "execution unwind across Cancun activation boundary is not allowed: checkpoint \
657                 block #{checkpoint_block} (ts={}) is Cancun-active but unwind target \
658                 #{unwind_to} (ts={}) is pre-Cancun",
659                checkpoint_header.timestamp(),
660                unwind_to_header.timestamp()
661            ))
662            .into(),
663        ))
664    }
665
666    Ok(())
667}
668
669fn execution_checkpoint<N>(
670    provider: &StaticFileProvider<N>,
671    start_block: BlockNumber,
672    max_block: BlockNumber,
673    checkpoint: StageCheckpoint,
674) -> Result<ExecutionCheckpoint, ProviderError>
675where
676    N: NodePrimitives<BlockHeader: reth_db_api::table::Value>,
677{
678    Ok(match checkpoint.execution_stage_checkpoint() {
679        // If checkpoint block range fully matches our range,
680        // we take the previously used stage checkpoint as-is.
681        Some(stage_checkpoint @ ExecutionCheckpoint { block_range, .. })
682            if block_range == CheckpointBlockRange::from(start_block..=max_block) =>
683        {
684            stage_checkpoint
685        }
686        // If checkpoint block range precedes our range seamlessly, we take the previously used
687        // stage checkpoint and add the amount of gas from our range to the checkpoint total.
688        Some(ExecutionCheckpoint {
689            block_range: CheckpointBlockRange { to, .. },
690            progress: EntitiesCheckpoint { processed, total },
691        }) if to == start_block - 1 => ExecutionCheckpoint {
692            block_range: CheckpointBlockRange { from: start_block, to: max_block },
693            progress: EntitiesCheckpoint {
694                processed,
695                total: total + calculate_gas_used_from_headers(provider, start_block..=max_block)?,
696            },
697        },
698        // If checkpoint block range ends on the same block as our range, we take the previously
699        // used stage checkpoint.
700        Some(ExecutionCheckpoint { block_range: CheckpointBlockRange { to, .. }, progress })
701            if to == max_block =>
702        {
703            ExecutionCheckpoint {
704                block_range: CheckpointBlockRange { from: start_block, to: max_block },
705                progress,
706            }
707        }
708        // If there's any other non-empty checkpoint, we calculate the remaining amount of total gas
709        // to be processed not including the checkpoint range.
710        Some(ExecutionCheckpoint { progress: EntitiesCheckpoint { processed, .. }, .. }) => {
711            let after_checkpoint_block_number =
712                calculate_gas_used_from_headers(provider, checkpoint.block_number + 1..=max_block)?;
713
714            ExecutionCheckpoint {
715                block_range: CheckpointBlockRange { from: start_block, to: max_block },
716                progress: EntitiesCheckpoint {
717                    processed,
718                    total: processed + after_checkpoint_block_number,
719                },
720            }
721        }
722        // Otherwise, we recalculate the whole stage checkpoint including the amount of gas
723        // already processed, if there's any.
724        _ => {
725            let genesis_block_number = provider.genesis_block_number();
726            let processed = calculate_gas_used_from_headers(
727                provider,
728                genesis_block_number..=max(start_block - 1, genesis_block_number),
729            )?;
730
731            ExecutionCheckpoint {
732                block_range: CheckpointBlockRange { from: start_block, to: max_block },
733                progress: EntitiesCheckpoint {
734                    processed,
735                    total: processed +
736                        calculate_gas_used_from_headers(provider, start_block..=max_block)?,
737                },
738            }
739        }
740    })
741}
742
743/// Calculates the total amount of gas used from the headers in the given range.
744pub fn calculate_gas_used_from_headers<N>(
745    provider: &StaticFileProvider<N>,
746    range: RangeInclusive<BlockNumber>,
747) -> Result<u64, ProviderError>
748where
749    N: NodePrimitives<BlockHeader: reth_db_api::table::Value>,
750{
751    debug!(target: "sync::stages::execution", ?range, "Calculating gas used from headers");
752
753    let mut gas_total = 0;
754
755    let start = Instant::now();
756
757    for entry in provider.fetch_range_iter(
758        StaticFileSegment::Headers,
759        *range.start()..*range.end() + 1,
760        |cursor, number| cursor.get_one::<HeaderMask<N::BlockHeader>>(number.into()),
761    )? {
762        if let Some(entry) = entry? {
763            gas_total += entry.gas_used();
764        }
765    }
766
767    let duration = start.elapsed();
768    debug!(target: "sync::stages::execution", ?range, ?duration, "Finished calculating gas used from headers");
769
770    Ok(gas_total)
771}
772
773#[cfg(test)]
774mod tests {
775    use super::*;
776    use crate::{stages::MERKLE_STAGE_DEFAULT_REBUILD_THRESHOLD, test_utils::TestStageDB};
777    use alloy_primitives::{address, hex_literal::hex, keccak256, Address, B256, U256};
778    use alloy_rlp::Decodable;
779    use assert_matches::assert_matches;
780    use reth_chainspec::{ChainSpecBuilder, EthereumHardfork, ForkCondition};
781    use reth_db_api::{
782        models::{metadata::StorageSettings, AccountBeforeTx},
783        transaction::{DbTx, DbTxMut},
784    };
785    use reth_ethereum_consensus::EthBeaconConsensus;
786    use reth_ethereum_primitives::Block;
787    use reth_evm_ethereum::EthEvmConfig;
788    use reth_primitives_traits::{Account, Block as _, Bytecode, SealedBlock, StorageEntry};
789    use reth_provider::{
790        test_utils::{create_test_provider_factory, create_test_provider_factory_with_chain_spec},
791        AccountReader, BlockWriter, DatabaseProviderFactory, ReceiptProvider,
792        StaticFileProviderFactory,
793    };
794    use reth_prune::PruneModes;
795    use reth_prune_types::{PruneMode, ReceiptsLogPruneConfig};
796    use reth_revm::revm::database::{AccountStatus, BundleAccount};
797    use reth_stages_api::StageUnitCheckpoint;
798    use reth_testing_utils::generators;
799    use std::collections::BTreeMap;
800
801    fn stage() -> ExecutionStage<EthEvmConfig> {
802        let evm_config =
803            EthEvmConfig::new(Arc::new(ChainSpecBuilder::mainnet().berlin_activated().build()));
804        let consensus = Arc::new(EthBeaconConsensus::new(Arc::new(
805            ChainSpecBuilder::mainnet().berlin_activated().build(),
806        )));
807        ExecutionStage::new(
808            evm_config,
809            consensus,
810            ExecutionStageThresholds {
811                max_blocks: Some(100),
812                max_changes: None,
813                max_cumulative_gas: None,
814                max_duration: None,
815            },
816            MERKLE_STAGE_DEFAULT_REBUILD_THRESHOLD,
817            ExExManagerHandle::empty(),
818        )
819    }
820
821    #[test]
822    fn destroyed_storage_is_materialized_without_reverts() {
823        let factory = create_test_provider_factory();
824        let provider = factory.database_provider_rw().unwrap();
825        let address = Address::repeat_byte(0x11);
826        let hashed_address = keccak256(address);
827        let first_slot = B256::repeat_byte(0x22);
828        let second_slot = B256::repeat_byte(0x33);
829
830        provider
831            .tx_ref()
832            .put::<tables::HashedStorages>(
833                hashed_address,
834                StorageEntry { key: first_slot, value: U256::from(2) },
835            )
836            .unwrap();
837        provider
838            .tx_ref()
839            .put::<tables::HashedStorages>(
840                hashed_address,
841                StorageEntry { key: second_slot, value: U256::from(3) },
842            )
843            .unwrap();
844
845        let mut state = ExecutionOutcome::<()>::default();
846        state.bundle.state.insert(
847            address,
848            BundleAccount::new(
849                Some(Default::default()),
850                None,
851                Default::default(),
852                AccountStatus::Destroyed,
853            ),
854        );
855
856        let hashed_state = provider.latest().hashed_post_state(&state.bundle).unwrap();
857
858        let storage = &hashed_state.storages[&hashed_address];
859        assert_eq!(storage.storage[&first_slot], U256::ZERO);
860        assert_eq!(storage.storage[&second_slot], U256::ZERO);
861        assert!(state.bundle.reverts.is_empty());
862    }
863
864    #[test]
865    fn execution_checkpoint_matches() {
866        let factory = create_test_provider_factory();
867
868        let previous_stage_checkpoint = ExecutionCheckpoint {
869            block_range: CheckpointBlockRange { from: 0, to: 0 },
870            progress: EntitiesCheckpoint { processed: 1, total: 2 },
871        };
872        let previous_checkpoint = StageCheckpoint {
873            block_number: 0,
874            stage_checkpoint: Some(StageUnitCheckpoint::Execution(previous_stage_checkpoint)),
875        };
876
877        let stage_checkpoint = execution_checkpoint(
878            &factory.static_file_provider(),
879            previous_stage_checkpoint.block_range.from,
880            previous_stage_checkpoint.block_range.to,
881            previous_checkpoint,
882        );
883
884        assert!(
885            matches!(stage_checkpoint, Ok(checkpoint) if checkpoint == previous_stage_checkpoint)
886        );
887    }
888
889    #[test]
890    fn execution_checkpoint_precedes() {
891        let factory = create_test_provider_factory();
892        let provider = factory.provider_rw().unwrap();
893
894        let mut genesis_rlp = hex!("f901faf901f5a00000000000000000000000000000000000000000000000000000000000000000a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347942adc25665018aa1fe0e6bc666dac8fc2697ff9baa045571b40ae66ca7480791bbb2887286e4e4c4b1b298b191c889d6959023a32eda056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421b901000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000083020000808502540be400808000a00000000000000000000000000000000000000000000000000000000000000000880000000000000000c0c0").as_slice();
895        let genesis = SealedBlock::<Block>::decode(&mut genesis_rlp).unwrap();
896        let mut block_rlp = hex!("f90262f901f9a075c371ba45999d87f4542326910a11af515897aebce5265d3f6acd1f1161f82fa01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347942adc25665018aa1fe0e6bc666dac8fc2697ff9baa098f2dcd87c8ae4083e7017a05456c14eea4b1db2032126e27b3b1563d57d7cc0a08151d548273f6683169524b66ca9fe338b9ce42bc3540046c828fd939ae23bcba03f4e5c2ec5b2170b711d97ee755c160457bb58d8daa338e835ec02ae6860bbabb901000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000083020000018502540be40082a8798203e800a00000000000000000000000000000000000000000000000000000000000000000880000000000000000f863f861800a8405f5e10094100000000000000000000000000000000000000080801ba07e09e26678ed4fac08a249ebe8ed680bf9051a5e14ad223e4b2b9d26e0208f37a05f6e3f188e3e6eab7d7d3b6568f5eac7d687b08d307d3154ccd8c87b4630509bc0").as_slice();
897        let block = SealedBlock::<Block>::decode(&mut block_rlp).unwrap();
898        provider.insert_block(&genesis.try_recover().unwrap()).unwrap();
899        provider.insert_block(&block.clone().try_recover().unwrap()).unwrap();
900        provider
901            .static_file_provider()
902            .latest_writer(StaticFileSegment::Headers)
903            .unwrap()
904            .commit()
905            .unwrap();
906        provider.commit().unwrap();
907
908        let previous_stage_checkpoint = ExecutionCheckpoint {
909            block_range: CheckpointBlockRange { from: 0, to: 0 },
910            progress: EntitiesCheckpoint { processed: 1, total: 1 },
911        };
912        let previous_checkpoint = StageCheckpoint {
913            block_number: 1,
914            stage_checkpoint: Some(StageUnitCheckpoint::Execution(previous_stage_checkpoint)),
915        };
916
917        let stage_checkpoint =
918            execution_checkpoint(&factory.static_file_provider(), 1, 1, previous_checkpoint);
919
920        assert_matches!(stage_checkpoint, Ok(ExecutionCheckpoint {
921            block_range: CheckpointBlockRange { from: 1, to: 1 },
922            progress: EntitiesCheckpoint {
923                processed,
924                total
925            }
926        }) if processed == previous_stage_checkpoint.progress.processed &&
927            total == previous_stage_checkpoint.progress.total + block.gas_used);
928    }
929
930    #[test]
931    fn execution_checkpoint_recalculate_full_previous_some() {
932        let factory = create_test_provider_factory();
933        let provider = factory.provider_rw().unwrap();
934
935        let mut genesis_rlp = hex!("f901faf901f5a00000000000000000000000000000000000000000000000000000000000000000a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347942adc25665018aa1fe0e6bc666dac8fc2697ff9baa045571b40ae66ca7480791bbb2887286e4e4c4b1b298b191c889d6959023a32eda056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421b901000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000083020000808502540be400808000a00000000000000000000000000000000000000000000000000000000000000000880000000000000000c0c0").as_slice();
936        let genesis = SealedBlock::<Block>::decode(&mut genesis_rlp).unwrap();
937        let mut block_rlp = hex!("f90262f901f9a075c371ba45999d87f4542326910a11af515897aebce5265d3f6acd1f1161f82fa01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347942adc25665018aa1fe0e6bc666dac8fc2697ff9baa098f2dcd87c8ae4083e7017a05456c14eea4b1db2032126e27b3b1563d57d7cc0a08151d548273f6683169524b66ca9fe338b9ce42bc3540046c828fd939ae23bcba03f4e5c2ec5b2170b711d97ee755c160457bb58d8daa338e835ec02ae6860bbabb901000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000083020000018502540be40082a8798203e800a00000000000000000000000000000000000000000000000000000000000000000880000000000000000f863f861800a8405f5e10094100000000000000000000000000000000000000080801ba07e09e26678ed4fac08a249ebe8ed680bf9051a5e14ad223e4b2b9d26e0208f37a05f6e3f188e3e6eab7d7d3b6568f5eac7d687b08d307d3154ccd8c87b4630509bc0").as_slice();
938        let block = SealedBlock::<Block>::decode(&mut block_rlp).unwrap();
939        provider.insert_block(&genesis.try_recover().unwrap()).unwrap();
940        provider.insert_block(&block.clone().try_recover().unwrap()).unwrap();
941        provider
942            .static_file_provider()
943            .latest_writer(StaticFileSegment::Headers)
944            .unwrap()
945            .commit()
946            .unwrap();
947        provider.commit().unwrap();
948
949        let previous_stage_checkpoint = ExecutionCheckpoint {
950            block_range: CheckpointBlockRange { from: 0, to: 0 },
951            progress: EntitiesCheckpoint { processed: 1, total: 1 },
952        };
953        let previous_checkpoint = StageCheckpoint {
954            block_number: 1,
955            stage_checkpoint: Some(StageUnitCheckpoint::Execution(previous_stage_checkpoint)),
956        };
957
958        let stage_checkpoint =
959            execution_checkpoint(&factory.static_file_provider(), 1, 1, previous_checkpoint);
960
961        assert_matches!(stage_checkpoint, Ok(ExecutionCheckpoint {
962            block_range: CheckpointBlockRange { from: 1, to: 1 },
963            progress: EntitiesCheckpoint {
964                processed,
965                total
966            }
967        }) if processed == previous_stage_checkpoint.progress.processed &&
968            total == previous_stage_checkpoint.progress.total + block.gas_used());
969    }
970
971    #[test]
972    fn execution_checkpoint_recalculate_full_previous_none() {
973        let factory = create_test_provider_factory();
974        let provider = factory.provider_rw().unwrap();
975
976        let mut genesis_rlp = hex!("f901faf901f5a00000000000000000000000000000000000000000000000000000000000000000a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347942adc25665018aa1fe0e6bc666dac8fc2697ff9baa045571b40ae66ca7480791bbb2887286e4e4c4b1b298b191c889d6959023a32eda056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421b901000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000083020000808502540be400808000a00000000000000000000000000000000000000000000000000000000000000000880000000000000000c0c0").as_slice();
977        let genesis = SealedBlock::<Block>::decode(&mut genesis_rlp).unwrap();
978        let mut block_rlp = hex!("f90262f901f9a075c371ba45999d87f4542326910a11af515897aebce5265d3f6acd1f1161f82fa01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347942adc25665018aa1fe0e6bc666dac8fc2697ff9baa098f2dcd87c8ae4083e7017a05456c14eea4b1db2032126e27b3b1563d57d7cc0a08151d548273f6683169524b66ca9fe338b9ce42bc3540046c828fd939ae23bcba03f4e5c2ec5b2170b711d97ee755c160457bb58d8daa338e835ec02ae6860bbabb901000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000083020000018502540be40082a8798203e800a00000000000000000000000000000000000000000000000000000000000000000880000000000000000f863f861800a8405f5e10094100000000000000000000000000000000000000080801ba07e09e26678ed4fac08a249ebe8ed680bf9051a5e14ad223e4b2b9d26e0208f37a05f6e3f188e3e6eab7d7d3b6568f5eac7d687b08d307d3154ccd8c87b4630509bc0").as_slice();
979        let block = SealedBlock::<Block>::decode(&mut block_rlp).unwrap();
980        provider.insert_block(&genesis.try_recover().unwrap()).unwrap();
981        provider.insert_block(&block.clone().try_recover().unwrap()).unwrap();
982        provider
983            .static_file_provider()
984            .latest_writer(StaticFileSegment::Headers)
985            .unwrap()
986            .commit()
987            .unwrap();
988        provider.commit().unwrap();
989
990        let previous_checkpoint = StageCheckpoint { block_number: 1, stage_checkpoint: None };
991
992        let stage_checkpoint =
993            execution_checkpoint(&factory.static_file_provider(), 1, 1, previous_checkpoint);
994
995        assert_matches!(stage_checkpoint, Ok(ExecutionCheckpoint {
996            block_range: CheckpointBlockRange { from: 1, to: 1 },
997            progress: EntitiesCheckpoint {
998                processed: 0,
999                total
1000            }
1001        }) if total == block.gas_used);
1002    }
1003
1004    #[tokio::test]
1005    async fn sanity_execution_of_block() {
1006        let factory = create_test_provider_factory();
1007        let provider = factory.provider_rw().unwrap();
1008        let input = ExecInput { target: Some(1), checkpoint: None };
1009        let mut genesis_rlp = hex!("f901faf901f5a00000000000000000000000000000000000000000000000000000000000000000a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347942adc25665018aa1fe0e6bc666dac8fc2697ff9baa045571b40ae66ca7480791bbb2887286e4e4c4b1b298b191c889d6959023a32eda056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421b901000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000083020000808502540be400808000a00000000000000000000000000000000000000000000000000000000000000000880000000000000000c0c0").as_slice();
1010        let genesis = SealedBlock::<Block>::decode(&mut genesis_rlp).unwrap();
1011        let mut block_rlp = hex!("f90262f901f9a075c371ba45999d87f4542326910a11af515897aebce5265d3f6acd1f1161f82fa01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347942adc25665018aa1fe0e6bc666dac8fc2697ff9baa098f2dcd87c8ae4083e7017a05456c14eea4b1db2032126e27b3b1563d57d7cc0a08151d548273f6683169524b66ca9fe338b9ce42bc3540046c828fd939ae23bcba03f4e5c2ec5b2170b711d97ee755c160457bb58d8daa338e835ec02ae6860bbabb901000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000083020000018502540be40082a8798203e800a00000000000000000000000000000000000000000000000000000000000000000880000000000000000f863f861800a8405f5e10094100000000000000000000000000000000000000080801ba07e09e26678ed4fac08a249ebe8ed680bf9051a5e14ad223e4b2b9d26e0208f37a05f6e3f188e3e6eab7d7d3b6568f5eac7d687b08d307d3154ccd8c87b4630509bc0").as_slice();
1012        let block = SealedBlock::<Block>::decode(&mut block_rlp).unwrap();
1013        provider.insert_block(&genesis.try_recover().unwrap()).unwrap();
1014        provider.insert_block(&block.clone().try_recover().unwrap()).unwrap();
1015        provider
1016            .static_file_provider()
1017            .latest_writer(StaticFileSegment::Headers)
1018            .unwrap()
1019            .commit()
1020            .unwrap();
1021        {
1022            let static_file_provider = provider.static_file_provider();
1023            let mut receipts_writer =
1024                static_file_provider.latest_writer(StaticFileSegment::Receipts).unwrap();
1025            receipts_writer.increment_block(0).unwrap();
1026            receipts_writer.commit().unwrap();
1027        }
1028        provider.commit().unwrap();
1029
1030        // insert pre state
1031        let provider = factory.provider_rw().unwrap();
1032
1033        let db_tx = provider.tx_ref();
1034        let acc1 = address!("0x1000000000000000000000000000000000000000");
1035        let acc2 = address!("0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b");
1036        let code = hex!("5a465a905090036002900360015500");
1037        let balance = U256::from(0x3635c9adc5dea00000u128);
1038        let code_hash = keccak256(code);
1039        db_tx
1040            .put::<tables::PlainAccountState>(
1041                acc1,
1042                Account { bytecode_hash: Some(code_hash), ..Default::default() },
1043            )
1044            .unwrap();
1045        db_tx
1046            .put::<tables::PlainAccountState>(acc2, Account { balance, ..Default::default() })
1047            .unwrap();
1048        db_tx.put::<tables::Bytecodes>(code_hash, Bytecode::new_raw(code.into())).unwrap();
1049        provider.commit().unwrap();
1050
1051        // execute
1052
1053        // If there is a pruning configuration, then it's forced to use the database.
1054        // This way we test both cases.
1055        let modes = [None, Some(PruneModes::default())];
1056        let random_filter = ReceiptsLogPruneConfig(BTreeMap::from([(
1057            Address::random(),
1058            PruneMode::Distance(100000),
1059        )]));
1060
1061        // Tests node with database and node with static files
1062        for mut mode in modes {
1063            let mut provider = factory.database_provider_rw().unwrap();
1064
1065            if let Some(mode) = &mut mode {
1066                // Simulating a full node where we write receipts to database
1067                mode.receipts_log_filter = random_filter.clone();
1068            }
1069
1070            let mut execution_stage = stage();
1071            provider.set_prune_modes(mode.clone().unwrap_or_default());
1072
1073            let output = execution_stage.execute(&provider, input).unwrap();
1074            provider.commit().unwrap();
1075
1076            assert_matches!(output, ExecOutput {
1077                checkpoint: StageCheckpoint {
1078                    block_number: 1,
1079                    stage_checkpoint: Some(StageUnitCheckpoint::Execution(ExecutionCheckpoint {
1080                        block_range: CheckpointBlockRange {
1081                            from: 1,
1082                            to: 1,
1083                        },
1084                        progress: EntitiesCheckpoint {
1085                            processed,
1086                            total
1087                        }
1088                    }))
1089                },
1090                done: true
1091            } if processed == total && total == block.gas_used);
1092
1093            {
1094                let provider = factory.provider().unwrap();
1095
1096                // check post state
1097                let account1 = address!("0x1000000000000000000000000000000000000000");
1098                let account1_info =
1099                    Account { bytecode_hash: Some(code_hash), ..Default::default() };
1100                let account2 = address!("0x2adc25665018aa1fe0e6bc666dac8fc2697ff9ba");
1101                let account2_info =
1102                    Account { balance: U256::from(0x1bc16d674ece94bau128), ..Default::default() };
1103                let account3 = address!("0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b");
1104                let account3_info = Account {
1105                    balance: U256::from(0x3635c9adc5de996b46u128),
1106                    nonce: 0x01,
1107                    ..Default::default()
1108                };
1109
1110                // assert accounts
1111                assert!(matches!(
1112                    provider.basic_account(&account1),
1113                    Ok(Some(acc)) if acc == account1_info
1114                ));
1115                assert!(matches!(
1116                    provider.basic_account(&account2),
1117                    Ok(Some(acc)) if acc == account2_info
1118                ));
1119                assert!(matches!(
1120                    provider.basic_account(&account3),
1121                    Ok(Some(acc)) if acc == account3_info
1122                ));
1123                // assert storage
1124                // Get on dupsort would return only first value. This is good enough for this test.
1125                assert!(matches!(
1126                    provider.tx_ref().get::<tables::PlainStorageState>(account1),
1127                    Ok(Some(entry)) if entry.key == B256::with_last_byte(1) && entry.value == U256::from(2)
1128                ));
1129            }
1130
1131            let mut provider = factory.database_provider_rw().unwrap();
1132            let mut stage = stage();
1133            provider.set_prune_modes(mode.unwrap_or_default());
1134
1135            let _result = stage
1136                .unwind(
1137                    &provider,
1138                    UnwindInput { checkpoint: output.checkpoint, unwind_to: 0, bad_block: None },
1139                )
1140                .unwrap();
1141            provider.commit().unwrap();
1142        }
1143    }
1144
1145    #[tokio::test]
1146    #[allow(clippy::clone_on_copy)]
1147    async fn sanity_execute_unwind() {
1148        let factory = create_test_provider_factory();
1149        let provider = factory.provider_rw().unwrap();
1150        let input = ExecInput { target: Some(1), checkpoint: None };
1151        let mut genesis_rlp = hex!("f901faf901f5a00000000000000000000000000000000000000000000000000000000000000000a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347942adc25665018aa1fe0e6bc666dac8fc2697ff9baa045571b40ae66ca7480791bbb2887286e4e4c4b1b298b191c889d6959023a32eda056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421b901000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000083020000808502540be400808000a00000000000000000000000000000000000000000000000000000000000000000880000000000000000c0c0").as_slice();
1152        let genesis = SealedBlock::<Block>::decode(&mut genesis_rlp).unwrap();
1153        let mut block_rlp = hex!("f90262f901f9a075c371ba45999d87f4542326910a11af515897aebce5265d3f6acd1f1161f82fa01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347942adc25665018aa1fe0e6bc666dac8fc2697ff9baa098f2dcd87c8ae4083e7017a05456c14eea4b1db2032126e27b3b1563d57d7cc0a08151d548273f6683169524b66ca9fe338b9ce42bc3540046c828fd939ae23bcba03f4e5c2ec5b2170b711d97ee755c160457bb58d8daa338e835ec02ae6860bbabb901000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000083020000018502540be40082a8798203e800a00000000000000000000000000000000000000000000000000000000000000000880000000000000000f863f861800a8405f5e10094100000000000000000000000000000000000000080801ba07e09e26678ed4fac08a249ebe8ed680bf9051a5e14ad223e4b2b9d26e0208f37a05f6e3f188e3e6eab7d7d3b6568f5eac7d687b08d307d3154ccd8c87b4630509bc0").as_slice();
1154        let block = SealedBlock::<Block>::decode(&mut block_rlp).unwrap();
1155        provider.insert_block(&genesis.try_recover().unwrap()).unwrap();
1156        provider.insert_block(&block.clone().try_recover().unwrap()).unwrap();
1157        provider
1158            .static_file_provider()
1159            .latest_writer(StaticFileSegment::Headers)
1160            .unwrap()
1161            .commit()
1162            .unwrap();
1163        {
1164            let static_file_provider = provider.static_file_provider();
1165            let mut receipts_writer =
1166                static_file_provider.latest_writer(StaticFileSegment::Receipts).unwrap();
1167            receipts_writer.increment_block(0).unwrap();
1168            receipts_writer.commit().unwrap();
1169        }
1170        provider.commit().unwrap();
1171
1172        // variables
1173        let code = hex!("5a465a905090036002900360015500");
1174        let balance = U256::from(0x3635c9adc5dea00000u128);
1175        let code_hash = keccak256(code);
1176        // pre state
1177        let provider = factory.provider_rw().unwrap();
1178
1179        let db_tx = provider.tx_ref();
1180        let acc1 = address!("0x1000000000000000000000000000000000000000");
1181        let acc1_info = Account { bytecode_hash: Some(code_hash), ..Default::default() };
1182        let acc2 = address!("0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b");
1183        let acc2_info = Account { balance, ..Default::default() };
1184
1185        db_tx.put::<tables::PlainAccountState>(acc1, acc1_info.clone()).unwrap();
1186        db_tx.put::<tables::PlainAccountState>(acc2, acc2_info.clone()).unwrap();
1187        db_tx.put::<tables::Bytecodes>(code_hash, Bytecode::new_raw(code.into())).unwrap();
1188        provider.commit().unwrap();
1189
1190        // execute
1191        let mut provider = factory.database_provider_rw().unwrap();
1192
1193        // If there is a pruning configuration, then it's forced to use the database.
1194        // This way we test both cases.
1195        let modes = [None, Some(PruneModes::default())];
1196        let random_filter = ReceiptsLogPruneConfig(BTreeMap::from([(
1197            Address::random(),
1198            PruneMode::Before(100000),
1199        )]));
1200
1201        // Tests node with database and node with static files
1202        for mut mode in modes {
1203            if let Some(mode) = &mut mode {
1204                // Simulating a full node where we write receipts to database
1205                mode.receipts_log_filter = random_filter.clone();
1206            }
1207
1208            // Test Execution
1209            let mut execution_stage = stage();
1210            provider.set_prune_modes(mode.clone().unwrap_or_default());
1211
1212            let result = execution_stage.execute(&provider, input).unwrap();
1213            provider.commit().unwrap();
1214
1215            // Test Unwind
1216            provider = factory.database_provider_rw().unwrap();
1217            let mut stage = stage();
1218            provider.set_prune_modes(mode.unwrap_or_default());
1219
1220            let result = stage
1221                .unwind(
1222                    &provider,
1223                    UnwindInput { checkpoint: result.checkpoint, unwind_to: 0, bad_block: None },
1224                )
1225                .unwrap();
1226
1227            provider.static_file_provider().commit().unwrap();
1228
1229            assert_matches!(result, UnwindOutput {
1230                checkpoint: StageCheckpoint {
1231                    block_number: 0,
1232                    stage_checkpoint: Some(StageUnitCheckpoint::Execution(ExecutionCheckpoint {
1233                        block_range: CheckpointBlockRange {
1234                            from: 1,
1235                            to: 1,
1236                        },
1237                        progress: EntitiesCheckpoint {
1238                            processed: 0,
1239                            total
1240                        }
1241                    }))
1242                }
1243            } if total == block.gas_used);
1244
1245            // assert unwind stage
1246            assert!(matches!(provider.basic_account(&acc1), Ok(Some(acc)) if acc == acc1_info));
1247            assert!(matches!(provider.basic_account(&acc2), Ok(Some(acc)) if acc == acc2_info));
1248
1249            let miner_acc = address!("0x2adc25665018aa1fe0e6bc666dac8fc2697ff9ba");
1250            assert!(matches!(provider.basic_account(&miner_acc), Ok(None)));
1251
1252            assert!(matches!(provider.receipt(0), Ok(None)));
1253        }
1254    }
1255
1256    #[test]
1257    fn unwind_from_cancun_to_pre_cancun_is_rejected() {
1258        let chain_spec = Arc::new(
1259            ChainSpecBuilder::mainnet()
1260                .berlin_activated()
1261                .with_fork(EthereumHardfork::Cancun, ForkCondition::Timestamp(15))
1262                .build(),
1263        );
1264        let factory = create_test_provider_factory_with_chain_spec(chain_spec);
1265        let provider = factory.database_provider_rw().unwrap();
1266
1267        let mut rng = generators::rng();
1268        let mut genesis = generators::random_block(
1269            &mut rng,
1270            0,
1271            generators::BlockParams { tx_count: Some(0), ..Default::default() },
1272        )
1273        .unseal();
1274        genesis.header.timestamp = 0;
1275        let genesis = genesis.seal_slow();
1276
1277        let mut block_1 = generators::random_block(
1278            &mut rng,
1279            1,
1280            generators::BlockParams {
1281                parent: Some(genesis.hash()),
1282                tx_count: Some(0),
1283                ..Default::default()
1284            },
1285        )
1286        .unseal();
1287        block_1.header.timestamp = 10;
1288        let block_1 = block_1.seal_slow();
1289
1290        let mut block_2 = generators::random_block(
1291            &mut rng,
1292            2,
1293            generators::BlockParams {
1294                parent: Some(block_1.hash()),
1295                tx_count: Some(0),
1296                ..Default::default()
1297            },
1298        )
1299        .unseal();
1300        block_2.header.timestamp = 20;
1301        let block_2 = block_2.seal_slow();
1302
1303        provider.insert_block(&genesis.try_recover().unwrap()).unwrap();
1304        provider.insert_block(&block_1.try_recover().unwrap()).unwrap();
1305        provider.insert_block(&block_2.try_recover().unwrap()).unwrap();
1306        provider
1307            .static_file_provider()
1308            .latest_writer(StaticFileSegment::Headers)
1309            .unwrap()
1310            .commit()
1311            .unwrap();
1312
1313        let mut execution_stage = stage();
1314        let err = execution_stage
1315            .unwind(
1316                &provider,
1317                UnwindInput { checkpoint: StageCheckpoint::new(2), unwind_to: 1, bad_block: None },
1318            )
1319            .unwrap_err();
1320
1321        assert_matches!(err, StageError::Fatal(_));
1322        assert!(err.to_string().contains("across Cancun activation boundary"));
1323    }
1324
1325    #[tokio::test]
1326    #[allow(clippy::clone_on_copy)]
1327    async fn test_selfdestruct() {
1328        let test_db = TestStageDB::default();
1329        let provider = test_db.factory.database_provider_rw().unwrap();
1330        let input = ExecInput { target: Some(1), checkpoint: None };
1331        let mut genesis_rlp = hex!("f901f8f901f3a00000000000000000000000000000000000000000000000000000000000000000a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347942adc25665018aa1fe0e6bc666dac8fc2697ff9baa0c9ceb8372c88cb461724d8d3d87e8b933f6fc5f679d4841800e662f4428ffd0da056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421b90100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008302000080830f4240808000a00000000000000000000000000000000000000000000000000000000000000000880000000000000000c0c0").as_slice();
1332        let genesis = SealedBlock::<Block>::decode(&mut genesis_rlp).unwrap();
1333        let mut block_rlp = hex!("f9025ff901f7a0c86e8cc0310ae7c531c758678ddbfd16fc51c8cef8cec650b032de9869e8b94fa01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347942adc25665018aa1fe0e6bc666dac8fc2697ff9baa050554882fbbda2c2fd93fdc466db9946ea262a67f7a76cc169e714f105ab583da00967f09ef1dfed20c0eacfaa94d5cd4002eda3242ac47eae68972d07b106d192a0e3c8b47fbfc94667ef4cceb17e5cc21e3b1eebd442cebb27f07562b33836290db90100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008302000001830f42408238108203e800a00000000000000000000000000000000000000000000000000000000000000000880000000000000000f862f860800a83061a8094095e7baea6a6c7c4c2dfeb977efac326af552d8780801ba072ed817487b84ba367d15d2f039b5fc5f087d0a8882fbdf73e8cb49357e1ce30a0403d800545b8fc544f92ce8124e2255f8c3c6af93f28243a120585d4c4c6a2a3c0").as_slice();
1334        let block = SealedBlock::<Block>::decode(&mut block_rlp).unwrap();
1335        provider.insert_block(&genesis.try_recover().unwrap()).unwrap();
1336        provider.insert_block(&block.clone().try_recover().unwrap()).unwrap();
1337        provider
1338            .static_file_provider()
1339            .latest_writer(StaticFileSegment::Headers)
1340            .unwrap()
1341            .commit()
1342            .unwrap();
1343        {
1344            let static_file_provider = provider.static_file_provider();
1345            let mut receipts_writer =
1346                static_file_provider.latest_writer(StaticFileSegment::Receipts).unwrap();
1347            receipts_writer.increment_block(0).unwrap();
1348            receipts_writer.commit().unwrap();
1349        }
1350        provider.commit().unwrap();
1351
1352        // variables
1353        let caller_address = address!("0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b");
1354        let destroyed_address = address!("0x095e7baea6a6c7c4c2dfeb977efac326af552d87");
1355        let beneficiary_address = address!("0x2adc25665018aa1fe0e6bc666dac8fc2697ff9ba");
1356
1357        let code = hex!("73095e7baea6a6c7c4c2dfeb977efac326af552d8731ff00");
1358        let balance = U256::from(0x0de0b6b3a7640000u64);
1359        let code_hash = keccak256(code);
1360
1361        // pre state
1362        let caller_info = Account { balance, ..Default::default() };
1363        let destroyed_info = Account { bytecode_hash: Some(code_hash), ..Default::default() };
1364
1365        // set account
1366        let provider = test_db.factory.provider_rw().unwrap();
1367        provider
1368            .tx_ref()
1369            .put::<tables::PlainAccountState>(caller_address, caller_info.clone())
1370            .unwrap();
1371        provider
1372            .tx_ref()
1373            .put::<tables::PlainAccountState>(destroyed_address, destroyed_info.clone())
1374            .unwrap();
1375        provider
1376            .tx_ref()
1377            .put::<tables::Bytecodes>(code_hash, Bytecode::new_raw(code.into()))
1378            .unwrap();
1379        // set storage to check when account gets destroyed.
1380        provider
1381            .tx_ref()
1382            .put::<tables::PlainStorageState>(
1383                destroyed_address,
1384                StorageEntry { key: B256::ZERO, value: U256::ZERO },
1385            )
1386            .unwrap();
1387        provider
1388            .tx_ref()
1389            .put::<tables::PlainStorageState>(
1390                destroyed_address,
1391                StorageEntry { key: B256::with_last_byte(1), value: U256::ONE },
1392            )
1393            .unwrap();
1394
1395        provider.commit().unwrap();
1396
1397        // execute
1398        let provider = test_db.factory.database_provider_rw().unwrap();
1399        let mut execution_stage = stage();
1400        let _ = execution_stage.execute(&provider, input).unwrap();
1401        provider.commit().unwrap();
1402
1403        // assert unwind stage
1404        let provider = test_db.factory.database_provider_rw().unwrap();
1405        assert!(matches!(provider.basic_account(&destroyed_address), Ok(None)));
1406
1407        assert!(matches!(
1408            provider.tx_ref().get::<tables::PlainStorageState>(destroyed_address),
1409            Ok(None)
1410        ));
1411        // drops tx so that it returns write privilege to test_tx
1412        drop(provider);
1413        let plain_accounts = test_db.table::<tables::PlainAccountState>().unwrap();
1414        let plain_storage = test_db.table::<tables::PlainStorageState>().unwrap();
1415
1416        assert_eq!(
1417            plain_accounts,
1418            vec![
1419                (
1420                    beneficiary_address,
1421                    Account { balance: U256::from(0x1bc16d674eca30a0u64), ..Default::default() }
1422                ),
1423                (
1424                    caller_address,
1425                    Account {
1426                        nonce: 1,
1427                        balance: U256::from(0xde0b6b3a761cf60u64),
1428                        ..Default::default()
1429                    }
1430                )
1431            ]
1432        );
1433        assert!(plain_storage.is_empty());
1434
1435        let account_changesets = test_db.table::<tables::AccountChangeSets>().unwrap();
1436        let storage_changesets = test_db.table::<tables::StorageChangeSets>().unwrap();
1437
1438        assert_eq!(
1439            account_changesets,
1440            vec![
1441                (
1442                    block.number,
1443                    AccountBeforeTx { address: destroyed_address, info: Some(destroyed_info) },
1444                ),
1445                (block.number, AccountBeforeTx { address: beneficiary_address, info: None }),
1446                (
1447                    block.number,
1448                    AccountBeforeTx { address: caller_address, info: Some(caller_info) }
1449                ),
1450            ]
1451        );
1452
1453        assert_eq!(
1454            storage_changesets,
1455            vec![
1456                (
1457                    (block.number, destroyed_address).into(),
1458                    StorageEntry { key: B256::ZERO, value: U256::ZERO }
1459                ),
1460                (
1461                    (block.number, destroyed_address).into(),
1462                    StorageEntry { key: B256::with_last_byte(1), value: U256::ONE }
1463                )
1464            ]
1465        );
1466    }
1467
1468    #[test]
1469    fn test_ensure_consistency_with_skipped_receipts() {
1470        // Test that ensure_consistency allows the case where receipts are intentionally
1471        // skipped. When receipts are skipped, blocks are still incremented in static files
1472        // but no receipt data is written.
1473
1474        let factory = create_test_provider_factory();
1475        factory.set_storage_settings_cache(StorageSettings::v2());
1476
1477        // Setup with block 1
1478        let provider_rw = factory.database_provider_rw().unwrap();
1479        let mut rng = generators::rng();
1480        let genesis = generators::random_block(&mut rng, 0, Default::default());
1481        provider_rw
1482            .insert_block(&genesis.try_recover().unwrap())
1483            .expect("failed to insert genesis");
1484        let block = generators::random_block(
1485            &mut rng,
1486            1,
1487            generators::BlockParams { tx_count: Some(2), ..Default::default() },
1488        );
1489        provider_rw.insert_block(&block.try_recover().unwrap()).expect("failed to insert block");
1490
1491        let static_file_provider = provider_rw.static_file_provider();
1492        static_file_provider.latest_writer(StaticFileSegment::Headers).unwrap().commit().unwrap();
1493
1494        // Simulate skipped receipts: increment block in receipts static file but don't write
1495        // receipts
1496        {
1497            let mut receipts_writer =
1498                static_file_provider.latest_writer(StaticFileSegment::Receipts).unwrap();
1499            receipts_writer.increment_block(0).unwrap();
1500            receipts_writer.increment_block(1).unwrap();
1501            receipts_writer.commit().unwrap();
1502        } // Explicitly drop receipts_writer here
1503
1504        provider_rw.commit().expect("failed to commit");
1505
1506        // Verify blocks are incremented but no receipts written
1507        assert_eq!(
1508            factory
1509                .static_file_provider()
1510                .get_highest_static_file_block(StaticFileSegment::Receipts),
1511            Some(1)
1512        );
1513        assert_eq!(
1514            factory.static_file_provider().get_highest_static_file_tx(StaticFileSegment::Receipts),
1515            None
1516        );
1517
1518        // Create execution stage
1519        let stage = stage();
1520
1521        // Run ensure_consistency - should NOT error
1522        // Block numbers match (both at 1), but tx numbers don't (database has txs, static files
1523        // don't) This is fine - receipts are being skipped
1524        let provider = factory.provider().unwrap();
1525        stage
1526            .ensure_consistency(&provider, 1, None)
1527            .expect("ensure_consistency should succeed when receipts are intentionally skipped");
1528    }
1529}