Skip to main content

ab_merkle_tree/
mmr.rs

1use crate::MerkleHasher;
2use crate::unbalanced::UnbalancedMerkleTree;
3#[cfg(feature = "alloc")]
4use alloc::boxed::Box;
5#[cfg(feature = "alloc")]
6use alloc::vec::Vec;
7use core::mem;
8use core::mem::MaybeUninit;
9use core::ops::{Deref, DerefMut};
10
11/// Number of peaks in MMR with `MAX_N` leaves
12pub const NUM_PEAKS<const MAX_N: u64>: usize = MAX_N.next_power_of_two().ilog2() as usize;
13/// Max number of elements in a proof for MMR with `MAX_N` leaves
14pub const MAX_PROOF_ELEMENTS<const MAX_N: u64>: usize = MAX_N.next_power_of_two().ilog2() as usize;
15const STACK_SIZE<const MAX_N: u64>: usize = {
16    // This constraint is actually needed to prevent issues related to MMR peaks, but making it
17    // unrepresentable is better
18    assert!(
19        MAX_N > 1,
20        "This Merkle Mountain Range must have MAX_N > 1 leaves"
21    );
22
23    MAX_N.next_power_of_two().ilog2() as usize + 1
24};
25
26/// Size of [`MerkleMountainRange`]/[`MerkleMountainRangeBytes`] in bytes
27const MERKLE_MOUNTAIN_RANGE_BYTES_SIZE<const MAX_N: u64, H: MerkleHasher>: usize =
28    size_of::<u64>() + H::OUT_LEN * (MAX_N.next_power_of_two().ilog2() as usize + 1);
29
30/// Hasher with a configurable output size for layout checks below
31#[derive(Debug)]
32struct LayoutHasher<const OUT_LEN: usize>;
33
34impl<const OUT_LEN: usize> MerkleHasher for LayoutHasher<OUT_LEN> {
35    const OUT_LEN: usize = OUT_LEN;
36
37    fn hash_pair(
38        &self,
39        pair: &[[u8; <Self as MerkleHasher>::OUT_LEN]; 2],
40    ) -> [u8; <Self as MerkleHasher>::OUT_LEN] {
41        let [left, _right] = pair;
42        *left
43    }
44}
45
46const {
47    assert!(
48        size_of::<MerkleMountainRangeBytes<2, LayoutHasher<32>>>()
49            == MERKLE_MOUNTAIN_RANGE_BYTES_SIZE::<2, LayoutHasher<32>>
50    );
51    assert!(
52        size_of::<MerkleMountainRange<2, LayoutHasher<32>>>()
53            == MERKLE_MOUNTAIN_RANGE_BYTES_SIZE::<2, LayoutHasher<32>>
54    );
55    assert!(
56        align_of::<MerkleMountainRangeBytes<2, LayoutHasher<32>>>()
57            == align_of::<MerkleMountainRange<2, LayoutHasher<32>>>()
58    );
59
60    // `MAX_N` doesn't have to be a power of two, verify the layout formula for a few more shapes,
61    // including a large one, to guard against regressions in `MERKLE_MOUNTAIN_RANGE_BYTES_SIZE`
62    assert!(
63        size_of::<MerkleMountainRangeBytes<100, LayoutHasher<32>>>()
64            == MERKLE_MOUNTAIN_RANGE_BYTES_SIZE::<100, LayoutHasher<32>>
65    );
66    assert!(
67        size_of::<MerkleMountainRange<100, LayoutHasher<32>>>()
68            == MERKLE_MOUNTAIN_RANGE_BYTES_SIZE::<100, LayoutHasher<32>>
69    );
70    assert!(
71        align_of::<MerkleMountainRangeBytes<100, LayoutHasher<32>>>()
72            == align_of::<MerkleMountainRange<100, LayoutHasher<32>>>()
73    );
74
75    assert!(
76        size_of::<MerkleMountainRangeBytes<65536, LayoutHasher<32>>>()
77            == MERKLE_MOUNTAIN_RANGE_BYTES_SIZE::<65536, LayoutHasher<32>>
78    );
79    assert!(
80        size_of::<MerkleMountainRange<65536, LayoutHasher<32>>>()
81            == MERKLE_MOUNTAIN_RANGE_BYTES_SIZE::<65536, LayoutHasher<32>>
82    );
83    assert!(
84        align_of::<MerkleMountainRangeBytes<65536, LayoutHasher<32>>>()
85            == align_of::<MerkleMountainRange<65536, LayoutHasher<32>>>()
86    );
87
88    // Hash size that is not a multiple of the alignment, in which case both types are padded the
89    // same way
90    assert!(
91        size_of::<MerkleMountainRangeBytes<4, LayoutHasher<20>>>()
92            == size_of::<MerkleMountainRange<4, LayoutHasher<20>>>()
93    );
94    assert!(
95        align_of::<MerkleMountainRangeBytes<4, LayoutHasher<20>>>()
96            == align_of::<MerkleMountainRange<4, LayoutHasher<20>>>()
97    );
98}
99
100/// MMR peaks for [`MerkleMountainRange`].
101///
102/// Primarily intended to be used with [`MerkleMountainRange::from_peaks()`], can be sent over the
103/// network, etc.
104#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)]
105pub struct MmrPeaks<const MAX_N: u64, H>
106where
107    H: MerkleHasher,
108{
109    /// Number of leaves in MMR
110    pub num_leaves: u64,
111    /// MMR peaks, first [`Self::num_peaks()`] elements are occupied by values, the rest are
112    /// ignored and do not need to be retained.
113    pub peaks: [[u8; H::OUT_LEN]; NUM_PEAKS::<MAX_N>],
114}
115
116impl<const MAX_N: u64, H> MmrPeaks<MAX_N, H>
117where
118    H: MerkleHasher,
119{
120    /// Number of peaks stored in [`Self::peaks`] that are occupied by actual values
121    #[inline(always)]
122    #[cfg_attr(feature = "no-panic", no_panic::no_panic)]
123    pub fn num_peaks(&self) -> u8 {
124        self.num_leaves.count_ones() as u8
125    }
126}
127
128/// Byte representation of [`MerkleMountainRange`] with correct alignment.
129///
130/// Somewhat similar in function to [`MmrPeaks`], but for local use only.
131#[derive(Debug, Copy, Clone)]
132#[repr(C, align(8))]
133pub struct MerkleMountainRangeBytes<const MAX_N: u64, H>(
134    [u8; MERKLE_MOUNTAIN_RANGE_BYTES_SIZE::<MAX_N, H>],
135)
136where
137    H: MerkleHasher;
138
139impl<const MAX_N: u64, H> Default for MerkleMountainRangeBytes<MAX_N, H>
140where
141    H: MerkleHasher,
142{
143    #[inline(always)]
144    fn default() -> Self {
145        Self([0; _])
146    }
147}
148
149impl<const MAX_N: u64, H> From<[u8; MERKLE_MOUNTAIN_RANGE_BYTES_SIZE::<MAX_N, H>]>
150    for MerkleMountainRangeBytes<MAX_N, H>
151where
152    H: MerkleHasher,
153{
154    fn from(value: [u8; MERKLE_MOUNTAIN_RANGE_BYTES_SIZE::<MAX_N, H>]) -> Self {
155        Self(value)
156    }
157}
158
159impl<const MAX_N: u64, H> From<MerkleMountainRangeBytes<MAX_N, H>>
160    for [u8; MERKLE_MOUNTAIN_RANGE_BYTES_SIZE::<MAX_N, H>]
161where
162    H: MerkleHasher,
163{
164    fn from(value: MerkleMountainRangeBytes<MAX_N, H>) -> Self {
165        value.0
166    }
167}
168
169impl<const MAX_N: u64, H> Deref for MerkleMountainRangeBytes<MAX_N, H>
170where
171    H: MerkleHasher,
172{
173    type Target = [u8; MERKLE_MOUNTAIN_RANGE_BYTES_SIZE::<MAX_N, H>];
174
175    #[inline(always)]
176    fn deref(&self) -> &Self::Target {
177        &self.0
178    }
179}
180
181impl<const MAX_N: u64, H> DerefMut for MerkleMountainRangeBytes<MAX_N, H>
182where
183    H: MerkleHasher,
184{
185    #[inline(always)]
186    fn deref_mut(&mut self) -> &mut Self::Target {
187        &mut self.0
188    }
189}
190
191/// Merkle Mountain Range variant that has pre-hashed leaves with arbitrary number of elements.
192///
193/// This can be considered a general case of [`UnbalancedMerkleTree`]. The root and proofs are
194/// identical for both. [`UnbalancedMerkleTree`] is more efficient and should be preferred when
195/// possible, while this data structure is designed for aggregating data incrementally over long
196/// periods of time.
197///
198/// `MAX_N` generic constant defines the maximum number of elements supported and controls stack
199/// usage.
200///
201/// Inner nodes are hashed with `H`, which must not depend on the tree size since it changes as
202/// leaves are added.
203#[derive(Debug, Copy, Clone)]
204#[repr(C)]
205pub struct MerkleMountainRange<const MAX_N: u64, H>
206where
207    H: MerkleHasher,
208{
209    num_leaves: u64,
210    // Stack of intermediate nodes per tree level
211    stack: [[u8; H::OUT_LEN]; STACK_SIZE::<MAX_N>],
212}
213
214impl<const MAX_N: u64, H> Default for MerkleMountainRange<MAX_N, H>
215where
216    [(); STACK_SIZE::<MAX_N>]:,
217    H: MerkleHasher + Default,
218{
219    #[inline(always)]
220    #[cfg_attr(feature = "no-panic", no_panic::no_panic)]
221    fn default() -> Self {
222        Self::new()
223    }
224}
225
226// TODO: Think harder about proof generation and verification API here
227impl<const MAX_N: u64, H> MerkleMountainRange<MAX_N, H>
228where
229    [(); STACK_SIZE::<MAX_N>]:,
230    H: MerkleHasher + Default,
231{
232    /// Create an empty instance
233    #[inline(always)]
234    #[cfg_attr(feature = "no-panic", no_panic::no_panic)]
235    pub fn new() -> Self {
236        // TODO: Technically redundant with the where-clause, which is not enforced in generic code
237        //  due to https://github.com/rust-lang/rust/issues/164019, remove once fixed
238        const { STACK_SIZE::<MAX_N> };
239
240        Self {
241            num_leaves: 0,
242            stack: [[0u8; H::OUT_LEN]; _],
243        }
244    }
245
246    /// Create a new instance from previously collected peaks.
247    ///
248    /// Returns `None` if input is invalid.
249    #[inline]
250    #[cfg_attr(feature = "no-panic", no_panic::no_panic)]
251    pub fn from_peaks(peaks: &MmrPeaks<MAX_N, H>) -> Option<Self> {
252        // TODO: Technically redundant with the where-clause, which is not enforced in generic code
253        //  due to https://github.com/rust-lang/rust/issues/164019, remove once fixed
254        const { STACK_SIZE::<MAX_N> };
255
256        // Stack and peaks may have space for more leaves than `MAX_N`
257        if peaks.num_leaves > MAX_N {
258            return None;
259        }
260
261        let mut result = Self {
262            num_leaves: peaks.num_leaves,
263            stack: [[0u8; H::OUT_LEN]; _],
264        };
265
266        // Convert peaks (where all occupied entries are all at the beginning of the list instead)
267        // to stack (where occupied entries are at corresponding offsets)
268        let mut stack_bits = peaks.num_leaves;
269        let mut peaks_offset = 0;
270
271        while stack_bits != 0 {
272            let stack_offset = stack_bits.trailing_zeros();
273
274            *result.stack.get_mut(stack_offset as usize)? = *peaks.peaks.get(peaks_offset)?;
275
276            peaks_offset += 1;
277            // Clear the lowest set bit
278            stack_bits &= !(1 << stack_offset);
279        }
280
281        Some(result)
282    }
283
284    /// Get byte representation of Merkle Mountain Range
285    #[inline(always)]
286    #[cfg_attr(feature = "no-panic", no_panic::no_panic)]
287    pub fn as_bytes(&self) -> &MerkleMountainRangeBytes<MAX_N, H> {
288        // SAFETY: Both are `#[repr(C)]`, the same size and alignment as `Self`, all bit patterns
289        // are valid
290        unsafe { mem::transmute(self) }
291    }
292
293    /// Create an instance from byte representation.
294    ///
295    /// # Safety
296    /// Bytes must be previously created by [`Self::as_bytes()`] of an instance of the same type.
297    #[inline(always)]
298    #[cfg_attr(feature = "no-panic", no_panic::no_panic)]
299    pub unsafe fn from_bytes(bytes: &MerkleMountainRangeBytes<MAX_N, H>) -> &Self {
300        // SAFETY: Both are `#[repr(C)]`, the same size and alignment as `Self`, all bit patterns
301        // are valid. `::from_bytes()` is an `unsafe` function with correct invariant being a
302        // prerequisite of calling it.
303        unsafe { mem::transmute(bytes) }
304    }
305
306    /// Get number of leaves aggregated in Merkle Mountain Range so far
307    #[inline(always)]
308    #[cfg_attr(feature = "no-panic", no_panic::no_panic)]
309    pub fn num_leaves(&self) -> u64 {
310        self.num_leaves
311    }
312
313    /// Calculate the root of Merkle Mountain Range.
314    ///
315    /// In case MMR contains a single leaf, [`MerkleHasher::hash_single_leaf()`] of it is returned,
316    /// `None` is returned if there were no leaves added yet.
317    #[inline]
318    #[cfg_attr(feature = "no-panic", no_panic::no_panic)]
319    pub fn root(&self) -> Option<[u8; H::OUT_LEN]> {
320        if self.num_leaves == 0 {
321            // If no leaves were provided
322            return None;
323        }
324
325        let hasher = H::default();
326        let mut root;
327        let mut stack_bits = self.num_leaves;
328        {
329            let lowest_active_level = stack_bits.trailing_zeros() as usize;
330            // SAFETY: Active level must have been set successfully before, hence it exists
331            root = *unsafe { self.stack.get_unchecked(lowest_active_level) };
332            // Clear lowest active level
333            stack_bits &= !(1 << lowest_active_level);
334        }
335
336        // Hash remaining peaks (if any) of the potentially unbalanced tree together
337        loop {
338            let lowest_active_level = stack_bits.trailing_zeros() as usize;
339
340            if lowest_active_level == u64::BITS as usize {
341                break;
342            }
343
344            // Clear lowest active level for next iteration
345            stack_bits &= !(1 << lowest_active_level);
346
347            // SAFETY: Active level must have been set successfully before, hence it exists
348            let lowest_active_level_item = unsafe { self.stack.get_unchecked(lowest_active_level) };
349
350            root = hasher.hash_pair(&[*lowest_active_level_item, root]);
351        }
352
353        if self.num_leaves == 1 {
354            root = hasher.hash_single_leaf(&root);
355        }
356
357        Some(root)
358    }
359
360    /// Get peaks of Merkle Mountain Range
361    #[inline]
362    #[cfg_attr(feature = "no-panic", no_panic::no_panic)]
363    pub fn peaks(&self) -> MmrPeaks<MAX_N, H> {
364        let mut result = MmrPeaks {
365            num_leaves: self.num_leaves,
366            peaks: [[0u8; H::OUT_LEN]; _],
367        };
368
369        // Convert stack (where occupied entries are at corresponding offsets) to peaks (where all
370        // occupied entries are all at the beginning of the list instead)
371        let mut stack_bits = self.num_leaves;
372        let mut peaks_offset = 0;
373        while stack_bits != 0 {
374            let stack_offset = stack_bits.trailing_zeros();
375
376            // SAFETY: Stack offset is always within the range of stack and peaks, this is
377            // guaranteed by internal invariants of the MMR
378            *unsafe { result.peaks.get_unchecked_mut(peaks_offset) } =
379                *unsafe { self.stack.get_unchecked(stack_offset as usize) };
380
381            peaks_offset += 1;
382            // Clear the lowest set bit
383            stack_bits &= !(1 << stack_offset);
384        }
385
386        result
387    }
388
389    /// Add leaf to Merkle Mountain Range.
390    ///
391    /// There is a more efficient version [`Self::add_leaves()`] in case multiple leaves are
392    /// available.
393    ///
394    /// Returns `true` on success, `false` if too many leaves were added.
395    #[inline]
396    #[cfg_attr(feature = "no-panic", no_panic::no_panic)]
397    pub fn add_leaf(&mut self, leaf: &[u8; H::OUT_LEN]) -> bool {
398        // How many leaves were processed so far. Should have been `num_leaves == MAX_N`, but `>=`
399        // helps compiler with panic safety checks.
400        if self.num_leaves >= MAX_N {
401            return false;
402        }
403
404        let hasher = H::default();
405        let mut current = *leaf;
406
407        // Every bit set to `1` corresponds to an active Merkle Tree level
408        let lowest_active_levels = self.num_leaves.trailing_ones() as usize;
409        for item in self.stack.iter().take(lowest_active_levels) {
410            current = hasher.hash_pair(&[*item, current]);
411        }
412
413        // Place the current hash at the first inactive level
414        *self
415            .stack
416            .get_mut(lowest_active_levels)
417            .expect("Stack has enough levels for `num_leaves < MAX_N`; qed") = current;
418        self.num_leaves += 1;
419
420        true
421    }
422
423    /// Add many leaves to Merkle Mountain Range.
424    ///
425    /// This is a more efficient version of [`Self::add_leaf()`] in case multiple leaves are
426    /// available.
427    ///
428    /// Returns `true` on success, `false` if too many leaves were added.
429    #[inline]
430    #[cfg_attr(feature = "no-panic", no_panic::no_panic)]
431    pub fn add_leaves<'a, Item, Iter>(&mut self, leaves: Iter) -> bool
432    where
433        Item: Into<[u8; H::OUT_LEN]>,
434        Iter: IntoIterator<Item = Item> + 'a,
435    {
436        let hasher = H::default();
437
438        // TODO: This can be optimized further
439        for leaf in leaves {
440            // How many leaves were processed so far. Should have been `num_leaves == MAX_N`, but
441            // `>=` helps compiler with panic safety checks.
442            if self.num_leaves >= MAX_N {
443                return false;
444            }
445
446            let mut current = leaf.into();
447
448            // Every bit set to `1` corresponds to an active Merkle Tree level
449            let lowest_active_levels = self.num_leaves.trailing_ones() as usize;
450            for item in self.stack.iter().take(lowest_active_levels) {
451                current = hasher.hash_pair(&[*item, current]);
452            }
453
454            // Place the current hash at the first inactive level
455            self.stack[lowest_active_levels] = current;
456            self.num_leaves += 1;
457        }
458
459        true
460    }
461
462    /// Add leaf to Merkle Mountain Range and generate inclusion proof.
463    ///
464    /// Returns `Some((root, proof))` on success, `None` if too many leaves were added.
465    #[expect(clippy::type_complexity, reason = "Return type")]
466    #[inline]
467    #[cfg(feature = "alloc")]
468    pub fn add_leaf_and_compute_proof(
469        &mut self,
470        leaf: &[u8; H::OUT_LEN],
471    ) -> Option<([u8; H::OUT_LEN], Vec<[u8; H::OUT_LEN]>)> {
472        // SAFETY: Inner value is `MaybeUninit`
473        let mut proof =
474            unsafe { Box::<[MaybeUninit<[u8; H::OUT_LEN]>; _]>::new_uninit().assume_init() };
475
476        let (root, proof_length) = self.add_leaf_and_compute_proof_inner(leaf, &mut proof)?;
477
478        let proof_capacity = proof.len();
479        let proof = Box::into_raw(proof);
480        // SAFETY: Points to correctly allocated memory where `proof_length` elements were
481        // initialized
482        let proof = unsafe {
483            Vec::from_raw_parts(
484                proof.cast::<[u8; H::OUT_LEN]>(),
485                proof_length,
486                proof_capacity,
487            )
488        };
489
490        Some((root, proof))
491    }
492
493    /// Add leaf to Merkle Mountain Range and generate inclusion proof.
494    ///
495    /// Returns `Some((root, proof))` on success, `None` if too many leaves were added.
496    #[expect(clippy::type_complexity, reason = "Return type")]
497    #[inline]
498    #[cfg_attr(feature = "no-panic", no_panic::no_panic)]
499    pub fn add_leaf_and_compute_proof_in<'proof>(
500        &mut self,
501        leaf: &[u8; H::OUT_LEN],
502        proof: &'proof mut [MaybeUninit<[u8; H::OUT_LEN]>; MAX_PROOF_ELEMENTS::<MAX_N>],
503    ) -> Option<([u8; H::OUT_LEN], &'proof mut [[u8; H::OUT_LEN]])> {
504        let (root, proof_length) = self.add_leaf_and_compute_proof_inner(leaf, proof)?;
505
506        // SAFETY: Just correctly initialized `proof_length` elements
507        let proof = unsafe { proof.get_unchecked_mut(..proof_length).assume_init_mut() };
508
509        Some((root, proof))
510    }
511
512    #[inline]
513    #[cfg_attr(feature = "no-panic", no_panic::no_panic)]
514    fn add_leaf_and_compute_proof_inner(
515        &mut self,
516        leaf: &[u8; H::OUT_LEN],
517        proof: &mut [MaybeUninit<[u8; H::OUT_LEN]>; MAX_PROOF_ELEMENTS::<MAX_N>],
518    ) -> Option<([u8; H::OUT_LEN], usize)> {
519        let hasher = H::default();
520        let mut proof_length = 0;
521
522        let current_target_level;
523        let mut position = self.num_leaves;
524
525        {
526            // How many leaves were processed so far. Should have been `num_leaves == MAX_N`, but
527            // `>=` helps compiler with panic safety checks.
528            if self.num_leaves >= MAX_N {
529                return None;
530            }
531
532            let mut current = *leaf;
533
534            // Every bit set to `1` corresponds to an active Merkle Tree level
535            let lowest_active_levels = self.num_leaves.trailing_ones() as usize;
536
537            for item in self.stack.iter().take(lowest_active_levels) {
538                // If at the target leaf index, need to collect the proof
539                proof
540                    .get_mut(proof_length)
541                    .expect("Proof has enough capacity for `num_leaves < MAX_N` checked above; qed")
542                    .write(*item);
543                proof_length += 1;
544
545                current = hasher.hash_pair(&[*item, current]);
546
547                // Move up the tree
548                position /= 2;
549            }
550
551            current_target_level = lowest_active_levels;
552
553            // Place the current hash at the first inactive level
554            self.stack[lowest_active_levels] = current;
555            self.num_leaves += 1;
556        }
557
558        let mut root;
559        let mut stack_bits = self.num_leaves;
560
561        {
562            let lowest_active_level = stack_bits.trailing_zeros() as usize;
563            // SAFETY: Active level must have been set successfully before, hence it exists
564            root = *unsafe { self.stack.get_unchecked(lowest_active_level) };
565            // Clear lowest active level
566            stack_bits &= !(1 << lowest_active_level);
567        }
568
569        // Hash remaining peaks (if any) of the potentially unbalanced tree together and collect
570        // proof hashes
571        let mut merged_peaks = false;
572        loop {
573            let lowest_active_level = stack_bits.trailing_zeros() as usize;
574
575            if lowest_active_level == u64::BITS as usize {
576                break;
577            }
578
579            // Clear lowest active level for next iteration
580            stack_bits &= !(1 << lowest_active_level);
581
582            // SAFETY: Active level must have been set successfully before, hence it exists
583            let lowest_active_level_item = unsafe { self.stack.get_unchecked(lowest_active_level) };
584
585            if lowest_active_level > current_target_level
586                || (lowest_active_level == current_target_level
587                    && !position.is_multiple_of(2)
588                    && !merged_peaks)
589            {
590                // SAFETY: Method signature guarantees upper bound of the proof length
591                unsafe { proof.get_unchecked_mut(proof_length) }.write(*lowest_active_level_item);
592                proof_length += 1;
593                merged_peaks = false;
594            } else if lowest_active_level == current_target_level {
595                // SAFETY: Method signature guarantees upper bound of the proof length
596                unsafe { proof.get_unchecked_mut(proof_length) }.write(root);
597                proof_length += 1;
598                merged_peaks = false;
599            } else {
600                // Not collecting proof because of the need to merge peaks of an unbalanced tree
601                merged_peaks = true;
602            }
603
604            // Collect the lowest peak into the proof
605            root = hasher.hash_pair(&[*lowest_active_level_item, root]);
606
607            position /= 2;
608        }
609
610        if self.num_leaves == 1 {
611            root = hasher.hash_single_leaf(&root);
612        }
613
614        Some((root, proof_length))
615    }
616
617    /// Verify a Merkle proof for a leaf at the given index.
618    ///
619    /// NOTE: `MAX_N` constant doesn't matter here and can be anything that is `> 1`.
620    #[inline]
621    #[cfg_attr(feature = "no-panic", no_panic::no_panic)]
622    pub fn verify(
623        root: &[u8; H::OUT_LEN],
624        proof: &[[u8; H::OUT_LEN]],
625        leaf_index: u64,
626        leaf: [u8; H::OUT_LEN],
627        num_leaves: u64,
628    ) -> bool {
629        // TODO: Technically redundant with the where-clause, which is not enforced in generic code
630        //  due to https://github.com/rust-lang/rust/issues/164019, remove once fixed
631        const { STACK_SIZE::<MAX_N> };
632
633        UnbalancedMerkleTree::<H>::verify(root, proof, leaf_index, leaf, num_leaves)
634    }
635}