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::{SnapGeneration, SnapSyncError};
8use reth_storage_api::{
9    MetadataProvider, MetadataWriter, SnapAttempt, SnapAttemptId, StorageSettings,
10};
11
12/// Persistence for the attempt that owns downloaded snap state.
13///
14/// Blanket-implemented over node metadata access, so these writes join the caller's transaction:
15/// state, bytecode and the attempt record commit together or not at all.
16pub trait SnapAttemptStore {
17    /// Starts an attempt anchored to `generation`, superseding any already recorded.
18    fn start_snap_attempt(&self, generation: SnapGeneration) -> Result<SnapWrite, SnapSyncError>;
19
20    /// Returns the write an unfinished attempt accepts, if one owns the persisted state.
21    fn active_snap_write(&self) -> Result<Option<SnapWrite>, SnapSyncError>;
22
23    /// Returns the attempt when `write` still owns the persisted state, rejecting it otherwise.
24    fn authorize_snap_write(&self, write: SnapWrite) -> Result<SnapAttempt, SnapSyncError>;
25
26    /// Re-anchors the attempt to `generation`, refusing writes proved against the previous root.
27    fn advance_snap_pivot(
28        &self,
29        write: SnapWrite,
30        generation: SnapGeneration,
31    ) -> Result<SnapWrite, SnapSyncError>;
32
33    /// Marks the attempt's downloaded state verified.
34    fn verify_snap_attempt(&self, write: SnapWrite) -> Result<(), SnapSyncError>;
35
36    /// Gives up on an unfinished attempt, refusing its outstanding writes.
37    fn abandon_snap_attempt(&self) -> Result<(), SnapSyncError>;
38}
39
40/// What a write presents to prove it belongs to the attempt owning the persisted state.
41#[derive(Clone, Copy, Debug, Eq, PartialEq)]
42pub struct SnapWrite {
43    // Attempt this write belongs to.
44    attempt: SnapAttemptId,
45    // Pivot generation within that attempt.
46    state_version: u64,
47}
48
49impl SnapWrite {
50    // What `attempt` currently accepts.
51    const fn of(attempt: &SnapAttempt) -> Self {
52        Self { attempt: attempt.id(), state_version: attempt.state_version() }
53    }
54
55    /// Attempt this write belongs to.
56    pub const fn attempt(&self) -> SnapAttemptId {
57        self.attempt
58    }
59
60    /// Pivot generation this write was proved against.
61    pub const fn state_version(&self) -> u64 {
62        self.state_version
63    }
64}
65
66impl<T> SnapAttemptStore for T
67where
68    T: MetadataProvider + MetadataWriter,
69{
70    fn start_snap_attempt(&self, generation: SnapGeneration) -> Result<SnapWrite, SnapSyncError> {
71        // Absent settings mean the legacy layout.
72        if !self.storage_settings()?.unwrap_or_else(StorageSettings::v1).use_hashed_state() {
73            return Err(SnapSyncError::UnsupportedStorage)
74        }
75
76        let attempt =
77            SnapAttempt::start(self.snap_attempt()?, generation.target(), generation.state_root());
78        self.write_snap_attempt(&attempt)?;
79        Ok(SnapWrite::of(&attempt))
80    }
81
82    fn active_snap_write(&self) -> Result<Option<SnapWrite>, SnapSyncError> {
83        Ok(self.snap_attempt()?.filter(SnapAttempt::is_unfinished).as_ref().map(SnapWrite::of))
84    }
85
86    fn authorize_snap_write(&self, write: SnapWrite) -> Result<SnapAttempt, SnapSyncError> {
87        let attempt = self.snap_attempt()?.ok_or(SnapSyncError::NoAttempt)?;
88        if !attempt.is_unfinished() || SnapWrite::of(&attempt) != write {
89            return Err(SnapSyncError::StaleWrite {
90                attempt: write.attempt,
91                state_version: write.state_version,
92            })
93        }
94        Ok(attempt)
95    }
96
97    fn advance_snap_pivot(
98        &self,
99        write: SnapWrite,
100        generation: SnapGeneration,
101    ) -> Result<SnapWrite, SnapSyncError> {
102        let mut attempt = self.authorize_snap_write(write)?;
103        attempt.re_anchor(generation.target(), generation.state_root());
104        self.write_snap_attempt(&attempt)?;
105        Ok(SnapWrite::of(&attempt))
106    }
107
108    fn verify_snap_attempt(&self, write: SnapWrite) -> Result<(), SnapSyncError> {
109        let mut attempt = self.authorize_snap_write(write)?;
110        attempt.verify();
111        self.write_snap_attempt(&attempt)?;
112        Ok(())
113    }
114
115    fn abandon_snap_attempt(&self) -> Result<(), SnapSyncError> {
116        if let Some(mut attempt) = self.snap_attempt()? &&
117            attempt.is_unfinished()
118        {
119            attempt.abandon();
120            self.write_snap_attempt(&attempt)?;
121        }
122        Ok(())
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129    use alloy_eips::BlockNumHash;
130    use alloy_primitives::{Bytes, B256};
131    use reth_db_api::{tables, transaction::DbTx};
132    use reth_primitives_traits::Account;
133    use reth_provider::{
134        test_utils::{create_test_provider_factory, MockNodeTypesWithDB},
135        DBProvider, DatabaseProviderFactory, ProviderFactory,
136    };
137    use reth_storage_api::{metadata::keys, StateWriter};
138    use reth_trie_common::HashedPostState;
139    use revm::{bytecode::Bytecode, database::states::StateChangeset};
140
141    const HASHED_ADDRESS: B256 = B256::repeat_byte(0xbb);
142
143    fn code() -> Bytecode {
144        Bytecode::new_raw(Bytes::from_static(&[0x60, 0x00]))
145    }
146
147    fn generation(block: u64) -> SnapGeneration {
148        SnapGeneration::new(
149            BlockNumHash::new(block, B256::repeat_byte(block as u8)),
150            B256::repeat_byte(0xaa),
151        )
152    }
153
154    // A database using the hashed state layout snap writes into.
155    fn factory() -> ProviderFactory<MockNodeTypesWithDB> {
156        let factory = create_test_provider_factory();
157        let provider = factory.database_provider_rw().unwrap();
158        provider.write_storage_settings(StorageSettings::v2()).unwrap();
159        provider.commit().unwrap();
160        factory
161    }
162
163    // Persists one account and its bytecode through the writers snap shares with execution.
164    fn download(provider: &impl StateWriter) {
165        let mut state = HashedPostState::default();
166        state.accounts.insert(HASHED_ADDRESS, Some(Account::default()));
167        provider.write_hashed_state(&state.into_sorted()).unwrap();
168        provider
169            .write_state_changes(StateChangeset {
170                contracts: vec![(code().hash_slow(), code())],
171                ..Default::default()
172            })
173            .unwrap();
174    }
175
176    fn downloaded(provider: &impl DBProvider) -> (bool, bool) {
177        let tx = provider.tx_ref();
178        (
179            tx.get::<tables::HashedAccounts>(HASHED_ADDRESS).unwrap().is_some(),
180            tx.get::<tables::Bytecodes>(code().hash_slow()).unwrap().is_some(),
181        )
182    }
183
184    #[test]
185    fn nothing_owns_the_state_before_an_attempt_starts() {
186        let factory = factory();
187        let provider = factory.database_provider_rw().unwrap();
188
189        assert_eq!(provider.active_snap_write().unwrap(), None);
190        let write = SnapWrite { attempt: SnapAttemptId::FIRST, state_version: 0 };
191        assert!(matches!(provider.authorize_snap_write(write), Err(SnapSyncError::NoAttempt)));
192    }
193
194    #[test]
195    fn address_keyed_state_cannot_host_an_attempt() {
196        let factory = create_test_provider_factory();
197        let provider = factory.database_provider_rw().unwrap();
198
199        // Nothing recorded means the legacy layout.
200        assert!(matches!(
201            provider.start_snap_attempt(generation(1)),
202            Err(SnapSyncError::UnsupportedStorage)
203        ));
204        provider.write_storage_settings(StorageSettings::v1()).unwrap();
205        assert!(matches!(
206            provider.start_snap_attempt(generation(1)),
207            Err(SnapSyncError::UnsupportedStorage)
208        ));
209        assert_eq!(provider.snap_attempt().unwrap(), None);
210    }
211
212    #[test]
213    fn restarting_at_the_same_pivot_takes_a_new_identity() {
214        let factory = factory();
215        let provider = factory.database_provider_rw().unwrap();
216
217        let first = provider.start_snap_attempt(generation(1)).unwrap();
218        let second = provider.start_snap_attempt(generation(1)).unwrap();
219
220        assert_ne!(first.attempt(), second.attempt());
221        // The superseded attempt's outstanding downloads no longer own the state.
222        assert!(matches!(
223            provider.authorize_snap_write(first),
224            Err(SnapSyncError::StaleWrite { .. })
225        ));
226        provider.authorize_snap_write(second).unwrap();
227    }
228
229    #[test]
230    fn abandoning_an_attempt_keeps_its_identity_taken() {
231        let factory = factory();
232        let provider = factory.database_provider_rw().unwrap();
233
234        let abandoned = provider.start_snap_attempt(generation(1)).unwrap();
235        provider.abandon_snap_attempt().unwrap();
236        assert_eq!(provider.active_snap_write().unwrap(), None);
237        let started = provider.start_snap_attempt(generation(2)).unwrap();
238
239        assert_ne!(abandoned.attempt(), started.attempt());
240        // The abandoned attempt's outstanding downloads cannot pass as the new attempt's work.
241        assert!(matches!(
242            provider.authorize_snap_write(abandoned),
243            Err(SnapSyncError::StaleWrite { .. })
244        ));
245    }
246
247    #[test]
248    fn the_attempt_survives_reopening_the_database() {
249        let factory = factory();
250        let provider = factory.database_provider_rw().unwrap();
251        let write = provider.start_snap_attempt(generation(7)).unwrap();
252        provider.commit().unwrap();
253
254        let reopened = factory.database_provider_rw().unwrap();
255
256        assert_eq!(reopened.active_snap_write().unwrap(), Some(write));
257        let attempt = reopened.snap_attempt().unwrap().unwrap();
258        assert_eq!(attempt.pivot(), BlockNumHash::new(7, B256::repeat_byte(7)));
259        assert_eq!(attempt.state_root(), B256::repeat_byte(0xaa));
260        assert!(attempt.is_unfinished());
261    }
262
263    #[test]
264    fn committing_keeps_downloaded_state_and_progress_together() {
265        let factory = factory();
266        let provider = factory.database_provider_rw().unwrap();
267        let write = provider.start_snap_attempt(generation(1)).unwrap();
268        download(&provider);
269        provider.commit().unwrap();
270
271        let provider = factory.database_provider_rw().unwrap();
272        assert_eq!(provider.active_snap_write().unwrap(), Some(write));
273        assert_eq!(downloaded(&provider), (true, true));
274    }
275
276    #[test]
277    fn rolling_back_drops_downloaded_state_and_progress_together() {
278        let factory = factory();
279        let provider = factory.database_provider_rw().unwrap();
280        provider.start_snap_attempt(generation(1)).unwrap();
281        download(&provider);
282        // Dropping without committing is the interrupted-commit case.
283        drop(provider);
284
285        let provider = factory.database_provider_rw().unwrap();
286        assert_eq!(provider.active_snap_write().unwrap(), None);
287        assert_eq!(downloaded(&provider), (false, false));
288    }
289
290    #[test]
291    fn advancing_the_pivot_rejects_writes_proved_against_the_old_root() {
292        let factory = factory();
293        let provider = factory.database_provider_rw().unwrap();
294
295        let before = provider.start_snap_attempt(generation(1)).unwrap();
296        let after = provider.advance_snap_pivot(before, generation(2)).unwrap();
297
298        assert_eq!(after.attempt(), before.attempt());
299        assert_eq!(after.state_version(), before.state_version() + 1);
300        assert!(matches!(
301            provider.authorize_snap_write(before),
302            Err(SnapSyncError::StaleWrite { .. })
303        ));
304        provider.authorize_snap_write(after).unwrap();
305    }
306
307    #[test]
308    fn a_verified_attempt_accepts_no_further_writes() {
309        let factory = factory();
310        let provider = factory.database_provider_rw().unwrap();
311        let write = provider.start_snap_attempt(generation(1)).unwrap();
312
313        provider.verify_snap_attempt(write).unwrap();
314
315        assert!(!provider.snap_attempt().unwrap().unwrap().is_unfinished());
316        assert_eq!(provider.active_snap_write().unwrap(), None);
317        assert!(matches!(
318            provider.authorize_snap_write(write),
319            Err(SnapSyncError::StaleWrite { .. })
320        ));
321        // Verified state is complete, so abandoning cannot turn it into leftovers.
322        provider.abandon_snap_attempt().unwrap();
323        assert!(provider.snap_attempt().unwrap().unwrap().is_verified());
324    }
325
326    #[test]
327    fn a_record_this_build_cannot_read_is_reported_rather_than_ignored() {
328        let factory = factory();
329
330        for record in [br#"{"version":999}"#.to_vec(), b"{}".to_vec(), b"not json".to_vec()] {
331            let provider = factory.database_provider_rw().unwrap();
332            provider.write_metadata(keys::SNAP_ATTEMPT, record).unwrap();
333
334            assert!(provider.snap_attempt().is_err());
335            // Starting a new attempt cannot guarantee a distinct identity over an unreadable one.
336            assert!(provider.start_snap_attempt(generation(1)).is_err());
337        }
338    }
339}