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