use polydigest::{CmtConfig, EpochTree, Hasher};
use sha2::{Digest, Sha256};
#[derive(Debug, Clone, Copy)]
struct Sha256Hasher;
impl polydigest::Hasher for Sha256Hasher {
fn leaf(&self, data: &[u8]) -> Vec<u8> {
Sha256::digest(data).to_vec()
}
fn node(&self, children: &[&[u8]]) -> Vec<u8> {
let mut h = Sha256::new();
for c in children {
h.update(c);
}
h.finalize().to_vec()
}
fn empty(&self) -> Vec<u8> {
Sha256::digest(b"").to_vec()
}
fn hash(&self, data: &[u8]) -> Vec<u8> {
Sha256::digest(data).to_vec()
}
fn clone_box(&self) -> Box<dyn polydigest::Hasher> {
Box::new(*self)
}
}
#[derive(Debug, Clone, Copy)]
struct DoubleSha;
impl polydigest::Hasher for DoubleSha {
fn leaf(&self, data: &[u8]) -> Vec<u8> {
Sha256::digest(Sha256::digest(data)).to_vec()
}
fn node(&self, children: &[&[u8]]) -> Vec<u8> {
let mut h = Sha256::new();
for c in children {
h.update(c);
}
Sha256::digest(h.finalize()).to_vec()
}
fn empty(&self) -> Vec<u8> {
Sha256::digest(Sha256::digest(b"")).to_vec()
}
fn hash(&self, data: &[u8]) -> Vec<u8> {
Sha256::digest(Sha256::digest(data)).to_vec()
}
fn clone_box(&self) -> Box<dyn polydigest::Hasher> {
Box::new(*self)
}
}
const ALG0: u64 = 0;
const ALG1: u64 = 1;
const K: u64 = 2;
fn tree_with(payloads: &[Vec<u8>]) -> EpochTree {
let mut t = EpochTree::new(CmtConfig { arity: K }).unwrap();
t.register_algorithm(ALG0, Box::new(Sha256Hasher)).unwrap();
for (i, p) in payloads.iter().enumerate() {
t.set(i as u64, p.clone(), Vec::new()).unwrap();
}
t
}
#[test]
fn single_algorithm_combined_root_promotes_to_member_root() {
for n in 1u64..16 {
let payloads: Vec<Vec<u8>> = (0..n).map(|i| format!("c{i}").into_bytes()).collect();
let t = tree_with(&payloads);
let member = t.root(ALG0).unwrap();
let combined = t
.combined_root(ALG0)
.expect("non-empty tree has a combined root");
assert_eq!(
combined, member,
"single-alg combined root must promote (n={n})"
);
}
}
#[test]
fn multi_algorithm_combined_root_is_the_fold_over_members() {
let payloads: Vec<Vec<u8>> = (0..5u64).map(|i| format!("c{i}").into_bytes()).collect();
let mut t = EpochTree::new(CmtConfig { arity: K }).unwrap();
t.register_algorithm(ALG0, Box::new(Sha256Hasher)).unwrap();
t.register_algorithm(ALG1, Box::new(DoubleSha)).unwrap();
for (i, p) in payloads.iter().enumerate() {
t.set(i as u64, p.clone(), Vec::new()).unwrap();
}
let mr0 = t.root(ALG0).unwrap();
let mr1 = t.root(ALG1).unwrap();
let combined0 = t.combined_root(ALG0).unwrap();
let expected0 = Sha256Hasher.node(&[mr0.as_slice(), mr1.as_slice()]);
assert_eq!(combined0, expected0);
let combined1 = t.combined_root(ALG1).unwrap();
let expected1 = DoubleSha.node(&[mr0.as_slice(), mr1.as_slice()]);
assert_eq!(combined1, expected1);
assert_ne!(combined0, combined1);
assert_ne!(combined0, mr0);
assert_ne!(combined0, mr1);
}
#[test]
fn combined_root_is_none_for_empty_or_unregistered() {
let mut empty = EpochTree::new(CmtConfig { arity: K }).unwrap();
empty
.register_algorithm(ALG0, Box::new(Sha256Hasher))
.unwrap();
assert_eq!(
empty.combined_root(ALG0),
None,
"empty tree has no combined root"
);
let t = tree_with(&[b"a".to_vec()]);
assert_eq!(
t.combined_root(99),
None,
"unregistered algorithm has no combined root"
);
}
#[test]
fn seal_yields_combined_currency_with_derived_roots() {
let payloads: Vec<Vec<u8>> = (0..3u64).map(|i| format!("s{i}").into_bytes()).collect();
let t = tree_with(&payloads);
let live_member = t.root(ALG0).unwrap();
let sealed = t.seal().unwrap();
assert_eq!(sealed.tree_size(), 3);
assert_eq!(sealed.arity(), K);
assert_eq!(
sealed.member_root(ALG0, &Sha256Hasher, polydigest::rebalanced_bag),
Some(live_member)
);
let hashers: [(u64, &dyn polydigest::Hasher); 1] = [(ALG0, &Sha256Hasher)];
assert!(
sealed
.binding_root(ALG0, &Sha256Hasher, &hashers, polydigest::rebalanced_bag)
.unwrap()
.is_some()
);
}