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)]
70pub struct BlockLocators<N: Network> {
71 pub recents: IndexMap<u32, N::BlockHash>,
73 pub checkpoints: IndexMap<u32, N::BlockHash>,
75}
76
77impl<N: Network> BlockLocators<N> {
78 pub fn new(recents: IndexMap<u32, N::BlockHash>, checkpoints: IndexMap<u32, N::BlockHash>) -> Result<Self> {
80 let locators = Self { recents, checkpoints };
82 locators.ensure_is_valid()?;
84 Ok(locators)
86 }
87
88 pub fn new_unchecked(recents: IndexMap<u32, N::BlockHash>, checkpoints: IndexMap<u32, N::BlockHash>) -> Self {
90 Self { recents, checkpoints }
91 }
92
93 pub fn new_genesis(genesis_hash: N::BlockHash) -> Self {
95 Self { recents: indexmap![0 => genesis_hash], checkpoints: indexmap![0 => genesis_hash] }
96 }
97}
98
99impl<N: Network> IntoIterator for BlockLocators<N> {
100 type IntoIter = IntoIter<u32, N::BlockHash>;
101 type Item = (u32, N::BlockHash);
102
103 fn into_iter(self) -> Self::IntoIter {
107 BTreeMap::from_iter(self.checkpoints.into_iter().chain(self.recents)).into_iter()
108 }
109}
110
111impl<N: Network> BlockLocators<N> {
112 pub fn latest_locator_height(&self) -> u32 {
114 self.recents.keys().last().copied().unwrap_or_default()
115 }
116
117 pub fn get_hash(&self, height: u32) -> Option<N::BlockHash> {
119 self.recents.get(&height).copied().or_else(|| self.checkpoints.get(&height).copied())
120 }
121
122 pub fn is_valid(&self) -> bool {
124 if let Err(error) = self.ensure_is_valid() {
126 warn!("Block locators are invalid: {error}");
127 return false;
128 }
129 true
130 }
131
132 pub fn is_consistent_with(&self, other: &Self) -> bool {
135 if let Err(error) = self.ensure_is_consistent_with(other) {
137 warn!("Inconsistent block locators: {error}");
138 return false;
139 }
140 true
141 }
142
143 pub fn ensure_is_valid(&self) -> Result<()> {
145 Self::check_block_locators(&self.recents, &self.checkpoints)
147 }
148
149 pub fn ensure_is_consistent_with(&self, other: &Self) -> Result<()> {
152 Self::check_consistent_block_locators(self, other)
153 }
154}
155
156impl<N: Network> BlockLocators<N> {
157 pub fn check_consistent_block_locators(
160 old_locators: &BlockLocators<N>,
161 new_locators: &BlockLocators<N>,
162 ) -> Result<()> {
163 for (height, hash) in new_locators.recents.iter() {
165 if let Some(recent_hash) = old_locators.recents.get(height) {
166 if recent_hash != hash {
167 bail!("Recent block hash mismatch at height {height}")
168 }
169 }
170 }
171 for (height, hash) in new_locators.checkpoints.iter() {
173 if let Some(checkpoint_hash) = old_locators.checkpoints.get(height) {
174 if checkpoint_hash != hash {
175 bail!("Block checkpoint hash mismatch for height {height}")
176 }
177 }
178 }
179 Ok(())
180 }
181
182 pub fn check_block_locators(
184 recents: &IndexMap<u32, N::BlockHash>,
185 checkpoints: &IndexMap<u32, N::BlockHash>,
186 ) -> Result<()> {
187 let last_recent_height = Self::check_recent_blocks(recents)?;
189 let last_checkpoint_height = Self::check_block_checkpoints(checkpoints)?;
191
192 let threshold = last_checkpoint_height.saturating_sub(NUM_RECENT_BLOCKS as u32);
194 if last_recent_height < threshold {
195 bail!("Recent height ({last_recent_height}) cannot be below checkpoint threshold ({threshold})")
196 }
197
198 if last_recent_height < NUM_RECENT_BLOCKS as u32
200 && recents.get(&0).copied().unwrap_or_default() != checkpoints.get(&0).copied().unwrap_or_default()
201 {
202 bail!("Recent genesis hash and checkpoint genesis hash mismatch at height {last_recent_height}")
203 }
204
205 if let Some(last_checkpoint_hash) = checkpoints.get(&last_recent_height) {
207 if let Some(last_recent_hash) = recents.get(&last_recent_height) {
208 if last_checkpoint_hash != last_recent_hash {
209 bail!("Recent block hash and checkpoint hash mismatch at height {last_recent_height}")
210 }
211 }
212 }
213 Ok(())
214 }
215
216 fn check_recent_blocks(recents: &IndexMap<u32, N::BlockHash>) -> Result<u32> {
225 if recents.is_empty() {
227 bail!("There must be at least 1 recent block")
228 }
229 if recents.len() > NUM_RECENT_BLOCKS {
232 bail!("There can be at most {NUM_RECENT_BLOCKS} blocks in the map")
233 }
234
235 let mut last_height = 0;
237 for (i, current_height) in recents.keys().enumerate() {
238 if i == 0 && recents.len() < NUM_RECENT_BLOCKS && *current_height != last_height {
239 bail!("Ledgers under {NUM_RECENT_BLOCKS} blocks must have the first recent block at height 0")
240 }
241 if i > 0 && *current_height <= last_height {
242 bail!("Recent blocks must increment in height")
243 }
244 if i > 0 && *current_height - last_height != RECENT_INTERVAL {
245 bail!("Recent blocks must increment by {RECENT_INTERVAL}")
246 }
247 last_height = *current_height;
248 }
249
250 if last_height < NUM_RECENT_BLOCKS as u32 && recents.len().saturating_sub(1) as u32 != last_height {
252 bail!("As the last height is below {NUM_RECENT_BLOCKS}, the number of recent blocks must match the height")
253 }
254 if last_height >= NUM_RECENT_BLOCKS as u32 && recents.len() != NUM_RECENT_BLOCKS {
256 bail!("Number of recent blocks must match {NUM_RECENT_BLOCKS}")
257 }
258
259 if has_duplicates(recents.values()) {
261 bail!("Recent block hashes must be unique")
262 }
263
264 Ok(last_height)
265 }
266
267 fn check_block_checkpoints(checkpoints: &IndexMap<u32, N::BlockHash>) -> Result<u32> {
275 ensure!(!checkpoints.is_empty(), "There must be at least 1 block checkpoint");
277
278 let mut last_height = 0;
280 for (i, current_height) in checkpoints.keys().enumerate() {
281 if i == 0 && *current_height != 0 {
282 bail!("First block checkpoint must be at height 0")
283 }
284 if i > 0 && *current_height <= last_height {
285 bail!("Block checkpoints must increment in height")
286 }
287 if i > 0 && *current_height - last_height != CHECKPOINT_INTERVAL {
288 bail!("Block checkpoints must increment by {CHECKPOINT_INTERVAL}")
289 }
290 last_height = *current_height;
291 }
292
293 if has_duplicates(checkpoints.values()) {
295 bail!("Block checkpoints must be unique")
296 }
297
298 Ok(last_height)
299 }
300}
301
302impl<N: Network> FromBytes for BlockLocators<N> {
303 fn read_le<R: Read>(mut reader: R) -> IoResult<Self> {
304 let num_recents = u32::read_le(&mut reader)?;
306 let mut recents = IndexMap::new();
308 for _ in 0..num_recents {
309 let height = u32::read_le(&mut reader)?;
310 let hash = N::BlockHash::read_le(&mut reader)?;
311 recents.insert(height, hash);
312 }
313
314 let num_checkpoints = u32::read_le(&mut reader)?;
316 let mut checkpoints = IndexMap::new();
318 for _ in 0..num_checkpoints {
319 let height = u32::read_le(&mut reader)?;
320 let hash = N::BlockHash::read_le(&mut reader)?;
321 checkpoints.insert(height, hash);
322 }
323
324 Self::new(recents, checkpoints).map_err(error)
325 }
326}
327
328impl<N: Network> ToBytes for BlockLocators<N> {
329 fn write_le<W: Write>(&self, mut writer: W) -> IoResult<()> {
330 u32::try_from(self.recents.len()).map_err(error)?.write_le(&mut writer)?;
332 for (height, hash) in &self.recents {
334 height.write_le(&mut writer)?;
335 hash.write_le(&mut writer)?;
336 }
337
338 u32::try_from(self.checkpoints.len()).map_err(error)?.write_le(&mut writer)?;
340 for (height, hash) in &self.checkpoints {
342 height.write_le(&mut writer)?;
343 hash.write_le(&mut writer)?;
344 }
345 Ok(())
346 }
347}
348
349#[cfg(any(test, feature = "test"))]
350pub mod test_helpers {
351 use super::*;
352 use snarkvm::prelude::Field;
353
354 type CurrentNetwork = snarkvm::prelude::MainnetV0;
355
356 pub fn sample_block_locators(height: u32) -> BlockLocators<CurrentNetwork> {
358 let mut recents = IndexMap::new();
360 let recents_range = match height < NUM_RECENT_BLOCKS as u32 {
361 true => 0..=height,
362 false => (height - NUM_RECENT_BLOCKS as u32 + 1)..=height,
363 };
364 for i in recents_range {
365 recents.insert(i, (Field::<CurrentNetwork>::from_u32(i)).into());
366 }
367
368 let mut checkpoints = IndexMap::new();
370 for i in (0..=height).step_by(CHECKPOINT_INTERVAL as usize) {
371 checkpoints.insert(i, (Field::<CurrentNetwork>::from_u32(i)).into());
372 }
373
374 BlockLocators::new(recents, checkpoints).unwrap()
376 }
377
378 pub fn sample_block_locators_with_fork(height: u32, fork_height: u32) -> BlockLocators<CurrentNetwork> {
380 assert!(fork_height <= height, "Fork height must be less than or equal to the given height");
381 assert!(
382 height - fork_height < NUM_RECENT_BLOCKS as u32,
383 "Fork must be within NUM_RECENT_BLOCKS of the given height"
384 );
385
386 let mut recents = IndexMap::new();
388 let recents_range = match height < NUM_RECENT_BLOCKS as u32 {
389 true => 0..=height,
390 false => (height - NUM_RECENT_BLOCKS as u32 + 1)..=height,
391 };
392 for i in recents_range {
393 if i >= fork_height {
394 recents.insert(i, (-Field::<CurrentNetwork>::from_u32(i)).into());
395 } else {
396 recents.insert(i, (Field::<CurrentNetwork>::from_u32(i)).into());
397 }
398 }
399
400 let mut checkpoints = IndexMap::new();
402 for i in (0..=height).step_by(CHECKPOINT_INTERVAL as usize) {
403 checkpoints.insert(i, (Field::<CurrentNetwork>::from_u32(i)).into());
404 }
405
406 BlockLocators::new(recents, checkpoints).unwrap()
408 }
409
410 #[test]
412 fn test_sample_block_locators() {
413 for expected_height in 0..=100_001u32 {
414 println!("Testing height - {expected_height}");
415
416 let expected_num_checkpoints = (expected_height / CHECKPOINT_INTERVAL) + 1;
417 let expected_num_recents = match expected_height < NUM_RECENT_BLOCKS as u32 {
418 true => expected_height + 1,
419 false => NUM_RECENT_BLOCKS as u32,
420 };
421
422 let block_locators = sample_block_locators(expected_height);
423 assert_eq!(block_locators.checkpoints.len(), expected_num_checkpoints as usize);
424 assert_eq!(block_locators.recents.len(), expected_num_recents as usize);
425 assert_eq!(block_locators.latest_locator_height(), expected_height);
426 assert!(block_locators.is_valid());
427 }
428 }
429}
430
431#[cfg(test)]
432mod tests {
433 use super::*;
434 use snarkvm::prelude::Field;
435
436 use core::ops::Range;
437
438 type CurrentNetwork = snarkvm::prelude::MainnetV0;
439
440 fn check_is_valid(checkpoints: IndexMap<u32, <CurrentNetwork as Network>::BlockHash>, heights: Range<u32>) {
442 for height in heights {
443 let mut recents = IndexMap::new();
444 for i in 0..NUM_RECENT_BLOCKS as u32 {
445 recents.insert(height + i, (Field::<CurrentNetwork>::from_u32(height + i)).into());
446
447 let block_locators =
448 BlockLocators::<CurrentNetwork>::new_unchecked(recents.clone(), checkpoints.clone());
449 if height == 0 && recents.len() < NUM_RECENT_BLOCKS {
450 block_locators.ensure_is_valid().unwrap();
452 } else if recents.len() < NUM_RECENT_BLOCKS {
453 block_locators.ensure_is_valid().unwrap_err();
455 } else {
456 block_locators.ensure_is_valid().unwrap();
458 }
459 }
460 recents.insert(
462 height + NUM_RECENT_BLOCKS as u32,
463 (Field::<CurrentNetwork>::from_u32(height + NUM_RECENT_BLOCKS as u32)).into(),
464 );
465 let block_locators = BlockLocators::<CurrentNetwork>::new_unchecked(recents.clone(), checkpoints.clone());
466 block_locators.ensure_is_valid().unwrap_err();
467 }
468 }
469
470 fn check_is_consistent(
472 checkpoints: IndexMap<u32, <CurrentNetwork as Network>::BlockHash>,
473 heights: Range<u32>,
474 genesis_locators: BlockLocators<CurrentNetwork>,
475 second_locators: BlockLocators<CurrentNetwork>,
476 ) {
477 for height in heights {
478 let mut recents = IndexMap::new();
479 for i in 0..NUM_RECENT_BLOCKS as u32 {
480 recents.insert(height + i, (Field::<CurrentNetwork>::from_u32(height + i)).into());
481
482 let block_locators =
483 BlockLocators::<CurrentNetwork>::new_unchecked(recents.clone(), checkpoints.clone());
484 block_locators.ensure_is_consistent_with(&block_locators).unwrap();
485
486 let is_first_num_recents_blocks = height == 0 && recents.len() < NUM_RECENT_BLOCKS;
488 let is_num_recents_blocks = recents.len() == NUM_RECENT_BLOCKS;
489 if is_first_num_recents_blocks || is_num_recents_blocks {
490 genesis_locators.ensure_is_consistent_with(&block_locators).unwrap();
492 block_locators.ensure_is_consistent_with(&genesis_locators).unwrap();
493
494 second_locators.ensure_is_consistent_with(&block_locators).unwrap();
496 block_locators.ensure_is_consistent_with(&second_locators).unwrap();
497 }
498 }
499 }
500 }
501
502 #[test]
503 fn test_ensure_is_valid() {
504 let zero: <CurrentNetwork as Network>::BlockHash = (Field::<CurrentNetwork>::from_u32(0)).into();
505 let checkpoint_1: <CurrentNetwork as Network>::BlockHash =
506 (Field::<CurrentNetwork>::from_u32(CHECKPOINT_INTERVAL)).into();
507
508 for height in 0..10 {
510 let block_locators = test_helpers::sample_block_locators(height);
511 block_locators.ensure_is_valid().unwrap();
512 }
513
514 let checkpoints = IndexMap::from([(0, zero)]);
516 let mut recents = IndexMap::new();
517 for i in 0..NUM_RECENT_BLOCKS {
518 recents.insert(i as u32, (Field::<CurrentNetwork>::from_u32(i as u32)).into());
519 let block_locators = BlockLocators::<CurrentNetwork>::new(recents.clone(), checkpoints.clone()).unwrap();
520 block_locators.ensure_is_valid().unwrap();
521 }
522 recents.insert(NUM_RECENT_BLOCKS as u32, (Field::<CurrentNetwork>::from_u32(NUM_RECENT_BLOCKS as u32)).into());
524 let block_locators = BlockLocators::<CurrentNetwork>::new_unchecked(recents.clone(), checkpoints);
525 block_locators.ensure_is_valid().unwrap_err();
526
527 let checkpoints = IndexMap::from([(0, zero)]);
529 check_is_valid(checkpoints, 0..(CHECKPOINT_INTERVAL - NUM_RECENT_BLOCKS as u32));
530
531 let checkpoints = IndexMap::from([(0, zero), (CHECKPOINT_INTERVAL, checkpoint_1)]);
533 check_is_valid(
534 checkpoints,
535 (CHECKPOINT_INTERVAL - NUM_RECENT_BLOCKS as u32)..(CHECKPOINT_INTERVAL * 2 - NUM_RECENT_BLOCKS as u32),
536 );
537 }
538
539 #[test]
540 fn test_ensure_is_valid_fails() {
541 let zero: <CurrentNetwork as Network>::BlockHash = (Field::<CurrentNetwork>::from_u32(0)).into();
542 let one: <CurrentNetwork as Network>::BlockHash = (Field::<CurrentNetwork>::from_u32(1)).into();
543
544 let block_locators = BlockLocators::<CurrentNetwork>::new_unchecked(Default::default(), Default::default());
546 block_locators.ensure_is_valid().unwrap_err();
547
548 let block_locators =
550 BlockLocators::<CurrentNetwork>::new_unchecked(IndexMap::from([(0, zero)]), IndexMap::from([(0, one)]));
551 block_locators.ensure_is_valid().unwrap_err();
552
553 let block_locators =
555 BlockLocators::<CurrentNetwork>::new_unchecked(IndexMap::from([(0, one)]), IndexMap::from([(0, zero)]));
556 block_locators.ensure_is_valid().unwrap_err();
557
558 let block_locators = BlockLocators::<CurrentNetwork>::new_unchecked(
560 IndexMap::from([(0, one), (1, zero)]),
561 IndexMap::from([(0, zero)]),
562 );
563 block_locators.ensure_is_valid().unwrap_err();
564
565 let block_locators = BlockLocators::<CurrentNetwork>::new_unchecked(
567 IndexMap::from([(0, zero), (1, zero)]),
568 IndexMap::from([(0, zero)]),
569 );
570 block_locators.ensure_is_valid().unwrap_err();
571 }
572
573 #[test]
574 fn test_ensure_is_consistent_with() {
575 let zero: <CurrentNetwork as Network>::BlockHash = (Field::<CurrentNetwork>::from_u32(0)).into();
576 let one: <CurrentNetwork as Network>::BlockHash = (Field::<CurrentNetwork>::from_u32(1)).into();
577
578 let genesis_locators =
579 BlockLocators::<CurrentNetwork>::new_unchecked(IndexMap::from([(0, zero)]), IndexMap::from([(0, zero)]));
580 let second_locators = BlockLocators::<CurrentNetwork>::new_unchecked(
581 IndexMap::from([(0, zero), (1, one)]),
582 IndexMap::from([(0, zero)]),
583 );
584
585 genesis_locators.ensure_is_consistent_with(&genesis_locators).unwrap();
587
588 genesis_locators.ensure_is_consistent_with(&second_locators).unwrap();
590 second_locators.ensure_is_consistent_with(&genesis_locators).unwrap();
591
592 let checkpoints = IndexMap::from([(0, Default::default())]);
594 check_is_consistent(
595 checkpoints,
596 0..(CHECKPOINT_INTERVAL - NUM_RECENT_BLOCKS as u32),
597 genesis_locators.clone(),
598 second_locators.clone(),
599 );
600
601 let checkpoints = IndexMap::from([(0, Default::default()), (CHECKPOINT_INTERVAL, Default::default())]);
603 check_is_consistent(
604 checkpoints,
605 (CHECKPOINT_INTERVAL - NUM_RECENT_BLOCKS as u32)..(CHECKPOINT_INTERVAL * 2 - NUM_RECENT_BLOCKS as u32),
606 genesis_locators,
607 second_locators,
608 );
609 }
610
611 #[test]
612 fn test_ensure_is_consistent_with_fails() {
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(IndexMap::from([(0, zero)]), IndexMap::from([(0, zero)])).unwrap();
618 let second_locators =
619 BlockLocators::<CurrentNetwork>::new(IndexMap::from([(0, zero), (1, one)]), IndexMap::from([(0, zero)]))
620 .unwrap();
621
622 let wrong_genesis_locators =
623 BlockLocators::<CurrentNetwork>::new(IndexMap::from([(0, one)]), IndexMap::from([(0, one)])).unwrap();
624 let wrong_second_locators =
625 BlockLocators::<CurrentNetwork>::new(IndexMap::from([(0, one), (1, zero)]), IndexMap::from([(0, one)]))
626 .unwrap();
627
628 genesis_locators.ensure_is_consistent_with(&wrong_genesis_locators).unwrap_err();
629 wrong_genesis_locators.ensure_is_consistent_with(&genesis_locators).unwrap_err();
630
631 genesis_locators.ensure_is_consistent_with(&wrong_second_locators).unwrap_err();
632 wrong_second_locators.ensure_is_consistent_with(&genesis_locators).unwrap_err();
633
634 second_locators.ensure_is_consistent_with(&wrong_genesis_locators).unwrap_err();
635 wrong_genesis_locators.ensure_is_consistent_with(&second_locators).unwrap_err();
636
637 second_locators.ensure_is_consistent_with(&wrong_second_locators).unwrap_err();
638 wrong_second_locators.ensure_is_consistent_with(&second_locators).unwrap_err();
639 }
640}