Skip to main content

snarkos_node_sync/block_sync/
sync_state.rs

1// Copyright (c) 2019-2026 Provable Inc.
2// This file is part of the snarkOS library.
3
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at:
7
8// http://www.apache.org/licenses/LICENSE-2.0
9
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16use super::MAX_BLOCKS_BEHIND;
17
18use std::{cmp::Ordering, time::Instant};
19
20#[derive(Clone, Copy, Debug, PartialEq, Eq)]
21pub enum SyncStatus {
22    Unsynced, // Never synced or no peers
23    Syncing,  // In progress
24    Synced,   // Fully synced with peers
25}
26
27/// Whether the BFT layer is using fast-sync (outside the GC range) or DAG sync (within GC range).
28///
29/// This is `None` for nodes without a BFT layer (clients, provers).
30#[derive(Clone, Copy, Debug, PartialEq, Eq)]
31pub enum BftSyncMode {
32    /// Block-based synchronization when outside the GC range.
33    /// Certificates are not inserted into the DAG.
34    Fast,
35    /// DAG-based synchronization when within the GC range.
36    /// Certificates are inserted into the DAG and consensus runs normally.
37    Dag,
38}
39
40#[derive(Clone)]
41pub(super) struct SyncState {
42    /// The height we synced to already
43    /// Note: This can be greater than the current ledger height,
44    ///       if blocks are not fully committed yet
45    sync_height: u32,
46    /// The largest height of a peer's block locator.
47    /// Is `None` if we never received a peer locator.
48    greatest_peer_height: Option<u32>,
49    /// Are we synced?
50    /// Allows keeping track of when the sync state changes.
51    status: SyncStatus,
52    /// Last time the sync state changed
53    last_change: Instant,
54    /// The BFT sync mode (fast or DAG), set by the BFT layer.
55    /// `None` for nodes without a BFT layer (clients, provers).
56    bft_sync_mode: Option<BftSyncMode>,
57}
58
59impl Default for SyncState {
60    fn default() -> Self {
61        // `status` is set to `Synced` by default to ensure validators of a newly created chain generate blocks.
62        Self {
63            sync_height: 0,
64            greatest_peer_height: None,
65            status: SyncStatus::Synced,
66            last_change: Instant::now(),
67            bft_sync_mode: None,
68        }
69    }
70}
71
72impl SyncState {
73    /// Initialize the sync state at the given height.
74    /// Useful, when starting a node that already has blocks in its local storage.
75    pub fn new_with_height(height: u32) -> Self {
76        Self { sync_height: height, ..Default::default() }
77    }
78
79    /// Did we catch up with the greatest known peer height?
80    /// This will return false if we never synced from a peer.
81    pub fn is_block_synced(&self) -> bool {
82        self.status == SyncStatus::Synced
83    }
84
85    /// Returns `true` if there a blocks to sync from other nodes.
86    /// Returns `false` if the node has fully caught up with the rest of the network.
87    pub fn can_issue_new_block_requests(&self) -> bool {
88        // Return true if sync state is false even if we there are no known blocks to fetch,
89        // because otherwise nodes will never  switch to synced at startup.
90        if let Some(num_behind) = self.num_blocks_behind() {
91            num_behind > 0
92        } else {
93            debug!("Cannot block sync: the node has not received block locators yet");
94            false
95        }
96    }
97
98    /// Returns the sync height (this is always greater or equal than the ledger height).
99    pub fn get_sync_height(&self) -> u32 {
100        self.sync_height
101    }
102
103    // Compute the number of blocks that we are behind by.
104    // Returns None, if there is no known peer height.
105    pub fn num_blocks_behind(&self) -> Option<u32> {
106        self.greatest_peer_height.map(|peer_height| peer_height.saturating_sub(self.sync_height))
107    }
108
109    /// Returns the greatest block height of any connected peer.
110    pub fn get_greatest_peer_height(&self) -> Option<u32> {
111        self.greatest_peer_height
112    }
113
114    /// Returns the BFT sync mode, or `None` if no BFT layer is attached.
115    pub fn get_bft_sync_mode(&self) -> Option<BftSyncMode> {
116        self.bft_sync_mode
117    }
118
119    /// Sets the BFT sync mode.
120    ///
121    /// # Returns
122    /// The previous BFT sync mode (if any).
123    pub fn set_bft_sync_mode(&mut self, mode: BftSyncMode) -> Option<BftSyncMode> {
124        let prev = self.bft_sync_mode;
125        self.bft_sync_mode = Some(mode);
126        prev
127    }
128
129    /// Update the height we are synced to.
130    /// If the value is lower than the current height, the sync height remains unchanged.
131    pub fn set_sync_height(&mut self, sync_height: u32) {
132        if sync_height <= self.sync_height {
133            return;
134        }
135
136        trace!("Sync height increased from {old_height} to {sync_height}", old_height = self.sync_height);
137        self.sync_height = sync_height;
138        self.update_is_block_synced();
139    }
140
141    /// Update the greatest known height of a connected peer.
142    pub fn set_greatest_peer_height(&mut self, peer_height: u32) {
143        if let Some(old_height) = self.greatest_peer_height {
144            match old_height.cmp(&peer_height) {
145                Ordering::Equal => return,
146                Ordering::Greater => warn!("Greatest peer height reduced from {old_height} to {peer_height}"),
147                Ordering::Less => trace!("Greatest peer height increased from {old_height} to {peer_height}"),
148            }
149        }
150
151        self.greatest_peer_height = Some(peer_height);
152        self.update_is_block_synced();
153    }
154
155    /// Remove the greatest peer height (used when all peers disconnect).
156    pub fn clear_greatest_peer_height(&mut self) {
157        // No-op if there is no change.
158        if self.greatest_peer_height.is_none() {
159            return;
160        }
161
162        self.greatest_peer_height = None;
163        self.update_is_block_synced();
164    }
165
166    /// Updates the state of `is_block_synced` for the sync module.
167    fn update_is_block_synced(&mut self) {
168        trace!(
169            "Updating is_block_synced: greatest_peer_height={greatest_peer:?}, current_height={current}, status={status:?}",
170            greatest_peer = self.greatest_peer_height,
171            current = self.sync_height,
172            status = self.status,
173        );
174
175        let num_blocks_behind = self.num_blocks_behind();
176        let old_status = self.status;
177
178        // If there are no block locators, we consider ourselves synced.
179        // Otherwise, validators will never propose certificates.
180        let new_status = match num_blocks_behind {
181            Some(num) if num <= MAX_BLOCKS_BEHIND => SyncStatus::Synced,
182            Some(_) => SyncStatus::Syncing,
183            None => SyncStatus::Unsynced,
184        };
185
186        // Return early if the state is unchanged
187        if new_status == old_status {
188            return;
189        }
190
191        // Measure how long sync took.
192        let now = Instant::now();
193        let elapsed = now.saturating_duration_since(self.last_change).as_secs();
194
195        self.status = new_status;
196        self.last_change = now;
197
198        match self.status {
199            SyncStatus::Synced => {
200                if old_status == SyncStatus::Syncing {
201                    let elapsed =
202                        if elapsed < 60 { format!("{elapsed} seconds") } else { format!("{} minutes", elapsed / 60) };
203
204                    debug!("Block sync state changed to \"synced\". It took {elapsed} to catch up with the network.");
205                } else {
206                    // If we move directly from unsynced to synced, it means we connected to a peer with a lower height.
207                    // In this case it does not make sense to print how long sync took.
208                    debug!("Block sync state changed to \"synced\".");
209                }
210            }
211            SyncStatus::Syncing => {
212                // num_blocks_behind should never be None at this point,
213                // but we still use `unwrap_or` just in case.
214                let behind_msg = num_blocks_behind.map(|n| n.to_string()).unwrap_or("unknown".to_string());
215
216                debug!("Block sync state changed to \"syncing\". We are {behind_msg} blocks behind.");
217            }
218            SyncStatus::Unsynced => {
219                debug!("Block sync state changed to \"unsynced\". Connect more peers to resume block sync.");
220            }
221        }
222
223        // Update the `IS_SYNCED` metric.
224        #[cfg(feature = "metrics")]
225        metrics::gauge(metrics::bft::IS_SYNCED, self.status == SyncStatus::Synced);
226    }
227}
228
229#[cfg(test)]
230mod tests {
231    use super::*;
232
233    /// A peer height that is far enough ahead to put the node out of sync.
234    const WAY_AHEAD: u32 = 1_000;
235
236    #[test]
237    fn a_fresh_state_is_synced_so_a_new_chain_can_produce_blocks() {
238        let state = SyncState::default();
239
240        // This default is load-bearing: a validator on a newly created chain has no peer locators,
241        // and would never propose a certificate if it started out unsynced.
242        assert_eq!(state.status, SyncStatus::Synced);
243        assert!(state.is_block_synced());
244        assert_eq!(state.get_sync_height(), 0);
245        assert_eq!(state.get_greatest_peer_height(), None);
246        assert_eq!(state.get_bft_sync_mode(), None);
247    }
248
249    #[test]
250    fn new_with_height_starts_synced_at_the_given_height() {
251        let state = SyncState::new_with_height(42);
252
253        assert_eq!(state.get_sync_height(), 42);
254        assert!(state.is_block_synced());
255        assert_eq!(state.get_greatest_peer_height(), None);
256    }
257
258    #[test]
259    fn num_blocks_behind_is_unknown_until_a_peer_locator_arrives() {
260        let mut state = SyncState::new_with_height(10);
261        assert_eq!(state.num_blocks_behind(), None);
262
263        state.set_greatest_peer_height(15);
264        assert_eq!(state.num_blocks_behind(), Some(5));
265    }
266
267    #[test]
268    fn num_blocks_behind_saturates_when_the_node_leads_every_peer() {
269        let mut state = SyncState::new_with_height(100);
270        state.set_greatest_peer_height(90);
271
272        // The node is ahead, not behind by a wrapped-around amount.
273        assert_eq!(state.num_blocks_behind(), Some(0));
274        assert!(state.is_block_synced());
275    }
276
277    #[test]
278    fn the_sync_height_only_moves_forward() {
279        let mut state = SyncState::new_with_height(10);
280
281        state.set_sync_height(20);
282        assert_eq!(state.get_sync_height(), 20);
283
284        // Equal and lower values are ignored; the sync height must not rewind when blocks are
285        // still being committed.
286        state.set_sync_height(20);
287        assert_eq!(state.get_sync_height(), 20);
288        state.set_sync_height(5);
289        assert_eq!(state.get_sync_height(), 20);
290    }
291
292    #[test]
293    fn the_greatest_peer_height_may_move_backwards() {
294        let mut state = SyncState::default();
295
296        state.set_greatest_peer_height(100);
297        assert_eq!(state.get_greatest_peer_height(), Some(100));
298
299        // Unlike the sync height, this one is not monotonic: it tracks the peers we can currently
300        // see, so it must be able to fall when the tallest peer disconnects.
301        state.set_greatest_peer_height(50);
302        assert_eq!(state.get_greatest_peer_height(), Some(50));
303    }
304
305    #[test]
306    fn the_status_flips_at_the_max_blocks_behind_boundary() {
307        let mut state = SyncState::default();
308
309        // Exactly `MAX_BLOCKS_BEHIND` behind still counts as synced...
310        state.set_greatest_peer_height(MAX_BLOCKS_BEHIND);
311        assert_eq!(state.num_blocks_behind(), Some(MAX_BLOCKS_BEHIND));
312        assert_eq!(state.status, SyncStatus::Synced);
313        assert!(state.is_block_synced());
314
315        // ...one block further behind does not.
316        state.set_greatest_peer_height(MAX_BLOCKS_BEHIND + 1);
317        assert_eq!(state.status, SyncStatus::Syncing);
318        assert!(!state.is_block_synced());
319    }
320
321    #[test]
322    fn catching_up_with_the_network_returns_the_state_to_synced() {
323        let mut state = SyncState::default();
324
325        state.set_greatest_peer_height(WAY_AHEAD);
326        assert_eq!(state.status, SyncStatus::Syncing);
327
328        state.set_sync_height(WAY_AHEAD);
329        assert_eq!(state.status, SyncStatus::Synced);
330        assert!(state.is_block_synced());
331    }
332
333    #[test]
334    fn losing_every_peer_marks_the_node_unsynced() {
335        let mut state = SyncState::default();
336        state.set_greatest_peer_height(WAY_AHEAD);
337        state.set_sync_height(WAY_AHEAD);
338        assert_eq!(state.status, SyncStatus::Synced);
339
340        state.clear_greatest_peer_height();
341
342        // With no peer height there is nothing to compare against, which is distinct from being
343        // caught up.
344        assert_eq!(state.get_greatest_peer_height(), None);
345        assert_eq!(state.num_blocks_behind(), None);
346        assert_eq!(state.status, SyncStatus::Unsynced);
347        assert!(!state.is_block_synced());
348    }
349
350    #[test]
351    fn clearing_an_absent_peer_height_leaves_the_state_alone() {
352        let mut state = SyncState::default();
353
354        // The early return matters: without it, a node that never saw a peer would be knocked out
355        // of its initial `Synced` state by a routine disconnect sweep.
356        state.clear_greatest_peer_height();
357
358        assert_eq!(state.status, SyncStatus::Synced);
359        assert!(state.is_block_synced());
360    }
361
362    #[test]
363    fn no_block_requests_are_issued_before_any_peer_locator_arrives() {
364        let state = SyncState::new_with_height(10);
365
366        assert!(!state.can_issue_new_block_requests());
367    }
368
369    #[test]
370    fn block_requests_are_issued_while_any_blocks_remain() {
371        let mut state = SyncState::default();
372        state.set_greatest_peer_height(WAY_AHEAD);
373
374        assert!(state.can_issue_new_block_requests());
375    }
376
377    #[test]
378    fn block_requests_stop_once_the_node_draws_level() {
379        let mut state = SyncState::default();
380        state.set_greatest_peer_height(WAY_AHEAD);
381        state.set_sync_height(WAY_AHEAD);
382
383        assert!(!state.can_issue_new_block_requests());
384    }
385
386    #[test]
387    fn a_synced_node_still_requests_the_blocks_within_the_tolerance() {
388        let mut state = SyncState::default();
389        state.set_greatest_peer_height(MAX_BLOCKS_BEHIND);
390
391        // Being "synced" is a tolerance, not a guarantee of equality: the node reports itself
392        // synced while still fetching the last block or so.
393        assert!(state.is_block_synced());
394        assert!(state.can_issue_new_block_requests());
395    }
396
397    #[test]
398    fn the_bft_sync_mode_reports_the_previous_value_when_set() {
399        let mut state = SyncState::default();
400
401        // `None` distinguishes nodes without a BFT layer from those that have not chosen a mode.
402        assert_eq!(state.set_bft_sync_mode(BftSyncMode::Fast), None);
403        assert_eq!(state.get_bft_sync_mode(), Some(BftSyncMode::Fast));
404
405        assert_eq!(state.set_bft_sync_mode(BftSyncMode::Dag), Some(BftSyncMode::Fast));
406        assert_eq!(state.get_bft_sync_mode(), Some(BftSyncMode::Dag));
407    }
408
409    #[test]
410    fn the_bft_sync_mode_is_independent_of_the_sync_status() {
411        let mut state = SyncState::default();
412        state.set_bft_sync_mode(BftSyncMode::Dag);
413
414        state.set_greatest_peer_height(WAY_AHEAD);
415        assert_eq!(state.status, SyncStatus::Syncing);
416        assert_eq!(state.get_bft_sync_mode(), Some(BftSyncMode::Dag));
417
418        state.clear_greatest_peer_height();
419        assert_eq!(state.status, SyncStatus::Unsynced);
420        assert_eq!(state.get_bft_sync_mode(), Some(BftSyncMode::Dag));
421    }
422}