use spine::{Hasher, ProofStep, SkeletonStep, fold_frontier, frontier_for_size, nary_mr};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BagNode {
Peak(usize),
Bag(Vec<BagNode>),
}
#[must_use]
pub fn bag_shape(peak_count: usize, k: usize) -> Option<BagNode> {
if peak_count == 0 {
return None;
}
let frontier: Vec<BagNode> = (0..peak_count).map(BagNode::Peak).collect();
Some(fold_frontier(frontier, k, |chunk| {
BagNode::Bag(chunk.to_vec())
}))
}
#[must_use]
pub fn bag_peaks(hasher: &dyn Hasher, peaks: &[Vec<u8>], k: u64) -> Vec<u8> {
match bag_shape(peaks.len(), k as usize) {
None => hasher.empty(),
Some(shape) => eval_bag(hasher, &shape, peaks),
}
}
fn eval_bag(hasher: &dyn Hasher, node: &BagNode, peaks: &[Vec<u8>]) -> Vec<u8> {
match node {
BagNode::Peak(f_idx) => peaks[*f_idx].clone(),
BagNode::Bag(children) => {
let child_digests: Vec<Vec<u8>> = children
.iter()
.map(|c| eval_bag(hasher, c, peaks))
.collect();
let refs: Vec<&[u8]> = child_digests.iter().map(Vec::as_slice).collect();
nary_mr(hasher, &refs)
},
}
}
#[must_use]
pub fn bag_path(peaks: &[Vec<u8>], f_idx: usize, hasher: &dyn Hasher, k: u64) -> Vec<ProofStep> {
let mut steps = Vec::new();
if let Some(shape) = bag_shape(peaks.len(), k as usize) {
descend_bag_path(&shape, f_idx, peaks, hasher, &mut steps);
steps.reverse();
}
steps
}
fn descend_bag_path(
node: &BagNode,
f_idx: usize,
peaks: &[Vec<u8>],
hasher: &dyn Hasher,
out: &mut Vec<ProofStep>,
) -> bool {
match node {
BagNode::Peak(idx) => *idx == f_idx,
BagNode::Bag(children) => {
let Some(position) = children.iter().position(|c| covers_peak(c, f_idx)) else {
return false;
};
let siblings: Vec<Vec<u8>> = children
.iter()
.enumerate()
.filter(|(i, _)| *i != position)
.map(|(_, c)| eval_bag(hasher, c, peaks))
.collect();
out.push(ProofStep { siblings, position });
descend_bag_path(&children[position], f_idx, peaks, hasher, out)
},
}
}
#[must_use]
pub fn mountain_skeleton(k: u64, tree_size: u64, index: u64) -> Option<Vec<SkeletonStep>> {
if !spine::ARITY_RANGE.contains(&k) {
return None;
}
let coords = frontier_for_size(tree_size, k);
if coords.is_empty() {
return None;
}
let k_usize = k as usize;
let mut target = None;
for (f_idx, &(left, height)) in coords.iter().enumerate() {
let cap = k.checked_pow(height)?;
let limit = left.checked_add(cap)?;
if index >= left && index < limit {
target = Some((f_idx, left, height));
break;
}
}
let (f_idx, left, height) = target?;
let mut steps = Vec::with_capacity(height as usize);
let mut offset = index - left;
for _ in 0..height {
steps.push(SkeletonStep {
position: (offset % k) as usize,
sibling_count: k_usize - 1,
});
offset /= k;
}
let shape = bag_shape(coords.len(), k_usize)?;
bag_path_steps(&shape, f_idx, &mut steps);
Some(steps)
}
fn bag_path_steps(shape: &BagNode, f_idx: usize, steps: &mut Vec<SkeletonStep>) {
let mut down = Vec::new();
descend_bag(shape, f_idx, &mut down);
down.reverse();
steps.extend(down);
}
fn descend_bag(node: &BagNode, target: usize, out: &mut Vec<SkeletonStep>) -> bool {
match node {
BagNode::Peak(f_idx) => *f_idx == target,
BagNode::Bag(children) => {
let position = children.iter().position(|c| covers_peak(c, target));
let Some(position) = position else {
return false;
};
out.push(SkeletonStep {
position,
sibling_count: children.len() - 1,
});
descend_bag(&children[position], target, out)
},
}
}
fn covers_peak(node: &BagNode, target: usize) -> bool {
match node {
BagNode::Peak(f_idx) => *f_idx == target,
BagNode::Bag(children) => children.iter().any(|c| covers_peak(c, target)),
}
}
#[cfg(test)]
mod tests {
use sha2::{Digest, Sha256};
use super::*;
#[derive(Debug)]
struct H;
impl Hasher for H {
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 Hasher> {
Box::new(H)
}
}
#[test]
fn single_peak_promotes() {
let peak = vec![0xAB; 32];
assert_eq!(bag_peaks(&H, std::slice::from_ref(&peak), 2), peak);
assert_eq!(mountain_skeleton(2, 1, 0), Some(vec![]));
}
#[test]
fn bag_shape_groups_rightmost_k_binary() {
let shape = bag_shape(4, 2).unwrap();
let expected = BagNode::Bag(vec![
BagNode::Peak(0),
BagNode::Bag(vec![
BagNode::Peak(1),
BagNode::Bag(vec![BagNode::Peak(2), BagNode::Peak(3)]),
]),
]);
assert_eq!(shape, expected);
}
#[test]
fn bag_peaks_equals_hand_fold_binary() {
let peaks: Vec<Vec<u8>> = (0u8..4).map(|i| vec![i; 32]).collect();
let inner = nary_mr(&H, &[peaks[2].as_slice(), peaks[3].as_slice()]);
let mid = nary_mr(&H, &[peaks[1].as_slice(), inner.as_slice()]);
let expected = nary_mr(&H, &[peaks[0].as_slice(), mid.as_slice()]);
assert_eq!(bag_peaks(&H, &peaks, 2), expected);
}
#[test]
fn bag_shape_kary() {
let shape = bag_shape(4, 3).unwrap();
let expected = BagNode::Bag(vec![
BagNode::Peak(0),
BagNode::Bag(vec![BagNode::Peak(1), BagNode::Peak(2), BagNode::Peak(3)]),
]);
assert_eq!(shape, expected);
}
#[test]
fn skeleton_steps_are_well_formed() {
for k in [2u64, 3, 5] {
for tree_size in 1..=130u64 {
for index in 0..tree_size {
let sk = mountain_skeleton(k, tree_size, index).expect("valid position");
for step in &sk {
assert!(step.sibling_count >= 1, "k={k} n={tree_size} i={index}");
assert!(step.position <= step.sibling_count);
}
}
}
}
}
#[test]
fn rejects_out_of_range() {
assert_eq!(mountain_skeleton(1, 4, 0), None);
assert_eq!(mountain_skeleton(257, 4, 0), None);
assert_eq!(mountain_skeleton(2, 0, 0), None);
assert_eq!(mountain_skeleton(2, 4, 4), None);
}
fn peaks(m: usize) -> Vec<Vec<u8>> {
(0..m).map(|i| vec![0x10 + i as u8; 32]).collect()
}
#[test]
fn prove_to_peak_then_bag_verifies() {
for m in 1..=12usize {
let ps = peaks(m);
let root = bag_peaks(&H, &ps, 2);
for (f_idx, peak) in ps.iter().enumerate() {
let path = bag_path(&ps, f_idx, &H, 2);
let mut sk = Vec::new();
bag_path_steps(&bag_shape(m, 2).unwrap(), f_idx, &mut sk);
assert!(
spine::verify_inclusion(&H, peak, &sk, &path, &root),
"m={m} f_idx={f_idx}: prove-to-peak+bag must verify"
);
let mut wrong = peak.clone();
wrong[0] ^= 0xFF;
assert!(
!spine::verify_inclusion(&H, &wrong, &sk, &path, &root),
"m={m} f_idx={f_idx}: a forged peak must not verify"
);
}
}
}
#[test]
fn a_formed_peak_is_permanent_across_append() {
for m in 1..=12usize {
let before = peaks(m);
let after = peaks(m + 1);
assert_eq!(&after[..m], &before[..], "m={m}: existing peaks rewritten");
}
}
#[test]
fn a_witness_survives_an_append() {
for m in 1..=12usize {
let f_idx = 0; let ps_n = peaks(m);
let ps_n1 = peaks(m + 1);
assert_eq!(ps_n[f_idx], ps_n1[f_idx]);
let root_n = bag_peaks(&H, &ps_n, 2);
let path_n = bag_path(&ps_n, f_idx, &H, 2);
let mut sk_n = Vec::new();
bag_path_steps(&bag_shape(m, 2).unwrap(), f_idx, &mut sk_n);
assert!(spine::verify_inclusion(
&H,
&ps_n[f_idx],
&sk_n,
&path_n,
&root_n
));
let root_n1 = bag_peaks(&H, &ps_n1, 2);
let path_n1 = bag_path(&ps_n1, f_idx, &H, 2);
let mut sk_n1 = Vec::new();
bag_path_steps(&bag_shape(m + 1, 2).unwrap(), f_idx, &mut sk_n1);
assert!(
spine::verify_inclusion(&H, &ps_n1[f_idx], &sk_n1, &path_n1, &root_n1),
"m={m}: a witness for the oldest peak must survive an append"
);
}
}
}