zebra_chain/parameters/checkpoint/
list.rs

1//! Checkpoint lists for checkpoint-based block verification
2//!
3//! Each checkpoint consists of a coinbase height and block header hash.
4//!
5//! Checkpoints can be used to verify their ancestors, by chaining backwards
6//! to another checkpoint, via each block's parent block hash.
7
8use std::{
9    collections::{BTreeMap, HashSet},
10    ops::RangeBounds,
11    str::FromStr,
12    sync::Arc,
13};
14
15use crate::{
16    block::{self, Height},
17    parameters::{Network, NetworkUpgrade},
18    BoxError,
19};
20
21#[cfg(test)]
22mod tests;
23
24/// The hard-coded checkpoints for mainnet, generated using the
25/// `zebra-checkpoints` tool.
26///
27/// To regenerate the latest checkpoints, use the following commands:
28/// ```sh
29/// LAST_CHECKPOINT=$(tail -1 main-checkpoints.txt | cut -d' ' -f1)
30/// echo "$LAST_CHECKPOINT"
31/// zebra-checkpoints --cli /path/to/zcash-cli --last-checkpoint "$LAST_CHECKPOINT" >> main-checkpoints.txt &
32/// tail -f main-checkpoints.txt
33/// ```
34///
35/// See the checkpoints [./README.md] for more details.
36const MAINNET_CHECKPOINTS: &str = include_str!("main-checkpoints.txt");
37
38/// The hard-coded checkpoints for testnet, generated using the
39/// `zebra-checkpoints` tool.
40///
41/// To use testnet, use the testnet checkpoints file, and run
42/// `zebra-checkpoints [other args] -- -testnet`.
43///
44/// See [`MAINNET_CHECKPOINTS`] for detailed `zebra-checkpoints` usage
45/// information.
46pub(crate) const TESTNET_CHECKPOINTS: &str = include_str!("test-checkpoints.txt");
47
48impl Network {
49    /// Returns the hash for the genesis block in `network`.
50    pub fn genesis_hash(&self) -> block::Hash {
51        match self {
52            // zcash-cli getblockhash 0
53            Network::Mainnet => "00040fe8ec8471911baa1db1266ea15dd06b4a8a5c453883c000b031973dce08"
54                .parse()
55                .expect("hard-coded hash parses"),
56            // See `zebra_chain::parameters::network::testnet` for more details.
57            Network::Testnet(params) => params.genesis_hash(),
58        }
59    }
60    /// Returns the hard-coded checkpoint list for `network`.
61    pub fn checkpoint_list(&self) -> Arc<CheckpointList> {
62        let checkpoints_for_network = match self {
63            Network::Mainnet => MAINNET_CHECKPOINTS,
64            Network::Testnet(params) => return params.checkpoints(),
65        };
66
67        // Check that the list starts with the correct genesis block and parses checkpoint list.
68        let first_checkpoint_height = checkpoints_for_network
69            .lines()
70            .next()
71            .map(checkpoint_height_and_hash);
72
73        let checkpoints = match first_checkpoint_height {
74            // parse calls CheckpointList::from_list
75            Some(Ok((block::Height(0), hash))) if hash == self.genesis_hash() => {
76                checkpoints_for_network
77                    .parse()
78                    .expect("hard-coded checkpoint list parses and validates")
79            }
80            Some(Ok((block::Height(0), _))) => {
81                panic!("the genesis checkpoint does not match the {self} genesis hash")
82            }
83            Some(Ok(_)) => panic!("checkpoints must start at the genesis block height 0"),
84            Some(Err(err)) => panic!("{err}"),
85
86            None if NetworkUpgrade::Canopy.activation_height(self) == Some(Height(1)) => {
87                CheckpointList::from_list([(block::Height(0), self.genesis_hash())])
88                    .expect("hard-coded checkpoint list parses and validates")
89            }
90            None => panic!(
91                "Zebra requires checkpoints on networks which do not activate \
92                 the Canopy network upgrade at block height 1"
93            ),
94        };
95
96        Arc::new(checkpoints)
97    }
98}
99
100/// Parses a checkpoint to a [`block::Height`] and [`block::Hash`].
101fn checkpoint_height_and_hash(checkpoint: &str) -> Result<(block::Height, block::Hash), BoxError> {
102    let fields = checkpoint.split(' ').collect::<Vec<_>>();
103    if let [height, hash] = fields[..] {
104        Ok((height.parse()?, hash.parse()?))
105    } else {
106        Err(format!("Invalid checkpoint format: expected 2 space-separated fields but found {}: '{checkpoint}'", fields.len()).into())
107    }
108}
109
110/// A list of block height and hash checkpoints.
111///
112/// Checkpoints should be chosen to avoid forks or chain reorganizations,
113/// which only happen in the last few hundred blocks in the chain.
114/// (zcashd allows chain reorganizations up to 99 blocks, and prunes
115/// orphaned side-chains after 288 blocks.)
116///
117/// This is actually a bijective map, but since it is read-only, we use a
118/// BTreeMap, and do the value uniqueness check on initialisation.
119#[derive(Clone, Debug, Eq, Hash, PartialEq)]
120pub struct CheckpointList(BTreeMap<block::Height, block::Hash>);
121
122impl FromStr for CheckpointList {
123    type Err = BoxError;
124
125    /// Parse a string into a CheckpointList.
126    ///
127    /// Each line has one checkpoint, consisting of a `block::Height` and
128    /// `block::Hash`, separated by a single space.
129    ///
130    /// Assumes that the provided genesis checkpoint is correct.
131    fn from_str(s: &str) -> Result<Self, Self::Err> {
132        let mut checkpoint_list: Vec<(block::Height, block::Hash)> = Vec::new();
133
134        for checkpoint in s.lines() {
135            checkpoint_list.push(checkpoint_height_and_hash(checkpoint)?);
136        }
137
138        CheckpointList::from_list(checkpoint_list)
139    }
140}
141
142impl CheckpointList {
143    /// Create a new checkpoint list for `network` from `checkpoint_list`.
144    ///
145    /// Assumes that the provided genesis checkpoint is correct.
146    ///
147    /// Checkpoint heights and checkpoint hashes must be unique.
148    /// There must be a checkpoint for a genesis block at block::Height 0.
149    /// (All other checkpoints are optional.)
150    pub fn from_list(
151        list: impl IntoIterator<Item = (block::Height, block::Hash)>,
152    ) -> Result<Self, BoxError> {
153        // BTreeMap silently ignores duplicates, so we count the checkpoints
154        // before adding them to the map
155        let original_checkpoints: Vec<(block::Height, block::Hash)> = list.into_iter().collect();
156        let original_len = original_checkpoints.len();
157
158        let checkpoints: BTreeMap<block::Height, block::Hash> =
159            original_checkpoints.into_iter().collect();
160
161        // Check that the list starts with _some_ genesis block
162        match checkpoints.iter().next() {
163            Some((block::Height(0), _hash)) => {}
164            Some(_) => Err("checkpoints must start at the genesis block height 0")?,
165            None => Err("there must be at least one checkpoint, for the genesis block")?,
166        };
167
168        // This check rejects duplicate heights, whether they have the same or
169        // different hashes
170        if checkpoints.len() != original_len {
171            Err("checkpoint heights must be unique")?;
172        }
173
174        let block_hashes: HashSet<&block::Hash> = checkpoints.values().collect();
175        if block_hashes.len() != original_len {
176            Err("checkpoint hashes must be unique")?;
177        }
178
179        // Make sure all the hashes are valid. In Bitcoin, [0; 32] is the null
180        // hash. It is also used as the parent hash of genesis blocks.
181        if block_hashes.contains(&block::Hash([0; 32])) {
182            Err("checkpoint list contains invalid checkpoint hash: found null hash")?;
183        }
184
185        let checkpoints = CheckpointList(checkpoints);
186        if checkpoints.max_height() > block::Height::MAX {
187            Err("checkpoint list contains invalid checkpoint: checkpoint height is greater than the maximum block height")?;
188        }
189
190        Ok(checkpoints)
191    }
192
193    /// Return true if there is a checkpoint at `height`.
194    ///
195    /// See `BTreeMap::contains_key()` for details.
196    pub fn contains(&self, height: block::Height) -> bool {
197        self.0.contains_key(&height)
198    }
199
200    /// Returns the hash corresponding to the checkpoint at `height`,
201    /// or None if there is no checkpoint at that height.
202    ///
203    /// See `BTreeMap::get()` for details.
204    pub fn hash(&self, height: block::Height) -> Option<block::Hash> {
205        self.0.get(&height).cloned()
206    }
207
208    /// Return the block height of the highest checkpoint in the checkpoint list.
209    ///
210    /// If there is only a single checkpoint, then the maximum height will be
211    /// zero. (The genesis block.)
212    pub fn max_height(&self) -> block::Height {
213        self.max_height_in_range(..)
214            .expect("checkpoint lists must have at least one checkpoint")
215    }
216
217    /// Return the block height of the lowest checkpoint in a sub-range.
218    pub fn min_height_in_range<R>(&self, range: R) -> Option<block::Height>
219    where
220        R: RangeBounds<block::Height>,
221    {
222        self.0.range(range).map(|(height, _)| *height).next()
223    }
224
225    /// Return the block height of the highest checkpoint in a sub-range.
226    pub fn max_height_in_range<R>(&self, range: R) -> Option<block::Height>
227    where
228        R: RangeBounds<block::Height>,
229    {
230        self.0.range(range).map(|(height, _)| *height).next_back()
231    }
232
233    /// Returns an iterator over all the checkpoints, in increasing height order.
234    pub fn iter(&self) -> impl Iterator<Item = (&block::Height, &block::Hash)> {
235        self.0.iter()
236    }
237
238    /// Returns an iterator over all the checkpoints, in increasing height order.
239    pub fn iter_cloned(&self) -> impl Iterator<Item = (block::Height, block::Hash)> + '_ {
240        self.iter().map(|(&height, &hash)| (height, hash))
241    }
242
243    /// Returns the checkpoint at `height`, as a zero-based index.
244    /// If `height` is not a checkpoint height, returns the checkpoint immediately before that height.
245    pub fn prev_checkpoint_index(&self, height: block::Height) -> usize {
246        self.0
247            .keys()
248            .rposition(|&key| key <= height)
249            .expect("checkpoints must start at the genesis block height 0")
250    }
251
252    /// Returns the number of checkpoints in the list.
253    //
254    // Checkpoint lists are never empty by construction.
255    #[allow(clippy::len_without_is_empty)]
256    pub fn len(&self) -> usize {
257        self.0.len()
258    }
259}