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)]
72pub struct BlockLocators<N: Network> {
73 pub recents: IndexMap<u32, N::BlockHash>,
75 pub checkpoints: IndexMap<u32, N::BlockHash>,
77}
78
79impl<N: Network> BlockLocators<N> {
80 pub fn new(recents: IndexMap<u32, N::BlockHash>, checkpoints: IndexMap<u32, N::BlockHash>) -> Result<Self> {
82 let locators = Self { recents, checkpoints };
84 locators.ensure_is_valid()?;
86 Ok(locators)
88 }
89
90 #[cfg(test)]
93 fn new_unchecked(recents: IndexMap<u32, N::BlockHash>, checkpoints: IndexMap<u32, N::BlockHash>) -> Self {
94 Self { recents, checkpoints }
95 }
96
97 pub fn new_genesis(genesis_hash: N::BlockHash) -> Self {
99 Self { recents: indexmap![0 => genesis_hash], checkpoints: indexmap![0 => genesis_hash] }
100 }
101}
102
103impl<N: Network> IntoIterator for BlockLocators<N> {
104 type IntoIter = IntoIter<u32, N::BlockHash>;
105 type Item = (u32, N::BlockHash);
106
107 fn into_iter(self) -> Self::IntoIter {
111 BTreeMap::from_iter(self.checkpoints.into_iter().chain(self.recents)).into_iter()
112 }
113}
114
115impl<N: Network> BlockLocators<N> {
116 pub fn latest_locator_height(&self) -> u32 {
118 self.recents.keys().last().copied().unwrap_or_default()
119 }
120
121 pub fn get_hash(&self, height: u32) -> Option<N::BlockHash> {
123 self.recents.get(&height).copied().or_else(|| self.checkpoints.get(&height).copied())
124 }
125
126 pub fn is_valid(&self) -> bool {
128 if let Err(_error) = self.ensure_is_valid() {
130 return false;
132 }
133 true
134 }
135
136 pub fn is_consistent_with(&self, other: &Self) -> bool {
139 if let Err(_error) = self.ensure_is_consistent_with(other) {
141 return false;
143 }
144 true
145 }
146
147 pub fn ensure_is_valid(&self) -> Result<()> {
149 Self::check_block_locators(&self.recents, &self.checkpoints)
151 }
152
153 pub fn ensure_is_consistent_with(&self, other: &Self) -> Result<()> {
156 Self::check_consistent_block_locators(self, other)
157 }
158}
159
160impl<N: Network> BlockLocators<N> {
161 pub fn check_consistent_block_locators(
164 old_locators: &BlockLocators<N>,
165 new_locators: &BlockLocators<N>,
166 ) -> Result<()> {
167 for (height, hash) in new_locators.recents.iter() {
169 if let Some(recent_hash) = old_locators.recents.get(height) {
170 if recent_hash != hash {
171 bail!("Recent block hash mismatch at height {height}")
172 }
173 }
174 }
175 for (height, hash) in new_locators.checkpoints.iter() {
177 if let Some(checkpoint_hash) = old_locators.checkpoints.get(height) {
178 if checkpoint_hash != hash {
179 bail!("Block checkpoint hash mismatch for height {height}")
180 }
181 }
182 }
183 Ok(())
184 }
185
186 pub fn check_block_locators(
188 recents: &IndexMap<u32, N::BlockHash>,
189 checkpoints: &IndexMap<u32, N::BlockHash>,
190 ) -> Result<()> {
191 let last_recent_height = Self::check_recent_blocks(recents)?;
193 let last_checkpoint_height = Self::check_block_checkpoints(checkpoints)?;
195
196 if !(last_checkpoint_height..last_checkpoint_height.saturating_add(CHECKPOINT_INTERVAL))
206 .contains(&last_recent_height)
207 {
208 bail!(
209 "Last checkpoint height ({last_checkpoint_height}) is not the largest multiple of \
210 {CHECKPOINT_INTERVAL} that does not exceed the last recent height ({last_recent_height})"
211 )
212 }
213
214 let last_recent_to_last_checkpoint_distance = last_recent_height % CHECKPOINT_INTERVAL;
227 if last_recent_to_last_checkpoint_distance < NUM_RECENT_BLOCKS as u32 {
228 let common = last_recent_height - last_recent_to_last_checkpoint_distance;
229 if recents.get(&common).unwrap() != checkpoints.get(&common).unwrap() {
230 bail!("Recent block hash and checkpoint hash mismatch at height {common}")
231 }
232 }
233
234 Ok(())
235 }
236
237 fn check_recent_blocks(recents: &IndexMap<u32, N::BlockHash>) -> Result<u32> {
246 if recents.is_empty() {
248 bail!("There must be at least 1 recent block")
249 }
250 if recents.len() > NUM_RECENT_BLOCKS {
253 bail!("There can be at most {NUM_RECENT_BLOCKS} blocks in the map")
254 }
255
256 let mut last_height = 0;
258 for (i, current_height) in recents.keys().enumerate() {
259 if i == 0 && recents.len() < NUM_RECENT_BLOCKS && *current_height > 0 {
260 bail!("Ledgers under {NUM_RECENT_BLOCKS} blocks must have the first recent block at height 0")
261 }
262 if i > 0 && *current_height <= last_height {
263 bail!("Recent blocks must increment in height")
264 }
265 if i > 0 && *current_height - last_height != RECENT_INTERVAL {
266 bail!("Recent blocks must increment by {RECENT_INTERVAL}")
267 }
268 last_height = *current_height;
269 }
270
271 if last_height >= NUM_RECENT_BLOCKS as u32 && recents.len() != NUM_RECENT_BLOCKS {
281 bail!("Number of recent blocks must match {NUM_RECENT_BLOCKS}")
282 }
283
284 if has_duplicates(recents.values()) {
286 bail!("Recent block hashes must be unique")
287 }
288
289 Ok(last_height)
290 }
291
292 fn check_block_checkpoints(checkpoints: &IndexMap<u32, N::BlockHash>) -> Result<u32> {
300 ensure!(!checkpoints.is_empty(), "There must be at least 1 block checkpoint");
302
303 let mut last_height = 0;
305 for (i, current_height) in checkpoints.keys().enumerate() {
306 if i == 0 && *current_height != 0 {
307 bail!("First block checkpoint must be at height 0")
308 }
309 if i > 0 && *current_height <= last_height {
310 bail!("Block checkpoints must increment in height")
311 }
312 if i > 0 && *current_height - last_height != CHECKPOINT_INTERVAL {
313 bail!("Block checkpoints must increment by {CHECKPOINT_INTERVAL}")
314 }
315 last_height = *current_height;
316 }
317
318 if has_duplicates(checkpoints.values()) {
320 bail!("Block checkpoints must be unique")
321 }
322
323 Ok(last_height)
324 }
325}
326
327impl<N: Network> FromBytes for BlockLocators<N> {
328 fn read_le<R: Read>(mut reader: R) -> IoResult<Self> {
329 let num_recents = u32::read_le(&mut reader)?;
331 let mut recents = IndexMap::new();
333 for _ in 0..num_recents {
334 let height = u32::read_le(&mut reader)?;
335 let hash = N::BlockHash::read_le(&mut reader)?;
336 recents.insert(height, hash);
337 }
338
339 let num_checkpoints = u32::read_le(&mut reader)?;
341 let mut checkpoints = IndexMap::new();
343 for _ in 0..num_checkpoints {
344 let height = u32::read_le(&mut reader)?;
345 let hash = N::BlockHash::read_le(&mut reader)?;
346 checkpoints.insert(height, hash);
347 }
348
349 Self::new(recents, checkpoints).map_err(error)
350 }
351}
352
353impl<N: Network> ToBytes for BlockLocators<N> {
354 fn write_le<W: Write>(&self, mut writer: W) -> IoResult<()> {
355 u32::try_from(self.recents.len()).map_err(error)?.write_le(&mut writer)?;
357 for (height, hash) in &self.recents {
359 height.write_le(&mut writer)?;
360 hash.write_le(&mut writer)?;
361 }
362
363 u32::try_from(self.checkpoints.len()).map_err(error)?.write_le(&mut writer)?;
365 for (height, hash) in &self.checkpoints {
367 height.write_le(&mut writer)?;
368 hash.write_le(&mut writer)?;
369 }
370 Ok(())
371 }
372}
373
374#[cfg(any(test, feature = "test"))]
375pub mod test_helpers {
376 use super::*;
377 use snarkvm::prelude::Field;
378
379 type CurrentNetwork = snarkvm::prelude::MainnetV0;
380
381 pub fn sample_block_locators(height: u32) -> BlockLocators<CurrentNetwork> {
385 let mut recents = IndexMap::new();
387 let recents_range = match height < NUM_RECENT_BLOCKS as u32 {
388 true => 0..=height,
389 false => (height - NUM_RECENT_BLOCKS as u32 + 1)..=height,
390 };
391 for i in recents_range {
392 recents.insert(i, (Field::<CurrentNetwork>::from_u32(i)).into());
393 }
394
395 let mut checkpoints = IndexMap::new();
397 for i in (0..=height).step_by(CHECKPOINT_INTERVAL as usize) {
398 checkpoints.insert(i, (Field::<CurrentNetwork>::from_u32(i)).into());
399 }
400
401 BlockLocators::new(recents, checkpoints).unwrap()
403 }
404
405 pub fn sample_block_locators_with_fork(height: u32, fork_height: u32) -> BlockLocators<CurrentNetwork> {
409 assert!(fork_height <= height, "Fork height must be less than or equal to the given height");
410 assert!(
411 height - fork_height < NUM_RECENT_BLOCKS as u32,
412 "Fork must be within NUM_RECENT_BLOCKS of the given height"
413 );
414
415 let mut recents = IndexMap::new();
417 let recents_range = match height < NUM_RECENT_BLOCKS as u32 {
418 true => 0..=height,
419 false => (height - NUM_RECENT_BLOCKS as u32 + 1)..=height,
420 };
421 for i in recents_range {
422 if i >= fork_height {
423 recents.insert(i, (-Field::<CurrentNetwork>::from_u32(i)).into());
424 } else {
425 recents.insert(i, (Field::<CurrentNetwork>::from_u32(i)).into());
426 }
427 }
428
429 let mut checkpoints = IndexMap::new();
431 for i in (0..=height).step_by(CHECKPOINT_INTERVAL as usize) {
432 checkpoints.insert(i, (Field::<CurrentNetwork>::from_u32(i)).into());
433 }
434
435 BlockLocators::new(recents, checkpoints).unwrap()
437 }
438
439 #[test]
441 fn test_sample_block_locators() {
442 for expected_height in 0..=100_001u32 {
443 println!("Testing height - {expected_height}");
444
445 let expected_num_checkpoints = (expected_height / CHECKPOINT_INTERVAL) + 1;
446 let expected_num_recents = match expected_height < NUM_RECENT_BLOCKS as u32 {
447 true => expected_height + 1,
448 false => NUM_RECENT_BLOCKS as u32,
449 };
450
451 let block_locators = sample_block_locators(expected_height);
452 assert_eq!(block_locators.checkpoints.len(), expected_num_checkpoints as usize);
453 assert_eq!(block_locators.recents.len(), expected_num_recents as usize);
454 assert_eq!(block_locators.latest_locator_height(), expected_height);
455 }
458 }
459}
460
461#[cfg(test)]
462mod tests {
463 use super::*;
464 use snarkvm::prelude::Field;
465
466 use core::ops::Range;
467
468 type CurrentNetwork = snarkvm::prelude::MainnetV0;
469
470 fn check_is_valid(checkpoints: IndexMap<u32, <CurrentNetwork as Network>::BlockHash>, heights: Range<u32>) {
472 for height in heights {
473 let mut recents = IndexMap::new();
474 for i in 0..NUM_RECENT_BLOCKS as u32 {
475 recents.insert(height + i, (Field::<CurrentNetwork>::from_u32(height + i)).into());
476
477 let block_locators =
478 BlockLocators::<CurrentNetwork>::new_unchecked(recents.clone(), checkpoints.clone());
479 if height == 0 && recents.len() < NUM_RECENT_BLOCKS {
480 block_locators.ensure_is_valid().unwrap();
482 } else if recents.len() < NUM_RECENT_BLOCKS {
483 block_locators.ensure_is_valid().unwrap_err();
485 } else {
486 block_locators.ensure_is_valid().unwrap();
488 }
489 }
490 recents.insert(
492 height + NUM_RECENT_BLOCKS as u32,
493 (Field::<CurrentNetwork>::from_u32(height + NUM_RECENT_BLOCKS as u32)).into(),
494 );
495 let block_locators = BlockLocators::<CurrentNetwork>::new_unchecked(recents.clone(), checkpoints.clone());
496 block_locators.ensure_is_valid().unwrap_err();
497 }
498 }
499
500 fn check_is_consistent(
502 checkpoints: IndexMap<u32, <CurrentNetwork as Network>::BlockHash>,
503 heights: Range<u32>,
504 genesis_locators: BlockLocators<CurrentNetwork>,
505 second_locators: BlockLocators<CurrentNetwork>,
506 ) {
507 for height in heights {
508 let mut recents = IndexMap::new();
509 for i in 0..NUM_RECENT_BLOCKS as u32 {
510 recents.insert(height + i, (Field::<CurrentNetwork>::from_u32(height + i)).into());
511
512 let block_locators =
513 BlockLocators::<CurrentNetwork>::new_unchecked(recents.clone(), checkpoints.clone());
514 block_locators.ensure_is_consistent_with(&block_locators).unwrap();
515
516 let is_first_num_recents_blocks = height == 0 && recents.len() < NUM_RECENT_BLOCKS;
518 let is_num_recents_blocks = recents.len() == NUM_RECENT_BLOCKS;
519 if is_first_num_recents_blocks || is_num_recents_blocks {
520 genesis_locators.ensure_is_consistent_with(&block_locators).unwrap();
522 block_locators.ensure_is_consistent_with(&genesis_locators).unwrap();
523
524 second_locators.ensure_is_consistent_with(&block_locators).unwrap();
526 block_locators.ensure_is_consistent_with(&second_locators).unwrap();
527 }
528 }
529 }
530 }
531
532 #[test]
533 fn test_ensure_is_valid() {
534 let zero: <CurrentNetwork as Network>::BlockHash = (Field::<CurrentNetwork>::from_u32(0)).into();
535 let checkpoint_1: <CurrentNetwork as Network>::BlockHash =
536 (Field::<CurrentNetwork>::from_u32(CHECKPOINT_INTERVAL)).into();
537
538 for height in 0..10 {
540 let block_locators = test_helpers::sample_block_locators(height);
541 block_locators.ensure_is_valid().unwrap();
542 }
543
544 let checkpoints = IndexMap::from([(0, zero)]);
546 let mut recents = IndexMap::new();
547 for i in 0..NUM_RECENT_BLOCKS {
548 recents.insert(i as u32, (Field::<CurrentNetwork>::from_u32(i as u32)).into());
549 let block_locators = BlockLocators::<CurrentNetwork>::new_unchecked(recents.clone(), checkpoints.clone());
550 block_locators.ensure_is_valid().unwrap();
551 }
552 recents.insert(NUM_RECENT_BLOCKS as u32, (Field::<CurrentNetwork>::from_u32(NUM_RECENT_BLOCKS as u32)).into());
554 let block_locators = BlockLocators::<CurrentNetwork>::new_unchecked(recents.clone(), checkpoints);
555 block_locators.ensure_is_valid().unwrap_err();
556
557 let checkpoints = IndexMap::from([(0, zero)]);
559 check_is_valid(checkpoints, 0..(CHECKPOINT_INTERVAL - NUM_RECENT_BLOCKS as u32));
560
561 let checkpoints = IndexMap::from([(0, zero), (CHECKPOINT_INTERVAL, checkpoint_1)]);
563 check_is_valid(
564 checkpoints,
565 (CHECKPOINT_INTERVAL - NUM_RECENT_BLOCKS as u32 + 1)..(CHECKPOINT_INTERVAL * 2 - NUM_RECENT_BLOCKS as u32),
566 );
567 }
568
569 #[test]
570 fn test_ensure_is_valid_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 block_locators = BlockLocators::<CurrentNetwork>::new_unchecked(Default::default(), Default::default());
576 block_locators.ensure_is_valid().unwrap_err();
577
578 let block_locators =
580 BlockLocators::<CurrentNetwork>::new_unchecked(IndexMap::from([(0, zero)]), IndexMap::from([(0, one)]));
581 block_locators.ensure_is_valid().unwrap_err();
582
583 let block_locators =
585 BlockLocators::<CurrentNetwork>::new_unchecked(IndexMap::from([(0, one)]), IndexMap::from([(0, zero)]));
586 block_locators.ensure_is_valid().unwrap_err();
587
588 let block_locators = BlockLocators::<CurrentNetwork>::new_unchecked(
590 IndexMap::from([(0, one), (1, zero)]),
591 IndexMap::from([(0, zero)]),
592 );
593 block_locators.ensure_is_valid().unwrap_err();
594
595 let block_locators = BlockLocators::<CurrentNetwork>::new_unchecked(
597 IndexMap::from([(0, zero), (1, zero)]),
598 IndexMap::from([(0, zero)]),
599 );
600 block_locators.ensure_is_valid().unwrap_err();
601
602 let mut recents = IndexMap::new();
604 for i in 0..NUM_RECENT_BLOCKS {
605 recents.insert(10_000 + i as u32, (Field::<CurrentNetwork>::from_u32(i as u32)).into());
606 }
607 let block_locators = BlockLocators::<CurrentNetwork>::new_unchecked(recents, IndexMap::from([(0, zero)]));
608 block_locators.ensure_is_valid().unwrap_err();
609 }
610
611 #[test]
612 fn test_ensure_is_consistent_with() {
613 let zero: <CurrentNetwork as Network>::BlockHash = (Field::<CurrentNetwork>::from_u32(0)).into();
614 let one: <CurrentNetwork as Network>::BlockHash = (Field::<CurrentNetwork>::from_u32(1)).into();
615
616 let genesis_locators =
617 BlockLocators::<CurrentNetwork>::new_unchecked(IndexMap::from([(0, zero)]), IndexMap::from([(0, zero)]));
618 let second_locators = BlockLocators::<CurrentNetwork>::new_unchecked(
619 IndexMap::from([(0, zero), (1, one)]),
620 IndexMap::from([(0, zero)]),
621 );
622
623 genesis_locators.ensure_is_consistent_with(&genesis_locators).unwrap();
625
626 genesis_locators.ensure_is_consistent_with(&second_locators).unwrap();
628 second_locators.ensure_is_consistent_with(&genesis_locators).unwrap();
629
630 let checkpoints = IndexMap::from([(0, Default::default())]);
632 check_is_consistent(
633 checkpoints,
634 0..(CHECKPOINT_INTERVAL - NUM_RECENT_BLOCKS as u32),
635 genesis_locators.clone(),
636 second_locators.clone(),
637 );
638
639 let checkpoints = IndexMap::from([(0, Default::default()), (CHECKPOINT_INTERVAL, Default::default())]);
641 check_is_consistent(
642 checkpoints,
643 (CHECKPOINT_INTERVAL - NUM_RECENT_BLOCKS as u32)..(CHECKPOINT_INTERVAL * 2 - NUM_RECENT_BLOCKS as u32),
644 genesis_locators,
645 second_locators,
646 );
647 }
648
649 #[test]
650 fn test_ensure_is_consistent_with_fails() {
651 let zero: <CurrentNetwork as Network>::BlockHash = (Field::<CurrentNetwork>::from_u32(0)).into();
652 let one: <CurrentNetwork as Network>::BlockHash = (Field::<CurrentNetwork>::from_u32(1)).into();
653
654 let genesis_locators =
655 BlockLocators::<CurrentNetwork>::new(IndexMap::from([(0, zero)]), IndexMap::from([(0, zero)])).unwrap();
656 let second_locators =
657 BlockLocators::<CurrentNetwork>::new(IndexMap::from([(0, zero), (1, one)]), IndexMap::from([(0, zero)]))
658 .unwrap();
659
660 let wrong_genesis_locators =
661 BlockLocators::<CurrentNetwork>::new(IndexMap::from([(0, one)]), IndexMap::from([(0, one)])).unwrap();
662 let wrong_second_locators =
663 BlockLocators::<CurrentNetwork>::new(IndexMap::from([(0, one), (1, zero)]), IndexMap::from([(0, one)]))
664 .unwrap();
665
666 genesis_locators.ensure_is_consistent_with(&wrong_genesis_locators).unwrap_err();
667 wrong_genesis_locators.ensure_is_consistent_with(&genesis_locators).unwrap_err();
668
669 genesis_locators.ensure_is_consistent_with(&wrong_second_locators).unwrap_err();
670 wrong_second_locators.ensure_is_consistent_with(&genesis_locators).unwrap_err();
671
672 second_locators.ensure_is_consistent_with(&wrong_genesis_locators).unwrap_err();
673 wrong_genesis_locators.ensure_is_consistent_with(&second_locators).unwrap_err();
674
675 second_locators.ensure_is_consistent_with(&wrong_second_locators).unwrap_err();
676 wrong_second_locators.ensure_is_consistent_with(&second_locators).unwrap_err();
677 }
678}