Skip to main content

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}