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}