use std::fmt::Debug;
use crate::proof;
use crate::proof::{ConsistencyProof, InclusionProof};
use crate::Error;
pub trait TreeHasher {
type Digest: Clone + Eq + Debug;
fn leaf(&self, data: &[u8]) -> Self::Digest;
fn node(&self, left: &Self::Digest, right: &Self::Digest) -> Self::Digest;
fn empty(&self) -> Self::Digest;
}
#[derive(Debug, Clone)]
pub struct Log<H: TreeHasher> {
hasher: H,
leaves: Vec<H::Digest>,
stack: Vec<H::Digest>,
}
impl<H: TreeHasher> Log<H> {
pub fn new(hasher: H) -> Self {
Self {
hasher,
leaves: Vec::new(),
stack: Vec::new(),
}
}
pub fn append(&mut self, data: &[u8]) -> u64 {
let hash = self.hasher.leaf(data);
let merge_count = count_trailing_ones(self.leaves.len() as u64);
self.leaves.push(hash.clone());
self.stack.push(hash);
for _ in 0..merge_count {
let right = self.stack.pop().expect("stack underflow in merge");
let left = self.stack.pop().expect("stack underflow in merge");
self.stack.push(self.hasher.node(&left, &right));
}
(self.leaves.len() - 1) as u64
}
#[must_use = "returns the current leaf count without modifying the log"]
pub fn size(&self) -> u64 {
self.leaves.len() as u64
}
#[must_use = "returns the root hash without modifying the log"]
pub fn root(&self) -> H::Digest {
if self.leaves.is_empty() {
return self.hasher.empty();
}
self.stack
.iter()
.rev()
.cloned()
.reduce(|acc, left| self.hasher.node(&left, &acc))
.expect("non-empty tree has non-empty stack")
}
pub fn hasher(&self) -> &H {
&self.hasher
}
#[cfg(test)]
fn stack_len(&self) -> usize {
self.stack.len()
}
pub fn inclusion_proof(&self, index: u64) -> Result<InclusionProof<H::Digest>, Error> {
let size = self.size();
if size == 0 {
return Err(Error::EmptyTree);
}
if index >= size {
return Err(Error::IndexOutOfBounds {
index,
tree_size: size,
});
}
let path = proof::gen_path(&self.hasher, index as usize, &self.leaves);
Ok(InclusionProof {
index,
tree_size: size,
path,
})
}
pub fn consistency_proof(&self, old_size: u64) -> Result<ConsistencyProof<H::Digest>, Error> {
let size = self.size();
if size == 0 {
return Err(Error::EmptyTree);
}
if old_size == 0 || old_size >= size {
return Err(Error::InvalidOldSize {
old_size,
new_size: size,
});
}
let path = proof::gen_subproof(&self.hasher, old_size as usize, &self.leaves, true);
Ok(ConsistencyProof {
old_size,
new_size: size,
path,
})
}
}
pub(crate) fn largest_pow2_lt(n: usize) -> usize {
assert!(n > 1, "largest_pow2_lt requires n > 1, got {n}");
1 << (usize::BITS - 1 - (n - 1).leading_zeros())
}
fn count_trailing_ones(n: u64) -> u32 {
(!n).trailing_zeros()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_largest_pow2_lt() {
assert_eq!(largest_pow2_lt(2), 1);
assert_eq!(largest_pow2_lt(3), 2);
assert_eq!(largest_pow2_lt(4), 2);
assert_eq!(largest_pow2_lt(5), 4);
assert_eq!(largest_pow2_lt(6), 4);
assert_eq!(largest_pow2_lt(7), 4);
assert_eq!(largest_pow2_lt(8), 4);
assert_eq!(largest_pow2_lt(9), 8);
assert_eq!(largest_pow2_lt(15), 8);
assert_eq!(largest_pow2_lt(16), 8);
assert_eq!(largest_pow2_lt(17), 16);
}
#[test]
fn test_count_trailing_ones() {
assert_eq!(count_trailing_ones(0b0000), 0);
assert_eq!(count_trailing_ones(0b0001), 1);
assert_eq!(count_trailing_ones(0b0011), 2);
assert_eq!(count_trailing_ones(0b0101), 1);
assert_eq!(count_trailing_ones(0b0111), 3);
assert_eq!(count_trailing_ones(0b1010), 0);
}
#[test]
fn a_stack_popcount_invariant() {
struct SimpleHasher;
impl TreeHasher for SimpleHasher {
type Digest = u64;
fn leaf(&self, data: &[u8]) -> u64 {
data.iter().fold(0u64, |acc, &b| acc.wrapping_add(b as u64))
}
fn node(&self, left: &u64, right: &u64) -> u64 {
left.wrapping_mul(31).wrapping_add(*right)
}
fn empty(&self) -> u64 {
0
}
}
let mut log = Log::new(SimpleHasher);
for i in 0u64..64 {
log.append(format!("leaf-{i}").as_bytes());
let expected = log.size().count_ones() as usize;
assert_eq!(
log.stack_len(),
expected,
"A-STACK failed at size={}: stack_len={}, popcount={}",
log.size(),
log.stack_len(),
expected
);
}
}
}