Skip to main content

reth_node_ethereum/snap/
mod.rs

1//! Runs a snap/2 bootstrap as the engine's backfill.
2//!
3//! One task alternates header sync and snap state writes while the engine skips forkchoice, so
4//! no other writer touches the database. Each pass:
5//!
6//! 1. Syncs headers to the target; nothing below the pivot executes.
7//! 2. Selects a pivot under the head, or resumes the recorded attempt while its pivot is canonical.
8//! 3. Downloads state against the pivot's root, carried to newer pivots by block access lists.
9//! 4. Hands the state to [`SnapHandoff`], which rebuilds the trie and publishes it at the pivot, or
10//!    abandons the attempt on a root mismatch so the next pass starts a new one.
11//!
12//! A forkchoice update ends the current step at its next boundary, so headers catch up first.
13//! The staged pipeline then runs the stages above the pivot.
14
15mod context;
16mod handoff;
17mod run;
18mod selection;
19
20pub use handoff::{HandoffOutcome, RebuildOutcome, SnapHandoff};
21pub use selection::{EthereumBackfill, EthereumBackfillSync};
22
23use alloy_consensus::BlockHeader;
24use alloy_primitives::B256;
25use futures::FutureExt;
26use reth_chainspec::EthChainSpec;
27use reth_engine_tree::backfill::{BackfillAction, BackfillEvent, BackfillSync};
28use reth_errors::RethError;
29use reth_network_p2p::{headers::client::HeadersClient, snap::client::SnapClient};
30use reth_provider::{
31    providers::ProviderNodeTypes, ChainSpecProvider, DatabaseProviderFactory, MetadataProvider,
32    ProviderFactory, ProviderResult, StageCheckpointReader,
33};
34use reth_stages::{Pipeline, PipelineError, PipelineTarget, PipelineWithResult, StageId};
35use reth_tasks::Runtime;
36use run::SnapRun;
37use std::task::{ready, Context, Poll};
38use tokio::sync::{oneshot, watch};
39use tokio_util::sync::{CancellationToken, DropGuard};
40
41/// Backfills canonical headers, then snap/2 state at a recent pivot.
42#[derive(Debug)]
43pub struct SnapBackfillSync<N: ProviderNodeTypes, C> {
44    // Serves the snap requests and reports peer counts.
45    client: C,
46    provider_factory: ProviderFactory<N>,
47    runtime: Runtime,
48    // Owns the pipeline while idle, the result channel while running.
49    state: SnapBackfillState<N>,
50    // Target queued by the engine, started on the next poll.
51    pending_target: Option<PipelineTarget>,
52    // Latest finalized block the engine reported, kept across runs.
53    finalized: watch::Sender<B256>,
54}
55
56impl<N: ProviderNodeTypes, C> SnapBackfillSync<N, C> {
57    /// Creates a backfill that has not started any work.
58    pub fn new(
59        pipeline: Pipeline<N>,
60        client: C,
61        provider_factory: ProviderFactory<N>,
62        runtime: Runtime,
63    ) -> Self {
64        Self {
65            client,
66            provider_factory,
67            runtime,
68            state: SnapBackfillState::Idle(Some(Box::new(pipeline))),
69            pending_target: None,
70            finalized: watch::channel(B256::ZERO).0,
71        }
72    }
73}
74
75impl<N, C> SnapBackfillSync<N, C>
76where
77    N: ProviderNodeTypes,
78    C: SnapClient + HeadersClient + Clone + Unpin + 'static,
79{
80    // Spawns a run if a target is queued and the pipeline is free.
81    fn try_spawn(&mut self) -> Option<BackfillEvent> {
82        if !matches!(self.state, SnapBackfillState::Idle(_)) {
83            return None
84        }
85        let target = self.pending_target.take()?;
86        // Once snap state is verified, or the node was not synced by snap, the staged pipeline
87        // backfills alone, including unwinds.
88        let event = match (self.needs_snap(), target) {
89            // The engine waits for every start to finish, so an unusable target fails the run
90            // instead of being dropped.
91            (_, PipelineTarget::Sync(hash)) if hash.is_zero() => {
92                BackfillEvent::Finished(Err(PipelineError::Internal(RethError::msg(
93                    "snap backfill cannot sync to the zero hash",
94                ))))
95            }
96            (Err(error), _) => {
97                BackfillEvent::Finished(Err(PipelineError::Internal(RethError::other(error))))
98            }
99            (Ok(false), target) => self.spawn_staged(target),
100            // Nothing executes on top of snap state before the handoff, so there is nothing a
101            // snap backfill could unwind.
102            (Ok(true), PipelineTarget::Unwind(block)) => {
103                BackfillEvent::Finished(Err(PipelineError::Internal(RethError::msg(format!(
104                    "snap backfill cannot unwind to block {block}"
105                )))))
106            }
107            (Ok(true), PipelineTarget::Sync(target)) => self.spawn_snap(target),
108        };
109        Some(event)
110    }
111
112    fn spawn_staged(&mut self, target: PipelineTarget) -> BackfillEvent {
113        let pipeline = self.take_pipeline();
114        let (result_tx, result) = oneshot::channel();
115        self.runtime.spawn_critical_blocking_task("pipeline task", async move {
116            let _ = result_tx.send(pipeline.run_as_fut(Some(target)).await);
117        });
118        self.state = SnapBackfillState::Staged(result);
119        BackfillEvent::Started(target)
120    }
121
122    fn spawn_snap(&mut self, target: B256) -> BackfillEvent {
123        let pipeline = self.take_pipeline();
124        let (result_tx, result) = oneshot::channel();
125        let (targets, target_rx) = watch::channel(target);
126        let stop = CancellationToken::new();
127        let run = SnapRun::new(
128            self.client.clone(),
129            self.provider_factory.clone(),
130            self.runtime.clone(),
131            stop.clone(),
132            self.finalized.subscribe(),
133        );
134        // Node shutdown drops this task as it does the pipeline's; every bootstrap step has either
135        // committed or left nothing behind.
136        self.runtime.spawn_critical_blocking_task("snap backfill task", async move {
137            let _ = result_tx.send(run.run(*pipeline, target_rx).await);
138        });
139        self.state = SnapBackfillState::Running { _stop: stop.drop_guard(), targets, result };
140        BackfillEvent::Started(PipelineTarget::Sync(target))
141    }
142
143    fn take_pipeline(&mut self) -> Box<Pipeline<N>> {
144        let SnapBackfillState::Idle(pipeline) = &mut self.state else {
145            unreachable!("only an idle backfill spawns a run")
146        };
147        pipeline.take().expect("idle backfill owns its pipeline")
148    }
149
150    // Snap bootstraps a node with nothing executed, and finishes any attempt it has not verified
151    // yet, including one interrupted after its state was published.
152    fn needs_snap(&self) -> ProviderResult<bool> {
153        let provider = self.provider_factory.database_provider_ro()?;
154        if let Some(attempt) = provider.snap_attempt()? {
155            return Ok(!attempt.is_verified())
156        }
157        // Genesis sets every checkpoint to its own block, which isn't always block 0.
158        let genesis = self.provider_factory.chain_spec().genesis_header().number();
159        Ok(provider
160            .get_stage_checkpoint(StageId::Execution)?
161            .is_none_or(|checkpoint| checkpoint.block_number == genesis))
162    }
163}
164
165impl<N, C> BackfillSync for SnapBackfillSync<N, C>
166where
167    N: ProviderNodeTypes,
168    C: SnapClient + HeadersClient + Clone + Unpin + 'static,
169{
170    fn on_action(&mut self, action: BackfillAction) {
171        match action {
172            // The zero hash never moves a target or anchors a pivot.
173            BackfillAction::UpdateTarget(hash) | BackfillAction::UpdateFinalized(hash)
174                if hash.is_zero() => {}
175            BackfillAction::Start(target) => self.pending_target = Some(target),
176            BackfillAction::UpdateTarget(hash) => {
177                if let SnapBackfillState::Running { targets, .. } = &self.state {
178                    targets.send_if_modified(|current| std::mem::replace(current, hash) != hash);
179                }
180            }
181            // Kept while idle too, so the next run anchors its pivot to known finality.
182            BackfillAction::UpdateFinalized(hash) => {
183                self.finalized.send_if_modified(|current| std::mem::replace(current, hash) != hash);
184            }
185        }
186    }
187
188    fn poll(&mut self, cx: &mut Context<'_>) -> Poll<BackfillEvent> {
189        if let Some(event) = self.try_spawn() {
190            return Poll::Ready(event)
191        }
192        let (SnapBackfillState::Running { result, .. } | SnapBackfillState::Staged(result)) =
193            &mut self.state
194        else {
195            return Poll::Pending
196        };
197        let event = match ready!(result.poll_unpin(cx)) {
198            Ok((pipeline, result)) => {
199                self.state = SnapBackfillState::Idle(Some(Box::new(pipeline)));
200                BackfillEvent::Finished(result)
201            }
202            Err(error) => BackfillEvent::TaskDropped(error.to_string()),
203        };
204        Poll::Ready(event)
205    }
206}
207
208// Owns the idle pipeline, or the handles of the run holding it.
209#[derive(Debug)]
210enum SnapBackfillState<N: ProviderNodeTypes> {
211    Idle(Option<Box<Pipeline<N>>>),
212    Running {
213        // Stops the run if the backfill is dropped. Declared first so the run sees the stop
214        // before the closed target channel.
215        _stop: DropGuard,
216        // Latest forkchoice target; older ones are overwritten.
217        targets: watch::Sender<B256>,
218        // Returns the pipeline with the run's result.
219        result: oneshot::Receiver<PipelineWithResult<N>>,
220    },
221    // The staged pipeline runs alone, over state that is verified or was never snap synced.
222    Staged(oneshot::Receiver<PipelineWithResult<N>>),
223}
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228    use crate::snap::run::tests::ServesHeaders;
229    use alloy_consensus::Header;
230    use futures::{future::poll_fn, Stream, StreamExt};
231    use reth_provider::{
232        test_utils::{
233            create_test_provider_factory, create_test_provider_factory_with_genesis_block_number,
234            MockNodeTypesWithDB,
235        },
236        DBProvider, DatabaseProviderFactory, MetadataWriter, StageCheckpointReader,
237        StageCheckpointWriter, StorageSettings, StorageSettingsCache,
238    };
239    use reth_prune::PruneModes;
240    use reth_stages::{
241        ControlFlow, ExecOutput, PipelineEvent, Stage, StageCheckpoint, StageError, StageId,
242        StageSetBuilder,
243    };
244    use reth_stages_api::test_utils::TestStage;
245    use reth_static_file::StaticFileProducer;
246    use std::task::Waker;
247
248    type TestBackfill = SnapBackfillSync<MockNodeTypesWithDB, ServesHeaders>;
249
250    type TestProvider =
251        <ProviderFactory<MockNodeTypesWithDB> as DatabaseProviderFactory>::ProviderRW;
252
253    // A backfill whose pipeline holds only a scripted header stage, with that pipeline's events.
254    fn backfill(headers: TestStage) -> (TestBackfill, impl Stream<Item = PipelineEvent> + Unpin) {
255        let (pipeline, factory) = pipeline(headers);
256        let events = pipeline.events();
257        let backfill =
258            SnapBackfillSync::new(pipeline, ServesHeaders::default(), factory, Runtime::test());
259        (backfill, events)
260    }
261
262    // A pipeline holding only a scripted header stage, over a database in the hashed state
263    // layout snap writes into.
264    pub(super) fn pipeline(
265        headers: TestStage,
266    ) -> (Pipeline<MockNodeTypesWithDB>, ProviderFactory<MockNodeTypesWithDB>) {
267        pipeline_with(headers, watch::channel(B256::ZERO).0)
268    }
269
270    // Like `pipeline`, with any header stage and a tip channel the test can observe.
271    pub(super) fn pipeline_with<S: Stage<TestProvider> + 'static>(
272        headers: S,
273        tip: watch::Sender<B256>,
274    ) -> (Pipeline<MockNodeTypesWithDB>, ProviderFactory<MockNodeTypesWithDB>) {
275        let factory = hashed_factory();
276        (pipeline_on(&factory, StageSetBuilder::default().add_stage(headers), tip), factory)
277    }
278
279    // A pipeline over `factory` holding `stages`.
280    pub(super) fn pipeline_on(
281        factory: &ProviderFactory<MockNodeTypesWithDB>,
282        stages: StageSetBuilder<TestProvider>,
283        tip: watch::Sender<B256>,
284    ) -> Pipeline<MockNodeTypesWithDB> {
285        Pipeline::<MockNodeTypesWithDB>::builder()
286            .add_stages(stages)
287            .with_tip_sender(tip)
288            .build(factory.clone(), StaticFileProducer::new(factory.clone(), PruneModes::default()))
289    }
290
291    // An empty database in the hashed state layout snap writes into.
292    pub(super) fn hashed_factory() -> ProviderFactory<MockNodeTypesWithDB> {
293        let factory = create_test_provider_factory();
294        let provider = factory.database_provider_rw().unwrap();
295        provider.write_storage_settings(StorageSettings::v2()).unwrap();
296        provider.commit().unwrap();
297        factory.set_storage_settings_cache(StorageSettings::v2());
298        factory
299    }
300
301    pub(super) fn headers_done(block: u64) -> Result<ExecOutput, StageError> {
302        Ok(ExecOutput { checkpoint: StageCheckpoint::new(block), done: true })
303    }
304
305    fn poll_once(backfill: &mut TestBackfill) -> Poll<BackfillEvent> {
306        backfill.poll(&mut Context::from_waker(Waker::noop()))
307    }
308
309    fn headers_checkpoint(factory: &ProviderFactory<MockNodeTypesWithDB>) -> Option<u64> {
310        let provider = factory.database_provider_ro().unwrap();
311        provider.get_stage_checkpoint(StageId::Headers).unwrap().map(|it| it.block_number)
312    }
313
314    // Waits for the running bootstrap's header stage to commit `block`.
315    pub(super) async fn headers_reach(
316        events: &mut (impl Stream<Item = PipelineEvent> + Unpin),
317        block: u64,
318    ) {
319        while let Some(event) = events.next().await {
320            if let PipelineEvent::Ran { stage_id: StageId::Headers, result, .. } = event &&
321                result.checkpoint.block_number == block
322            {
323                return
324            }
325        }
326        panic!("the pipeline stopped before headers reached {block}")
327    }
328
329    pub(super) fn target() -> B256 {
330        Header { number: 100, ..Default::default() }.hash_slow()
331    }
332
333    pub(super) fn next_target() -> B256 {
334        Header { number: 101, ..Default::default() }.hash_slow()
335    }
336
337    #[tokio::test]
338    async fn executed_state_backfills_with_the_staged_pipeline() {
339        let headers = TestStage::new(StageId::Headers).add_exec(headers_done(5));
340        let (pipeline, factory) = pipeline(headers);
341        let provider = factory.database_provider_rw().unwrap();
342        provider.save_stage_checkpoint(StageId::Execution, StageCheckpoint::new(42)).unwrap();
343        provider.commit().unwrap();
344        let mut backfill = SnapBackfillSync::new(
345            pipeline,
346            ServesHeaders::default(),
347            factory.clone(),
348            Runtime::test(),
349        );
350
351        // Executed state has nothing for snap to download, so the pipeline runs alone.
352        backfill.on_action(BackfillAction::Start(PipelineTarget::Sync(target())));
353        assert!(matches!(poll_once(&mut backfill), Poll::Ready(BackfillEvent::Started(_))));
354        assert!(matches!(backfill.state, SnapBackfillState::Staged(_)));
355        assert!(matches!(poll_fn(|cx| backfill.poll(cx)).await, BackfillEvent::Finished(Ok(_))));
356
357        assert_eq!(headers_checkpoint(&factory), Some(5));
358        assert!(matches!(backfill.state, SnapBackfillState::Idle(Some(_))));
359    }
360
361    #[test]
362    fn a_genesis_above_block_zero_still_snap_syncs() {
363        let factory = create_test_provider_factory_with_genesis_block_number(5);
364        let provider = factory.database_provider_rw().unwrap();
365        provider.save_stage_checkpoint(StageId::Execution, StageCheckpoint::new(5)).unwrap();
366        provider.commit().unwrap();
367        // Only the provider factory decides eligibility, so the pipeline's own database is unused.
368        let (pipeline, _) = pipeline(TestStage::new(StageId::Headers));
369        let backfill =
370            TestBackfill::new(pipeline, ServesHeaders::default(), factory, Runtime::test());
371
372        assert!(backfill.needs_snap().unwrap());
373    }
374
375    #[test]
376    fn a_zero_hash_target_fails_without_starting_a_run() {
377        let (mut backfill, _events) = backfill(TestStage::new(StageId::Headers));
378
379        backfill.on_action(BackfillAction::Start(PipelineTarget::Sync(B256::ZERO)));
380
381        assert!(matches!(poll_once(&mut backfill), Poll::Ready(BackfillEvent::Finished(Err(_)))));
382        assert!(matches!(backfill.state, SnapBackfillState::Idle(Some(_))));
383    }
384
385    #[test]
386    fn the_finalized_block_is_kept_while_idle() {
387        let (mut backfill, _events) = backfill(TestStage::new(StageId::Headers));
388        let finalized = backfill.finalized.subscribe();
389
390        backfill.on_action(BackfillAction::UpdateFinalized(target()));
391        backfill.on_action(BackfillAction::UpdateFinalized(B256::ZERO));
392
393        assert_eq!(*finalized.borrow(), target());
394        assert!(poll_once(&mut backfill).is_pending());
395    }
396
397    #[test]
398    fn a_target_update_while_idle_starts_nothing() {
399        let (mut backfill, _events) = backfill(TestStage::new(StageId::Headers));
400
401        backfill.on_action(BackfillAction::UpdateTarget(target()));
402
403        assert!(poll_once(&mut backfill).is_pending());
404        assert!(matches!(backfill.state, SnapBackfillState::Idle(Some(_))));
405    }
406
407    #[tokio::test]
408    async fn an_active_bootstrap_holds_the_pipeline_and_coalesces_targets() {
409        let (mut backfill, mut events) =
410            backfill(TestStage::new(StageId::Headers).add_exec(headers_done(0)));
411
412        backfill.on_action(BackfillAction::Start(PipelineTarget::Sync(target())));
413        assert!(matches!(
414            poll_once(&mut backfill),
415            Poll::Ready(BackfillEvent::Started(PipelineTarget::Sync(hash))) if hash == target()
416        ));
417        headers_reach(&mut events, 0).await;
418
419        let SnapBackfillState::Running { targets, .. } = &backfill.state else {
420            panic!("the run owns the pipeline")
421        };
422        let mut observer = targets.subscribe();
423        backfill.on_action(BackfillAction::UpdateTarget(B256::ZERO));
424        assert!(!observer.has_changed().unwrap());
425        backfill.on_action(BackfillAction::UpdateTarget(next_target()));
426        backfill.on_action(BackfillAction::UpdateTarget(next_target()));
427
428        assert!(observer.has_changed().unwrap());
429        assert_eq!(*observer.borrow_and_update(), next_target());
430        assert!(!observer.has_changed().unwrap());
431        // A second start cannot run beside the active one.
432        backfill.on_action(BackfillAction::Start(PipelineTarget::Sync(next_target())));
433        assert!(poll_once(&mut backfill).is_pending());
434    }
435
436    #[tokio::test]
437    async fn a_dropped_backfill_stops_its_run_and_releases_the_pipeline() {
438        let (mut backfill, mut events) =
439            backfill(TestStage::new(StageId::Headers).add_exec(headers_done(0)));
440        backfill.on_action(BackfillAction::Start(PipelineTarget::Sync(target())));
441        assert!(poll_once(&mut backfill).is_ready());
442        headers_reach(&mut events, 0).await;
443
444        let SnapBackfillState::Running { _stop, result, .. } =
445            std::mem::replace(&mut backfill.state, SnapBackfillState::Idle(None))
446        else {
447            panic!("the run owns the pipeline")
448        };
449        drop(_stop);
450
451        let (_pipeline, result) = result.await.unwrap();
452        assert!(matches!(result, Ok(ControlFlow::NoProgress { block_number: None })));
453    }
454
455    #[tokio::test]
456    async fn node_shutdown_keeps_committed_progress() {
457        let (mut backfill, mut events) =
458            backfill(TestStage::new(StageId::Headers).add_exec(headers_done(0)));
459        backfill.on_action(BackfillAction::Start(PipelineTarget::Sync(target())));
460        assert!(poll_once(&mut backfill).is_ready());
461        headers_reach(&mut events, 0).await;
462
463        backfill.runtime.graceful_shutdown();
464
465        // The runtime drops the task; the engine treats that as fatal and the node exits.
466        assert!(matches!(poll_fn(|cx| backfill.poll(cx)).await, BackfillEvent::TaskDropped(_)));
467        assert_eq!(headers_checkpoint(&backfill.provider_factory), Some(0));
468    }
469
470    #[tokio::test]
471    async fn a_failed_run_returns_the_pipeline() {
472        let (mut backfill, _events) =
473            backfill(TestStage::new(StageId::Headers).add_exec(Err(StageError::ChannelClosed)));
474        backfill.on_action(BackfillAction::Start(PipelineTarget::Sync(target())));
475        assert!(poll_once(&mut backfill).is_ready());
476
477        assert!(matches!(poll_fn(|cx| backfill.poll(cx)).await, BackfillEvent::Finished(Err(_))));
478        // Control and the pipeline are back with the engine, ready for the next target.
479        assert!(matches!(backfill.state, SnapBackfillState::Idle(Some(_))));
480    }
481
482    #[tokio::test]
483    async fn an_unwind_target_fails_without_starting_a_run() {
484        let (mut backfill, _events) = backfill(TestStage::new(StageId::Headers));
485
486        backfill.on_action(BackfillAction::Start(PipelineTarget::Unwind(1)));
487
488        assert!(matches!(poll_once(&mut backfill), Poll::Ready(BackfillEvent::Finished(Err(_)))));
489        assert!(matches!(backfill.state, SnapBackfillState::Idle(Some(_))));
490    }
491}