snarkos_node_sync/block_sync/
sync_state.rs1use super::MAX_BLOCKS_BEHIND;
17
18use std::{cmp::Ordering, time::Instant};
19
20#[derive(Clone, Copy, Debug, PartialEq, Eq)]
21pub enum SyncStatus {
22 Unsynced, Syncing, Synced, }
26
27#[derive(Clone, Copy, Debug, PartialEq, Eq)]
31pub enum BftSyncMode {
32 Fast,
35 Dag,
38}
39
40#[derive(Clone)]
41pub(super) struct SyncState {
42 sync_height: u32,
46 greatest_peer_height: Option<u32>,
49 status: SyncStatus,
52 last_change: Instant,
54 bft_sync_mode: Option<BftSyncMode>,
57}
58
59impl Default for SyncState {
60 fn default() -> Self {
61 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 pub fn new_with_height(height: u32) -> Self {
76 Self { sync_height: height, ..Default::default() }
77 }
78
79 pub fn is_block_synced(&self) -> bool {
82 self.status == SyncStatus::Synced
83 }
84
85 pub fn can_issue_new_block_requests(&self) -> bool {
88 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 pub fn get_sync_height(&self) -> u32 {
100 self.sync_height
101 }
102
103 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 pub fn get_greatest_peer_height(&self) -> Option<u32> {
111 self.greatest_peer_height
112 }
113
114 pub fn get_bft_sync_mode(&self) -> Option<BftSyncMode> {
116 self.bft_sync_mode
117 }
118
119 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 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 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 pub fn clear_greatest_peer_height(&mut self) {
157 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 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 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 if new_status == old_status {
188 return;
189 }
190
191 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 debug!("Block sync state changed to \"synced\".");
209 }
210 }
211 SyncStatus::Syncing => {
212 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 #[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 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 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 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 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 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 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 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 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 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 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 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}