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::{SnapGeneration, SnapPivotPolicy, SnapSyncError};
4use reth_storage_api::HeaderProvider;
5use tokio_util::sync::CancellationToken;
6
7/// One snap synchronization attempt, from the pivot it targets to the work it owns.
8///
9/// Targets come from the local chain, never from a peer: peers only supply state, which is
10/// authenticated against the target's state root.
11#[derive(Debug)]
12pub struct SnapSyncSession {
13    // Decides which blocks are eligible targets.
14    policy: SnapPivotPolicy,
15    // How far the attempt has got.
16    state: SnapSyncSessionState,
17    // Cancelled once, watched by whatever took the target.
18    cancellation: CancellationToken,
19}
20
21impl SnapSyncSession {
22    /// Creates a session waiting for its first eligible target.
23    pub fn new(policy: SnapPivotPolicy) -> Self {
24        Self {
25            policy,
26            state: SnapSyncSessionState::Waiting,
27            cancellation: CancellationToken::new(),
28        }
29    }
30
31    /// What the session is doing.
32    pub const fn state(&self) -> &SnapSyncSessionState {
33        &self.state
34    }
35
36    /// Pivot this session is anchored to, if it has one.
37    pub const fn target(&self) -> Option<&SnapGeneration> {
38        self.state.target()
39    }
40
41    /// Returns whether this session has been cancelled.
42    pub fn is_cancelled(&self) -> bool {
43        self.cancellation.is_cancelled()
44    }
45
46    /// Selects a target under `head`, or waits while no block is eligible.
47    ///
48    /// A target no work has taken yet is replaced by a newer eligible one and dropped when none is
49    /// eligible, since nothing authenticates against its root so far. Moving a target that work
50    /// has taken is instead pivot advancement, which has to carry the downloaded state forward.
51    pub fn select(
52        &mut self,
53        provider: &impl HeaderProvider,
54        head: u64,
55        finalized: Option<u64>,
56    ) -> Result<&SnapSyncSessionState, SnapSyncError> {
57        if matches!(self.state, SnapSyncSessionState::Waiting | SnapSyncSessionState::Selected(_)) {
58            self.state = match self.policy.select(provider, head, finalized)? {
59                Some(generation) => SnapSyncSessionState::Selected(generation),
60                None => SnapSyncSessionState::Waiting,
61            };
62        }
63        Ok(&self.state)
64    }
65
66    /// Hands the selected target, and the token to watch, to the work downloading against it.
67    ///
68    /// Taking the target is what starts it, so only the first caller gets one: a target already
69    /// being downloaded has an owner, and a waiting or cancelled session has nothing to hand out.
70    pub fn start(&mut self) -> Option<(SnapGeneration, CancellationToken)> {
71        let SnapSyncSessionState::Selected(generation) = self.state else { return None };
72        self.state = SnapSyncSessionState::Downloading(generation);
73        Some((generation, self.cancellation.clone()))
74    }
75
76    /// Signals outstanding work to stop and ends the session.
77    ///
78    /// Terminal: a later attempt needs a new session.
79    pub fn cancel(&mut self) {
80        self.cancellation.cancel();
81        self.state = SnapSyncSessionState::Cancelled;
82    }
83}
84
85/// What a [`SnapSyncSession`] is doing.
86#[derive(Clone, Copy, Debug, Eq, PartialEq)]
87pub enum SnapSyncSessionState {
88    /// No block is eligible yet, so the session holds no target.
89    Waiting,
90    /// A target is selected, but no work has taken it yet.
91    Selected(SnapGeneration),
92    /// Work is outstanding against the target.
93    Downloading(SnapGeneration),
94    /// The session was cancelled and its outstanding work signalled to stop.
95    Cancelled,
96}
97
98impl SnapSyncSessionState {
99    /// Target of this state, if it has one.
100    pub const fn target(&self) -> Option<&SnapGeneration> {
101        match self {
102            Self::Selected(generation) | Self::Downloading(generation) => Some(generation),
103            Self::Waiting | Self::Cancelled => None,
104        }
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111    use crate::test_utils::{chain, policy, provider_with};
112
113    fn session() -> SnapSyncSession {
114        SnapSyncSession::new(policy())
115    }
116
117    #[test]
118    fn waits_while_no_block_is_eligible() {
119        // The only block access list commitment is at the head, past the head distance.
120        let provider = provider_with(chain(Some(3)));
121        let mut session = session();
122
123        assert_eq!(session.select(&provider, 3, None).unwrap(), &SnapSyncSessionState::Waiting);
124        assert_eq!(session.target(), None);
125        assert!(session.start().is_none());
126    }
127
128    #[test]
129    fn selects_an_eligible_target() {
130        let headers = chain(Some(0));
131        let expected = headers[2].clone();
132        let provider = provider_with(headers);
133        let mut session = session();
134
135        session.select(&provider, 3, None).unwrap();
136
137        let target = session.target().unwrap();
138        assert_eq!(target.target().number, 2);
139        assert_eq!(target.target().hash, expected.hash_slow());
140    }
141
142    #[test]
143    fn a_target_no_work_took_is_replaced_as_the_head_advances() {
144        let provider = provider_with(chain(Some(0)));
145        let mut session = session();
146
147        session.select(&provider, 2, None).unwrap();
148        assert_eq!(session.target().unwrap().target().number, 1);
149
150        session.select(&provider, 3, None).unwrap();
151        assert_eq!(session.target().unwrap().target().number, 2);
152    }
153
154    #[test]
155    fn a_target_no_work_took_is_dropped_once_it_is_no_longer_eligible() {
156        let provider = provider_with(chain(Some(0)));
157        let mut session = session();
158        session.select(&provider, 3, None).unwrap();
159
160        // The head has run past the downloaded headers, so no candidate confirms the old target.
161        session.select(&provider, 9, None).unwrap();
162
163        assert_eq!(session.state(), &SnapSyncSessionState::Waiting);
164        assert!(session.start().is_none());
165    }
166
167    #[test]
168    fn a_target_work_took_is_kept() {
169        let provider = provider_with(chain(Some(0)));
170        let mut session = session();
171        session.select(&provider, 2, None).unwrap();
172        let (started, _) = session.start().unwrap();
173
174        session.select(&provider, 3, None).unwrap();
175
176        assert_eq!(session.state(), &SnapSyncSessionState::Downloading(started));
177    }
178
179    #[test]
180    fn only_one_worker_takes_a_target() {
181        let provider = provider_with(chain(Some(0)));
182        let mut session = session();
183        session.select(&provider, 3, None).unwrap();
184
185        assert!(session.start().is_some());
186        assert!(session.start().is_none());
187    }
188
189    #[test]
190    fn cancellation_stops_outstanding_work() {
191        let provider = provider_with(chain(Some(0)));
192        let mut session = session();
193        session.select(&provider, 3, None).unwrap();
194        let (_, outstanding) = session.start().unwrap();
195
196        session.cancel();
197
198        assert!(outstanding.is_cancelled());
199        assert!(session.is_cancelled());
200        assert_eq!(session.state(), &SnapSyncSessionState::Cancelled);
201        assert_eq!(session.target(), None);
202    }
203
204    #[test]
205    fn a_cancelled_session_selects_nothing() {
206        let provider = provider_with(chain(Some(0)));
207        let mut session = session();
208        session.cancel();
209
210        assert_eq!(session.select(&provider, 3, None).unwrap(), &SnapSyncSessionState::Cancelled);
211        assert!(session.start().is_none());
212    }
213}