zebrad/components/sync/status.rs
1//! Syncer chain tip status, based on recent block locator responses from peers.
2
3use tokio::sync::watch;
4use zebra_chain::chain_sync_status::ChainSyncStatus;
5
6use super::RecentSyncLengths;
7
8#[cfg(any(test, feature = "proptest-impl"))]
9pub mod mock;
10#[cfg(test)]
11mod tests;
12
13/// A helper type to determine if the synchronizer has likely reached the chain tip.
14///
15/// This type can be used as a handle, so cloning it is cheap.
16#[derive(Clone, Debug)]
17pub struct SyncStatus {
18 latest_sync_length: watch::Receiver<Vec<usize>>,
19 is_regtest: bool,
20}
21
22impl SyncStatus {
23 /// The threshold that determines if the synchronization is at the chain
24 /// tip.
25 ///
26 /// This is based on the fact that sync lengths are around 2-20 blocks long
27 /// once Zebra reaches the tip.
28 const MIN_DIST_FROM_TIP: usize = 20;
29
30 /// Create an instance of [`SyncStatus`] for a specific network.
31 ///
32 /// The status is determined based on the latest counts of synchronized blocks, observed
33 /// through `latest_sync_length`. In regtest, [`ChainSyncStatus::is_close_to_tip`] always returns `true`.
34 pub fn new_for_network(
35 network: &zebra_chain::parameters::Network,
36 ) -> (Self, RecentSyncLengths) {
37 let (recent_sync_lengths, latest_sync_length) = RecentSyncLengths::new();
38 let status = SyncStatus {
39 latest_sync_length,
40 is_regtest: network.is_regtest(),
41 };
42
43 (status, recent_sync_lengths)
44 }
45
46 /// Create an instance of [`SyncStatus`].
47 ///
48 /// The status is determined based on the latest counts of synchronized blocks, observed
49 /// through `latest_sync_length`.
50 pub fn new() -> (Self, RecentSyncLengths) {
51 let (recent_sync_lengths, latest_sync_length) = RecentSyncLengths::new();
52 let status = SyncStatus {
53 latest_sync_length,
54 is_regtest: false,
55 };
56
57 (status, recent_sync_lengths)
58 }
59
60 /// Wait until the synchronization is likely close to the tip.
61 ///
62 /// Returns an error if communication with the synchronizer is lost.
63 pub async fn wait_until_close_to_tip(&mut self) -> Result<(), watch::error::RecvError> {
64 while !self.is_close_to_tip() {
65 self.latest_sync_length.changed().await?;
66 }
67
68 Ok(())
69 }
70}
71
72impl ChainSyncStatus for SyncStatus {
73 /// Check if the synchronization is likely close to the chain tip.
74 fn is_close_to_tip(&self) -> bool {
75 if self.is_regtest {
76 return true;
77 }
78
79 let sync_lengths = self.latest_sync_length.borrow();
80
81 // Return early if sync_lengths is empty.
82 if sync_lengths.is_empty() {
83 return false;
84 }
85
86 // Compute the sum of the `sync_lengths`.
87 // The sum is computed by saturating addition in order to avoid overflowing.
88 let sum = sync_lengths
89 .iter()
90 .fold(0usize, |sum, rhs| sum.saturating_add(*rhs));
91
92 // Compute the average sync length.
93 // This value effectively represents a simple moving average.
94 let avg = sum / sync_lengths.len();
95
96 // The synchronization process is close to the chain tip once the
97 // average sync length falls below the threshold.
98 avg < Self::MIN_DIST_FROM_TIP
99 }
100}