Skip to main content

zebrad/components/sync/
downloads.rs

1//! A download stream for Zebra's block syncer.
2
3use std::{
4    collections::HashMap,
5    convert,
6    pin::Pin,
7    sync::Arc,
8    task::{Context, Poll},
9    time::Duration,
10};
11
12use futures::{
13    future::{FutureExt, TryFutureExt},
14    ready,
15    stream::{FuturesUnordered, Stream},
16};
17use pin_project::pin_project;
18use thiserror::Error;
19use tokio::{
20    sync::{oneshot, watch},
21    task::JoinHandle,
22    time::{timeout, timeout_at, Instant},
23};
24use tower::{Service, ServiceExt};
25use tracing_futures::Instrument;
26
27use zebra_chain::{
28    block::{self, Height, HeightDiff},
29    chain_tip::ChainTip,
30};
31use zebra_network::{self as zn, PeerSocketAddr};
32use zebra_state as zs;
33
34use crate::components::sync::{
35    BLOCK_DOWNLOAD_ROUND_TIMEOUT, FINAL_CHECKPOINT_BLOCK_VERIFY_TIMEOUT,
36    FINAL_CHECKPOINT_BLOCK_VERIFY_TIMEOUT_LIMIT,
37};
38
39#[cfg(test)]
40mod tests;
41
42type BoxError = Box<dyn std::error::Error + Send + Sync + 'static>;
43
44/// A multiplier used to calculate the extra number of blocks we allow in the
45/// verifier, state, and block commit pipelines, on top of the lookahead limit.
46///
47/// The extra number of blocks is calculated using
48/// `lookahead_limit * VERIFICATION_PIPELINE_SCALING_MULTIPLIER`.
49///
50/// This allows the verifier and state queues, and the block commit channel,
51/// to hold a few extra tips responses worth of blocks,
52/// even if the syncer queue is full. Any unused capacity is shared between both queues.
53///
54/// If this capacity is exceeded, the downloader will tell the syncer to pause new downloads.
55///
56/// Since the syncer queue is limited to the `lookahead_limit`,
57/// the rest of the capacity is reserved for the other queues.
58/// There is no reserved capacity for the syncer queue:
59/// if the other queues stay full, the syncer will eventually time out and reset.
60pub const VERIFICATION_PIPELINE_SCALING_MULTIPLIER: usize = 2;
61
62/// The maximum height difference between Zebra's state tip and a downloaded block.
63/// Blocks higher than this will get dropped and return an error.
64pub const VERIFICATION_PIPELINE_DROP_LIMIT: HeightDiff = 50_000;
65
66/// How long to wait for the parent-height lookup that decides whether a behind-tip block was
67/// forged.
68///
69/// Bounds a drop path that also fires for honest old blocks. Timing out is treated as "no proof",
70/// so a slow state read costs the peer nothing.
71const PARENT_LOOKUP_TIMEOUT: Duration = Duration::from_secs(5);
72
73/// Errors that can occur while downloading and verifying a block.
74#[derive(Error, Debug)]
75#[allow(dead_code)]
76pub enum BlockDownloadVerifyError {
77    #[error("permanent readiness error from the network service: {error:?}")]
78    NetworkServiceError {
79        #[source]
80        error: BoxError,
81    },
82
83    #[error("permanent readiness error from the verifier service: {error:?}")]
84    VerifierServiceError {
85        #[source]
86        error: BoxError,
87    },
88
89    #[error("duplicate block hash queued for download: {hash:?}")]
90    DuplicateBlockQueuedForDownload { hash: block::Hash },
91
92    #[error("error downloading block: {error:?} {hash:?}")]
93    DownloadFailed {
94        #[source]
95        error: BoxError,
96        hash: block::Hash,
97    },
98
99    /// A downloaded block was a long way ahead of the state chain tip.
100    /// This error should be very rare during normal operation.
101    ///
102    /// We need to reset the syncer on this error, to allow the verifier and state to catch up,
103    /// or prevent it following a bad chain.
104    ///
105    /// If we don't reset the syncer on this error, it will continue downloading blocks from a bad
106    /// chain, or blocks far ahead of the current state tip.
107    #[error("downloaded block was too far ahead of the chain tip: {height:?} {hash:?}")]
108    AboveLookaheadHeightLimit {
109        height: block::Height,
110        hash: block::Hash,
111    },
112
113    /// A downloaded block claimed a height a long way behind the state chain tip.
114    ///
115    /// A peer can answer with a canonical header and a rewritten coinbase height because the
116    /// initial hash check does not recompute the header's commitment to the body's authorizing data
117    /// (GHSA-g95h-hw6g-pvgv). Peers also legitimately serve blocks that are genuinely this old, so
118    /// the block is always dropped, but `advertiser_addr` is only set when the parent Zebra already
119    /// holds proves the claimed height wrong — a block's height is one more than its parent's. An
120    /// authentic old block, or one whose parent Zebra does not hold, is dropped anonymously.
121    #[error("downloaded block was too far behind the chain tip: {height:?} {hash:?}")]
122    BehindTipHeightLimit {
123        height: block::Height,
124        hash: block::Hash,
125        advertiser_addr: Option<PeerSocketAddr>,
126    },
127
128    #[error("downloaded block had an invalid height: {hash:?}")]
129    InvalidHeight {
130        hash: block::Hash,
131        advertiser_addr: Option<PeerSocketAddr>,
132    },
133
134    #[error("block failed consensus validation: {error:?} {height:?} {hash:?}")]
135    Invalid {
136        #[source]
137        error: zebra_consensus::router::RouterError,
138        height: block::Height,
139        hash: block::Hash,
140        advertiser_addr: Option<PeerSocketAddr>,
141    },
142
143    #[error("block validation request failed: {error:?} {height:?} {hash:?}")]
144    ValidationRequestError {
145        #[source]
146        error: BoxError,
147        height: block::Height,
148        hash: block::Hash,
149    },
150
151    #[error("block download & verification was cancelled during download: {hash:?}")]
152    CancelledDuringDownload { hash: block::Hash },
153
154    #[error(
155        "block download & verification was cancelled while waiting for the verifier service: \
156         to become ready: {height:?} {hash:?}"
157    )]
158    CancelledAwaitingVerifierReadiness {
159        height: block::Height,
160        hash: block::Hash,
161    },
162
163    #[error(
164        "block download & verification was cancelled during verification: {height:?} {hash:?}"
165    )]
166    CancelledDuringVerification {
167        height: block::Height,
168        hash: block::Hash,
169    },
170
171    #[error(
172        "timeout during service readiness, download, verification, or internal downloader operation"
173    )]
174    Timeout,
175}
176
177impl From<tokio::time::error::Elapsed> for BlockDownloadVerifyError {
178    fn from(_value: tokio::time::error::Elapsed) -> Self {
179        BlockDownloadVerifyError::Timeout
180    }
181}
182
183/// Represents a [`Stream`] of download and verification tasks during chain sync.
184#[pin_project]
185#[derive(Debug)]
186pub struct Downloads<ZN, ZV, ZS, ZSTip>
187where
188    ZN: Service<zn::Request, Response = zn::Response, Error = BoxError> + Send + Sync + 'static,
189    ZN::Future: Send,
190    ZV: Service<zebra_consensus::Request, Response = block::Hash, Error = BoxError>
191        + Send
192        + Sync
193        + Clone
194        + 'static,
195    ZV::Future: Send,
196    ZS: zs::ReadState + Sync,
197    ZSTip: ChainTip + Clone + Send + 'static,
198{
199    // Services
200    //
201    /// A service that forwards requests to connected peers, and returns their
202    /// responses.
203    network: ZN,
204
205    /// A service that verifies downloaded blocks.
206    verifier: ZV,
207
208    /// A read-only handle to the state, used to query the blocks Zebra already holds.
209    ///
210    /// Checks a downloaded block's claimed height against its parent's, so a rewritten coinbase
211    /// height can be told apart from a genuinely old block (GHSA-g95h-hw6g-pvgv).
212    read_state: ZS,
213
214    /// Allows efficient access to the best tip of the blockchain.
215    latest_chain_tip: ZSTip,
216
217    // Configuration
218    //
219    /// The configured lookahead limit, after applying the minimum limit.
220    lookahead_limit: usize,
221
222    /// The largest block height for the checkpoint verifier, based on the current config.
223    max_checkpoint_height: Height,
224
225    // Shared syncer state
226    //
227    /// Sender that is set to `true` when the downloader is past the lookahead limit.
228    /// This is based on the downloaded block height and the state tip height.
229    past_lookahead_limit_sender: Arc<std::sync::Mutex<watch::Sender<bool>>>,
230
231    /// Receiver for `past_lookahead_limit_sender`, which is used to avoid accessing the mutex.
232    past_lookahead_limit_receiver: zs::WatchReceiver<bool>,
233
234    // Internal downloads state
235    //
236    /// A list of pending block download and verify tasks.
237    #[pin]
238    pending: FuturesUnordered<
239        JoinHandle<Result<(Height, block::Hash), (BlockDownloadVerifyError, block::Hash)>>,
240    >,
241
242    /// A list of channels that can be used to cancel pending block download and
243    /// verify tasks.
244    cancel_handles: HashMap<block::Hash, oneshot::Sender<()>>,
245}
246
247impl<ZN, ZV, ZS, ZSTip> Stream for Downloads<ZN, ZV, ZS, ZSTip>
248where
249    ZN: Service<zn::Request, Response = zn::Response, Error = BoxError> + Send + Sync + 'static,
250    ZN::Future: Send,
251    ZV: Service<zebra_consensus::Request, Response = block::Hash, Error = BoxError>
252        + Send
253        + Sync
254        + Clone
255        + 'static,
256    ZV::Future: Send,
257    ZS: zs::ReadState + Sync,
258    ZSTip: ChainTip + Clone + Send + 'static,
259{
260    type Item = Result<(Height, block::Hash), BlockDownloadVerifyError>;
261
262    fn poll_next(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Option<Self::Item>> {
263        let this = self.project();
264        // CORRECTNESS
265        //
266        // The current task must be scheduled for wakeup every time we return
267        // `Poll::Pending`.
268        //
269        // If no download and verify tasks have exited since the last poll, this
270        // task is scheduled for wakeup when the next task becomes ready.
271        //
272        // TODO: this would be cleaner with poll_map (#2693)
273        if let Some(join_result) = ready!(this.pending.poll_next(cx)) {
274            match join_result.expect("block download and verify tasks must not panic") {
275                Ok((height, hash)) => {
276                    this.cancel_handles.remove(&hash);
277
278                    Poll::Ready(Some(Ok((height, hash))))
279                }
280                Err((e, hash)) => {
281                    this.cancel_handles.remove(&hash);
282                    Poll::Ready(Some(Err(e)))
283                }
284            }
285        } else {
286            Poll::Ready(None)
287        }
288    }
289
290    fn size_hint(&self) -> (usize, Option<usize>) {
291        self.pending.size_hint()
292    }
293}
294
295impl<ZN, ZV, ZS, ZSTip> Downloads<ZN, ZV, ZS, ZSTip>
296where
297    ZN: Service<zn::Request, Response = zn::Response, Error = BoxError> + Send + Sync + 'static,
298    ZN::Future: Send,
299    ZV: Service<zebra_consensus::Request, Response = block::Hash, Error = BoxError>
300        + Send
301        + Sync
302        + Clone
303        + 'static,
304    ZV::Future: Send,
305    ZS: zs::ReadState + Sync,
306    ZSTip: ChainTip + Clone + Send + 'static,
307{
308    /// Initialize a new download stream with the provided `network` and
309    /// `verifier` services.
310    ///
311    /// Uses the `latest_chain_tip` and `lookahead_limit` to drop blocks
312    /// that are too far ahead of the current state tip.
313    /// Uses `max_checkpoint_height` to work around a known block timeout (#5125).
314    ///
315    /// Retry and hedge policies are supplied by `network`. A single deadline bounds network
316    /// readiness and the complete download round, including retries and hedges.
317    pub fn new(
318        network: ZN,
319        verifier: ZV,
320        read_state: ZS,
321        latest_chain_tip: ZSTip,
322        past_lookahead_limit_sender: watch::Sender<bool>,
323        lookahead_limit: usize,
324        max_checkpoint_height: Height,
325    ) -> Self {
326        let past_lookahead_limit_receiver =
327            zs::WatchReceiver::new(past_lookahead_limit_sender.subscribe());
328
329        Self {
330            network,
331            verifier,
332            read_state,
333            latest_chain_tip,
334            lookahead_limit,
335            max_checkpoint_height,
336            past_lookahead_limit_sender: Arc::new(std::sync::Mutex::new(
337                past_lookahead_limit_sender,
338            )),
339            past_lookahead_limit_receiver,
340            pending: FuturesUnordered::new(),
341            cancel_handles: HashMap::new(),
342        }
343    }
344
345    /// Queue a block for download and verification.
346    ///
347    /// This method waits for the network to become ready, returning an error if readiness fails
348    /// or the download deadline expires. It returns immediately after queuing the request.
349    #[instrument(level = "debug", skip(self), fields(%hash))]
350    pub async fn download_and_verify(
351        &mut self,
352        hash: block::Hash,
353    ) -> Result<(), BlockDownloadVerifyError> {
354        if self.cancel_handles.contains_key(&hash) {
355            metrics::counter!("sync.already.queued.dropped.block.hash.count").increment(1);
356            return Err(BlockDownloadVerifyError::DuplicateBlockQueuedForDownload { hash });
357        }
358
359        // We construct the block requests sequentially, waiting for the peer
360        // set to be ready to process each request. This ensures that we start
361        // block downloads in the order we want them (though they may resolve
362        // out of order), and it means that we respect backpressure. Otherwise,
363        // if we waited for readiness and did the service call in the spawned
364        // tasks, all of the spawned tasks would race each other waiting for the
365        // network to become ready.
366        let deadline = Instant::now() + BLOCK_DOWNLOAD_ROUND_TIMEOUT;
367        let block_req = timeout_at(deadline, self.network.ready())
368            .await?
369            .map_err(|error| BlockDownloadVerifyError::NetworkServiceError { error })?
370            .call(zn::Request::BlockDownload {
371                hash,
372                advertiser: None,
373            });
374
375        // This oneshot is used to signal cancellation to the download task.
376        let (cancel_tx, mut cancel_rx) = oneshot::channel::<()>();
377
378        let mut verifier = self.verifier.clone();
379        let read_state = self.read_state.clone();
380        let latest_chain_tip = self.latest_chain_tip.clone();
381
382        let lookahead_limit = self.lookahead_limit;
383        let max_checkpoint_height = self.max_checkpoint_height;
384
385        let past_lookahead_limit_sender = self.past_lookahead_limit_sender.clone();
386        let past_lookahead_limit_receiver = self.past_lookahead_limit_receiver.clone();
387
388        let task = tokio::spawn(
389            async move {
390                // Download the block.
391                // Prefer the cancel handle if both are ready.
392                let download_start = std::time::Instant::now();
393                let rsp = tokio::select! {
394                    biased;
395                    _ = &mut cancel_rx => {
396                        trace!("task cancelled prior to download completion");
397                        metrics::counter!("sync.cancelled.download.count").increment(1);
398                        metrics::histogram!("sync.block.download.duration_seconds", "result" => "cancelled")
399                            .record(download_start.elapsed().as_secs_f64());
400                        return Err(BlockDownloadVerifyError::CancelledDuringDownload { hash })
401                    }
402                    rsp = timeout_at(deadline, block_req) => rsp?
403                        .map_err(|error| BlockDownloadVerifyError::DownloadFailed { error, hash})?,
404                };
405
406                let (block, advertiser_addr) = if let zn::Response::Blocks(blocks) = rsp {
407                    assert_eq!(
408                        blocks.len(),
409                        1,
410                        "wrong number of blocks in response to a single hash"
411                    );
412
413                    blocks
414                        .first()
415                        .expect("just checked length")
416                        .available()
417                        .expect("unexpected missing block status: single block failures should be errors")
418                } else {
419                    unreachable!("wrong response to block request");
420                };
421                metrics::counter!("sync.downloaded.block.count").increment(1);
422                metrics::histogram!("sync.block.download.duration_seconds", "result" => "success")
423                    .record(download_start.elapsed().as_secs_f64());
424
425                // Security & Performance: reject blocks that are too far ahead of our tip.
426                // Avoids denial of service attacks, and reduces wasted work on high blocks
427                // that will timeout before being verified.
428                let tip_height = latest_chain_tip.best_tip_height();
429
430                let (lookahead_drop_height, lookahead_pause_height, lookahead_reset_height) = if let Some(tip_height) = tip_height {
431                    // Scale the height limit with the lookahead limit,
432                    // so users with low capacity or under DoS can reduce them both.
433                    let lookahead_pause = HeightDiff::try_from(
434                        lookahead_limit + lookahead_limit * VERIFICATION_PIPELINE_SCALING_MULTIPLIER,
435                    )
436                        .expect("fits in HeightDiff");
437
438
439                    ((tip_height + VERIFICATION_PIPELINE_DROP_LIMIT).expect("tip is much lower than Height::MAX"),
440                     (tip_height + lookahead_pause).expect("tip is much lower than Height::MAX"),
441                     (tip_height + lookahead_pause/2).expect("tip is much lower than Height::MAX"))
442                } else {
443                    let genesis_drop = VERIFICATION_PIPELINE_DROP_LIMIT.try_into().expect("fits in u32");
444                    let genesis_lookahead =
445                        u32::try_from(lookahead_limit - 1).expect("fits in u32");
446
447                    (block::Height(genesis_drop),
448                     block::Height(genesis_lookahead),
449                     block::Height(genesis_lookahead/2))
450                };
451
452                // Get the finalized tip height, assuming we're using the non-finalized state.
453                //
454                // It doesn't matter if we're a few blocks off here, because blocks this low
455                // are part of a fork with much less work. So they would be rejected anyway.
456                //
457                // And if we're still checkpointing, the checkpointer will reject blocks behind
458                // the finalized tip anyway.
459                //
460                // TODO: get the actual finalized tip height
461                let min_accepted_height = tip_height
462                    .map(|tip_height| {
463                        block::Height(tip_height.0.saturating_sub(zs::MAX_BLOCK_REORG_HEIGHT))
464                    })
465                    .unwrap_or(block::Height(0));
466
467                let block_height = if let Some(block_height) = block.coinbase_height() {
468                    block_height
469                } else {
470                    debug!(
471                        ?hash,
472                        "synced block with no height: dropped downloaded block"
473                    );
474                    metrics::counter!("sync.no.height.dropped.block.count").increment(1);
475
476                    return Err(BlockDownloadVerifyError::InvalidHeight { hash, advertiser_addr });
477                };
478
479                if block_height > lookahead_drop_height {
480                    Err(BlockDownloadVerifyError::AboveLookaheadHeightLimit {
481                        height: block_height,
482                        hash,
483                    })?;
484                } else if block_height > lookahead_pause_height {
485                    // This log can be very verbose, usually hundreds of blocks are dropped.
486                    // So we only log at info level for the first above-height block.
487                    if !past_lookahead_limit_receiver.cloned_watch_data() {
488                        info!(
489                            ?hash,
490                            ?block_height,
491                            ?tip_height,
492                            ?lookahead_pause_height,
493                            ?lookahead_reset_height,
494                            lookahead_limit = ?lookahead_limit,
495                            "synced block height too far ahead of the tip: \
496                             waiting for downloaded blocks to commit to the state",
497                        );
498
499                        // Set the watched value to true, since we're over the limit.
500                        //
501                        // It is ok to block here, because we're going to pause new downloads anyway.
502                        // But if Zebra is shutting down, ignore the send error.
503                        let _ = past_lookahead_limit_sender.lock().expect("thread panicked while holding the past_lookahead_limit_sender mutex guard").send(true);
504                    } else {
505                        debug!(
506                            ?hash,
507                            ?block_height,
508                            ?tip_height,
509                            ?lookahead_pause_height,
510                            ?lookahead_reset_height,
511                            lookahead_limit = ?lookahead_limit,
512                            "synced block height too far ahead of the tip: \
513                             waiting for downloaded blocks to commit to the state",
514                        );
515                    }
516
517                    metrics::counter!("sync.max.height.limit.paused.count").increment(1);
518                } else if block_height <= lookahead_reset_height && past_lookahead_limit_receiver.cloned_watch_data() {
519                    // Reset the watched value to false, since we're well under the limit.
520                    // We need to block here, because if we don't the syncer can hang.
521
522                    // But if Zebra is shutting down, ignore the send error.
523                    let _ = past_lookahead_limit_sender.lock().expect("thread panicked while holding the past_lookahead_limit_sender mutex guard").send(false);
524                    metrics::counter!("sync.max.height.limit.reset.count").increment(1);
525
526                    metrics::counter!("sync.max.height.limit.reset.attempt.count").increment(1);
527                }
528
529                if block_height < min_accepted_height {
530                    debug!(
531                        ?hash,
532                        ?block_height,
533                        ?tip_height,
534                        ?min_accepted_height,
535                        behind_tip_limit = ?zs::MAX_BLOCK_REORG_HEIGHT,
536                        "synced block height behind the finalized tip: dropped downloaded block"
537                    );
538                    metrics::counter!("gossip.min.height.limit.dropped.block.count").increment(1);
539
540                    // Security: only attribute the drop to the supplying peer when the block's own
541                    // parent proves the claimed height wrong.
542                    //
543                    // A peer can answer with a canonical header and a rewritten coinbase height
544                    // because the initial hash check does not recompute the header's commitment to
545                    // the body's authorizing data (GHSA-g95h-hw6g-pvgv). A block's height is one
546                    // more than its parent's, so a mismatch proves the body was rewritten.
547                    //
548                    // Peers legitimately serve genuinely old blocks: the syncer tolerates an
549                    // out-of-order first hash in `FindBlocks` responses, so a requested hash can
550                    // resolve to an old side-chain block that an honest peer still has. Those
551                    // responses are consistent with their parent, or have a parent we don't hold, so
552                    // they are dropped without scoring.
553                    let advertiser_addr = match advertiser_addr {
554                        Some(advertiser_addr) => match timeout(
555                            PARENT_LOOKUP_TIMEOUT,
556                            read_state.oneshot(zs::ReadRequest::BlockHeader(
557                                block.header.previous_block_hash.into(),
558                            )),
559                        )
560                        .await
561                        {
562                            Ok(Ok(zs::ReadResponse::BlockHeader {
563                                height: parent_height,
564                                ..
565                            })) if (parent_height + 1) != Some(block_height) => Some(advertiser_addr),
566                            // Parent unknown, height consistent with it, or the lookup failed or
567                            // timed out: there is no proof of misbehaviour, so the peer is not
568                            // scored.
569                            _ => None,
570                        },
571                        // There is no peer to score, so skip the state lookup.
572                        None => None,
573                    };
574
575                    return Err(BlockDownloadVerifyError::BehindTipHeightLimit {
576                        height: block_height,
577                        hash,
578                        advertiser_addr,
579                    });
580                }
581
582                // Wait for the verifier service to be ready.
583                let readiness = verifier.ready();
584                // Prefer the cancel handle if both are ready.
585                let verifier = tokio::select! {
586                    biased;
587                    _ = &mut cancel_rx => {
588                        trace!("task cancelled waiting for verifier service readiness");
589                        metrics::counter!("sync.cancelled.verify.ready.count").increment(1);
590                        return Err(BlockDownloadVerifyError::CancelledAwaitingVerifierReadiness { height: block_height, hash })
591                    }
592                    verifier = readiness => verifier,
593                };
594
595                // Verify the block.
596                let verify_start = std::time::Instant::now();
597                let mut rsp = verifier
598                    .map_err(|error| BlockDownloadVerifyError::VerifierServiceError { error })?
599                    .call(zebra_consensus::Request::Commit(block)).boxed();
600
601                // Add a shorter timeout to workaround a known bug (#5125)
602                let short_timeout_max = (max_checkpoint_height + FINAL_CHECKPOINT_BLOCK_VERIFY_TIMEOUT_LIMIT).expect("checkpoint block height is in valid range");
603                // Only fully verified blocks: the checkpoint verifier holds the final checkpoint
604                // block until its whole range arrives, which can legitimately take longer.
605                if block_height > max_checkpoint_height && block_height <= short_timeout_max {
606                    // Keep the typed `Elapsed` so `should_restart_sync` can match it (#11168).
607                    rsp = timeout(FINAL_CHECKPOINT_BLOCK_VERIFY_TIMEOUT, rsp)
608                        .map_err(BoxError::from)
609                        .map(|nested_result| nested_result.and_then(convert::identity)).boxed();
610                }
611
612                let verification = tokio::select! {
613                    biased;
614                    _ = &mut cancel_rx => {
615                        trace!("task cancelled prior to verification");
616                        metrics::counter!("sync.cancelled.verify.count").increment(1);
617                        metrics::histogram!("sync.block.verify.duration_seconds", "result" => "cancelled")
618                            .record(verify_start.elapsed().as_secs_f64());
619                        return Err(BlockDownloadVerifyError::CancelledDuringVerification { height: block_height, hash })
620                    }
621                    verification = rsp => verification,
622                };
623
624                let verify_result = if verification.is_ok() { "success" } else { "failure" };
625                metrics::histogram!("sync.block.verify.duration_seconds", "result" => verify_result)
626                    .record(verify_start.elapsed().as_secs_f64());
627
628                if verification.is_ok() {
629                    metrics::counter!("sync.verified.block.count").increment(1);
630                }
631
632                verification
633                    .map(|hash| (block_height, hash))
634                    .map_err(|err| {
635                        match err.downcast::<zebra_consensus::router::RouterError>() {
636                            Ok(error) => BlockDownloadVerifyError::Invalid { error: *error, height: block_height, hash, advertiser_addr },
637                            Err(error) => BlockDownloadVerifyError::ValidationRequestError { error, height: block_height, hash },
638                        }
639                    })
640            }
641            .in_current_span()
642            // Tack the hash onto the error so we can remove the cancel handle
643            // on failure as well as on success.
644            .map_err(move |e| (e, hash)),
645        );
646
647        // Try to start the spawned task before queueing the next block request
648        tokio::task::yield_now().await;
649
650        self.pending.push(task);
651        assert!(
652            self.cancel_handles.insert(hash, cancel_tx).is_none(),
653            "blocks are only queued once"
654        );
655
656        Ok(())
657    }
658
659    /// Cancel all running tasks and reset the downloader state.
660    pub fn cancel_all(&mut self) {
661        // Replace the pending task list with an empty one and drop it.
662        let _ = std::mem::take(&mut self.pending);
663
664        // Signal cancellation to all running tasks.
665        // Since we already dropped the JoinHandles above, they should
666        // fail silently.
667        for (_hash, cancel) in self.cancel_handles.drain() {
668            let _ = cancel.send(());
669        }
670
671        assert!(self.pending.is_empty());
672        assert!(self.cancel_handles.is_empty());
673
674        // Set the lookahead limit to false, since we're empty (so we're under the limit).
675        //
676        // It is ok to block here, because we're doing a reset and sleep anyway.
677        // But if Zebra is shutting down, ignore the send error.
678        let _ = self
679            .past_lookahead_limit_sender
680            .lock()
681            .expect("thread panicked while holding the past_lookahead_limit_sender mutex guard")
682            .send(false);
683    }
684
685    /// Get the number of currently in-flight download and verify tasks.
686    pub fn in_flight(&mut self) -> usize {
687        self.pending.len()
688    }
689
690    /// Returns true if there are no in-flight download and verify tasks.
691    #[allow(dead_code)]
692    pub fn is_empty(&mut self) -> bool {
693        self.pending.is_empty()
694    }
695}