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