Skip to main content

reth_stages/stages/execution/
mod.rs

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