Skip to main content

reth_snap_sync/
session.rs

1//! Drives one snap synchronization attempt: what it targets, and when it stops.
2
3use crate::{SnapAttemptStore, SnapGeneration, SnapPivotPolicy, SnapSyncError, SnapWrite};
4use reth_storage_api::{BlockHashReader, HeaderProvider, MetadataProvider, MetadataWriter};
5
6/// One snap synchronization attempt, from the pivot it targets to the work it owns.
7///
8/// Targets come from the local chain, never from a peer: peers only supply state, which is
9/// authenticated against the target's state root.
10#[derive(Debug)]
11pub struct SnapSyncSession {
12    // Decides which blocks are eligible targets.
13    policy: SnapPivotPolicy,
14    // How far the attempt has got.
15    state: SnapSyncSessionState,
16}
17
18impl SnapSyncSession {
19    /// Creates a session waiting for its first eligible target.
20    pub const fn new(policy: SnapPivotPolicy) -> Self {
21        Self { policy, state: SnapSyncSessionState::Waiting }
22    }
23
24    /// What the session is doing.
25    pub const fn state(&self) -> &SnapSyncSessionState {
26        &self.state
27    }
28
29    /// Pivot this session is anchored to, if it has one.
30    pub const fn target(&self) -> Option<&SnapGeneration> {
31        self.state.target()
32    }
33
34    /// Selects a target under `head`, or waits while no block is eligible.
35    ///
36    /// A target no work has taken yet is replaced by a newer eligible one and dropped when none is
37    /// eligible, since nothing authenticates against its root so far. Moving a target that work
38    /// has taken is instead pivot advancement, which has to carry the downloaded state forward.
39    pub fn select(
40        &mut self,
41        provider: &impl HeaderProvider,
42        head: u64,
43        finalized: Option<u64>,
44    ) -> Result<&SnapSyncSessionState, SnapSyncError> {
45        if matches!(self.state, SnapSyncSessionState::Waiting | SnapSyncSessionState::Selected(_)) {
46            self.state = match self.policy.select(provider, head, finalized)? {
47                Some(generation) => SnapSyncSessionState::Selected(generation),
48                None => SnapSyncSessionState::Waiting,
49            };
50        }
51        Ok(&self.state)
52    }
53
54    /// Hands the selected target to the work downloading against it.
55    ///
56    /// Taking the target is what starts it, so only the first caller gets one: a target already
57    /// being downloaded has an owner, and a waiting session has nothing to hand out.
58    pub const fn start(&mut self) -> Option<SnapGeneration> {
59        let SnapSyncSessionState::Selected(generation) = self.state else { return None };
60        self.state = SnapSyncSessionState::Downloading(generation);
61        Some(generation)
62    }
63
64    /// Takes over the target of an attempt a previous run recorded, as [`Self::start`] does for a
65    /// selected one.
66    ///
67    /// Only a session that has not handed out a target can take one over, so this returns whether
68    /// it did.
69    pub const fn resume(&mut self, generation: SnapGeneration) -> bool {
70        if !matches!(self.state, SnapSyncSessionState::Waiting | SnapSyncSessionState::Selected(_))
71        {
72            return false
73        }
74        self.state = SnapSyncSessionState::Downloading(generation);
75        true
76    }
77
78    /// Re-anchors the downloaded target once it lags too far behind `head`, returning the new
79    /// write.
80    ///
81    /// The target is re-read from the attempt record first, so an advance whose transaction was
82    /// dropped is retried rather than assumed.
83    pub fn advance<P>(
84        &mut self,
85        provider: &P,
86        write: SnapWrite,
87        head: u64,
88        finalized: Option<u64>,
89    ) -> Result<Option<SnapWrite>, SnapSyncError>
90    where
91        P: HeaderProvider + BlockHashReader + MetadataProvider + MetadataWriter,
92    {
93        let SnapSyncSessionState::Downloading(mut current) = self.state else { return Ok(None) };
94        let attempt = provider.authorize_snap_write(write)?;
95        if attempt.pivot() != current.target() {
96            current = SnapGeneration::new(attempt.pivot(), attempt.state_root());
97            self.state = SnapSyncSessionState::Downloading(current);
98        }
99        if !self.policy.needs_advance(current, head) {
100            return Ok(None)
101        }
102        let Some(next) = self.policy.select(provider, head, finalized)? else { return Ok(None) };
103        // A policy anchoring further back than it re-anchors can select a block behind the pivot.
104        if next.target().number <= current.target().number {
105            return Ok(None)
106        }
107        let write = provider.advance_snap_pivot(write, next)?;
108        self.state = SnapSyncSessionState::Downloading(next);
109        Ok(Some(write))
110    }
111}
112
113/// What a [`SnapSyncSession`] is doing.
114#[derive(Clone, Copy, Debug, Eq, PartialEq)]
115pub enum SnapSyncSessionState {
116    /// No block is eligible yet, so the session holds no target.
117    Waiting,
118    /// A target is selected, but no work has taken it yet.
119    Selected(SnapGeneration),
120    /// Work is outstanding against the target.
121    Downloading(SnapGeneration),
122}
123
124impl SnapSyncSessionState {
125    /// Target of this state, if it has one.
126    pub const fn target(&self) -> Option<&SnapGeneration> {
127        match self {
128            Self::Selected(generation) | Self::Downloading(generation) => Some(generation),
129            Self::Waiting => None,
130        }
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137    use crate::test_utils::{chain, hashed_factory, policy, provider_with};
138    use reth_primitives_traits::SealedHeader;
139    use reth_provider::{
140        test_utils::{insert_headers, MockNodeTypesWithDB},
141        DBProvider, DatabaseProviderFactory, ProviderFactory,
142    };
143
144    fn session() -> SnapSyncSession {
145        SnapSyncSession::new(policy())
146    }
147
148    // A session downloading block 1 under head 2, re-anchoring once its pivot lags by two blocks,
149    // with the attempt recorded in a database holding blocks 0 through 3.
150    fn downloading() -> (ProviderFactory<MockNodeTypesWithDB>, SnapSyncSession, SnapWrite) {
151        let factory = hashed_factory();
152        let headers: Vec<_> = chain(Some(0)).into_iter().map(SealedHeader::seal_slow).collect();
153        insert_headers(&factory, &headers);
154        let provider = factory.database_provider_rw().unwrap();
155        let mut session = SnapSyncSession::new(policy().with_advance_after(1));
156        session.select(&provider, 2, None).unwrap();
157        let generation = session.start().unwrap();
158        let write = provider.start_snap_attempt(generation).unwrap();
159        provider.commit().unwrap();
160        (factory, session, write)
161    }
162
163    #[test]
164    fn a_target_no_work_took_is_replaced_as_the_head_advances() {
165        let provider = provider_with(chain(Some(0)));
166        let mut session = session();
167
168        session.select(&provider, 2, None).unwrap();
169        assert_eq!(session.target().unwrap().target().number, 1);
170
171        session.select(&provider, 3, None).unwrap();
172        assert_eq!(session.target().unwrap().target().number, 2);
173    }
174
175    #[test]
176    fn a_target_no_work_took_is_dropped_once_it_is_no_longer_eligible() {
177        let provider = provider_with(chain(Some(0)));
178        let mut session = session();
179        session.select(&provider, 3, None).unwrap();
180
181        // The head has run past the downloaded headers, so no candidate confirms the old target.
182        session.select(&provider, 9, None).unwrap();
183
184        assert_eq!(session.state(), &SnapSyncSessionState::Waiting);
185        assert!(session.start().is_none());
186    }
187
188    #[test]
189    fn a_target_work_took_is_kept() {
190        let provider = provider_with(chain(Some(0)));
191        let mut session = session();
192        session.select(&provider, 2, None).unwrap();
193        let started = session.start().unwrap();
194
195        session.select(&provider, 3, None).unwrap();
196
197        assert_eq!(session.state(), &SnapSyncSessionState::Downloading(started));
198    }
199
200    #[test]
201    fn a_recorded_target_is_resumed_once() {
202        let (factory, _, write) = downloading();
203        let provider = factory.database_provider_rw().unwrap();
204        let attempt = provider.authorize_snap_write(write).unwrap();
205        let recorded = SnapGeneration::new(attempt.pivot(), attempt.state_root());
206        let mut session = SnapSyncSession::new(policy().with_advance_after(1));
207
208        assert!(session.resume(recorded));
209        assert!(!session.resume(recorded));
210        assert!(session.start().is_none());
211        assert_eq!(session.state(), &SnapSyncSessionState::Downloading(recorded));
212        // A resumed target advances like a started one.
213        assert!(session.advance(&provider, write, 3, None).unwrap().is_some());
214        assert_eq!(session.target().unwrap().target().number, 2);
215    }
216
217    #[test]
218    fn a_lagging_target_advances_with_the_attempt() {
219        let (factory, mut session, write) = downloading();
220        let provider = factory.database_provider_rw().unwrap();
221
222        let advanced = session.advance(&provider, write, 3, None).unwrap().unwrap();
223
224        let target = *session.target().unwrap();
225        assert_eq!(target.target().number, 2);
226        assert!(matches!(session.state(), SnapSyncSessionState::Downloading(_)));
227        let attempt = provider.authorize_snap_write(advanced).unwrap();
228        assert_eq!((attempt.pivot(), attempt.state_root()), (target.target(), target.state_root()));
229        assert!(matches!(
230            provider.authorize_snap_write(write),
231            Err(SnapSyncError::StaleWrite { .. })
232        ));
233    }
234
235    #[test]
236    fn a_target_within_the_window_is_kept() {
237        let (factory, mut session, write) = downloading();
238        let provider = factory.database_provider_rw().unwrap();
239        let before = *session.target().unwrap();
240
241        assert_eq!(session.advance(&provider, write, 2, None).unwrap(), None);
242
243        assert_eq!(session.target(), Some(&before));
244        provider.authorize_snap_write(write).unwrap();
245    }
246
247    #[test]
248    fn a_refused_advance_keeps_the_target() {
249        let (factory, mut session, write) = downloading();
250        let provider = factory.database_provider_rw().unwrap();
251        let before = *session.target().unwrap();
252        provider.abandon_snap_attempt().unwrap();
253
254        let refused = session.advance(&provider, write, 3, None);
255
256        assert!(matches!(refused, Err(SnapSyncError::StaleWrite { .. })));
257        assert_eq!(session.state(), &SnapSyncSessionState::Downloading(before));
258    }
259
260    #[test]
261    fn a_rolled_back_advance_is_retried() {
262        let (factory, mut session, write) = downloading();
263        let provider = factory.database_provider_rw().unwrap();
264        session.advance(&provider, write, 3, None).unwrap().unwrap();
265        drop(provider);
266
267        let provider = factory.database_provider_rw().unwrap();
268        let advanced = session.advance(&provider, write, 3, None).unwrap().unwrap();
269
270        assert_eq!(session.target().unwrap().target().number, 2);
271        assert_eq!(provider.authorize_snap_write(advanced).unwrap().pivot().number, 2);
272    }
273
274    #[test]
275    fn a_target_no_work_took_is_not_advanced() {
276        let (factory, _, write) = downloading();
277        let provider = factory.database_provider_rw().unwrap();
278        let mut session = SnapSyncSession::new(policy().with_advance_after(1));
279        session.select(&provider, 2, None).unwrap();
280
281        assert_eq!(session.advance(&provider, write, 3, None).unwrap(), None);
282        assert_eq!(session.target().unwrap().target().number, 1);
283    }
284}