Skip to main content

reth_snap_sync/
attempt.rs

1//! Persistence for the attempt that owns downloaded snap state.
2//!
3//! Snap writes land in the canonical hashed state tables, so nothing in the data says which
4//! attempt produced it. Every write presents a [`SnapWrite`]; ones that no longer match are
5//! refused.
6
7use crate::{
8    account::StoredCoverage, common::SnapRecord, reorg::StoredAncestry, repair::StoredRepairs,
9    storage::StoredProgress, verify::StoredRebuild, CatchUpProgress, DownloadedAccount,
10    SnapAccountStore, SnapCatchUpStore, SnapGeneration, SnapReorg, SnapSyncError, StateRepairs,
11};
12use alloy_eips::{eip7928::bal::DecodedBal, BlockNumHash};
13use alloy_primitives::keccak256;
14use reth_storage_api::{
15    BlockHashReader, DBProvider, HeaderProvider, MetadataProvider, MetadataWriter, SnapAttempt,
16    SnapAttemptId, StorageSettings,
17};
18use serde::{Deserialize, Serialize};
19
20/// Persistence for the attempt that owns downloaded snap state.
21///
22/// Blanket-implemented over node metadata access, so these writes join the caller's transaction:
23/// state, bytecode and the attempt record commit together or not at all.
24pub trait SnapAttemptStore {
25    /// Starts an attempt anchored to `generation`, superseding any already recorded and the
26    /// progress it kept, with its catch-up progress at that pivot.
27    fn start_snap_attempt(&self, generation: SnapGeneration) -> Result<SnapWrite, SnapSyncError>
28    where
29        Self: MetadataWriter + HeaderProvider;
30
31    /// Returns the write an unfinished attempt accepts, if one owns the persisted state.
32    fn active_snap_write(&self) -> Result<Option<SnapWrite>, SnapSyncError>;
33
34    /// Returns the attempt when `write` still owns the persisted state, rejecting it otherwise.
35    fn authorize_snap_write(&self, write: SnapWrite) -> Result<SnapAttempt, SnapSyncError>;
36
37    /// Returns the attempt when `write` owns its state and its pivot is still canonical.
38    ///
39    /// Downloaded ranges are proved against the pivot, so they describe the fork it sits on, not
40    /// the canonical chain the applied blocks were checked against.
41    fn authorize_canonical_snap_write(
42        &self,
43        write: SnapWrite,
44    ) -> Result<SnapAttempt, SnapSyncError>
45    where
46        Self: BlockHashReader;
47
48    /// Re-anchors the attempt to `generation`, refusing writes proved against the previous root.
49    ///
50    /// Both pivots must be canonical and the new one past the old, so it descends from all
51    /// downloaded state. An orphaned pivot requires recovery or a fresh attempt.
52    fn advance_snap_pivot(
53        &self,
54        write: SnapWrite,
55        generation: SnapGeneration,
56    ) -> Result<SnapWrite, SnapSyncError>
57    where
58        Self: MetadataWriter + HeaderProvider + BlockHashReader;
59
60    /// Marks the attempt's downloaded state verified.
61    fn verify_snap_attempt(&self, write: SnapWrite) -> Result<(), SnapSyncError>
62    where
63        Self: MetadataWriter;
64
65    /// Gives up on an unfinished attempt, refusing its outstanding writes.
66    fn abandon_snap_attempt(&self) -> Result<(), SnapSyncError>
67    where
68        Self: MetadataWriter;
69
70    /// Returns where the canonical chain diverges from the branch the pivot of `write`'s attempt
71    /// is on.
72    ///
73    /// `Ok(None)` when the kept headers do not reach back to where the branches part.
74    fn snap_reorg(
75        &self,
76        write: SnapWrite,
77    ) -> Result<Option<SnapReorg<<Self as HeaderProvider>::Header>>, SnapSyncError>
78    where
79        Self: HeaderProvider + BlockHashReader;
80
81    /// Schedules what `lists`, those of the orphaned blocks, changed in the downloaded state for
82    /// repair, and moves the attempt to `generation` with catch-up back at `ancestor` at most.
83    ///
84    /// Storage persisted ahead of its range may hold the orphaned values, so it is dropped.
85    fn commit_reorg_recovery(
86        &self,
87        write: SnapWrite,
88        ancestor: BlockNumHash,
89        lists: &[DecodedBal],
90        generation: SnapGeneration,
91    ) -> Result<SnapWrite, SnapSyncError>
92    where
93        Self: MetadataWriter + HeaderProvider + BlockHashReader + DBProvider;
94}
95
96/// What a write presents to prove it belongs to the attempt owning the persisted state.
97#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
98pub struct SnapWrite {
99    // Attempt this write belongs to.
100    attempt: SnapAttemptId,
101    // Pivot generation within that attempt.
102    state_version: u64,
103}
104
105impl SnapWrite {
106    // What `attempt` currently accepts.
107    const fn of(attempt: &SnapAttempt) -> Self {
108        Self { attempt: attempt.id(), state_version: attempt.state_version() }
109    }
110
111    /// Attempt this write belongs to.
112    pub const fn attempt(&self) -> SnapAttemptId {
113        self.attempt
114    }
115
116    /// Pivot generation this write was proved against.
117    pub const fn state_version(&self) -> u64 {
118        self.state_version
119    }
120}
121
122impl<T: MetadataProvider> SnapAttemptStore for T {
123    fn start_snap_attempt(&self, generation: SnapGeneration) -> Result<SnapWrite, SnapSyncError>
124    where
125        Self: MetadataWriter + HeaderProvider,
126    {
127        // Absent settings mean the legacy layout.
128        if !self.storage_settings()?.unwrap_or_else(StorageSettings::v1).use_hashed_state() {
129            return Err(SnapSyncError::UnsupportedStorage)
130        }
131
132        let attempt =
133            SnapAttempt::start(self.snap_attempt()?, generation.target(), generation.state_root());
134        self.write_snap_attempt(&attempt)?;
135        // Ranges committed at this pivot need every list after it, however far the pivot moves.
136        CatchUpProgress::at_pivot(attempt.pivot()).write(self, attempt.id())?;
137        // Earlier attempts' progress never applies to this one, and a record another build wrote
138        // would fail every read of it.
139        StoredCoverage::clear(self)?;
140        StoredProgress::clear(self)?;
141        StoredRepairs::clear(self)?;
142        StoredRebuild::clear(self)?;
143        StoredAncestry::record(self, attempt.id(), attempt.pivot())?;
144        Ok(SnapWrite::of(&attempt))
145    }
146
147    fn active_snap_write(&self) -> Result<Option<SnapWrite>, SnapSyncError> {
148        Ok(self.snap_attempt()?.filter(SnapAttempt::is_unfinished).as_ref().map(SnapWrite::of))
149    }
150
151    fn authorize_snap_write(&self, write: SnapWrite) -> Result<SnapAttempt, SnapSyncError> {
152        let attempt = self.snap_attempt()?.ok_or(SnapSyncError::NoAttempt)?;
153        if !attempt.is_unfinished() || SnapWrite::of(&attempt) != write {
154            return Err(SnapSyncError::StaleWrite {
155                attempt: write.attempt,
156                state_version: write.state_version,
157            })
158        }
159        Ok(attempt)
160    }
161
162    fn authorize_canonical_snap_write(&self, write: SnapWrite) -> Result<SnapAttempt, SnapSyncError>
163    where
164        Self: BlockHashReader,
165    {
166        let attempt = self.authorize_snap_write(write)?;
167        let pivot = attempt.pivot();
168        if self.block_hash(pivot.number)? != Some(pivot.hash) {
169            return Err(SnapSyncError::NonCanonicalBlock { block: pivot.number, hash: pivot.hash })
170        }
171        Ok(attempt)
172    }
173
174    fn advance_snap_pivot(
175        &self,
176        write: SnapWrite,
177        generation: SnapGeneration,
178    ) -> Result<SnapWrite, SnapSyncError>
179    where
180        Self: MetadataWriter + HeaderProvider + BlockHashReader,
181    {
182        // Replacing an orphaned pivot would hide the fork that downloaded ranges belong to.
183        let mut attempt = self.authorize_canonical_snap_write(write)?;
184        let (pivot, target) = (attempt.pivot(), generation.target());
185        // Ranges already committed hold the old pivot's changes, which no list can undo.
186        if target.number <= pivot.number {
187            return Err(SnapSyncError::PivotNotAdvanced {
188                pivot: pivot.number,
189                target: target.number,
190            })
191        }
192        if self.block_hash(target.number)? != Some(target.hash) {
193            return Err(SnapSyncError::NonCanonicalBlock { block: target.number, hash: target.hash })
194        }
195        attempt.re_anchor(target, generation.state_root());
196        self.write_snap_attempt(&attempt)?;
197        StoredAncestry::record(self, attempt.id(), target)?;
198        Ok(SnapWrite::of(&attempt))
199    }
200
201    fn verify_snap_attempt(&self, write: SnapWrite) -> Result<(), SnapSyncError>
202    where
203        Self: MetadataWriter,
204    {
205        let mut attempt = self.authorize_snap_write(write)?;
206        attempt.verify();
207        self.write_snap_attempt(&attempt)?;
208        Ok(())
209    }
210
211    fn abandon_snap_attempt(&self) -> Result<(), SnapSyncError>
212    where
213        Self: MetadataWriter,
214    {
215        if let Some(mut attempt) = self.snap_attempt()? &&
216            attempt.is_unfinished()
217        {
218            attempt.abandon();
219            self.write_snap_attempt(&attempt)?;
220        }
221        Ok(())
222    }
223
224    fn snap_reorg(
225        &self,
226        write: SnapWrite,
227    ) -> Result<Option<SnapReorg<<Self as HeaderProvider>::Header>>, SnapSyncError>
228    where
229        Self: HeaderProvider + BlockHashReader,
230    {
231        StoredAncestry::reorg(self, &self.authorize_snap_write(write)?)
232    }
233
234    fn commit_reorg_recovery(
235        &self,
236        write: SnapWrite,
237        ancestor: BlockNumHash,
238        lists: &[DecodedBal],
239        generation: SnapGeneration,
240    ) -> Result<SnapWrite, SnapSyncError>
241    where
242        Self: MetadataWriter + HeaderProvider + BlockHashReader + DBProvider,
243    {
244        let mut attempt = self.authorize_snap_write(write)?;
245        let target = generation.target();
246        for block in [ancestor, target] {
247            if self.block_hash(block.number)? != Some(block.hash) {
248                return Err(SnapSyncError::NonCanonicalBlock {
249                    block: block.number,
250                    hash: block.hash,
251                })
252            }
253        }
254        if target.number < ancestor.number {
255            return Err(SnapSyncError::PivotBelowAncestor {
256                ancestor: ancestor.number,
257                target: target.number,
258            })
259        }
260
261        let coverage = self.account_coverage(write)?.ok_or(SnapSyncError::NoCoverage)?;
262        let mut repairs = StateRepairs::default();
263        for changes in lists.iter().flat_map(|list| list.as_bal().iter()) {
264            let hashed_address = keccak256(changes.address());
265            // Accounts not downloaded yet are downloaded whole at a canonical pivot later.
266            if !changes.has_changes() ||
267                self.downloaded_account(coverage, hashed_address)? == DownloadedAccount::Unknown
268            {
269                continue
270            }
271            repairs.insert_changes(hashed_address, changes);
272        }
273        self.schedule_snap_repairs(write, repairs)?;
274
275        let applied = self
276            .catch_up_progress(write)?
277            .ok_or(SnapSyncError::NoCatchUpProgress)?
278            .resume_after(ancestor);
279        attempt.re_anchor(target, generation.state_root());
280        self.write_snap_attempt(&attempt)?;
281        CatchUpProgress::at_pivot(applied).write(self, attempt.id())?;
282        StoredProgress::clear(self)?;
283        StoredAncestry::record(self, attempt.id(), target)?;
284        Ok(SnapWrite::of(&attempt))
285    }
286}
287
288#[cfg(test)]
289mod tests {
290    use super::*;
291    use crate::test_utils::insert_generation_headers;
292    use alloy_eips::BlockNumHash;
293    use alloy_primitives::{Bytes, B256};
294    use reth_db_api::{tables, transaction::DbTx};
295    use reth_primitives_traits::Account;
296    use reth_provider::{
297        test_utils::{create_test_provider_factory, MockNodeTypesWithDB},
298        DBProvider, DatabaseProviderFactory, ProviderFactory,
299    };
300    use reth_storage_api::{metadata::keys, StateWriter};
301    use reth_trie_common::HashedPostState;
302    use revm::{bytecode::Bytecode, database::states::StateChangeset};
303
304    const HASHED_ADDRESS: B256 = B256::repeat_byte(0xbb);
305
306    fn code() -> Bytecode {
307        Bytecode::new_raw(Bytes::from_static(&[0x60, 0x00]))
308    }
309
310    fn generation(block: u64) -> SnapGeneration {
311        SnapGeneration::new(
312            BlockNumHash::new(block, B256::repeat_byte(block as u8)),
313            B256::repeat_byte(0xaa),
314        )
315    }
316
317    // A database using the hashed state layout snap writes into, holding the blocks the
318    // generations above anchor to.
319    fn factory() -> ProviderFactory<MockNodeTypesWithDB> {
320        let factory = create_test_provider_factory();
321        let provider = factory.database_provider_rw().unwrap();
322        provider.write_storage_settings(StorageSettings::v2()).unwrap();
323        provider.commit().unwrap();
324        insert_generation_headers(&factory);
325        factory
326    }
327
328    // Persists one account and its bytecode through the writers snap shares with execution.
329    fn download(provider: &impl StateWriter) {
330        let mut state = HashedPostState::default();
331        state.accounts.insert(HASHED_ADDRESS, Some(Account::default()));
332        provider.write_hashed_state(&state.into_sorted()).unwrap();
333        provider
334            .write_state_changes(StateChangeset {
335                contracts: vec![(code().hash_slow(), code())],
336                ..Default::default()
337            })
338            .unwrap();
339    }
340
341    fn downloaded(provider: &impl DBProvider) -> (bool, bool) {
342        let tx = provider.tx_ref();
343        (
344            tx.get::<tables::HashedAccounts>(HASHED_ADDRESS).unwrap().is_some(),
345            tx.get::<tables::Bytecodes>(code().hash_slow()).unwrap().is_some(),
346        )
347    }
348
349    #[test]
350    fn nothing_owns_the_state_before_an_attempt_starts() {
351        let factory = factory();
352        let provider = factory.database_provider_rw().unwrap();
353
354        assert_eq!(provider.active_snap_write().unwrap(), None);
355        let write = SnapWrite { attempt: SnapAttemptId::FIRST, state_version: 0 };
356        assert!(matches!(provider.authorize_snap_write(write), Err(SnapSyncError::NoAttempt)));
357    }
358
359    #[test]
360    fn address_keyed_state_cannot_host_an_attempt() {
361        let factory = create_test_provider_factory();
362        let provider = factory.database_provider_rw().unwrap();
363
364        // Nothing recorded means the legacy layout.
365        assert!(matches!(
366            provider.start_snap_attempt(generation(1)),
367            Err(SnapSyncError::UnsupportedStorage)
368        ));
369        provider.write_storage_settings(StorageSettings::v1()).unwrap();
370        assert!(matches!(
371            provider.start_snap_attempt(generation(1)),
372            Err(SnapSyncError::UnsupportedStorage)
373        ));
374        assert_eq!(provider.snap_attempt().unwrap(), None);
375    }
376
377    #[test]
378    fn a_new_attempt_drops_progress_another_build_wrote() {
379        let factory = factory();
380        let provider = factory.database_provider_rw().unwrap();
381        let unreadable = br#"{"version":999}"#.to_vec();
382        for key in [StoredCoverage::KEY, StoredProgress::KEY, StoredRebuild::KEY] {
383            provider.write_metadata(key, unreadable.clone()).unwrap();
384        }
385
386        let write = provider.start_snap_attempt(generation(1)).unwrap();
387
388        assert_eq!(
389            crate::SnapAccountStore::start_account_coverage(&provider, write).unwrap(),
390            crate::AccountCoverage::START
391        );
392        for key in [StoredProgress::KEY, StoredRebuild::KEY] {
393            assert_eq!(provider.get_metadata(key).unwrap(), None);
394        }
395    }
396
397    #[test]
398    fn restarting_at_the_same_pivot_takes_a_new_identity() {
399        let factory = factory();
400        let provider = factory.database_provider_rw().unwrap();
401
402        let first = provider.start_snap_attempt(generation(1)).unwrap();
403        let second = provider.start_snap_attempt(generation(1)).unwrap();
404
405        assert_ne!(first.attempt(), second.attempt());
406        // The superseded attempt's outstanding downloads no longer own the state.
407        assert!(matches!(
408            provider.authorize_snap_write(first),
409            Err(SnapSyncError::StaleWrite { .. })
410        ));
411        provider.authorize_snap_write(second).unwrap();
412    }
413
414    #[test]
415    fn abandoning_an_attempt_keeps_its_identity_taken() {
416        let factory = factory();
417        let provider = factory.database_provider_rw().unwrap();
418
419        let abandoned = provider.start_snap_attempt(generation(1)).unwrap();
420        provider.abandon_snap_attempt().unwrap();
421        assert_eq!(provider.active_snap_write().unwrap(), None);
422        let started = provider.start_snap_attempt(generation(2)).unwrap();
423
424        assert_ne!(abandoned.attempt(), started.attempt());
425        // The abandoned attempt's outstanding downloads cannot pass as the new attempt's work.
426        assert!(matches!(
427            provider.authorize_snap_write(abandoned),
428            Err(SnapSyncError::StaleWrite { .. })
429        ));
430    }
431
432    #[test]
433    fn the_attempt_survives_reopening_the_database() {
434        let factory = factory();
435        let provider = factory.database_provider_rw().unwrap();
436        let write = provider.start_snap_attempt(generation(7)).unwrap();
437        provider.commit().unwrap();
438
439        let reopened = factory.database_provider_rw().unwrap();
440
441        assert_eq!(reopened.active_snap_write().unwrap(), Some(write));
442        let attempt = reopened.snap_attempt().unwrap().unwrap();
443        assert_eq!(attempt.pivot(), BlockNumHash::new(7, B256::repeat_byte(7)));
444        assert_eq!(attempt.state_root(), B256::repeat_byte(0xaa));
445        assert!(attempt.is_unfinished());
446    }
447
448    #[test]
449    fn committing_keeps_downloaded_state_and_progress_together() {
450        let factory = factory();
451        let provider = factory.database_provider_rw().unwrap();
452        let write = provider.start_snap_attempt(generation(1)).unwrap();
453        download(&provider);
454        provider.commit().unwrap();
455
456        let provider = factory.database_provider_rw().unwrap();
457        assert_eq!(provider.active_snap_write().unwrap(), Some(write));
458        assert_eq!(downloaded(&provider), (true, true));
459    }
460
461    #[test]
462    fn rolling_back_drops_downloaded_state_and_progress_together() {
463        let factory = factory();
464        let provider = factory.database_provider_rw().unwrap();
465        provider.start_snap_attempt(generation(1)).unwrap();
466        download(&provider);
467        // Dropping without committing is the interrupted-commit case.
468        drop(provider);
469
470        let provider = factory.database_provider_rw().unwrap();
471        assert_eq!(provider.active_snap_write().unwrap(), None);
472        assert_eq!(downloaded(&provider), (false, false));
473    }
474
475    #[test]
476    fn advancing_the_pivot_rejects_writes_proved_against_the_old_root() {
477        let factory = factory();
478        let provider = factory.database_provider_rw().unwrap();
479
480        let before = provider.start_snap_attempt(generation(1)).unwrap();
481        let after = provider.advance_snap_pivot(before, generation(2)).unwrap();
482
483        assert_eq!(after.attempt(), before.attempt());
484        assert_eq!(after.state_version(), before.state_version() + 1);
485        assert!(matches!(
486            provider.authorize_snap_write(before),
487            Err(SnapSyncError::StaleWrite { .. })
488        ));
489        provider.authorize_snap_write(after).unwrap();
490    }
491
492    #[test]
493    fn the_pivot_advances_repeatedly_within_one_attempt() {
494        let factory = factory();
495        let provider = factory.database_provider_rw().unwrap();
496        let first = provider.start_snap_attempt(generation(1)).unwrap();
497
498        let second = provider.advance_snap_pivot(first, generation(2)).unwrap();
499        let third = provider.advance_snap_pivot(second, generation(3)).unwrap();
500
501        assert_eq!(third.attempt(), first.attempt());
502        assert_eq!(third.state_version(), first.state_version() + 2);
503        assert_eq!(provider.snap_attempt().unwrap().unwrap().pivot().number, 3);
504        for stale in [first, second] {
505            assert!(matches!(
506                provider.authorize_snap_write(stale),
507                Err(SnapSyncError::StaleWrite { .. })
508            ));
509        }
510    }
511
512    #[test]
513    fn the_pivot_only_moves_forward() {
514        let factory = factory();
515        let provider = factory.database_provider_rw().unwrap();
516        let write = provider.start_snap_attempt(generation(2)).unwrap();
517
518        // Committed ranges hold block 2's changes, which a lower pivot's lists cannot undo.
519        for target in [1, 2] {
520            assert!(matches!(
521                provider.advance_snap_pivot(write, generation(target)),
522                Err(SnapSyncError::PivotNotAdvanced { pivot: 2, target: refused })
523                    if refused == target
524            ));
525        }
526        provider.authorize_snap_write(write).unwrap();
527    }
528
529    #[test]
530    fn the_pivot_cannot_move_off_the_canonical_chain() {
531        let factory = factory();
532        let provider = factory.database_provider_rw().unwrap();
533        let write = provider.start_snap_attempt(generation(1)).unwrap();
534        let orphan = SnapGeneration::new(BlockNumHash::new(2, B256::repeat_byte(0xee)), B256::ZERO);
535
536        let refused = provider.advance_snap_pivot(write, orphan);
537
538        assert!(matches!(refused, Err(SnapSyncError::NonCanonicalBlock { block: 2, .. })));
539        provider.authorize_snap_write(write).unwrap();
540    }
541
542    #[test]
543    fn an_interrupted_advance_keeps_the_previous_pivot() {
544        let factory = factory();
545        let provider = factory.database_provider_rw().unwrap();
546        let write = provider.start_snap_attempt(generation(1)).unwrap();
547        provider.commit().unwrap();
548
549        let provider = factory.database_provider_rw().unwrap();
550        provider.advance_snap_pivot(write, generation(2)).unwrap();
551        drop(provider);
552
553        // Downloads still in flight against the old root keep committing.
554        let provider = factory.database_provider_rw().unwrap();
555        assert_eq!(provider.active_snap_write().unwrap(), Some(write));
556        assert_eq!(provider.authorize_snap_write(write).unwrap().pivot().number, 1);
557    }
558
559    #[test]
560    fn a_verified_attempt_accepts_no_further_writes() {
561        let factory = factory();
562        let provider = factory.database_provider_rw().unwrap();
563        let write = provider.start_snap_attempt(generation(1)).unwrap();
564
565        provider.verify_snap_attempt(write).unwrap();
566
567        assert!(!provider.snap_attempt().unwrap().unwrap().is_unfinished());
568        assert_eq!(provider.active_snap_write().unwrap(), None);
569        assert!(matches!(
570            provider.authorize_snap_write(write),
571            Err(SnapSyncError::StaleWrite { .. })
572        ));
573        // Verified state is complete, so abandoning cannot turn it into leftovers.
574        provider.abandon_snap_attempt().unwrap();
575        assert!(provider.snap_attempt().unwrap().unwrap().is_verified());
576    }
577
578    #[test]
579    fn a_record_this_build_cannot_read_is_reported_rather_than_ignored() {
580        let factory = factory();
581
582        for record in [br#"{"version":999}"#.to_vec(), b"{}".to_vec(), b"not json".to_vec()] {
583            let provider = factory.database_provider_rw().unwrap();
584            provider.write_metadata(keys::SNAP_ATTEMPT, record).unwrap();
585
586            assert!(provider.snap_attempt().is_err());
587            // Starting a new attempt cannot guarantee a distinct identity over an unreadable one.
588            assert!(provider.start_snap_attempt(generation(1)).is_err());
589        }
590    }
591}