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