Skip to main content

reth_snap_sync/
bootstrap.rs

1//! Runs snap synchronization from pivot selection until its state is handed to the trie rebuild.
2//!
3//! Every step commits before the next one starts and the attempt record is re-read on each pass,
4//! so a run stopped anywhere resumes from what the database holds.
5
6use crate::{
7    metrics::SnapSyncMetrics, AccountRangeDownload, AccountRangeStep, BlockAccessListCatchUp,
8    BytecodeDownload, BytecodeStep, CatchUpStep, SnapAccountStore, SnapAttemptStore,
9    SnapCatchUpStore, SnapGeneration, SnapPivotPolicy, SnapStateVerifier, SnapSyncError,
10    SnapSyncSession, SnapWrite, StorageRangeDownload, StorageRangeStep, VerifiedRange,
11    DEFAULT_SCAN_CHUNK,
12};
13use alloy_eips::BlockNumHash;
14use reth_db_api::transaction::DbTxMut;
15use reth_network_p2p::{error::RequestError, snap::client::SnapClient};
16use reth_primitives_traits::AlloyBlockHeader;
17use reth_storage_api::{
18    BlockHashReader, DBProvider, DatabaseProviderFactory, HeaderProvider, MetadataProvider,
19    MetadataWriter, StageCheckpointWriter, StateWriter,
20};
21use reth_tasks::Runtime;
22use std::{fmt, future::Future};
23use tokio_util::sync::CancellationToken;
24use tracing::{debug, info};
25
26/// Default number of account ranges committed between pivot checks.
27///
28/// Bounds how long a pivot can lag unnoticed while ranges download.
29pub const DEFAULT_RANGES_PER_CHECK: usize = 64;
30
31/// Drives snap synchronization until its downloaded state is ready for the trie rebuild.
32///
33/// Each pass moves a lagging pivot forward, applies the lists carrying the state to it, refetches
34/// scheduled repairs there, then downloads account ranges with storage and code. A reorg across the
35/// pivot is repaired from the orphaned lists, while handed-off or expired state starts over.
36pub struct SnapBootstrap<C, F, X> {
37    factory: F,
38    // Blocking database work runs off the async worker.
39    runtime: Runtime,
40    // Head, finality and peer progress come from the node running the sync.
41    context: X,
42    policy: SnapPivotPolicy,
43    // Target of the attempt being driven, rebuilt from the attempt record on every pass.
44    session: SnapSyncSession,
45    ranges_per_check: usize,
46    // Stops the run at the next step boundary, leaving committed progress in place.
47    cancel: CancellationToken,
48    // Stops the all-or-nothing hand-off scan, which `cancel` otherwise stops.
49    shutdown: Option<CancellationToken>,
50    accounts: AccountRangeDownload<C, F>,
51    storage: StorageRangeDownload<C, F>,
52    bytecode: BytecodeDownload<C, F>,
53    catch_up: BlockAccessListCatchUp<C, F>,
54    metrics: SnapSyncMetrics,
55}
56
57impl<C: Clone, F: Clone, X> SnapBootstrap<C, F, X> {
58    /// Creates a run that has not touched the network or the database yet.
59    pub fn new(client: C, factory: F, runtime: Runtime, context: X) -> Self {
60        let policy = SnapPivotPolicy::default();
61        Self {
62            accounts: AccountRangeDownload::new(client.clone(), factory.clone(), runtime.clone()),
63            storage: StorageRangeDownload::new(client.clone(), factory.clone(), runtime.clone()),
64            bytecode: BytecodeDownload::new(client.clone(), factory.clone(), runtime.clone()),
65            catch_up: BlockAccessListCatchUp::new(client, factory.clone(), runtime.clone()),
66            factory,
67            runtime,
68            context,
69            policy,
70            session: SnapSyncSession::new(policy),
71            ranges_per_check: DEFAULT_RANGES_PER_CHECK,
72            cancel: CancellationToken::new(),
73            shutdown: None,
74            metrics: SnapSyncMetrics::default(),
75        }
76    }
77}
78
79impl<C, F, X> SnapBootstrap<C, F, X> {
80    /// Returns this run anchoring and re-anchoring pivots with `policy`.
81    pub const fn with_policy(mut self, policy: SnapPivotPolicy) -> Self {
82        self.policy = policy;
83        self.session = SnapSyncSession::new(policy);
84        self
85    }
86
87    /// Returns this run committing at most `ranges` account ranges between pivot checks, at
88    /// least one.
89    pub const fn with_ranges_per_check(mut self, ranges: usize) -> Self {
90        self.ranges_per_check = if ranges == 0 { 1 } else { ranges };
91        self
92    }
93
94    /// Returns this run stopping once `cancel` fires.
95    pub fn with_cancellation(mut self, cancel: CancellationToken) -> Self {
96        self.cancel = cancel;
97        self
98    }
99
100    /// Returns this run finishing the hand-off scan unless `shutdown` fires.
101    ///
102    /// The scan restarts from scratch once stopped, so [`Self::with_cancellation`] lets it finish.
103    pub fn with_shutdown(mut self, shutdown: CancellationToken) -> Self {
104        self.shutdown = Some(shutdown);
105        self
106    }
107}
108
109impl<C, F, X> SnapBootstrap<C, F, X>
110where
111    C: SnapClient + Clone + Unpin,
112    F: DatabaseProviderFactory + Clone + 'static,
113    F::Provider: HeaderProvider + MetadataProvider + BlockHashReader,
114    F::ProviderRW: HeaderProvider
115        + BlockHashReader
116        + MetadataProvider
117        + MetadataWriter
118        + StageCheckpointWriter
119        + StateWriter
120        + DBProvider<Tx: DbTxMut>,
121    X: SnapSyncContext,
122{
123    /// Runs until the downloaded state is handed to the merkle stage, or the run stops.
124    ///
125    /// Peers that do not serve the state, or headers not downloaded yet, wait for the context to
126    /// report progress instead of failing the run.
127    pub async fn run(&mut self) -> Result<SnapBootstrapOutcome, SnapSyncError> {
128        // Waits recur every few seconds, so only the first for a reason logs at info until a pass
129        // makes progress.
130        let mut announced = None;
131        loop {
132            if self.cancel.is_cancelled() {
133                return Ok(SnapBootstrapOutcome::Stopped)
134            }
135            let head = self.context.head()?;
136            let step = match self.resolve(head)? {
137                Resolved::Verified(pivot) => return Ok(SnapBootstrapOutcome::Verified { pivot }),
138                Resolved::BeforeBlockAccessLists => {
139                    return Ok(SnapBootstrapOutcome::BeforeBlockAccessLists)
140                }
141                Resolved::Waiting => {
142                    if announced.replace(Wait::Pivot) == Some(Wait::Pivot) {
143                        debug!(target: "sync::snap", head, "No eligible snap pivot yet, waiting for headers");
144                    } else {
145                        info!(target: "sync::snap", head, "No eligible snap pivot yet, waiting for headers");
146                    }
147                    Step::Wait
148                }
149                Resolved::Active(write) => match self.drive(write, head).await {
150                    Ok(step) => step,
151                    // The network fails snap requests at once while no connected peer serves them.
152                    Err(SnapSyncError::Request(RequestError::UnsupportedCapability)) => {
153                        if announced.replace(Wait::SnapPeer) == Some(Wait::SnapPeer) {
154                            debug!(target: "sync::snap", "No connected peer serves snap/2, waiting for one");
155                        } else {
156                            info!(target: "sync::snap", "No connected peer serves snap/2, waiting for one");
157                        }
158                        Step::Wait
159                    }
160                    Err(error) if error.is_transient() => {
161                        debug!(target: "sync::snap", %error, "Waiting for peers or headers");
162                        Step::Wait
163                    }
164                    // The next pass finds the pivot orphaned and recovers or restarts it.
165                    Err(error) if error.is_reorg() => {
166                        debug!(target: "sync::snap", %error, "Snap pivot was reorged");
167                        Step::Continue
168                    }
169                    // Progress this build cannot read is unusable, so the attempt starts over.
170                    Err(
171                        error @ (SnapSyncError::UnsupportedRecord { .. } |
172                        SnapSyncError::UnreadableRecord { .. }),
173                    ) => {
174                        info!(target: "sync::snap", %error, "Snap progress is unreadable, restarting");
175                        Step::Restart
176                    }
177                    Err(SnapSyncError::Cancelled) => Step::Stop,
178                    Err(error) => return Err(error),
179                },
180            };
181            match step {
182                Step::HandedOff(write) => {
183                    let pivot = self.pivot()?;
184                    info!(target: "sync::snap", ?pivot, "Snap state handed to the trie rebuild");
185                    return Ok(SnapBootstrapOutcome::TrieRebuild { write, pivot })
186                }
187                Step::Continue => announced = None,
188                Step::Wait => {
189                    if !self.wait(head).await {
190                        return Ok(SnapBootstrapOutcome::Stopped)
191                    }
192                }
193                Step::Restart => {
194                    let provider = self.factory.database_provider_rw()?;
195                    provider.abandon_snap_attempt()?;
196                    provider.commit()?;
197                    self.metrics.restarts.increment(1);
198                    self.metrics.clear_attempt();
199                }
200                Step::Stop => return Ok(SnapBootstrapOutcome::Stopped),
201            }
202        }
203    }
204
205    // Resumes the recorded attempt, even one whose pivot a reorg orphaned, otherwise starts one at
206    // the pivot under `head`. Starting one reads the kept blocks and writes a few records, cheap
207    // enough for the async worker.
208    fn resolve(&mut self, head: u64) -> Result<Resolved, SnapSyncError> {
209        let provider = self.factory.database_provider_rw()?;
210        let mut session = SnapSyncSession::new(self.policy);
211        if let Some(attempt) = provider.snap_attempt()? {
212            if attempt.is_verified() {
213                return Ok(Resolved::Verified(attempt.pivot()))
214            }
215            if let Some(write) = provider.active_snap_write()? {
216                // An orphaned pivot is resumed too, so the pass can recover or restart it.
217                session.resume(SnapGeneration::new(attempt.pivot(), attempt.state_root()));
218                self.session = session;
219                return Ok(Resolved::Active(write))
220            }
221        }
222
223        session.select(&provider, head, self.context.finalized()?)?;
224        let Some(generation) = session.start() else {
225            // A head without a block access list predates them, so no block under it can anchor.
226            // Once the staged pipeline executes past genesis, the node stays on it. The first
227            // header pass may stop at the finalized block, so if block access lists
228            // activated between it and the head, the node full syncs. That window is
229            // narrow and accepted.
230            return match provider.sealed_header(head)? {
231                Some(header) if header.block_access_list_hash().is_none() => {
232                    Ok(Resolved::BeforeBlockAccessLists)
233                }
234                _ => Ok(Resolved::Waiting),
235            }
236        };
237        let write = provider.start_snap_attempt(generation)?;
238        provider.start_account_coverage(write)?;
239        provider.commit()?;
240        info!(
241            target: "sync::snap",
242            pivot = ?generation.target(),
243            state_root = %generation.state_root(),
244            "Started snap attempt"
245        );
246        self.session = session;
247        Ok(Resolved::Active(write))
248    }
249
250    // One pass over the attempt: reorg recovery, pivot, catch-up, repairs, then up to
251    // `ranges_per_check` account ranges.
252    async fn drive(&mut self, write: SnapWrite, head: u64) -> Result<Step, SnapSyncError> {
253        let (applied, complete) = {
254            let provider = self.factory.database_provider_ro()?;
255            let generation = self.session.target().copied().ok_or(SnapSyncError::NoAttempt)?;
256            self.metrics.pivot_block.set(generation.target().number as f64);
257            let handed_off = provider.is_trie_rebuild_started(write)?;
258            let canonical = generation.is_canonical(&provider)?;
259            if handed_off {
260                // Handed-off state may be partly rebuilt, so it cannot be repaired in place.
261                if !canonical {
262                    info!(target: "sync::snap", pivot = ?generation.target(), "Handed-off snap pivot was reorged, restarting");
263                    return Ok(Step::Restart)
264                }
265                // The hand-off required every account, lists through the pivot and no repairs.
266                self.metrics.set_account_progress(None);
267                self.metrics.applied_block.set(generation.target().number as f64);
268                return Ok(Step::HandedOff(write))
269            }
270            let applied = provider
271                .catch_up_progress(write)?
272                .ok_or(SnapSyncError::NoCatchUpProgress)?
273                .applied();
274            let coverage = provider.account_coverage(write)?;
275            // Recovery can wait for orphaned lists, so publish the stored progress first.
276            self.metrics.applied_block.set(applied.number as f64);
277            if let Some(coverage) = coverage {
278                self.metrics.set_account_progress(coverage.next());
279            }
280            if !canonical {
281                drop(provider);
282                return self.recover(write, head).await
283            }
284            // Repairs are fetched at the pivot, so pending ones still need it served.
285            let complete = coverage.is_some_and(|c| c.is_complete()) &&
286                provider.snap_repairs(write)?.is_empty();
287            (applied, complete)
288        };
289        // Complete state carried to its pivot needs no further lists, only its trie rebuilt, so
290        // neither their retention nor a newer pivot applies to it.
291        if complete && applied == self.pivot()? {
292            return self.hand_off(write).await
293        }
294        // Catch-up continues from the last applied block, not the pivot, so once that block's
295        // successor is no longer served the state cannot be carried to any newer pivot.
296        if !self.policy.is_catchable_from(applied.number, head) {
297            info!(target: "sync::snap", ?applied, head, "Snap catch-up outlived the served block access lists, restarting");
298            return Ok(Step::Restart)
299        }
300        let write = self.advance_pivot(write, head)?;
301        self.metrics.pivot_block.set(self.pivot()?.number as f64);
302        // Lists only update accounts already downloaded, so coverage grows once they reach the
303        // pivot.
304        if let Some(step) = self.catch_up(write).await? {
305            return Ok(step)
306        }
307        // Repairs take the pivot's values, which the rest of the state holds once the lists reach
308        // it.
309        if let Some(step) = self.repair().await? {
310            return Ok(step)
311        }
312        self.download_ranges(write).await
313    }
314
315    // Repairs what a reorg across the pivot left in the downloaded state, as EIP-8189 describes:
316    // the orphaned blocks' lists schedule what they changed for repair, and catch-up continues from
317    // the last block both branches share. Unserved lists are waited for within the served state
318    // window.
319    async fn recover(&mut self, write: SnapWrite, head: u64) -> Result<Step, SnapSyncError> {
320        let pivot = self.pivot()?;
321        let provider = self.factory.database_provider_ro()?;
322        let Some(reorg) = provider.snap_reorg(write)? else {
323            info!(target: "sync::snap", ?pivot, "Snap pivot was reorged past its kept blocks, restarting");
324            return Ok(Step::Restart)
325        };
326        let ancestor = reorg.ancestor();
327        // Lists activate by timestamp, which grows along a chain, so an ancestor committing to one
328        // means every block after it on either branch does too.
329        let committed = provider
330            .sealed_header(ancestor.number)?
331            .is_some_and(|header| header.block_access_list_hash().is_some());
332        if !committed {
333            info!(target: "sync::snap", ?pivot, ?ancestor, "Snap pivot was reorged across block access list activation, restarting");
334            return Ok(Step::Restart)
335        }
336        let resume = provider
337            .catch_up_progress(write)?
338            .ok_or(SnapSyncError::NoCatchUpProgress)?
339            .resume_after(ancestor)
340            .number;
341        if !self.policy.is_catchable_from(resume, head) {
342            info!(target: "sync::snap", ?pivot, ?ancestor, resume, head, "Orphaned block access lists expired, restarting");
343            return Ok(Step::Restart)
344        }
345        let generation = self.policy.select(&provider, head, self.context.finalized()?)?;
346        // The new pivot must descend from the ancestor, or the new branch is still too short.
347        // Checked first, so waiting for it does not fetch the orphaned lists on every pass.
348        let Some(generation) = generation.filter(|g| g.target().number >= ancestor.number) else {
349            return Ok(Step::Wait)
350        };
351        drop(provider);
352        // A peer lacking side-chain lists says nothing of the others, so another is asked later.
353        let Some(lists) = self.catch_up.orphaned_lists(reorg.orphaned()).await? else {
354            if !self.policy.awaits_orphaned_lists(ancestor.number, head) {
355                info!(target: "sync::snap", ?pivot, ?ancestor, head, "Orphaned block access lists stayed unavailable, restarting");
356                return Ok(Step::Restart)
357            }
358            debug!(target: "sync::snap", ?pivot, "Orphaned block access lists are unavailable");
359            return Ok(Step::Wait)
360        };
361
362        // Scheduling reads every orphaned list, so it runs on the blocking pool.
363        self.commit_blocking(move |provider| {
364            provider.commit_reorg_recovery(write, ancestor, &lists, generation).map(|_| ())
365        })
366        .await?;
367        info!(target: "sync::snap", ?pivot, ?ancestor, to = ?generation.target(), "Recovered snap state from a pivot reorg");
368        Ok(Step::Continue)
369    }
370
371    // Moves a lagging pivot forward, since a few lists cost less than downloading state peers no
372    // longer serve. Returns the write the attempt accepts afterwards.
373    fn advance_pivot(&mut self, write: SnapWrite, head: u64) -> Result<SnapWrite, SnapSyncError> {
374        let provider = self.factory.database_provider_rw()?;
375        let from = self.pivot()?;
376        let Some(advanced) =
377            self.session.advance(&provider, write, head, self.context.finalized()?)?
378        else {
379            return Ok(write)
380        };
381        // The database takes one writer at a time, so this commits before any download does.
382        provider.commit()?;
383        info!(target: "sync::snap", ?from, to = ?self.pivot()?, "Advanced snap pivot");
384        Ok(advanced)
385    }
386
387    // Applies lists until the downloaded state reaches the pivot. `Some` ends the pass.
388    async fn catch_up(&mut self, write: SnapWrite) -> Result<Option<Step>, SnapSyncError> {
389        let pivot = self.pivot()?.number;
390        loop {
391            match self.catch_up.next(write, pivot).await? {
392                CatchUpStep::Complete => return Ok(None),
393                CatchUpStep::Applied { progress, .. } => {
394                    debug!(target: "sync::snap", applied = ?progress.applied(), pivot, "Applied block access lists");
395                    self.metrics.applied_block.set(progress.applied().number as f64);
396                }
397                CatchUpStep::Unavailable { peer_id, .. } => {
398                    debug!(target: "sync::snap", ?peer_id, pivot, "Peer does not serve the pivot's block access lists");
399                    return Ok(Some(Step::Wait))
400                }
401            }
402            if self.cancel.is_cancelled() {
403                return Ok(Some(Step::Stop))
404            }
405        }
406    }
407
408    // Fetches scheduled repairs again at the pivot, with the slots and code they need, committing
409    // each batch. `Some` ends the pass, at the latest after `ranges_per_check` commits so the pivot
410    // is checked again.
411    async fn repair(&mut self) -> Result<Option<Step>, SnapSyncError> {
412        for _ in 0..self.ranges_per_check {
413            let range = match self.accounts.next_repair().await? {
414                None => return Ok(None),
415                Some(AccountRangeStep::Verified(range)) => range,
416                Some(AccountRangeStep::Unavailable { .. }) => return Ok(Some(Step::Wait)),
417            };
418            let Some(slots) = self.storage.repair_slots(&range).await? else {
419                return Ok(Some(Step::Wait))
420            };
421            if let Some(step) = self.download_code(&range).await? {
422                return Ok(Some(step))
423            }
424            let hashed_address = range.origin();
425            let remaining = self.accounts.commit_repair(range, slots).await?;
426            debug!(target: "sync::snap", %hashed_address, remaining, "Committed snap repair batch");
427            if self.cancel.is_cancelled() {
428                return Ok(Some(Step::Stop))
429            }
430        }
431        Ok(Some(Step::Continue))
432    }
433
434    // Commits up to `ranges_per_check` account ranges, handing the state off once none remain.
435    async fn download_ranges(&mut self, write: SnapWrite) -> Result<Step, SnapSyncError> {
436        for _ in 0..self.ranges_per_check {
437            if self.cancel.is_cancelled() {
438                return Ok(Step::Stop)
439            }
440            let range = match self.accounts.next().await? {
441                Some(AccountRangeStep::Verified(range)) => range,
442                Some(AccountRangeStep::Unavailable { origin, peer_id }) => {
443                    debug!(target: "sync::snap", ?peer_id, %origin, "Peer does not serve the pivot state");
444                    return Ok(Step::Wait)
445                }
446                None => return self.hand_off(write).await,
447            };
448            if let Some(step) = self.download_storage_and_code(&range).await? {
449                return Ok(step)
450            }
451            let coverage = self.accounts.commit(range, Default::default(), Vec::new()).await?;
452            self.metrics.set_account_progress(coverage.next());
453        }
454        Ok(Step::Continue)
455    }
456
457    // Persists the storage and code `range` needs. `Some` ends the pass. Both commit as they
458    // arrive, so a range dropped here is fetched again without repeating them.
459    async fn download_storage_and_code(
460        &mut self,
461        range: &VerifiedRange,
462    ) -> Result<Option<Step>, SnapSyncError> {
463        loop {
464            match self.storage.next(range).await? {
465                StorageRangeStep::Complete => break,
466                StorageRangeStep::Committed(_) => {}
467                StorageRangeStep::Unavailable { peer_id, .. } => {
468                    debug!(target: "sync::snap", ?peer_id, "Peer does not serve the pivot's storage");
469                    return Ok(Some(Step::Wait))
470                }
471            }
472            // A large contract takes many responses, each committed, so any of them is a
473            // resumable place to stop.
474            if self.cancel.is_cancelled() {
475                return Ok(Some(Step::Stop))
476            }
477        }
478        self.download_code(range).await
479    }
480
481    // Persists the code `range` references. `Some` ends the pass.
482    async fn download_code(
483        &mut self,
484        range: &VerifiedRange,
485    ) -> Result<Option<Step>, SnapSyncError> {
486        loop {
487            match self.bytecode.next(range).await? {
488                BytecodeStep::Complete => return Ok(None),
489                BytecodeStep::Committed { .. } => {}
490                BytecodeStep::Unavailable { peer_id, .. } => {
491                    debug!(target: "sync::snap", ?peer_id, "Peer does not serve the pivot's code");
492                    return Ok(Some(Step::Wait))
493                }
494            }
495            if self.cancel.is_cancelled() {
496                return Ok(Some(Step::Stop))
497            }
498        }
499    }
500
501    // Checks the downloaded state is complete and hands it to the merkle stage. The scan reads
502    // every account, so it runs on the blocking pool.
503    async fn hand_off(&self, write: SnapWrite) -> Result<Step, SnapSyncError> {
504        let cancel = self.shutdown.as_ref().unwrap_or(&self.cancel).clone();
505        self.commit_blocking(move |provider| {
506            provider.start_trie_rebuild(write, DEFAULT_SCAN_CHUNK, &cancel)
507        })
508        .await?;
509        Ok(Step::HandedOff(write))
510    }
511
512    // Runs `apply` in one transaction on the blocking pool, committing only if it succeeds.
513    async fn commit_blocking(
514        &self,
515        apply: impl FnOnce(&F::ProviderRW) -> Result<(), SnapSyncError> + Send + 'static,
516    ) -> Result<(), SnapSyncError> {
517        let factory = self.factory.clone();
518        self.runtime
519            .spawn_blocking(move || -> Result<(), SnapSyncError> {
520                let provider = factory.database_provider_rw()?;
521                apply(&provider)?;
522                provider.commit()?;
523                Ok(())
524            })
525            .await
526            .map_err(SnapSyncError::blocking_task)?
527    }
528
529    // Pivot of the attempt being driven.
530    fn pivot(&self) -> Result<BlockNumHash, SnapSyncError> {
531        self.session.target().map(SnapGeneration::target).ok_or(SnapSyncError::NoAttempt)
532    }
533
534    // Whether the context reported progress before the run was cancelled.
535    async fn wait(&mut self, head: u64) -> bool {
536        let Self { cancel, context, .. } = self;
537        cancel.run_until_cancelled(context.wait_for_progress(head)).await.unwrap_or(false)
538    }
539}
540
541impl<C, F, X> fmt::Debug for SnapBootstrap<C, F, X> {
542    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
543        f.debug_struct("SnapBootstrap")
544            .field("policy", &self.policy)
545            .field("session", &self.session)
546            .field("ranges_per_check", &self.ranges_per_check)
547            .finish_non_exhaustive()
548    }
549}
550
551/// How a [`SnapBootstrap`] run ended.
552#[derive(Clone, Copy, Debug, Eq, PartialEq)]
553pub enum SnapBootstrapOutcome {
554    /// The downloaded state is complete and handed to the merkle stage; once the stage reaches
555    /// `pivot`, [`SnapStateVerifier::verify_state_root`] accepts it under `write`.
556    TrieRebuild {
557        /// Write the state was downloaded under.
558        write: SnapWrite,
559        /// Block the state is anchored to.
560        pivot: BlockNumHash,
561    },
562    /// An earlier run's state was already verified, so there is nothing to download.
563    Verified {
564        /// Block the verified state is anchored to.
565        pivot: BlockNumHash,
566    },
567    /// The run was cancelled or the context reported no further progress. Committed progress is
568    /// kept for the next run.
569    Stopped,
570    /// The head predates block access lists, so snap/2 can't sync this chain yet.
571    BeforeBlockAccessLists,
572}
573
574/// What a [`SnapBootstrap`] needs to know about the chain and peers it synchronizes from.
575pub trait SnapSyncContext: Send {
576    /// Highest block whose canonical header is downloaded.
577    fn head(&self) -> Result<u64, SnapSyncError>;
578
579    /// Latest finalized block, preferred as the pivot when recent enough. `None` if no finalized
580    /// block is known locally yet.
581    fn finalized(&self) -> Result<Option<u64>, SnapSyncError> {
582        Ok(None)
583    }
584
585    /// Waits for a reason to retry the stalled step and returns `true` if one came, such as new
586    /// peers connecting or a retry delay passing. Returns `false` if the run should stop instead,
587    /// so the caller can refresh its headers, as when a new target arrived or the source of
588    /// targets is gone.
589    fn wait_for_progress(&mut self, head: u64) -> impl Future<Output = bool> + Send;
590}
591
592// Whether the attempt being driven can continue, and how.
593enum Resolved {
594    // An unfinished attempt, resumed or just started, accepting this write.
595    Active(SnapWrite),
596    // An earlier run's state was verified at this pivot, so nothing is left to download.
597    Verified(BlockNumHash),
598    // No block is eligible as a pivot yet.
599    Waiting,
600    // The head predates block access lists, so no block can be a pivot.
601    BeforeBlockAccessLists,
602}
603
604// What one pass over the attempt left to do.
605enum Step {
606    HandedOff(SnapWrite),
607    // The range budget ran out; the pivot is checked again before more ranges.
608    Continue,
609    // Peers or headers are missing until the node progresses.
610    Wait,
611    // The attempt's state can no longer be carried forward.
612    Restart,
613    // The run was cancelled; committed progress stays for the next one.
614    Stop,
615}
616
617// Why a run waits, so a repeated wait logs at debug.
618#[derive(Clone, Copy, PartialEq, Eq)]
619enum Wait {
620    Pivot,
621    SnapPeer,
622}
623
624#[cfg(test)]
625mod tests {
626    use super::*;
627    use crate::{
628        common::SnapRecord,
629        reorg::StoredAncestry,
630        test_utils::{
631            account, account_range, hashed_factory, header, key, policy, state_root,
632            storage_ranges, storage_root_of, stored_slots, verified_range, ReorgFactoryExt,
633            ScriptedSnapClient,
634        },
635        StateRepairs, VerifiedSnapState,
636    };
637    use alloy_eip7928::{
638        compute_block_access_list_hash, AccountChanges, BalanceChange, BlockAccessIndex,
639    };
640    use alloy_primitives::{keccak256, Address, Bytes, B256, U256};
641    use metrics_util::debugging::{DebugValue, DebuggingRecorder, Snapshotter};
642    use reth_eth_wire_types::{
643        snap::{AccountRangeMessage, BlockAccessListsMessage},
644        BlockAccessLists,
645    };
646    use reth_network_p2p::{error::PeerRequestResult, snap::client::SnapResponse};
647    use reth_network_peers::{PeerId, WithPeerId};
648    use reth_primitives_traits::{Account, AlloyBlockHeader, Header, SealedHeader};
649    use reth_provider::{
650        test_utils::{insert_headers, MockNodeTypesWithDB},
651        ProviderFactory,
652    };
653    use reth_stages::stages::MerkleStage;
654    use reth_stages_api::{ExecInput, Stage};
655    use reth_stages_types::StageId;
656    use reth_storage_api::{SnapAttemptId, StageCheckpointReader};
657    use reth_trie_common::{HashedPostState, TrieAccount};
658    use std::{
659        cell::RefCell,
660        collections::{HashMap, VecDeque},
661        sync::Arc,
662    };
663
664    type Factory = ProviderFactory<MockNodeTypesWithDB>;
665    type Bootstrap = SnapBootstrap<Arc<ScriptedSnapClient>, Factory, TestContext>;
666
667    const FAR: B256 = B256::repeat_byte(0xaa);
668    // Changed on the orphaned branch, which credited it.
669    const STALE: Address = Address::repeat_byte(0x51);
670    // Untouched by the orphaned branch.
671    const KEPT: Address = Address::repeat_byte(0x52);
672
673    fn accounts() -> Vec<(B256, TrieAccount)> {
674        vec![(key(1), account(1)), (key(2), account(2)), (FAR, account(3))]
675    }
676
677    // Blocks `0..=tip`, each committing to an empty list and to `root`, so any of them anchors
678    // the same state.
679    fn insert_chain(factory: &Factory, tip: u64, root: B256) {
680        insert_headers(factory, &chain(tip, root));
681    }
682
683    // A peer serving `blocks` empty lists.
684    fn empty_lists(request_id: u64, blocks: usize) -> PeerRequestResult<SnapResponse> {
685        lists(request_id, &vec![Vec::new(); blocks], true)
686    }
687
688    // A peer holding no list for the first block it is asked for.
689    fn no_lists(request_id: u64) -> PeerRequestResult<SnapResponse> {
690        lists(request_id, &[Vec::new()], false)
691    }
692
693    fn scripted(
694        factory: &Factory,
695        responses: impl IntoIterator<Item = PeerRequestResult<SnapResponse>>,
696        heads: impl IntoIterator<Item = u64>,
697    ) -> (Arc<ScriptedSnapClient>, Bootstrap) {
698        let client = Arc::new(ScriptedSnapClient::new(responses));
699        let context = TestContext { heads: RefCell::new(heads.into_iter().collect()), waits: 0 };
700        let bootstrap =
701            SnapBootstrap::new(Arc::clone(&client), factory.clone(), Runtime::test(), context)
702                .with_policy(policy());
703        (client, bootstrap)
704    }
705
706    // An attempt at block 2 that downloaded all of `accounts()`, not handed off yet.
707    fn downloaded(factory: &Factory) -> SnapWrite {
708        let accounts = accounts();
709        insert_chain(factory, 3, state_root(&accounts));
710        let provider = factory.database_provider_rw().unwrap();
711        let pivot = provider.sealed_header(2).unwrap().unwrap().num_hash();
712        let write =
713            provider.start_snap_attempt(SnapGeneration::new(pivot, state_root(&accounts))).unwrap();
714        provider.start_account_coverage(write).unwrap();
715        let range = verified_range(&accounts, 0..accounts.len(), B256::ZERO, &[]);
716        provider.commit_account_range(write, &range, Default::default(), Vec::new()).unwrap();
717        provider.commit().unwrap();
718        write
719    }
720
721    // Gives `bootstrap` metrics recorded in the returned snapshotter. `default` handles are cached
722    // process-wide, so they are registered afresh for each recorder.
723    fn recorded(mut bootstrap: Bootstrap) -> (Snapshotter, Bootstrap) {
724        let recorder = DebuggingRecorder::new();
725        let snapshotter = recorder.snapshotter();
726        bootstrap.metrics = metrics::with_local_recorder(&recorder, || {
727            SnapSyncMetrics::new_with_labels(Vec::<metrics::Label>::new())
728        });
729        (snapshotter, bootstrap)
730    }
731
732    // Taking a snapshot resets every value, so it is taken once per check.
733    fn snapshot(snapshotter: &Snapshotter) -> HashMap<String, DebugValue> {
734        snapshotter
735            .snapshot()
736            .into_vec()
737            .into_iter()
738            .map(|(key, _, _, value)| {
739                (key.key().name().trim_start_matches("sync.snap.").to_owned(), value)
740            })
741            .collect()
742    }
743
744    fn attempt_id(factory: &Factory) -> SnapAttemptId {
745        factory.database_provider_ro().unwrap().snap_attempt().unwrap().unwrap().id()
746    }
747
748    // Serves heads in order, repeating the last, and ends the run at the first wait.
749    struct TestContext {
750        heads: RefCell<VecDeque<u64>>,
751        waits: usize,
752    }
753
754    impl SnapSyncContext for TestContext {
755        fn head(&self) -> Result<u64, SnapSyncError> {
756            let mut heads = self.heads.borrow_mut();
757            let head = if heads.len() > 1 { heads.pop_front() } else { heads.front().copied() };
758            Ok(head.expect("the context is given at least one head"))
759        }
760
761        fn wait_for_progress(&mut self, _head: u64) -> impl Future<Output = bool> + Send {
762            self.waits += 1;
763            std::future::ready(false)
764        }
765    }
766
767    // Blocks `0..=tip`, each committing to an empty list and to `root`.
768    fn chain(tip: u64, root: B256) -> Vec<SealedHeader> {
769        let access_list_hash = compute_block_access_list_hash(&Vec::<AccountChanges>::new());
770        let mut parent = B256::ZERO;
771        (0..=tip)
772            .map(|number| {
773                let mut header = header(number, parent, Some(access_list_hash));
774                header.state_root = root;
775                let sealed = SealedHeader::seal_slow(header);
776                parent = sealed.hash();
777                sealed
778            })
779            .collect()
780    }
781
782    // Blocks continuing `parent`, one per list, each committing to its list and to `root`.
783    fn branch(
784        parent: &SealedHeader,
785        lists: &[Vec<AccountChanges>],
786        root: B256,
787    ) -> Vec<SealedHeader> {
788        let mut parent = parent.clone();
789        lists
790            .iter()
791            .map(|list| {
792                let access_list_hash = compute_block_access_list_hash(list);
793                let mut header = header(parent.number() + 1, parent.hash(), Some(access_list_hash));
794                header.state_root = root;
795                parent = SealedHeader::seal_slow(header);
796                parent.clone()
797            })
798            .collect()
799    }
800
801    // A peer serving `lists` in order, or none of them.
802    fn lists(
803        request_id: u64,
804        lists: &[Vec<AccountChanges>],
805        served: bool,
806    ) -> PeerRequestResult<SnapResponse> {
807        let block_access_lists =
808            lists.iter().map(|list| served.then(|| alloy_rlp::encode(list).into())).collect();
809        let message = BlockAccessListsMessage {
810            request_id,
811            block_access_lists: BlockAccessLists(block_access_lists),
812        };
813        Ok(WithPeerId::new(PeerId::random(), SnapResponse::BlockAccessLists(message)))
814    }
815
816    // Rebuilds the trie of `write`'s state up to `target` and checks it against that header.
817    fn rebuild_and_verify(factory: &Factory, write: SnapWrite, target: u64) -> VerifiedSnapState {
818        let provider = factory.database_provider_rw().unwrap();
819        let mut stage = MerkleStage::default_execution();
820        loop {
821            let checkpoint = provider.get_stage_checkpoint(StageId::MerkleExecute).unwrap();
822            let output =
823                stage.execute(&provider, ExecInput { target: Some(target), checkpoint }).unwrap();
824            provider.save_stage_checkpoint(StageId::MerkleExecute, output.checkpoint).unwrap();
825            if output.done {
826                break
827            }
828        }
829        provider.verify_state_root(write).unwrap()
830    }
831
832    // The accounts at every block of the new branch, which never credits `STALE`.
833    fn reorg_accounts() -> Vec<(B256, TrieAccount)> {
834        let mut accounts = vec![(keccak256(STALE), account(1)), (keccak256(KEPT), account(2))];
835        accounts.sort_by_key(|(hashed_address, _)| *hashed_address);
836        accounts
837    }
838
839    fn stale_changes() -> AccountChanges {
840        AccountChanges::new(STALE)
841    }
842
843    // An attempt that downloaded every account at pivot 4 of a branch crediting `STALE` in blocks
844    // 3 and 4, then a reorg to a branch forking after block 2 whose first lists are `new_lists`.
845    // Returns the orphaned headers and the new branch's.
846    fn reorged(
847        factory: &Factory,
848        new_lists: [Vec<AccountChanges>; 2],
849    ) -> (SnapAttemptId, Vec<SealedHeader>, Vec<SealedHeader>) {
850        let accounts = reorg_accounts();
851        let root = state_root(&accounts);
852        let shared = chain(2, root);
853        let orphaned = branch(&shared[2], &orphaned_lists(), root);
854        insert_headers(factory, &shared);
855        insert_headers(factory, &orphaned);
856        let provider = factory.database_provider_rw().unwrap();
857        let write =
858            provider.start_snap_attempt(SnapGeneration::new(orphaned[1].num_hash(), root)).unwrap();
859        provider.start_account_coverage(write).unwrap();
860        let range = verified_range(&accounts, 0..2, B256::ZERO, &[]);
861        provider.commit_account_range(write, &range, Default::default(), Vec::new()).unwrap();
862        // The range was served at the orphaned pivot, holding its credit.
863        let mut credited = Account::from(account(1));
864        credited.balance = U256::from(1_000);
865        let stale = HashedPostState::default().with_accounts([(keccak256(STALE), Some(credited))]);
866        provider.write_hashed_state(&stale.into_sorted()).unwrap();
867        provider.commit().unwrap();
868
869        let [first, second] = new_lists;
870        let new = branch(&shared[2], &[first, second, Vec::new()], root);
871        factory.replace_headers_after(2, &new);
872        (attempt_id(factory), orphaned, new)
873    }
874
875    // The orphaned branch's lists for blocks 3 and 4, crediting `STALE` in each.
876    fn orphaned_lists() -> Vec<Vec<AccountChanges>> {
877        [999, 1_000]
878            .map(|balance| {
879                vec![stale_changes().with_balance_change(BalanceChange::new(
880                    BlockAccessIndex::new(1),
881                    U256::from(balance),
882                ))]
883            })
884            .to_vec()
885    }
886
887    // A peer serving no account range.
888    fn unserved_range(request_id: u64) -> PeerRequestResult<SnapResponse> {
889        let message = AccountRangeMessage { request_id, accounts: Vec::new(), proof: Vec::new() };
890        Ok(WithPeerId::new(PeerId::random(), SnapResponse::AccountRange(message)))
891    }
892
893    // A peer serving the account at `hashed_address` alone.
894    fn served_account(
895        request_id: u64,
896        accounts: &[(B256, TrieAccount)],
897        hashed_address: B256,
898    ) -> PeerRequestResult<SnapResponse> {
899        let index = accounts.iter().position(|(key, _)| *key == hashed_address).unwrap();
900        account_range(request_id, accounts, index..index + 1, &[hashed_address])
901    }
902
903    fn hashes(headers: &[SealedHeader]) -> Vec<B256> {
904        headers.iter().map(SealedHeader::hash).collect()
905    }
906
907    // Runs a bootstrap through a reorg to a branch whose first lists are `new_lists`, returning
908    // the account ranges it fetched again.
909    async fn recover(new_lists: [Vec<AccountChanges>; 2], repaired: bool) -> Vec<B256> {
910        let accounts = reorg_accounts();
911        let factory = hashed_factory();
912        let (attempt, orphaned, new) = reorged(&factory, new_lists.clone());
913        let mut responses = vec![
914            lists(1, &orphaned_lists(), true),
915            // Blocks 3 and 4 of the new branch carry the state to its pivot.
916            lists(2, &new_lists, true),
917        ];
918        if repaired {
919            responses.push(served_account(1, &accounts, keccak256(STALE)));
920        }
921        let (client, mut bootstrap) = scripted(&factory, responses, [5]);
922
923        let outcome = bootstrap.run().await.unwrap();
924
925        let SnapBootstrapOutcome::TrieRebuild { write, pivot } = outcome else {
926            panic!("the state is repaired: {outcome:?}")
927        };
928        assert_eq!(pivot, new[1].num_hash());
929        assert_eq!(attempt_id(&factory), attempt);
930        assert_eq!(*client.block_requests(), [hashes(&orphaned), hashes(&new[..2])]);
931        let origins = client.origins().clone();
932        // The repaired state is the new pivot's, as its header commits to.
933        let verified = rebuild_and_verify(&factory, write, pivot.number);
934        assert_eq!(verified.state_root(), state_root(&accounts));
935        origins
936    }
937
938    #[tokio::test]
939    async fn downloads_the_state_and_hands_it_to_the_trie_rebuild() {
940        let accounts = accounts();
941        let factory = hashed_factory();
942        insert_chain(&factory, 3, state_root(&accounts));
943        let (client, mut bootstrap) =
944            scripted(&factory, [account_range(1, &accounts, 0..3, &[])], [3]);
945
946        let outcome = bootstrap.run().await.unwrap();
947
948        let SnapBootstrapOutcome::TrieRebuild { write, pivot } = outcome else {
949            panic!("the state is complete: {outcome:?}")
950        };
951        assert_eq!(pivot.number, 2);
952        assert_eq!(*client.origins(), [B256::ZERO]);
953        let provider = factory.database_provider_ro().unwrap();
954        assert!(provider.is_trie_rebuild_started(write).unwrap());
955    }
956
957    #[tokio::test]
958    async fn pending_repairs_follow_the_pivot_once_the_accounts_are_complete() {
959        let accounts = accounts();
960        let root = state_root(&accounts);
961        let factory = hashed_factory();
962        insert_chain(&factory, 8, root);
963        let provider = factory.database_provider_rw().unwrap();
964        let pivot = provider.sealed_header(2).unwrap().unwrap().num_hash();
965        let write = provider.start_snap_attempt(SnapGeneration::new(pivot, root)).unwrap();
966        provider.start_account_coverage(write).unwrap();
967        let range = verified_range(&accounts, 0..3, B256::ZERO, &[]);
968        provider.commit_account_range(write, &range, Default::default(), Vec::new()).unwrap();
969        let mut repairs = StateRepairs::default();
970        repairs.insert_account(key(2));
971        provider.schedule_snap_repairs(write, repairs).unwrap();
972        provider.commit().unwrap();
973        // No peer serves the repair at pivot 2 any more.
974        let (_, mut bootstrap) = scripted(&factory, [unserved_range(1)], [3]);
975        assert_eq!(bootstrap.run().await.unwrap(), SnapBootstrapOutcome::Stopped);
976
977        // Once the head moves on, the pivot advances and the repair is fetched there.
978        let responses = [empty_lists(1, 5), account_range(1, &accounts, 1..2, &[key(2)])];
979        let (client, mut bootstrap) = scripted(&factory, responses, [8]);
980        let outcome = bootstrap.run().await.unwrap();
981
982        let SnapBootstrapOutcome::TrieRebuild { pivot, .. } = outcome else {
983            panic!("the state is complete: {outcome:?}")
984        };
985        assert_eq!(pivot.number, 7);
986        assert_eq!(*client.origins(), [key(2)]);
987    }
988
989    #[tokio::test]
990    async fn scheduled_repairs_are_fetched_at_the_pivot_before_ranges() {
991        let accounts = accounts();
992        let factory = hashed_factory();
993        insert_chain(&factory, 3, state_root(&accounts));
994        let provider = factory.database_provider_rw().unwrap();
995        let pivot = provider.sealed_header(2).unwrap().unwrap().num_hash();
996        let write =
997            provider.start_snap_attempt(SnapGeneration::new(pivot, state_root(&accounts))).unwrap();
998        provider.start_account_coverage(write).unwrap();
999        let mut repairs = StateRepairs::default();
1000        repairs.insert_account(key(2));
1001        provider.schedule_snap_repairs(write, repairs).unwrap();
1002        provider.commit().unwrap();
1003        // One download fetches the repair and then the ranges, numbering both requests.
1004        let responses =
1005            [account_range(1, &accounts, 1..2, &[key(2)]), account_range(2, &accounts, 0..3, &[])];
1006        let (client, mut bootstrap) = scripted(&factory, responses, [3]);
1007
1008        let outcome = bootstrap.run().await.unwrap();
1009
1010        assert!(matches!(outcome, SnapBootstrapOutcome::TrieRebuild { .. }), "{outcome:?}");
1011        assert_eq!(*client.origins(), [key(2), B256::ZERO]);
1012        let provider = factory.database_provider_ro().unwrap();
1013        assert!(provider.snap_repairs(write).unwrap().is_empty());
1014    }
1015
1016    #[tokio::test]
1017    async fn repairs_yield_to_pivot_checks_between_batches() {
1018        let accounts = accounts();
1019        let root = state_root(&accounts);
1020        let factory = hashed_factory();
1021        insert_chain(&factory, 8, root);
1022        let provider = factory.database_provider_rw().unwrap();
1023        let pivot = provider.sealed_header(2).unwrap().unwrap().num_hash();
1024        let write = provider.start_snap_attempt(SnapGeneration::new(pivot, root)).unwrap();
1025        provider.start_account_coverage(write).unwrap();
1026        let range = verified_range(&accounts, 0..3, B256::ZERO, &[]);
1027        provider.commit_account_range(write, &range, Default::default(), Vec::new()).unwrap();
1028        let mut repairs = StateRepairs::default();
1029        repairs.insert_account(key(1));
1030        repairs.insert_account(key(2));
1031        provider.schedule_snap_repairs(write, repairs).unwrap();
1032        provider.commit().unwrap();
1033        let responses = [
1034            account_range(1, &accounts, 0..1, &[key(1)]),
1035            // The head moved past the advance window meanwhile, so the pivot moves to block 7.
1036            empty_lists(1, 5),
1037            account_range(2, &accounts, 1..2, &[key(2)]),
1038        ];
1039        let (client, bootstrap) = scripted(&factory, responses, [3, 8]);
1040        let mut bootstrap = bootstrap.with_ranges_per_check(1);
1041
1042        let outcome = bootstrap.run().await.unwrap();
1043
1044        let SnapBootstrapOutcome::TrieRebuild { pivot, .. } = outcome else {
1045            panic!("the state is repaired: {outcome:?}")
1046        };
1047        assert_eq!(pivot.number, 7);
1048        assert_eq!(*client.origins(), [key(1), key(2)]);
1049        assert_eq!(client.block_requests().len(), 1);
1050    }
1051
1052    #[tokio::test]
1053    async fn a_lagging_pivot_advances_before_more_ranges_download() {
1054        let accounts = accounts();
1055        let root = state_root(&accounts);
1056        let factory = hashed_factory();
1057        insert_chain(&factory, 8, root);
1058        let responses = [
1059            account_range(1, &accounts, 0..1, &[key(1)]),
1060            // Blocks 3 through 7 carry the first account from pivot 2 to pivot 7.
1061            empty_lists(1, 5),
1062            account_range(2, &accounts, 1..3, &[key(2), FAR]),
1063        ];
1064        // The head moves past the advance window after the first range.
1065        let (client, bootstrap) = scripted(&factory, responses, [3, 8]);
1066        let (snapshotter, mut bootstrap) = recorded(bootstrap.with_ranges_per_check(1));
1067
1068        let outcome = bootstrap.run().await.unwrap();
1069
1070        let SnapBootstrapOutcome::TrieRebuild { write, pivot } = outcome else {
1071            panic!("the state is complete: {outcome:?}")
1072        };
1073        assert_eq!(pivot.number, 7);
1074        assert_eq!(*client.origins(), [B256::ZERO, key(2)]);
1075        assert_eq!(client.block_requests().len(), 1);
1076        let metrics = snapshot(&snapshotter);
1077        assert_eq!(metrics["pivot_block"], DebugValue::Gauge(7.0.into()));
1078        assert_eq!(metrics["applied_block"], DebugValue::Gauge(7.0.into()));
1079        assert_eq!(metrics["account_progress"], DebugValue::Gauge(1.0.into()));
1080
1081        // The merkle stage rebuilds the trie, which the pivot's header then accepts.
1082        let verified = rebuild_and_verify(&factory, write, 7);
1083        assert_eq!((verified.target(), verified.state_root()), (pivot, root));
1084    }
1085
1086    #[tokio::test]
1087    async fn peers_without_the_state_wait_and_the_next_run_resumes() {
1088        let accounts = accounts();
1089        let factory = hashed_factory();
1090        insert_chain(&factory, 3, state_root(&accounts));
1091        let responses = [
1092            account_range(1, &accounts, 0..1, &[key(1)]),
1093            Err(RequestError::UnsupportedCapability),
1094        ];
1095        let (_, mut bootstrap) = scripted(&factory, responses, [3]);
1096
1097        assert_eq!(bootstrap.run().await.unwrap(), SnapBootstrapOutcome::Stopped);
1098        assert_eq!(bootstrap.context.waits, 1);
1099        let attempt = attempt_id(&factory);
1100
1101        let (client, mut resumed) =
1102            scripted(&factory, [account_range(1, &accounts, 1..3, &[key(2), FAR])], [3]);
1103        let outcome = resumed.run().await.unwrap();
1104
1105        assert!(matches!(outcome, SnapBootstrapOutcome::TrieRebuild { .. }));
1106        assert_eq!(attempt_id(&factory), attempt);
1107        assert_eq!(*client.origins(), [key(2)]);
1108    }
1109
1110    #[tokio::test]
1111    async fn no_eligible_pivot_waits_without_starting_an_attempt() {
1112        let factory = hashed_factory();
1113        insert_chain(&factory, 0, state_root(&accounts()));
1114        let (client, mut bootstrap) = scripted(&factory, [], [0]);
1115
1116        assert_eq!(bootstrap.run().await.unwrap(), SnapBootstrapOutcome::Stopped);
1117
1118        assert_eq!(bootstrap.context.waits, 1);
1119        assert!(client.origins().is_empty());
1120        assert!(factory.database_provider_ro().unwrap().snap_attempt().unwrap().is_none());
1121    }
1122
1123    #[tokio::test]
1124    async fn a_head_before_block_access_lists_falls_back_without_waiting() {
1125        let factory = hashed_factory();
1126        let genesis = header(0, B256::ZERO, None);
1127        insert_headers(&factory, &[SealedHeader::seal_slow(genesis)]);
1128        let (client, mut bootstrap) = scripted(&factory, [], [0]);
1129
1130        assert_eq!(bootstrap.run().await.unwrap(), SnapBootstrapOutcome::BeforeBlockAccessLists);
1131
1132        assert_eq!(bootstrap.context.waits, 0);
1133        assert!(client.origins().is_empty());
1134        assert!(factory.database_provider_ro().unwrap().snap_attempt().unwrap().is_none());
1135    }
1136
1137    #[tokio::test]
1138    async fn a_reorged_pivot_without_kept_blocks_restarts_the_attempt() {
1139        let accounts = accounts();
1140        let factory = hashed_factory();
1141        let blocks = chain(3, state_root(&accounts));
1142        insert_headers(&factory, &blocks);
1143        // A previous run anchored to a block the canonical chain then replaced.
1144        let provider = factory.database_provider_rw().unwrap();
1145        let orphaned = SnapGeneration::new(blocks[2].num_hash(), state_root(&accounts));
1146        provider.start_snap_attempt(orphaned).unwrap();
1147        StoredAncestry::clear(&provider).unwrap();
1148        provider.commit().unwrap();
1149        let mut replaced = chain(3, state_root(&accounts));
1150        replaced[2] = SealedHeader::seal_slow(Header {
1151            extra_data: Bytes::from_static(&[1]),
1152            ..replaced[2].header().clone()
1153        });
1154        replaced[3] = SealedHeader::seal_slow(Header {
1155            parent_hash: replaced[2].hash(),
1156            ..replaced[3].header().clone()
1157        });
1158        factory.replace_headers_after(1, &replaced[2..]);
1159        let orphaned = attempt_id(&factory);
1160        let (_, bootstrap) = scripted(&factory, [account_range(1, &accounts, 0..3, &[])], [3]);
1161        let (snapshotter, mut bootstrap) = recorded(bootstrap);
1162
1163        let outcome = bootstrap.run().await.unwrap();
1164
1165        let SnapBootstrapOutcome::TrieRebuild { pivot, .. } = outcome else {
1166            panic!("the state is complete: {outcome:?}")
1167        };
1168        assert_ne!(pivot.hash, blocks[2].hash());
1169        assert_ne!(attempt_id(&factory), orphaned);
1170        let metrics = snapshot(&snapshotter);
1171        assert_eq!(metrics["restarts"], DebugValue::Counter(1));
1172    }
1173
1174    #[tokio::test]
1175    async fn a_handed_off_attempt_is_not_downloaded_again() {
1176        let accounts = accounts();
1177        let factory = hashed_factory();
1178        insert_chain(&factory, 3, state_root(&accounts));
1179        let (_, mut bootstrap) = scripted(&factory, [account_range(1, &accounts, 0..3, &[])], [3]);
1180        let first = bootstrap.run().await.unwrap();
1181
1182        let (client, resumed) = scripted(&factory, [], [3]);
1183        let (snapshotter, mut resumed) = recorded(resumed);
1184
1185        assert_eq!(resumed.run().await.unwrap(), first);
1186        assert!(client.origins().is_empty());
1187        // The stored progress is reported while the trie rebuilds.
1188        let metrics = snapshot(&snapshotter);
1189        assert_eq!(metrics["account_progress"], DebugValue::Gauge(1.0.into()));
1190        assert_eq!(metrics["applied_block"], DebugValue::Gauge(2.0.into()));
1191    }
1192
1193    #[tokio::test]
1194    async fn a_catch_up_stalled_past_the_served_lists_restarts() {
1195        let accounts = accounts();
1196        let factory = hashed_factory();
1197        insert_chain(&factory, 11, state_root(&accounts));
1198        // The pivot moves from 2 to 7, but no peer serves block 3's list.
1199        let responses = [account_range(1, &accounts, 0..1, &[key(1)]), no_lists(1)];
1200        let (_, stalled) = scripted(&factory, responses, [3, 8]);
1201        let mut stalled = stalled.with_ranges_per_check(1);
1202        assert_eq!(stalled.run().await.unwrap(), SnapBootstrapOutcome::Stopped);
1203        let attempt = attempt_id(&factory);
1204
1205        // Pivot 7 is still recent under head 11, but block 3's list is past the served history.
1206        let (client, mut restarted) =
1207            scripted(&factory, [account_range(1, &accounts, 0..3, &[])], [11]);
1208        let outcome = restarted.run().await.unwrap();
1209
1210        let SnapBootstrapOutcome::TrieRebuild { pivot, .. } = outcome else {
1211            panic!("the state is complete: {outcome:?}")
1212        };
1213        assert_eq!(pivot.number, 10);
1214        assert_ne!(attempt_id(&factory), attempt);
1215        assert!(client.block_requests().is_empty());
1216    }
1217
1218    #[tokio::test]
1219    async fn complete_state_is_handed_off_past_the_served_lists() {
1220        let accounts = accounts();
1221        let factory = hashed_factory();
1222        insert_chain(&factory, 11, state_root(&accounts));
1223        // A run committed every range at pivot 2, then stopped before the hand-off.
1224        let provider = factory.database_provider_rw().unwrap();
1225        let pivot = provider.sealed_header(2).unwrap().unwrap();
1226        let generation = SnapGeneration::new(pivot.num_hash(), pivot.state_root);
1227        let downloaded = provider.start_snap_attempt(generation).unwrap();
1228        provider.start_account_coverage(downloaded).unwrap();
1229        let range = verified_range(&accounts, 0..3, B256::ZERO, &[]);
1230        provider.commit_account_range(downloaded, &range, Default::default(), Vec::new()).unwrap();
1231        provider.commit().unwrap();
1232
1233        // Block 3's list is past the served history under head 11, but nothing needs it.
1234        let (client, resumed) = scripted(&factory, [], [11]);
1235        let (snapshotter, mut resumed) = recorded(resumed);
1236        let outcome = resumed.run().await.unwrap();
1237
1238        assert_eq!(
1239            outcome,
1240            SnapBootstrapOutcome::TrieRebuild { write: downloaded, pivot: pivot.num_hash() }
1241        );
1242        assert!(client.origins().is_empty());
1243        assert!(client.block_requests().is_empty());
1244        // The resumed run reports the stored progress without committing anything.
1245        let metrics = snapshot(&snapshotter);
1246        assert_eq!(metrics["pivot_block"], DebugValue::Gauge(2.0.into()));
1247        assert_eq!(metrics["account_progress"], DebugValue::Gauge(1.0.into()));
1248    }
1249
1250    #[tokio::test]
1251    async fn unreadable_progress_restarts_the_attempt() {
1252        // A record another build wrote, and one that is not a record at all.
1253        for record in [br#"{"version":999}"#.as_slice(), b"not json"] {
1254            let accounts = accounts();
1255            let factory = hashed_factory();
1256            insert_chain(&factory, 3, state_root(&accounts));
1257            let (_, mut stopped) =
1258                scripted(&factory, [Err(RequestError::UnsupportedCapability)], [3]);
1259            assert_eq!(stopped.run().await.unwrap(), SnapBootstrapOutcome::Stopped);
1260            // Another build rewrote the coverage the attempt resumes from.
1261            let provider = factory.database_provider_rw().unwrap();
1262            let stale = provider.active_snap_write().unwrap().unwrap();
1263            provider.write_metadata("snap_account_coverage", record.to_vec()).unwrap();
1264            provider.commit().unwrap();
1265
1266            let (_, mut restarted) =
1267                scripted(&factory, [account_range(1, &accounts, 0..3, &[])], [3]);
1268            let outcome = restarted.run().await.unwrap();
1269
1270            let SnapBootstrapOutcome::TrieRebuild { write, .. } = outcome else {
1271                panic!("the state is complete: {outcome:?}")
1272            };
1273            assert_ne!(write.attempt(), stale.attempt());
1274            let provider = factory.database_provider_ro().unwrap();
1275            assert!(matches!(
1276                provider.authorize_snap_write(stale),
1277                Err(SnapSyncError::StaleWrite { .. })
1278            ));
1279        }
1280    }
1281
1282    #[tokio::test]
1283    async fn cancellation_between_storage_responses_stops_the_run() {
1284        let slots = vec![(key(1), U256::from(11)), (key(2), U256::from(12))];
1285        let mut contract = account(1);
1286        contract.storage_root = storage_root_of(&slots);
1287        let accounts = vec![(key(1), contract)];
1288        let factory = hashed_factory();
1289        insert_chain(&factory, 3, state_root(&accounts));
1290        let cancel = CancellationToken::new();
1291        let on_request = cancel.clone();
1292        // The contract's storage takes two responses, and shutdown fires during the first.
1293        let client = Arc::new(
1294            ScriptedSnapClient::new([
1295                account_range(1, &accounts, 0..1, &[]),
1296                storage_ranges(1, &[&slots[..1]], &slots, &[B256::ZERO, key(1)]),
1297            ])
1298            .on_storage_request(move || on_request.cancel()),
1299        );
1300        let context = TestContext { heads: RefCell::new(VecDeque::from([3])), waits: 0 };
1301        let mut bootstrap =
1302            SnapBootstrap::new(Arc::clone(&client), factory.clone(), Runtime::test(), context)
1303                .with_policy(policy())
1304                .with_cancellation(cancel);
1305
1306        assert_eq!(bootstrap.run().await.unwrap(), SnapBootstrapOutcome::Stopped);
1307        assert_eq!(client.storage_requests().len(), 1);
1308    }
1309
1310    #[tokio::test]
1311    async fn a_new_branch_too_short_for_a_pivot_waits_before_fetching_lists() {
1312        let factory = hashed_factory();
1313        let (attempt, _, _) = reorged(&factory, [Vec::new(), Vec::new()]);
1314        // Head 2 puts the pivot at block 1, below the ancestor at block 2.
1315        let (client, mut bootstrap) = scripted(&factory, [], [2]);
1316
1317        assert_eq!(bootstrap.run().await.unwrap(), SnapBootstrapOutcome::Stopped);
1318
1319        assert!(client.block_requests().is_empty());
1320        assert_eq!(attempt_id(&factory), attempt);
1321    }
1322
1323    #[tokio::test]
1324    async fn a_handed_off_pivot_reorged_before_verification_restarts_the_attempt() {
1325        let accounts = accounts();
1326        let root = state_root(&accounts);
1327        let factory = hashed_factory();
1328        let shared = chain(3, root);
1329        insert_headers(&factory, &shared);
1330        let (_, mut bootstrap) = scripted(&factory, [account_range(1, &accounts, 0..3, &[])], [3]);
1331        let SnapBootstrapOutcome::TrieRebuild { pivot: orphaned, .. } =
1332            bootstrap.run().await.unwrap()
1333        else {
1334            panic!("the state is complete")
1335        };
1336        let attempt = attempt_id(&factory);
1337        // Blocks 2 and 3 are replaced before the rebuild verifies the orphaned pivot.
1338        let new = branch(&shared[1], &[vec![stale_changes()], Vec::new()], root);
1339        factory.replace_headers_after(1, &new);
1340        let (_, mut resumed) = scripted(&factory, [account_range(1, &accounts, 0..3, &[])], [3]);
1341
1342        let outcome = resumed.run().await.unwrap();
1343
1344        let SnapBootstrapOutcome::TrieRebuild { write, pivot } = outcome else {
1345            panic!("the new branch's state is complete: {outcome:?}")
1346        };
1347        assert_ne!(pivot, orphaned);
1348        assert_eq!(pivot, new[0].num_hash());
1349        assert_ne!(attempt_id(&factory), attempt);
1350        let verified = rebuild_and_verify(&factory, write, pivot.number);
1351        assert_eq!(verified.state_root(), root);
1352    }
1353
1354    #[tokio::test]
1355    async fn a_field_only_the_orphaned_branch_changed_is_fetched_again() {
1356        let origins = recover([Vec::new(), Vec::new()], true).await;
1357
1358        // The untouched account is kept, never fetched again.
1359        assert_eq!(origins, [keccak256(STALE)]);
1360    }
1361
1362    #[tokio::test]
1363    async fn a_field_the_new_branch_changes_too_needs_no_repair() {
1364        let balance = stale_changes()
1365            .with_balance_change(BalanceChange::new(BlockAccessIndex::new(1), U256::ONE));
1366
1367        let origins = recover([vec![balance], Vec::new()], false).await;
1368
1369        assert!(origins.is_empty());
1370    }
1371
1372    #[tokio::test]
1373    async fn a_second_reorg_repairs_what_the_first_new_branch_changed() {
1374        let accounts = reorg_accounts();
1375        let factory = hashed_factory();
1376        let kept = AccountChanges::new(KEPT)
1377            .with_balance_change(BalanceChange::new(BlockAccessIndex::new(1), U256::from(7)));
1378        let first_lists = [vec![kept], Vec::new()];
1379        let (attempt, _, first) = reorged(&factory, first_lists.clone());
1380        // Catch-up applies the first new branch, then no peer serves the repair at its pivot.
1381        let responses =
1382            [lists(1, &orphaned_lists(), true), lists(2, &first_lists, true), unserved_range(1)];
1383        let (_, mut bootstrap) = scripted(&factory, responses, [5]);
1384        assert_eq!(bootstrap.run().await.unwrap(), SnapBootstrapOutcome::Stopped);
1385
1386        // After a restart, a second reorg orphans the re-anchored pivot too.
1387        let shared = factory.database_provider_ro().unwrap().sealed_header(2).unwrap().unwrap();
1388        let second = branch(&shared, &[Vec::new(), Vec::new(), Vec::new()], state_root(&accounts));
1389        factory.replace_headers_after(2, &second);
1390        let mut responses =
1391            vec![lists(1, &first_lists, true), lists(2, &[Vec::new(), Vec::new()], true)];
1392        // Both the first orphaned branch's and the first new branch's changes are fetched again.
1393        let mut repaired = [keccak256(STALE), keccak256(KEPT)];
1394        repaired.sort();
1395        for (id, hashed_address) in (1..).zip(repaired) {
1396            responses.push(served_account(id, &accounts, hashed_address));
1397        }
1398        let (client, mut bootstrap) = scripted(&factory, responses, [5]);
1399
1400        let outcome = bootstrap.run().await.unwrap();
1401
1402        let SnapBootstrapOutcome::TrieRebuild { write, pivot } = outcome else {
1403            panic!("the state is repaired: {outcome:?}")
1404        };
1405        assert_eq!(pivot, second[1].num_hash());
1406        assert_eq!(attempt_id(&factory), attempt);
1407        assert_eq!(client.block_requests()[0], hashes(&first[..2]));
1408        assert_eq!(*client.origins(), repaired);
1409        let verified = rebuild_and_verify(&factory, write, pivot.number);
1410        assert_eq!(verified.state_root(), state_root(&accounts));
1411    }
1412
1413    #[tokio::test]
1414    async fn unserved_orphaned_lists_are_waited_for() {
1415        let accounts = reorg_accounts();
1416        let factory = hashed_factory();
1417        let (attempt, orphaned, new) = reorged(&factory, [Vec::new(), Vec::new()]);
1418        let (_, bootstrap) = scripted(&factory, [lists(1, &orphaned_lists(), false)], [5]);
1419        let (snapshotter, mut bootstrap) = recorded(bootstrap);
1420        assert_eq!(bootstrap.run().await.unwrap(), SnapBootstrapOutcome::Stopped);
1421        assert_eq!(attempt_id(&factory), attempt);
1422        let metrics = snapshot(&snapshotter);
1423        assert_eq!(metrics["account_progress"], DebugValue::Gauge(1.0.into()));
1424        assert_eq!(metrics["applied_block"], DebugValue::Gauge(4.0.into()));
1425
1426        // A restarted node resumes the orphaned attempt, and another peer serves them.
1427        let responses = [
1428            lists(1, &orphaned_lists(), true),
1429            lists(2, &[Vec::new(), Vec::new()], true),
1430            served_account(1, &accounts, keccak256(STALE)),
1431        ];
1432        let (client, mut bootstrap) = scripted(&factory, responses, [5]);
1433        let outcome = bootstrap.run().await.unwrap();
1434
1435        let SnapBootstrapOutcome::TrieRebuild { pivot, .. } = outcome else {
1436            panic!("the state is repaired: {outcome:?}")
1437        };
1438        assert_eq!(pivot, new[1].num_hash());
1439        assert_eq!(attempt_id(&factory), attempt);
1440        assert_eq!(client.block_requests()[0], hashes(&orphaned));
1441    }
1442
1443    #[tokio::test]
1444    async fn a_reorg_across_block_access_list_activation_restarts_the_attempt() {
1445        let accounts = accounts();
1446        let root = state_root(&accounts);
1447        let factory = hashed_factory();
1448        let mut shared = chain(1, root);
1449        // The ancestor predates block access lists, so the branches may hold blocks without them.
1450        let mut ancestor = header(2, shared[1].hash(), None);
1451        ancestor.state_root = root;
1452        shared.push(SealedHeader::seal_slow(ancestor));
1453        let orphaned = branch(&shared[2], &orphaned_lists(), root);
1454        insert_headers(&factory, &shared);
1455        insert_headers(&factory, &orphaned);
1456        let provider = factory.database_provider_rw().unwrap();
1457        provider.start_snap_attempt(SnapGeneration::new(orphaned[1].num_hash(), root)).unwrap();
1458        provider.commit().unwrap();
1459        let attempt = attempt_id(&factory);
1460        let new = branch(&shared[2], &[vec![stale_changes()], Vec::new(), Vec::new()], root);
1461        factory.replace_headers_after(2, &new);
1462        let (client, mut bootstrap) =
1463            scripted(&factory, [account_range(1, &accounts, 0..3, &[])], [5]);
1464
1465        let outcome = bootstrap.run().await.unwrap();
1466
1467        let SnapBootstrapOutcome::TrieRebuild { pivot, .. } = outcome else {
1468            panic!("the new branch's state is complete: {outcome:?}")
1469        };
1470        assert_eq!(pivot, new[1].num_hash());
1471        assert_ne!(attempt_id(&factory), attempt);
1472        assert!(client.block_requests().is_empty());
1473    }
1474
1475    #[tokio::test]
1476    async fn a_catch_up_stalled_below_the_ancestor_restarts_before_fetching_lists() {
1477        let accounts = accounts();
1478        let root = state_root(&accounts);
1479        let factory = hashed_factory();
1480        let shared = chain(2, root);
1481        let orphaned = branch(&shared[2], &orphaned_lists(), root);
1482        insert_headers(&factory, &shared);
1483        insert_headers(&factory, &orphaned);
1484        // The pivot advanced to block 4 while catch-up stayed at block 1, below the ancestor.
1485        let provider = factory.database_provider_rw().unwrap();
1486        let write =
1487            provider.start_snap_attempt(SnapGeneration::new(shared[1].num_hash(), root)).unwrap();
1488        provider
1489            .advance_snap_pivot(write, SnapGeneration::new(orphaned[1].num_hash(), root))
1490            .unwrap();
1491        provider.commit().unwrap();
1492        let attempt = attempt_id(&factory);
1493        let mut lists = vec![vec![stale_changes()]];
1494        lists.resize(8, Vec::new());
1495        let new = branch(&shared[2], &lists, root);
1496        factory.replace_headers_after(2, &new);
1497        // Head 10 still serves the list after the ancestor, but not the one after block 1.
1498        let (client, mut bootstrap) =
1499            scripted(&factory, [account_range(1, &accounts, 0..3, &[])], [10]);
1500
1501        let outcome = bootstrap.run().await.unwrap();
1502
1503        let SnapBootstrapOutcome::TrieRebuild { pivot, .. } = outcome else {
1504            panic!("the new branch's state is complete: {outcome:?}")
1505        };
1506        assert_eq!(pivot, new[6].num_hash());
1507        assert_ne!(attempt_id(&factory), attempt);
1508        assert!(client.block_requests().is_empty());
1509    }
1510
1511    #[tokio::test]
1512    async fn orphaned_lists_unserved_past_the_state_window_restart_the_attempt() {
1513        let accounts = accounts();
1514        let root = state_root(&accounts);
1515        let factory = hashed_factory();
1516        let shared = chain(2, root);
1517        let orphaned = branch(&shared[2], &orphaned_lists(), root);
1518        insert_headers(&factory, &shared);
1519        insert_headers(&factory, &orphaned);
1520        let provider = factory.database_provider_rw().unwrap();
1521        provider.start_snap_attempt(SnapGeneration::new(orphaned[1].num_hash(), root)).unwrap();
1522        provider.commit().unwrap();
1523        let attempt = attempt_id(&factory);
1524        let mut new_lists = vec![vec![stale_changes()]];
1525        new_lists.resize(129, Vec::new());
1526        let new = branch(&shared[2], &new_lists, root);
1527        factory.replace_headers_after(2, &new);
1528        // Head 131 leaves the ancestor past the served state window, but its lists are still
1529        // served.
1530        let responses =
1531            [lists(1, &orphaned_lists(), false), account_range(1, &accounts, 0..3, &[])];
1532        let (_, bootstrap) = scripted(&factory, responses, [131]);
1533        let mut bootstrap = bootstrap.with_policy(policy().with_history(256));
1534
1535        let outcome = bootstrap.run().await.unwrap();
1536
1537        let SnapBootstrapOutcome::TrieRebuild { pivot, .. } = outcome else {
1538            panic!("the new branch's state is complete: {outcome:?}")
1539        };
1540        assert_eq!(pivot, new[127].num_hash());
1541        assert_ne!(attempt_id(&factory), attempt);
1542    }
1543
1544    #[tokio::test]
1545    async fn a_cancelled_repair_resumes_at_its_pending_slots() {
1546        let slots = vec![(key(1), U256::from(11)), (key(2), U256::from(12))];
1547        let mut contract = account(1);
1548        contract.storage_root = storage_root_of(&slots);
1549        let accounts = vec![(key(1), contract)];
1550        let factory = hashed_factory();
1551        insert_chain(&factory, 3, state_root(&accounts));
1552        let provider = factory.database_provider_rw().unwrap();
1553        let pivot = provider.sealed_header(2).unwrap().unwrap().num_hash();
1554        let write =
1555            provider.start_snap_attempt(SnapGeneration::new(pivot, state_root(&accounts))).unwrap();
1556        provider.start_account_coverage(write).unwrap();
1557        let mut repairs = StateRepairs::default();
1558        repairs.insert_slot(key(1), key(1));
1559        repairs.insert_slot(key(1), key(2));
1560        provider.schedule_snap_repairs(write, repairs).unwrap();
1561        provider.commit().unwrap();
1562        let cancel = CancellationToken::new();
1563        let on_request = cancel.clone();
1564        // Cancellation is checked between repair batches, so the batch ends first, with
1565        // `key(2)` left pending by a peer that does not serve it.
1566        let client = Arc::new(
1567            ScriptedSnapClient::new([
1568                account_range(1, &accounts, 0..1, &[key(1)]),
1569                storage_ranges(1, &[&slots[..1]], &slots, &[key(1)]),
1570                storage_ranges(2, &[], &[], &[]),
1571            ])
1572            .on_storage_request(move || on_request.cancel()),
1573        );
1574        let context = TestContext { heads: RefCell::new(VecDeque::from([3])), waits: 0 };
1575        let mut bootstrap =
1576            SnapBootstrap::new(Arc::clone(&client), factory.clone(), Runtime::test(), context)
1577                .with_policy(policy())
1578                .with_cancellation(cancel);
1579        assert_eq!(bootstrap.run().await.unwrap(), SnapBootstrapOutcome::Stopped);
1580        assert_eq!(client.storage_requests().len(), 2);
1581        let provider = factory.database_provider_ro().unwrap();
1582        let pending = provider.snap_repairs(write).unwrap().slots(key(1)).collect::<Vec<_>>();
1583        assert_eq!(pending, [key(2)]);
1584        assert_eq!(stored_slots(&provider, key(1)), slots[..1]);
1585        drop(provider);
1586
1587        let (client, mut resumed) = scripted(
1588            &factory,
1589            [
1590                account_range(1, &accounts, 0..1, &[key(1)]),
1591                storage_ranges(1, &[&slots[1..]], &slots, &[key(2)]),
1592                Err(RequestError::UnsupportedCapability),
1593            ],
1594            [3],
1595        );
1596        assert_eq!(resumed.run().await.unwrap(), SnapBootstrapOutcome::Stopped);
1597        assert_eq!(*client.storage_requests(), [(vec![key(1)], key(2))]);
1598        let provider = factory.database_provider_ro().unwrap();
1599        assert!(provider.snap_repairs(write).unwrap().is_empty());
1600        assert_eq!(stored_slots(&provider, key(1)), slots);
1601    }
1602
1603    #[tokio::test]
1604    async fn a_cancelled_run_still_finishes_the_hand_off_scan() {
1605        let factory = hashed_factory();
1606        let write = downloaded(&factory);
1607        let cancel = CancellationToken::new();
1608        cancel.cancel();
1609        let (_, bootstrap) = scripted(&factory, [], [3]);
1610        let bootstrap = bootstrap.with_cancellation(cancel).with_shutdown(CancellationToken::new());
1611
1612        // The scan restarts from scratch once stopped, so a header refresh doesn't stop it.
1613        assert!(matches!(bootstrap.hand_off(write).await, Ok(Step::HandedOff(_))));
1614
1615        let provider = factory.database_provider_ro().unwrap();
1616        assert!(provider.is_trie_rebuild_started(write).unwrap());
1617    }
1618
1619    #[tokio::test]
1620    async fn shutdown_stops_the_hand_off_scan_without_committing() {
1621        let factory = hashed_factory();
1622        let write = downloaded(&factory);
1623        let shutdown = CancellationToken::new();
1624        shutdown.cancel();
1625        let (_, bootstrap) = scripted(&factory, [], [3]);
1626        let bootstrap = bootstrap.with_shutdown(shutdown);
1627
1628        assert!(matches!(bootstrap.hand_off(write).await, Err(SnapSyncError::Cancelled)));
1629
1630        let provider = factory.database_provider_ro().unwrap();
1631        assert!(!provider.is_trie_rebuild_started(write).unwrap());
1632    }
1633}