Skip to main content

ssz/
sparse.rs

1//! `SparseList<T, N>` — a list view that may omit materializing parts of
2//! the tree, using cached subtree roots or zero-hashes for empty regions.
3//!
4//! The hash tree root is byte-identical to a fully-materialised
5//! `List<T, N>` with the same effective contents. The algorithm walks the
6//! implicit balanced binary tree of depth `ceil_log2(N)` recursively, with
7//! recursion depth bounded by `ceil_log2(N)` — never materialising the full
8//! `N` leaves.
9//!
10//! ## Storage
11//!
12//! Both inner maps are sorted `Vec`s keyed by `u64`. Sorted Vec gives us:
13//!
14//! - **O(log n) lookup** via `binary_search_by_key`.
15//! - **O(n) insert/remove** at the sorted position. For the cnode-slot
16//!   use case (N ≤ 256, typically very sparse), the linear shift is
17//!   trivial.
18//! - **O(log n) range queries** via `partition_point`, used by
19//!   `compute_subtree_root` to short-circuit empty subtrees.
20//! - **Iteration in sorted order**, byte-equivalent to `BTreeMap::iter`.
21
22use alloc::vec::Vec;
23use core::fmt;
24use digest::Digest;
25use digest::typenum::U32;
26
27use crate::merkle::{ceil_log2, hash_pair, mix_in_length, zero_hash};
28use crate::missing::MissingOr;
29use crate::{BYTES_PER_LENGTH_OFFSET, Decode, DecodeError, Encode, HashTreeRoot};
30
31/// A list with a maximum length of `N` that exposes its tree structure
32/// for sparse fill-in: materialized indices, cached subtree roots, or
33/// implicit zero-hashes for never-written regions.
34///
35/// Hash is byte-identical to a fully-materialised `List<T, N>` with the
36/// same effective contents.
37pub struct SparseList<T, const N: u64> {
38    len: u64,
39    /// Sorted (by `u64` key) entries: leaf index → optional materialized
40    /// value (or precomputed hash). Absent indices contribute
41    /// `zero_hash(0)` to the root unless covered by
42    /// [`cached_subtree_roots`].
43    entries: Vec<(u64, MissingOr<T>)>,
44    /// Sorted (by `u64` key) cache of precomputed subtree roots. Key is
45    /// a tree coordinate `(depth, index_at_depth)` flattened via
46    /// `coord_to_key(depth, idx) = (1u64 << depth) | idx` — the standard
47    /// "heap index" of a node in a complete binary tree.
48    cached_subtree_roots: Vec<(u64, [u8; 32])>,
49}
50
51impl<T, const N: u64> SparseList<T, N> {
52    /// Build an empty sparse list.
53    pub fn new() -> Self {
54        Self {
55            len: 0,
56            entries: Vec::new(),
57            cached_subtree_roots: Vec::new(),
58        }
59    }
60
61    /// Logical length.
62    pub fn len(&self) -> u64 {
63        self.len
64    }
65
66    /// `true` iff no entries are present and `len == 0`.
67    pub fn is_empty(&self) -> bool {
68        self.len == 0
69    }
70
71    /// Iterator over `(index, MissingOr<T>)` for materialized entries only.
72    pub fn iter(&self) -> impl Iterator<Item = (u64, &MissingOr<T>)> {
73        self.entries.iter().map(|(k, v)| (*k, v))
74    }
75
76    /// Mutable iterator over `(index, &mut MissingOr<T>)` for materialized
77    /// entries only. Used by callers that need to rewrite entry values
78    /// in place (e.g., resolving `Ref` targets to `Hash` after settle).
79    pub fn iter_mut(&mut self) -> impl Iterator<Item = (u64, &mut MissingOr<T>)> {
80        self.entries.iter_mut().map(|(k, v)| (*k, v))
81    }
82
83    /// Look up a single entry by leaf index. O(log n).
84    pub fn get(&self, idx: u64) -> Option<&MissingOr<T>> {
85        match self.entries.binary_search_by_key(&idx, |(k, _)| *k) {
86            Ok(pos) => Some(&self.entries[pos].1),
87            Err(_) => None,
88        }
89    }
90
91    /// Insert a materialized entry. Updates `len` to `max(len, idx + 1)`.
92    /// O(n) — sorted shift on insert. If `idx` is already present, the
93    /// existing value is overwritten (matching `BTreeMap::insert` semantics).
94    pub fn insert(&mut self, idx: u64, value: MissingOr<T>) -> Result<(), DecodeError> {
95        if idx >= N {
96            return Err(DecodeError::BoundExceeded {
97                len: idx + 1,
98                bound: N,
99            });
100        }
101        self.len = self.len.max(idx + 1);
102        match self.entries.binary_search_by_key(&idx, |(k, _)| *k) {
103            Ok(pos) => {
104                self.entries[pos].1 = value;
105            }
106            Err(pos) => {
107                self.entries.insert(pos, (idx, value));
108            }
109        }
110        Ok(())
111    }
112
113    /// Remove the entry at `idx`, returning its previous value if any.
114    /// Does **not** decrement `len` — the logical length is independent
115    /// of which indices are materialized.
116    pub fn remove(&mut self, idx: u64) -> Option<MissingOr<T>> {
117        match self.entries.binary_search_by_key(&idx, |(k, _)| *k) {
118            Ok(pos) => Some(self.entries.remove(pos).1),
119            Err(_) => None,
120        }
121    }
122
123    /// Set the logical length explicitly (does not affect entries).
124    pub fn set_len(&mut self, len: u64) -> Result<(), DecodeError> {
125        if len > N {
126            return Err(DecodeError::BoundExceeded { len, bound: N });
127        }
128        self.len = len;
129        Ok(())
130    }
131
132    /// Cache a precomputed subtree root at tree position `(depth, idx)`.
133    /// `depth == 0` corresponds to the root; deeper means closer to leaves.
134    /// O(n) — sorted insert into `cached_subtree_roots`.
135    pub fn cache_subtree_root(&mut self, depth: usize, idx: u64, root: [u8; 32]) {
136        let key = coord_to_key(depth, idx);
137        match self
138            .cached_subtree_roots
139            .binary_search_by_key(&key, |(k, _)| *k)
140        {
141            Ok(pos) => {
142                self.cached_subtree_roots[pos].1 = root;
143            }
144            Err(pos) => {
145                self.cached_subtree_roots.insert(pos, (key, root));
146            }
147        }
148    }
149
150    /// Number of cached subtree roots. Used by [`fmt::Debug`].
151    pub fn cached_subtree_roots_count(&self) -> usize {
152        self.cached_subtree_roots.len()
153    }
154
155    /// Iterator over cached subtree roots in sorted-by-key order.
156    pub fn cached_subtree_roots(&self) -> impl Iterator<Item = (u64, &[u8; 32])> {
157        self.cached_subtree_roots.iter().map(|(k, v)| (*k, v))
158    }
159
160    /// Internal: look up a cached subtree root by its `coord_to_key`-encoded key.
161    fn cached_subtree_root(&self, key: u64) -> Option<&[u8; 32]> {
162        match self
163            .cached_subtree_roots
164            .binary_search_by_key(&key, |(k, _)| *k)
165        {
166            Ok(pos) => Some(&self.cached_subtree_roots[pos].1),
167            Err(_) => None,
168        }
169    }
170}
171
172impl<T, const N: u64> Default for SparseList<T, N> {
173    fn default() -> Self {
174        Self::new()
175    }
176}
177
178#[inline]
179fn coord_to_key(depth: usize, idx: u64) -> u64 {
180    (1u64 << depth) | idx
181}
182
183impl<T: fmt::Debug, const N: u64> fmt::Debug for SparseList<T, N> {
184    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
185        f.debug_struct("SparseList")
186            .field("cap", &N)
187            .field("len", &self.len)
188            .field("materialized", &self.entries.len())
189            .field("cached_subtrees", &self.cached_subtree_roots.len())
190            .finish()
191    }
192}
193
194impl<T: Clone, const N: u64> Clone for SparseList<T, N> {
195    fn clone(&self) -> Self {
196        Self {
197            len: self.len,
198            entries: self.entries.clone(),
199            cached_subtree_roots: self.cached_subtree_roots.clone(),
200        }
201    }
202}
203
204impl<T: PartialEq, const N: u64> PartialEq for SparseList<T, N> {
205    fn eq(&self, other: &Self) -> bool {
206        if self.len != other.len
207            || self.entries.len() != other.entries.len()
208            || self.cached_subtree_roots.len() != other.cached_subtree_roots.len()
209        {
210            return false;
211        }
212        // Both vectors are sorted by key, so element-wise comparison
213        // suffices.
214        for ((ka, va), (kb, vb)) in self.entries.iter().zip(other.entries.iter()) {
215            if ka != kb || va != vb {
216                return false;
217            }
218        }
219        for ((ka, va), (kb, vb)) in self
220            .cached_subtree_roots
221            .iter()
222            .zip(other.cached_subtree_roots.iter())
223        {
224            if ka != kb || va != vb {
225                return false;
226            }
227        }
228        true
229    }
230}
231
232impl<T: Eq, const N: u64> Eq for SparseList<T, N> {}
233
234// --------------------------------------------------------------------------
235// Wire format: (len: u64, List<(u64, MissingOr<T>)>).
236// --------------------------------------------------------------------------
237//
238// The list element is an SSZ Container with a fixed `u64` key plus a
239// variable-length `MissingOr<T>` payload. We encode it inline rather than
240// going through `List<(u64, MissingOr<T>)>` to keep the wire format
241// independent of the workspace `(K, V)` tuple impl (which currently
242// doesn't exist; we have only the `BTreeMap<K, V>` impl in collections.rs).
243
244impl<T: Encode, const N: u64> Encode for SparseList<T, N> {
245    fn is_ssz_fixed_len() -> bool {
246        false
247    }
248    fn ssz_fixed_len() -> usize {
249        BYTES_PER_LENGTH_OFFSET
250    }
251    fn ssz_bytes_len(&self) -> usize {
252        // 8 (len) + 4 (entries-list offset) + offset table + payloads.
253        let n_entries = self.entries.len();
254        let entry_var_size: usize = self
255            .entries
256            .iter()
257            .map(|(_, v)| v.ssz_bytes_len())
258            .sum::<usize>();
259        // Each entry is (u64 key, MissingOr<T> value). u64 is fixed (8B);
260        // MissingOr<T> is variable, so each entry has a 4B offset slot.
261        // Plus the entries list itself is variable — we wrap it in an
262        // offset container with the leading `len: u64`.
263        // Layout:
264        //   [0..8]   len: u64
265        //   [8..12]  entries_offset: u32 (always 12)
266        //   [12..]   entries list payload
267        //
268        // entries list payload (variable list of variable elements):
269        //   per-entry: (u64 key, 4B value-offset)  ← 12 bytes each
270        //   then concatenated payloads
271        12 + n_entries * 12 + entry_var_size
272    }
273    fn ssz_append(&self, buf: &mut Vec<u8>) {
274        // len
275        buf.extend_from_slice(&self.len.to_le_bytes());
276        // entries_offset = 12
277        buf.extend_from_slice(&12u32.to_le_bytes());
278        // Now encode the entries list. SSZ list of container elements
279        // where the container is (u64 key, MissingOr<T> value).
280        encode_sparse_entries_list(&self.entries, buf);
281    }
282}
283
284fn encode_sparse_entries_list<T: Encode>(entries: &[(u64, MissingOr<T>)], buf: &mut Vec<u8>) {
285    let n = entries.len();
286    // Each entry container: (u64 key, MissingOr<T> value).
287    // Key is fixed (8B), value is variable. Per-entry container:
288    //   [0..8]: key
289    //   [8..12]: value-offset (= 12 → payload starts immediately)
290    //   [12..]: value payload
291    // So per-entry "fixed" portion is 12 bytes; variable portion is the
292    // MissingOr payload.
293    //
294    // To put this inside a list-of-variable-elements, we need a top-level
295    // offset table of `n` × 4 bytes pointing at each entry container,
296    // followed by the entry containers laid out back-to-back.
297
298    let header_size = n * BYTES_PER_LENGTH_OFFSET;
299    let start = buf.len();
300    buf.resize(start + header_size, 0u8);
301
302    let mut running = header_size as u32;
303    for (i, (key, val)) in entries.iter().enumerate() {
304        let off_pos = start + i * BYTES_PER_LENGTH_OFFSET;
305        buf[off_pos..off_pos + 4].copy_from_slice(&running.to_le_bytes());
306
307        let entry_start = buf.len();
308        // key
309        buf.extend_from_slice(&key.to_le_bytes());
310        // value-offset within this entry container
311        buf.extend_from_slice(&12u32.to_le_bytes());
312        // value payload
313        val.ssz_append(buf);
314
315        let entry_end = buf.len();
316        running = running
317            .checked_add((entry_end - entry_start) as u32)
318            .expect("ssz offset overflow");
319    }
320}
321
322impl<T: Decode, const N: u64> Decode for SparseList<T, N> {
323    fn is_ssz_fixed_len() -> bool {
324        false
325    }
326    fn ssz_fixed_len() -> usize {
327        BYTES_PER_LENGTH_OFFSET
328    }
329    fn from_ssz_bytes(bytes: &[u8]) -> Result<Self, DecodeError> {
330        if bytes.len() < 12 {
331            return Err(DecodeError::UnexpectedEof {
332                expected: 12,
333                actual: bytes.len(),
334            });
335        }
336        let mut len_bytes = [0u8; 8];
337        len_bytes.copy_from_slice(&bytes[0..8]);
338        let len = u64::from_le_bytes(len_bytes);
339        if len > N {
340            return Err(DecodeError::BoundExceeded { len, bound: N });
341        }
342        let entries_offset = u32::from_le_bytes(bytes[8..12].try_into().unwrap()) as usize;
343        if entries_offset != 12 {
344            return Err(DecodeError::InvalidOffset {
345                offset: entries_offset,
346                len: bytes.len(),
347                fixed: 12,
348            });
349        }
350        let payload = &bytes[12..];
351        let entries_in = decode_sparse_entries_list::<T>(payload)?;
352        let mut entries: Vec<(u64, MissingOr<T>)> = Vec::with_capacity(entries_in.len());
353        let mut prev_key: Option<u64> = None;
354        for (k, v) in entries_in {
355            if k >= N {
356                return Err(DecodeError::BoundExceeded {
357                    len: k + 1,
358                    bound: N,
359                });
360            }
361            if let Some(p) = prev_key
362                && k <= p
363            {
364                return Err(DecodeError::NotSorted);
365            }
366            prev_key = Some(k);
367            entries.push((k, v));
368        }
369        Ok(Self {
370            len,
371            entries,
372            cached_subtree_roots: Vec::new(),
373        })
374    }
375}
376
377fn decode_sparse_entries_list<T: Decode>(
378    bytes: &[u8],
379) -> Result<Vec<(u64, MissingOr<T>)>, DecodeError> {
380    if bytes.is_empty() {
381        return Ok(Vec::new());
382    }
383    if bytes.len() < 4 {
384        return Err(DecodeError::UnexpectedEof {
385            expected: 4,
386            actual: bytes.len(),
387        });
388    }
389    let first = u32::from_le_bytes(bytes[0..4].try_into().unwrap()) as usize;
390    if !first.is_multiple_of(BYTES_PER_LENGTH_OFFSET) || first > bytes.len() {
391        return Err(DecodeError::InvalidOffset {
392            offset: first,
393            len: bytes.len(),
394            fixed: 0,
395        });
396    }
397    let n = first / BYTES_PER_LENGTH_OFFSET;
398    let mut offsets = Vec::with_capacity(n + 1);
399    offsets.push(first);
400    for i in 1..n {
401        if bytes.len() < (i + 1) * BYTES_PER_LENGTH_OFFSET {
402            return Err(DecodeError::UnexpectedEof {
403                expected: (i + 1) * BYTES_PER_LENGTH_OFFSET,
404                actual: bytes.len(),
405            });
406        }
407        let off = u32::from_le_bytes(
408            bytes[i * BYTES_PER_LENGTH_OFFSET..(i + 1) * BYTES_PER_LENGTH_OFFSET]
409                .try_into()
410                .unwrap(),
411        ) as usize;
412        if off < *offsets.last().unwrap() {
413            return Err(DecodeError::OffsetsNotMonotonic {
414                prev: *offsets.last().unwrap(),
415                curr: off,
416            });
417        }
418        if off > bytes.len() {
419            return Err(DecodeError::InvalidOffset {
420                offset: off,
421                len: bytes.len(),
422                fixed: first,
423            });
424        }
425        offsets.push(off);
426    }
427    offsets.push(bytes.len());
428
429    let mut out = Vec::with_capacity(n);
430    for i in 0..n {
431        let entry_slice = &bytes[offsets[i]..offsets[i + 1]];
432        // Each entry: u64 key + value-offset (must be 12) + MissingOr<T> payload.
433        if entry_slice.len() < 12 {
434            return Err(DecodeError::UnexpectedEof {
435                expected: 12,
436                actual: entry_slice.len(),
437            });
438        }
439        let mut kbytes = [0u8; 8];
440        kbytes.copy_from_slice(&entry_slice[0..8]);
441        let key = u64::from_le_bytes(kbytes);
442        let value_offset = u32::from_le_bytes(entry_slice[8..12].try_into().unwrap()) as usize;
443        if value_offset != 12 {
444            return Err(DecodeError::InvalidOffset {
445                offset: value_offset,
446                len: entry_slice.len(),
447                fixed: 12,
448            });
449        }
450        let value = MissingOr::<T>::from_ssz_bytes(&entry_slice[12..])?;
451        out.push((key, value));
452    }
453    Ok(out)
454}
455
456// --------------------------------------------------------------------------
457// HashTreeRoot
458// --------------------------------------------------------------------------
459
460// --------------------------------------------------------------------------
461// rkyv: hand-rolled via delegation to `SparseListRepr` (a non-generic-N
462// helper that mirrors the wire-relevant fields). `cached_subtree_roots` is
463// skipped — it's a pure in-memory cache (rebuilt on demand from `entries`).
464// --------------------------------------------------------------------------
465
466#[derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
467pub struct SparseListRepr<T>
468where
469    T: rkyv::Archive,
470    MissingOr<T>: rkyv::Archive,
471{
472    pub len: u64,
473    pub entries: Vec<(u64, MissingOr<T>)>,
474}
475
476impl<T, const N: u64> rkyv::Archive for SparseList<T, N>
477where
478    T: rkyv::Archive + Clone,
479    MissingOr<T>: rkyv::Archive,
480{
481    type Archived = <SparseListRepr<T> as rkyv::Archive>::Archived;
482    type Resolver = <SparseListRepr<T> as rkyv::Archive>::Resolver;
483
484    fn resolve(&self, resolver: Self::Resolver, out: rkyv::Place<Self::Archived>) {
485        let repr = SparseListRepr {
486            len: self.len,
487            entries: self.entries.clone(),
488        };
489        <SparseListRepr<T> as rkyv::Archive>::resolve(&repr, resolver, out)
490    }
491}
492
493impl<T, S, const N: u64> rkyv::Serialize<S> for SparseList<T, N>
494where
495    T: rkyv::Archive + Clone,
496    MissingOr<T>: rkyv::Archive,
497    SparseListRepr<T>: rkyv::Serialize<S>,
498    S: rkyv::rancor::Fallible + ?Sized,
499{
500    fn serialize(
501        &self,
502        serializer: &mut S,
503    ) -> Result<Self::Resolver, <S as rkyv::rancor::Fallible>::Error> {
504        let repr = SparseListRepr {
505            len: self.len,
506            entries: self.entries.clone(),
507        };
508        <SparseListRepr<T> as rkyv::Serialize<S>>::serialize(&repr, serializer)
509    }
510}
511
512impl<T, D, const N: u64> rkyv::Deserialize<SparseList<T, N>, D>
513    for <SparseListRepr<T> as rkyv::Archive>::Archived
514where
515    T: rkyv::Archive + Clone,
516    MissingOr<T>: rkyv::Archive,
517    <SparseListRepr<T> as rkyv::Archive>::Archived: rkyv::Deserialize<SparseListRepr<T>, D>,
518    D: rkyv::rancor::Fallible + ?Sized,
519{
520    fn deserialize(
521        &self,
522        deserializer: &mut D,
523    ) -> Result<SparseList<T, N>, <D as rkyv::rancor::Fallible>::Error> {
524        let repr: SparseListRepr<T> =
525            rkyv::Deserialize::<SparseListRepr<T>, D>::deserialize(self, deserializer)?;
526        Ok(SparseList {
527            len: repr.len,
528            entries: repr.entries,
529            cached_subtree_roots: Vec::new(),
530        })
531    }
532}
533
534impl<T: HashTreeRoot + Encode, const N: u64> HashTreeRoot for SparseList<T, N> {
535    fn hash_tree_root<D: Digest<OutputSize = U32>>(&self) -> [u8; 32] {
536        // The chunk-tree depth is ceil_log2(N) — the depth at which there
537        // are exactly N leaves (one chunk per logical element, since we
538        // treat elements as composite types via `HashTreeRoot`).
539        // Special-case N <= 1 → depth 0 → root is the (zero or single)
540        // chunk.
541        let depth = ceil_log2(N);
542        let inner = self.compute_subtree_root::<D>(0, 0, depth);
543        mix_in_length::<D>(inner, self.len)
544    }
545}
546
547impl<T: HashTreeRoot, const N: u64> SparseList<T, N> {
548    /// Compute the merkle root of the subtree rooted at coordinate
549    /// `(node_depth, node_index_at_depth)` within a balanced binary chunk
550    /// tree of total depth `total_depth` (i.e., `2^total_depth` leaves).
551    ///
552    /// Uses recursive DFS; recursion depth is bounded by `total_depth`
553    /// (≤ 64 for any u64 cap), so it never overflows.
554    fn compute_subtree_root<D: Digest<OutputSize = U32>>(
555        &self,
556        node_depth: usize,
557        node_index_at_depth: u64,
558        total_depth: usize,
559    ) -> [u8; 32] {
560        // Fast path: explicitly cached subtree root for this coordinate.
561        if let Some(cached) =
562            self.cached_subtree_root(coord_to_key(node_depth, node_index_at_depth))
563        {
564            return *cached;
565        }
566
567        // Leaf case: we're at the chunk level.
568        if node_depth == total_depth {
569            // Leaf index is `node_index_at_depth`. Return the chunk root.
570            return self
571                .get(node_index_at_depth)
572                .map(|e| e.hash_tree_root::<D>())
573                .unwrap_or([0u8; 32]);
574        }
575
576        // Determine the leaf-index range covered by this subtree.
577        let levels_below = total_depth - node_depth;
578        let leaves_per_subtree = 1u64 << levels_below;
579        let lo = node_index_at_depth * leaves_per_subtree;
580        let hi = lo + leaves_per_subtree; // exclusive
581
582        // If no materialized entries fall in [lo, hi), this subtree is
583        // entirely empty → it's a zero-hash at the appropriate depth.
584        // `partition_point` gives us the index of the first entry with
585        // key >= lo. If that entry's key is < hi, there's at least one
586        // materialized entry in range.
587        let pos = self.entries.partition_point(|(k, _)| *k < lo);
588        let has_entries = self.entries.get(pos).is_some_and(|(k, _)| *k < hi);
589        if !has_entries {
590            return zero_hash::<D>(levels_below);
591        }
592
593        // Recurse into children.
594        let left =
595            self.compute_subtree_root::<D>(node_depth + 1, node_index_at_depth * 2, total_depth);
596        let right = self.compute_subtree_root::<D>(
597            node_depth + 1,
598            node_index_at_depth * 2 + 1,
599            total_depth,
600        );
601        hash_pair::<D>(&left, &right)
602    }
603}