commonware_storage/mmr/
location.rs

1use super::position::Position;
2use crate::mmr::MAX_POSITION;
3use core::{
4    convert::TryFrom,
5    fmt,
6    ops::{Add, AddAssign, Deref, Range, Sub, SubAssign},
7};
8use thiserror::Error;
9
10/// Maximum valid [Location] value that can exist in a valid MMR.
11///
12/// This limit exists because the total MMR size (number of nodes) must be representable in a u64
13/// with at least one leading zero bit for validity checking. The MMR size for N leaves is:
14///
15/// ```text
16/// MMR_size = 2*N - popcount(N)
17/// ```
18///
19/// where `popcount(N)` is the number of set bits in N (the number of binary trees in the MMR forest).
20///
21/// The worst case occurs when N is a power of 2 (popcount = 1), giving `MMR_size = 2*N - 1`.
22///
23/// For validity, we require `MMR_size < 2^63` (top bit clear), which gives us:
24///
25/// ```text
26/// 2*N - 1 < 2^63
27/// 2*N < 2^63 + 1
28/// N ≤ 2^62
29/// ```
30///
31/// Therefore, the maximum number of leaves is `2^62`, and the maximum location (0-indexed) is `2^62 - 1`.
32///
33/// ## Verification
34///
35/// For `N = 2^62` leaves (worst case):
36/// - `MMR_size = 2 * 2^62 - 1 = 2^63 - 1 = 0x7FFF_FFFF_FFFF_FFFF` ✓
37/// - Leading zeros: 1 ✓
38///
39/// For `N = 2^62 + 1` leaves:
40/// - `2 * N = 2^63 + 2` ✗ (exceeds maximum valid MMR size)
41pub const MAX_LOCATION: u64 = 0x3FFF_FFFF_FFFF_FFFF; // 2^62 - 1
42
43/// A [Location] is an index into an MMR's _leaves_.
44/// This is in contrast to a [Position], which is an index into an MMR's _nodes_.
45///
46/// # Limits
47///
48/// While [Location] can technically hold any `u64` value, only values up to [MAX_LOCATION]
49/// can be safely converted to [Position]. Values beyond this are considered invalid.
50#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Default, Debug)]
51pub struct Location(u64);
52
53impl Location {
54    /// Create a new [Location] from a raw `u64` without validation.
55    ///
56    /// This is an internal constructor that assumes the value is valid. For creating
57    /// locations from external or untrusted sources, use [Location::new].
58    #[inline]
59    pub(crate) const fn new_unchecked(loc: u64) -> Self {
60        Self(loc)
61    }
62
63    /// Create a new [Location] from a raw `u64`, validating it does not exceed [MAX_LOCATION].
64    ///
65    /// Returns `None` if `loc > MAX_LOCATION`.
66    ///
67    /// # Examples
68    ///
69    /// ```
70    /// use commonware_storage::mmr::{Location, MAX_LOCATION};
71    ///
72    /// let loc = Location::new(100).unwrap();
73    /// assert_eq!(*loc, 100);
74    ///
75    /// // Values at MAX_LOCATION are valid
76    /// assert!(Location::new(MAX_LOCATION).is_some());
77    ///
78    /// // Values exceeding MAX_LOCATION return None
79    /// assert!(Location::new(MAX_LOCATION + 1).is_none());
80    /// assert!(Location::new(u64::MAX).is_none());
81    /// ```
82    #[inline]
83    pub const fn new(loc: u64) -> Option<Self> {
84        if loc > MAX_LOCATION {
85            None
86        } else {
87            Some(Self(loc))
88        }
89    }
90
91    /// Return the underlying `u64` value.
92    #[inline]
93    pub const fn as_u64(self) -> u64 {
94        self.0
95    }
96
97    /// Returns `true` iff this location can be safely converted to a [Position].
98    #[inline]
99    pub const fn is_valid(self) -> bool {
100        self.0 <= MAX_LOCATION
101    }
102
103    /// Return `self + rhs` returning `None` on overflow or if result exceeds [MAX_LOCATION].
104    #[inline]
105    pub const fn checked_add(self, rhs: u64) -> Option<Self> {
106        match self.0.checked_add(rhs) {
107            Some(value) => {
108                if value <= MAX_LOCATION {
109                    Some(Self(value))
110                } else {
111                    None
112                }
113            }
114            None => None,
115        }
116    }
117
118    /// Return `self - rhs` returning `None` on underflow.
119    #[inline]
120    pub const fn checked_sub(self, rhs: u64) -> Option<Self> {
121        match self.0.checked_sub(rhs) {
122            Some(value) => Some(Self(value)),
123            None => None,
124        }
125    }
126
127    /// Return `self + rhs` saturating at [MAX_LOCATION].
128    #[inline]
129    pub const fn saturating_add(self, rhs: u64) -> Self {
130        let result = self.0.saturating_add(rhs);
131        if result > MAX_LOCATION {
132            Self(MAX_LOCATION)
133        } else {
134            Self(result)
135        }
136    }
137
138    /// Return `self - rhs` saturating at zero.
139    #[inline]
140    pub const fn saturating_sub(self, rhs: u64) -> Self {
141        Self(self.0.saturating_sub(rhs))
142    }
143}
144
145impl fmt::Display for Location {
146    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
147        write!(f, "Location({})", self.0)
148    }
149}
150
151impl From<u64> for Location {
152    #[inline]
153    fn from(value: u64) -> Self {
154        Self::new_unchecked(value)
155    }
156}
157
158impl From<usize> for Location {
159    #[inline]
160    fn from(value: usize) -> Self {
161        Self::new_unchecked(value as u64)
162    }
163}
164
165impl Deref for Location {
166    type Target = u64;
167    fn deref(&self) -> &Self::Target {
168        &self.0
169    }
170}
171
172impl From<Location> for u64 {
173    #[inline]
174    fn from(loc: Location) -> Self {
175        *loc
176    }
177}
178
179/// Add two locations together.
180///
181/// # Panics
182///
183/// Panics if the result overflows.
184impl Add for Location {
185    type Output = Self;
186
187    #[inline]
188    fn add(self, rhs: Self) -> Self::Output {
189        Self(self.0 + rhs.0)
190    }
191}
192
193/// Add a location and a `u64`.
194///
195/// # Panics
196///
197/// Panics if the result overflows.
198impl Add<u64> for Location {
199    type Output = Self;
200
201    #[inline]
202    fn add(self, rhs: u64) -> Self::Output {
203        Self(self.0 + rhs)
204    }
205}
206
207/// Subtract two locations.
208///
209/// # Panics
210///
211/// Panics if the result underflows.
212impl Sub for Location {
213    type Output = Self;
214
215    #[inline]
216    fn sub(self, rhs: Self) -> Self::Output {
217        Self(self.0 - rhs.0)
218    }
219}
220
221/// Subtract a `u64` from a location.
222///
223/// # Panics
224///
225/// Panics if the result underflows.
226impl Sub<u64> for Location {
227    type Output = Self;
228
229    #[inline]
230    fn sub(self, rhs: u64) -> Self::Output {
231        Self(self.0 - rhs)
232    }
233}
234
235impl PartialEq<u64> for Location {
236    #[inline]
237    fn eq(&self, other: &u64) -> bool {
238        self.0 == *other
239    }
240}
241
242impl PartialOrd<u64> for Location {
243    #[inline]
244    fn partial_cmp(&self, other: &u64) -> Option<core::cmp::Ordering> {
245        self.0.partial_cmp(other)
246    }
247}
248
249// Allow u64 to be compared with Location too
250impl PartialEq<Location> for u64 {
251    #[inline]
252    fn eq(&self, other: &Location) -> bool {
253        *self == other.0
254    }
255}
256
257impl PartialOrd<Location> for u64 {
258    #[inline]
259    fn partial_cmp(&self, other: &Location) -> Option<core::cmp::Ordering> {
260        self.partial_cmp(&other.0)
261    }
262}
263
264/// Add a `u64` to a location.
265///
266/// # Panics
267///
268/// Panics if the result overflows.
269impl AddAssign<u64> for Location {
270    #[inline]
271    fn add_assign(&mut self, rhs: u64) {
272        self.0 += rhs;
273    }
274}
275
276/// Subtract a `u64` from a location.
277///
278/// # Panics
279///
280/// Panics if the result underflows.
281impl SubAssign<u64> for Location {
282    #[inline]
283    fn sub_assign(&mut self, rhs: u64) {
284        self.0 -= rhs;
285    }
286}
287
288impl TryFrom<Position> for Location {
289    type Error = LocationError;
290
291    /// Attempt to derive the [Location] of a given node [Position].
292    ///
293    /// Returns an error if the position does not correspond to an MMR leaf or if position
294    /// overflow occurs.
295    ///
296    /// This computation is O(log2(n)) in the given position.
297    #[inline]
298    fn try_from(pos: Position) -> Result<Self, Self::Error> {
299        // Reject positions beyond the valid MMR range. This ensures `pos + 1` won't overflow below.
300        if *pos > MAX_POSITION {
301            return Err(LocationError::Overflow(pos));
302        }
303        // Position 0 is always the first leaf at location 0.
304        if *pos == 0 {
305            return Ok(Self(0));
306        }
307
308        // Find the height of the perfect binary tree containing this position.
309        // Safe: pos + 1 cannot overflow since pos <= MAX_POSITION (checked above).
310        let start = u64::MAX >> (pos + 1).leading_zeros();
311        let height = start.trailing_ones();
312        // Height 0 means this position is a peak (not a leaf in a tree).
313        if height == 0 {
314            return Err(LocationError::NonLeaf(pos));
315        }
316        let mut two_h = 1 << (height - 1);
317        let mut cur_node = start - 1;
318        let mut leaf_loc_floor = 0u64;
319
320        while two_h > 1 {
321            if cur_node == *pos {
322                return Err(LocationError::NonLeaf(pos));
323            }
324            let left_pos = cur_node - two_h;
325            two_h >>= 1;
326            if *pos > left_pos {
327                // The leaf is in the right subtree, so we must account for the leaves in the left
328                // subtree all of which precede it.
329                leaf_loc_floor += two_h;
330                cur_node -= 1; // move to the right child
331            } else {
332                // The node is in the left subtree
333                cur_node = left_pos;
334            }
335        }
336
337        Ok(Self(leaf_loc_floor))
338    }
339}
340
341/// Error returned when attempting to convert a [Position] to a [Location].
342#[derive(Debug, Clone, Copy, Eq, PartialEq, Error)]
343pub enum LocationError {
344    #[error("{0} is not a leaf")]
345    NonLeaf(Position),
346
347    #[error("{0} > MAX_LOCATION")]
348    Overflow(Position),
349}
350
351/// Extension trait for converting `Range<Location>` into other range types.
352pub trait LocationRangeExt {
353    /// Convert a `Range<Location>` to a `Range<usize>` suitable for slice indexing.
354    fn to_usize_range(&self) -> Range<usize>;
355}
356
357impl LocationRangeExt for Range<Location> {
358    #[inline]
359    fn to_usize_range(&self) -> Range<usize> {
360        *self.start as usize..*self.end as usize
361    }
362}
363
364#[cfg(test)]
365mod tests {
366    use super::{Location, MAX_LOCATION};
367    use crate::mmr::{position::Position, LocationError, MAX_POSITION};
368
369    // Test that the [Location::try_from] function returns the correct location for leaf positions.
370    #[test]
371    fn test_try_from_position() {
372        const CASES: &[(Position, Location)] = &[
373            (Position::new(0), Location::new_unchecked(0)),
374            (Position::new(1), Location::new_unchecked(1)),
375            (Position::new(3), Location::new_unchecked(2)),
376            (Position::new(4), Location::new_unchecked(3)),
377            (Position::new(7), Location::new_unchecked(4)),
378            (Position::new(8), Location::new_unchecked(5)),
379            (Position::new(10), Location::new_unchecked(6)),
380            (Position::new(11), Location::new_unchecked(7)),
381            (Position::new(15), Location::new_unchecked(8)),
382            (Position::new(16), Location::new_unchecked(9)),
383            (Position::new(18), Location::new_unchecked(10)),
384            (Position::new(19), Location::new_unchecked(11)),
385            (Position::new(22), Location::new_unchecked(12)),
386            (Position::new(23), Location::new_unchecked(13)),
387            (Position::new(25), Location::new_unchecked(14)),
388            (Position::new(26), Location::new_unchecked(15)),
389        ];
390        for (pos, expected_loc) in CASES {
391            let loc = Location::try_from(*pos).expect("should map to a leaf location");
392            assert_eq!(loc, *expected_loc);
393        }
394    }
395
396    // Test that the [Location::try_from] function returns an error for non-leaf positions.
397    #[test]
398    fn test_try_from_position_error() {
399        const CASES: &[Position] = &[
400            Position::new(2),
401            Position::new(5),
402            Position::new(6),
403            Position::new(9),
404            Position::new(12),
405            Position::new(13),
406            Position::new(14),
407            Position::new(17),
408            Position::new(20),
409            Position::new(21),
410            Position::new(24),
411            Position::new(27),
412            Position::new(28),
413            Position::new(29),
414            Position::new(30),
415        ];
416        for &pos in CASES {
417            let err = Location::try_from(pos).expect_err("position is not a leaf");
418            assert_eq!(err, LocationError::NonLeaf(pos));
419        }
420    }
421
422    #[test]
423    fn test_try_from_position_error_overflow() {
424        let overflow_pos = Position::new(u64::MAX);
425        let err = Location::try_from(overflow_pos).expect_err("should overflow");
426        assert_eq!(err, LocationError::Overflow(overflow_pos));
427
428        // MAX_POSITION doesn't overflow and isn't a leaf
429        let result = Location::try_from(MAX_POSITION);
430        assert_eq!(result, Err(LocationError::NonLeaf(MAX_POSITION)));
431
432        let overflow_pos = MAX_POSITION + 1;
433        let err = Location::try_from(overflow_pos).expect_err("should overflow");
434        assert_eq!(err, LocationError::Overflow(overflow_pos));
435    }
436
437    #[test]
438    fn test_checked_add() {
439        let loc = Location::new_unchecked(10);
440        assert_eq!(loc.checked_add(5).unwrap(), 15);
441
442        // Overflow returns None
443        assert!(Location::new_unchecked(u64::MAX).checked_add(1).is_none());
444
445        // Exceeding MAX_LOCATION returns None
446        assert!(Location::new_unchecked(MAX_LOCATION)
447            .checked_add(1)
448            .is_none());
449
450        // At MAX_LOCATION is OK
451        let loc = Location::new_unchecked(MAX_LOCATION - 10);
452        assert_eq!(loc.checked_add(10).unwrap(), MAX_LOCATION);
453    }
454
455    #[test]
456    fn test_checked_sub() {
457        let loc = Location::new_unchecked(10);
458        assert_eq!(loc.checked_sub(5).unwrap(), 5);
459        assert!(loc.checked_sub(11).is_none());
460    }
461
462    #[test]
463    fn test_saturating_add() {
464        let loc = Location::new_unchecked(10);
465        assert_eq!(loc.saturating_add(5), 15);
466
467        // Saturates at MAX_LOCATION, not u64::MAX
468        assert_eq!(
469            Location::new_unchecked(u64::MAX).saturating_add(1),
470            MAX_LOCATION
471        );
472        assert_eq!(
473            Location::new_unchecked(MAX_LOCATION).saturating_add(1),
474            MAX_LOCATION
475        );
476        assert_eq!(
477            Location::new_unchecked(MAX_LOCATION).saturating_add(1000),
478            MAX_LOCATION
479        );
480    }
481
482    #[test]
483    fn test_saturating_sub() {
484        let loc = Location::new_unchecked(10);
485        assert_eq!(loc.saturating_sub(5), 5);
486        assert_eq!(Location::new_unchecked(0).saturating_sub(1), 0);
487    }
488
489    #[test]
490    fn test_display() {
491        let location = Location::new_unchecked(42);
492        assert_eq!(location.to_string(), "Location(42)");
493    }
494
495    #[test]
496    fn test_add() {
497        let loc1 = Location::new_unchecked(10);
498        let loc2 = Location::new_unchecked(5);
499        assert_eq!((loc1 + loc2), 15);
500    }
501
502    #[test]
503    fn test_sub() {
504        let loc1 = Location::new_unchecked(10);
505        let loc2 = Location::new_unchecked(3);
506        assert_eq!((loc1 - loc2), 7);
507    }
508
509    #[test]
510    fn test_comparison_with_u64() {
511        let loc = Location::new_unchecked(42);
512
513        // Test equality
514        assert_eq!(loc, 42u64);
515        assert_eq!(42u64, loc);
516        assert_ne!(loc, 43u64);
517        assert_ne!(43u64, loc);
518
519        // Test ordering
520        assert!(loc < 43u64);
521        assert!(43u64 > loc);
522        assert!(loc > 41u64);
523        assert!(41u64 < loc);
524        assert!(loc <= 42u64);
525        assert!(42u64 >= loc);
526    }
527
528    #[test]
529    fn test_assignment_with_u64() {
530        let mut loc = Location::new_unchecked(10);
531
532        // Test add assignment
533        loc += 5;
534        assert_eq!(loc, 15u64);
535
536        // Test sub assignment
537        loc -= 3;
538        assert_eq!(loc, 12u64);
539    }
540
541    #[test]
542    fn test_new() {
543        // Valid locations
544        assert!(Location::new(0).is_some());
545        assert!(Location::new(1000).is_some());
546        assert!(Location::new(MAX_LOCATION).is_some());
547
548        // Invalid locations (too large)
549        assert!(Location::new(MAX_LOCATION + 1).is_none());
550        assert!(Location::new(u64::MAX).is_none());
551    }
552
553    #[test]
554    fn test_is_valid() {
555        assert!(Location::new_unchecked(0).is_valid());
556        assert!(Location::new_unchecked(1000).is_valid());
557        assert!(Location::new_unchecked(MAX_LOCATION).is_valid());
558        assert!(Location::new_unchecked(MAX_LOCATION).is_valid());
559        assert!(!Location::new_unchecked(u64::MAX).is_valid());
560    }
561
562    #[test]
563    fn test_max_location_boundary() {
564        // MAX_LOCATION should convert successfully
565        let max_loc = Location::new_unchecked(MAX_LOCATION);
566        assert!(max_loc.is_valid());
567        let pos = Position::try_from(max_loc).unwrap();
568        // Verify the position value
569        // For MAX_LOCATION = 2^62 - 1 = 0x3FFFFFFFFFFFFFFF, popcount = 62
570        // Position = 2 * (2^62 - 1) - 62 = 2^63 - 2 - 62 = 2^63 - 64
571        let expected = (1u64 << 63) - 64;
572        assert_eq!(*pos, expected);
573    }
574
575    #[test]
576    fn test_overflow_location_returns_error() {
577        // MAX_LOCATION + 1 should return error
578        let over_loc = Location::new_unchecked(MAX_LOCATION + 1);
579        assert!(Position::try_from(over_loc).is_err());
580
581        // Verify the error message
582        match Position::try_from(over_loc) {
583            Err(crate::mmr::Error::LocationOverflow(loc)) => {
584                assert_eq!(loc, over_loc);
585            }
586            _ => panic!("expected LocationOverflow error"),
587        }
588    }
589}