#[cfg(test)]
mod tests;
use crate::certificate::CertifiedLedgerTip;
use candid::{CandidType, Nat};
use icrc_ledger_types::icrc::generic_value::Value;
use serde::Deserialize;
use thiserror::Error;
#[derive(CandidType, Clone, Debug, Deserialize)]
pub struct LedgerBlock {
pub id: Nat,
pub block: Value,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct BlockEvidenceLimits {
pub blocks: usize,
pub nodes: usize,
pub bytes: usize,
pub depth: u8,
}
#[derive(Debug, Error, Eq, PartialEq)]
pub enum BlockEvidenceError {
#[error("block evidence exceeds its work budget")]
BudgetExceeded,
#[error("block evidence does not match the certified contiguous chain")]
ChainMismatch,
#[error("block value is unsupported by the canonical ICRC hasher")]
UnsupportedValue,
}
#[derive(Debug)]
pub struct BoundedLedgerBlocks<'a> {
blocks: &'a [LedgerBlock],
}
impl<'a> BoundedLedgerBlocks<'a> {
pub fn admit(
blocks: &'a [LedgerBlock],
limits: BlockEvidenceLimits,
) -> Result<Self, BlockEvidenceError> {
if blocks.len() > limits.blocks {
return Err(BlockEvidenceError::BudgetExceeded);
}
let mut nodes = limits.nodes;
let mut bytes = limits.bytes;
for block in blocks {
if block.id.0.bits() > 128 {
return Err(BlockEvidenceError::ChainMismatch);
}
check_value(&block.block, limits.depth, &mut nodes, &mut bytes)?;
}
Ok(Self { blocks })
}
pub fn verify_chain(
&self,
tip: &CertifiedLedgerTip,
target: u128,
) -> Result<&'a Value, BlockEvidenceError> {
let expected_len = tip.index.checked_sub(target).and_then(|n| n.checked_add(1));
if self.blocks.is_empty() || expected_len != Some(self.blocks.len() as u128) {
return Err(BlockEvidenceError::ChainMismatch);
}
let mut expected_hash = tip.hash;
for (offset, block) in self.blocks.iter().enumerate() {
let expected_index = tip.index - offset as u128;
if block.id != expected_index || block.block.hash() != expected_hash {
return Err(BlockEvidenceError::ChainMismatch);
}
let Value::Map(fields) = &block.block else {
return Err(BlockEvidenceError::ChainMismatch);
};
if expected_index == 0 {
if fields.contains_key("phash") {
return Err(BlockEvidenceError::ChainMismatch);
}
} else {
let Some(Value::Blob(bytes)) = fields.get("phash") else {
return Err(BlockEvidenceError::ChainMismatch);
};
expected_hash = bytes
.as_ref()
.try_into()
.map_err(|_| BlockEvidenceError::ChainMismatch)?;
}
}
self.blocks
.last()
.map(|block| &block.block)
.ok_or(BlockEvidenceError::ChainMismatch)
}
}
fn check_value(
value: &Value,
depth: u8,
nodes: &mut usize,
bytes: &mut usize,
) -> Result<(), BlockEvidenceError> {
*nodes = nodes
.checked_sub(1)
.ok_or(BlockEvidenceError::BudgetExceeded)?;
let depth = depth
.checked_sub(1)
.ok_or(BlockEvidenceError::BudgetExceeded)?;
let scalar_bytes = match value {
Value::Blob(value) => value.len(),
Value::Text(value) => value.len(),
Value::Nat64(_) => size_of::<u64>(),
Value::Nat(value) => usize::try_from(value.0.bits().div_ceil(8))
.map_err(|_| BlockEvidenceError::BudgetExceeded)?,
Value::Int(value) => {
i128::try_from(&value.0).map_err(|_| BlockEvidenceError::UnsupportedValue)?;
usize::try_from(value.0.bits().div_ceil(8))
.map_err(|_| BlockEvidenceError::BudgetExceeded)?
}
Value::Array(values) => {
for value in values {
check_value(value, depth, nodes, bytes)?;
}
0
}
Value::Map(values) => {
for (key, value) in values {
*bytes = bytes
.checked_sub(key.len())
.ok_or(BlockEvidenceError::BudgetExceeded)?;
check_value(value, depth, nodes, bytes)?;
}
0
}
};
*bytes = bytes
.checked_sub(scalar_bytes)
.ok_or(BlockEvidenceError::BudgetExceeded)?;
Ok(())
}