1use 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
44pub const VERIFICATION_PIPELINE_SCALING_MULTIPLIER: usize = 2;
61
62pub const VERIFICATION_PIPELINE_DROP_LIMIT: HeightDiff = 50_000;
65
66const PARENT_LOOKUP_TIMEOUT: Duration = Duration::from_secs(5);
72
73#[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 #[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 #[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#[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 network: ZN,
204
205 verifier: ZV,
207
208 read_state: ZS,
213
214 latest_chain_tip: ZSTip,
216
217 lookahead_limit: usize,
221
222 max_checkpoint_height: Height,
224
225 past_lookahead_limit_sender: Arc<std::sync::Mutex<watch::Sender<bool>>>,
230
231 past_lookahead_limit_receiver: zs::WatchReceiver<bool>,
233
234 #[pin]
238 pending: FuturesUnordered<
239 JoinHandle<Result<(Height, block::Hash), (BlockDownloadVerifyError, block::Hash)>>,
240 >,
241
242 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 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 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 #[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 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 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 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 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 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 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 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 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 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 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 _ => None,
570 },
571 None => None,
573 };
574
575 return Err(BlockDownloadVerifyError::BehindTipHeightLimit {
576 height: block_height,
577 hash,
578 advertiser_addr,
579 });
580 }
581
582 let readiness = verifier.ready();
584 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 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 let short_timeout_max = (max_checkpoint_height + FINAL_CHECKPOINT_BLOCK_VERIFY_TIMEOUT_LIMIT).expect("checkpoint block height is in valid range");
603 if block_height > max_checkpoint_height && block_height <= short_timeout_max {
606 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 .map_err(move |e| (e, hash)),
645 );
646
647 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 pub fn cancel_all(&mut self) {
661 let _ = std::mem::take(&mut self.pending);
663
664 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 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 pub fn in_flight(&mut self) -> usize {
687 self.pending.len()
688 }
689
690 #[allow(dead_code)]
692 pub fn is_empty(&mut self) -> bool {
693 self.pending.is_empty()
694 }
695}