Skip to main content

zebrad/components/sync/
progress.rs

1//! Progress tracking for blockchain syncing.
2
3use std::{
4    cmp::min,
5    ops::Add,
6    time::{Duration, Instant},
7};
8
9use chrono::Utc;
10use num_integer::div_ceil;
11
12use tokio::sync::watch;
13use zebra_chain::{
14    block::{Height, HeightDiff},
15    chain_sync_status::ChainSyncStatus,
16    chain_tip::ChainTip,
17    fmt::humantime_seconds,
18    parameters::{Network, NetworkUpgrade},
19};
20use zebra_state::MAX_BLOCK_REORG_HEIGHT;
21
22use crate::components::{health::ChainTipMetrics, sync::SyncStatus};
23
24/// The amount of time between progress logs.
25const LOG_INTERVAL: Duration = Duration::from_secs(60);
26
27/// The amount of time between progress bar updates.
28const PROGRESS_BAR_INTERVAL: Duration = Duration::from_secs(5);
29
30/// The number of blocks we consider to be close to the tip.
31///
32/// Most chain forks are 1-7 blocks long.
33const MAX_CLOSE_TO_TIP_BLOCKS: HeightDiff = 1;
34
35/// Skip slow sync warnings when we are this close to the tip.
36///
37/// In testing, we've seen warnings around 30 blocks.
38///
39/// TODO: replace with `MAX_CLOSE_TO_TIP_BLOCKS` after fixing slow syncing near tip (#3375)
40const MIN_SYNC_WARNING_BLOCKS: HeightDiff = 60;
41
42/// The number of fractional digits in sync percentages.
43const SYNC_PERCENT_FRAC_DIGITS: usize = 3;
44
45/// The minimum number of extra blocks mined between updating a checkpoint list,
46/// and running an automated test that depends on that list.
47///
48/// Makes sure that the block finalization code always runs in sync tests,
49/// even if the miner or test node clock is wrong by a few minutes.
50///
51/// This is an estimate based on the time it takes to:
52/// - get the tip height from `zcashd`,
53/// - run `zebra-checkpoints` to update the checkpoint list,
54/// - submit a pull request, and
55/// - run a CI test that logs progress based on the new checkpoint height.
56///
57/// We might add tests that sync from a cached tip state,
58/// so we only allow a few extra blocks here.
59//
60// TODO: change to HeightDiff?
61const MIN_BLOCKS_MINED_AFTER_CHECKPOINT_UPDATE: u32 = 10;
62
63/// Logs Zebra's estimated progress towards the chain tip every minute or so, and
64/// updates a terminal progress bar every few seconds.
65///
66/// TODO:
67/// - log progress towards, remaining blocks before, and remaining time to next network upgrade
68/// - add some progress info to the metrics
69pub async fn show_block_chain_progress(
70    network: Network,
71    latest_chain_tip: impl ChainTip,
72    sync_status: SyncStatus,
73    chain_tip_metrics_sender: watch::Sender<ChainTipMetrics>,
74) -> ! {
75    // The minimum number of extra blocks after the highest checkpoint, based on:
76    // - the non-finalized state limit, and
77    // - the minimum number of extra blocks mined between a checkpoint update,
78    //   and the automated tests for that update.
79    let min_after_checkpoint_blocks =
80        MAX_BLOCK_REORG_HEIGHT + MIN_BLOCKS_MINED_AFTER_CHECKPOINT_UPDATE;
81    let min_after_checkpoint_blocks: HeightDiff = min_after_checkpoint_blocks.into();
82
83    // The minimum height of the valid best chain, based on:
84    // - the hard-coded checkpoint height,
85    // - the minimum number of blocks after the highest checkpoint.
86    let after_checkpoint_height = network
87        .checkpoint_list()
88        .max_height()
89        .add(min_after_checkpoint_blocks)
90        .expect("hard-coded checkpoint height is far below Height::MAX");
91
92    // The blocks after a checkpoint update are mined just above the highest checkpoint.
93    let min_minutes_after_checkpoint_update = div_ceil(
94        i64::from(MIN_BLOCKS_MINED_AFTER_CHECKPOINT_UPDATE)
95            * NetworkUpgrade::target_spacing_for_height(
96                &network,
97                network.checkpoint_list().max_height(),
98            )
99            .num_seconds(),
100        60,
101    );
102
103    // The last time we downloaded and verified at least one block.
104    //
105    // Initialized to the start time to simplify the code.
106    let mut last_state_change_time = Utc::now();
107    let mut last_state_change_instant = Instant::now();
108
109    // The state tip height, when we last downloaded and verified at least one block.
110    //
111    // Initialized to the genesis height to simplify the code.
112    let mut last_state_change_height = Height(0);
113
114    // The last time we logged an update.
115    let mut last_log_time = Instant::now();
116
117    #[cfg(feature = "progress-bar")]
118    let block_bar = howudoin::new().label("Blocks");
119    let mut is_chain_metrics_chan_closed = false;
120
121    loop {
122        let now = Utc::now();
123        let instant_now = Instant::now();
124
125        let is_syncer_stopped = sync_status.is_close_to_tip();
126
127        if let Some(estimated_height) =
128            latest_chain_tip.estimate_network_chain_tip_height(&network, now)
129        {
130            // The estimate/actual race doesn't matter here,
131            // because we're only using it for metrics and logging.
132            let current_height = latest_chain_tip
133                .best_tip_height()
134                .expect("unexpected empty state: estimate requires a block height");
135            let network_upgrade = NetworkUpgrade::current(&network, current_height);
136
137            // Send progress reports for block height
138            //
139            // TODO: split the progress bar height update into its own function.
140            #[cfg(feature = "progress-bar")]
141            if matches!(howudoin::cancelled(), Some(true)) {
142                block_bar.close();
143            } else {
144                block_bar
145                    .set_pos(current_height.0)
146                    .set_len(u64::from(estimated_height.0));
147            }
148
149            let mut remaining_sync_blocks = estimated_height - current_height;
150
151            if remaining_sync_blocks < 0 {
152                remaining_sync_blocks = 0;
153            }
154
155            // Export sync distance metrics for observability
156            metrics::gauge!("sync.estimated_network_tip_height").set(estimated_height.0 as f64);
157            metrics::gauge!("sync.estimated_distance_to_tip").set(remaining_sync_blocks as f64);
158
159            // Work out how long it has been since the state height has increased.
160            //
161            // Non-finalized forks can decrease the height, we only want to track increases.
162            if current_height > last_state_change_height {
163                last_state_change_height = current_height;
164                last_state_change_time = now;
165                last_state_change_instant = instant_now;
166            }
167
168            if !is_chain_metrics_chan_closed {
169                if let Err(err) = chain_tip_metrics_sender.send(ChainTipMetrics::new(
170                    last_state_change_instant,
171                    Some(remaining_sync_blocks),
172                )) {
173                    tracing::warn!(?err, "chain tip metrics channel closed");
174                    is_chain_metrics_chan_closed = true
175                };
176            }
177
178            // Skip logging and status updates if it isn't time for them yet.
179            let elapsed_since_log = instant_now.saturating_duration_since(last_log_time);
180            if elapsed_since_log < LOG_INTERVAL {
181                tokio::time::sleep(PROGRESS_BAR_INTERVAL).await;
182                continue;
183            } else {
184                last_log_time = instant_now;
185            }
186
187            // Use the spacing at the estimated network tip, which changes at NU7 (ZIP 218).
188            let target_block_spacing =
189                NetworkUpgrade::target_spacing_for_height(&network, estimated_height);
190            let max_block_spacing =
191                NetworkUpgrade::minimum_difficulty_spacing_for_height(&network, estimated_height);
192
193            // We expect the state height to increase at least once in this interval.
194            //
195            // Most chain forks are 1-7 blocks long.
196            //
197            // TODO: remove the target_block_spacing multiplier,
198            //       after fixing slow syncing near tip (#3375)
199            let min_state_block_interval =
200                max_block_spacing.unwrap_or(target_block_spacing * 4) * 2;
201
202            // Formatted strings for logging.
203            let target_block_spacing = humantime_seconds(
204                target_block_spacing
205                    .to_std()
206                    .expect("constant fits in std::Duration"),
207            );
208            let max_block_spacing = max_block_spacing
209                .map(|duration| {
210                    humantime_seconds(duration.to_std().expect("constant fits in std::Duration"))
211                })
212                .unwrap_or_else(|| "None".to_string());
213
214            // TODO: split logging / status updates into their own function.
215
216            // Work out the sync progress towards the estimated tip.
217            let sync_progress = f64::from(current_height.0) / f64::from(estimated_height.0);
218            let sync_percent = format!(
219                "{:.frac$}%",
220                sync_progress * 100.0,
221                frac = SYNC_PERCENT_FRAC_DIGITS,
222            );
223
224            let time_since_last_state_block_chrono =
225                now.signed_duration_since(last_state_change_time);
226            let time_since_last_state_block = humantime_seconds(
227                time_since_last_state_block_chrono
228                    .to_std()
229                    .unwrap_or_default(),
230            );
231
232            if time_since_last_state_block_chrono > min_state_block_interval {
233                // The state tip height hasn't increased for a long time.
234                //
235                // Block verification can fail if the local node's clock is wrong.
236                warn!(
237                    %sync_percent,
238                    ?current_height,
239                    ?network_upgrade,
240                    %time_since_last_state_block,
241                    %target_block_spacing,
242                    %max_block_spacing,
243                    ?is_syncer_stopped,
244                    "chain updates have stalled, \
245                     state height has not increased for {} minutes. \
246                     Hint: check your network connection, \
247                     and your computer clock and time zone",
248                    time_since_last_state_block_chrono.num_minutes(),
249                );
250
251                // TODO: use add_warn(), but only add each warning once
252                #[cfg(feature = "progress-bar")]
253                block_bar.desc(format!("{network_upgrade}: sync has stalled"));
254            } else if is_syncer_stopped && remaining_sync_blocks > MIN_SYNC_WARNING_BLOCKS {
255                // We've stopped syncing blocks, but we estimate we're a long way from the tip.
256                //
257                // TODO: warn after fixing slow syncing near tip (#3375)
258                info!(
259                    %sync_percent,
260                    ?current_height,
261                    ?network_upgrade,
262                    ?remaining_sync_blocks,
263                    ?after_checkpoint_height,
264                    %time_since_last_state_block,
265                    "initial sync is very slow, or estimated tip is wrong. \
266                     Hint: check your network connection, \
267                     and your computer clock and time zone",
268                );
269
270                #[cfg(feature = "progress-bar")]
271                block_bar.desc(format!(
272                    "{network_upgrade}: sync is very slow, or estimated tip is wrong"
273                ));
274            } else if is_syncer_stopped && current_height <= after_checkpoint_height {
275                // We've stopped syncing blocks,
276                // but we're below the minimum height estimated from our checkpoints.
277                warn!(
278                    %sync_percent,
279                    ?current_height,
280                    ?network_upgrade,
281                    ?remaining_sync_blocks,
282                    ?after_checkpoint_height,
283                    %time_since_last_state_block,
284                    "initial sync is very slow, and state is below the highest checkpoint. \
285                     Hint: check your network connection, \
286                     and your computer clock and time zone. \
287                     Dev Hint: were the checkpoints updated in the last {} minutes?",
288                    min_minutes_after_checkpoint_update,
289                );
290
291                #[cfg(feature = "progress-bar")]
292                block_bar.desc(format!("{network_upgrade}: sync is very slow"));
293            } else if is_syncer_stopped {
294                // We've stayed near the tip for a while, and we've stopped syncing lots of blocks.
295                // So we're mostly using gossiped blocks now.
296                info!(
297                    %sync_percent,
298                    ?current_height,
299                    ?network_upgrade,
300                    ?remaining_sync_blocks,
301                    %time_since_last_state_block,
302                    "finished initial sync to chain tip, using gossiped blocks",
303                );
304
305                #[cfg(feature = "progress-bar")]
306                block_bar.desc(format!("{network_upgrade}: waiting for next block"));
307            } else if remaining_sync_blocks <= MAX_CLOSE_TO_TIP_BLOCKS {
308                // We estimate we're near the tip, but we have been syncing lots of blocks recently.
309                // We might also be using some gossiped blocks.
310                info!(
311                    %sync_percent,
312                    ?current_height,
313                    ?network_upgrade,
314                    ?remaining_sync_blocks,
315                    %time_since_last_state_block,
316                    "close to finishing initial sync, \
317                     confirming using syncer and gossiped blocks",
318                );
319
320                #[cfg(feature = "progress-bar")]
321                block_bar.desc(format!("{network_upgrade}: finishing initial sync"));
322            } else {
323                // We estimate we're far from the tip, and we've been syncing lots of blocks.
324                info!(
325                    %sync_percent,
326                    ?current_height,
327                    ?network_upgrade,
328                    ?remaining_sync_blocks,
329                    %time_since_last_state_block,
330                    "estimated progress to chain tip",
331                );
332
333                #[cfg(feature = "progress-bar")]
334                block_bar.desc(format!("{network_upgrade}: syncing blocks"));
335            }
336        } else {
337            let sync_percent = format!("{:.SYNC_PERCENT_FRAC_DIGITS$} %", 0.0f64,);
338            #[cfg(feature = "progress-bar")]
339            let network_upgrade = NetworkUpgrade::Genesis;
340
341            if is_syncer_stopped {
342                // We've stopped syncing blocks,
343                // but we haven't downloaded and verified the genesis block.
344                warn!(
345                    %sync_percent,
346                    current_height = %"None",
347                    "initial sync can't download and verify the genesis block. \
348                     Hint: check your network connection, \
349                     and your computer clock and time zone",
350                );
351
352                #[cfg(feature = "progress-bar")]
353                block_bar.desc(format!("{network_upgrade}: can't download genesis block"));
354            } else {
355                // We're waiting for the genesis block to be committed to the state,
356                // before we can estimate the best chain tip.
357                info!(
358                    %sync_percent,
359                    current_height = %"None",
360                    "initial sync is waiting to download the genesis block",
361                );
362
363                #[cfg(feature = "progress-bar")]
364                block_bar.desc(format!(
365                    "{network_upgrade}: waiting to download genesis block"
366                ));
367            }
368        }
369
370        tokio::time::sleep(min(LOG_INTERVAL, PROGRESS_BAR_INTERVAL)).await;
371    }
372}