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; const MAX_CHECKPOINTS: usize = (u32::MAX / CHECKPOINT_INTERVAL) as usize;
31
32#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
74pub struct BlockLocators<N: Network> {
75 pub recents: IndexMap<u32, N::BlockHash>,
77 pub checkpoints: IndexMap<u32, N::BlockHash>,
79}
80
81impl<N: Network> BlockLocators<N> {
82 pub fn new(recents: IndexMap<u32, N::BlockHash>, checkpoints: IndexMap<u32, N::BlockHash>) -> Result<Self> {
84 let locators = Self { recents, checkpoints };
86 locators.ensure_is_valid()?;
88 Ok(locators)
90 }
91
92 #[cfg(test)]
95 fn new_unchecked(recents: IndexMap<u32, N::BlockHash>, checkpoints: IndexMap<u32, N::BlockHash>) -> Self {
96 Self { recents, checkpoints }
97 }
98
99 pub fn new_genesis(genesis_hash: N::BlockHash) -> Self {
101 Self { recents: indexmap![0 => genesis_hash], checkpoints: indexmap![0 => genesis_hash] }
102 }
103}
104
105impl<N: Network> IntoIterator for BlockLocators<N> {
106 type IntoIter = IntoIter<u32, N::BlockHash>;
107 type Item = (u32, N::BlockHash);
108
109 fn into_iter(self) -> Self::IntoIter {
113 BTreeMap::from_iter(self.checkpoints.into_iter().chain(self.recents)).into_iter()
114 }
115}
116
117impl<N: Network> BlockLocators<N> {
118 pub fn latest_locator_height(&self) -> u32 {
120 self.recents.keys().last().copied().unwrap_or_default()
121 }
122
123 pub fn get_hash(&self, height: u32) -> Option<N::BlockHash> {
125 self.recents.get(&height).copied().or_else(|| self.checkpoints.get(&height).copied())
126 }
127
128 pub fn is_valid(&self) -> bool {
130 if let Err(error) = self.ensure_is_valid() {
132 warn!("Block locators are invalid: {error}");
133 return false;
134 }
135 true
136 }
137
138 pub fn is_consistent_with(&self, other: &Self) -> bool {
141 if let Err(error) = self.ensure_is_consistent_with(other) {
143 warn!("Inconsistent block locators: {error}");
144 return false;
145 }
146 true
147 }
148
149 pub fn ensure_is_valid(&self) -> Result<()> {
151 Self::check_block_locators(&self.recents, &self.checkpoints)
153 }
154
155 pub fn ensure_is_consistent_with(&self, other: &Self) -> Result<()> {
158 Self::check_consistent_block_locators(self, other)
159 }
160}
161
162impl<N: Network> BlockLocators<N> {
163 pub fn check_consistent_block_locators(
166 old_locators: &BlockLocators<N>,
167 new_locators: &BlockLocators<N>,
168 ) -> Result<()> {
169 for (height, hash) in new_locators.recents.iter() {
171 if let Some(recent_hash) = old_locators.recents.get(height) {
172 if recent_hash != hash {
173 bail!("Recent block hash mismatch at height {height}")
174 }
175 }
176 }
177 for (height, hash) in new_locators.checkpoints.iter() {
179 if let Some(checkpoint_hash) = old_locators.checkpoints.get(height) {
180 if checkpoint_hash != hash {
181 bail!("Block checkpoint hash mismatch for height {height}")
182 }
183 }
184 }
185 Ok(())
186 }
187
188 pub fn check_block_locators(
190 recents: &IndexMap<u32, N::BlockHash>,
191 checkpoints: &IndexMap<u32, N::BlockHash>,
192 ) -> Result<()> {
193 let last_recent_height = Self::check_recent_blocks(recents)?;
195 let last_checkpoint_height = Self::check_block_checkpoints(checkpoints)?;
197
198 if !(last_checkpoint_height..last_checkpoint_height.saturating_add(CHECKPOINT_INTERVAL))
208 .contains(&last_recent_height)
209 {
210 bail!(
211 "Last checkpoint height ({last_checkpoint_height}) is not the largest multiple of \
212 {CHECKPOINT_INTERVAL} that does not exceed the last recent height ({last_recent_height})"
213 )
214 }
215
216 let last_recent_to_last_checkpoint_distance = last_recent_height % CHECKPOINT_INTERVAL;
229 if last_recent_to_last_checkpoint_distance < NUM_RECENT_BLOCKS as u32 {
230 let common = last_recent_height - last_recent_to_last_checkpoint_distance;
231 if recents.get(&common).unwrap() != checkpoints.get(&common).unwrap() {
232 bail!("Recent block hash and checkpoint hash mismatch at height {common}")
233 }
234 }
235
236 Ok(())
237 }
238
239 fn check_recent_blocks(recents: &IndexMap<u32, N::BlockHash>) -> Result<u32> {
248 if recents.is_empty() {
250 bail!("There must be at least 1 recent block")
251 }
252 if recents.len() > NUM_RECENT_BLOCKS {
255 bail!("There can be at most {NUM_RECENT_BLOCKS} blocks in the map")
256 }
257
258 let mut last_height = 0;
260 for (i, current_height) in recents.keys().enumerate() {
261 if i == 0 && recents.len() < NUM_RECENT_BLOCKS && *current_height > 0 {
262 bail!("Ledgers under {NUM_RECENT_BLOCKS} blocks must have the first recent block at height 0")
263 }
264 if i > 0 && *current_height <= last_height {
265 bail!("Recent blocks must increment in height")
266 }
267 if i > 0 && *current_height - last_height != RECENT_INTERVAL {
268 bail!("Recent blocks must increment by {RECENT_INTERVAL}")
269 }
270 last_height = *current_height;
271 }
272
273 if last_height >= NUM_RECENT_BLOCKS as u32 && recents.len() != NUM_RECENT_BLOCKS {
283 bail!("Number of recent blocks must match {NUM_RECENT_BLOCKS}")
284 }
285
286 if has_duplicates(recents.values()) {
288 bail!("Recent block hashes must be unique")
289 }
290
291 Ok(last_height)
292 }
293
294 fn check_block_checkpoints(checkpoints: &IndexMap<u32, N::BlockHash>) -> Result<u32> {
302 ensure!(!checkpoints.is_empty(), "There must be at least 1 block checkpoint");
304
305 let mut last_height = 0;
307 for (i, current_height) in checkpoints.keys().enumerate() {
308 if i == 0 && *current_height != 0 {
309 bail!("First block checkpoint must be at height 0")
310 }
311 if i > 0 && *current_height <= last_height {
312 bail!("Block checkpoints must increment in height")
313 }
314 if i > 0 && *current_height - last_height != CHECKPOINT_INTERVAL {
315 bail!("Block checkpoints must increment by {CHECKPOINT_INTERVAL}")
316 }
317 last_height = *current_height;
318 }
319
320 if has_duplicates(checkpoints.values()) {
322 bail!("Block checkpoints must be unique")
323 }
324
325 Ok(last_height)
326 }
327}
328
329impl<N: Network> FromBytes for BlockLocators<N> {
330 fn read_le<R: Read>(mut reader: R) -> IoResult<Self> {
331 let num_recents = u32::read_le(&mut reader)?;
333 if num_recents as usize > NUM_RECENT_BLOCKS {
335 return Err(error(format!(
336 "Number of recent blocks ({num_recents}) is greater than the maximum ({NUM_RECENT_BLOCKS})"
337 )));
338 }
339 let mut recents = IndexMap::with_capacity(num_recents as usize);
341 for _ in 0..num_recents {
342 let height = u32::read_le(&mut reader)?;
343 let hash = N::BlockHash::read_le(&mut reader)?;
344 recents.insert(height, hash);
345 }
346
347 let num_checkpoints = u32::read_le(&mut reader)?;
349 if num_checkpoints as usize > MAX_CHECKPOINTS {
351 return Err(error(format!(
352 "Number of checkpoints ({num_checkpoints}) is greater than the maximum ({MAX_CHECKPOINTS})"
353 )));
354 }
355 let mut checkpoints = IndexMap::new();
357 for _ in 0..num_checkpoints {
358 let height = u32::read_le(&mut reader)?;
359 let hash = N::BlockHash::read_le(&mut reader)?;
360 checkpoints.insert(height, hash);
361 }
362
363 Self::new(recents, checkpoints).map_err(error)
364 }
365}
366
367impl<N: Network> ToBytes for BlockLocators<N> {
368 fn write_le<W: Write>(&self, mut writer: W) -> IoResult<()> {
369 u32::try_from(self.recents.len()).map_err(error)?.write_le(&mut writer)?;
371 for (height, hash) in &self.recents {
373 height.write_le(&mut writer)?;
374 hash.write_le(&mut writer)?;
375 }
376
377 u32::try_from(self.checkpoints.len()).map_err(error)?.write_le(&mut writer)?;
379 for (height, hash) in &self.checkpoints {
381 height.write_le(&mut writer)?;
382 hash.write_le(&mut writer)?;
383 }
384 Ok(())
385 }
386}
387
388#[cfg(any(test, feature = "test"))]
389pub mod test_helpers {
390 use super::*;
391 use snarkvm::prelude::Field;
392
393 type CurrentNetwork = snarkvm::prelude::MainnetV0;
394
395 pub fn sample_block_locators(height: u32) -> BlockLocators<CurrentNetwork> {
399 let mut recents = IndexMap::new();
401 let recents_range = match height < NUM_RECENT_BLOCKS as u32 {
402 true => 0..=height,
403 false => (height - NUM_RECENT_BLOCKS as u32 + 1)..=height,
404 };
405 for i in recents_range {
406 recents.insert(i, (Field::<CurrentNetwork>::from_u32(i)).into());
407 }
408
409 let mut checkpoints = IndexMap::new();
411 for i in (0..=height).step_by(CHECKPOINT_INTERVAL as usize) {
412 checkpoints.insert(i, (Field::<CurrentNetwork>::from_u32(i)).into());
413 }
414
415 BlockLocators::new(recents, checkpoints).unwrap()
417 }
418
419 pub fn sample_block_locators_with_fork(height: u32, fork_height: u32) -> BlockLocators<CurrentNetwork> {
423 assert!(fork_height <= height, "Fork height must be less than or equal to the given height");
424 assert!(
425 height - fork_height < NUM_RECENT_BLOCKS as u32,
426 "Fork must be within NUM_RECENT_BLOCKS of the given height"
427 );
428
429 let mut recents = IndexMap::new();
431 let recents_range = match height < NUM_RECENT_BLOCKS as u32 {
432 true => 0..=height,
433 false => (height - NUM_RECENT_BLOCKS as u32 + 1)..=height,
434 };
435 for i in recents_range {
436 if i >= fork_height {
437 recents.insert(i, (-Field::<CurrentNetwork>::from_u32(i)).into());
438 } else {
439 recents.insert(i, (Field::<CurrentNetwork>::from_u32(i)).into());
440 }
441 }
442
443 let mut checkpoints = IndexMap::new();
445 for i in (0..=height).step_by(CHECKPOINT_INTERVAL as usize) {
446 checkpoints.insert(i, (Field::<CurrentNetwork>::from_u32(i)).into());
447 }
448
449 BlockLocators::new(recents, checkpoints).unwrap()
451 }
452
453 #[test]
455 fn test_sample_block_locators() {
456 for expected_height in 0..=100_001u32 {
457 println!("Testing height - {expected_height}");
458
459 let expected_num_checkpoints = (expected_height / CHECKPOINT_INTERVAL) + 1;
460 let expected_num_recents = match expected_height < NUM_RECENT_BLOCKS as u32 {
461 true => expected_height + 1,
462 false => NUM_RECENT_BLOCKS as u32,
463 };
464
465 let block_locators = sample_block_locators(expected_height);
466 assert_eq!(block_locators.checkpoints.len(), expected_num_checkpoints as usize);
467 assert_eq!(block_locators.recents.len(), expected_num_recents as usize);
468 assert_eq!(block_locators.latest_locator_height(), expected_height);
469 }
472 }
473}
474
475#[cfg(test)]
476mod tests {
477 use super::*;
478 use snarkvm::prelude::Field;
479
480 use core::ops::Range;
481
482 type CurrentNetwork = snarkvm::prelude::MainnetV0;
483
484 fn check_is_valid(checkpoints: IndexMap<u32, <CurrentNetwork as Network>::BlockHash>, heights: Range<u32>) {
486 for height in heights {
487 let mut recents = IndexMap::new();
488 for i in 0..NUM_RECENT_BLOCKS as u32 {
489 recents.insert(height + i, (Field::<CurrentNetwork>::from_u32(height + i)).into());
490
491 let block_locators =
492 BlockLocators::<CurrentNetwork>::new_unchecked(recents.clone(), checkpoints.clone());
493 if height == 0 && recents.len() < NUM_RECENT_BLOCKS {
494 block_locators.ensure_is_valid().unwrap();
496 } else if recents.len() < NUM_RECENT_BLOCKS {
497 block_locators.ensure_is_valid().unwrap_err();
499 } else {
500 block_locators.ensure_is_valid().unwrap();
502 }
503 }
504 recents.insert(
506 height + NUM_RECENT_BLOCKS as u32,
507 (Field::<CurrentNetwork>::from_u32(height + NUM_RECENT_BLOCKS as u32)).into(),
508 );
509 let block_locators = BlockLocators::<CurrentNetwork>::new_unchecked(recents.clone(), checkpoints.clone());
510 block_locators.ensure_is_valid().unwrap_err();
511 }
512 }
513
514 fn check_is_consistent(
516 checkpoints: IndexMap<u32, <CurrentNetwork as Network>::BlockHash>,
517 heights: Range<u32>,
518 genesis_locators: BlockLocators<CurrentNetwork>,
519 second_locators: BlockLocators<CurrentNetwork>,
520 ) {
521 for height in heights {
522 let mut recents = IndexMap::new();
523 for i in 0..NUM_RECENT_BLOCKS as u32 {
524 recents.insert(height + i, (Field::<CurrentNetwork>::from_u32(height + i)).into());
525
526 let block_locators =
527 BlockLocators::<CurrentNetwork>::new_unchecked(recents.clone(), checkpoints.clone());
528 block_locators.ensure_is_consistent_with(&block_locators).unwrap();
529
530 let is_first_num_recents_blocks = height == 0 && recents.len() < NUM_RECENT_BLOCKS;
532 let is_num_recents_blocks = recents.len() == NUM_RECENT_BLOCKS;
533 if is_first_num_recents_blocks || is_num_recents_blocks {
534 genesis_locators.ensure_is_consistent_with(&block_locators).unwrap();
536 block_locators.ensure_is_consistent_with(&genesis_locators).unwrap();
537
538 second_locators.ensure_is_consistent_with(&block_locators).unwrap();
540 block_locators.ensure_is_consistent_with(&second_locators).unwrap();
541 }
542 }
543 }
544 }
545
546 #[test]
547 fn test_ensure_is_valid() {
548 let zero: <CurrentNetwork as Network>::BlockHash = (Field::<CurrentNetwork>::from_u32(0)).into();
549 let checkpoint_1: <CurrentNetwork as Network>::BlockHash =
550 (Field::<CurrentNetwork>::from_u32(CHECKPOINT_INTERVAL)).into();
551
552 for height in 0..10 {
554 let block_locators = test_helpers::sample_block_locators(height);
555 block_locators.ensure_is_valid().unwrap();
556 }
557
558 let checkpoints = IndexMap::from([(0, zero)]);
560 let mut recents = IndexMap::new();
561 for i in 0..NUM_RECENT_BLOCKS {
562 recents.insert(i as u32, (Field::<CurrentNetwork>::from_u32(i as u32)).into());
563 let block_locators = BlockLocators::<CurrentNetwork>::new_unchecked(recents.clone(), checkpoints.clone());
564 block_locators.ensure_is_valid().unwrap();
565 }
566 recents.insert(NUM_RECENT_BLOCKS as u32, (Field::<CurrentNetwork>::from_u32(NUM_RECENT_BLOCKS as u32)).into());
568 let block_locators = BlockLocators::<CurrentNetwork>::new_unchecked(recents.clone(), checkpoints);
569 block_locators.ensure_is_valid().unwrap_err();
570
571 let checkpoints = IndexMap::from([(0, zero)]);
573 check_is_valid(checkpoints, 0..(CHECKPOINT_INTERVAL - NUM_RECENT_BLOCKS as u32));
574
575 let checkpoints = IndexMap::from([(0, zero), (CHECKPOINT_INTERVAL, checkpoint_1)]);
577 check_is_valid(
578 checkpoints,
579 (CHECKPOINT_INTERVAL - NUM_RECENT_BLOCKS as u32 + 1)..(CHECKPOINT_INTERVAL * 2 - NUM_RECENT_BLOCKS as u32),
580 );
581 }
582
583 #[test]
584 fn test_ensure_is_valid_fails() {
585 let zero: <CurrentNetwork as Network>::BlockHash = (Field::<CurrentNetwork>::from_u32(0)).into();
586 let one: <CurrentNetwork as Network>::BlockHash = (Field::<CurrentNetwork>::from_u32(1)).into();
587
588 let block_locators = BlockLocators::<CurrentNetwork>::new_unchecked(Default::default(), Default::default());
590 block_locators.ensure_is_valid().unwrap_err();
591
592 let block_locators =
594 BlockLocators::<CurrentNetwork>::new_unchecked(IndexMap::from([(0, zero)]), IndexMap::from([(0, one)]));
595 block_locators.ensure_is_valid().unwrap_err();
596
597 let block_locators =
599 BlockLocators::<CurrentNetwork>::new_unchecked(IndexMap::from([(0, one)]), IndexMap::from([(0, zero)]));
600 block_locators.ensure_is_valid().unwrap_err();
601
602 let block_locators = BlockLocators::<CurrentNetwork>::new_unchecked(
604 IndexMap::from([(0, one), (1, zero)]),
605 IndexMap::from([(0, zero)]),
606 );
607 block_locators.ensure_is_valid().unwrap_err();
608
609 let block_locators = BlockLocators::<CurrentNetwork>::new_unchecked(
611 IndexMap::from([(0, zero), (1, zero)]),
612 IndexMap::from([(0, zero)]),
613 );
614 block_locators.ensure_is_valid().unwrap_err();
615
616 let mut recents = IndexMap::new();
618 for i in 0..NUM_RECENT_BLOCKS {
619 recents.insert(10_000 + i as u32, (Field::<CurrentNetwork>::from_u32(i as u32)).into());
620 }
621 let block_locators = BlockLocators::<CurrentNetwork>::new_unchecked(recents, IndexMap::from([(0, zero)]));
622 block_locators.ensure_is_valid().unwrap_err();
623 }
624
625 #[test]
626 fn test_ensure_is_consistent_with() {
627 let zero: <CurrentNetwork as Network>::BlockHash = (Field::<CurrentNetwork>::from_u32(0)).into();
628 let one: <CurrentNetwork as Network>::BlockHash = (Field::<CurrentNetwork>::from_u32(1)).into();
629
630 let genesis_locators =
631 BlockLocators::<CurrentNetwork>::new_unchecked(IndexMap::from([(0, zero)]), IndexMap::from([(0, zero)]));
632 let second_locators = BlockLocators::<CurrentNetwork>::new_unchecked(
633 IndexMap::from([(0, zero), (1, one)]),
634 IndexMap::from([(0, zero)]),
635 );
636
637 genesis_locators.ensure_is_consistent_with(&genesis_locators).unwrap();
639
640 genesis_locators.ensure_is_consistent_with(&second_locators).unwrap();
642 second_locators.ensure_is_consistent_with(&genesis_locators).unwrap();
643
644 let checkpoints = IndexMap::from([(0, Default::default())]);
646 check_is_consistent(
647 checkpoints,
648 0..(CHECKPOINT_INTERVAL - NUM_RECENT_BLOCKS as u32),
649 genesis_locators.clone(),
650 second_locators.clone(),
651 );
652
653 let checkpoints = IndexMap::from([(0, Default::default()), (CHECKPOINT_INTERVAL, Default::default())]);
655 check_is_consistent(
656 checkpoints,
657 (CHECKPOINT_INTERVAL - NUM_RECENT_BLOCKS as u32)..(CHECKPOINT_INTERVAL * 2 - NUM_RECENT_BLOCKS as u32),
658 genesis_locators,
659 second_locators,
660 );
661 }
662
663 #[test]
664 fn test_ensure_is_consistent_with_fails() {
665 let zero: <CurrentNetwork as Network>::BlockHash = (Field::<CurrentNetwork>::from_u32(0)).into();
666 let one: <CurrentNetwork as Network>::BlockHash = (Field::<CurrentNetwork>::from_u32(1)).into();
667
668 let genesis_locators =
669 BlockLocators::<CurrentNetwork>::new(IndexMap::from([(0, zero)]), IndexMap::from([(0, zero)])).unwrap();
670 let second_locators =
671 BlockLocators::<CurrentNetwork>::new(IndexMap::from([(0, zero), (1, one)]), IndexMap::from([(0, zero)]))
672 .unwrap();
673
674 let wrong_genesis_locators =
675 BlockLocators::<CurrentNetwork>::new(IndexMap::from([(0, one)]), IndexMap::from([(0, one)])).unwrap();
676 let wrong_second_locators =
677 BlockLocators::<CurrentNetwork>::new(IndexMap::from([(0, one), (1, zero)]), IndexMap::from([(0, one)]))
678 .unwrap();
679
680 genesis_locators.ensure_is_consistent_with(&wrong_genesis_locators).unwrap_err();
681 wrong_genesis_locators.ensure_is_consistent_with(&genesis_locators).unwrap_err();
682
683 genesis_locators.ensure_is_consistent_with(&wrong_second_locators).unwrap_err();
684 wrong_second_locators.ensure_is_consistent_with(&genesis_locators).unwrap_err();
685
686 second_locators.ensure_is_consistent_with(&wrong_genesis_locators).unwrap_err();
687 wrong_genesis_locators.ensure_is_consistent_with(&second_locators).unwrap_err();
688
689 second_locators.ensure_is_consistent_with(&wrong_second_locators).unwrap_err();
690 wrong_second_locators.ensure_is_consistent_with(&second_locators).unwrap_err();
691 }
692}