Skip to main content

reth_stages_api/pipeline/
mod.rs

1mod ctrl;
2mod event;
3pub use crate::pipeline::ctrl::ControlFlow;
4use crate::{PipelineTarget, StageCheckpoint, StageId};
5use alloy_primitives::{BlockNumber, B256};
6pub use event::*;
7use futures_util::Future;
8use reth_primitives_traits::constants::BEACON_CONSENSUS_REORG_UNWIND_DEPTH;
9use reth_provider::{
10    providers::ProviderNodeTypes, BlockHashReader, BlockNumReader, ChainStateBlockReader,
11    ChainStateBlockWriter, DBProvider, DatabaseProviderFactory, ProviderFactory,
12    PruneCheckpointReader, StageCheckpointReader, StageCheckpointWriter, StorageSettingsCache,
13};
14use reth_prune::PrunerBuilder;
15use reth_static_file::StaticFileProducer;
16use reth_tokio_util::{EventSender, EventStream};
17use std::{
18    pin::Pin,
19    time::{Duration, Instant},
20};
21use tokio::sync::watch;
22use tracing::*;
23
24mod builder;
25mod progress;
26mod set;
27
28use crate::{
29    BlockErrorKind, ExecInput, ExecOutput, MetricEvent, MetricEventsSender, PipelineError, Stage,
30    StageError, StageExt, UnwindInput,
31};
32pub use builder::*;
33use progress::*;
34use reth_errors::RethResult;
35pub use set::*;
36
37/// A container for a queued stage.
38pub(crate) type BoxedStage<DB> = Box<dyn Stage<DB>>;
39
40/// The future that returns the owned pipeline and the result of the pipeline run. See
41/// [`Pipeline::run_as_fut`].
42pub type PipelineFut<N> = Pin<Box<dyn Future<Output = PipelineWithResult<N>> + Send>>;
43
44/// The pipeline type itself with the result of [`Pipeline::run_as_fut`]
45pub type PipelineWithResult<N> = (Pipeline<N>, Result<ControlFlow, PipelineError>);
46
47#[cfg_attr(doc, aquamarine::aquamarine)]
48/// A staged sync pipeline.
49///
50/// The pipeline executes queued [stages][Stage] serially. An external component determines the tip
51/// of the chain and the pipeline then executes each stage in order from the current local chain tip
52/// and the external chain tip. When a stage is executed, it will run until it reaches the chain
53/// tip.
54///
55/// After the entire pipeline has been run, it will run again unless asked to stop (see
56/// [`Pipeline::set_max_block`]).
57///
58/// `include_mmd!("docs/mermaid/pipeline.mmd`")
59///
60/// # Unwinding
61///
62/// In case of a validation error (as determined by the consensus engine) in one of the stages, the
63/// pipeline will unwind the stages in reverse order of execution. It is also possible to
64/// request an unwind manually (see [`Pipeline::unwind`]).
65///
66/// # Defaults
67///
68/// The [`DefaultStages`](crate::sets::DefaultStages) are used to fully sync reth.
69pub struct Pipeline<N: ProviderNodeTypes> {
70    /// Provider factory.
71    provider_factory: ProviderFactory<N>,
72    /// All configured stages in the order they will be executed.
73    stages: Vec<BoxedStage<<ProviderFactory<N> as DatabaseProviderFactory>::ProviderRW>>,
74    /// The maximum block number to sync to.
75    max_block: Option<BlockNumber>,
76    static_file_producer: StaticFileProducer<ProviderFactory<N>>,
77    /// Sender for events the pipeline emits.
78    event_sender: EventSender<PipelineEvent>,
79    /// Keeps track of the progress of the pipeline.
80    progress: PipelineProgress,
81    /// A Sender for the current chain tip to sync to.
82    ///
83    /// This is used to notify the headers stage about a new sync target.
84    tip_tx: Option<watch::Sender<B256>>,
85    metrics_tx: Option<MetricEventsSender>,
86    /// Whether an unwind should fail the syncing process. Should only be set when downloading
87    /// blocks from trusted sources and expecting them to be valid.
88    fail_on_unwind: bool,
89    /// Block that was chosen as a target of the last unwind triggered by
90    /// [`StageError::DetachedHead`] error.
91    last_detached_head_unwind_target: Option<B256>,
92    /// Number of consecutive unwind attempts due to [`StageError::DetachedHead`] for the current
93    /// fork.
94    detached_head_attempts: u64,
95}
96
97impl<N: ProviderNodeTypes> Pipeline<N> {
98    /// Construct a pipeline using a [`PipelineBuilder`].
99    pub fn builder() -> PipelineBuilder<<ProviderFactory<N> as DatabaseProviderFactory>::ProviderRW>
100    {
101        PipelineBuilder::default()
102    }
103
104    /// Return the minimum block number achieved by
105    /// any stage during the execution of the pipeline.
106    pub const fn minimum_block_number(&self) -> Option<u64> {
107        self.progress.minimum_block_number
108    }
109
110    /// Set tip for reverse sync.
111    #[track_caller]
112    pub fn set_tip(&self, tip: B256) {
113        let _ = self.tip_tx.as_ref().expect("tip sender is set").send(tip).map_err(|_| {
114            warn!(target: "sync::pipeline", "Chain tip channel closed");
115        });
116    }
117
118    /// Listen for events on the pipeline.
119    pub fn events(&self) -> EventStream<PipelineEvent> {
120        self.event_sender.new_listener()
121    }
122
123    /// Get a mutable reference to a stage by index.
124    pub fn stage(
125        &mut self,
126        idx: usize,
127    ) -> &mut dyn Stage<<ProviderFactory<N> as DatabaseProviderFactory>::ProviderRW> {
128        &mut self.stages[idx]
129    }
130}
131
132impl<N: ProviderNodeTypes> Pipeline<N> {
133    /// Registers progress metrics for each registered stage
134    pub fn register_metrics(&mut self) -> Result<(), PipelineError> {
135        let Some(metrics_tx) = &mut self.metrics_tx else { return Ok(()) };
136        let provider = self.provider_factory.provider()?;
137
138        for stage in &self.stages {
139            let stage_id = stage.id();
140            let _ = metrics_tx.send(MetricEvent::StageCheckpoint {
141                stage_id,
142                checkpoint: provider.get_stage_checkpoint(stage_id)?.unwrap_or_default(),
143                max_block_number: None,
144                elapsed: Duration::default(),
145            });
146        }
147        Ok(())
148    }
149
150    /// Consume the pipeline and run it until it reaches the provided tip, if set. Return the
151    /// pipeline and its result as a future.
152    #[track_caller]
153    pub fn run_as_fut(mut self, target: Option<PipelineTarget>) -> PipelineFut<N> {
154        let _ = self.register_metrics();
155        Box::pin(async move {
156            // NOTE: the tip should only be None if we are in continuous sync mode.
157            if let Some(target) = target {
158                match target {
159                    PipelineTarget::Sync(tip) => self.set_tip(tip),
160                    PipelineTarget::Unwind(target) => {
161                        if let Err(err) = self.move_to_static_files() {
162                            return (self, Err(err.into()))
163                        }
164                        if let Err(err) = self.unwind(target, None) {
165                            return (self, Err(err))
166                        }
167                        self.progress.update(target);
168
169                        return (self, Ok(ControlFlow::Continue { block_number: target }))
170                    }
171                }
172            }
173
174            let result = self.run_loop().await;
175            trace!(target: "sync::pipeline", ?target, ?result, "Pipeline finished");
176            (self, result)
177        })
178    }
179
180    /// Run the pipeline in an infinite loop. Will terminate early if the user has specified
181    /// a `max_block` in the pipeline.
182    pub async fn run(&mut self) -> Result<(), PipelineError> {
183        let _ = self.register_metrics(); // ignore error
184
185        loop {
186            let next_action = self.run_loop().await?;
187
188            if next_action.is_unwind() && self.fail_on_unwind {
189                return Err(PipelineError::UnexpectedUnwind)
190            }
191
192            // Terminate the loop early if it's reached the maximum user
193            // configured block.
194            if next_action.should_continue() &&
195                self.progress
196                    .minimum_block_number
197                    .zip(self.max_block)
198                    .is_some_and(|(progress, target)| progress >= target)
199            {
200                trace!(
201                    target: "sync::pipeline",
202                    ?next_action,
203                    minimum_block_number = ?self.progress.minimum_block_number,
204                    max_block = ?self.max_block,
205                    "Terminating pipeline."
206                );
207                return Ok(())
208            }
209        }
210    }
211
212    /// Performs one pass of the pipeline across all stages. After successful
213    /// execution of each stage, it proceeds to commit it to the database.
214    ///
215    /// If any stage is unsuccessful at execution, we proceed to
216    /// unwind. This will undo the progress across the entire pipeline
217    /// up to the block that caused the error.
218    ///
219    /// Returns the control flow after it ran the pipeline.
220    /// This will be [`ControlFlow::Continue`] or [`ControlFlow::NoProgress`] of the _last_ stage in
221    /// the pipeline (for example the `Finish` stage). Or [`ControlFlow::Unwind`] of the stage
222    /// that caused the unwind.
223    pub async fn run_loop(&mut self) -> Result<ControlFlow, PipelineError> {
224        self.run_loop_until(None).await
225    }
226
227    /// Runs one pipeline pass through `last_stage`, including that stage.
228    ///
229    /// Each stage in the selected prefix runs to completion; later stages are not executed.
230    /// A [`PipelineTarget::Sync`] updates the tip before running the prefix, while a
231    /// [`PipelineTarget::Unwind`] unwinds the entire pipeline and returns without executing stages.
232    /// Stage errors that request an unwind also unwind the entire pipeline. Normal static-file
233    /// migration and pruning still run.
234    ///
235    /// Returns the control flow of the partial pass, or [`PipelineError::MissingStage`] if
236    /// `last_stage` is absent from the pipeline.
237    pub async fn run_until(
238        &mut self,
239        last_stage: StageId,
240        target: Option<PipelineTarget>,
241    ) -> Result<ControlFlow, PipelineError> {
242        let _ = self.register_metrics();
243        if !self.stages.iter().any(|stage| stage.id() == last_stage) {
244            return Err(PipelineError::MissingStage(last_stage))
245        }
246
247        match target {
248            Some(PipelineTarget::Sync(tip)) => self.set_tip(tip),
249            // An unwind rewinds every stage, so stopping part way through has no meaning.
250            Some(PipelineTarget::Unwind(target)) => {
251                self.move_to_static_files()?;
252                self.unwind(target, None)?;
253                self.progress.update(target);
254                return Ok(ControlFlow::Continue { block_number: target })
255            }
256            None => {}
257        }
258
259        self.run_loop_until(Some(last_stage)).await
260    }
261
262    // Shared stage loop. `last_stage` stops the pass once that stage has been executed.
263    async fn run_loop_until(
264        &mut self,
265        last_stage: Option<StageId>,
266    ) -> Result<ControlFlow, PipelineError> {
267        self.move_to_static_files()?;
268
269        let mut previous_stage = None;
270        for stage_index in 0..self.stages.len() {
271            let stage = &self.stages[stage_index];
272            let stage_id = stage.id();
273
274            trace!(target: "sync::pipeline", stage = %stage_id, "Executing stage");
275            let next = self.execute_stage_to_completion(previous_stage, stage_index).await?;
276
277            trace!(target: "sync::pipeline", stage = %stage_id, ?next, "Completed stage");
278
279            match next {
280                ControlFlow::NoProgress { block_number } => {
281                    if let Some(block_number) = block_number {
282                        self.progress.update(block_number);
283                    }
284                }
285                ControlFlow::Continue { block_number } => self.progress.update(block_number),
286                ControlFlow::Unwind { target, bad_block } => {
287                    self.unwind(target, Some(bad_block.block.number))?;
288                    return Ok(ControlFlow::Unwind { target, bad_block })
289                }
290            }
291
292            previous_stage = Some(
293                self.provider_factory
294                    .provider()?
295                    .get_stage_checkpoint(stage_id)?
296                    .unwrap_or_default()
297                    .block_number,
298            );
299
300            if last_stage == Some(stage_id) {
301                break
302            }
303        }
304
305        Ok(self.progress.next_ctrl())
306    }
307
308    /// Run [static file producer](StaticFileProducer) and [pruner](reth_prune::Pruner) to **move**
309    /// all data from the database to static files for corresponding
310    /// [segments](reth_static_file_types::StaticFileSegment), according to their [stage
311    /// checkpoints](StageCheckpoint):
312    /// - [`StaticFileSegment::Headers`](reth_static_file_types::StaticFileSegment::Headers) ->
313    ///   [`StageId::Headers`]
314    /// - [`StaticFileSegment::Receipts`](reth_static_file_types::StaticFileSegment::Receipts) ->
315    ///   [`StageId::Execution`]
316    /// - [`StaticFileSegment::Transactions`](reth_static_file_types::StaticFileSegment::Transactions)
317    ///   -> [`StageId::Bodies`]
318    ///
319    /// This is a legacy storage.v1 backfill step. Storage.v2 writes directly to static files and
320    /// `RocksDB`, so there is no MDBX -> static-file migration to perform.
321    ///
322    /// CAUTION: This method locks the static file producer Mutex, hence can block the thread if the
323    /// lock is occupied.
324    pub fn move_to_static_files(&self) -> RethResult<()> {
325        if self.provider_factory.cached_storage_settings().is_v2() {
326            return Ok(())
327        }
328
329        // Copies data from database to static files
330        let lowest_static_file_height =
331            self.static_file_producer.lock().copy_to_static_files()?.min_block_num();
332
333        // Deletes data which has been copied to static files.
334        if let Some(prune_tip) = lowest_static_file_height {
335            // Run the pruner so we don't potentially end up with higher height in the database vs
336            // static files during a pipeline unwind
337            let mut pruner = PrunerBuilder::new(Default::default())
338                .delete_limit(usize::MAX)
339                .build_with_provider_factory(self.provider_factory.clone());
340
341            pruner.run(prune_tip)?;
342        }
343
344        Ok(())
345    }
346
347    /// Unwind the stages to the target block (exclusive).
348    ///
349    /// If the unwind is due to a bad block the number of that block should be specified.
350    pub fn unwind(
351        &mut self,
352        to: BlockNumber,
353        bad_block: Option<BlockNumber>,
354    ) -> Result<(), PipelineError> {
355        // Add validation before starting unwind
356        let (latest_block, prune_modes, checkpoints) = {
357            let provider = self.provider_factory.provider()?;
358            (
359                provider.last_block_number()?,
360                provider.prune_modes_ref().clone(),
361                provider.get_prune_checkpoints()?,
362            )
363        };
364        prune_modes.ensure_unwind_target_unpruned(latest_block, to, &checkpoints)?;
365
366        // Unwind stages in reverse order of execution
367        let unwind_pipeline = self.stages.iter_mut().rev();
368
369        // Legacy Engine: This prevents a race condition in which the `StaticFileProducer` could
370        // attempt to proceed with a finalized block which has been unwinded
371        let _locked_sf_producer = self.static_file_producer.lock();
372
373        let mut provider_rw =
374            self.provider_factory.unwind_provider_rw()?.disable_long_read_transaction_safety();
375
376        for stage in unwind_pipeline {
377            let stage_id = stage.id();
378            let span = info_span!("Unwinding", stage = %stage_id);
379            let _enter = span.enter();
380
381            let mut checkpoint = provider_rw.get_stage_checkpoint(stage_id)?.unwrap_or_default();
382            if checkpoint.block_number < to {
383                debug!(
384                    target: "sync::pipeline",
385                    from = %checkpoint.block_number,
386                    %to,
387                    "Unwind point too far for stage"
388                );
389                self.event_sender.notify(PipelineEvent::Skipped { stage_id });
390
391                continue
392            }
393
394            info!(
395                target: "sync::pipeline",
396                from = %checkpoint.block_number,
397                %to,
398                ?bad_block,
399                "Starting unwind"
400            );
401            while checkpoint.block_number > to {
402                let unwind_started_at = Instant::now();
403                let input = UnwindInput { checkpoint, unwind_to: to, bad_block };
404                self.event_sender.notify(PipelineEvent::Unwind { stage_id, input });
405
406                let output = stage.unwind(&provider_rw, input);
407                match output {
408                    Ok(unwind_output) => {
409                        checkpoint = unwind_output.checkpoint;
410                        info!(
411                            target: "sync::pipeline",
412                            stage = %stage_id,
413                            unwind_to = to,
414                            progress = checkpoint.block_number,
415                            done = checkpoint.block_number == to,
416                            "Stage unwound"
417                        );
418
419                        provider_rw.save_stage_checkpoint(stage_id, checkpoint)?;
420
421                        // Notify event listeners and update metrics.
422                        self.event_sender
423                            .notify(PipelineEvent::Unwound { stage_id, result: unwind_output });
424
425                        if let Some(metrics_tx) = &mut self.metrics_tx {
426                            let _ = metrics_tx.send(MetricEvent::StageCheckpoint {
427                                stage_id,
428                                checkpoint,
429                                // We assume it was set in the previous execute iteration, so it
430                                // doesn't change when we unwind.
431                                max_block_number: None,
432                                elapsed: unwind_started_at.elapsed(),
433                            });
434                        }
435
436                        // update finalized and safe block if needed
437                        let last_saved_finalized_block_number =
438                            provider_rw.last_finalized_block_number()?;
439
440                        // If None, that means the finalized block is not written so we should
441                        // always save in that case
442                        if last_saved_finalized_block_number.is_none() ||
443                            Some(checkpoint.block_number) < last_saved_finalized_block_number
444                        {
445                            provider_rw.save_finalized_block_number(BlockNumber::from(
446                                checkpoint.block_number,
447                            ))?;
448                        }
449
450                        let last_saved_safe_block_number = provider_rw.last_safe_block_number()?;
451
452                        if last_saved_safe_block_number.is_none() ||
453                            Some(checkpoint.block_number) < last_saved_safe_block_number
454                        {
455                            provider_rw.save_safe_block_number(BlockNumber::from(
456                                checkpoint.block_number,
457                            ))?;
458                        }
459
460                        provider_rw.commit()?;
461
462                        stage.post_unwind_commit()?;
463
464                        provider_rw = self.provider_factory.unwind_provider_rw()?;
465                    }
466                    Err(err) => {
467                        self.event_sender.notify(PipelineEvent::Error { stage_id });
468
469                        return Err(PipelineError::Stage(StageError::Fatal(Box::new(err))))
470                    }
471                }
472            }
473        }
474
475        Ok(())
476    }
477
478    async fn execute_stage_to_completion(
479        &mut self,
480        previous_stage: Option<BlockNumber>,
481        stage_index: usize,
482    ) -> Result<ControlFlow, PipelineError> {
483        let total_stages = self.stages.len();
484
485        let stage_id = self.stage(stage_index).id();
486        let mut made_progress = false;
487        let target = self.max_block.or(previous_stage);
488
489        loop {
490            let prev_checkpoint = self.provider_factory.get_stage_checkpoint(stage_id)?;
491
492            let stage_reached_max_block = prev_checkpoint
493                .zip(self.max_block)
494                .is_some_and(|(prev_progress, target)| prev_progress.block_number >= target);
495            if stage_reached_max_block {
496                warn!(
497                    target: "sync::pipeline",
498                    stage = %stage_id,
499                    max_block = self.max_block,
500                    prev_block = prev_checkpoint.map(|progress| progress.block_number),
501                    "Stage reached target block, skipping."
502                );
503                self.event_sender.notify(PipelineEvent::Skipped { stage_id });
504
505                // We reached the maximum block, so we skip the stage
506                return Ok(ControlFlow::NoProgress {
507                    block_number: prev_checkpoint.map(|progress| progress.block_number),
508                })
509            }
510
511            let exec_input = ExecInput { target, checkpoint: prev_checkpoint };
512
513            self.event_sender.notify(PipelineEvent::Prepare {
514                pipeline_stages_progress: PipelineStagesProgress {
515                    current: stage_index + 1,
516                    total: total_stages,
517                },
518                stage_id,
519                checkpoint: prev_checkpoint,
520                target,
521            });
522
523            if let Err(err) = self.stage(stage_index).execute_ready(exec_input).await {
524                self.event_sender.notify(PipelineEvent::Error { stage_id });
525                match self.on_stage_error(stage_id, prev_checkpoint, err)? {
526                    Some(ctrl) => return Ok(ctrl),
527                    None => continue,
528                };
529            }
530
531            let stage_started_at = Instant::now();
532            let provider_rw = self.provider_factory.database_provider_rw()?;
533
534            self.event_sender.notify(PipelineEvent::Run {
535                pipeline_stages_progress: PipelineStagesProgress {
536                    current: stage_index + 1,
537                    total: total_stages,
538                },
539                stage_id,
540                checkpoint: prev_checkpoint,
541                target,
542            });
543
544            match self.stage(stage_index).execute(&provider_rw, exec_input) {
545                Ok(out @ ExecOutput { checkpoint, done }) => {
546                    // Update stage checkpoint.
547                    provider_rw.save_stage_checkpoint(stage_id, checkpoint)?;
548
549                    // Commit processed data to the database.
550                    provider_rw.commit()?;
551
552                    // Invoke stage post commit hook.
553                    self.stage(stage_index).post_execute_commit()?;
554
555                    // Notify event listeners and update metrics.
556                    self.event_sender.notify(PipelineEvent::Ran {
557                        pipeline_stages_progress: PipelineStagesProgress {
558                            current: stage_index + 1,
559                            total: total_stages,
560                        },
561                        stage_id,
562                        result: out,
563                    });
564                    if let Some(metrics_tx) = &mut self.metrics_tx {
565                        let _ = metrics_tx.send(MetricEvent::StageCheckpoint {
566                            stage_id,
567                            checkpoint,
568                            max_block_number: target,
569                            elapsed: stage_started_at.elapsed(),
570                        });
571                    }
572
573                    let block_number = checkpoint.block_number;
574                    let prev_block_number = prev_checkpoint.unwrap_or_default().block_number;
575                    made_progress |= block_number != prev_block_number;
576                    if done {
577                        return Ok(if made_progress {
578                            ControlFlow::Continue { block_number }
579                        } else {
580                            ControlFlow::NoProgress { block_number: Some(block_number) }
581                        })
582                    }
583                }
584                Err(err) => {
585                    drop(provider_rw);
586                    self.event_sender.notify(PipelineEvent::Error { stage_id });
587
588                    if let Some(ctrl) = self.on_stage_error(stage_id, prev_checkpoint, err)? {
589                        return Ok(ctrl)
590                    }
591                }
592            }
593        }
594    }
595
596    fn on_stage_error(
597        &mut self,
598        stage_id: StageId,
599        prev_checkpoint: Option<StageCheckpoint>,
600        err: StageError,
601    ) -> Result<Option<ControlFlow>, PipelineError> {
602        if let StageError::DetachedHead { local_head, header, error } = err {
603            warn!(target: "sync::pipeline", stage = %stage_id, ?local_head, ?header, %error, "Stage encountered detached head");
604
605            if let Some(last_detached_head_unwind_target) = self.last_detached_head_unwind_target {
606                if local_head.block.hash == last_detached_head_unwind_target &&
607                    header.block.number == local_head.block.number + 1
608                {
609                    self.detached_head_attempts += 1;
610                } else {
611                    self.detached_head_attempts = 1;
612                }
613            } else {
614                self.detached_head_attempts = 1;
615            }
616
617            // We unwind because of a detached head.
618            let unwind_to = local_head
619                .block
620                .number
621                .saturating_sub(
622                    BEACON_CONSENSUS_REORG_UNWIND_DEPTH.saturating_mul(self.detached_head_attempts),
623                )
624                .max(1);
625
626            self.last_detached_head_unwind_target = self.provider_factory.block_hash(unwind_to)?;
627            Ok(Some(ControlFlow::Unwind { target: unwind_to, bad_block: local_head }))
628        } else if let StageError::Block { block, error } = err {
629            match error {
630                BlockErrorKind::Validation(validation_error) => {
631                    error!(
632                        target: "sync::pipeline",
633                        stage = %stage_id,
634                        bad_block = %block.block.number,
635                        bad_block_hash = %block.block.hash,
636                        "Stage encountered a validation error: {validation_error}"
637                    );
638
639                    // FIXME: When handling errors, we do not commit the database transaction. This
640                    // leads to the Merkle stage not clearing its checkpoint, and restarting from an
641                    // invalid place.
642                    // Only reset MerkleExecute checkpoint if MerkleExecute itself failed
643                    if stage_id == StageId::MerkleExecute {
644                        let provider_rw = self.provider_factory.database_provider_rw()?;
645                        provider_rw
646                            .save_stage_checkpoint_progress(StageId::MerkleExecute, vec![])?;
647                        provider_rw.save_stage_checkpoint(
648                            StageId::MerkleExecute,
649                            prev_checkpoint.unwrap_or_default(),
650                        )?;
651
652                        provider_rw.commit()?;
653                    }
654                }
655                BlockErrorKind::Execution(execution_error) => {
656                    error!(
657                        target: "sync::pipeline",
658                        stage = %stage_id,
659                        bad_block = %block.block.number,
660                        bad_block_hash = %block.block.hash,
661                        "Stage encountered an execution error: {execution_error}"
662                    );
663                }
664            }
665
666            // We unwind because of a block error. If the unwind itself fails, we bail entirely;
667            // otherwise, we restart the execution loop from the beginning.
668            Ok(Some(ControlFlow::Unwind {
669                target: prev_checkpoint.unwrap_or_default().block_number,
670                bad_block: block,
671            }))
672        } else if let StageError::MissingStaticFileData { block, segment } = err {
673            error!(
674                target: "sync::pipeline",
675                stage = %stage_id,
676                bad_block = %block.block.number,
677                segment = %segment,
678                "Stage is missing static file data."
679            );
680
681            Ok(Some(ControlFlow::Unwind {
682                target: block.block.number.saturating_sub(1),
683                bad_block: block,
684            }))
685        } else if err.is_fatal() {
686            error!(target: "sync::pipeline", stage = %stage_id, "Stage encountered a fatal error: {err}");
687            Err(err.into())
688        } else {
689            // On other errors we assume they are recoverable if we discard the
690            // transaction and run the stage again.
691            warn!(
692                target: "sync::pipeline",
693                stage = %stage_id,
694                "Stage encountered a non-fatal error: {err}. Retrying..."
695            );
696            Ok(None)
697        }
698    }
699}
700
701impl<N: ProviderNodeTypes> std::fmt::Debug for Pipeline<N> {
702    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
703        f.debug_struct("Pipeline")
704            .field("stages", &self.stages.iter().map(|stage| stage.id()).collect::<Vec<StageId>>())
705            .field("max_block", &self.max_block)
706            .field("event_sender", &self.event_sender)
707            .field("fail_on_unwind", &self.fail_on_unwind)
708            .finish()
709    }
710}
711
712#[cfg(test)]
713mod tests {
714    use std::sync::atomic::Ordering;
715
716    use super::*;
717    use crate::{test_utils::TestStage, UnwindOutput};
718    use assert_matches::assert_matches;
719    use reth_consensus::ConsensusError;
720    use reth_errors::ProviderError;
721    use reth_provider::test_utils::{create_test_provider_factory, MockNodeTypesWithDB};
722    use reth_prune::PruneModes;
723    use reth_testing_utils::generators::{self, random_block_with_parent};
724    use tokio_stream::StreamExt;
725
726    #[test]
727    fn record_progress_calculates_outliers() {
728        let mut progress = PipelineProgress::default();
729
730        progress.update(10);
731        assert_eq!(progress.minimum_block_number, Some(10));
732        assert_eq!(progress.maximum_block_number, Some(10));
733
734        progress.update(20);
735        assert_eq!(progress.minimum_block_number, Some(10));
736        assert_eq!(progress.maximum_block_number, Some(20));
737
738        progress.update(1);
739        assert_eq!(progress.minimum_block_number, Some(1));
740        assert_eq!(progress.maximum_block_number, Some(20));
741    }
742
743    #[test]
744    fn progress_ctrl_flow() {
745        let mut progress = PipelineProgress::default();
746
747        assert_eq!(progress.next_ctrl(), ControlFlow::NoProgress { block_number: None });
748
749        progress.update(1);
750        assert_eq!(progress.next_ctrl(), ControlFlow::Continue { block_number: 1 });
751    }
752
753    /// Runs only the requested prefix of the pipeline.
754    #[tokio::test]
755    async fn run_until_stops_after_the_named_stage() {
756        let provider_factory = create_test_provider_factory();
757
758        let mut pipeline = Pipeline::<MockNodeTypesWithDB>::builder()
759            .add_stage(
760                TestStage::new(StageId::Other("A"))
761                    .add_exec(Ok(ExecOutput { checkpoint: StageCheckpoint::new(10), done: true })),
762            )
763            // No execution is queued for B, so running it would panic on the empty script.
764            .add_stage(TestStage::new(StageId::Other("B")))
765            .with_max_block(10)
766            .build(
767                provider_factory.clone(),
768                StaticFileProducer::new(provider_factory.clone(), PruneModes::default()),
769            );
770
771        pipeline.run_until(StageId::Other("A"), None).await.unwrap();
772
773        let provider = provider_factory.provider().unwrap();
774        assert_eq!(
775            provider.get_stage_checkpoint(StageId::Other("A")).unwrap(),
776            Some(StageCheckpoint::new(10))
777        );
778        assert_eq!(provider.get_stage_checkpoint(StageId::Other("B")).unwrap(), None);
779    }
780
781    /// A prefix run naming a stage the pipeline does not have is an error, not a full run.
782    #[tokio::test]
783    async fn run_until_rejects_an_unknown_stage() {
784        let provider_factory = create_test_provider_factory();
785
786        let mut pipeline = Pipeline::<MockNodeTypesWithDB>::builder()
787            .add_stage(TestStage::new(StageId::Other("A")))
788            .build(
789                provider_factory.clone(),
790                StaticFileProducer::new(provider_factory.clone(), PruneModes::default()),
791            );
792
793        let result = pipeline.run_until(StageId::Other("Z"), None).await;
794
795        assert_matches!(result, Err(PipelineError::MissingStage(StageId::Other("Z"))));
796    }
797
798    /// Runs a simple pipeline.
799    #[tokio::test]
800    async fn run_pipeline() {
801        let provider_factory = create_test_provider_factory();
802
803        let stage_a = TestStage::new(StageId::Other("A"))
804            .add_exec(Ok(ExecOutput { checkpoint: StageCheckpoint::new(20), done: true }));
805        let (stage_a, post_execute_commit_counter_a) = stage_a.with_post_execute_commit_counter();
806        let (stage_a, post_unwind_commit_counter_a) = stage_a.with_post_unwind_commit_counter();
807
808        let stage_b = TestStage::new(StageId::Other("B"))
809            .add_exec(Ok(ExecOutput { checkpoint: StageCheckpoint::new(10), done: true }));
810        let (stage_b, post_execute_commit_counter_b) = stage_b.with_post_execute_commit_counter();
811        let (stage_b, post_unwind_commit_counter_b) = stage_b.with_post_unwind_commit_counter();
812
813        let mut pipeline = Pipeline::<MockNodeTypesWithDB>::builder()
814            .add_stage(stage_a)
815            .add_stage(stage_b)
816            .with_max_block(10)
817            .build(
818                provider_factory.clone(),
819                StaticFileProducer::new(provider_factory.clone(), PruneModes::default()),
820            );
821        let events = pipeline.events();
822
823        // Run pipeline
824        tokio::spawn(async move {
825            pipeline.run().await.unwrap();
826        });
827
828        // Check that the stages were run in order
829        assert_eq!(
830            events.collect::<Vec<PipelineEvent>>().await,
831            vec![
832                PipelineEvent::Prepare {
833                    pipeline_stages_progress: PipelineStagesProgress { current: 1, total: 2 },
834                    stage_id: StageId::Other("A"),
835                    checkpoint: None,
836                    target: Some(10),
837                },
838                PipelineEvent::Run {
839                    pipeline_stages_progress: PipelineStagesProgress { current: 1, total: 2 },
840                    stage_id: StageId::Other("A"),
841                    checkpoint: None,
842                    target: Some(10),
843                },
844                PipelineEvent::Ran {
845                    pipeline_stages_progress: PipelineStagesProgress { current: 1, total: 2 },
846                    stage_id: StageId::Other("A"),
847                    result: ExecOutput { checkpoint: StageCheckpoint::new(20), done: true },
848                },
849                PipelineEvent::Prepare {
850                    pipeline_stages_progress: PipelineStagesProgress { current: 2, total: 2 },
851                    stage_id: StageId::Other("B"),
852                    checkpoint: None,
853                    target: Some(10),
854                },
855                PipelineEvent::Run {
856                    pipeline_stages_progress: PipelineStagesProgress { current: 2, total: 2 },
857                    stage_id: StageId::Other("B"),
858                    checkpoint: None,
859                    target: Some(10),
860                },
861                PipelineEvent::Ran {
862                    pipeline_stages_progress: PipelineStagesProgress { current: 2, total: 2 },
863                    stage_id: StageId::Other("B"),
864                    result: ExecOutput { checkpoint: StageCheckpoint::new(10), done: true },
865                },
866            ]
867        );
868
869        assert_eq!(post_execute_commit_counter_a.load(Ordering::Relaxed), 1);
870        assert_eq!(post_unwind_commit_counter_a.load(Ordering::Relaxed), 0);
871
872        assert_eq!(post_execute_commit_counter_b.load(Ordering::Relaxed), 1);
873        assert_eq!(post_unwind_commit_counter_b.load(Ordering::Relaxed), 0);
874    }
875
876    /// Unwinds a simple pipeline.
877    #[tokio::test]
878    async fn unwind_pipeline() {
879        let provider_factory = create_test_provider_factory();
880
881        let stage_a = TestStage::new(StageId::Other("A"))
882            .add_exec(Ok(ExecOutput { checkpoint: StageCheckpoint::new(100), done: true }))
883            .add_unwind(Ok(UnwindOutput { checkpoint: StageCheckpoint::new(1) }));
884        let (stage_a, post_execute_commit_counter_a) = stage_a.with_post_execute_commit_counter();
885        let (stage_a, post_unwind_commit_counter_a) = stage_a.with_post_unwind_commit_counter();
886
887        let stage_b = TestStage::new(StageId::Other("B"))
888            .add_exec(Ok(ExecOutput { checkpoint: StageCheckpoint::new(10), done: true }))
889            .add_unwind(Ok(UnwindOutput { checkpoint: StageCheckpoint::new(1) }));
890        let (stage_b, post_execute_commit_counter_b) = stage_b.with_post_execute_commit_counter();
891        let (stage_b, post_unwind_commit_counter_b) = stage_b.with_post_unwind_commit_counter();
892
893        let stage_c = TestStage::new(StageId::Other("C"))
894            .add_exec(Ok(ExecOutput { checkpoint: StageCheckpoint::new(20), done: true }))
895            .add_unwind(Ok(UnwindOutput { checkpoint: StageCheckpoint::new(1) }));
896        let (stage_c, post_execute_commit_counter_c) = stage_c.with_post_execute_commit_counter();
897        let (stage_c, post_unwind_commit_counter_c) = stage_c.with_post_unwind_commit_counter();
898
899        let mut pipeline = Pipeline::<MockNodeTypesWithDB>::builder()
900            .add_stage(stage_a)
901            .add_stage(stage_b)
902            .add_stage(stage_c)
903            .with_max_block(10)
904            .build(
905                provider_factory.clone(),
906                StaticFileProducer::new(provider_factory.clone(), PruneModes::default()),
907            );
908        let events = pipeline.events();
909
910        // Run pipeline
911        tokio::spawn(async move {
912            // Sync first
913            pipeline.run().await.expect("Could not run pipeline");
914
915            // Unwind
916            pipeline.unwind(1, None).expect("Could not unwind pipeline");
917        });
918
919        // Check that the stages were unwound in reverse order
920        assert_eq!(
921            events.collect::<Vec<PipelineEvent>>().await,
922            vec![
923                // Executing
924                PipelineEvent::Prepare {
925                    pipeline_stages_progress: PipelineStagesProgress { current: 1, total: 3 },
926                    stage_id: StageId::Other("A"),
927                    checkpoint: None,
928                    target: Some(10),
929                },
930                PipelineEvent::Run {
931                    pipeline_stages_progress: PipelineStagesProgress { current: 1, total: 3 },
932                    stage_id: StageId::Other("A"),
933                    checkpoint: None,
934                    target: Some(10),
935                },
936                PipelineEvent::Ran {
937                    pipeline_stages_progress: PipelineStagesProgress { current: 1, total: 3 },
938                    stage_id: StageId::Other("A"),
939                    result: ExecOutput { checkpoint: StageCheckpoint::new(100), done: true },
940                },
941                PipelineEvent::Prepare {
942                    pipeline_stages_progress: PipelineStagesProgress { current: 2, total: 3 },
943                    stage_id: StageId::Other("B"),
944                    checkpoint: None,
945                    target: Some(10),
946                },
947                PipelineEvent::Run {
948                    pipeline_stages_progress: PipelineStagesProgress { current: 2, total: 3 },
949                    stage_id: StageId::Other("B"),
950                    checkpoint: None,
951                    target: Some(10),
952                },
953                PipelineEvent::Ran {
954                    pipeline_stages_progress: PipelineStagesProgress { current: 2, total: 3 },
955                    stage_id: StageId::Other("B"),
956                    result: ExecOutput { checkpoint: StageCheckpoint::new(10), done: true },
957                },
958                PipelineEvent::Prepare {
959                    pipeline_stages_progress: PipelineStagesProgress { current: 3, total: 3 },
960                    stage_id: StageId::Other("C"),
961                    checkpoint: None,
962                    target: Some(10),
963                },
964                PipelineEvent::Run {
965                    pipeline_stages_progress: PipelineStagesProgress { current: 3, total: 3 },
966                    stage_id: StageId::Other("C"),
967                    checkpoint: None,
968                    target: Some(10),
969                },
970                PipelineEvent::Ran {
971                    pipeline_stages_progress: PipelineStagesProgress { current: 3, total: 3 },
972                    stage_id: StageId::Other("C"),
973                    result: ExecOutput { checkpoint: StageCheckpoint::new(20), done: true },
974                },
975                // Unwinding
976                PipelineEvent::Unwind {
977                    stage_id: StageId::Other("C"),
978                    input: UnwindInput {
979                        checkpoint: StageCheckpoint::new(20),
980                        unwind_to: 1,
981                        bad_block: None
982                    }
983                },
984                PipelineEvent::Unwound {
985                    stage_id: StageId::Other("C"),
986                    result: UnwindOutput { checkpoint: StageCheckpoint::new(1) },
987                },
988                PipelineEvent::Unwind {
989                    stage_id: StageId::Other("B"),
990                    input: UnwindInput {
991                        checkpoint: StageCheckpoint::new(10),
992                        unwind_to: 1,
993                        bad_block: None
994                    }
995                },
996                PipelineEvent::Unwound {
997                    stage_id: StageId::Other("B"),
998                    result: UnwindOutput { checkpoint: StageCheckpoint::new(1) },
999                },
1000                PipelineEvent::Unwind {
1001                    stage_id: StageId::Other("A"),
1002                    input: UnwindInput {
1003                        checkpoint: StageCheckpoint::new(100),
1004                        unwind_to: 1,
1005                        bad_block: None
1006                    }
1007                },
1008                PipelineEvent::Unwound {
1009                    stage_id: StageId::Other("A"),
1010                    result: UnwindOutput { checkpoint: StageCheckpoint::new(1) },
1011                },
1012            ]
1013        );
1014
1015        assert_eq!(post_execute_commit_counter_a.load(Ordering::Relaxed), 1);
1016        assert_eq!(post_unwind_commit_counter_a.load(Ordering::Relaxed), 1);
1017
1018        assert_eq!(post_execute_commit_counter_b.load(Ordering::Relaxed), 1);
1019        assert_eq!(post_unwind_commit_counter_b.load(Ordering::Relaxed), 1);
1020
1021        assert_eq!(post_execute_commit_counter_c.load(Ordering::Relaxed), 1);
1022        assert_eq!(post_unwind_commit_counter_c.load(Ordering::Relaxed), 1);
1023    }
1024
1025    /// Unwinds a pipeline with intermediate progress.
1026    #[tokio::test]
1027    async fn unwind_pipeline_with_intermediate_progress() {
1028        let provider_factory = create_test_provider_factory();
1029
1030        let mut pipeline = Pipeline::<MockNodeTypesWithDB>::builder()
1031            .add_stage(
1032                TestStage::new(StageId::Other("A"))
1033                    .add_exec(Ok(ExecOutput { checkpoint: StageCheckpoint::new(100), done: true }))
1034                    .add_unwind(Ok(UnwindOutput { checkpoint: StageCheckpoint::new(50) })),
1035            )
1036            .add_stage(
1037                TestStage::new(StageId::Other("B"))
1038                    .add_exec(Ok(ExecOutput { checkpoint: StageCheckpoint::new(10), done: true })),
1039            )
1040            .with_max_block(10)
1041            .build(
1042                provider_factory.clone(),
1043                StaticFileProducer::new(provider_factory.clone(), PruneModes::default()),
1044            );
1045        let events = pipeline.events();
1046
1047        // Run pipeline
1048        tokio::spawn(async move {
1049            // Sync first
1050            pipeline.run().await.expect("Could not run pipeline");
1051
1052            // Unwind
1053            pipeline.unwind(50, None).expect("Could not unwind pipeline");
1054        });
1055
1056        // Check that the stages were unwound in reverse order
1057        assert_eq!(
1058            events.collect::<Vec<PipelineEvent>>().await,
1059            vec![
1060                // Executing
1061                PipelineEvent::Prepare {
1062                    pipeline_stages_progress: PipelineStagesProgress { current: 1, total: 2 },
1063                    stage_id: StageId::Other("A"),
1064                    checkpoint: None,
1065                    target: Some(10),
1066                },
1067                PipelineEvent::Run {
1068                    pipeline_stages_progress: PipelineStagesProgress { current: 1, total: 2 },
1069                    stage_id: StageId::Other("A"),
1070                    checkpoint: None,
1071                    target: Some(10),
1072                },
1073                PipelineEvent::Ran {
1074                    pipeline_stages_progress: PipelineStagesProgress { current: 1, total: 2 },
1075                    stage_id: StageId::Other("A"),
1076                    result: ExecOutput { checkpoint: StageCheckpoint::new(100), done: true },
1077                },
1078                PipelineEvent::Prepare {
1079                    pipeline_stages_progress: PipelineStagesProgress { current: 2, total: 2 },
1080                    stage_id: StageId::Other("B"),
1081                    checkpoint: None,
1082                    target: Some(10),
1083                },
1084                PipelineEvent::Run {
1085                    pipeline_stages_progress: PipelineStagesProgress { current: 2, total: 2 },
1086                    stage_id: StageId::Other("B"),
1087                    checkpoint: None,
1088                    target: Some(10),
1089                },
1090                PipelineEvent::Ran {
1091                    pipeline_stages_progress: PipelineStagesProgress { current: 2, total: 2 },
1092                    stage_id: StageId::Other("B"),
1093                    result: ExecOutput { checkpoint: StageCheckpoint::new(10), done: true },
1094                },
1095                // Unwinding
1096                // Nothing to unwind in stage "B"
1097                PipelineEvent::Skipped { stage_id: StageId::Other("B") },
1098                PipelineEvent::Unwind {
1099                    stage_id: StageId::Other("A"),
1100                    input: UnwindInput {
1101                        checkpoint: StageCheckpoint::new(100),
1102                        unwind_to: 50,
1103                        bad_block: None
1104                    }
1105                },
1106                PipelineEvent::Unwound {
1107                    stage_id: StageId::Other("A"),
1108                    result: UnwindOutput { checkpoint: StageCheckpoint::new(50) },
1109                },
1110            ]
1111        );
1112    }
1113
1114    /// Runs a pipeline that unwinds during sync.
1115    ///
1116    /// The flow is:
1117    ///
1118    /// - Stage A syncs to block 10
1119    /// - Stage B triggers an unwind, marking block 5 as bad
1120    /// - Stage B unwinds to its previous progress, block 0 but since it is still at block 0, it is
1121    ///   skipped entirely (there is nothing to unwind)
1122    /// - Stage A unwinds to its previous progress, block 0
1123    /// - Stage A syncs back up to block 10
1124    /// - Stage B syncs to block 10
1125    /// - The pipeline finishes
1126    #[tokio::test]
1127    async fn run_pipeline_with_unwind() {
1128        let provider_factory = create_test_provider_factory();
1129
1130        let mut pipeline = Pipeline::<MockNodeTypesWithDB>::builder()
1131            .add_stage(
1132                TestStage::new(StageId::Other("A"))
1133                    .add_exec(Ok(ExecOutput { checkpoint: StageCheckpoint::new(10), done: true }))
1134                    .add_unwind(Ok(UnwindOutput { checkpoint: StageCheckpoint::new(0) }))
1135                    .add_exec(Ok(ExecOutput { checkpoint: StageCheckpoint::new(10), done: true })),
1136            )
1137            .add_stage(
1138                TestStage::new(StageId::Other("B"))
1139                    .add_exec(Err(StageError::Block {
1140                        block: Box::new(random_block_with_parent(
1141                            &mut generators::rng(),
1142                            5,
1143                            Default::default(),
1144                        )),
1145                        error: BlockErrorKind::Validation(ConsensusError::BaseFeeMissing),
1146                    }))
1147                    .add_unwind(Ok(UnwindOutput { checkpoint: StageCheckpoint::new(0) }))
1148                    .add_exec(Ok(ExecOutput { checkpoint: StageCheckpoint::new(10), done: true })),
1149            )
1150            .with_max_block(10)
1151            .build(
1152                provider_factory.clone(),
1153                StaticFileProducer::new(provider_factory.clone(), PruneModes::default()),
1154            );
1155        let events = pipeline.events();
1156
1157        // Run pipeline
1158        tokio::spawn(async move {
1159            pipeline.run().await.expect("Could not run pipeline");
1160        });
1161
1162        // Check that the stages were unwound in reverse order
1163        assert_eq!(
1164            events.collect::<Vec<PipelineEvent>>().await,
1165            vec![
1166                PipelineEvent::Prepare {
1167                    pipeline_stages_progress: PipelineStagesProgress { current: 1, total: 2 },
1168                    stage_id: StageId::Other("A"),
1169                    checkpoint: None,
1170                    target: Some(10),
1171                },
1172                PipelineEvent::Run {
1173                    pipeline_stages_progress: PipelineStagesProgress { current: 1, total: 2 },
1174                    stage_id: StageId::Other("A"),
1175                    checkpoint: None,
1176                    target: Some(10),
1177                },
1178                PipelineEvent::Ran {
1179                    pipeline_stages_progress: PipelineStagesProgress { current: 1, total: 2 },
1180                    stage_id: StageId::Other("A"),
1181                    result: ExecOutput { checkpoint: StageCheckpoint::new(10), done: true },
1182                },
1183                PipelineEvent::Prepare {
1184                    pipeline_stages_progress: PipelineStagesProgress { current: 2, total: 2 },
1185                    stage_id: StageId::Other("B"),
1186                    checkpoint: None,
1187                    target: Some(10),
1188                },
1189                PipelineEvent::Run {
1190                    pipeline_stages_progress: PipelineStagesProgress { current: 2, total: 2 },
1191                    stage_id: StageId::Other("B"),
1192                    checkpoint: None,
1193                    target: Some(10),
1194                },
1195                PipelineEvent::Error { stage_id: StageId::Other("B") },
1196                PipelineEvent::Unwind {
1197                    stage_id: StageId::Other("A"),
1198                    input: UnwindInput {
1199                        checkpoint: StageCheckpoint::new(10),
1200                        unwind_to: 0,
1201                        bad_block: Some(5)
1202                    }
1203                },
1204                PipelineEvent::Unwound {
1205                    stage_id: StageId::Other("A"),
1206                    result: UnwindOutput { checkpoint: StageCheckpoint::new(0) },
1207                },
1208                PipelineEvent::Prepare {
1209                    pipeline_stages_progress: PipelineStagesProgress { current: 1, total: 2 },
1210                    stage_id: StageId::Other("A"),
1211                    checkpoint: Some(StageCheckpoint::new(0)),
1212                    target: Some(10),
1213                },
1214                PipelineEvent::Run {
1215                    pipeline_stages_progress: PipelineStagesProgress { current: 1, total: 2 },
1216                    stage_id: StageId::Other("A"),
1217                    checkpoint: Some(StageCheckpoint::new(0)),
1218                    target: Some(10),
1219                },
1220                PipelineEvent::Ran {
1221                    pipeline_stages_progress: PipelineStagesProgress { current: 1, total: 2 },
1222                    stage_id: StageId::Other("A"),
1223                    result: ExecOutput { checkpoint: StageCheckpoint::new(10), done: true },
1224                },
1225                PipelineEvent::Prepare {
1226                    pipeline_stages_progress: PipelineStagesProgress { current: 2, total: 2 },
1227                    stage_id: StageId::Other("B"),
1228                    checkpoint: None,
1229                    target: Some(10),
1230                },
1231                PipelineEvent::Run {
1232                    pipeline_stages_progress: PipelineStagesProgress { current: 2, total: 2 },
1233                    stage_id: StageId::Other("B"),
1234                    checkpoint: None,
1235                    target: Some(10),
1236                },
1237                PipelineEvent::Ran {
1238                    pipeline_stages_progress: PipelineStagesProgress { current: 2, total: 2 },
1239                    stage_id: StageId::Other("B"),
1240                    result: ExecOutput { checkpoint: StageCheckpoint::new(10), done: true },
1241                },
1242            ]
1243        );
1244    }
1245
1246    /// Checks that the pipeline re-runs stages on non-fatal errors and stops on fatal ones.
1247    #[tokio::test]
1248    async fn pipeline_error_handling() {
1249        // Non-fatal
1250        let provider_factory = create_test_provider_factory();
1251        let mut pipeline = Pipeline::<MockNodeTypesWithDB>::builder()
1252            .add_stage(
1253                TestStage::new(StageId::Other("NonFatal"))
1254                    .add_exec(Err(StageError::Recoverable(Box::new(std::fmt::Error))))
1255                    .add_exec(Ok(ExecOutput { checkpoint: StageCheckpoint::new(10), done: true })),
1256            )
1257            .with_max_block(10)
1258            .build(
1259                provider_factory.clone(),
1260                StaticFileProducer::new(provider_factory.clone(), PruneModes::default()),
1261            );
1262        let result = pipeline.run().await;
1263        assert_matches!(result, Ok(()));
1264
1265        // Fatal
1266        let provider_factory = create_test_provider_factory();
1267        let mut pipeline = Pipeline::<MockNodeTypesWithDB>::builder()
1268            .add_stage(TestStage::new(StageId::Other("Fatal")).add_exec(Err(
1269                StageError::DatabaseIntegrity(ProviderError::BlockBodyIndicesNotFound(5)),
1270            )))
1271            .build(
1272                provider_factory.clone(),
1273                StaticFileProducer::new(provider_factory.clone(), PruneModes::default()),
1274            );
1275        let result = pipeline.run().await;
1276        assert_matches!(
1277            result,
1278            Err(PipelineError::Stage(StageError::DatabaseIntegrity(
1279                ProviderError::BlockBodyIndicesNotFound(5)
1280            )))
1281        );
1282    }
1283}