use std::collections::BTreeMap;
use crate::error::{Error, Result};
use crate::hasher::Hasher;
use crate::null::NullTable;
use crate::storage::Storage;
#[derive(Debug)]
struct AlgState {
hasher: Box<dyn Hasher>,
epochs: Vec<(u64, u64)>,
stack: Vec<Vec<u8>>,
null_table: NullTable,
}
impl AlgState {
fn is_active(&self) -> bool {
self.epochs.last().is_some_and(|&(_, end)| end == u64::MAX)
}
fn is_active_at(&self, i: u64) -> bool {
self.epochs
.iter()
.any(|&(start, end)| start <= i && i < end)
}
fn tree_size(&self, global_size: u64) -> u64 {
if self.is_active() {
global_size
} else {
self.epochs.last().map_or(0, |&(_, end)| end)
}
}
fn first_activation(&self) -> u64 {
self.epochs.first().map_or(0, |&(start, _)| start)
}
fn active_range(&self, lo: u64, hi: u64) -> bool {
self.epochs
.iter()
.any(|&(start, end)| start < hi && end > lo)
}
}
fn count_trailing_ones(n: u64) -> u32 {
(!n).trailing_zeros()
}
pub(crate) fn serialize_epochs(epochs: &[(u64, u64)]) -> Vec<u8> {
let mut bytes = Vec::with_capacity(8 + epochs.len() * 16);
bytes.extend_from_slice(&(epochs.len() as u64).to_be_bytes());
for &(start, end) in epochs {
bytes.extend_from_slice(&start.to_be_bytes());
bytes.extend_from_slice(&end.to_be_bytes());
}
bytes
}
fn null_prefix_peaks(hasher: &dyn Hasher, null_table: &mut NullTable, k: u64) -> Vec<Vec<u8>> {
if k == 0 {
return Vec::new();
}
let mut peaks = Vec::new();
let bit_width = 64 - k.leading_zeros();
for bit in (0..bit_width).rev() {
if k & (1 << bit) != 0 {
let null_root = null_table.get(hasher, bit as usize).to_vec();
peaks.push(null_root);
}
}
peaks
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AlgorithmInfo {
pub id: u64,
pub root: Vec<u8>,
pub activation_index: u64,
pub deactivation_index: Option<u64>,
pub tree_size: u64,
pub epochs: Vec<(u64, Option<u64>)>,
pub manifest_hash: Vec<u8>,
}
#[derive(Debug)]
pub struct Log<S: Storage> {
storage: S,
algs: BTreeMap<u64, AlgState>,
}
impl<S: Storage> Log<S> {
pub fn new(storage: S) -> Self {
Self {
storage,
algs: BTreeMap::new(),
}
}
pub fn from_storage(storage: S, hashers: Vec<(u64, Box<dyn Hasher>)>) -> Result<Self> {
let metas = storage
.load_algorithm_metas()
.map_err(|e| Error::Storage(Box::new(e)))?;
let mut hasher_map: BTreeMap<u64, Box<dyn Hasher>> = hashers.into_iter().collect();
for &(alg_id, _) in &metas {
if !hasher_map.contains_key(&alg_id) {
return Err(Error::OrphanedMetadata(alg_id));
}
}
let meta_ids: std::collections::BTreeSet<u64> = metas.iter().map(|&(id, _)| id).collect();
for &alg_id in hasher_map.keys() {
if !meta_ids.contains(&alg_id) {
return Err(Error::UnknownMetadata(alg_id));
}
}
let global_size = storage.len();
let mut log = Self {
storage,
algs: BTreeMap::new(),
};
for (alg_id, epochs) in metas {
let hasher = hasher_map.remove(&alg_id).expect("validated above");
let mut null_table = NullTable::new(hasher.as_ref());
let is_active = epochs.last().is_some_and(|&(_, end)| end == u64::MAX);
let tree_size = if is_active {
global_size
} else {
epochs.last().map_or(0, |&(_, end)| end)
};
if tree_size > 0 {
let max_height = (64 - tree_size.leading_zeros()) as usize;
null_table.ensure_height(hasher.as_ref(), max_height);
}
let state = AlgState {
hasher,
epochs: epochs.clone(),
stack: Vec::new(),
null_table,
};
let stack = Self::reconstruct_frontier(&log, &state, alg_id, tree_size)?;
log.algs.insert(alg_id, AlgState { stack, ..state });
}
Ok(log)
}
fn reconstruct_frontier(
log: &Self,
state: &AlgState,
alg_id: u64,
tree_size: u64,
) -> Result<Vec<Vec<u8>>> {
if tree_size == 0 {
return Ok(Vec::new());
}
let mut stack = Vec::new();
let bit_width = 64 - tree_size.leading_zeros();
let mut pos = 0u64;
for bit in (0..bit_width).rev() {
if tree_size & (1 << bit) != 0 {
let subtree_size = 1u64 << bit;
let root = log.subtree_root(state, alg_id, pos, pos + subtree_size)?;
stack.push(root);
pos += subtree_size;
}
}
Ok(stack)
}
#[must_use]
pub fn size(&self) -> u64 {
self.storage.len()
}
pub fn into_storage(self) -> S {
self.storage
}
pub fn add_algorithm(&mut self, alg_id: u64, hasher: Box<dyn Hasher>) -> Result<()> {
if self.algs.contains_key(&alg_id) {
return Err(Error::DuplicateAlgorithm(alg_id));
}
let activation = self.size();
let epochs = vec![(activation, u64::MAX)];
self.storage
.store_algorithm_meta(alg_id, &epochs)
.map_err(|e| Error::Storage(Box::new(e)))?;
let mut null_table = NullTable::new(hasher.as_ref());
let stack = null_prefix_peaks(hasher.as_ref(), &mut null_table, activation);
if activation > 0 {
let max_height = (64 - activation.leading_zeros()) as usize;
null_table.ensure_height(hasher.as_ref(), max_height);
}
self.algs.insert(
alg_id,
AlgState {
hasher,
epochs,
stack,
null_table,
},
);
Ok(())
}
pub fn remove_algorithm(&mut self, alg_id: u64) -> Result<()> {
let current_size = self.size();
let state = self
.algs
.get_mut(&alg_id)
.ok_or(Error::UnknownAlgorithm(alg_id))?;
if !state.is_active() {
return Err(Error::FrozenAlgorithm(alg_id));
}
let mut new_epochs = state.epochs.clone();
if let Some(last) = new_epochs.last_mut() {
last.1 = current_size;
}
self.storage
.store_algorithm_meta(alg_id, &new_epochs)
.map_err(|e| Error::Storage(Box::new(e)))?;
self.algs.get_mut(&alg_id).unwrap().epochs = new_epochs;
Ok(())
}
pub fn resume_algorithm(&mut self, alg_id: u64) -> Result<()> {
let current_size = self.storage.len();
let state = self
.algs
.get(&alg_id)
.ok_or(Error::UnknownAlgorithm(alg_id))?;
if state.is_active() {
return Err(Error::AlgorithmActive(alg_id));
}
let deactivation = state.epochs.last().unwrap().1;
let gap = current_size - deactivation;
let mut new_epochs = state.epochs.clone();
new_epochs.push((current_size, u64::MAX));
self.storage
.store_algorithm_meta(alg_id, &new_epochs)
.map_err(|e| Error::Storage(Box::new(e)))?;
if gap > 0 {
{
let state = self.algs.get_mut(&alg_id).unwrap();
let max_height = (64 - current_size.leading_zeros()) as usize;
state
.null_table
.ensure_height(state.hasher.as_ref(), max_height);
}
let state = &self.algs[&alg_id];
let new_stack = Self::reconstruct_frontier(self, state, alg_id, current_size)?;
self.algs.get_mut(&alg_id).unwrap().stack = new_stack;
}
{
let state = self.algs.get_mut(&alg_id).unwrap();
let max_height = (64 - current_size.leading_zeros()) as usize;
state
.null_table
.ensure_height(state.hasher.as_ref(), max_height);
}
self.algs.get_mut(&alg_id).unwrap().epochs = new_epochs;
Ok(())
}
#[must_use]
pub fn is_active(&self, alg_id: u64) -> Option<bool> {
self.algs.get(&alg_id).map(|s| s.is_active())
}
pub fn tree_size(&self, alg_id: u64) -> Result<u64> {
self.algs
.get(&alg_id)
.map(|s| s.tree_size(self.size()))
.ok_or(Error::UnknownAlgorithm(alg_id))
}
pub fn append(&mut self, data: &[u8]) -> Result<u64> {
if !self.algs.values().any(|s| s.is_active()) {
return Err(Error::NoActiveAlgorithms);
}
let index = self.size();
let merge_count = count_trailing_ones(index);
self.storage
.store_leaf(index, data)
.map_err(|e| Error::Storage(Box::new(e)))?;
for (&alg_id, state) in self.algs.iter_mut() {
if !state.is_active() {
continue;
}
let digest = if state.is_active_at(index) {
state.hasher.leaf(data)
} else {
state.null_table.leaf_null().to_vec()
};
state.stack.push(digest);
for j in 1..=merge_count {
let right = state.stack.pop().expect("stack underflow in merge");
let left = state.stack.pop().expect("stack underflow in merge");
let parent = state.hasher.node(&left, &right);
let height = j as usize;
let left_pos = index + 1 - (1u64 << height);
self.storage
.store_node(alg_id, left_pos, height, &parent)
.map_err(|e| Error::Storage(Box::new(e)))?;
state.stack.push(parent);
}
let tree_size = index + 1;
let max_height = (64 - tree_size.leading_zeros()) as usize;
state
.null_table
.ensure_height(state.hasher.as_ref(), max_height);
}
Ok(index)
}
pub fn root(&self, alg_id: u64) -> Result<Vec<u8>> {
let state = self
.algs
.get(&alg_id)
.ok_or(Error::UnknownAlgorithm(alg_id))?;
if state.stack.is_empty() {
return Ok(state.hasher.empty());
}
let mut iter = state.stack.iter().rev();
let first = iter
.next()
.expect("non-empty stack has at least one element")
.clone();
let root = iter.fold(first, |acc, left| state.hasher.node(left, &acc));
Ok(root)
}
pub fn algorithm_ids(&self) -> impl Iterator<Item = u64> + '_ {
self.algs.keys().copied()
}
pub fn activation_index(&self, alg_id: u64) -> Result<u64> {
self.algs
.get(&alg_id)
.map(|s| s.first_activation())
.ok_or(Error::UnknownAlgorithm(alg_id))
}
pub fn deactivation_index(&self, alg_id: u64) -> Result<Option<u64>> {
self.algs
.get(&alg_id)
.map(|s| {
if s.is_active() {
None
} else {
s.epochs.last().map(|&(_, end)| end)
}
})
.ok_or(Error::UnknownAlgorithm(alg_id))
}
pub fn epochs(&self, alg_id: u64) -> Result<Vec<(u64, Option<u64>)>> {
self.algs
.get(&alg_id)
.map(|s| {
s.epochs
.iter()
.map(|&(start, end)| {
if end == u64::MAX {
(start, None)
} else {
(start, Some(end))
}
})
.collect()
})
.ok_or(Error::UnknownAlgorithm(alg_id))
}
pub fn algorithms(&self) -> Vec<AlgorithmInfo> {
let global_size = self.size();
self.algs
.iter()
.map(|(&id, state)| {
let ts = state.tree_size(global_size);
let root = if state.stack.is_empty() {
state.hasher.empty()
} else {
let mut iter = state.stack.iter().rev();
let first = iter
.next()
.expect("non-empty stack has at least one element")
.clone();
iter.fold(first, |acc, left| state.hasher.node(left, &acc))
};
let serialized = serialize_epochs(&state.epochs);
let manifest_hash = state.hasher.hash(&serialized);
AlgorithmInfo {
id,
root,
activation_index: state.epochs.first().map(|e| e.0).unwrap_or(0),
deactivation_index: state
.epochs
.last()
.and_then(|e| if e.1 == u64::MAX { None } else { Some(e.1) }),
tree_size: ts,
epochs: state
.epochs
.iter()
.map(|&(start, end)| {
(start, if end == u64::MAX { None } else { Some(end) })
})
.collect(),
manifest_hash,
}
})
.collect()
}
#[cfg(test)]
fn stack_len(&self, alg_id: u64) -> Option<usize> {
self.algs.get(&alg_id).map(|s| s.stack.len())
}
#[cfg(test)]
pub(crate) fn test_subtree_root(&self, alg_id: u64, lo: u64, hi: u64) -> Result<Vec<u8>> {
let state = self
.algs
.get(&alg_id)
.ok_or(Error::UnknownAlgorithm(alg_id))?;
self.subtree_root(state, alg_id, lo, hi)
}
fn subtree_root(&self, state: &AlgState, alg_id: u64, lo: u64, hi: u64) -> Result<Vec<u8>> {
let size = hi - lo;
if size == 0 {
return Ok(state.hasher.empty());
}
if size == 1 {
if state.is_active_at(lo) {
let data = self
.storage
.get_leaf(lo)
.map_err(|e| Error::Storage(Box::new(e)))?;
return Ok(state.hasher.leaf(&data));
}
return Ok(state.null_table.leaf_null().to_vec());
}
if !state.active_range(lo, hi) {
if size.is_power_of_two() {
let h = size.trailing_zeros() as usize;
return Ok(state.null_table.get_precomputed(h).to_vec());
}
return self.null_range_root(state, size);
}
if size.is_power_of_two() {
let h = size.trailing_zeros() as usize;
if let Some(hash) = self
.storage
.get_node(alg_id, lo, h)
.map_err(|e| Error::Storage(Box::new(e)))?
{
return Ok(hash);
}
}
let k = crate::proof::largest_pow2_lt(size);
let left = self.subtree_root(state, alg_id, lo, lo + k)?;
let right = self.subtree_root(state, alg_id, lo + k, hi)?;
Ok(state.hasher.node(&left, &right))
}
fn null_range_root(&self, state: &AlgState, size: u64) -> Result<Vec<u8>> {
debug_assert!(size > 0, "null_range_root requires size > 0");
if size == 1 {
return Ok(state.null_table.leaf_null().to_vec());
}
let k_bits = 63 - (size.leading_zeros() as u64);
let k = 1u64 << k_bits;
let left_root = state.null_table.get_precomputed(k_bits as usize).to_vec();
let remainder = size - k;
if remainder == 0 {
return Ok(left_root);
}
let right_root = self.null_range_root(state, remainder)?;
Ok(state.hasher.node(&left_root, &right_root))
}
#[cfg(test)]
pub fn project(&self, alg_id: u64) -> Result<Vec<Vec<u8>>> {
let state = self
.algs
.get(&alg_id)
.ok_or(Error::UnknownAlgorithm(alg_id))?;
let ts = state.tree_size(self.size());
let mut leaves = Vec::with_capacity(ts as usize);
for i in 0..ts {
let leaf_hash = if state.is_active_at(i) {
let data = self
.storage
.get_leaf(i)
.map_err(|e| Error::Storage(Box::new(e)))?;
state.hasher.leaf(&data)
} else {
state.null_table.leaf_null().to_vec()
};
leaves.push(leaf_hash);
}
Ok(leaves)
}
pub fn inclusion_proof(&self, alg_id: u64, index: u64) -> Result<crate::proof::InclusionProof> {
let state = self
.algs
.get(&alg_id)
.ok_or(Error::UnknownAlgorithm(alg_id))?;
let ts = state.tree_size(self.size());
if ts == 0 || index >= ts {
return Err(Error::IndexOutOfBounds {
index,
tree_size: ts,
});
}
let mut path = Vec::with_capacity(64);
self.path(state, alg_id, index, 0, ts, &mut path)?;
Ok(crate::proof::InclusionProof {
index,
tree_size: ts,
path,
})
}
fn path(
&self,
state: &AlgState,
alg_id: u64,
m: u64,
lo: u64,
hi: u64,
path: &mut Vec<Vec<u8>>,
) -> Result<()> {
let size = hi - lo;
if size <= 1 {
return Ok(());
}
let k = crate::proof::largest_pow2_lt(size);
if m - lo < k {
self.path(state, alg_id, m, lo, lo + k, path)?;
path.push(self.subtree_root(state, alg_id, lo + k, hi)?);
} else {
self.path(state, alg_id, m, lo + k, hi, path)?;
path.push(self.subtree_root(state, alg_id, lo, lo + k)?);
}
Ok(())
}
pub fn consistency_proof(
&self,
alg_id: u64,
old_size: u64,
) -> Result<crate::proof::ConsistencyProof> {
let state = self
.algs
.get(&alg_id)
.ok_or(Error::UnknownAlgorithm(alg_id))?;
let ts = state.tree_size(self.size());
if old_size == 0 || old_size >= ts {
return Err(Error::IndexOutOfBounds {
index: old_size,
tree_size: ts,
});
}
let mut path = Vec::with_capacity(64);
self.subproof(state, alg_id, old_size, 0, ts, true, &mut path)?;
Ok(crate::proof::ConsistencyProof {
old_size,
new_size: ts,
path,
})
}
#[allow(clippy::too_many_arguments)]
fn subproof(
&self,
state: &AlgState,
alg_id: u64,
m: u64,
lo: u64,
hi: u64,
b: bool,
path: &mut Vec<Vec<u8>>,
) -> Result<()> {
let size = hi - lo;
if m == size {
if !b {
path.push(self.subtree_root(state, alg_id, lo, hi)?);
}
return Ok(());
}
let k = crate::proof::largest_pow2_lt(size);
if m <= k {
self.subproof(state, alg_id, m, lo, lo + k, b, path)?;
path.push(self.subtree_root(state, alg_id, lo + k, hi)?);
} else {
self.subproof(state, alg_id, m - k, lo + k, hi, false, path)?;
path.push(self.subtree_root(state, alg_id, lo, lo + k)?);
}
Ok(())
}
}
impl Default for Log<crate::storage::MemoryStorage> {
fn default() -> Self {
Self::new(crate::storage::MemoryStorage::new())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::storage::MemoryStorage;
use crate::test_hashers::{AltHasher, Sha256Hasher};
fn batch_root(hasher: &dyn Hasher, leaf_hashes: &[Vec<u8>]) -> Vec<u8> {
crate::proof::mth(hasher, leaf_hashes)
}
#[test]
fn a_equiv_single_algorithm() {
let mut log = Log::new(MemoryStorage::new());
log.add_algorithm(0, Box::new(Sha256Hasher)).unwrap();
for i in 0..16u8 {
log.append(&[i]).unwrap();
let incremental = log.root(0).unwrap();
let projected = log.project(0).unwrap();
let batch = batch_root(&Sha256Hasher, &projected);
assert_eq!(incremental, batch, "A-EQUIV-EML failed at size {}", i + 1);
}
}
#[test]
fn a_equiv_mid_stream_algorithm() {
let mut log = Log::new(MemoryStorage::new());
log.add_algorithm(0, Box::new(Sha256Hasher)).unwrap();
for i in 0..4u8 {
log.append(&[i]).unwrap();
}
log.add_algorithm(1, Box::new(AltHasher)).unwrap();
for i in 4..8u8 {
log.append(&[i]).unwrap();
}
for alg_id in [0, 1] {
let incremental = log.root(alg_id).unwrap();
let projected = log.project(alg_id).unwrap();
let hasher: &dyn Hasher = if alg_id == 0 {
&Sha256Hasher
} else {
&AltHasher
};
let batch = batch_root(hasher, &projected);
assert_eq!(incremental, batch, "A-EQUIV-EML failed for alg {alg_id}");
}
}
#[test]
fn a_stack_popcount_invariant() {
let mut log = Log::new(MemoryStorage::new());
log.add_algorithm(0, Box::new(Sha256Hasher)).unwrap();
for i in 0..20u8 {
log.append(&[i]).unwrap();
let expected = (log.size()).count_ones() as usize;
assert_eq!(
log.stack_len(0).unwrap(),
expected,
"A-STACK-EML failed at size {}",
log.size()
);
}
}
#[test]
fn a_stack_frozen_algorithm() {
let mut log = Log::new(MemoryStorage::new());
log.add_algorithm(0, Box::new(Sha256Hasher)).unwrap();
for i in 0..6u8 {
log.append(&[i]).unwrap();
}
log.remove_algorithm(0).unwrap();
let frozen_stack_len = log.stack_len(0).unwrap();
let expected = 6u64.count_ones() as usize; assert_eq!(frozen_stack_len, expected);
log.add_algorithm(1, Box::new(AltHasher)).unwrap();
for i in 6..10u8 {
log.append(&[i]).unwrap();
}
assert_eq!(log.stack_len(0).unwrap(), frozen_stack_len);
}
#[test]
fn t_bound_null_prefix_differs_from_real_leaf() {
let mut log = Log::new(MemoryStorage::new());
log.add_algorithm(0, Box::new(Sha256Hasher)).unwrap();
for i in 0..4u8 {
log.append(&[i]).unwrap();
}
log.add_algorithm(1, Box::new(AltHasher)).unwrap();
for i in 4..8u8 {
log.append(&[i]).unwrap();
}
let state = log.algs.get(&1).unwrap();
let null_leaf = state.null_table.leaf_null();
let leaf0_data = log.storage.get_leaf(0).unwrap();
let real_leaf = state.hasher.leaf(&leaf0_data);
assert_ne!(
null_leaf,
real_leaf.as_slice(),
"T-BOUND: null prefix position must differ from real leaf hash"
);
}
#[test]
fn alg_ind_different_algorithms_different_roots() {
let mut log = Log::new(MemoryStorage::new());
log.add_algorithm(0, Box::new(Sha256Hasher)).unwrap();
log.add_algorithm(1, Box::new(AltHasher)).unwrap();
for i in 0..8u8 {
log.append(&[i]).unwrap();
}
let root0 = log.root(0).unwrap();
let root1 = log.root(1).unwrap();
assert_ne!(
root0, root1,
"ALG-IND: different algorithms must produce different roots"
);
}
#[test]
fn error_duplicate_algorithm() {
let mut log = Log::new(MemoryStorage::new());
log.add_algorithm(0, Box::new(Sha256Hasher)).unwrap();
let err = log.add_algorithm(0, Box::new(Sha256Hasher)).unwrap_err();
assert_eq!(err, Error::DuplicateAlgorithm(0));
}
#[test]
fn error_unknown_algorithm() {
let log = Log::new(MemoryStorage::new());
let err = log.root(99).unwrap_err();
assert_eq!(err, Error::UnknownAlgorithm(99));
}
#[test]
fn error_no_active_algorithms() {
let mut log = Log::new(MemoryStorage::new());
let err = log.append(b"data").unwrap_err();
assert_eq!(err, Error::NoActiveAlgorithms);
}
#[test]
fn error_double_freeze() {
let mut log = Log::new(MemoryStorage::new());
log.add_algorithm(0, Box::new(Sha256Hasher)).unwrap();
log.append(b"data").unwrap();
log.remove_algorithm(0).unwrap();
let err = log.remove_algorithm(0).unwrap_err();
assert_eq!(err, Error::FrozenAlgorithm(0));
}
#[test]
fn empty_tree_root() {
let mut log = Log::new(MemoryStorage::new());
log.add_algorithm(0, Box::new(Sha256Hasher)).unwrap();
let root = log.root(0).unwrap();
assert_eq!(root, Sha256Hasher.empty());
}
#[test]
fn frozen_root_is_stable() {
let mut log = Log::new(MemoryStorage::new());
log.add_algorithm(0, Box::new(Sha256Hasher)).unwrap();
for i in 0..5u8 {
log.append(&[i]).unwrap();
}
let root_before = log.root(0).unwrap();
log.remove_algorithm(0).unwrap();
let root_after = log.root(0).unwrap();
assert_eq!(root_before, root_after, "root must not change on freeze");
log.add_algorithm(1, Box::new(AltHasher)).unwrap();
for i in 5..10u8 {
log.append(&[i]).unwrap();
}
let root_still = log.root(0).unwrap();
assert_eq!(root_before, root_still, "frozen root must remain stable");
}
#[test]
fn a_equiv_non_power_of_two() {
for size in [1, 3, 5, 7, 9, 11, 13, 15, 17, 19] {
let mut log = Log::new(MemoryStorage::new());
log.add_algorithm(0, Box::new(Sha256Hasher)).unwrap();
for i in 0..size as u8 {
log.append(&[i]).unwrap();
}
let incremental = log.root(0).unwrap();
let projected = log.project(0).unwrap();
let batch = batch_root(&Sha256Hasher, &projected);
assert_eq!(incremental, batch, "A-EQUIV failed at size {size}");
}
}
#[test]
fn i_sound_single_algorithm() {
let mut log = Log::new(MemoryStorage::new());
log.add_algorithm(0, Box::new(Sha256Hasher)).unwrap();
for i in 0..12u8 {
log.append(&[i]).unwrap();
}
let root = log.root(0).unwrap();
let projected = log.project(0).unwrap();
for idx in 0..12u64 {
let proof = log.inclusion_proof(0, idx).unwrap();
let leaf_hash = &projected[idx as usize];
assert!(
crate::proof::verify_inclusion(&Sha256Hasher, leaf_hash, &proof, &root),
"I-SOUND-EML failed at index {idx}"
);
}
}
#[test]
fn i_sound_mid_stream_algorithm() {
let mut log = Log::new(MemoryStorage::new());
log.add_algorithm(0, Box::new(Sha256Hasher)).unwrap();
for i in 0..4u8 {
log.append(&[i]).unwrap();
}
log.add_algorithm(1, Box::new(AltHasher)).unwrap();
for i in 4..12u8 {
log.append(&[i]).unwrap();
}
let root = log.root(1).unwrap();
let projected = log.project(1).unwrap();
for idx in 0..12u64 {
let proof = log.inclusion_proof(1, idx).unwrap();
let leaf_hash = &projected[idx as usize];
assert!(
crate::proof::verify_inclusion(&AltHasher, leaf_hash, &proof, &root),
"I-SOUND-EML (mid-stream) failed at index {idx}"
);
}
}
#[test]
fn k_sound_single_algorithm() {
let mut log = Log::new(MemoryStorage::new());
log.add_algorithm(0, Box::new(Sha256Hasher)).unwrap();
let mut roots: Vec<Vec<u8>> = Vec::new();
for i in 0..8u8 {
log.append(&[i]).unwrap();
roots.push(log.root(0).unwrap());
}
let current_root = log.root(0).unwrap();
for old_size in 1..8u64 {
let proof = log.consistency_proof(0, old_size).unwrap();
let old_root = &roots[(old_size - 1) as usize];
assert!(
crate::proof::verify_consistency(&Sha256Hasher, &proof, old_root, ¤t_root),
"K-SOUND-EML failed for old_size={old_size}"
);
}
}
#[test]
fn t_bound_inclusion_proof_at_null_position() {
let mut log = Log::new(MemoryStorage::new());
log.add_algorithm(0, Box::new(Sha256Hasher)).unwrap();
for i in 0..4u8 {
log.append(&[i]).unwrap();
}
log.add_algorithm(1, Box::new(AltHasher)).unwrap();
for i in 4..8u8 {
log.append(&[i]).unwrap();
}
let root = log.root(1).unwrap();
let proof = log.inclusion_proof(1, 0).unwrap();
let null_leaf = AltHasher.null();
assert!(
crate::proof::verify_inclusion(&AltHasher, &null_leaf, &proof, &root),
"null leaf should verify at null position"
);
for d in [b"any".as_slice(), b"data", b"", &[0], &[1], &[2], &[3]] {
let forged_leaf = AltHasher.leaf(d);
assert!(
!crate::proof::verify_inclusion(&AltHasher, &forged_leaf, &proof, &root),
"T-BOUND violated: real leaf verified at null position for data {:?}",
d
);
}
}
#[test]
fn inclusion_proof_out_of_bounds() {
let mut log = Log::new(MemoryStorage::new());
log.add_algorithm(0, Box::new(Sha256Hasher)).unwrap();
log.append(b"data").unwrap();
let err = log.inclusion_proof(0, 1).unwrap_err();
assert_eq!(
err,
Error::IndexOutOfBounds {
index: 1,
tree_size: 1
}
);
}
#[test]
fn consistency_proof_bounds() {
let mut log = Log::new(MemoryStorage::new());
log.add_algorithm(0, Box::new(Sha256Hasher)).unwrap();
for i in 0..4u8 {
log.append(&[i]).unwrap();
}
let err = log.consistency_proof(0, 0).unwrap_err();
assert_eq!(
err,
Error::IndexOutOfBounds {
index: 0,
tree_size: 4
}
);
let err = log.consistency_proof(0, 4).unwrap_err();
assert_eq!(
err,
Error::IndexOutOfBounds {
index: 4,
tree_size: 4
}
);
}
#[test]
fn algorithms_returns_manifest_data() {
let mut log = Log::new(MemoryStorage::new());
log.add_algorithm(0, Box::new(Sha256Hasher)).unwrap();
for i in 0..4u8 {
log.append(&[i]).unwrap();
}
log.add_algorithm(1, Box::new(AltHasher)).unwrap();
log.remove_algorithm(0).unwrap();
for i in 4..8u8 {
log.append(&[i]).unwrap();
}
let infos = log.algorithms();
assert_eq!(infos.len(), 2);
let a0 = infos.iter().find(|a| a.id == 0).unwrap();
assert_eq!(a0.activation_index, 0);
assert_eq!(a0.deactivation_index, Some(4));
assert_eq!(a0.tree_size, 4);
assert_eq!(a0.root, log.root(0).unwrap());
let expected_a0_serialized = serialize_epochs(&[(0, 4)]);
let expected_a0_hash = Sha256Hasher.hash(&expected_a0_serialized);
assert_eq!(a0.manifest_hash, expected_a0_hash);
let a1 = infos.iter().find(|a| a.id == 1).unwrap();
assert_eq!(a1.activation_index, 4);
assert_eq!(a1.deactivation_index, None);
assert_eq!(a1.tree_size, 8); assert_eq!(a1.root, log.root(1).unwrap());
let expected_a1_serialized = serialize_epochs(&[(4, u64::MAX)]);
let expected_a1_hash = AltHasher.hash(&expected_a1_serialized);
assert_eq!(a1.manifest_hash, expected_a1_hash);
}
#[test]
fn resume_basic_a_equiv() {
let mut log = Log::new(MemoryStorage::new());
log.add_algorithm(0, Box::new(Sha256Hasher)).unwrap();
log.add_algorithm(1, Box::new(AltHasher)).unwrap();
for i in 0..4u8 {
log.append(&[i]).unwrap();
}
log.remove_algorithm(0).unwrap();
for i in 4..8u8 {
log.append(&[i]).unwrap();
}
log.resume_algorithm(0).unwrap();
for i in 8..12u8 {
log.append(&[i]).unwrap();
}
let root = log.root(0).unwrap();
let projected = log.project(0).unwrap();
let batch_root = crate::proof::mth(&Sha256Hasher, &projected);
assert_eq!(root, batch_root, "A-EQUIV violated after resume");
}
#[test]
fn resume_a_stack_invariant() {
let mut log = Log::new(MemoryStorage::new());
log.add_algorithm(0, Box::new(Sha256Hasher)).unwrap();
log.add_algorithm(1, Box::new(AltHasher)).unwrap();
for i in 0..3u8 {
log.append(&[i]).unwrap();
}
log.remove_algorithm(0).unwrap();
for i in 3..8u8 {
log.append(&[i]).unwrap();
}
log.resume_algorithm(0).unwrap();
for i in 8..13u8 {
log.append(&[i]).unwrap();
}
let ts = log.tree_size(0).unwrap();
let expected_len = ts.count_ones() as usize;
assert_eq!(
log.stack_len(0).unwrap(),
expected_len,
"A-STACK violated after resume: tree_size={ts}"
);
}
#[test]
fn resume_error_active_algorithm() {
let mut log = Log::new(MemoryStorage::new());
log.add_algorithm(0, Box::new(Sha256Hasher)).unwrap();
log.append(b"a").unwrap();
let err = log.resume_algorithm(0).unwrap_err();
assert_eq!(err, Error::AlgorithmActive(0));
}
#[test]
fn resume_error_unknown_algorithm() {
let mut log = Log::new(MemoryStorage::new());
log.add_algorithm(0, Box::new(Sha256Hasher)).unwrap();
let err = log.resume_algorithm(99).unwrap_err();
assert_eq!(err, Error::UnknownAlgorithm(99));
}
#[test]
fn resume_inclusion_proof_soundness() {
let mut log = Log::new(MemoryStorage::new());
log.add_algorithm(0, Box::new(Sha256Hasher)).unwrap();
log.add_algorithm(1, Box::new(AltHasher)).unwrap();
for i in 0..4u8 {
log.append(&[i]).unwrap();
}
log.remove_algorithm(0).unwrap();
for i in 4..8u8 {
log.append(&[i]).unwrap();
}
log.resume_algorithm(0).unwrap();
for i in 8..12u8 {
log.append(&[i]).unwrap();
}
let root = log.root(0).unwrap();
let projected = log.project(0).unwrap();
for &idx in &[0u64, 1, 2, 3, 8, 9, 10, 11] {
let proof = log.inclusion_proof(0, idx).unwrap();
assert!(
crate::verify_inclusion(&Sha256Hasher, &projected[idx as usize], &proof, &root),
"I-SOUND failed at active index {idx}"
);
}
for idx in 4..8u64 {
let proof = log.inclusion_proof(0, idx).unwrap();
assert!(
crate::verify_inclusion(&Sha256Hasher, &projected[idx as usize], &proof, &root),
"proof at null gap position {idx} failed"
);
}
}
#[test]
fn resume_consistency_proof_soundness() {
let mut log = Log::new(MemoryStorage::new());
log.add_algorithm(0, Box::new(Sha256Hasher)).unwrap();
log.add_algorithm(1, Box::new(AltHasher)).unwrap();
for i in 0..4u8 {
log.append(&[i]).unwrap();
}
let root_at_4 = log.root(0).unwrap();
log.remove_algorithm(0).unwrap();
for i in 4..8u8 {
log.append(&[i]).unwrap();
}
log.resume_algorithm(0).unwrap();
for i in 8..12u8 {
log.append(&[i]).unwrap();
}
let proof = log.consistency_proof(0, 4).unwrap();
let root_now = log.root(0).unwrap();
assert!(
crate::verify_consistency(&Sha256Hasher, &proof, &root_at_4, &root_now),
"K-SOUND failed after resume"
);
}
#[test]
fn resume_consistency_across_gap() {
let mut log = Log::new(MemoryStorage::new());
log.add_algorithm(0, Box::new(Sha256Hasher)).unwrap();
log.add_algorithm(1, Box::new(AltHasher)).unwrap();
for i in 0..4u8 {
log.append(&[i]).unwrap();
}
log.remove_algorithm(0).unwrap();
for i in 4..8u8 {
log.append(&[i]).unwrap();
}
log.resume_algorithm(0).unwrap();
for i in 8..12u8 {
log.append(&[i]).unwrap();
}
let root_now = log.root(0).unwrap();
let projected = log.project(0).unwrap();
for old_size in 1..12u64 {
let old_root = crate::proof::mth(&Sha256Hasher, &projected[..old_size as usize]);
let proof = log.consistency_proof(0, old_size).unwrap();
assert!(
crate::verify_consistency(&Sha256Hasher, &proof, &old_root, &root_now),
"K-SOUND across gap failed for old_size={old_size}"
);
}
}
#[test]
fn resume_epochs_metadata() {
let mut log = Log::new(MemoryStorage::new());
log.add_algorithm(0, Box::new(Sha256Hasher)).unwrap();
log.add_algorithm(1, Box::new(AltHasher)).unwrap();
for i in 0..4u8 {
log.append(&[i]).unwrap();
}
log.remove_algorithm(0).unwrap();
for i in 4..8u8 {
log.append(&[i]).unwrap();
}
log.resume_algorithm(0).unwrap();
let epochs = log.epochs(0).unwrap();
assert_eq!(epochs.len(), 2);
assert_eq!(epochs[0], (0, Some(4)));
assert_eq!(epochs[1], (8, None));
assert_eq!(log.activation_index(0).unwrap(), 0);
assert_eq!(log.deactivation_index(0).unwrap(), None); }
#[test]
fn resume_large_gap_o_log_g() {
let mut log = Log::new(MemoryStorage::new());
log.add_algorithm(0, Box::new(Sha256Hasher)).unwrap();
for i in 0..8u8 {
log.append(&[i]).unwrap();
}
log.remove_algorithm(0).unwrap();
log.add_algorithm(1, Box::new(AltHasher)).unwrap();
let gap_size: u64 = 1 << 16;
for i in 0..gap_size {
log.append(&(i as u32).to_le_bytes()).unwrap();
}
log.resume_algorithm(0).unwrap();
for i in 0..4u8 {
log.append(&[200 + i]).unwrap();
}
let root = log.root(0).unwrap();
let projected = log.project(0).unwrap();
let batch_root = crate::proof::mth(&Sha256Hasher, &projected);
assert_eq!(root, batch_root, "A-EQUIV violated after large-gap resume");
let ts = log.tree_size(0).unwrap();
let expected_len = ts.count_ones() as usize;
assert_eq!(
log.stack_len(0).unwrap(),
expected_len,
"A-STACK violated after large-gap resume: tree_size={ts}"
);
}
#[test]
fn resume_immediate_no_gap() {
let mut log = Log::new(MemoryStorage::new());
log.add_algorithm(0, Box::new(Sha256Hasher)).unwrap();
for i in 0..4u8 {
log.append(&[i]).unwrap();
}
let root_before = log.root(0).unwrap();
log.remove_algorithm(0).unwrap();
log.resume_algorithm(0).unwrap();
let root_after = log.root(0).unwrap();
assert_eq!(root_before, root_after, "zero-gap resume changed root");
}
#[test]
fn resume_elide_multi_epoch() {
let mut log = Log::new(MemoryStorage::new());
log.add_algorithm(0, Box::new(Sha256Hasher)).unwrap();
log.add_algorithm(1, Box::new(AltHasher)).unwrap();
for i in 0..4u8 {
log.append(&[i]).unwrap();
}
log.remove_algorithm(0).unwrap();
for i in 4..8u8 {
log.append(&[i]).unwrap();
}
log.resume_algorithm(0).unwrap();
for i in 8..16u8 {
log.append(&[i]).unwrap();
}
let epochs = log.epochs(0).unwrap();
let root = log.root(0).unwrap();
let full_proof = log.inclusion_proof(0, 10).unwrap();
let projected = log.project(0).unwrap();
assert!(crate::verify_inclusion(
&Sha256Hasher,
&projected[10],
&full_proof,
&root
));
let elided = crate::elide_inclusion_proof(&full_proof, &epochs);
let rehydrated = crate::rehydrate_inclusion_proof(&elided, &Sha256Hasher);
assert_eq!(rehydrated, full_proof, "multi-epoch elide roundtrip failed");
assert!(crate::verify_inclusion(
&Sha256Hasher,
&projected[10],
&rehydrated,
&root
));
}
#[test]
fn from_storage_single_algorithm() {
let mut log = Log::new(MemoryStorage::new());
log.add_algorithm(0, Box::new(Sha256Hasher)).unwrap();
for i in 0..20u8 {
log.append(&[i]).unwrap();
}
let original_root = log.root(0).unwrap();
let original_size = log.size();
let original_algos = log.algorithms();
let storage = log.into_storage();
let reconstructed = Log::from_storage(storage, vec![(0, Box::new(Sha256Hasher))]).unwrap();
assert_eq!(reconstructed.size(), original_size);
assert_eq!(reconstructed.root(0).unwrap(), original_root);
assert_eq!(reconstructed.algorithms(), original_algos);
}
#[test]
fn from_storage_multi_algorithm_frozen_active() {
let mut log = Log::new(MemoryStorage::new());
log.add_algorithm(0, Box::new(Sha256Hasher)).unwrap();
log.add_algorithm(1, Box::new(Sha256Hasher)).unwrap();
for i in 0..10u8 {
log.append(&[i]).unwrap();
}
log.remove_algorithm(1).unwrap();
for i in 10..20u8 {
log.append(&[i]).unwrap();
}
let root0 = log.root(0).unwrap();
let root1 = log.root(1).unwrap();
let algos = log.algorithms();
let storage = log.into_storage();
let reconstructed = Log::from_storage(
storage,
vec![(0, Box::new(Sha256Hasher)), (1, Box::new(Sha256Hasher))],
)
.unwrap();
assert_eq!(reconstructed.root(0).unwrap(), root0);
assert_eq!(reconstructed.root(1).unwrap(), root1);
assert_eq!(reconstructed.algorithms(), algos);
}
#[test]
fn from_storage_resume_after_gap() {
let mut log = Log::new(MemoryStorage::new());
log.add_algorithm(0, Box::new(Sha256Hasher)).unwrap();
for i in 0..4u8 {
log.append(&[i]).unwrap();
}
log.remove_algorithm(0).unwrap();
log.add_algorithm(1, Box::new(Sha256Hasher)).unwrap();
for i in 4..8u8 {
log.append(&[i]).unwrap();
}
log.resume_algorithm(0).unwrap();
for i in 8..16u8 {
log.append(&[i]).unwrap();
}
let root0 = log.root(0).unwrap();
let root1 = log.root(1).unwrap();
let algos = log.algorithms();
let storage = log.into_storage();
let reconstructed = Log::from_storage(
storage,
vec![(0, Box::new(Sha256Hasher)), (1, Box::new(Sha256Hasher))],
)
.unwrap();
assert_eq!(reconstructed.root(0).unwrap(), root0);
assert_eq!(reconstructed.root(1).unwrap(), root1);
assert_eq!(reconstructed.algorithms(), algos);
}
#[test]
fn from_storage_continued_appends() {
let mut original = Log::new(MemoryStorage::new());
original.add_algorithm(0, Box::new(Sha256Hasher)).unwrap();
for i in 0..10u8 {
original.append(&[i]).unwrap();
}
let storage = original.into_storage();
let mut reconstructed =
Log::from_storage(storage, vec![(0, Box::new(Sha256Hasher))]).unwrap();
let mut reference = Log::new(MemoryStorage::new());
reference.add_algorithm(0, Box::new(Sha256Hasher)).unwrap();
for i in 0..10u8 {
reference.append(&[i]).unwrap();
}
for i in 10..20u8 {
reconstructed.append(&[i]).unwrap();
reference.append(&[i]).unwrap();
}
assert_eq!(reconstructed.root(0).unwrap(), reference.root(0).unwrap());
assert_eq!(reconstructed.size(), reference.size());
}
#[test]
fn from_storage_error_orphaned_metadata() {
let mut log = Log::new(MemoryStorage::new());
log.add_algorithm(0, Box::new(Sha256Hasher)).unwrap();
log.append(b"data").unwrap();
let storage = log.into_storage();
let result = Log::from_storage(storage, vec![]);
assert_eq!(result.unwrap_err(), Error::OrphanedMetadata(0));
}
#[test]
fn from_storage_error_unknown_metadata() {
let mut log = Log::new(MemoryStorage::new());
log.add_algorithm(0, Box::new(Sha256Hasher)).unwrap();
log.append(b"data").unwrap();
let storage = log.into_storage();
let result = Log::from_storage(
storage,
vec![(0, Box::new(Sha256Hasher)), (99, Box::new(Sha256Hasher))],
);
assert_eq!(result.unwrap_err(), Error::UnknownMetadata(99));
}
#[test]
fn from_storage_empty_log() {
let log = Log::new(MemoryStorage::new());
let storage = log.into_storage();
let reconstructed = Log::<MemoryStorage>::from_storage(storage, vec![]).unwrap();
assert_eq!(reconstructed.size(), 0);
assert!(reconstructed.algorithms().is_empty());
}
#[test]
fn from_storage_various_sizes() {
for n in [1, 2, 3, 4, 7, 8, 15, 16, 31, 32, 33, 63, 64, 100] {
let mut log = Log::new(MemoryStorage::new());
log.add_algorithm(0, Box::new(Sha256Hasher)).unwrap();
for i in 0..n {
log.append(&(i as u64).to_le_bytes()).unwrap();
}
let original_root = log.root(0).unwrap();
let storage = log.into_storage();
let reconstructed =
Log::from_storage(storage, vec![(0, Box::new(Sha256Hasher))]).unwrap();
assert_eq!(
reconstructed.root(0).unwrap(),
original_root,
"root mismatch for n={n}"
);
}
}
}