#![allow(clippy::arithmetic_side_effects, clippy::indexing_slicing)]
use crate::hash::{Hash, tagged};
const TAG_EMPTY: &[u8] = b"doubleentry/merkle/empty/v1";
const TAG_LEAF: &[u8] = b"doubleentry/merkle/leaf/v1";
const TAG_NODE: &[u8] = b"doubleentry/merkle/node/v1";
#[must_use]
pub fn empty_root() -> Hash {
tagged(TAG_EMPTY, &[])
}
#[must_use]
pub fn leaf_hash(payload: &Hash) -> Hash {
tagged(TAG_LEAF, payload.as_bytes())
}
#[must_use]
fn node_hash(left: &Hash, right: &Hash) -> Hash {
let mut buf = [0u8; Hash::LEN * 2];
buf[..Hash::LEN].copy_from_slice(left.as_bytes());
buf[Hash::LEN..].copy_from_slice(right.as_bytes());
tagged(TAG_NODE, &buf)
}
fn split_point(n: usize) -> usize {
debug_assert!(n > 1);
let mut k = 1usize;
while k << 1 < n {
k <<= 1;
}
k
}
fn mth(leaves: &[Hash]) -> Hash {
match leaves.len() {
0 => empty_root(),
1 => leaf_hash(&leaves[0]),
n => {
let k = split_point(n);
node_hash(&mth(&leaves[..k]), &mth(&leaves[k..]))
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct TreeHead {
pub size: u64,
pub root: Hash,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum ProofError {
#[error("leaf index {index} out of range for log of size {size}")]
IndexOutOfRange {
index: u64,
size: u64,
},
#[error("cannot prove consistency from size {from} against log of size {size}")]
SizeOutOfRange {
from: u64,
size: u64,
},
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error(
"accumulator state is not a valid cover of {size} leaves: \
expected subtree heights {expected:?}, found {found:?}"
)]
pub struct MalformedAccumulator {
pub size: u64,
pub expected: Vec<u8>,
pub found: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct InclusionProof {
pub leaf_index: u64,
pub tree_size: u64,
pub path: Vec<Hash>,
}
impl InclusionProof {
#[must_use]
pub fn verify(&self, leaf_payload: &Hash, root: &Hash) -> bool {
if self.leaf_index >= self.tree_size {
return false;
}
let mut fname = self.leaf_index;
let mut sname = self.tree_size - 1;
let mut acc = leaf_hash(leaf_payload);
for p in &self.path {
if sname == 0 {
return false;
}
if fname & 1 == 1 || fname == sname {
acc = node_hash(p, &acc);
while fname & 1 == 0 && fname != 0 {
fname >>= 1;
sname >>= 1;
}
} else {
acc = node_hash(&acc, p);
}
fname >>= 1;
sname >>= 1;
}
sname == 0 && acc == *root
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ConsistencyProof {
pub old_size: u64,
pub new_size: u64,
pub path: Vec<Hash>,
}
impl ConsistencyProof {
#[must_use]
pub fn verify(&self, old_root: &Hash, new_root: &Hash) -> bool {
if self.old_size > self.new_size {
return false;
}
if self.old_size == self.new_size {
return self.path.is_empty() && old_root == new_root;
}
if self.old_size == 0 {
return self.path.is_empty() && *old_root == empty_root();
}
let old_is_power_of_two = self.old_size & (self.old_size - 1) == 0;
let mut iter = self.path.iter();
let seed = if old_is_power_of_two {
*old_root
} else {
match iter.next() {
Some(h) => *h,
None => return false,
}
};
let mut fname = self.old_size - 1;
let mut sname = self.new_size - 1;
while fname & 1 == 1 {
fname >>= 1;
sname >>= 1;
}
let mut fr = seed;
let mut sr = seed;
for p in iter {
if sname == 0 {
return false;
}
if fname & 1 == 1 || fname == sname {
fr = node_hash(p, &fr);
sr = node_hash(p, &sr);
while fname & 1 == 0 && fname != 0 {
fname >>= 1;
sname >>= 1;
}
} else {
sr = node_hash(&sr, p);
}
fname >>= 1;
sname >>= 1;
}
sname == 0 && fr == *old_root && sr == *new_root
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct MerkleAccumulator {
subtrees: Vec<(u8, Hash)>,
size: u64,
}
impl MerkleAccumulator {
#[must_use]
pub fn new() -> Self {
Self::default()
}
fn cover_of(size: u64) -> Vec<u8> {
(0..u64::BITS)
.rev()
.filter(|bit| size & (1u64 << bit) != 0)
.filter_map(|bit| u8::try_from(bit).ok())
.collect()
}
pub fn try_from_parts(
subtrees: Vec<(u8, Hash)>,
size: u64,
) -> Result<Self, MalformedAccumulator> {
let expected = Self::cover_of(size);
let found: Vec<u8> = subtrees.iter().map(|(height, _)| *height).collect();
if found == expected {
Ok(Self { subtrees, size })
} else {
Err(MalformedAccumulator {
size,
expected,
found,
})
}
}
#[must_use]
pub fn subtrees(&self) -> &[(u8, Hash)] {
&self.subtrees
}
#[must_use]
pub fn size(&self) -> u64 {
self.size
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.size == 0
}
pub fn push(&mut self, payload: Hash) -> u64 {
let index = self.size;
let mut node = leaf_hash(&payload);
let mut level = 0u8;
while let Some(&(top_level, top_hash)) = self.subtrees.last() {
if top_level != level {
break;
}
self.subtrees.pop();
node = node_hash(&top_hash, &node);
level = level.saturating_add(1);
}
self.subtrees.push((level, node));
self.size = self.size.saturating_add(1);
index
}
#[must_use]
pub fn root(&self) -> Hash {
let mut iter = self.subtrees.iter().rev();
let Some(&(_, seed)) = iter.next() else {
return empty_root();
};
let mut acc = seed;
for &(_, left) in iter {
acc = node_hash(&left, &acc);
}
acc
}
#[must_use]
pub fn head(&self) -> TreeHead {
TreeHead {
size: self.size,
root: self.root(),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct MerkleLog {
leaves: Vec<Hash>,
accumulator: MerkleAccumulator,
}
impl MerkleLog {
#[must_use]
pub fn new() -> Self {
Self {
leaves: Vec::new(),
accumulator: MerkleAccumulator::new(),
}
}
#[must_use]
pub fn from_leaves(leaves: Vec<Hash>) -> Self {
let mut log = Self::new();
for leaf in leaves {
log.append(leaf);
}
log
}
#[must_use]
pub fn len(&self) -> u64 {
self.leaves.len() as u64
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.leaves.is_empty()
}
#[must_use]
pub fn leaves(&self) -> &[Hash] {
&self.leaves
}
pub fn append(&mut self, payload: Hash) -> u64 {
self.leaves.push(payload);
self.accumulator.push(payload)
}
#[must_use]
pub fn accumulator(&self) -> &MerkleAccumulator {
&self.accumulator
}
pub(crate) fn truncate(&mut self, len: u64) {
let len = usize::try_from(len).unwrap_or(usize::MAX);
if len >= self.leaves.len() {
return;
}
self.leaves.truncate(len);
self.accumulator = MerkleAccumulator::new();
for leaf in &self.leaves {
self.accumulator.push(*leaf);
}
}
#[must_use]
pub fn root(&self) -> Hash {
self.accumulator.root()
}
#[must_use]
pub fn verify_incremental_state(&self) -> bool {
self.root() == mth(&self.leaves)
}
#[must_use]
pub fn head(&self) -> TreeHead {
TreeHead {
size: self.len(),
root: self.root(),
}
}
pub fn root_at(&self, size: u64) -> Result<Hash, ProofError> {
let size_usize = usize::try_from(size).unwrap_or(usize::MAX);
if size_usize > self.leaves.len() {
return Err(ProofError::SizeOutOfRange {
from: size,
size: self.len(),
});
}
Ok(mth(&self.leaves[..size_usize]))
}
pub fn inclusion_proof(&self, index: u64) -> Result<InclusionProof, ProofError> {
let n = self.leaves.len();
let m = usize::try_from(index).unwrap_or(usize::MAX);
if m >= n {
return Err(ProofError::IndexOutOfRange {
index,
size: self.len(),
});
}
let mut path = Vec::new();
build_inclusion_path(m, &self.leaves, &mut path);
Ok(InclusionProof {
leaf_index: index,
tree_size: self.len(),
path,
})
}
pub fn consistency_proof(&self, old_size: u64) -> Result<ConsistencyProof, ProofError> {
let n = self.leaves.len();
let m = usize::try_from(old_size).unwrap_or(usize::MAX);
if m > n {
return Err(ProofError::SizeOutOfRange {
from: old_size,
size: self.len(),
});
}
let mut path = Vec::new();
if m > 0 && m < n {
build_consistency_path(m, &self.leaves, true, &mut path);
}
Ok(ConsistencyProof {
old_size,
new_size: self.len(),
path,
})
}
}
fn build_inclusion_path(m: usize, leaves: &[Hash], out: &mut Vec<Hash>) {
let n = leaves.len();
if n <= 1 {
return;
}
let k = split_point(n);
if m < k {
build_inclusion_path(m, &leaves[..k], out);
out.push(mth(&leaves[k..]));
} else {
build_inclusion_path(m - k, &leaves[k..], out);
out.push(mth(&leaves[..k]));
}
}
fn build_consistency_path(m: usize, leaves: &[Hash], b: bool, out: &mut Vec<Hash>) {
let n = leaves.len();
if m == n {
if !b {
out.push(mth(leaves));
}
return;
}
let k = split_point(n);
if m <= k {
build_consistency_path(m, &leaves[..k], b, out);
out.push(mth(&leaves[k..]));
} else {
build_consistency_path(m - k, &leaves[k..], false, out);
out.push(mth(&leaves[..k]));
}
}
#[cfg(test)]
mod tests {
use super::*;
fn payload(i: u64) -> Hash {
tagged(b"test/leaf", &i.to_le_bytes())
}
fn log_of(n: u64) -> MerkleLog {
let mut log = MerkleLog::new();
for i in 0..n {
log.append(payload(i));
}
log
}
#[test]
fn empty_log_has_empty_root() {
assert_eq!(MerkleLog::new().root(), empty_root());
assert_eq!(MerkleLog::new().head().size, 0);
}
#[test]
fn single_leaf_root_is_leaf_hash() {
let mut log = MerkleLog::new();
log.append(payload(0));
assert_eq!(log.root(), leaf_hash(&payload(0)));
}
#[test]
fn append_returns_sequential_indices() {
let mut log = MerkleLog::new();
assert_eq!(log.append(payload(0)), 0);
assert_eq!(log.append(payload(1)), 1);
assert_eq!(log.len(), 2);
}
#[test]
fn root_changes_when_a_leaf_changes() {
let a = log_of(8);
let mut leaves = a.leaves().to_vec();
leaves[3] = payload(999);
let b = MerkleLog::from_leaves(leaves);
assert_ne!(a.root(), b.root());
}
#[test]
fn inclusion_proofs_verify_for_every_leaf_and_size() {
for n in 1..=33u64 {
let log = log_of(n);
let root = log.root();
for i in 0..n {
let proof = log.inclusion_proof(i).expect("in range");
assert!(
proof.verify(&payload(i), &root),
"inclusion proof failed for leaf {i} of {n}"
);
}
}
}
#[test]
fn inclusion_proof_rejects_wrong_leaf() {
let log = log_of(8);
let root = log.root();
let proof = log.inclusion_proof(3).expect("in range");
assert!(!proof.verify(&payload(4), &root));
}
#[test]
fn inclusion_proof_rejects_wrong_root() {
let log = log_of(8);
let proof = log.inclusion_proof(3).expect("in range");
assert!(!proof.verify(&payload(3), &payload(0)));
}
#[test]
fn inclusion_proof_rejects_tampered_path() {
let log = log_of(8);
let root = log.root();
let mut proof = log.inclusion_proof(3).expect("in range");
proof.path[0] = payload(12345);
assert!(!proof.verify(&payload(3), &root));
}
#[test]
fn inclusion_proof_out_of_range_is_an_error() {
let log = log_of(4);
assert!(matches!(
log.inclusion_proof(4),
Err(ProofError::IndexOutOfRange { index: 4, size: 4 })
));
}
#[test]
fn inclusion_path_is_logarithmic() {
let log = log_of(1024);
let proof = log.inclusion_proof(500).expect("in range");
assert_eq!(proof.path.len(), 10);
}
#[test]
fn consistency_proofs_verify_for_every_prefix() {
for n in 1..=33u64 {
let log = log_of(n);
let new_root = log.root();
for m in 0..=n {
let old_root = log.root_at(m).expect("in range");
let proof = log.consistency_proof(m).expect("in range");
assert!(
proof.verify(&old_root, &new_root),
"consistency proof failed from {m} to {n}"
);
}
}
}
#[test]
fn consistency_proof_rejects_a_rewritten_prefix() {
let log = log_of(16);
let new_root = log.root();
let proof = log.consistency_proof(8).expect("in range");
let mut tampered = log.leaves()[..8].to_vec();
tampered[2] = payload(4242);
let forged_old_root = MerkleLog::from_leaves(tampered).root();
assert!(!proof.verify(&forged_old_root, &new_root));
}
#[test]
fn consistency_proof_rejects_growing_backwards() {
let log = log_of(8);
assert!(matches!(
log.consistency_proof(9),
Err(ProofError::SizeOutOfRange { from: 9, size: 8 })
));
}
#[test]
fn consistency_from_the_empty_tree_requires_the_empty_root() {
let log = log_of(8);
let proof = log.consistency_proof(0).expect("in range");
assert!(proof.verify(&empty_root(), &log.root()));
assert!(!proof.verify(&payload(1), &log.root()));
assert!(!proof.verify(&log.root(), &log.root()));
}
#[test]
fn incremental_root_matches_full_recomputation() {
let mut log = MerkleLog::new();
assert_eq!(log.root(), mth(&[]));
for i in 0..129u64 {
log.append(payload(i));
assert_eq!(log.root(), mth(log.leaves()), "diverged at size {}", i + 1);
assert!(log.verify_incremental_state());
}
}
#[test]
fn subtree_count_is_the_population_count_of_the_size() {
for n in 1u64..64 {
let log = log_of(n);
assert_eq!(
log.accumulator().subtrees().len() as u32,
n.count_ones(),
"size {n}"
);
}
}
#[test]
fn the_accumulator_matches_the_log_at_every_size() {
let mut acc = MerkleAccumulator::new();
let mut log = MerkleLog::new();
for i in 0..80u64 {
acc.push(payload(i));
log.append(payload(i));
assert_eq!(acc.root(), log.root(), "diverged at size {}", i + 1);
assert_eq!(acc.size(), log.len());
}
}
#[test]
fn an_accumulator_restores_from_persisted_parts() {
let mut original = MerkleAccumulator::new();
for i in 0..37u64 {
original.push(payload(i));
}
let restored =
MerkleAccumulator::try_from_parts(original.subtrees().to_vec(), original.size())
.expect("well-formed");
assert_eq!(restored.root(), original.root());
let mut a = original.clone();
let mut b = restored;
for i in 37..50u64 {
a.push(payload(i));
b.push(payload(i));
}
assert_eq!(a.root(), b.root());
}
#[test]
fn restoring_an_accumulator_checks_its_shape() {
let mut original = MerkleAccumulator::new();
for i in 0..37u64 {
original.push(payload(i));
}
let parts = original.subtrees().to_vec();
assert_eq!(
parts.iter().map(|(h, _)| *h).collect::<Vec<_>>(),
vec![5, 2, 0]
);
let mut short = parts.clone();
short.pop();
assert!(matches!(
MerkleAccumulator::try_from_parts(short, 37),
Err(MalformedAccumulator { size: 37, .. })
));
let mut shuffled = parts.clone();
shuffled.reverse();
assert!(MerkleAccumulator::try_from_parts(shuffled, 37).is_err());
assert!(MerkleAccumulator::try_from_parts(parts, 36).is_err());
assert!(MerkleAccumulator::try_from_parts(Vec::new(), 0).is_ok());
assert!(MerkleAccumulator::try_from_parts(Vec::new(), 1).is_err());
}
#[test]
fn a_restored_accumulator_is_checked_at_every_size() {
let mut acc = MerkleAccumulator::new();
for i in 0..200u64 {
acc.push(payload(i));
let restored = MerkleAccumulator::try_from_parts(acc.subtrees().to_vec(), acc.size())
.expect("its own state must round-trip");
assert_eq!(restored.root(), acc.root());
}
}
#[test]
fn an_empty_accumulator_is_the_empty_root() {
let acc = MerkleAccumulator::new();
assert!(acc.is_empty());
assert_eq!(acc.root(), empty_root());
assert_eq!(acc.head().size, 0);
}
#[test]
fn from_leaves_and_repeated_append_agree() {
let leaves: Vec<Hash> = (0..37).map(payload).collect();
let built = MerkleLog::from_leaves(leaves.clone());
let mut appended = MerkleLog::new();
for l in leaves {
appended.append(l);
}
assert_eq!(built.root(), appended.root());
assert!(built.verify_incremental_state());
}
#[test]
fn consistency_with_equal_sizes_is_empty_and_reflexive() {
let log = log_of(8);
let root = log.root();
let proof = log.consistency_proof(8).expect("in range");
assert!(proof.path.is_empty());
assert!(proof.verify(&root, &root));
}
#[test]
fn root_at_reconstructs_historical_roots() {
let log = log_of(20);
for m in 0..=20u64 {
assert_eq!(log.root_at(m).expect("in range"), log_of(m).root());
}
}
#[test]
fn split_point_is_largest_power_of_two_below() {
assert_eq!(split_point(2), 1);
assert_eq!(split_point(3), 2);
assert_eq!(split_point(4), 2);
assert_eq!(split_point(5), 4);
assert_eq!(split_point(1024), 512);
assert_eq!(split_point(1025), 1024);
}
#[test]
fn leaf_and_node_hashing_are_domain_separated() {
let l = payload(0);
assert_ne!(leaf_hash(&l), node_hash(&l, &l));
assert_ne!(leaf_hash(&l), empty_root());
}
}