Skip to main content

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