Skip to main content

cmt/
shape.rs

1//! The proof-spine shape, materialized as an explicit positional tree.
2//!
3//! The CMT is *mutable*, so it cannot reuse the append-only log's frontier
4//! (left-subtrees-sealed is unsound once interior cells change). Instead it
5//! materializes the proof spine — the same `(size, arity)` → topology the
6//! [`spine`] crate pins inclusion proofs against — as an explicit tree of
7//! [`ShapeNode`]s over the leaf positions. A single cell's change recomputes only
8//! the digests on its root-path, which is what makes both `set` and retroactive
9//! per-node algorithm addition cost `O(log n)` rather than a full `O(n)` rebuild.
10//!
11//! The shape is derived purely from `(size, arity)` by two rules the CMT owns:
12//! decompose into a frontier of perfect k-ary subtrees ([`spine::frontier_for_size`],
13//! the shared structural primitive), then fold the frontier by repeatedly
14//! grouping the rightmost `k` — the **rebalanced** topology. The spine itself is
15//! topology-agnostic (it owns no fold); the CMT owns this rebalanced fold and the
16//! matching [`rebalanced_skeleton`] the spine verifier pins a proof against, the
17//! mutable peer of the append-only log's mountain range. [`crate::Cmt::root`] is
18//! property-tested to equal [`spine::evaluate`] over the canonical subtree, so a
19//! drift in this shape is caught deterministically.
20
21use spine::{ARITY_RANGE, Hasher, SkeletonStep, fold_frontier, frontier_for_size, nary_mr};
22
23/// One node of the materialized proof spine.
24///
25/// A `Leaf(position)` names a logical cell; an `Inner(children)` is a hashing
26/// node whose children are shape nodes left-to-right. The shape is uniquely
27/// determined by `(size, arity)` — never stored, always re-derived — so it
28/// stays in lockstep with the spine topology that inclusion proofs check.
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub enum ShapeNode {
31    /// A logical leaf cell at this flat position.
32    Leaf(u64),
33    /// A hashing inner node over its children, left to right.
34    Inner(Vec<ShapeNode>),
35}
36
37/// Build the proof-spine shape for `size` leaves at arity `k`.
38///
39/// Returns `None` when the shape is undefined: `k` out of the spine's
40/// `2..=256` range, or an empty tree (`size == 0`) — a CMT always has a root
41/// only once it holds at least one cell.
42#[must_use]
43pub fn build(size: u64, k: u64) -> Option<ShapeNode> {
44    if !ARITY_RANGE.contains(&k) || size == 0 {
45        return None;
46    }
47    let coords = frontier_for_size(size, k);
48    if coords.is_empty() {
49        return None;
50    }
51
52    // Each frontier entry is a perfect k-ary subtree rooted at its left index.
53    let frontier: Vec<ShapeNode> = coords
54        .iter()
55        .map(|&(left, height)| perfect(left, height, k))
56        .collect();
57
58    // Fold the frontier by repeatedly grouping the rightmost `k` via
59    // `fold_frontier` — the shared canonical grouping loop. A singleton frontier
60    // promotes to the subtree directly; otherwise the final group becomes the root
61    // inner node.
62    Some(fold_frontier(frontier, k as usize, |chunk| {
63        ShapeNode::Inner(chunk.to_vec())
64    }))
65}
66
67/// A perfect k-ary subtree of the given `height`, rooted so its leftmost leaf is
68/// flat position `left`. Height 0 is a lone leaf.
69pub(crate) fn perfect(left: u64, height: u32, k: u64) -> ShapeNode {
70    if height == 0 {
71        return ShapeNode::Leaf(left);
72    }
73    let child_span = k.pow(height - 1);
74    let children = (0..k)
75        .map(|c| perfect(left + c * child_span, height - 1, k))
76        .collect();
77    ShapeNode::Inner(children)
78}
79
80/// The leftmost flat leaf position covered by `node`.
81pub(crate) fn leftmost(node: &ShapeNode) -> u64 {
82    match node {
83        ShapeNode::Leaf(pos) => *pos,
84        ShapeNode::Inner(children) => leftmost(&children[0]),
85    }
86}
87
88/// The rightmost flat leaf position covered by `node`.
89pub(crate) fn rightmost(node: &ShapeNode) -> u64 {
90    match node {
91        ShapeNode::Leaf(pos) => *pos,
92        ShapeNode::Inner(children) => rightmost(children.last().expect("inner node has children")),
93    }
94}
95
96/// Whether `node` covers flat position `index`.
97pub(crate) fn covers(node: &ShapeNode, index: u64) -> bool {
98    leftmost(node) <= index && index <= rightmost(node)
99}
100
101/// The CMT's concrete inclusion skeleton for `index` in a tree of `size` leaves
102/// at arity `k` — the rebalanced topology the spine verifier pins a proof
103/// against.
104///
105/// Walks the [`build`] shape from the root down to the leaf, emitting one
106/// [`SkeletonStep`] per inner node on the path; the steps are returned leaf →
107/// root, matching the proof path order. Returns `None` for an undefined shape
108/// (`k` out of range, `size == 0`, or `index >= size`).
109///
110/// This is the mutable peer of the append-only log's `mountain_skeleton`: the
111/// spine owns no concrete topology, so the CMT supplies its own. `build` is the
112/// single shape source, so the producer's `inclusion_path` and this verifier
113/// skeleton cannot drift.
114#[must_use]
115pub fn rebalanced_skeleton(size: u64, k: u64, index: u64) -> Option<Vec<SkeletonStep>> {
116    if index >= size {
117        return None;
118    }
119    let shape = build(size, k)?;
120    let mut steps = Vec::new();
121    descend_skeleton(&shape, index, &mut steps);
122    // `descend_skeleton` pushes root → leaf; the proof path is leaf → root.
123    steps.reverse();
124    Some(steps)
125}
126
127/// Push one [`SkeletonStep`] per inner node on the root → leaf path to `index`,
128/// in root → leaf order. A lone-child inner node never occurs in `build` (a
129/// perfect subtree or a `2..=k` group), so every emitted step hashes.
130fn descend_skeleton(node: &ShapeNode, index: u64, out: &mut Vec<SkeletonStep>) {
131    if let ShapeNode::Inner(children) = node {
132        let position = children
133            .iter()
134            .position(|c| covers(c, index))
135            .expect("the path index is covered by exactly one child");
136        out.push(SkeletonStep {
137            position,
138            sibling_count: children.len() - 1,
139        });
140        descend_skeleton(&children[position], index, out);
141    }
142}
143
144/// Bag a frontier's peaks into the CMT member root under `hasher` — the
145/// rebalanced fold, grouping the rightmost `k` (the mutable peer of the
146/// append-only log's backward-bag). Mirrors [`build`]'s grouping so the folded
147/// member root equals the live [`crate::Cmt::root`] over the same peaks. An empty
148/// peak set is the empty-tree root.
149#[must_use]
150pub fn rebalanced_bag(hasher: &dyn Hasher, peaks: &[Vec<u8>], k: u64) -> Vec<u8> {
151    if peaks.is_empty() {
152        return hasher.empty();
153    }
154    fold_frontier(peaks.to_vec(), k as usize, |chunk| {
155        let refs: Vec<&[u8]> = chunk.iter().map(|v| v.as_slice()).collect();
156        nary_mr(hasher, &refs)
157    })
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163
164    #[test]
165    fn rejects_undefined_shapes() {
166        assert_eq!(build(0, 2), None);
167        assert_eq!(build(4, 1), None);
168        assert_eq!(build(4, 257), None);
169    }
170
171    #[test]
172    fn singleton_is_a_lone_leaf() {
173        assert_eq!(build(1, 2), Some(ShapeNode::Leaf(0)));
174    }
175
176    /// Every flat position `0..size` appears exactly once, left to right.
177    #[test]
178    fn covers_every_position_in_order() {
179        for k in [2u64, 3, 5] {
180            for size in 1..=64u64 {
181                let mut seen = Vec::new();
182                collect_leaves(&build(size, k).expect("defined"), &mut seen);
183                let expected: Vec<u64> = (0..size).collect();
184                assert_eq!(seen, expected, "k={k} size={size}");
185            }
186        }
187    }
188
189    fn collect_leaves(node: &ShapeNode, out: &mut Vec<u64>) {
190        match node {
191            ShapeNode::Leaf(p) => out.push(*p),
192            ShapeNode::Inner(children) => {
193                for c in children {
194                    collect_leaves(c, out);
195                }
196            },
197        }
198    }
199}