1use snarkvm::prelude::{FromBytes, IoResult, Network, Read, ToBytes, Write, error, has_duplicates};
17
18use anyhow::{Result, bail, ensure};
19use indexmap::{IndexMap, indexmap};
20use serde::{Deserialize, Serialize};
21use std::collections::{BTreeMap, btree_map::IntoIter};
22
23pub const NUM_RECENT_BLOCKS: usize = 100; const RECENT_INTERVAL: u32 = 1; pub const CHECKPOINT_INTERVAL: u32 = 10_000; #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
31pub struct BlockLocators<N: Network> {
32 pub recents: IndexMap<u32, N::BlockHash>,
34 pub checkpoints: IndexMap<u32, N::BlockHash>,
36}
37
38impl<N: Network> BlockLocators<N> {
39 pub fn new(recents: IndexMap<u32, N::BlockHash>, checkpoints: IndexMap<u32, N::BlockHash>) -> Result<Self> {
41 let locators = Self { recents, checkpoints };
43 locators.ensure_is_valid()?;
45 Ok(locators)
47 }
48
49 pub fn new_unchecked(recents: IndexMap<u32, N::BlockHash>, checkpoints: IndexMap<u32, N::BlockHash>) -> Self {
51 Self { recents, checkpoints }
52 }
53
54 pub fn new_genesis(genesis_hash: N::BlockHash) -> Self {
56 Self { recents: indexmap![0 => genesis_hash], checkpoints: indexmap![0 => genesis_hash] }
57 }
58}
59
60impl<N: Network> IntoIterator for BlockLocators<N> {
61 type IntoIter = IntoIter<u32, N::BlockHash>;
62 type Item = (u32, N::BlockHash);
63
64 fn into_iter(self) -> Self::IntoIter {
68 BTreeMap::from_iter(self.checkpoints.into_iter().chain(self.recents)).into_iter()
69 }
70}
71
72impl<N: Network> BlockLocators<N> {
73 pub fn latest_locator_height(&self) -> u32 {
75 self.recents.keys().last().copied().unwrap_or_default()
76 }
77
78 pub fn get_hash(&self, height: u32) -> Option<N::BlockHash> {
80 self.recents.get(&height).copied().or_else(|| self.checkpoints.get(&height).copied())
81 }
82
83 pub fn is_valid(&self) -> bool {
85 if let Err(error) = self.ensure_is_valid() {
87 warn!("Block locators are invalid: {error}");
88 return false;
89 }
90 true
91 }
92
93 pub fn is_consistent_with(&self, other: &Self) -> bool {
96 if let Err(error) = self.ensure_is_consistent_with(other) {
98 warn!("Inconsistent block locators: {error}");
99 return false;
100 }
101 true
102 }
103
104 pub fn ensure_is_valid(&self) -> Result<()> {
106 Self::check_block_locators(&self.recents, &self.checkpoints)
108 }
109
110 pub fn ensure_is_consistent_with(&self, other: &Self) -> Result<()> {
113 Self::check_consistent_block_locators(self, other)
114 }
115}
116
117impl<N: Network> BlockLocators<N> {
118 pub fn check_consistent_block_locators(
121 old_locators: &BlockLocators<N>,
122 new_locators: &BlockLocators<N>,
123 ) -> Result<()> {
124 for (height, hash) in new_locators.recents.iter() {
126 if let Some(recent_hash) = old_locators.recents.get(height) {
127 if recent_hash != hash {
128 bail!("Recent block hash mismatch at height {height}")
129 }
130 }
131 }
132 for (height, hash) in new_locators.checkpoints.iter() {
134 if let Some(checkpoint_hash) = old_locators.checkpoints.get(height) {
135 if checkpoint_hash != hash {
136 bail!("Block checkpoint hash mismatch for height {height}")
137 }
138 }
139 }
140 Ok(())
141 }
142
143 pub fn check_block_locators(
145 recents: &IndexMap<u32, N::BlockHash>,
146 checkpoints: &IndexMap<u32, N::BlockHash>,
147 ) -> Result<()> {
148 let last_recent_height = Self::check_recent_blocks(recents)?;
150 let last_checkpoint_height = Self::check_block_checkpoints(checkpoints)?;
152
153 let threshold = last_checkpoint_height.saturating_sub(NUM_RECENT_BLOCKS as u32);
155 if last_recent_height < threshold {
156 bail!("Recent height ({last_recent_height}) cannot be below checkpoint threshold ({threshold})")
157 }
158
159 if last_recent_height < NUM_RECENT_BLOCKS as u32
161 && recents.get(&0).copied().unwrap_or_default() != checkpoints.get(&0).copied().unwrap_or_default()
162 {
163 bail!("Recent genesis hash and checkpoint genesis hash mismatch at height {last_recent_height}")
164 }
165
166 if let Some(last_checkpoint_hash) = checkpoints.get(&last_recent_height) {
168 if let Some(last_recent_hash) = recents.get(&last_recent_height) {
169 if last_checkpoint_hash != last_recent_hash {
170 bail!("Recent block hash and checkpoint hash mismatch at height {last_recent_height}")
171 }
172 }
173 }
174 Ok(())
175 }
176
177 fn check_recent_blocks(recents: &IndexMap<u32, N::BlockHash>) -> Result<u32> {
186 if recents.is_empty() {
188 bail!("There must be at least 1 recent block")
189 }
190 if recents.len() > NUM_RECENT_BLOCKS {
193 bail!("There can be at most {NUM_RECENT_BLOCKS} blocks in the map")
194 }
195
196 let mut last_height = 0;
198 for (i, current_height) in recents.keys().enumerate() {
199 if i == 0 && recents.len() < NUM_RECENT_BLOCKS && *current_height != last_height {
200 bail!("Ledgers under {NUM_RECENT_BLOCKS} blocks must have the first recent block at height 0")
201 }
202 if i > 0 && *current_height <= last_height {
203 bail!("Recent blocks must increment in height")
204 }
205 if i > 0 && *current_height - last_height != RECENT_INTERVAL {
206 bail!("Recent blocks must increment by {RECENT_INTERVAL}")
207 }
208 last_height = *current_height;
209 }
210
211 if last_height < NUM_RECENT_BLOCKS as u32 && recents.len().saturating_sub(1) as u32 != last_height {
213 bail!("As the last height is below {NUM_RECENT_BLOCKS}, the number of recent blocks must match the height")
214 }
215 if last_height >= NUM_RECENT_BLOCKS as u32 && recents.len() != NUM_RECENT_BLOCKS {
217 bail!("Number of recent blocks must match {NUM_RECENT_BLOCKS}")
218 }
219
220 if has_duplicates(recents.values()) {
222 bail!("Recent block hashes must be unique")
223 }
224
225 Ok(last_height)
226 }
227
228 fn check_block_checkpoints(checkpoints: &IndexMap<u32, N::BlockHash>) -> Result<u32> {
236 ensure!(!checkpoints.is_empty(), "There must be at least 1 block checkpoint");
238
239 let mut last_height = 0;
241 for (i, current_height) in checkpoints.keys().enumerate() {
242 if i == 0 && *current_height != 0 {
243 bail!("First block checkpoint must be at height 0")
244 }
245 if i > 0 && *current_height <= last_height {
246 bail!("Block checkpoints must increment in height")
247 }
248 if i > 0 && *current_height - last_height != CHECKPOINT_INTERVAL {
249 bail!("Block checkpoints must increment by {CHECKPOINT_INTERVAL}")
250 }
251 last_height = *current_height;
252 }
253
254 if has_duplicates(checkpoints.values()) {
256 bail!("Block checkpoints must be unique")
257 }
258
259 Ok(last_height)
260 }
261}
262
263impl<N: Network> FromBytes for BlockLocators<N> {
264 fn read_le<R: Read>(mut reader: R) -> IoResult<Self> {
265 let num_recents = u32::read_le(&mut reader)?;
267 let mut recents = IndexMap::new();
269 for _ in 0..num_recents {
270 let height = u32::read_le(&mut reader)?;
271 let hash = N::BlockHash::read_le(&mut reader)?;
272 recents.insert(height, hash);
273 }
274
275 let num_checkpoints = u32::read_le(&mut reader)?;
277 let mut checkpoints = IndexMap::new();
279 for _ in 0..num_checkpoints {
280 let height = u32::read_le(&mut reader)?;
281 let hash = N::BlockHash::read_le(&mut reader)?;
282 checkpoints.insert(height, hash);
283 }
284
285 Self::new(recents, checkpoints).map_err(error)
286 }
287}
288
289impl<N: Network> ToBytes for BlockLocators<N> {
290 fn write_le<W: Write>(&self, mut writer: W) -> IoResult<()> {
291 u32::try_from(self.recents.len()).map_err(error)?.write_le(&mut writer)?;
293 for (height, hash) in &self.recents {
295 height.write_le(&mut writer)?;
296 hash.write_le(&mut writer)?;
297 }
298
299 u32::try_from(self.checkpoints.len()).map_err(error)?.write_le(&mut writer)?;
301 for (height, hash) in &self.checkpoints {
303 height.write_le(&mut writer)?;
304 hash.write_le(&mut writer)?;
305 }
306 Ok(())
307 }
308}
309
310#[cfg(any(test, feature = "test"))]
311pub mod test_helpers {
312 use super::*;
313 use snarkvm::prelude::Field;
314
315 type CurrentNetwork = snarkvm::prelude::MainnetV0;
316
317 pub fn sample_block_locators(height: u32) -> BlockLocators<CurrentNetwork> {
319 let mut recents = IndexMap::new();
321 let recents_range = match height < NUM_RECENT_BLOCKS as u32 {
322 true => 0..=height,
323 false => (height - NUM_RECENT_BLOCKS as u32 + 1)..=height,
324 };
325 for i in recents_range {
326 recents.insert(i, (Field::<CurrentNetwork>::from_u32(i)).into());
327 }
328
329 let mut checkpoints = IndexMap::new();
331 for i in (0..=height).step_by(CHECKPOINT_INTERVAL as usize) {
332 checkpoints.insert(i, (Field::<CurrentNetwork>::from_u32(i)).into());
333 }
334
335 BlockLocators::new(recents, checkpoints).unwrap()
337 }
338
339 pub fn sample_block_locators_with_fork(height: u32, fork_height: u32) -> BlockLocators<CurrentNetwork> {
341 assert!(fork_height <= height, "Fork height must be less than or equal to the given height");
342 assert!(height - fork_height < NUM_RECENT_BLOCKS as u32, "Fork must be within NUM_RECENTS of the given height");
343
344 let mut recents = IndexMap::new();
346 let recents_range = match height < NUM_RECENT_BLOCKS as u32 {
347 true => 0..=height,
348 false => (height - NUM_RECENT_BLOCKS as u32 + 1)..=height,
349 };
350 for i in recents_range {
351 if i >= fork_height {
352 recents.insert(i, (-Field::<CurrentNetwork>::from_u32(i)).into());
353 } else {
354 recents.insert(i, (Field::<CurrentNetwork>::from_u32(i)).into());
355 }
356 }
357
358 let mut checkpoints = IndexMap::new();
360 for i in (0..=height).step_by(CHECKPOINT_INTERVAL as usize) {
361 checkpoints.insert(i, (Field::<CurrentNetwork>::from_u32(i)).into());
362 }
363
364 BlockLocators::new(recents, checkpoints).unwrap()
366 }
367
368 #[test]
370 fn test_sample_block_locators() {
371 for expected_height in 0..=100_001u32 {
372 println!("Testing height - {expected_height}");
373
374 let expected_num_checkpoints = (expected_height / CHECKPOINT_INTERVAL) + 1;
375 let expected_num_recents = match expected_height < NUM_RECENT_BLOCKS as u32 {
376 true => expected_height + 1,
377 false => NUM_RECENT_BLOCKS as u32,
378 };
379
380 let block_locators = sample_block_locators(expected_height);
381 assert_eq!(block_locators.checkpoints.len(), expected_num_checkpoints as usize);
382 assert_eq!(block_locators.recents.len(), expected_num_recents as usize);
383 assert_eq!(block_locators.latest_locator_height(), expected_height);
384 assert!(block_locators.is_valid());
385 }
386 }
387}
388
389#[cfg(test)]
390mod tests {
391 use super::*;
392 use snarkvm::prelude::Field;
393
394 use core::ops::Range;
395
396 type CurrentNetwork = snarkvm::prelude::MainnetV0;
397
398 fn check_is_valid(checkpoints: IndexMap<u32, <CurrentNetwork as Network>::BlockHash>, heights: Range<u32>) {
400 for height in heights {
401 let mut recents = IndexMap::new();
402 for i in 0..NUM_RECENT_BLOCKS as u32 {
403 recents.insert(height + i, (Field::<CurrentNetwork>::from_u32(height + i)).into());
404
405 let block_locators =
406 BlockLocators::<CurrentNetwork>::new_unchecked(recents.clone(), checkpoints.clone());
407 if height == 0 && recents.len() < NUM_RECENT_BLOCKS {
408 block_locators.ensure_is_valid().unwrap();
410 } else if recents.len() < NUM_RECENT_BLOCKS {
411 block_locators.ensure_is_valid().unwrap_err();
413 } else {
414 block_locators.ensure_is_valid().unwrap();
416 }
417 }
418 recents.insert(
420 height + NUM_RECENT_BLOCKS as u32,
421 (Field::<CurrentNetwork>::from_u32(height + NUM_RECENT_BLOCKS as u32)).into(),
422 );
423 let block_locators = BlockLocators::<CurrentNetwork>::new_unchecked(recents.clone(), checkpoints.clone());
424 block_locators.ensure_is_valid().unwrap_err();
425 }
426 }
427
428 fn check_is_consistent(
430 checkpoints: IndexMap<u32, <CurrentNetwork as Network>::BlockHash>,
431 heights: Range<u32>,
432 genesis_locators: BlockLocators<CurrentNetwork>,
433 second_locators: BlockLocators<CurrentNetwork>,
434 ) {
435 for height in heights {
436 let mut recents = IndexMap::new();
437 for i in 0..NUM_RECENT_BLOCKS as u32 {
438 recents.insert(height + i, (Field::<CurrentNetwork>::from_u32(height + i)).into());
439
440 let block_locators =
441 BlockLocators::<CurrentNetwork>::new_unchecked(recents.clone(), checkpoints.clone());
442 block_locators.ensure_is_consistent_with(&block_locators).unwrap();
443
444 let is_first_num_recents_blocks = height == 0 && recents.len() < NUM_RECENT_BLOCKS;
446 let is_num_recents_blocks = recents.len() == NUM_RECENT_BLOCKS;
447 if is_first_num_recents_blocks || is_num_recents_blocks {
448 genesis_locators.ensure_is_consistent_with(&block_locators).unwrap();
450 block_locators.ensure_is_consistent_with(&genesis_locators).unwrap();
451
452 second_locators.ensure_is_consistent_with(&block_locators).unwrap();
454 block_locators.ensure_is_consistent_with(&second_locators).unwrap();
455 }
456 }
457 }
458 }
459
460 #[test]
461 fn test_ensure_is_valid() {
462 let zero: <CurrentNetwork as Network>::BlockHash = (Field::<CurrentNetwork>::from_u32(0)).into();
463 let checkpoint_1: <CurrentNetwork as Network>::BlockHash =
464 (Field::<CurrentNetwork>::from_u32(CHECKPOINT_INTERVAL)).into();
465
466 for height in 0..10 {
468 let block_locators = test_helpers::sample_block_locators(height);
469 block_locators.ensure_is_valid().unwrap();
470 }
471
472 let checkpoints = IndexMap::from([(0, zero)]);
474 let mut recents = IndexMap::new();
475 for i in 0..NUM_RECENT_BLOCKS {
476 recents.insert(i as u32, (Field::<CurrentNetwork>::from_u32(i as u32)).into());
477 let block_locators = BlockLocators::<CurrentNetwork>::new(recents.clone(), checkpoints.clone()).unwrap();
478 block_locators.ensure_is_valid().unwrap();
479 }
480 recents.insert(NUM_RECENT_BLOCKS as u32, (Field::<CurrentNetwork>::from_u32(NUM_RECENT_BLOCKS as u32)).into());
482 let block_locators = BlockLocators::<CurrentNetwork>::new_unchecked(recents.clone(), checkpoints);
483 block_locators.ensure_is_valid().unwrap_err();
484
485 let checkpoints = IndexMap::from([(0, zero)]);
487 check_is_valid(checkpoints, 0..(CHECKPOINT_INTERVAL - NUM_RECENT_BLOCKS as u32));
488
489 let checkpoints = IndexMap::from([(0, zero), (CHECKPOINT_INTERVAL, checkpoint_1)]);
491 check_is_valid(
492 checkpoints,
493 (CHECKPOINT_INTERVAL - NUM_RECENT_BLOCKS as u32)..(CHECKPOINT_INTERVAL * 2 - NUM_RECENT_BLOCKS as u32),
494 );
495 }
496
497 #[test]
498 fn test_ensure_is_valid_fails() {
499 let zero: <CurrentNetwork as Network>::BlockHash = (Field::<CurrentNetwork>::from_u32(0)).into();
500 let one: <CurrentNetwork as Network>::BlockHash = (Field::<CurrentNetwork>::from_u32(1)).into();
501
502 let block_locators = BlockLocators::<CurrentNetwork>::new_unchecked(Default::default(), Default::default());
504 block_locators.ensure_is_valid().unwrap_err();
505
506 let block_locators =
508 BlockLocators::<CurrentNetwork>::new_unchecked(IndexMap::from([(0, zero)]), IndexMap::from([(0, one)]));
509 block_locators.ensure_is_valid().unwrap_err();
510
511 let block_locators =
513 BlockLocators::<CurrentNetwork>::new_unchecked(IndexMap::from([(0, one)]), IndexMap::from([(0, zero)]));
514 block_locators.ensure_is_valid().unwrap_err();
515
516 let block_locators = BlockLocators::<CurrentNetwork>::new_unchecked(
518 IndexMap::from([(0, one), (1, zero)]),
519 IndexMap::from([(0, zero)]),
520 );
521 block_locators.ensure_is_valid().unwrap_err();
522
523 let block_locators = BlockLocators::<CurrentNetwork>::new_unchecked(
525 IndexMap::from([(0, zero), (1, zero)]),
526 IndexMap::from([(0, zero)]),
527 );
528 block_locators.ensure_is_valid().unwrap_err();
529 }
530
531 #[test]
532 fn test_ensure_is_consistent_with() {
533 let zero: <CurrentNetwork as Network>::BlockHash = (Field::<CurrentNetwork>::from_u32(0)).into();
534 let one: <CurrentNetwork as Network>::BlockHash = (Field::<CurrentNetwork>::from_u32(1)).into();
535
536 let genesis_locators =
537 BlockLocators::<CurrentNetwork>::new_unchecked(IndexMap::from([(0, zero)]), IndexMap::from([(0, zero)]));
538 let second_locators = BlockLocators::<CurrentNetwork>::new_unchecked(
539 IndexMap::from([(0, zero), (1, one)]),
540 IndexMap::from([(0, zero)]),
541 );
542
543 genesis_locators.ensure_is_consistent_with(&genesis_locators).unwrap();
545
546 genesis_locators.ensure_is_consistent_with(&second_locators).unwrap();
548 second_locators.ensure_is_consistent_with(&genesis_locators).unwrap();
549
550 let checkpoints = IndexMap::from([(0, Default::default())]);
552 check_is_consistent(
553 checkpoints,
554 0..(CHECKPOINT_INTERVAL - NUM_RECENT_BLOCKS as u32),
555 genesis_locators.clone(),
556 second_locators.clone(),
557 );
558
559 let checkpoints = IndexMap::from([(0, Default::default()), (CHECKPOINT_INTERVAL, Default::default())]);
561 check_is_consistent(
562 checkpoints,
563 (CHECKPOINT_INTERVAL - NUM_RECENT_BLOCKS as u32)..(CHECKPOINT_INTERVAL * 2 - NUM_RECENT_BLOCKS as u32),
564 genesis_locators,
565 second_locators,
566 );
567 }
568
569 #[test]
570 fn test_ensure_is_consistent_with_fails() {
571 let zero: <CurrentNetwork as Network>::BlockHash = (Field::<CurrentNetwork>::from_u32(0)).into();
572 let one: <CurrentNetwork as Network>::BlockHash = (Field::<CurrentNetwork>::from_u32(1)).into();
573
574 let genesis_locators =
575 BlockLocators::<CurrentNetwork>::new(IndexMap::from([(0, zero)]), IndexMap::from([(0, zero)])).unwrap();
576 let second_locators =
577 BlockLocators::<CurrentNetwork>::new(IndexMap::from([(0, zero), (1, one)]), IndexMap::from([(0, zero)]))
578 .unwrap();
579
580 let wrong_genesis_locators =
581 BlockLocators::<CurrentNetwork>::new(IndexMap::from([(0, one)]), IndexMap::from([(0, one)])).unwrap();
582 let wrong_second_locators =
583 BlockLocators::<CurrentNetwork>::new(IndexMap::from([(0, one), (1, zero)]), IndexMap::from([(0, one)]))
584 .unwrap();
585
586 genesis_locators.ensure_is_consistent_with(&wrong_genesis_locators).unwrap_err();
587 wrong_genesis_locators.ensure_is_consistent_with(&genesis_locators).unwrap_err();
588
589 genesis_locators.ensure_is_consistent_with(&wrong_second_locators).unwrap_err();
590 wrong_second_locators.ensure_is_consistent_with(&genesis_locators).unwrap_err();
591
592 second_locators.ensure_is_consistent_with(&wrong_genesis_locators).unwrap_err();
593 wrong_genesis_locators.ensure_is_consistent_with(&second_locators).unwrap_err();
594
595 second_locators.ensure_is_consistent_with(&wrong_second_locators).unwrap_err();
596 wrong_second_locators.ensure_is_consistent_with(&second_locators).unwrap_err();
597 }
598}