Skip to main content

spine/
mr.rs

1//! Merkle root and evaluation over the n-ary subtree, with canonicalization.
2//!
3//! Canonicalization is one generic reduction composed of **two distinct
4//! primitives**, applied structurally on every fold (always-on, never a
5//! toggle):
6//!
7//! - **promotion** — a lone (single) child is lifted in place of the wrapping hashed node.
8//!   Structurally deterministic: a verifier re-derives it.
9//! - **collapse** — children of the *same value* fold to that value. The all-null case is *one
10//!   instance* of general same-value collapse, not a separate operation; an all-null run is just
11//!   the dominant instance in a sparse log.
12//!
13//! The literal `nary_mr` symbol predates this vocabulary and is kept verbatim;
14//! the two primitives are named in this prose, not split in the code here.
15
16use crate::hasher::Hasher;
17use crate::proof::ProofStep;
18use crate::subtree::Subtree;
19
20/// Compute the Merkle root of an ordered sequence of child digests.
21///
22/// Applies the two canonicalization primitives: **promotion** (the lone-child
23/// case) and **collapse** (the same-value fold). Collapse is general —
24/// *any* run of equal children folds to that value, with the all-null run the
25/// dominant instance in a sparse log, not a special case. The collapse is
26/// value-dependent and always-on; there is no toggle.
27///
28/// The collapsed value's *multiplicity* (how many leaves the run spans) is not
29/// in the digest — it is committed separately as the minimal run-extent
30/// (INV-AUTH-BOUNDARY), so distinct equal-entry runs are never conflated on
31/// unroll. For non-null runs that extent is mirrored across every algorithm's
32/// tree (equal logical data ⇒ equal digest under every hash) and rides free;
33/// only null runs are per-tree-divergent and are committed (the null-run-extents
34/// in the binding root).
35#[must_use]
36pub fn nary_mr(hasher: &dyn Hasher, children: &[&[u8]]) -> Vec<u8> {
37    match children.len() {
38        0 => hasher.empty(),
39        1 => children[0].to_vec(),
40        _ => {
41            // Fixed-width contract (see `Hasher`): the node hash concatenates
42            // these child digests with no length prefix, so the child *boundaries*
43            // are recoverable only if the children share a width — otherwise a
44            // different split of the same bytes is an equally valid child list and
45            // the node digest fails to bind its children. Checked here, at the one
46            // fold boundary, so a contract violation trips in debug/test builds
47            // rather than silently producing an unbinding root.
48            //
49            // The check is mutual sibling-width-equality, not equality to
50            // `hasher.digest_len()`: a binding-root node folds *other* algorithms'
51            // member roots (raw, opaque digests — D9) under this hasher, so the
52            // children are not this hasher's own outputs. Within a single tree
53            // they are, and either way the soundness property is that the siblings
54            // agree on a width.
55            debug_assert!(
56                children.windows(2).all(|w| w[0].len() == w[1].len()),
57                "node() children must share a digest width (got widths {:?}); the unprefixed \
58                 concatenation is otherwise not uniquely parseable",
59                children.iter().map(|c| c.len()).collect::<Vec<_>>()
60            );
61            // Collapse: if every child is the same value, the parent is that
62            // value. The all-null run is the dominant instance of this one rule.
63            let first = children[0];
64            if children.iter().all(|&c| c == first) {
65                first.to_vec()
66            } else {
67                hasher.node(children)
68            }
69        },
70    }
71}
72
73/// Recursively evaluate the root hash of a structured subtree.
74#[must_use]
75pub fn evaluate(hasher: &dyn Hasher, subtree: &Subtree) -> Vec<u8> {
76    match subtree {
77        Subtree::Leaf(data) => hasher.leaf(data),
78        Subtree::Node(children) => {
79            let child_hashes: Vec<Vec<u8>> = children.iter().map(|c| evaluate(hasher, c)).collect();
80            let child_refs: Vec<&[u8]> = child_hashes.iter().map(|c| c.as_slice()).collect();
81            nary_mr(hasher, &child_refs)
82        },
83    }
84}
85
86/// Count the total number of leaves in a structured subtree.
87#[must_use]
88pub fn count_leaves(subtree: &Subtree) -> u64 {
89    match subtree {
90        Subtree::Leaf(_) => 1,
91        Subtree::Node(children) => children.iter().map(count_leaves).sum(),
92    }
93}
94
95/// Generate the inclusion proof path for a leaf index within a structured subtree.
96///
97/// Returns `Some(path)` if the leaf index is found in the subtree, otherwise `None`.
98/// The `leaf_index` represents the flat 0-based leaf index inside this subtree.
99#[must_use]
100pub fn within_subtree_path(
101    hasher: &dyn Hasher,
102    subtree: &Subtree,
103    leaf_index: u64,
104) -> Option<Vec<ProofStep>> {
105    match subtree {
106        Subtree::Leaf(_) => {
107            if leaf_index == 0 {
108                Some(Vec::new())
109            } else {
110                None
111            }
112        },
113        Subtree::Node(children) => {
114            let mut cumulative_leaves = 0;
115            for (child_idx, child) in children.iter().enumerate() {
116                let child_leaves = count_leaves(child);
117                if leaf_index < cumulative_leaves + child_leaves {
118                    let mut path =
119                        within_subtree_path(hasher, child, leaf_index - cumulative_leaves)?;
120
121                    // Canonical proof encoding: a promoted (lone-child) node is
122                    // lifted to its parent without hashing, so it contributes no
123                    // proof step (the recursion already returns the correct
124                    // sub-path). Only genuinely hashing (multi-child) nodes emit
125                    // a step.
126                    if children.len() > 1 {
127                        let mut child_hashes = Vec::with_capacity(children.len());
128                        for c in children {
129                            child_hashes.push(evaluate(hasher, c));
130                        }
131                        child_hashes.remove(child_idx);
132                        path.push(ProofStep {
133                            siblings: child_hashes,
134                            position: child_idx,
135                        });
136                    }
137                    return Some(path);
138                }
139                cumulative_leaves += child_leaves;
140            }
141            None
142        },
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use sha2::{Digest, Sha256};
149
150    use super::*;
151
152    #[derive(Debug)]
153    struct Sha256Hasher;
154
155    impl Hasher for Sha256Hasher {
156        fn leaf(&self, data: &[u8]) -> Vec<u8> {
157            Sha256::digest(data).to_vec()
158        }
159
160        fn node(&self, children: &[&[u8]]) -> Vec<u8> {
161            let mut h = Sha256::new();
162            for child in children {
163                h.update(child);
164            }
165            h.finalize().to_vec()
166        }
167
168        fn empty(&self) -> Vec<u8> {
169            Sha256::digest(b"").to_vec()
170        }
171
172        fn hash(&self, data: &[u8]) -> Vec<u8> {
173            Sha256::digest(data).to_vec()
174        }
175
176        fn clone_box(&self) -> Box<dyn Hasher> {
177            Box::new(Sha256Hasher)
178        }
179    }
180
181    #[test]
182    fn test_nary_mr_empty() {
183        let hasher = Sha256Hasher;
184        let expected = Sha256::digest(b"").to_vec();
185        assert_eq!(nary_mr(&hasher, &[]), expected);
186    }
187
188    #[test]
189    fn test_nary_mr_promotion() {
190        let hasher = Sha256Hasher;
191        let leaf_hash = hasher.leaf(b"hello");
192        assert_eq!(nary_mr(&hasher, &[&leaf_hash]), leaf_hash);
193    }
194
195    #[test]
196    fn test_nary_mr_null_collapse() {
197        let hasher = Sha256Hasher;
198        let null = hasher.null();
199        // A node with two null children should collapse to null
200        assert_eq!(nary_mr(&hasher, &[&null, &null]), null);
201
202        // A node with a mix of null and non-null should NOT collapse to null
203        let leaf = hasher.leaf(b"hello");
204        let expected = hasher.node(&[&null, &leaf]);
205        assert_eq!(nary_mr(&hasher, &[&null, &leaf]), expected);
206    }
207
208    #[test]
209    fn test_nary_mr_general_same_value_collapse() {
210        let hasher = Sha256Hasher;
211        // General collapse: all children EQUAL to the same non-null value fold to
212        // that value. Null is one instance of this, not the rule.
213        let v = hasher.leaf(b"hello");
214        assert_eq!(nary_mr(&hasher, &[&v, &v]), v);
215        assert_eq!(nary_mr(&hasher, &[&v, &v, &v]), v);
216
217        // A mix of two distinct non-null values must NOT collapse — it hashes.
218        let w = hasher.leaf(b"world");
219        let expected = hasher.node(&[&v, &w]);
220        assert_eq!(nary_mr(&hasher, &[&v, &w]), expected);
221        assert_ne!(nary_mr(&hasher, &[&v, &w]), v);
222
223        // The null collapse is exactly the same rule at value = null().
224        let null = hasher.null();
225        assert_eq!(nary_mr(&hasher, &[&null, &null]), null);
226    }
227
228    #[test]
229    fn null_is_distinct_from_empty_data_leaf() {
230        // Hard constraint: null() = H(b"null") MUST stay distinct from a genuine
231        // empty-data leaf leaf(b"") = H(b""), so the null subset of collapses is
232        // unambiguous. The preimages differ (4 bytes vs 0), so the digests differ.
233        let hasher = Sha256Hasher;
234        assert_ne!(hasher.null(), hasher.leaf(b""));
235        // And a two-empty-leaf node collapses to the empty-leaf value (general
236        // collapse), which is itself distinct from null() — collapse never
237        // conflates an empty-data run with a null run.
238        let empty_leaf = hasher.leaf(b"");
239        assert_eq!(nary_mr(&hasher, &[&empty_leaf, &empty_leaf]), empty_leaf);
240        assert_ne!(nary_mr(&hasher, &[&empty_leaf, &empty_leaf]), hasher.null());
241    }
242
243    #[test]
244    fn test_evaluate_promotion_chain() {
245        let hasher = Sha256Hasher;
246        // Node([Node([Leaf("x")])])
247        let tree = Subtree::Node(vec![Subtree::Node(vec![Subtree::Leaf(b"x".to_vec())])]);
248        let expected = hasher.leaf(b"x");
249        assert_eq!(evaluate(&hasher, &tree), expected);
250    }
251
252    #[test]
253    fn test_within_subtree_path() {
254        let hasher = Sha256Hasher;
255        // Subtree: Node([Leaf("a"), Node([Leaf("b"), Leaf("c")]), Leaf("d")])
256        let subtree = Subtree::Node(vec![
257            Subtree::Leaf(b"a".to_vec()),
258            Subtree::Node(vec![
259                Subtree::Leaf(b"b".to_vec()),
260                Subtree::Leaf(b"c".to_vec()),
261            ]),
262            Subtree::Leaf(b"d".to_vec()),
263        ]);
264
265        let path = within_subtree_path(&hasher, &subtree, 1).unwrap();
266        assert_eq!(path.len(), 2);
267
268        let leaf_hash = hasher.leaf(b"b");
269        let root = evaluate(&hasher, &subtree);
270        let proof = crate::proof::InclusionProof { path };
271        // An explicit subtree carries no frontier grouping, so every step is a
272        // hash-chained subtree-prefix step and the skeleton is empty.
273        assert!(crate::proof::verify_inclusion(
274            &hasher,
275            &leaf_hash,
276            &[],
277            &proof.path,
278            &root
279        ));
280    }
281}