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