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; pub const MAX_CHECKPOINTS: usize = (u32::MAX / CHECKPOINT_INTERVAL) as usize;
35
36#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
78pub struct BlockLocators<N: Network> {
79 pub recents: IndexMap<u32, N::BlockHash>,
81 pub checkpoints: IndexMap<u32, N::BlockHash>,
83}
84
85impl<N: Network> BlockLocators<N> {
86 pub fn new(recents: IndexMap<u32, N::BlockHash>, checkpoints: IndexMap<u32, N::BlockHash>) -> Result<Self> {
88 let locators = Self { recents, checkpoints };
90 locators.ensure_is_valid()?;
92 Ok(locators)
94 }
95
96 #[cfg(test)]
99 fn new_unchecked(recents: IndexMap<u32, N::BlockHash>, checkpoints: IndexMap<u32, N::BlockHash>) -> Self {
100 Self { recents, checkpoints }
101 }
102
103 pub fn new_genesis(genesis_hash: N::BlockHash) -> Self {
105 Self { recents: indexmap![0 => genesis_hash], checkpoints: indexmap![0 => genesis_hash] }
106 }
107}
108
109impl<N: Network> IntoIterator for BlockLocators<N> {
110 type IntoIter = IntoIter<u32, N::BlockHash>;
111 type Item = (u32, N::BlockHash);
112
113 fn into_iter(self) -> Self::IntoIter {
117 BTreeMap::from_iter(self.checkpoints.into_iter().chain(self.recents)).into_iter()
118 }
119}
120
121impl<N: Network> BlockLocators<N> {
122 pub fn latest_locator_height(&self) -> u32 {
124 self.recents.keys().last().copied().unwrap_or_default()
125 }
126
127 pub fn get_hash(&self, height: u32) -> Option<N::BlockHash> {
129 self.recents.get(&height).copied().or_else(|| self.checkpoints.get(&height).copied())
130 }
131
132 pub fn is_valid(&self) -> bool {
134 if let Err(error) = self.ensure_is_valid() {
136 warn!("Block locators are invalid: {error}");
137 return false;
138 }
139 true
140 }
141
142 pub fn is_consistent_with(&self, other: &Self) -> bool {
145 if let Err(error) = self.ensure_is_consistent_with(other) {
147 warn!("Inconsistent block locators: {error}");
148 return false;
149 }
150 true
151 }
152
153 pub fn ensure_is_valid(&self) -> Result<()> {
155 Self::check_block_locators(&self.recents, &self.checkpoints)
157 }
158
159 pub fn ensure_is_consistent_with(&self, other: &Self) -> Result<()> {
162 Self::check_consistent_block_locators(self, other)
163 }
164}
165
166impl<N: Network> BlockLocators<N> {
167 pub fn check_consistent_block_locators(
170 old_locators: &BlockLocators<N>,
171 new_locators: &BlockLocators<N>,
172 ) -> Result<()> {
173 for (height, hash) in new_locators.recents.iter() {
175 if let Some(recent_hash) = old_locators.recents.get(height)
176 && recent_hash != hash
177 {
178 bail!("Recent block hash mismatch at height {height}")
179 }
180 }
181 for (height, hash) in new_locators.checkpoints.iter() {
183 if let Some(checkpoint_hash) = old_locators.checkpoints.get(height)
184 && checkpoint_hash != hash
185 {
186 bail!("Block checkpoint hash mismatch for height {height}")
187 }
188 }
189 Ok(())
190 }
191
192 pub fn check_block_locators(
194 recents: &IndexMap<u32, N::BlockHash>,
195 checkpoints: &IndexMap<u32, N::BlockHash>,
196 ) -> Result<()> {
197 let last_recent_height = Self::check_recent_blocks(recents)?;
199 let last_checkpoint_height = Self::check_block_checkpoints(checkpoints)?;
201
202 if !(last_checkpoint_height..last_checkpoint_height.saturating_add(CHECKPOINT_INTERVAL))
212 .contains(&last_recent_height)
213 {
214 bail!(
215 "Last checkpoint height ({last_checkpoint_height}) is not the largest multiple of \
216 {CHECKPOINT_INTERVAL} that does not exceed the last recent height ({last_recent_height})"
217 )
218 }
219
220 let last_recent_to_last_checkpoint_distance = last_recent_height % CHECKPOINT_INTERVAL;
233 if last_recent_to_last_checkpoint_distance < NUM_RECENT_BLOCKS as u32 {
234 let common = last_recent_height - last_recent_to_last_checkpoint_distance;
235 if recents.get(&common).unwrap() != checkpoints.get(&common).unwrap() {
236 bail!("Recent block hash and checkpoint hash mismatch at height {common}")
237 }
238 }
239
240 Ok(())
241 }
242
243 fn check_recent_blocks(recents: &IndexMap<u32, N::BlockHash>) -> Result<u32> {
252 if recents.is_empty() {
254 bail!("There must be at least 1 recent block")
255 }
256 if recents.len() > NUM_RECENT_BLOCKS {
259 bail!("There can be at most {NUM_RECENT_BLOCKS} blocks in the map")
260 }
261
262 let mut last_height = 0;
264 for (i, current_height) in recents.keys().enumerate() {
265 if i == 0 && recents.len() < NUM_RECENT_BLOCKS && *current_height > 0 {
266 bail!("Ledgers under {NUM_RECENT_BLOCKS} blocks must have the first recent block at height 0")
267 }
268 if i > 0 && *current_height <= last_height {
269 bail!("Recent blocks must increment in height")
270 }
271 if i > 0 && *current_height - last_height != RECENT_INTERVAL {
272 bail!("Recent blocks must increment by {RECENT_INTERVAL}")
273 }
274 last_height = *current_height;
275 }
276
277 if last_height >= NUM_RECENT_BLOCKS as u32 && recents.len() != NUM_RECENT_BLOCKS {
287 bail!("Number of recent blocks must match {NUM_RECENT_BLOCKS}")
288 }
289
290 if has_duplicates(recents.values()) {
292 bail!("Recent block hashes must be unique")
293 }
294
295 Ok(last_height)
296 }
297
298 fn check_block_checkpoints(checkpoints: &IndexMap<u32, N::BlockHash>) -> Result<u32> {
306 ensure!(!checkpoints.is_empty(), "There must be at least 1 block checkpoint");
308
309 let mut last_height = 0;
311 for (i, current_height) in checkpoints.keys().enumerate() {
312 if i == 0 && *current_height != 0 {
313 bail!("First block checkpoint must be at height 0")
314 }
315 if i > 0 && *current_height <= last_height {
316 bail!("Block checkpoints must increment in height")
317 }
318 if i > 0 && *current_height - last_height != CHECKPOINT_INTERVAL {
319 bail!("Block checkpoints must increment by {CHECKPOINT_INTERVAL}")
320 }
321 last_height = *current_height;
322 }
323
324 if has_duplicates(checkpoints.values()) {
326 bail!("Block checkpoints must be unique")
327 }
328
329 Ok(last_height)
330 }
331}
332
333impl<N: Network> FromBytes for BlockLocators<N> {
334 fn read_le<R: Read>(mut reader: R) -> IoResult<Self> {
335 let num_recents = u32::read_le(&mut reader)?;
337 if num_recents as usize > NUM_RECENT_BLOCKS {
339 return Err(error(format!(
340 "Number of recent blocks ({num_recents}) is greater than the maximum ({NUM_RECENT_BLOCKS})"
341 )));
342 }
343 let mut recents = IndexMap::with_capacity(num_recents as usize);
345 for _ in 0..num_recents {
346 let height = u32::read_le(&mut reader)?;
347 let hash = N::BlockHash::read_le(&mut reader)?;
348 recents.insert(height, hash);
349 }
350
351 let num_checkpoints = u32::read_le(&mut reader)?;
353 if num_checkpoints as usize > MAX_CHECKPOINTS {
355 return Err(error(format!(
356 "Number of checkpoints ({num_checkpoints}) is greater than the maximum ({MAX_CHECKPOINTS})"
357 )));
358 }
359 let mut checkpoints = IndexMap::new();
361 for _ in 0..num_checkpoints {
362 let height = u32::read_le(&mut reader)?;
363 let hash = N::BlockHash::read_le(&mut reader)?;
364 checkpoints.insert(height, hash);
365 }
366
367 Self::new(recents, checkpoints).map_err(error)
368 }
369}
370
371impl<N: Network> ToBytes for BlockLocators<N> {
372 fn write_le<W: Write>(&self, mut writer: W) -> IoResult<()> {
373 u32::try_from(self.recents.len()).map_err(error)?.write_le(&mut writer)?;
375 for (height, hash) in &self.recents {
377 height.write_le(&mut writer)?;
378 hash.write_le(&mut writer)?;
379 }
380
381 u32::try_from(self.checkpoints.len()).map_err(error)?.write_le(&mut writer)?;
383 for (height, hash) in &self.checkpoints {
385 height.write_le(&mut writer)?;
386 hash.write_le(&mut writer)?;
387 }
388 Ok(())
389 }
390}
391
392#[cfg(any(test, feature = "test"))]
393pub mod test_helpers {
394 use super::*;
395 use snarkvm::prelude::Field;
396
397 type CurrentNetwork = snarkvm::prelude::MainnetV0;
398
399 pub fn sample_block_locators(height: u32) -> BlockLocators<CurrentNetwork> {
403 let mut recents = IndexMap::new();
405 let recents_range = match height < NUM_RECENT_BLOCKS as u32 {
406 true => 0..=height,
407 false => (height - NUM_RECENT_BLOCKS as u32 + 1)..=height,
408 };
409 for i in recents_range {
410 recents.insert(i, (Field::<CurrentNetwork>::from_u32(i)).into());
411 }
412
413 let mut checkpoints = IndexMap::new();
415 for i in (0..=height).step_by(CHECKPOINT_INTERVAL as usize) {
416 checkpoints.insert(i, (Field::<CurrentNetwork>::from_u32(i)).into());
417 }
418
419 BlockLocators::new(recents, checkpoints).unwrap()
421 }
422
423 pub fn sample_block_locators_with_fork(height: u32, fork_height: u32) -> BlockLocators<CurrentNetwork> {
427 assert!(fork_height <= height, "Fork height must be less than or equal to the given height");
428 assert!(
429 height - fork_height < NUM_RECENT_BLOCKS as u32,
430 "Fork must be within NUM_RECENT_BLOCKS of the given height"
431 );
432
433 let mut recents = IndexMap::new();
435 let recents_range = match height < NUM_RECENT_BLOCKS as u32 {
436 true => 0..=height,
437 false => (height - NUM_RECENT_BLOCKS as u32 + 1)..=height,
438 };
439 for i in recents_range {
440 if i >= fork_height {
441 recents.insert(i, (-Field::<CurrentNetwork>::from_u32(i)).into());
442 } else {
443 recents.insert(i, (Field::<CurrentNetwork>::from_u32(i)).into());
444 }
445 }
446
447 let mut checkpoints = IndexMap::new();
449 for i in (0..=height).step_by(CHECKPOINT_INTERVAL as usize) {
450 checkpoints.insert(i, (Field::<CurrentNetwork>::from_u32(i)).into());
451 }
452
453 BlockLocators::new(recents, checkpoints).unwrap()
455 }
456
457 #[test]
459 fn test_sample_block_locators() {
460 for expected_height in 0..=100_001u32 {
461 println!("Testing height - {expected_height}");
462
463 let expected_num_checkpoints = (expected_height / CHECKPOINT_INTERVAL) + 1;
464 let expected_num_recents = match expected_height < NUM_RECENT_BLOCKS as u32 {
465 true => expected_height + 1,
466 false => NUM_RECENT_BLOCKS as u32,
467 };
468
469 let block_locators = sample_block_locators(expected_height);
470 assert_eq!(block_locators.checkpoints.len(), expected_num_checkpoints as usize);
471 assert_eq!(block_locators.recents.len(), expected_num_recents as usize);
472 assert_eq!(block_locators.latest_locator_height(), expected_height);
473 }
476 }
477}
478
479#[cfg(test)]
480mod tests {
481 use super::*;
482 use snarkvm::prelude::Field;
483
484 use core::ops::Range;
485
486 type CurrentNetwork = snarkvm::prelude::MainnetV0;
487
488 fn check_is_valid(checkpoints: IndexMap<u32, <CurrentNetwork as Network>::BlockHash>, heights: Range<u32>) {
490 for height in heights {
491 let mut recents = IndexMap::new();
492 for i in 0..NUM_RECENT_BLOCKS as u32 {
493 recents.insert(height + i, (Field::<CurrentNetwork>::from_u32(height + i)).into());
494
495 let block_locators =
496 BlockLocators::<CurrentNetwork>::new_unchecked(recents.clone(), checkpoints.clone());
497 if height == 0 && recents.len() < NUM_RECENT_BLOCKS {
498 block_locators.ensure_is_valid().unwrap();
500 } else if recents.len() < NUM_RECENT_BLOCKS {
501 block_locators.ensure_is_valid().unwrap_err();
503 } else {
504 block_locators.ensure_is_valid().unwrap();
506 }
507 }
508 recents.insert(
510 height + NUM_RECENT_BLOCKS as u32,
511 (Field::<CurrentNetwork>::from_u32(height + NUM_RECENT_BLOCKS as u32)).into(),
512 );
513 let block_locators = BlockLocators::<CurrentNetwork>::new_unchecked(recents.clone(), checkpoints.clone());
514 block_locators.ensure_is_valid().unwrap_err();
515 }
516 }
517
518 fn check_is_consistent(
520 checkpoints: IndexMap<u32, <CurrentNetwork as Network>::BlockHash>,
521 heights: Range<u32>,
522 genesis_locators: BlockLocators<CurrentNetwork>,
523 second_locators: BlockLocators<CurrentNetwork>,
524 ) {
525 for height in heights {
526 let mut recents = IndexMap::new();
527 for i in 0..NUM_RECENT_BLOCKS as u32 {
528 recents.insert(height + i, (Field::<CurrentNetwork>::from_u32(height + i)).into());
529
530 let block_locators =
531 BlockLocators::<CurrentNetwork>::new_unchecked(recents.clone(), checkpoints.clone());
532 block_locators.ensure_is_consistent_with(&block_locators).unwrap();
533
534 let is_first_num_recents_blocks = height == 0 && recents.len() < NUM_RECENT_BLOCKS;
536 let is_num_recents_blocks = recents.len() == NUM_RECENT_BLOCKS;
537 if is_first_num_recents_blocks || is_num_recents_blocks {
538 genesis_locators.ensure_is_consistent_with(&block_locators).unwrap();
540 block_locators.ensure_is_consistent_with(&genesis_locators).unwrap();
541
542 second_locators.ensure_is_consistent_with(&block_locators).unwrap();
544 block_locators.ensure_is_consistent_with(&second_locators).unwrap();
545 }
546 }
547 }
548 }
549
550 #[test]
551 fn test_ensure_is_valid() {
552 let zero: <CurrentNetwork as Network>::BlockHash = (Field::<CurrentNetwork>::from_u32(0)).into();
553 let checkpoint_1: <CurrentNetwork as Network>::BlockHash =
554 (Field::<CurrentNetwork>::from_u32(CHECKPOINT_INTERVAL)).into();
555
556 for height in 0..10 {
558 let block_locators = test_helpers::sample_block_locators(height);
559 block_locators.ensure_is_valid().unwrap();
560 }
561
562 let checkpoints = IndexMap::from([(0, zero)]);
564 let mut recents = IndexMap::new();
565 for i in 0..NUM_RECENT_BLOCKS {
566 recents.insert(i as u32, (Field::<CurrentNetwork>::from_u32(i as u32)).into());
567 let block_locators = BlockLocators::<CurrentNetwork>::new_unchecked(recents.clone(), checkpoints.clone());
568 block_locators.ensure_is_valid().unwrap();
569 }
570 recents.insert(NUM_RECENT_BLOCKS as u32, (Field::<CurrentNetwork>::from_u32(NUM_RECENT_BLOCKS as u32)).into());
572 let block_locators = BlockLocators::<CurrentNetwork>::new_unchecked(recents.clone(), checkpoints);
573 block_locators.ensure_is_valid().unwrap_err();
574
575 let checkpoints = IndexMap::from([(0, zero)]);
577 check_is_valid(checkpoints, 0..(CHECKPOINT_INTERVAL - NUM_RECENT_BLOCKS as u32));
578
579 let checkpoints = IndexMap::from([(0, zero), (CHECKPOINT_INTERVAL, checkpoint_1)]);
581 check_is_valid(
582 checkpoints,
583 (CHECKPOINT_INTERVAL - NUM_RECENT_BLOCKS as u32 + 1)..(CHECKPOINT_INTERVAL * 2 - NUM_RECENT_BLOCKS as u32),
584 );
585 }
586
587 #[test]
588 fn test_ensure_is_valid_fails() {
589 let zero: <CurrentNetwork as Network>::BlockHash = (Field::<CurrentNetwork>::from_u32(0)).into();
590 let one: <CurrentNetwork as Network>::BlockHash = (Field::<CurrentNetwork>::from_u32(1)).into();
591
592 let block_locators = BlockLocators::<CurrentNetwork>::new_unchecked(Default::default(), Default::default());
594 block_locators.ensure_is_valid().unwrap_err();
595
596 let block_locators =
598 BlockLocators::<CurrentNetwork>::new_unchecked(IndexMap::from([(0, zero)]), IndexMap::from([(0, one)]));
599 block_locators.ensure_is_valid().unwrap_err();
600
601 let block_locators =
603 BlockLocators::<CurrentNetwork>::new_unchecked(IndexMap::from([(0, one)]), IndexMap::from([(0, zero)]));
604 block_locators.ensure_is_valid().unwrap_err();
605
606 let block_locators = BlockLocators::<CurrentNetwork>::new_unchecked(
608 IndexMap::from([(0, one), (1, zero)]),
609 IndexMap::from([(0, zero)]),
610 );
611 block_locators.ensure_is_valid().unwrap_err();
612
613 let block_locators = BlockLocators::<CurrentNetwork>::new_unchecked(
615 IndexMap::from([(0, zero), (1, zero)]),
616 IndexMap::from([(0, zero)]),
617 );
618 block_locators.ensure_is_valid().unwrap_err();
619
620 let mut recents = IndexMap::new();
622 for i in 0..NUM_RECENT_BLOCKS {
623 recents.insert(10_000 + i as u32, (Field::<CurrentNetwork>::from_u32(i as u32)).into());
624 }
625 let block_locators = BlockLocators::<CurrentNetwork>::new_unchecked(recents, IndexMap::from([(0, zero)]));
626 block_locators.ensure_is_valid().unwrap_err();
627 }
628
629 #[test]
630 fn test_ensure_is_consistent_with() {
631 let zero: <CurrentNetwork as Network>::BlockHash = (Field::<CurrentNetwork>::from_u32(0)).into();
632 let one: <CurrentNetwork as Network>::BlockHash = (Field::<CurrentNetwork>::from_u32(1)).into();
633
634 let genesis_locators =
635 BlockLocators::<CurrentNetwork>::new_unchecked(IndexMap::from([(0, zero)]), IndexMap::from([(0, zero)]));
636 let second_locators = BlockLocators::<CurrentNetwork>::new_unchecked(
637 IndexMap::from([(0, zero), (1, one)]),
638 IndexMap::from([(0, zero)]),
639 );
640
641 genesis_locators.ensure_is_consistent_with(&genesis_locators).unwrap();
643
644 genesis_locators.ensure_is_consistent_with(&second_locators).unwrap();
646 second_locators.ensure_is_consistent_with(&genesis_locators).unwrap();
647
648 let checkpoints = IndexMap::from([(0, Default::default())]);
650 check_is_consistent(
651 checkpoints,
652 0..(CHECKPOINT_INTERVAL - NUM_RECENT_BLOCKS as u32),
653 genesis_locators.clone(),
654 second_locators.clone(),
655 );
656
657 let checkpoints = IndexMap::from([(0, Default::default()), (CHECKPOINT_INTERVAL, Default::default())]);
659 check_is_consistent(
660 checkpoints,
661 (CHECKPOINT_INTERVAL - NUM_RECENT_BLOCKS as u32)..(CHECKPOINT_INTERVAL * 2 - NUM_RECENT_BLOCKS as u32),
662 genesis_locators,
663 second_locators,
664 );
665 }
666
667 #[test]
668 fn test_ensure_is_consistent_with_fails() {
669 let zero: <CurrentNetwork as Network>::BlockHash = (Field::<CurrentNetwork>::from_u32(0)).into();
670 let one: <CurrentNetwork as Network>::BlockHash = (Field::<CurrentNetwork>::from_u32(1)).into();
671
672 let genesis_locators =
673 BlockLocators::<CurrentNetwork>::new(IndexMap::from([(0, zero)]), IndexMap::from([(0, zero)])).unwrap();
674 let second_locators =
675 BlockLocators::<CurrentNetwork>::new(IndexMap::from([(0, zero), (1, one)]), IndexMap::from([(0, zero)]))
676 .unwrap();
677
678 let wrong_genesis_locators =
679 BlockLocators::<CurrentNetwork>::new(IndexMap::from([(0, one)]), IndexMap::from([(0, one)])).unwrap();
680 let wrong_second_locators =
681 BlockLocators::<CurrentNetwork>::new(IndexMap::from([(0, one), (1, zero)]), IndexMap::from([(0, one)]))
682 .unwrap();
683
684 genesis_locators.ensure_is_consistent_with(&wrong_genesis_locators).unwrap_err();
685 wrong_genesis_locators.ensure_is_consistent_with(&genesis_locators).unwrap_err();
686
687 genesis_locators.ensure_is_consistent_with(&wrong_second_locators).unwrap_err();
688 wrong_second_locators.ensure_is_consistent_with(&genesis_locators).unwrap_err();
689
690 second_locators.ensure_is_consistent_with(&wrong_genesis_locators).unwrap_err();
691 wrong_genesis_locators.ensure_is_consistent_with(&second_locators).unwrap_err();
692
693 second_locators.ensure_is_consistent_with(&wrong_second_locators).unwrap_err();
694 wrong_second_locators.ensure_is_consistent_with(&second_locators).unwrap_err();
695 }
696}