zebra_chain/parameters/checkpoint/
list.rs1use 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
24const MAINNET_CHECKPOINTS: &str = include_str!("main-checkpoints.txt");
37
38pub(crate) const TESTNET_CHECKPOINTS: &str = include_str!("test-checkpoints.txt");
47
48impl Network {
49 pub fn genesis_hash(&self) -> block::Hash {
51 match self {
52 Network::Mainnet => "00040fe8ec8471911baa1db1266ea15dd06b4a8a5c453883c000b031973dce08"
54 .parse()
55 .expect("hard-coded hash parses"),
56 Network::Testnet(params) => params.genesis_hash(),
58 }
59 }
60 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 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 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
100fn 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#[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 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 pub fn from_list(
151 list: impl IntoIterator<Item = (block::Height, block::Hash)>,
152 ) -> Result<Self, BoxError> {
153 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 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 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 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 pub fn contains(&self, height: block::Height) -> bool {
197 self.0.contains_key(&height)
198 }
199
200 pub fn hash(&self, height: block::Height) -> Option<block::Hash> {
205 self.0.get(&height).cloned()
206 }
207
208 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 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 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 pub fn iter(&self) -> impl Iterator<Item = (&block::Height, &block::Hash)> {
235 self.0.iter()
236 }
237
238 pub fn iter_cloned(&self) -> impl Iterator<Item = (block::Height, block::Hash)> + '_ {
240 self.iter().map(|(&height, &hash)| (height, hash))
241 }
242
243 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 #[allow(clippy::len_without_is_empty)]
256 pub fn len(&self) -> usize {
257 self.0.len()
258 }
259}