use snarkvm::prelude::{has_duplicates, Network};
use anyhow::{bail, ensure, Result};
use indexmap::{indexmap, IndexMap};
use serde::{Deserialize, Serialize};
use std::collections::{btree_map::IntoIter, BTreeMap};
pub const NUM_RECENTS: usize = 100; pub const RECENT_INTERVAL: u32 = 1; pub const CHECKPOINT_INTERVAL: u32 = 10_000; #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct BlockLocators<N: Network> {
pub recents: IndexMap<u32, N::BlockHash>,
pub checkpoints: IndexMap<u32, N::BlockHash>,
}
impl<N: Network> IntoIterator for BlockLocators<N> {
type IntoIter = IntoIter<u32, N::BlockHash>;
type Item = (u32, N::BlockHash);
fn into_iter(self) -> Self::IntoIter {
BTreeMap::from_iter(self.checkpoints.into_iter().chain(self.recents.into_iter())).into_iter()
}
}
impl<N: Network> BlockLocators<N> {
pub fn new(recents: IndexMap<u32, N::BlockHash>, checkpoints: IndexMap<u32, N::BlockHash>) -> Self {
Self { recents, checkpoints }
}
pub fn new_genesis(genesis_hash: N::BlockHash) -> Self {
Self { recents: indexmap![0 => genesis_hash], checkpoints: indexmap![0 => genesis_hash] }
}
pub fn latest_locator_height(&self) -> u32 {
self.recents.keys().last().copied().unwrap_or_default()
}
pub fn get_hash(&self, height: u32) -> Option<N::BlockHash> {
self.recents.get(&height).copied().or_else(|| self.checkpoints.get(&height).copied())
}
pub fn is_valid(&self) -> bool {
if let Err(error) = self.ensure_is_valid() {
warn!("Block locators are invalid: {error}");
return false;
}
true
}
pub fn is_consistent_with(&self, other: &Self) -> bool {
if let Err(error) = self.ensure_is_consistent_with(other) {
warn!("Inconsistent block locators: {error}");
return false;
}
true
}
pub fn ensure_is_valid(&self) -> Result<()> {
Self::check_block_locators(&self.recents, &self.checkpoints)
}
pub fn ensure_is_consistent_with(&self, other: &Self) -> Result<()> {
Self::check_consistent_block_locators(self, other)
}
}
impl<N: Network> BlockLocators<N> {
pub fn check_consistent_block_locators(
old_locators: &BlockLocators<N>,
new_locators: &BlockLocators<N>,
) -> Result<()> {
for (height, hash) in new_locators.recents.iter() {
if let Some(recent_hash) = old_locators.recents.get(height) {
if recent_hash != hash {
bail!("Recent block hash mismatch at height {height}")
}
}
}
for (height, hash) in new_locators.checkpoints.iter() {
if let Some(checkpoint_hash) = old_locators.checkpoints.get(height) {
if checkpoint_hash != hash {
bail!("Block checkpoint hash mismatch for height {height}")
}
}
}
Ok(())
}
pub fn check_block_locators(
recents: &IndexMap<u32, N::BlockHash>,
checkpoints: &IndexMap<u32, N::BlockHash>,
) -> Result<()> {
let last_recent_height = Self::check_recent_blocks(recents)?;
let last_checkpoint_height = Self::check_block_checkpoints(checkpoints)?;
let threshold = last_checkpoint_height.saturating_sub(NUM_RECENTS as u32);
if last_recent_height < threshold {
bail!("Recent height ({last_recent_height}) cannot be below checkpoint threshold ({threshold})")
}
if last_recent_height < NUM_RECENTS as u32
&& recents.get(&0).copied().unwrap_or_default() != checkpoints.get(&0).copied().unwrap_or_default()
{
bail!("Recent genesis hash and checkpoint genesis hash mismatch at height {last_recent_height}")
}
if let Some(last_checkpoint_hash) = checkpoints.get(&last_recent_height) {
if let Some(last_recent_hash) = recents.get(&last_recent_height) {
if last_checkpoint_hash != last_recent_hash {
bail!("Recent block hash and checkpoint hash mismatch at height {last_recent_height}")
}
}
}
Ok(())
}
fn check_recent_blocks(recents: &IndexMap<u32, N::BlockHash>) -> Result<u32> {
if recents.is_empty() {
bail!("There must be at least 1 recent block")
}
if recents.len() > NUM_RECENTS {
bail!("There can be at most {NUM_RECENTS} blocks in the map")
}
let mut last_height = 0;
for (i, current_height) in recents.keys().enumerate() {
if i == 0 && recents.len() < NUM_RECENTS && *current_height != last_height {
bail!("Ledgers under {NUM_RECENTS} blocks must have the first recent block at height 0")
}
if i > 0 && *current_height <= last_height {
bail!("Recent blocks must increment in height")
}
if i > 0 && *current_height - last_height != RECENT_INTERVAL {
bail!("Recent blocks must increment by {RECENT_INTERVAL}")
}
last_height = *current_height;
}
if last_height < NUM_RECENTS as u32 && recents.len().saturating_sub(1) as u32 != last_height {
bail!("As the last height is below {NUM_RECENTS}, the number of recent blocks must match the height")
}
if last_height >= NUM_RECENTS as u32 && recents.len() != NUM_RECENTS {
bail!("Number of recent blocks must match {NUM_RECENTS}")
}
if has_duplicates(recents.values()) {
bail!("Recent block hashes must be unique")
}
Ok(last_height)
}
fn check_block_checkpoints(checkpoints: &IndexMap<u32, N::BlockHash>) -> Result<u32> {
ensure!(!checkpoints.is_empty(), "There must be at least 1 block checkpoint");
let mut last_height = 0;
for (i, current_height) in checkpoints.keys().enumerate() {
if i == 0 && *current_height != 0 {
bail!("First block checkpoint must be at height 0")
}
if i > 0 && *current_height <= last_height {
bail!("Block checkpoints must increment in height")
}
if i > 0 && *current_height - last_height != CHECKPOINT_INTERVAL {
bail!("Block checkpoints must increment by {CHECKPOINT_INTERVAL}")
}
last_height = *current_height;
}
if has_duplicates(checkpoints.values()) {
bail!("Block checkpoints must be unique")
}
Ok(last_height)
}
}
#[cfg(any(test, feature = "test"))]
pub mod test_helpers {
use super::*;
use snarkvm::prelude::Field;
type CurrentNetwork = snarkvm::prelude::Testnet3;
pub fn sample_block_locators(height: u32) -> BlockLocators<CurrentNetwork> {
let mut recents = IndexMap::new();
let recents_range = match height < NUM_RECENTS as u32 {
true => 0..=height,
false => (height - NUM_RECENTS as u32 + 1)..=height,
};
for i in recents_range {
recents.insert(i, (Field::<CurrentNetwork>::from_u32(i)).into());
}
let mut checkpoints = IndexMap::new();
for i in (0..=height).step_by(CHECKPOINT_INTERVAL as usize) {
checkpoints.insert(i, (Field::<CurrentNetwork>::from_u32(i)).into());
}
BlockLocators::new(recents, checkpoints)
}
pub fn sample_block_locators_with_fork(height: u32, fork_height: u32) -> BlockLocators<CurrentNetwork> {
assert!(fork_height <= height, "Fork height must be less than or equal to the given height");
assert!(height - fork_height < NUM_RECENTS as u32, "Fork must be within NUM_RECENTS of the given height");
let mut recents = IndexMap::new();
let recents_range = match height < NUM_RECENTS as u32 {
true => 0..=height,
false => (height - NUM_RECENTS as u32 + 1)..=height,
};
for i in recents_range {
if i >= fork_height {
recents.insert(i, (-Field::<CurrentNetwork>::from_u32(i)).into());
} else {
recents.insert(i, (Field::<CurrentNetwork>::from_u32(i)).into());
}
}
let mut checkpoints = IndexMap::new();
for i in (0..=height).step_by(CHECKPOINT_INTERVAL as usize) {
checkpoints.insert(i, (Field::<CurrentNetwork>::from_u32(i)).into());
}
BlockLocators::new(recents, checkpoints)
}
#[test]
fn test_sample_block_locators() {
for expected_height in 0..=100_001u32 {
println!("Testing height - {expected_height}");
let expected_num_checkpoints = (expected_height / CHECKPOINT_INTERVAL) + 1;
let expected_num_recents = match expected_height < NUM_RECENTS as u32 {
true => expected_height + 1,
false => NUM_RECENTS as u32,
};
let block_locators = sample_block_locators(expected_height);
assert_eq!(block_locators.checkpoints.len(), expected_num_checkpoints as usize);
assert_eq!(block_locators.recents.len(), expected_num_recents as usize);
assert_eq!(block_locators.latest_locator_height(), expected_height);
assert!(block_locators.is_valid());
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use snarkvm::prelude::Field;
use core::ops::Range;
type CurrentNetwork = snarkvm::prelude::Testnet3;
fn check_is_valid(checkpoints: IndexMap<u32, <CurrentNetwork as Network>::BlockHash>, heights: Range<u32>) {
for height in heights {
let mut recents = IndexMap::new();
for i in 0..NUM_RECENTS as u32 {
recents.insert(height + i, (Field::<CurrentNetwork>::from_u32(height + i)).into());
let block_locators = BlockLocators::<CurrentNetwork>::new(recents.clone(), checkpoints.clone());
if height == 0 && recents.len() < NUM_RECENTS {
block_locators.ensure_is_valid().unwrap();
} else if recents.len() < NUM_RECENTS {
block_locators.ensure_is_valid().unwrap_err();
} else {
block_locators.ensure_is_valid().unwrap();
}
}
recents.insert(
height + NUM_RECENTS as u32,
(Field::<CurrentNetwork>::from_u32(height + NUM_RECENTS as u32)).into(),
);
let block_locators = BlockLocators::<CurrentNetwork>::new(recents.clone(), checkpoints.clone());
block_locators.ensure_is_valid().unwrap_err();
}
}
fn check_is_consistent(
checkpoints: IndexMap<u32, <CurrentNetwork as Network>::BlockHash>,
heights: Range<u32>,
genesis_locators: BlockLocators<CurrentNetwork>,
second_locators: BlockLocators<CurrentNetwork>,
) {
for height in heights {
let mut recents = IndexMap::new();
for i in 0..NUM_RECENTS as u32 {
recents.insert(height + i, (Field::<CurrentNetwork>::from_u32(height + i)).into());
let block_locators = BlockLocators::<CurrentNetwork>::new(recents.clone(), checkpoints.clone());
block_locators.ensure_is_consistent_with(&block_locators).unwrap();
let is_first_num_recents_blocks = height == 0 && recents.len() < NUM_RECENTS;
let is_num_recents_blocks = recents.len() == NUM_RECENTS;
if is_first_num_recents_blocks || is_num_recents_blocks {
genesis_locators.ensure_is_consistent_with(&block_locators).unwrap();
block_locators.ensure_is_consistent_with(&genesis_locators).unwrap();
second_locators.ensure_is_consistent_with(&block_locators).unwrap();
block_locators.ensure_is_consistent_with(&second_locators).unwrap();
}
}
}
}
#[test]
fn test_ensure_is_valid() {
let zero: <CurrentNetwork as Network>::BlockHash = (Field::<CurrentNetwork>::from_u32(0)).into();
let checkpoint_1: <CurrentNetwork as Network>::BlockHash =
(Field::<CurrentNetwork>::from_u32(CHECKPOINT_INTERVAL)).into();
for height in 0..10 {
let block_locators = test_helpers::sample_block_locators(height);
block_locators.ensure_is_valid().unwrap();
}
let checkpoints = IndexMap::from([(0, zero)]);
let mut recents = IndexMap::new();
for i in 0..NUM_RECENTS {
recents.insert(i as u32, (Field::<CurrentNetwork>::from_u32(i as u32)).into());
let block_locators = BlockLocators::<CurrentNetwork>::new(recents.clone(), checkpoints.clone());
block_locators.ensure_is_valid().unwrap();
}
recents.insert(NUM_RECENTS as u32, (Field::<CurrentNetwork>::from_u32(NUM_RECENTS as u32)).into());
let block_locators = BlockLocators::<CurrentNetwork>::new(recents.clone(), checkpoints);
block_locators.ensure_is_valid().unwrap_err();
let checkpoints = IndexMap::from([(0, zero)]);
check_is_valid(checkpoints, 0..(CHECKPOINT_INTERVAL - NUM_RECENTS as u32));
let checkpoints = IndexMap::from([(0, zero), (CHECKPOINT_INTERVAL, checkpoint_1)]);
check_is_valid(
checkpoints,
(CHECKPOINT_INTERVAL - NUM_RECENTS as u32)..(CHECKPOINT_INTERVAL * 2 - NUM_RECENTS as u32),
);
}
#[test]
fn test_ensure_is_valid_fails() {
let zero: <CurrentNetwork as Network>::BlockHash = (Field::<CurrentNetwork>::from_u32(0)).into();
let one: <CurrentNetwork as Network>::BlockHash = (Field::<CurrentNetwork>::from_u32(1)).into();
let block_locators = BlockLocators::<CurrentNetwork>::new(Default::default(), Default::default());
block_locators.ensure_is_valid().unwrap_err();
let block_locators =
BlockLocators::<CurrentNetwork>::new(IndexMap::from([(0, zero)]), IndexMap::from([(0, one)]));
block_locators.ensure_is_valid().unwrap_err();
let block_locators =
BlockLocators::<CurrentNetwork>::new(IndexMap::from([(0, one)]), IndexMap::from([(0, zero)]));
block_locators.ensure_is_valid().unwrap_err();
let block_locators =
BlockLocators::<CurrentNetwork>::new(IndexMap::from([(0, one), (1, zero)]), IndexMap::from([(0, zero)]));
block_locators.ensure_is_valid().unwrap_err();
let block_locators =
BlockLocators::<CurrentNetwork>::new(IndexMap::from([(0, zero), (1, zero)]), IndexMap::from([(0, zero)]));
block_locators.ensure_is_valid().unwrap_err();
}
#[test]
fn test_ensure_is_consistent_with() {
let zero: <CurrentNetwork as Network>::BlockHash = (Field::<CurrentNetwork>::from_u32(0)).into();
let one: <CurrentNetwork as Network>::BlockHash = (Field::<CurrentNetwork>::from_u32(1)).into();
let genesis_locators =
BlockLocators::<CurrentNetwork>::new(IndexMap::from([(0, zero)]), IndexMap::from([(0, zero)]));
let second_locators =
BlockLocators::<CurrentNetwork>::new(IndexMap::from([(0, zero), (1, one)]), IndexMap::from([(0, zero)]));
genesis_locators.ensure_is_consistent_with(&genesis_locators).unwrap();
genesis_locators.ensure_is_consistent_with(&second_locators).unwrap();
second_locators.ensure_is_consistent_with(&genesis_locators).unwrap();
let checkpoints = IndexMap::from([(0, Default::default())]);
check_is_consistent(
checkpoints,
0..(CHECKPOINT_INTERVAL - NUM_RECENTS as u32),
genesis_locators.clone(),
second_locators.clone(),
);
let checkpoints = IndexMap::from([(0, Default::default()), (CHECKPOINT_INTERVAL, Default::default())]);
check_is_consistent(
checkpoints,
(CHECKPOINT_INTERVAL - NUM_RECENTS as u32)..(CHECKPOINT_INTERVAL * 2 - NUM_RECENTS as u32),
genesis_locators,
second_locators,
);
}
#[test]
fn test_ensure_is_consistent_with_fails() {
let zero: <CurrentNetwork as Network>::BlockHash = (Field::<CurrentNetwork>::from_u32(0)).into();
let one: <CurrentNetwork as Network>::BlockHash = (Field::<CurrentNetwork>::from_u32(1)).into();
let genesis_locators =
BlockLocators::<CurrentNetwork>::new(IndexMap::from([(0, zero)]), IndexMap::from([(0, zero)]));
let second_locators =
BlockLocators::<CurrentNetwork>::new(IndexMap::from([(0, zero), (1, one)]), IndexMap::from([(0, zero)]));
let wrong_genesis_locators =
BlockLocators::<CurrentNetwork>::new(IndexMap::from([(0, one)]), IndexMap::from([(0, one)]));
let wrong_second_locators =
BlockLocators::<CurrentNetwork>::new(IndexMap::from([(0, one), (1, zero)]), IndexMap::from([(0, one)]));
genesis_locators.ensure_is_consistent_with(&wrong_genesis_locators).unwrap_err();
wrong_genesis_locators.ensure_is_consistent_with(&genesis_locators).unwrap_err();
genesis_locators.ensure_is_consistent_with(&wrong_second_locators).unwrap_err();
wrong_second_locators.ensure_is_consistent_with(&genesis_locators).unwrap_err();
second_locators.ensure_is_consistent_with(&wrong_genesis_locators).unwrap_err();
wrong_genesis_locators.ensure_is_consistent_with(&second_locators).unwrap_err();
second_locators.ensure_is_consistent_with(&wrong_second_locators).unwrap_err();
wrong_second_locators.ensure_is_consistent_with(&second_locators).unwrap_err();
}
}