commonware-storage 2026.4.0

Persist and retrieve data from an abstract store.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
//! MMR-specific proof tests.

#[cfg(test)]
mod tests {
    use crate::merkle::{
        self as merkle,
        mmr::{
            iterator::PeakIterator, mem::Mmr, Error, Family, Location, Position,
            StandardHasher as Standard,
        },
        proof::{nodes_required_for_multi_proof, Blueprint},
        Family as _, LocationRangeExt as _,
    };
    use commonware_cryptography::{sha256::Digest, Hasher, Sha256};

    fn test_digest(v: u8) -> Digest {
        Sha256::hash(&[v])
    }

    #[test]
    fn test_proving_digests_from_range() {
        // create a new MMR and add a non-trivial amount (49) of elements
        let hasher: Standard<Sha256> = Standard::new();
        let mut mmr = Mmr::new(&hasher);
        let elements: Vec<_> = (0..49).map(test_digest).collect();
        let batch = {
            let mut batch = mmr.new_batch();
            for element in &elements {
                batch = batch.add(&hasher, element);
            }
            batch.merkleize(&mmr, &hasher)
        };
        mmr.apply_batch(&batch).unwrap();
        let root = mmr.root();

        // Test 1: compute_digests over the entire range should contain a digest for every node
        // in the tree.
        let proof = mmr
            .range_proof(&hasher, Location::new(0)..mmr.leaves())
            .unwrap();
        let mut node_digests = proof
            .verify_range_inclusion_and_extract_digests(&hasher, &elements, Location::new(0), root)
            .unwrap();
        assert_eq!(node_digests.len() as u64, mmr.size());
        node_digests.sort_by_key(|(pos, _)| *pos);
        for (i, (pos, d)) in node_digests.into_iter().enumerate() {
            assert_eq!(pos, i as u64);
            assert_eq!(mmr.get_node(pos).unwrap(), d);
        }
        // Make sure the wrong root fails.
        let wrong_root = elements[0]; // any other digest will do
        assert!(matches!(
            proof.verify_range_inclusion_and_extract_digests(
                &hasher,
                &elements,
                Location::new(0),
                &wrong_root
            ),
            Err(Error::RootMismatch)
        ));

        // Test 2: Single element range (first element)
        let range = Location::new(0)..Location::new(1);
        let single_proof = mmr.range_proof(&hasher, range.clone()).unwrap();
        let range_start = range.start;
        let single_digests = single_proof
            .verify_range_inclusion_and_extract_digests(
                &hasher,
                &elements[range.to_usize_range()],
                range_start,
                root,
            )
            .unwrap();
        assert!(single_digests.len() > 1);

        // Test 3: Single element range (middle element)
        let mid_idx = 24;
        let range = Location::new(mid_idx)..Location::new(mid_idx + 1);
        let range_start = range.start;
        let mid_proof = mmr.range_proof(&hasher, range.clone()).unwrap();
        let mid_digests = mid_proof
            .verify_range_inclusion_and_extract_digests(
                &hasher,
                &elements[range.to_usize_range()],
                range_start,
                root,
            )
            .unwrap();
        assert!(mid_digests.len() > 1);

        // Test 4: Single element range (last element)
        let last_idx = elements.len() as u64 - 1;
        let range = Location::new(last_idx)..Location::new(last_idx + 1);
        let range_start = range.start;
        let last_proof = mmr.range_proof(&hasher, range.clone()).unwrap();
        let last_digests = last_proof
            .verify_range_inclusion_and_extract_digests(
                &hasher,
                &elements[range.to_usize_range()],
                range_start,
                root,
            )
            .unwrap();
        assert!(!last_digests.is_empty());

        // Test 5: Small range at the beginning
        let range = Location::new(0)..Location::new(5);
        let range_start = range.start;
        let small_proof = mmr.range_proof(&hasher, range.clone()).unwrap();
        let small_digests = small_proof
            .verify_range_inclusion_and_extract_digests(
                &hasher,
                &elements[range.to_usize_range()],
                range_start,
                root,
            )
            .unwrap();
        // Verify that we get digests for the range elements and their ancestors
        assert!(small_digests.len() > 5);

        // Test 6: Medium range in the middle
        let range = Location::new(10)..Location::new(31);
        let range_start = range.start;
        let mid_range_proof = mmr.range_proof(&hasher, range.clone()).unwrap();
        let mid_range_digests = mid_range_proof
            .verify_range_inclusion_and_extract_digests(
                &hasher,
                &elements[range.to_usize_range()],
                range_start,
                root,
            )
            .unwrap();
        let num_elements = range.end - range.start;
        assert!(mid_range_digests.len() as u64 > num_elements);
    }

    #[test]
    fn test_max_location_is_provable() {
        // MAX_LEAVES is the largest valid leaf count.
        let max_loc = Family::MAX_LEAVES;
        let max_loc_plus_1 = Location::new(*max_loc + 1);

        let result = Blueprint::new(max_loc, max_loc - 1..max_loc);
        assert!(
            result.is_ok(),
            "Should be able to prove with MAX_LEAVES leaves"
        );

        // MAX_LEAVES + 1 should be rejected.
        let result_overflow = Blueprint::new(max_loc_plus_1, max_loc..max_loc_plus_1);
        assert!(
            result_overflow.is_err(),
            "Should reject location > MAX_LEAVES"
        );
        assert!(matches!(
            result_overflow,
            Err(merkle::Error::LocationOverflow(_))
        ));
    }

    #[test]
    fn test_max_location_multi_proof() {
        let max_loc = Family::MAX_LEAVES;
        let result = nodes_required_for_multi_proof(max_loc, &[max_loc - 1]);
        assert!(
            result.is_ok(),
            "Should be able to generate multi-proof for MAX_LEAVES"
        );

        // MAX_LEAVES + 1 should be rejected.
        let invalid_loc = max_loc + 1;
        let result_overflow = nodes_required_for_multi_proof(invalid_loc, &[max_loc]);
        assert!(
            result_overflow.is_err(),
            "Should reject location > MAX_LEAVES in multi-proof"
        );
    }

    #[test]
    fn test_max_proof_digests_per_element_sufficient() {
        // Verify that MAX_PROOF_DIGESTS_PER_ELEMENT (122) is sufficient for any single-element
        // proof in the largest valid MMR.
        //
        // MMR sizes follow: mmr_size(N) = 2*N - popcount(N) where N = leaf count.
        // The number of peaks equals popcount(N).
        //
        // To maximize peaks, we want N with maximum popcount. N = 2^62 - 1 has 62 one-bits:
        //   N = 0x3FFFFFFFFFFFFFFF = 2^0 + 2^1 + ... + 2^61
        //
        // This gives us 62 perfect binary trees with leaf counts 2^0, 2^1, ..., 2^61
        // and corresponding heights 0, 1, ..., 61.
        //
        // mmr_size(2^62 - 1) = 2*(2^62 - 1) - 62 = 2^63 - 2 - 62 = 2^63 - 64
        //
        // For a single-element proof in a tree of height h:
        //   - Path siblings from leaf to peak: h digests
        //   - Other peaks (not containing the element): (62 - 1) = 61 digests
        //   - Total: h + 61 digests
        //
        // Worst case: element in tallest tree (h = 61)
        //   - Path siblings: 61
        //   - Other peaks: 61
        //   - Total: 61 + 61 = 122 digests

        const NUM_PEAKS: usize = 62;
        const MAX_TREE_HEIGHT: usize = 61;
        const EXPECTED_WORST_CASE: usize = MAX_TREE_HEIGHT + (NUM_PEAKS - 1);

        let many_peaks_size = Position::new((1u64 << 63) - 64);
        assert!(
            many_peaks_size.is_valid_size(),
            "Size {many_peaks_size} should be a valid MMR size",
        );

        let peak_count = PeakIterator::new(many_peaks_size).count();
        assert_eq!(peak_count, NUM_PEAKS);

        // Verify the peak heights are 61, 60, ..., 1, 0 (from left to right)
        let peaks: Vec<_> = PeakIterator::new(many_peaks_size).collect();
        for (i, &(_pos, height)) in peaks.iter().enumerate() {
            let expected_height = (NUM_PEAKS - 1 - i) as u32;
            assert_eq!(
                height, expected_height,
                "Peak {i} should have height {expected_height}, got {height}",
            );
        }

        // Test location 0 (leftmost leaf, in tallest tree of height 61)
        // Expected: 61 path siblings + 61 other peaks = 122 digests
        let leaves = Location::try_from(many_peaks_size).unwrap();
        let loc = Location::new(0);
        let bp =
            Blueprint::new(leaves, loc..loc + 1).expect("should compute blueprint for location 0");
        let total_nodes = bp.fold_prefix.len() + bp.fetch_nodes.len();

        assert_eq!(
            total_nodes,
            EXPECTED_WORST_CASE,
            "Location 0 proof should require exactly {EXPECTED_WORST_CASE} digests (61 path + 61 peaks)",
        );

        // Test the rightmost leaf (in smallest tree of height 0, which is itself a peak)
        // Expected: 0 path siblings + 61 other peaks = 61 digests
        let last_leaf_loc = leaves - 1;
        let bp = Blueprint::new(leaves, last_leaf_loc..last_leaf_loc + 1)
            .expect("should compute blueprint for last leaf");
        let total_nodes = bp.fold_prefix.len() + bp.fetch_nodes.len();

        let expected_last_leaf = NUM_PEAKS - 1;
        assert_eq!(
            total_nodes,
            expected_last_leaf,
            "Last leaf proof should require exactly {expected_last_leaf} digests (0 path + 61 peaks)",
        );
    }

    #[test]
    fn test_max_proof_digests_per_element_is_maximum() {
        // For K peaks, the worst-case proof needs: (max_tree_height) + (K - 1) digests
        // With K peaks of heights K-1, K-2, ..., 0, this is (K-1) + (K-1) = 2*(K-1)
        //
        // To get K peaks, leaf count N must have exactly K bits set.
        // MMR size = 2*N - popcount(N) = 2*N - K
        //
        // For 63 peaks: N = 2^63 - 1 (63 bits set), size = 2*(2^63 - 1) - 63 = 2^64 - 65

        // This exceeds MAX_NODES, so is_valid_size() returns false.

        let n_for_63_peaks = (1u128 << 63) - 1;
        let size_for_63_peaks = 2 * n_for_63_peaks - 63; // = 2^64 - 65
        assert!(
            size_for_63_peaks > *Family::MAX_NODES as u128,
            "63 peaks requires size {size_for_63_peaks} > MAX_NODES",
        );

        let size_truncated = size_for_63_peaks as u64;
        assert!(
            !Position::new(size_truncated).is_valid_size(),
            "Size for 63 peaks should fail is_valid_size()"
        );
    }

    /// Regression test: pinned nodes that are sibling digests (not fold-prefix peaks) must be
    /// verified against the extracted proof digests. A 3-leaf MMR with start_loc=1 has a pinned
    /// node at position 0 (L0) which is a sibling within the range peak, not a fold-prefix peak.
    #[test]
    fn test_verify_proof_and_pinned_nodes_sibling_case() {
        let hasher: Standard<Sha256> = Standard::new();
        let mut mmr = Mmr::new(&hasher);
        let elements: Vec<Digest> = (0..3).map(test_digest).collect();
        let batch = {
            let mut batch = mmr.new_batch();
            for e in &elements {
                batch = batch.add(&hasher, e);
            }
            batch.merkleize(&mmr, &hasher)
        };
        mmr.apply_batch(&batch).unwrap();
        let root = mmr.root();

        // Proof for range [1, 3) -- fold prefix is empty, pinned node at position 0 is a sibling.
        let start_loc = Location::new(1);
        let proof = mmr
            .range_proof(&hasher, start_loc..Location::new(3))
            .unwrap();

        let pinned: Vec<Digest> = mmr.nodes_to_pin(start_loc).into_values().collect();
        assert_eq!(pinned.len(), 1, "should have exactly one pinned node");

        // Correct pinned nodes must verify.
        assert!(
            proof.verify_proof_and_pinned_nodes(&hasher, &elements[1..], start_loc, &pinned, root,),
            "valid pinned nodes should verify"
        );

        // Wrong pinned digest must fail.
        let bad_pinned = vec![test_digest(99)];
        assert!(
            !proof.verify_proof_and_pinned_nodes(
                &hasher,
                &elements[1..],
                start_loc,
                &bad_pinned,
                root,
            ),
            "wrong pinned digest should fail"
        );

        // Extra pinned node must fail.
        let extra_pinned = vec![pinned[0], test_digest(42)];
        assert!(
            !proof.verify_proof_and_pinned_nodes(
                &hasher,
                &elements[1..],
                start_loc,
                &extra_pinned,
                root,
            ),
            "extra pinned node should fail"
        );

        // Empty pinned nodes must fail (start_loc > 0 requires at least one).
        assert!(
            !proof.verify_proof_and_pinned_nodes(&hasher, &elements[1..], start_loc, &[], root,),
            "missing pinned nodes should fail"
        );
    }

    /// Test verify_proof_and_pinned_nodes when pinned nodes ARE fold-prefix peaks.
    #[test]
    fn test_verify_proof_and_pinned_nodes_fold_prefix_case() {
        let hasher: Standard<Sha256> = Standard::new();
        let mut mmr = Mmr::new(&hasher);
        // 10-leaf MMR: peaks at positions covering [0-7] and [8-9].
        // start_loc=8 puts the first peak entirely in the fold prefix.
        let elements: Vec<Digest> = (0..10).map(test_digest).collect();
        let batch = {
            let mut batch = mmr.new_batch();
            for e in &elements {
                batch = batch.add(&hasher, e);
            }
            batch.merkleize(&mmr, &hasher)
        };
        mmr.apply_batch(&batch).unwrap();
        let root = mmr.root();

        let start_loc = Location::new(8);
        let proof = mmr
            .range_proof(&hasher, start_loc..Location::new(10))
            .unwrap();

        let pinned: Vec<Digest> = mmr.nodes_to_pin(start_loc).into_values().collect();
        assert_eq!(pinned.len(), 1, "should have one fold-prefix peak");

        assert!(
            proof.verify_proof_and_pinned_nodes(&hasher, &elements[8..], start_loc, &pinned, root,),
            "valid fold-prefix pinned nodes should verify"
        );

        // Wrong digest must fail.
        assert!(
            !proof.verify_proof_and_pinned_nodes(
                &hasher,
                &elements[8..],
                start_loc,
                &[test_digest(99)],
                root,
            ),
            "wrong fold-prefix digest should fail"
        );
    }

    #[cfg(feature = "arbitrary")]
    mod conformance {
        use super::Family;
        use crate::merkle::proof::Proof;
        use commonware_codec::conformance::CodecConformance;
        use commonware_cryptography::sha256::Digest as Sha256Digest;

        commonware_conformance::conformance_tests! {
            CodecConformance<Proof<Family, Sha256Digest>>,
        }
    }
}