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