Skip to main content

cml/
mountain.rs

1//! The Merkle Mountain Range commitment topology — the append-only log's own
2//! bagging of its perfect-subtree peaks, and the inclusion skeleton that pins a
3//! proof against it.
4//!
5//! The [`spine`] decomposes a log of `n` leaves into a frontier of perfect k-ary
6//! subtrees ([`spine::frontier_for_size`]) — the MMR **mountains**, whose peaks
7//! are permanent (a perfect subtree's hash never changes as the log grows). This
8//! module owns what the spine deliberately does not: how those peaks **bag** into
9//! one root, and what an inclusion proof points at.
10//!
11//! # The peak bag — root-preserving
12//!
13//! The peaks are bagged with the **established fold**: repeatedly group the
14//! rightmost `k` into one [`nary_mr`](spine::nary_mr) node, exactly the shape the
15//! structural `fold_frontier` and `cmt::shape::build` produce. So [`bag_peaks`]
16//! is byte-identical to that fold at every arity, and the MMR commitment **root
17//! is unchanged** from the pre-MMR log — durability is bought entirely in the
18//! *proof*, not by re-rooting the commitment.
19//!
20//! Each `H` is plain `nary_mr` (collapse + promotion, no domain separation, **no
21//! size prefix**) — deliberately unlike the OpenTimestamps/Grin `H(size ‖ peak ‖
22//! acc)` convention: those bag a sole commitment where `size` is otherwise
23//! unbound, so the prefix guards cross-size confusion. Our model binds
24//! `tree_size` as a trusted verifier parameter and closes malleability with the
25//! proven canonical-encoding uniqueness over `nary_mr` (`inclusion_proof_unique`),
26//! so the prefix's job is already discharged. Keeping the bag the established
27//! `nary_mr` fold is also what makes the root preservation hold and lets the Lean
28//! corpus transfer verbatim.
29//!
30//! # Durable witnesses
31//!
32//! An inclusion proof is **prove-to-peak then bag**: the `peakPath` from the leaf
33//! up to its own mountain's peak (entirely inside a permanent perfect subtree —
34//! the durable prefix) followed by the `bagPath` from the peak to the root. As
35//! the log grows the `peakPath` never changes; only the `bagPath` suffix is
36//! re-derived against the current peak set. That is the durable-witness property
37//! RFC-6962's rebalancing tree structurally cannot have — and it is achieved
38//! *without* changing the root: the old construction climbed the leaf through the
39//! rebalanced tree's ephemeral interior nodes, while this stops at the permanent
40//! peak and re-bags, over the very same peak fold.
41//!
42//! # One shape, two readers
43//!
44//! [`bag_shape`] builds the bag as an explicit tree once; [`bag_peaks`] evaluates
45//! it to the member root, and [`mountain_skeleton`] walks it to the per-step
46//! `(position, sibling_count)` skeleton the [`spine`] verifier pins a proof
47//! against. Deriving both from one shape is what keeps the producer's fold and
48//! the verifier's skeleton in lockstep (the same discipline `cmt::shape` uses for
49//! the rebalanced tree).
50
51use spine::{Hasher, ProofStep, SkeletonStep, fold_frontier, frontier_for_size, nary_mr};
52
53/// One node of the bag tree.
54///
55/// A `Peak(f_idx)` is a frontier peak at index `f_idx` (left-to-right, the
56/// [`frontier_for_size`] order); a `Bag(children)` is a hashing node over its
57/// children left-to-right. The shape is uniquely determined by `(peak_count,
58/// arity)` — never stored, always re-derived — so the producer's fold and the
59/// verifier's skeleton cannot drift.
60#[derive(Debug, Clone, PartialEq, Eq)]
61pub enum BagNode {
62    /// A frontier peak at index `f_idx` in the `frontier_for_size` order.
63    Peak(usize),
64    /// A hashing bag node over its children, left to right.
65    Bag(Vec<BagNode>),
66}
67
68/// Build the peak-bag shape over `peak_count` peaks at arity `k`. Returns `None`
69/// for an empty peak set (a non-empty log always has at least one peak).
70///
71/// The peaks are grouped by repeatedly folding the **rightmost `k`** into one bag
72/// node — the same shape `cmt::shape::build` produces — so `bag_peaks` over
73/// this shape is byte-identical to that fold at every arity, and the MMR
74/// commitment **root is unchanged** from the pre-MMR log. Durability does not
75/// come from the bag's associativity; it comes from what the *proof* points at
76/// (prove-to-peak), so the bag stays the established fold and only the inclusion
77/// path is restructured.
78///
79/// Expressed via [`fold_frontier`]: a single peak promotes to itself (no `Bag`
80/// wrapper); `2..=k` or more peaks fold rightmost-`k` until one root bag node
81/// remains.
82#[must_use]
83pub fn bag_shape(peak_count: usize, k: usize) -> Option<BagNode> {
84    if peak_count == 0 {
85        return None;
86    }
87    let frontier: Vec<BagNode> = (0..peak_count).map(BagNode::Peak).collect();
88    Some(fold_frontier(frontier, k, |chunk| {
89        BagNode::Bag(chunk.to_vec())
90    }))
91}
92
93/// Bag a frontier's peaks into the single member root under `hasher`.
94///
95/// Evaluates [`bag_shape`] with each [`nary_mr`] node hashing its children. A
96/// single peak promotes to itself; an empty peak set is the empty-tree root
97/// (`hasher.empty()`).
98#[must_use]
99pub fn bag_peaks(hasher: &dyn Hasher, peaks: &[Vec<u8>], k: u64) -> Vec<u8> {
100    match bag_shape(peaks.len(), k as usize) {
101        None => hasher.empty(),
102        Some(shape) => eval_bag(hasher, &shape, peaks),
103    }
104}
105
106/// Evaluate a bag shape to its digest, reading peak digests from `peaks`.
107fn eval_bag(hasher: &dyn Hasher, node: &BagNode, peaks: &[Vec<u8>]) -> Vec<u8> {
108    match node {
109        BagNode::Peak(f_idx) => peaks[*f_idx].clone(),
110        BagNode::Bag(children) => {
111            let child_digests: Vec<Vec<u8>> = children
112                .iter()
113                .map(|c| eval_bag(hasher, c, peaks))
114                .collect();
115            let refs: Vec<&[u8]> = child_digests.iter().map(Vec::as_slice).collect();
116            nary_mr(hasher, &refs)
117        },
118    }
119}
120
121/// Generate the `bagPath` proof steps that lift the peak at frontier index
122/// `f_idx` to the bagged root, given every peak's digest in `peaks`
123/// (left-to-right). The steps are ordered peak → root, so a caller appends them
124/// after the within-mountain `peakPath` to form the full leaf → root path.
125///
126/// Each step carries the path node's `position` among its bag-node siblings and
127/// those siblings' digests (the other children of that bag node) — exactly the
128/// shape [`spine::reconstruct_inclusion_root`] folds against, pinned by
129/// [`mountain_skeleton`]. Returns an empty path when the peak is the sole peak
130/// (it promotes directly to the root).
131#[must_use]
132pub fn bag_path(peaks: &[Vec<u8>], f_idx: usize, hasher: &dyn Hasher, k: u64) -> Vec<ProofStep> {
133    let mut steps = Vec::new();
134    if let Some(shape) = bag_shape(peaks.len(), k as usize) {
135        // Collected root → peak; reverse to peak → root for the leaf → root path.
136        descend_bag_path(&shape, f_idx, peaks, hasher, &mut steps);
137        steps.reverse();
138    }
139    steps
140}
141
142/// Walk from `node` to `Peak(f_idx)`, pushing one [`ProofStep`] per bag node
143/// crossed (root → peak order), with each step's sibling digests evaluated from
144/// `peaks`. Returns whether `f_idx` is under `node`.
145fn descend_bag_path(
146    node: &BagNode,
147    f_idx: usize,
148    peaks: &[Vec<u8>],
149    hasher: &dyn Hasher,
150    out: &mut Vec<ProofStep>,
151) -> bool {
152    match node {
153        BagNode::Peak(idx) => *idx == f_idx,
154        BagNode::Bag(children) => {
155            let Some(position) = children.iter().position(|c| covers_peak(c, f_idx)) else {
156                return false;
157            };
158            let siblings: Vec<Vec<u8>> = children
159                .iter()
160                .enumerate()
161                .filter(|(i, _)| *i != position)
162                .map(|(_, c)| eval_bag(hasher, c, peaks))
163                .collect();
164            out.push(ProofStep { siblings, position });
165            descend_bag_path(&children[position], f_idx, peaks, hasher, out)
166        },
167    }
168}
169
170/// Compute the MMR inclusion skeleton for a leaf `index` in a tree of
171/// `tree_size` leaves at arity `k`.
172///
173/// The skeleton runs leaf → root: the `peakPath` (the base-k digit steps inside
174/// the leaf's perfect-subtree mountain, each carrying `k - 1` siblings) followed
175/// by the `bagPath` (the bag-node steps from the peak up to the root). Returns
176/// `None` when the inputs cannot describe a valid log position (`k` out of range,
177/// empty frontier, or `index` outside the covered range).
178///
179/// This is the concrete topology the append-only log feeds the spine's abstract
180/// verifier — the MMR analog of the rebalanced skeleton the spine used to derive
181/// internally.
182#[must_use]
183pub fn mountain_skeleton(k: u64, tree_size: u64, index: u64) -> Option<Vec<SkeletonStep>> {
184    if !spine::ARITY_RANGE.contains(&k) {
185        return None;
186    }
187    let coords = frontier_for_size(tree_size, k);
188    if coords.is_empty() {
189        return None;
190    }
191    let k_usize = k as usize;
192
193    // Locate the perfect mountain that contains `index`.
194    let mut target = None;
195    for (f_idx, &(left, height)) in coords.iter().enumerate() {
196        let cap = k.checked_pow(height)?;
197        let limit = left.checked_add(cap)?;
198        if index >= left && index < limit {
199            target = Some((f_idx, left, height));
200            break;
201        }
202    }
203    let (f_idx, left, height) = target?;
204
205    let mut steps = Vec::with_capacity(height as usize);
206
207    // peakPath: base-k digits of the offset, low digit first (leaf → peak). These
208    // are the durable, permanent steps inside the perfect mountain.
209    let mut offset = index - left;
210    for _ in 0..height {
211        steps.push(SkeletonStep {
212            position: (offset % k) as usize,
213            sibling_count: k_usize - 1,
214        });
215        offset /= k;
216    }
217
218    // bagPath: the bag-node steps from the mountain's peak up to the root.
219    let shape = bag_shape(coords.len(), k_usize)?;
220    bag_path_steps(&shape, f_idx, &mut steps);
221
222    Some(steps)
223}
224
225/// Append the bag-path steps that reach `Peak(f_idx)` in `shape`, ordered from
226/// the peak up to the root, to `steps`.
227///
228/// At each bag node on the path, the step records the path child's `position`
229/// among its siblings and the `sibling_count`. Because the recursion descends
230/// from the root, the steps are collected root → peak and reversed into the
231/// leaf → root order the verifier expects.
232fn bag_path_steps(shape: &BagNode, f_idx: usize, steps: &mut Vec<SkeletonStep>) {
233    let mut down = Vec::new();
234    descend_bag(shape, f_idx, &mut down);
235    // `down` is root → peak; the skeleton is leaf → root, so the bag steps
236    // (above the peak) append in peak → root order.
237    down.reverse();
238    steps.extend(down);
239}
240
241/// Walk from `node` to `Peak(target)`, pushing one [`SkeletonStep`] per bag node
242/// crossed, ordered root → peak. Returns whether `target` is under `node`.
243fn descend_bag(node: &BagNode, target: usize, out: &mut Vec<SkeletonStep>) -> bool {
244    match node {
245        BagNode::Peak(f_idx) => *f_idx == target,
246        BagNode::Bag(children) => {
247            let position = children.iter().position(|c| covers_peak(c, target));
248            let Some(position) = position else {
249                return false;
250            };
251            out.push(SkeletonStep {
252                position,
253                sibling_count: children.len() - 1,
254            });
255            descend_bag(&children[position], target, out)
256        },
257    }
258}
259
260/// Whether `node` covers peak index `target`.
261///
262/// Used by the bag-shape descent in both [`descend_bag`] and [`descend_bag_path`].
263fn covers_peak(node: &BagNode, target: usize) -> bool {
264    match node {
265        BagNode::Peak(f_idx) => *f_idx == target,
266        BagNode::Bag(children) => children.iter().any(|c| covers_peak(c, target)),
267    }
268}
269
270#[cfg(test)]
271mod tests {
272    use sha2::{Digest, Sha256};
273
274    use super::*;
275
276    #[derive(Debug)]
277    struct H;
278    impl Hasher for H {
279        fn leaf(&self, data: &[u8]) -> Vec<u8> {
280            Sha256::digest(data).to_vec()
281        }
282
283        fn node(&self, children: &[&[u8]]) -> Vec<u8> {
284            let mut h = Sha256::new();
285            for c in children {
286                h.update(c);
287            }
288            h.finalize().to_vec()
289        }
290
291        fn empty(&self) -> Vec<u8> {
292            Sha256::digest(b"").to_vec()
293        }
294
295        fn hash(&self, data: &[u8]) -> Vec<u8> {
296            Sha256::digest(data).to_vec()
297        }
298
299        fn clone_box(&self) -> Box<dyn Hasher> {
300            Box::new(H)
301        }
302    }
303
304    /// A single peak promotes to the member root (no bag node).
305    #[test]
306    fn single_peak_promotes() {
307        let peak = vec![0xAB; 32];
308        assert_eq!(bag_peaks(&H, std::slice::from_ref(&peak), 2), peak);
309        // And its skeleton has no bag steps for a singleton tree.
310        assert_eq!(mountain_skeleton(2, 1, 0), Some(vec![]));
311    }
312
313    /// The bag is right-recursive: for m>k the oldest k-1 peaks are direct
314    /// children and the rest bag into the last child.
315    #[test]
316    fn bag_shape_groups_rightmost_k_binary() {
317        // k=2, 4 peaks: repeatedly group the rightmost 2 →
318        // H(P0, H(P1, H(P2, P3))). (At k=2 this coincides with a right-recursive
319        // fold, which is why the binary root is unchanged from the pre-MMR log.)
320        let shape = bag_shape(4, 2).unwrap();
321        let expected = BagNode::Bag(vec![
322            BagNode::Peak(0),
323            BagNode::Bag(vec![
324                BagNode::Peak(1),
325                BagNode::Bag(vec![BagNode::Peak(2), BagNode::Peak(3)]),
326            ]),
327        ]);
328        assert_eq!(shape, expected);
329    }
330
331    /// The bag fold equals a hand-rolled rightmost-k grouping over the peaks.
332    #[test]
333    fn bag_peaks_equals_hand_fold_binary() {
334        let peaks: Vec<Vec<u8>> = (0u8..4).map(|i| vec![i; 32]).collect();
335        // H(P0, H(P1, H(P2, P3)))
336        let inner = nary_mr(&H, &[peaks[2].as_slice(), peaks[3].as_slice()]);
337        let mid = nary_mr(&H, &[peaks[1].as_slice(), inner.as_slice()]);
338        let expected = nary_mr(&H, &[peaks[0].as_slice(), mid.as_slice()]);
339        assert_eq!(bag_peaks(&H, &peaks, 2), expected);
340    }
341
342    /// k-ary: with k=3 and 5 peaks, level 0 holds P0,P1 + bag(P2..P5);
343    /// the inner bag holds P2,P3,P4 (<= k, one node).
344    #[test]
345    fn bag_shape_kary() {
346        // k=3, 4 peaks — a case where rightmost-k grouping differs from a
347        // right-recursive fold: group the rightmost 3 → H(P0, H(P1, P2, P3)).
348        let shape = bag_shape(4, 3).unwrap();
349        let expected = BagNode::Bag(vec![
350            BagNode::Peak(0),
351            BagNode::Bag(vec![BagNode::Peak(1), BagNode::Peak(2), BagNode::Peak(3)]),
352        ]);
353        assert_eq!(shape, expected);
354    }
355
356    /// The skeleton emitted for every leaf matches the bag/peak structure:
357    /// every step carries at least one sibling (no promoted step) and the
358    /// position is within bounds.
359    #[test]
360    fn skeleton_steps_are_well_formed() {
361        for k in [2u64, 3, 5] {
362            for tree_size in 1..=130u64 {
363                for index in 0..tree_size {
364                    let sk = mountain_skeleton(k, tree_size, index).expect("valid position");
365                    for step in &sk {
366                        assert!(step.sibling_count >= 1, "k={k} n={tree_size} i={index}");
367                        assert!(step.position <= step.sibling_count);
368                    }
369                }
370            }
371        }
372    }
373
374    #[test]
375    fn rejects_out_of_range() {
376        assert_eq!(mountain_skeleton(1, 4, 0), None);
377        assert_eq!(mountain_skeleton(257, 4, 0), None);
378        assert_eq!(mountain_skeleton(2, 0, 0), None);
379        assert_eq!(mountain_skeleton(2, 4, 4), None);
380    }
381
382    // --- MMR essentials + durability seed (N51; N55 expands these) -----------
383
384    /// Synthetic distinct peak digests `[0x10+i; 32]` for a peak set of `m`.
385    fn peaks(m: usize) -> Vec<Vec<u8>> {
386        (0..m).map(|i| vec![0x10 + i as u8; 32]).collect()
387    }
388
389    /// **Prove-to-peak then bag verifies.** For every peak in every peak set, the
390    /// `bagPath` from that peak folds (under the abstract spine verifier, pinned by
391    /// the bag portion of `mountain_skeleton`) to the bagged root. Modelled at the
392    /// peak granularity: the peak stands in for "a leaf already lifted to its
393    /// mountain peak" (empty within-mountain `peakPath`), so the proof is purely
394    /// the `bagPath` and the skeleton is purely its bag steps.
395    #[test]
396    fn prove_to_peak_then_bag_verifies() {
397        for m in 1..=12usize {
398            let ps = peaks(m);
399            let root = bag_peaks(&H, &ps, 2);
400            for (f_idx, peak) in ps.iter().enumerate() {
401                let path = bag_path(&ps, f_idx, &H, 2);
402                // The bag skeleton for a peak: take the full skeleton of a tree
403                // whose mountain decomposition is exactly `m` height-0 peaks (a
404                // size-`m` tree at k=… would not give m singleton peaks, so build
405                // the skeleton directly from the bag shape instead).
406                let mut sk = Vec::new();
407                bag_path_steps(&bag_shape(m, 2).unwrap(), f_idx, &mut sk);
408                assert!(
409                    spine::verify_inclusion(&H, peak, &sk, &path, &root),
410                    "m={m} f_idx={f_idx}: prove-to-peak+bag must verify"
411                );
412                // A wrong peak digest at the same position must not verify.
413                let mut wrong = peak.clone();
414                wrong[0] ^= 0xFF;
415                assert!(
416                    !spine::verify_inclusion(&H, &wrong, &sk, &path, &root),
417                    "m={m} f_idx={f_idx}: a forged peak must not verify"
418                );
419            }
420        }
421    }
422
423    /// **Peak permanence across append.** A formed peak's digest is identical in
424    /// the size-`m` peak set and the size-`m+1` peak set — append adds/merges only
425    /// at the right edge and never rewrites an existing peak. (The leftmost,
426    /// oldest peaks are the most stable.)
427    #[test]
428    fn a_formed_peak_is_permanent_across_append() {
429        for m in 1..=12usize {
430            let before = peaks(m);
431            let after = peaks(m + 1);
432            // The first `m` peaks are byte-identical; only the new (rightmost)
433            // peak is added.
434            assert_eq!(&after[..m], &before[..], "m={m}: existing peaks rewritten");
435        }
436    }
437
438    /// **Durability seed.** A witness issued for a peak at size `m` (its `bagPath`)
439    /// verifies at size `m`, and the SAME peak — with a re-derived `bagPath` —
440    /// still verifies at size `m+1`. The peak's own digest (the durable prefix the
441    /// real `peakPath` would terminate at) never changes; only the short `bagPath`
442    /// suffix is extended. This is the property RFC-6962's rebalancing tree
443    /// structurally cannot have; N55 expands it to full leaf-level witnesses and a
444    /// metamorphic sweep.
445    #[test]
446    fn a_witness_survives_an_append() {
447        for m in 1..=12usize {
448            let f_idx = 0; // the oldest peak — the canonical durable witness.
449            let ps_n = peaks(m);
450            let ps_n1 = peaks(m + 1);
451
452            // The peak digest (the durable prefix endpoint) is unchanged.
453            assert_eq!(ps_n[f_idx], ps_n1[f_idx]);
454
455            // Issued at size m: verifies against the size-m root.
456            let root_n = bag_peaks(&H, &ps_n, 2);
457            let path_n = bag_path(&ps_n, f_idx, &H, 2);
458            let mut sk_n = Vec::new();
459            bag_path_steps(&bag_shape(m, 2).unwrap(), f_idx, &mut sk_n);
460            assert!(spine::verify_inclusion(
461                &H,
462                &ps_n[f_idx],
463                &sk_n,
464                &path_n,
465                &root_n
466            ));
467
468            // After append: the SAME peak, with a re-derived bagPath, verifies
469            // against the size-(m+1) root.
470            let root_n1 = bag_peaks(&H, &ps_n1, 2);
471            let path_n1 = bag_path(&ps_n1, f_idx, &H, 2);
472            let mut sk_n1 = Vec::new();
473            bag_path_steps(&bag_shape(m + 1, 2).unwrap(), f_idx, &mut sk_n1);
474            assert!(
475                spine::verify_inclusion(&H, &ps_n1[f_idx], &sk_n1, &path_n1, &root_n1),
476                "m={m}: a witness for the oldest peak must survive an append"
477            );
478        }
479    }
480}