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}