zebra_consensus/checkpoint.rs
1//! Checkpoint-based block verification.
2//!
3//! Checkpoint-based verification uses a list of checkpoint hashes to
4//! speed up the initial chain sync for Zebra. This list is distributed
5//! with Zebra.
6//!
7//! The checkpoint verifier queues pending blocks. Once there is a
8//! chain from the previous checkpoint to a target checkpoint, it
9//! verifies all the blocks in that chain, and sends accepted blocks to
10//! the state service as finalized chain state, skipping the majority of
11//! contextual verification checks.
12//!
13//! Verification starts at the first checkpoint, which is the genesis
14//! block for the configured network.
15
16use std::{
17 collections::BTreeMap,
18 ops::{Bound, Bound::*},
19 pin::Pin,
20 sync::{mpsc, Arc},
21 task::{Context, Poll},
22};
23
24use futures::{Future, FutureExt, TryFutureExt};
25use thiserror::Error;
26use tokio::sync::oneshot;
27use tower::{Service, ServiceExt};
28use tracing::instrument;
29
30use zebra_chain::{
31 amount::{self},
32 block::{self, Block},
33 parameters::{
34 checkpoint::list::CheckpointList, subsidy::SubsidyError, Network,
35 GENESIS_PREVIOUS_BLOCK_HASH,
36 },
37 work::equihash,
38};
39use zebra_state::{self as zs, CheckpointVerifiedBlock};
40
41use crate::{
42 block::VerifyBlockError,
43 checkpoint::types::{
44 Progress::{self, *},
45 TargetHeight::{self, *},
46 },
47 error::BlockError,
48 BoxError,
49};
50
51mod types;
52
53#[cfg(test)]
54mod tests;
55
56pub use zebra_node_services::constants::{MAX_CHECKPOINT_BYTE_COUNT, MAX_CHECKPOINT_HEIGHT_GAP};
57
58/// An unverified block, which is in the queue for checkpoint verification.
59#[derive(Debug)]
60struct QueuedBlock {
61 /// The block, with additional precalculated data.
62 block: CheckpointVerifiedBlock,
63 /// The transmitting end of the oneshot channel for this block's result.
64 tx: oneshot::Sender<Result<block::Hash, VerifyCheckpointError>>,
65}
66
67/// The unverified block, with a receiver for the [`QueuedBlock`]'s result.
68#[derive(Debug)]
69struct RequestBlock {
70 /// The block, with additional precalculated data.
71 block: CheckpointVerifiedBlock,
72 /// The receiving end of the oneshot channel for this block's result.
73 rx: oneshot::Receiver<Result<block::Hash, VerifyCheckpointError>>,
74}
75
76/// A list of unverified blocks at a particular height.
77///
78/// Typically contains a single block, but might contain more if a peer
79/// has an old chain fork. (Or sends us a bad block.)
80///
81/// The CheckpointVerifier avoids creating zero-block lists.
82type QueuedBlockList = Vec<QueuedBlock>;
83
84/// The maximum number of queued blocks at any one height.
85///
86/// This value is a tradeoff between:
87/// - rejecting bad blocks: if we queue more blocks, we need fewer network
88/// retries, but use a bit more CPU when verifying,
89/// - avoiding a memory DoS: if we queue fewer blocks, we use less memory.
90///
91/// Memory usage is controlled by the sync service, because it controls block
92/// downloads. When the verifier services process blocks, they reduce memory
93/// usage by committing blocks to the disk state. (Or dropping invalid blocks.)
94pub const MAX_QUEUED_BLOCKS_PER_HEIGHT: usize = 4;
95
96/// Convert a tip into its hash and matching progress.
97fn progress_from_tip(
98 checkpoint_list: &CheckpointList,
99 tip: Option<(block::Height, block::Hash)>,
100) -> (Option<block::Hash>, Progress<block::Height>) {
101 match tip {
102 Some((height, hash)) => {
103 if height >= checkpoint_list.max_height() {
104 (None, Progress::FinalCheckpoint)
105 } else {
106 metrics::gauge!("checkpoint.verified.height").set(height.0 as f64);
107 metrics::gauge!("checkpoint.processing.next.height").set(height.0 as f64);
108 (Some(hash), Progress::InitialTip(height))
109 }
110 }
111 // We start by verifying the genesis block, by itself
112 None => (None, Progress::BeforeGenesis),
113 }
114}
115
116/// A checkpointing block verifier.
117///
118/// Verifies blocks using a supplied list of checkpoints. There must be at
119/// least one checkpoint for the genesis block.
120pub struct CheckpointVerifier<S>
121where
122 S: Service<zs::Request, Response = zs::Response, Error = BoxError> + Send + Clone + 'static,
123 S::Future: Send + 'static,
124{
125 /// The checkpoint list for this verifier.
126 checkpoint_list: Arc<CheckpointList>,
127
128 /// The network rules used by this verifier.
129 network: Network,
130
131 /// The hash of the initial tip, if any.
132 initial_tip_hash: Option<block::Hash>,
133
134 /// The underlying state service, possibly wrapped in other services.
135 state_service: S,
136
137 /// A queue of unverified blocks.
138 ///
139 /// Contains a list of unverified blocks at each block height. In most cases,
140 /// the checkpoint verifier will store zero or one block at each height.
141 ///
142 /// Blocks are verified in order, once there is a chain from the previous
143 /// checkpoint to a target checkpoint.
144 ///
145 /// The first checkpoint does not have any ancestors, so it only verifies the
146 /// genesis block.
147 queued: BTreeMap<block::Height, QueuedBlockList>,
148
149 /// The current progress of this verifier.
150 verifier_progress: Progress<block::Height>,
151
152 /// A channel to receive requests to reset the verifier,
153 /// receiving the tip of the state.
154 reset_receiver: mpsc::Receiver<Option<(block::Height, block::Hash)>>,
155 /// A channel to send requests to reset the verifier,
156 /// passing the tip of the state.
157 reset_sender: mpsc::Sender<Option<(block::Height, block::Hash)>>,
158
159 /// Queued block height progress transmitter.
160 #[cfg(feature = "progress-bar")]
161 queued_blocks_bar: howudoin::Tx,
162
163 /// Verified checkpoint progress transmitter.
164 #[cfg(feature = "progress-bar")]
165 verified_checkpoint_bar: howudoin::Tx,
166}
167
168impl<S> std::fmt::Debug for CheckpointVerifier<S>
169where
170 S: Service<zs::Request, Response = zs::Response, Error = BoxError> + Send + Clone + 'static,
171 S::Future: Send + 'static,
172{
173 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
174 f.debug_struct("CheckpointVerifier")
175 .field("checkpoint_list", &self.checkpoint_list)
176 .field("network", &self.network)
177 .field("initial_tip_hash", &self.initial_tip_hash)
178 .field("queued", &self.queued)
179 .field("verifier_progress", &self.verifier_progress)
180 .finish()
181 }
182}
183
184impl<S> CheckpointVerifier<S>
185where
186 S: Service<zs::Request, Response = zs::Response, Error = BoxError> + Send + Clone + 'static,
187 S::Future: Send + 'static,
188{
189 /// Return a checkpoint verification service for `network`, using the
190 /// hard-coded checkpoint list, and the provided `state_service`.
191 ///
192 /// If `initial_tip` is Some(_), the verifier starts at that initial tip.
193 /// The initial tip can be between the checkpoints in the hard-coded
194 /// checkpoint list.
195 ///
196 /// The checkpoint verifier holds a state service of type `S`, into which newly
197 /// verified blocks will be committed. This state is pluggable to allow for
198 /// testing or instrumentation.
199 ///
200 /// This function should be called only once for a particular network, rather
201 /// than constructing multiple verification services for the same network. To
202 /// clone a CheckpointVerifier, you might need to wrap it in a
203 /// `tower::Buffer` service.
204 #[allow(dead_code)]
205 pub fn new(
206 network: &Network,
207 initial_tip: Option<(block::Height, block::Hash)>,
208 state_service: S,
209 ) -> Self {
210 let checkpoint_list = network.checkpoint_list();
211 let max_height = checkpoint_list.max_height();
212 tracing::info!(
213 ?max_height,
214 ?network,
215 ?initial_tip,
216 "initialising CheckpointVerifier"
217 );
218 Self::from_checkpoint_list(checkpoint_list, network, initial_tip, state_service)
219 }
220
221 /// Return a checkpoint verification service using `list`, `network`,
222 /// `initial_tip`, and `state_service`.
223 ///
224 /// Assumes that the provided genesis checkpoint is correct.
225 ///
226 /// Callers should prefer `CheckpointVerifier::new`, which uses the
227 /// hard-coded checkpoint lists, or `CheckpointList::from_list` if you need
228 /// to specify a custom checkpoint list. See those functions for more
229 /// details.
230 ///
231 /// This function is designed for use in tests.
232 #[allow(dead_code)]
233 pub(crate) fn from_list(
234 list: impl IntoIterator<Item = (block::Height, block::Hash)>,
235 network: &Network,
236 initial_tip: Option<(block::Height, block::Hash)>,
237 state_service: S,
238 ) -> Result<Self, VerifyCheckpointError> {
239 Ok(Self::from_checkpoint_list(
240 CheckpointList::from_list(list)
241 .map(Arc::new)
242 .map_err(VerifyCheckpointError::CheckpointList)?,
243 network,
244 initial_tip,
245 state_service,
246 ))
247 }
248
249 /// Return a checkpoint verification service using `checkpoint_list`,
250 /// `network`, `initial_tip`, and `state_service`.
251 ///
252 /// Assumes that the provided genesis checkpoint is correct.
253 ///
254 /// Callers should prefer `CheckpointVerifier::new`, which uses the
255 /// hard-coded checkpoint lists. See that function for more details.
256 pub(crate) fn from_checkpoint_list(
257 checkpoint_list: Arc<CheckpointList>,
258 network: &Network,
259 initial_tip: Option<(block::Height, block::Hash)>,
260 state_service: S,
261 ) -> Self {
262 // All the initialisers should call this function, so we only have to
263 // change fields or default values in one place.
264 let (initial_tip_hash, verifier_progress) =
265 progress_from_tip(&checkpoint_list, initial_tip);
266
267 let (sender, receiver) = mpsc::channel();
268
269 #[cfg(feature = "progress-bar")]
270 let queued_blocks_bar = howudoin::new_root().label("Checkpoint Queue Height");
271
272 #[cfg(feature = "progress-bar")]
273 let verified_checkpoint_bar =
274 howudoin::new_with_parent(queued_blocks_bar.id()).label("Verified Checkpoints");
275
276 let verifier = CheckpointVerifier {
277 checkpoint_list,
278 network: network.clone(),
279 initial_tip_hash,
280 state_service,
281 queued: BTreeMap::new(),
282 verifier_progress,
283 reset_receiver: receiver,
284 reset_sender: sender,
285 #[cfg(feature = "progress-bar")]
286 queued_blocks_bar,
287 #[cfg(feature = "progress-bar")]
288 verified_checkpoint_bar,
289 };
290
291 if verifier_progress.is_final_checkpoint() {
292 verifier.finish_diagnostics();
293 } else {
294 verifier.verified_checkpoint_diagnostics(verifier_progress.height());
295 }
296
297 verifier
298 }
299
300 /// Update diagnostics for queued blocks.
301 fn queued_block_diagnostics(&self, height: block::Height, hash: block::Hash) {
302 let max_queued_height = self
303 .queued
304 .keys()
305 .next_back()
306 .expect("queued has at least one entry");
307
308 metrics::gauge!("checkpoint.queued.max.height").set(max_queued_height.0 as f64);
309
310 let is_checkpoint = self.checkpoint_list.contains(height);
311 tracing::debug!(?height, ?hash, ?is_checkpoint, "queued block");
312
313 #[cfg(feature = "progress-bar")]
314 if matches!(howudoin::cancelled(), Some(true)) {
315 self.finish_diagnostics();
316 } else {
317 self.queued_blocks_bar
318 .set_pos(max_queued_height.0)
319 .set_len(u64::from(self.checkpoint_list.max_height().0));
320 }
321 }
322
323 /// Update diagnostics for verified checkpoints.
324 fn verified_checkpoint_diagnostics(&self, verified_height: impl Into<Option<block::Height>>) {
325 let Some(verified_height) = verified_height.into() else {
326 // We don't know if we have already finished, or haven't started yet,
327 // so don't register any progress
328 return;
329 };
330
331 metrics::gauge!("checkpoint.verified.height").set(verified_height.0 as f64);
332
333 let checkpoint_index = self.checkpoint_list.prev_checkpoint_index(verified_height);
334 let checkpoint_count = self.checkpoint_list.len();
335
336 metrics::gauge!("checkpoint.verified.count").set(checkpoint_index as f64);
337
338 tracing::debug!(
339 ?verified_height,
340 ?checkpoint_index,
341 ?checkpoint_count,
342 "verified checkpoint",
343 );
344
345 #[cfg(feature = "progress-bar")]
346 if matches!(howudoin::cancelled(), Some(true)) {
347 self.finish_diagnostics();
348 } else {
349 self.verified_checkpoint_bar
350 .set_pos(u64::try_from(checkpoint_index).expect("fits in u64"))
351 .set_len(u64::try_from(checkpoint_count).expect("fits in u64"));
352 }
353 }
354
355 /// Finish checkpoint verifier diagnostics.
356 fn finish_diagnostics(&self) {
357 #[cfg(feature = "progress-bar")]
358 {
359 self.queued_blocks_bar.close();
360 self.verified_checkpoint_bar.close();
361 }
362 }
363
364 /// Reset the verifier progress back to given tip.
365 fn reset_progress(&mut self, tip: Option<(block::Height, block::Hash)>) {
366 let (initial_tip_hash, verifier_progress) = progress_from_tip(&self.checkpoint_list, tip);
367 self.initial_tip_hash = initial_tip_hash;
368 self.verifier_progress = verifier_progress;
369
370 self.verified_checkpoint_diagnostics(verifier_progress.height());
371 }
372
373 /// Return the current verifier's progress.
374 ///
375 /// If verification has not started yet, returns `BeforeGenesis`,
376 /// or `InitialTip(height)` if there were cached verified blocks.
377 ///
378 /// If verification is ongoing, returns `PreviousCheckpoint(height)`.
379 /// `height` increases as checkpoints are verified.
380 ///
381 /// If verification has finished, returns `FinalCheckpoint`.
382 fn previous_checkpoint_height(&self) -> Progress<block::Height> {
383 self.verifier_progress
384 }
385
386 /// Return the start of the current checkpoint range.
387 ///
388 /// Returns None if verification has finished.
389 fn current_start_bound(&self) -> Option<Bound<block::Height>> {
390 match self.previous_checkpoint_height() {
391 BeforeGenesis => Some(Unbounded),
392 InitialTip(height) | PreviousCheckpoint(height) => Some(Excluded(height)),
393 FinalCheckpoint => None,
394 }
395 }
396
397 /// Return the target checkpoint height that we want to verify.
398 ///
399 /// If we need more blocks, returns `WaitingForBlocks`.
400 ///
401 /// If the queued blocks are continuous from the previous checkpoint to a
402 /// target checkpoint, returns `Checkpoint(height)`. The target checkpoint
403 /// can be multiple checkpoints ahead of the previous checkpoint.
404 ///
405 /// `height` increases as checkpoints are verified.
406 ///
407 /// If verification has finished, returns `FinishedVerifying`.
408 fn target_checkpoint_height(&self) -> TargetHeight {
409 // Find the height we want to start searching at
410 let start_height = match self.previous_checkpoint_height() {
411 // Check if we have the genesis block as a special case, to simplify the loop
412 BeforeGenesis if !self.queued.contains_key(&block::Height(0)) => {
413 tracing::trace!("Waiting for genesis block");
414 metrics::counter!("checkpoint.waiting.count").increment(1);
415 return WaitingForBlocks;
416 }
417 BeforeGenesis => block::Height(0),
418 InitialTip(height) | PreviousCheckpoint(height) => height,
419 FinalCheckpoint => return FinishedVerifying,
420 };
421
422 // Find the end of the continuous sequence of blocks, starting at the
423 // last verified checkpoint. If there is no verified checkpoint, start
424 // *after* the genesis block (which we checked above).
425 //
426 // If `btree_map::Range` implements `ExactSizeIterator`, it would be
427 // much faster to walk the checkpoint list, and compare the length of
428 // the `btree_map::Range` to the block height difference between
429 // checkpoints. (In maps, keys are unique, so we don't need to check
430 // each height value.)
431 //
432 // But at the moment, this implementation is slightly faster, because
433 // it stops after the first gap.
434 let mut pending_height = start_height;
435 for (&height, _) in self.queued.range((Excluded(pending_height), Unbounded)) {
436 // If the queued blocks are continuous.
437 if height == block::Height(pending_height.0 + 1) {
438 pending_height = height;
439 } else {
440 let gap = height.0 - pending_height.0;
441 // Try to log a useful message when checkpointing has issues
442 tracing::trace!(contiguous_height = ?pending_height,
443 next_height = ?height,
444 ?gap,
445 "Waiting for more checkpoint blocks");
446 break;
447 }
448 }
449 metrics::gauge!("checkpoint.queued.continuous.height").set(pending_height.0 as f64);
450
451 // Now find the start of the checkpoint range
452 let start = self.current_start_bound().expect(
453 "if verification has finished, we should have returned earlier in the function",
454 );
455 // Find the highest checkpoint below pending_height, excluding any
456 // previously verified checkpoints
457 let target_checkpoint = self
458 .checkpoint_list
459 .max_height_in_range((start, Included(pending_height)));
460
461 tracing::trace!(
462 checkpoint_start = ?start,
463 highest_contiguous_block = ?pending_height,
464 ?target_checkpoint
465 );
466
467 if let Some(block::Height(target_checkpoint)) = target_checkpoint {
468 metrics::gauge!("checkpoint.processing.next.height").set(target_checkpoint as f64);
469 } else {
470 // Use the start height if there is no potential next checkpoint
471 metrics::gauge!("checkpoint.processing.next.height").set(start_height.0 as f64);
472 metrics::counter!("checkpoint.waiting.count").increment(1);
473 }
474
475 target_checkpoint
476 .map(Checkpoint)
477 .unwrap_or(WaitingForBlocks)
478 }
479
480 /// Return the most recently verified checkpoint's hash.
481 ///
482 /// See `previous_checkpoint_height()` for details.
483 fn previous_checkpoint_hash(&self) -> Progress<block::Hash> {
484 match self.previous_checkpoint_height() {
485 BeforeGenesis => BeforeGenesis,
486 InitialTip(_) => self
487 .initial_tip_hash
488 .map(InitialTip)
489 .expect("initial tip height must have an initial tip hash"),
490 PreviousCheckpoint(height) => self
491 .checkpoint_list
492 .hash(height)
493 .map(PreviousCheckpoint)
494 .expect("every checkpoint height must have a hash"),
495 FinalCheckpoint => FinalCheckpoint,
496 }
497 }
498
499 /// Check that `height` is valid and able to be verified.
500 ///
501 /// Returns an error if:
502 /// - the block's height is greater than the maximum checkpoint
503 /// - there are no checkpoints
504 /// - the block's height is less than or equal to the previously verified
505 /// checkpoint
506 /// - verification has finished
507 fn check_height(&self, height: block::Height) -> Result<(), VerifyCheckpointError> {
508 if height > self.checkpoint_list.max_height() {
509 Err(VerifyCheckpointError::TooHigh {
510 height,
511 max_height: self.checkpoint_list.max_height(),
512 })?;
513 }
514
515 match self.previous_checkpoint_height() {
516 // Any height is valid
517 BeforeGenesis => {}
518 // Greater heights are valid
519 InitialTip(previous_height) | PreviousCheckpoint(previous_height)
520 if (height <= previous_height) =>
521 {
522 let e = Err(VerifyCheckpointError::AlreadyVerified {
523 height,
524 verified_height: previous_height,
525 });
526 tracing::trace!(?e);
527 e?;
528 }
529 InitialTip(_) | PreviousCheckpoint(_) => {}
530 // We're finished, so no checkpoint height is valid
531 FinalCheckpoint => Err(VerifyCheckpointError::Finished)?,
532 };
533
534 Ok(())
535 }
536
537 /// Increase the current checkpoint height to `verified_height`,
538 fn update_progress(&mut self, verified_height: block::Height) {
539 if let Some(max_height) = self.queued.keys().next_back() {
540 metrics::gauge!("checkpoint.queued.max.height").set(max_height.0 as f64);
541 } else {
542 // use f64::NAN as a sentinel value for "None", because 0 is a valid height
543 metrics::gauge!("checkpoint.queued.max.height").set(f64::NAN);
544 }
545 metrics::gauge!("checkpoint.queued_slots").set(self.queued.len() as f64);
546
547 // Ignore blocks that are below the previous checkpoint, or otherwise
548 // have invalid heights.
549 //
550 // We ignore out-of-order verification, such as:
551 // - the height is less than the previous checkpoint height, or
552 // - the previous checkpoint height is the maximum height (checkpoint verifies are finished),
553 // because futures might not resolve in height order.
554 if self.check_height(verified_height).is_err() {
555 return;
556 }
557
558 // Ignore heights that aren't checkpoint heights
559 if verified_height == self.checkpoint_list.max_height() {
560 self.verifier_progress = FinalCheckpoint;
561
562 tracing::info!(
563 final_checkpoint_height = ?verified_height,
564 "verified final checkpoint: starting full validation",
565 );
566
567 self.verified_checkpoint_diagnostics(verified_height);
568 self.finish_diagnostics();
569 } else if self.checkpoint_list.contains(verified_height) {
570 self.verifier_progress = PreviousCheckpoint(verified_height);
571 // We're done with the initial tip hash now
572 self.initial_tip_hash = None;
573
574 self.verified_checkpoint_diagnostics(verified_height);
575 }
576 }
577
578 /// Check that the block height, proof of work, and Merkle root are valid.
579 ///
580 /// Returns a [`CheckpointVerifiedBlock`] with precalculated block data.
581 ///
582 /// ## Security
583 ///
584 /// Checking the proof of work makes resource exhaustion attacks harder to
585 /// carry out, because malicious blocks require a valid proof of work.
586 ///
587 /// Checking the Merkle root ensures that the block hash binds the block
588 /// contents. To prevent malleability (CVE-2012-2459), we also need to check
589 /// whether the transaction hashes are unique.
590 fn check_block(
591 &self,
592 block: Arc<Block>,
593 ) -> Result<CheckpointVerifiedBlock, VerifyCheckpointError> {
594 let hash = block.hash();
595 let height = block
596 .coinbase_height()
597 .ok_or(VerifyCheckpointError::CoinbaseHeight { hash })?;
598 self.check_height(height)?;
599
600 if self.network.disable_pow() {
601 crate::block::check::difficulty_threshold_is_valid(
602 &block.header,
603 &self.network,
604 &height,
605 &hash,
606 )?;
607 } else {
608 crate::block::check::difficulty_is_valid(&block.header, &self.network, &height, &hash)?;
609 crate::block::check::equihash_solution_is_valid(&block.header)?;
610 }
611
612 // don't do precalculation until the block passes basic difficulty checks
613 let block = CheckpointVerifiedBlock::new(block, Some(hash));
614
615 crate::block::check::merkle_root_validity(
616 &self.network,
617 &block.block,
618 &block.transaction_hashes,
619 )?;
620
621 Ok(block)
622 }
623
624 /// Queue `block` for verification.
625 ///
626 /// On success, returns a [`RequestBlock`] containing the block,
627 /// precalculated request data, and the queued result receiver.
628 ///
629 /// Verification will finish when the chain to the next checkpoint is
630 /// complete, and the caller will be notified via the channel.
631 ///
632 /// If the block does not pass basic validity checks,
633 /// returns an error immediately.
634 #[allow(clippy::unwrap_in_result)]
635 fn queue_block(&mut self, block: Arc<Block>) -> Result<RequestBlock, VerifyCheckpointError> {
636 // Set up a oneshot channel to send results
637 let (tx, rx) = oneshot::channel();
638
639 // Check that the height and Merkle roots are valid.
640 let block = self.check_block(block)?;
641 let height = block.height;
642 let hash = block.hash;
643
644 let new_qblock = QueuedBlock {
645 block: block.clone(),
646 tx,
647 };
648 let req_block = RequestBlock { block, rx };
649
650 // Since we're using Arc<Block>, each entry is a single pointer to the
651 // Arc. But there are a lot of QueuedBlockLists in the queue, so we keep
652 // allocations as small as possible.
653 let qblocks = self
654 .queued
655 .entry(height)
656 .or_insert_with(|| QueuedBlockList::with_capacity(1));
657
658 // Replace older requests with newer ones.
659 // The newer block is ok, the older block is an error.
660 for qb in qblocks.iter_mut() {
661 if qb.block.hash == hash {
662 let e = VerifyCheckpointError::NewerRequest { height, hash };
663 tracing::trace!(?e, "failing older of duplicate requests");
664
665 // ## Security
666 //
667 // Replace the entire queued block.
668 //
669 // We don't check the authorizing data hash until checkpoint blocks reach the state.
670 // So signatures, proofs, or scripts could be different,
671 // even if the block hash is the same.
672
673 let old = std::mem::replace(qb, new_qblock);
674 let _ = old.tx.send(Err(e));
675 return Ok(req_block);
676 }
677 }
678
679 // Memory DoS resistance: limit the queued blocks at each height
680 if qblocks.len() >= MAX_QUEUED_BLOCKS_PER_HEIGHT {
681 let e = VerifyCheckpointError::QueuedLimit;
682 tracing::warn!(?e);
683 return Err(e);
684 }
685
686 // Add the block to the list of queued blocks at this height
687 // This is a no-op for the first block in each QueuedBlockList.
688 qblocks.reserve_exact(1);
689 qblocks.push(new_qblock);
690
691 self.queued_block_diagnostics(height, hash);
692
693 Ok(req_block)
694 }
695
696 /// During checkpoint range processing, process all the blocks at `height`.
697 ///
698 /// Returns the first valid block. If there is no valid block, returns None.
699 #[allow(clippy::unwrap_in_result)]
700 fn process_height(
701 &mut self,
702 height: block::Height,
703 expected_hash: block::Hash,
704 ) -> Option<QueuedBlock> {
705 let mut qblocks = self
706 .queued
707 .remove(&height)
708 .expect("the current checkpoint range has continuous Vec<QueuedBlock>s");
709 assert!(
710 !qblocks.is_empty(),
711 "the current checkpoint range has continuous Blocks"
712 );
713
714 // Check interim checkpoints
715 if let Some(checkpoint_hash) = self.checkpoint_list.hash(height) {
716 // We assume the checkpoints are valid. And we have verified back
717 // from the target checkpoint, so the last block must also be valid.
718 // This is probably a bad checkpoint list, a zebra bug, or a bad
719 // chain (in a testing mode like regtest).
720 assert_eq!(expected_hash, checkpoint_hash,
721 "checkpoints in the range should match: bad checkpoint list, zebra bug, or bad chain"
722 );
723 }
724
725 // Find a queued block at this height, which is part of the hash chain.
726 //
727 // There are two possible outcomes here:
728 // - one of the blocks matches the chain (the common case)
729 // - no blocks match the chain, verification has failed for this range
730 // If there are any side-chain blocks, they fail validation.
731 let mut valid_qblock = None;
732 for qblock in qblocks.drain(..) {
733 if qblock.block.hash == expected_hash {
734 if valid_qblock.is_none() {
735 // The first valid block at the current height
736 valid_qblock = Some(qblock);
737 } else {
738 unreachable!("unexpected duplicate block {:?} {:?}: duplicate blocks should be rejected before being queued",
739 height, qblock.block.hash);
740 }
741 } else {
742 tracing::info!(?height, ?qblock.block.hash, ?expected_hash,
743 "Side chain hash at height in CheckpointVerifier");
744 let _ = qblock
745 .tx
746 .send(Err(VerifyCheckpointError::UnexpectedSideChain {
747 found: qblock.block.hash,
748 expected: expected_hash,
749 }));
750 }
751 }
752
753 valid_qblock
754 }
755
756 /// Try to verify from the previous checkpoint to a target checkpoint.
757 ///
758 /// Send `Ok` for the blocks that are in the chain, and `Err` for side-chain
759 /// blocks.
760 ///
761 /// Does nothing if we are waiting for more blocks, or if verification has
762 /// finished.
763 fn process_checkpoint_range(&mut self) {
764 // If this code shows up in profiles, we can try the following
765 // optimisations:
766 // - only check the chain when the length of the queue is greater
767 // than or equal to the length of a checkpoint interval
768 // (note: the genesis checkpoint interval is only one block long)
769 // - cache the height of the last continuous chain as a new field in
770 // self, and start at that height during the next check.
771
772 // Return early if verification has finished
773 let previous_checkpoint_hash = match self.previous_checkpoint_hash() {
774 // Since genesis blocks are hard-coded in zcashd, and not verified
775 // like other blocks, the genesis parent hash is set by the
776 // consensus parameters.
777 BeforeGenesis => GENESIS_PREVIOUS_BLOCK_HASH,
778 InitialTip(hash) | PreviousCheckpoint(hash) => hash,
779 FinalCheckpoint => return,
780 };
781 // Return early if we're still waiting for more blocks
782 let (target_checkpoint_height, mut expected_hash) = match self.target_checkpoint_height() {
783 Checkpoint(height) => (
784 height,
785 self.checkpoint_list
786 .hash(height)
787 .expect("every checkpoint height must have a hash"),
788 ),
789 WaitingForBlocks => {
790 return;
791 }
792 FinishedVerifying => {
793 unreachable!("the FinalCheckpoint case should have returned earlier")
794 }
795 };
796
797 // Keep the old previous checkpoint height, to make sure we're making
798 // progress
799 let old_prev_check_height = self.previous_checkpoint_height();
800
801 // Work out which blocks and checkpoints we're checking
802 let current_range = (
803 self.current_start_bound()
804 .expect("earlier code checks if verification has finished"),
805 Included(target_checkpoint_height),
806 );
807 let range_heights: Vec<block::Height> = self
808 .queued
809 .range_mut(current_range)
810 .rev()
811 .map(|(key, _)| *key)
812 .collect();
813 // A list of pending valid blocks, in reverse chain order
814 let mut rev_valid_blocks = Vec::new();
815
816 // Check all the blocks, and discard all the bad blocks
817 for current_height in range_heights {
818 let valid_qblock = self.process_height(current_height, expected_hash);
819 if let Some(qblock) = valid_qblock {
820 expected_hash = qblock.block.block.header.previous_block_hash;
821 // Add the block to the end of the pending block list
822 // (since we're walking the chain backwards, the list is
823 // in reverse chain order)
824 rev_valid_blocks.push(qblock);
825 } else {
826 // The last block height we processed did not have any blocks
827 // with a matching hash, so chain verification has failed.
828 tracing::info!(
829 ?current_height,
830 ?current_range,
831 "No valid blocks at height in CheckpointVerifier"
832 );
833
834 // We kept all the matching blocks down to this height, in
835 // anticipation of the chain verifying. But the chain is
836 // incomplete, so we have to put them back in the queue.
837 //
838 // The order here shouldn't matter, but add the blocks in
839 // height order, for consistency.
840 for vblock in rev_valid_blocks.drain(..).rev() {
841 self.queued
842 .entry(vblock.block.height)
843 .or_default()
844 .push(vblock);
845 }
846
847 // Make sure the current progress hasn't changed
848 assert_eq!(
849 self.previous_checkpoint_height(),
850 old_prev_check_height,
851 "we must not change the previous checkpoint on failure"
852 );
853 // We've reduced the target
854 //
855 // This check should be cheap, because we just reduced the target
856 let current_target = self.target_checkpoint_height();
857 assert!(
858 current_target == WaitingForBlocks
859 || current_target < Checkpoint(target_checkpoint_height),
860 "we must decrease or eliminate our target on failure"
861 );
862
863 // Stop verifying, and wait for the next valid block
864 return;
865 }
866 }
867
868 // The checkpoint and the parent hash must match.
869 // See the detailed checkpoint comparison comment above.
870 assert_eq!(
871 expected_hash, previous_checkpoint_hash,
872 "the previous checkpoint should match: bad checkpoint list, zebra bug, or bad chain"
873 );
874
875 let block_count = rev_valid_blocks.len();
876 tracing::info!(?block_count, ?current_range, "verified checkpoint range");
877 metrics::counter!("checkpoint.verified.block.count").increment(block_count as u64);
878
879 // All the blocks we've kept are valid, so let's verify them
880 // in height order.
881 for qblock in rev_valid_blocks.drain(..).rev() {
882 // Sending can fail, but there's nothing we can do about it.
883 let _ = qblock.tx.send(Ok(qblock.block.hash));
884 }
885
886 // Finally, update the checkpoint bounds
887 self.update_progress(target_checkpoint_height);
888
889 // Ensure that we're making progress
890 let new_progress = self.previous_checkpoint_height();
891 assert!(
892 new_progress > old_prev_check_height,
893 "we must make progress on success"
894 );
895 // We met the old target
896 if new_progress == FinalCheckpoint {
897 assert_eq!(
898 target_checkpoint_height,
899 self.checkpoint_list.max_height(),
900 "we finish at the maximum checkpoint"
901 );
902 } else {
903 assert_eq!(
904 new_progress,
905 PreviousCheckpoint(target_checkpoint_height),
906 "the new previous checkpoint must match the old target"
907 );
908 }
909 // We processed all available checkpoints
910 //
911 // We've cleared the target range, so this check should be cheap
912 let new_target = self.target_checkpoint_height();
913 assert!(
914 new_target == WaitingForBlocks || new_target == FinishedVerifying,
915 "processing must cover all available checkpoints"
916 );
917 }
918}
919
920/// CheckpointVerifier rejects pending futures on drop.
921impl<S> Drop for CheckpointVerifier<S>
922where
923 S: Service<zs::Request, Response = zs::Response, Error = BoxError> + Send + Clone + 'static,
924 S::Future: Send + 'static,
925{
926 /// Send an error on `tx` for any `QueuedBlock`s that haven't been verified.
927 ///
928 /// We can't implement `Drop` on QueuedBlock, because `send()` consumes
929 /// `tx`. And `tx` doesn't implement `Copy` or `Default` (for `take()`).
930 fn drop(&mut self) {
931 self.finish_diagnostics();
932
933 let drop_keys: Vec<_> = self.queued.keys().cloned().collect();
934 for key in drop_keys {
935 let mut qblocks = self
936 .queued
937 .remove(&key)
938 .expect("each entry is only removed once");
939 for qblock in qblocks.drain(..) {
940 // Sending can fail, but there's nothing we can do about it.
941 let _ = qblock.tx.send(Err(VerifyCheckpointError::Dropped));
942 }
943 }
944 }
945}
946
947#[derive(Debug, Error)]
948#[allow(missing_docs)]
949pub enum VerifyCheckpointError {
950 #[error("checkpoint request after the final checkpoint has been verified")]
951 Finished,
952 #[error("block at {height:?} is higher than the maximum checkpoint {max_height:?}")]
953 TooHigh {
954 height: block::Height,
955 max_height: block::Height,
956 },
957 #[error("block {height:?} is less than or equal to the verified tip {verified_height:?}")]
958 AlreadyVerified {
959 height: block::Height,
960 verified_height: block::Height,
961 },
962 #[error("rejected older of duplicate verification requests for block at {height:?} {hash:?}")]
963 NewerRequest {
964 height: block::Height,
965 hash: block::Hash,
966 },
967 #[error("the block {hash:?} does not have a coinbase height")]
968 CoinbaseHeight { hash: block::Hash },
969 #[error("merkle root {actual:?} does not match expected {expected:?}")]
970 BadMerkleRoot {
971 actual: block::merkle::Root,
972 expected: block::merkle::Root,
973 },
974 #[error("duplicate transactions in block")]
975 DuplicateTransaction,
976 #[error("checkpoint verifier was dropped")]
977 Dropped,
978 #[error(transparent)]
979 CommitCheckpointVerified(BoxError),
980 #[error(transparent)]
981 Tip(BoxError),
982 #[error(transparent)]
983 CheckpointList(BoxError),
984 #[error(transparent)]
985 VerifyBlock(VerifyBlockError),
986 #[error("invalid block subsidy: {0}")]
987 SubsidyError(#[from] SubsidyError),
988 #[error("invalid amount: {0}")]
989 AmountError(#[from] amount::Error),
990 #[error("too many queued blocks at this height")]
991 QueuedLimit,
992 #[error("the block hash does not match the chained checkpoint hash, expected {expected:?} found {found:?}")]
993 UnexpectedSideChain {
994 expected: block::Hash,
995 found: block::Hash,
996 },
997 #[error("zebra is shutting down")]
998 ShuttingDown,
999}
1000
1001impl From<VerifyBlockError> for VerifyCheckpointError {
1002 fn from(err: VerifyBlockError) -> VerifyCheckpointError {
1003 VerifyCheckpointError::VerifyBlock(err)
1004 }
1005}
1006
1007impl From<BlockError> for VerifyCheckpointError {
1008 fn from(err: BlockError) -> VerifyCheckpointError {
1009 VerifyCheckpointError::VerifyBlock(err.into())
1010 }
1011}
1012
1013impl From<equihash::Error> for VerifyCheckpointError {
1014 fn from(err: equihash::Error) -> VerifyCheckpointError {
1015 VerifyCheckpointError::VerifyBlock(err.into())
1016 }
1017}
1018
1019impl VerifyCheckpointError {
1020 /// Returns `true` if this is definitely a duplicate request.
1021 /// Some duplicate requests might not be detected, and therefore return `false`.
1022 pub fn is_duplicate_request(&self) -> bool {
1023 match self {
1024 VerifyCheckpointError::AlreadyVerified { .. } => true,
1025 // TODO: make this duplicate-incomplete
1026 VerifyCheckpointError::NewerRequest { .. } => true,
1027 VerifyCheckpointError::VerifyBlock(block_error) => block_error.is_duplicate_request(),
1028 // The state boxes commit errors as `zs::CommitCheckpointVerifiedError`,
1029 // a newtype around `zs::CommitBlockError`, so the wrapper must be
1030 // unwrapped to classify duplicate blocks as benign.
1031 VerifyCheckpointError::CommitCheckpointVerified(source) => source
1032 .downcast_ref::<zs::CommitCheckpointVerifiedError>()
1033 .is_some_and(|commit_err| commit_err.inner().is_duplicate_request()),
1034 _ => false,
1035 }
1036 }
1037
1038 /// Returns a suggested misbehaviour score increment for a certain error.
1039 pub fn misbehavior_score(&self) -> u32 {
1040 // TODO: Adjust these values based on zcashd (#9258).
1041 match self {
1042 VerifyCheckpointError::VerifyBlock(verify_block_error) => {
1043 verify_block_error.misbehavior_score()
1044 }
1045 VerifyCheckpointError::SubsidyError(_)
1046 | VerifyCheckpointError::CoinbaseHeight { .. }
1047 | VerifyCheckpointError::DuplicateTransaction
1048 | VerifyCheckpointError::AmountError(_) => 100,
1049 _other => 0,
1050 }
1051 }
1052}
1053
1054/// The CheckpointVerifier service implementation.
1055///
1056/// After verification, the block futures resolve to their hashes.
1057impl<S> Service<Arc<Block>> for CheckpointVerifier<S>
1058where
1059 S: Service<zs::Request, Response = zs::Response, Error = BoxError> + Send + Clone + 'static,
1060 S::Future: Send + 'static,
1061{
1062 type Response = block::Hash;
1063 type Error = VerifyCheckpointError;
1064 type Future =
1065 Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send + 'static>>;
1066
1067 fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
1068 Poll::Ready(Ok(()))
1069 }
1070
1071 #[instrument(name = "checkpoint", skip(self, block))]
1072 fn call(&mut self, block: Arc<Block>) -> Self::Future {
1073 // Reset the verifier back to the state tip if requested
1074 // (e.g. due to an error when committing a block to the state)
1075 if let Ok(tip) = self.reset_receiver.try_recv() {
1076 self.reset_progress(tip);
1077 }
1078
1079 // Immediately reject all incoming blocks that arrive after we've finished.
1080 if let FinalCheckpoint = self.previous_checkpoint_height() {
1081 return async { Err(VerifyCheckpointError::Finished) }.boxed();
1082 }
1083
1084 let req_block = match self.queue_block(block) {
1085 Ok(req_block) => req_block,
1086 Err(e) => return async { Err(e) }.boxed(),
1087 };
1088
1089 self.process_checkpoint_range();
1090
1091 metrics::gauge!("checkpoint.queued_slots").set(self.queued.len() as f64);
1092
1093 // Because the checkpoint verifier duplicates state from the state
1094 // service (it tracks which checkpoints have been verified), we must
1095 // commit blocks transactionally on a per-checkpoint basis. Otherwise,
1096 // the checkpoint verifier's state could desync from the underlying
1097 // state service. Among other problems, this could cause the checkpoint
1098 // verifier to reject blocks not already in the state as
1099 // already-verified.
1100 //
1101 // # Dropped Receivers
1102 //
1103 // To commit blocks transactionally on a per-checkpoint basis, we must
1104 // commit all verified blocks in a checkpoint range, regardless of
1105 // whether or not the response futures for each block were dropped.
1106 //
1107 // We accomplish this by spawning a new task containing the
1108 // commit-if-verified logic. This task will always execute, except if
1109 // the program is interrupted, in which case there is no longer a
1110 // checkpoint verifier to keep in sync with the state.
1111 //
1112 // # State Commit Failures
1113 //
1114 // If the state commit fails due to corrupt block data,
1115 // we don't reject the entire checkpoint.
1116 // Instead, we reset the verifier to the successfully committed state tip.
1117 let state_service = self.state_service.clone();
1118 let commit_checkpoint_verified = tokio::spawn(async move {
1119 let hash = req_block
1120 .rx
1121 .await
1122 .map_err(Into::into)
1123 .map_err(VerifyCheckpointError::CommitCheckpointVerified)
1124 .expect("CheckpointVerifier does not leave dangling receivers")?;
1125
1126 // We use a `ServiceExt::oneshot`, so that every state service
1127 // `poll_ready` has a corresponding `call`. See #1593.
1128 match state_service
1129 .oneshot(zs::Request::CommitCheckpointVerifiedBlock(req_block.block))
1130 .map_err(VerifyCheckpointError::CommitCheckpointVerified)
1131 .await?
1132 {
1133 zs::Response::Committed(committed_hash) => {
1134 assert_eq!(committed_hash, hash, "state must commit correct hash");
1135 Ok(hash)
1136 }
1137 _ => unreachable!("wrong response for CommitCheckpointVerifiedBlock"),
1138 }
1139 });
1140
1141 let state_service = self.state_service.clone();
1142 let reset_sender = self.reset_sender.clone();
1143 async move {
1144 let result = commit_checkpoint_verified.await;
1145 // Avoid a panic on shutdown
1146 //
1147 // When `zebrad` is terminated using Ctrl-C, the `commit_checkpoint_verified` task
1148 // can return a `JoinError::Cancelled`. We expect task cancellation on shutdown,
1149 // so we don't need to panic here. The persistent state is correct even when the
1150 // task is cancelled, because block data is committed inside transactions, in
1151 // height order.
1152 let result = if zebra_chain::shutdown::is_shutting_down() {
1153 Err(VerifyCheckpointError::ShuttingDown)
1154 } else {
1155 result.expect("commit_checkpoint_verified should not panic")
1156 };
1157 if result.is_err() {
1158 // If there was an error committing the block, then this verifier
1159 // will be out of sync with the state. In that case, reset
1160 // its progress back to the state tip.
1161 let tip = match state_service
1162 .oneshot(zs::Request::Tip)
1163 .await
1164 .map_err(VerifyCheckpointError::Tip)?
1165 {
1166 zs::Response::Tip(tip) => tip,
1167 _ => unreachable!("wrong response for Tip"),
1168 };
1169 // Ignore errors since send() can fail only when the verifier
1170 // is being dropped, and then it doesn't matter anymore.
1171 let _ = reset_sender.send(tip);
1172 }
1173 result
1174 }
1175 .boxed()
1176 }
1177}