Skip to main content

ssz/
radix.rs

1//! `RadixMap<V, KEY_BYTES>` — a structurally-compressed sparse binary radix
2//! merkle key→value map (the "option b" content-addressed map).
3//!
4//! Keys are fixed-width `[u8; KEY_BYTES]` bit strings (bit 0 = the MSB of
5//! byte 0, MSB-first / big-endian), values are any `V: HashTreeRoot`. The
6//! root is a **canonical, binding, shallow** commitment to the key→value
7//! set: a pure function of the set (insertion-order independent), and a
8//! commitment that no two distinct sets can share under collision
9//! resistance of the digest `D` alone.
10//!
11//! This is distinct from [`crate::SparseList`] (the "option a" map): a
12//! `SparseList` is a *fixed-depth* sparse Merkle tree whose root is
13//! byte-identical to a dense `List<T, N>` (leaves are bare value roots,
14//! plus `mix_in_length`). A `RadixMap` is **not** byte-compatible with any
15//! SSZ `List`/`Vector` — it is a deliberately different structure (see
16//! "Dense ≠ Vector" below).
17//!
18//! # Structure (EIP-7864-aligned hybrid)
19//!
20//! The recursion `node(S, depth)` over a set `S` of `(key, value)` entries:
21//!
22//! - **empty** (`|S| = 0`) → [`EMPTY`] = `[0u8; 32]` (zero hash ops).
23//! - **leaf** (`|S| = 1`) → [`leaf_hash`] of that entry's `(key, value_root)`.
24//!   A lone key collapses to a single leaf wherever its subtree narrows to
25//!   one element — so a lone key is *shallow*, independent of `depth`.
26//! - **branch** (`|S| ≥ 2`) → [`branch_hash`] of the two children obtained
27//!   by splitting `S` on `bit(key, depth)`.
28//!
29//! Only **empty** and **singleton** subtrees collapse; a multi-key shared
30//! prefix is materialized as a chain of branch nodes each with one
31//! [`EMPTY`] sibling (exactly like EIP-7864's outer "minimal InternalNodes"
32//! tree, and like `SparseList`'s zero-padding). This is what keeps
33//! **non-membership proofs complete**: an absent key always lands on a
34//! materialized `EMPTY` sibling (or a diverging leaf) at a well-defined
35//! depth — there is no skipped unary run for it to fall into. (A *fully*
36//! unary-skip-compressed Patricia trie is shallower for adversarially-close
37//! keys but needs an explicit skip-prefix commitment in every node for
38//! non-membership completeness; we deliberately avoid that complexity.)
39//!
40//! Depth is bounded by `KEY_BITS = 8 * KEY_BYTES`. It is shallow in
41//! practice: for well-spread keys (e.g. address-derived) the singleton
42//! collapse fires at depth ≈ `log2(|S|)`; for the DataCap (group-index
43//! keys) `KEY_BYTES` is chosen small enough to bound the depth directly.
44//!
45//! # Domain separation (binding under collision resistance alone)
46//!
47//! Leaf and branch nodes are tagged with **distinct** one-byte domain
48//! prefixes:
49//!
50//! ```text
51//! EMPTY  = [0u8; 32]
52//! leaf   = D( LEAF_TAG(0x00)   || key || value_root )
53//! branch = D( BRANCH_TAG(0x01) || left || right )
54//! ```
55//!
56//! Tagging **both** node kinds (not just the leaf) is load-bearing. For
57//! `KEY_BYTES ≤ 32`, an SSZ `merkleize(pack_bytes(key), 1)` returns the raw
58//! key chunk *un-hashed*, so a bare `hash_pair(key, value_root)` leaf would
59//! be byte-identical to a branch `hash_pair(left, right)` — yielding a
60//! trivial, collision-free forgery: the one-entry map `{(H_L, Missing(H_R))}`
61//! would share a root with the two-entry branch `branch(H_L, H_R)`. The
62//! distinct tag bytes make the leaf and branch preimages differ in a fixed
63//! position regardless of content, so a collision between the two domains is
64//! a collision of `D` itself. This binds the map under collision resistance
65//! *alone* (no second-preimage / preimage assumption) and ports cleanly to
66//! a future SNARK-friendly sponge (the tag is a structural domain element,
67//! not an artifact of byte-hash length padding) — a stronger footing than
68//! EIP-7864's asymmetric `stem || 0x00 || …` stem commitment.
69//!
70//! The leaf commits the **full key** (not just the bits consumed to reach
71//! its collapsed depth). This pins each leaf to one logical key — required
72//! for canonicality (the shallow placement is uniquely determined by the
73//! set) and for sound non-membership proofs (a diverging-leaf exclusion
74//! reveals `k' ≠ key`). The value is taken via [`MissingOr`] so an
75//! un-materialized leaf (`Missing(h)`) contributes `value_root = h` with no
76//! `mix_in_selector` — substitution-transparent, exactly like `SparseList`.
77//!
78//! Because both node kinds are tagged, a `RadixMap` root also cannot be
79//! confused with a `List`/`SparseList` root (bare `hash_pair` + no tag) even
80//! absent an enclosing union selector. Note `EMPTY == [0u8;32]` *does* alias
81//! the zero summaries `zero_hash(0)` / `PageSlot::Empty` / `MissingOr::Missing([0;32])`;
82//! a `RadixMap` root must therefore only ever sit under a domain-separating
83//! parent (e.g. the `Cap` SSZ-union selector), never in a position that also
84//! admits a raw zero summary.
85//!
86//! # Canonicality
87//!
88//! `node(S, depth)` branches only on set membership and `bit(key, depth)`,
89//! both intrinsic to the set; the base cases depend only on the (≤ 1)
90//! element present and commit the full key. So the root is a pure function
91//! of the key→value-**root** set, independent of insertion order or
92//! `MissingOr` materialization pattern. Storage is a strictly-ascending
93//! sorted `Vec`, so there is exactly one in-memory and one wire form per
94//! set; duplicate keys are rejected on decode and overwritten on insert.
95//!
96//! # Dense ≠ Vector
97//!
98//! Even a fully-dense `RadixMap` root does **not** coincide with an SSZ
99//! `List`/`Vector` root: radix leaves are tagged + key-committed
100//! (`D(0x00 || key || value_root)`, not the bare `value_root` a `Vector`
101//! leaf carries), branches are tagged, and there is no `mix_in_length`.
102//! Standard SSZ merkleization appears **only inside the leaf value** — e.g.
103//! the DataCap leaf value is a dense `FixedVector<Page, 512>` merkleized the
104//! ordinary SSZ way; that `value_root` is then fed into `leaf_hash`.
105//!
106//! # Wire format
107//!
108//! Like [`crate::SparseList`] minus the `len` field (a map has no logical length,
109//! only a key set): a single variable field holding a sorted SSZ list of
110//! `(key: ByteVector[KEY_BYTES], value: MissingOr<V>)` containers. Decode is
111//! strict (loud on any non-canonical encoding): top-level offset must be 4,
112//! per-entry value-offset must be `KEY_BYTES + 4`, keys must be strictly
113//! ascending (rejecting duplicates and bad order), no trailing bytes.
114
115use alloc::vec::Vec;
116use core::fmt;
117use digest::Digest;
118use digest::typenum::U32;
119
120use crate::missing::MissingOr;
121use crate::{BYTES_PER_LENGTH_OFFSET, Decode, DecodeError, Encode, HashTreeRoot};
122
123/// One radix entry: a fixed-width key and its (possibly-elided) value.
124type Entry<V, const KEY_BYTES: usize> = ([u8; KEY_BYTES], MissingOr<V>);
125
126/// The empty-subtree hash: the all-zero 32-byte chunk (= `zero_hash(0)`).
127pub const EMPTY: [u8; 32] = [0u8; 32];
128
129/// Domain tag prepended to a leaf-node preimage.
130pub const LEAF_TAG: u8 = 0x00;
131
132/// Domain tag prepended to a branch-node (internal-node) preimage.
133pub const BRANCH_TAG: u8 = 0x01;
134
135/// Extract bit `i` of `key`, MSB-first: bit 0 is the most-significant bit of
136/// `key[0]`. Caller must ensure `i < 8 * key.len()`.
137#[inline]
138pub fn bit(key: &[u8], i: usize) -> u8 {
139    (key[i >> 3] >> (7 - (i & 7))) & 1
140}
141
142/// Leaf-node hash: `D(LEAF_TAG || key || value_root)`. Commits the full key
143/// (canonicality + non-membership) and the value root (via [`MissingOr`]).
144#[inline]
145pub fn leaf_hash<D: Digest<OutputSize = U32>>(key: &[u8], value_root: &[u8; 32]) -> [u8; 32] {
146    let mut hasher = D::new();
147    hasher.update([LEAF_TAG]);
148    hasher.update(key);
149    hasher.update(value_root);
150    let out = hasher.finalize();
151    let mut arr = [0u8; 32];
152    arr.copy_from_slice(out.as_slice());
153    arr
154}
155
156/// Branch-node hash: `D(BRANCH_TAG || left || right)`.
157#[inline]
158pub fn branch_hash<D: Digest<OutputSize = U32>>(left: &[u8; 32], right: &[u8; 32]) -> [u8; 32] {
159    let mut hasher = D::new();
160    hasher.update([BRANCH_TAG]);
161    hasher.update(left);
162    hasher.update(right);
163    let out = hasher.finalize();
164    let mut arr = [0u8; 32];
165    arr.copy_from_slice(out.as_slice());
166    arr
167}
168
169/// Compute the radix root of the (sorted, distinct-key) entry slice `s`,
170/// whose keys all share the first `depth` bits. Recursion depth is bounded
171/// by `KEY_BITS = 8 * KEY_BYTES`.
172fn radix_node<D, V, const KEY_BYTES: usize>(s: &[Entry<V, KEY_BYTES>], depth: usize) -> [u8; 32]
173where
174    D: Digest<OutputSize = U32>,
175    V: HashTreeRoot,
176{
177    // EMPTY subtree.
178    if s.is_empty() {
179        return EMPTY;
180    }
181    // SINGLETON: collapse to a leaf wherever the subtree narrows to one key.
182    if s.len() == 1 {
183        return leaf_hash::<D>(&s[0].0, &s[0].1.hash_tree_root::<D>());
184    }
185    // Distinct keys must diverge before bit KEY_BITS. Reaching the maximum
186    // depth with ≥ 2 entries means duplicate keys (a violated invariant);
187    // be loud in debug and deterministic (never an out-of-bounds `bit`
188    // read) in release.
189    if depth >= KEY_BYTES * 8 {
190        debug_assert!(false, "RadixMap: duplicate keys at maximum depth");
191        return leaf_hash::<D>(&s[0].0, &s[0].1.hash_tree_root::<D>());
192    }
193    // Within a subtree sharing the first `depth` bits, the strictly-ascending
194    // key order makes `bit(depth) == 0` entries a prefix of the slice.
195    let mid = s.partition_point(|(k, _)| bit(k, depth) == 0);
196    let left = radix_node::<D, V, KEY_BYTES>(&s[..mid], depth + 1);
197    let right = radix_node::<D, V, KEY_BYTES>(&s[mid..], depth + 1);
198    branch_hash::<D>(&left, &right)
199}
200
201/// A structurally-compressed sparse binary radix merkle map (option "b").
202///
203/// Keys are `[u8; KEY_BYTES]` (MSB-first); values are `V: HashTreeRoot`. The
204/// root is canonical and binding (see the module docs). Storage is the
205/// strictly-ascending sorted set of `(key, MissingOr<V>)` entries; the tree
206/// structure is recomputed on [`HashTreeRoot::hash_tree_root`].
207pub struct RadixMap<V, const KEY_BYTES: usize> {
208    /// Sorted strictly-ascending by key (lexicographic byte order ==
209    /// MSB-first bit order). Sole source of truth.
210    entries: Vec<([u8; KEY_BYTES], MissingOr<V>)>,
211}
212
213impl<V, const KEY_BYTES: usize> RadixMap<V, KEY_BYTES> {
214    /// Compile-time guard: a zero-width key is nonsensical.
215    const ASSERT_NONZERO: () = assert!(KEY_BYTES >= 1, "RadixMap KEY_BYTES must be >= 1");
216
217    /// Number of significant key bits (`8 * KEY_BYTES`).
218    pub const KEY_BITS: usize = KEY_BYTES * 8;
219
220    /// Build an empty map.
221    pub fn new() -> Self {
222        let () = Self::ASSERT_NONZERO;
223        Self {
224            entries: Vec::new(),
225        }
226    }
227
228    /// Number of entries (the map's "length" is just its key count).
229    pub fn len(&self) -> usize {
230        self.entries.len()
231    }
232
233    /// `true` iff the map holds no entries.
234    pub fn is_empty(&self) -> bool {
235        self.entries.is_empty()
236    }
237
238    /// Look up an entry by key. O(log n).
239    pub fn get(&self, key: &[u8; KEY_BYTES]) -> Option<&MissingOr<V>> {
240        match self
241            .entries
242            .binary_search_by(|(k, _)| k.as_slice().cmp(key.as_slice()))
243        {
244            Ok(pos) => Some(&self.entries[pos].1),
245            Err(_) => None,
246        }
247    }
248
249    /// Insert (or overwrite) an entry, keeping the sorted invariant.
250    /// Returns the previous value at `key`, if any. O(n) (sorted shift).
251    pub fn insert(&mut self, key: [u8; KEY_BYTES], value: MissingOr<V>) -> Option<MissingOr<V>> {
252        match self
253            .entries
254            .binary_search_by(|(k, _)| k.as_slice().cmp(key.as_slice()))
255        {
256            Ok(pos) => Some(core::mem::replace(&mut self.entries[pos].1, value)),
257            Err(pos) => {
258                self.entries.insert(pos, (key, value));
259                None
260            }
261        }
262    }
263
264    /// Remove the entry at `key`, returning its previous value if present.
265    pub fn remove(&mut self, key: &[u8; KEY_BYTES]) -> Option<MissingOr<V>> {
266        match self
267            .entries
268            .binary_search_by(|(k, _)| k.as_slice().cmp(key.as_slice()))
269        {
270            Ok(pos) => Some(self.entries.remove(pos).1),
271            Err(_) => None,
272        }
273    }
274
275    /// Iterate entries in ascending key order.
276    pub fn iter(&self) -> impl Iterator<Item = (&[u8; KEY_BYTES], &MissingOr<V>)> {
277        self.entries.iter().map(|(k, v)| (k, v))
278    }
279
280    /// Mutably iterate entries in ascending key order (e.g. to resolve `Ref`
281    /// targets after settle), preserving the key set / order.
282    pub fn iter_mut(&mut self) -> impl Iterator<Item = (&[u8; KEY_BYTES], &mut MissingOr<V>)> {
283        self.entries.iter_mut().map(|(k, v)| (&*k, v))
284    }
285}
286
287impl<V, const KEY_BYTES: usize> Default for RadixMap<V, KEY_BYTES> {
288    fn default() -> Self {
289        Self::new()
290    }
291}
292
293impl<V: fmt::Debug, const KEY_BYTES: usize> fmt::Debug for RadixMap<V, KEY_BYTES> {
294    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
295        f.debug_struct("RadixMap")
296            .field("key_bytes", &KEY_BYTES)
297            .field("entries", &self.entries.len())
298            .finish()
299    }
300}
301
302impl<V: Clone, const KEY_BYTES: usize> Clone for RadixMap<V, KEY_BYTES> {
303    fn clone(&self) -> Self {
304        Self {
305            entries: self.entries.clone(),
306        }
307    }
308}
309
310impl<V: PartialEq, const KEY_BYTES: usize> PartialEq for RadixMap<V, KEY_BYTES> {
311    fn eq(&self, other: &Self) -> bool {
312        if self.entries.len() != other.entries.len() {
313            return false;
314        }
315        // Both sorted by key, so element-wise comparison suffices.
316        self.entries
317            .iter()
318            .zip(other.entries.iter())
319            .all(|((ka, va), (kb, vb))| ka == kb && va == vb)
320    }
321}
322
323impl<V: Eq, const KEY_BYTES: usize> Eq for RadixMap<V, KEY_BYTES> {}
324
325impl<V: HashTreeRoot, const KEY_BYTES: usize> HashTreeRoot for RadixMap<V, KEY_BYTES> {
326    fn hash_tree_root<D: Digest<OutputSize = U32>>(&self) -> [u8; 32] {
327        radix_node::<D, V, KEY_BYTES>(&self.entries, 0)
328    }
329}
330
331// --------------------------------------------------------------------------
332// Compact membership / non-membership proofs.
333//
334// A proof is for a single queried key `q`. The verifier folds `q`'s path
335// from the terminal up to the root using the (RLE-compressed) co-path
336// siblings; EMPTY siblings are omitted and spliced back as the public
337// constant. Both membership and non-membership are supported and complete:
338// every absent key reaches either an EMPTY terminal or a diverging leaf at a
339// well-defined depth (no unary-skip gap). Proofs are NOT unique — for some
340// absent keys both an EMPTY and a diverging-leaf terminal can be produced —
341// but every accepted proof is sound under collision resistance.
342// --------------------------------------------------------------------------
343
344/// The node `q`'s walk terminates on.
345#[derive(Debug, Clone, PartialEq, Eq)]
346pub enum RadixTerminal<V, const KEY_BYTES: usize> {
347    /// Membership: a leaf whose key is the queried key, carrying its value.
348    Leaf { value: MissingOr<V> },
349    /// Non-membership: `q`'s path reaches an empty subtree.
350    Empty,
351    /// Non-membership: `q`'s path reaches a leaf for a *different* key that
352    /// shares `q`'s consumed prefix.
353    DivergingLeaf {
354        other_key: [u8; KEY_BYTES],
355        other_value_root: [u8; 32],
356    },
357}
358
359/// A compact proof of (non-)membership of a single key in a [`RadixMap`].
360#[derive(Debug, Clone, PartialEq, Eq)]
361pub struct RadixProof<V, const KEY_BYTES: usize> {
362    /// Depth of the terminal node = number of branch levels on `q`'s path.
363    pub term_depth: u32,
364    /// Non-empty co-path siblings, `(level, hash)`, strictly ascending by
365    /// `level`, all `< term_depth`. Absent levels are the EMPTY constant.
366    pub siblings: Vec<(u32, [u8; 32])>,
367    /// The node `q`'s walk terminates on.
368    pub terminal: RadixTerminal<V, KEY_BYTES>,
369}
370
371/// Outcome of a verified proof.
372#[derive(Debug, Clone, Copy, PartialEq, Eq)]
373pub enum RadixVerdict {
374    /// The queried key is present (its value is in the proof's terminal).
375    Present,
376    /// The queried key is absent.
377    Absent,
378}
379
380/// Count of leading equal bits (MSB-first) shared by `a` and `b`.
381fn shared_prefix_bits(a: &[u8], b: &[u8]) -> usize {
382    let mut n = 0;
383    for (x, y) in a.iter().zip(b.iter()) {
384        let d = x ^ y;
385        if d == 0 {
386            n += 8;
387        } else {
388            n += d.leading_zeros() as usize;
389            break;
390        }
391    }
392    n
393}
394
395impl<V: HashTreeRoot + Clone, const KEY_BYTES: usize> RadixMap<V, KEY_BYTES> {
396    /// Produce a compact (non-)membership proof for `key`.
397    pub fn prove<D: Digest<OutputSize = U32>>(
398        &self,
399        key: &[u8; KEY_BYTES],
400    ) -> RadixProof<V, KEY_BYTES> {
401        let mut siblings = Vec::new();
402        let (terminal, term_depth) =
403            prove_walk::<D, V, KEY_BYTES>(&self.entries, key, 0, &mut siblings);
404        RadixProof {
405            term_depth: term_depth as u32,
406            siblings,
407            terminal,
408        }
409    }
410}
411
412/// Walk `q`'s path through the (sorted) entry slice, collecting non-empty
413/// co-path siblings; returns the terminal and its depth.
414fn prove_walk<D, V, const KEY_BYTES: usize>(
415    s: &[Entry<V, KEY_BYTES>],
416    q: &[u8; KEY_BYTES],
417    depth: usize,
418    siblings: &mut Vec<(u32, [u8; 32])>,
419) -> (RadixTerminal<V, KEY_BYTES>, usize)
420where
421    D: Digest<OutputSize = U32>,
422    V: HashTreeRoot + Clone,
423{
424    if s.is_empty() {
425        return (RadixTerminal::Empty, depth);
426    }
427    if s.len() == 1 || depth >= KEY_BYTES * 8 {
428        let (k, v) = &s[0];
429        return if k.as_slice() == q.as_slice() {
430            (RadixTerminal::Leaf { value: v.clone() }, depth)
431        } else {
432            (
433                RadixTerminal::DivergingLeaf {
434                    other_key: *k,
435                    other_value_root: v.hash_tree_root::<D>(),
436                },
437                depth,
438            )
439        };
440    }
441    let mid = s.partition_point(|(k, _)| bit(k, depth) == 0);
442    let (left, right) = s.split_at(mid);
443    let (chosen, sibling_slice) = if bit(q, depth) == 0 {
444        (left, right)
445    } else {
446        (right, left)
447    };
448    let sib_root = radix_node::<D, V, KEY_BYTES>(sibling_slice, depth + 1);
449    if sib_root != EMPTY {
450        siblings.push((depth as u32, sib_root));
451    }
452    prove_walk::<D, V, KEY_BYTES>(chosen, q, depth + 1, siblings)
453}
454
455/// Verify a [`RadixProof`] for `key` against `root`. Returns the verdict, or
456/// `None` if the proof is malformed or does not reconstruct `root`.
457///
458/// Hardening (all enforced before accepting): `term_depth ≤ KEY_BITS`;
459/// sibling levels strictly ascending, distinct, and all `< term_depth`; a
460/// `DivergingLeaf` must carry `other_key ≠ key` sharing `key`'s
461/// `term_depth`-bit prefix. The terminal depth is soft-authenticated by the
462/// fold (a wrong depth cannot reconstruct `root` without a collision).
463pub fn verify<D: Digest<OutputSize = U32>, V: HashTreeRoot, const KEY_BYTES: usize>(
464    root: &[u8; 32],
465    key: &[u8; KEY_BYTES],
466    proof: &RadixProof<V, KEY_BYTES>,
467) -> Option<RadixVerdict> {
468    let td = proof.term_depth as usize;
469    if td > KEY_BYTES * 8 {
470        return None;
471    }
472    // Sibling levels: strictly ascending, distinct, all < term_depth.
473    let mut prev: Option<u32> = None;
474    for (lvl, _) in &proof.siblings {
475        if (*lvl as usize) >= td {
476            return None;
477        }
478        if let Some(p) = prev
479            && *lvl <= p
480        {
481            return None;
482        }
483        prev = Some(*lvl);
484    }
485    // Terminal node hash + verdict.
486    let (mut cur, verdict) = match &proof.terminal {
487        RadixTerminal::Leaf { value } => (
488            leaf_hash::<D>(key, &value.hash_tree_root::<D>()),
489            RadixVerdict::Present,
490        ),
491        RadixTerminal::Empty => (EMPTY, RadixVerdict::Absent),
492        RadixTerminal::DivergingLeaf {
493            other_key,
494            other_value_root,
495        } => {
496            if other_key.as_slice() == key.as_slice() {
497                return None;
498            }
499            if shared_prefix_bits(other_key, key) < td {
500                return None;
501            }
502            (
503                leaf_hash::<D>(other_key, other_value_root),
504                RadixVerdict::Absent,
505            )
506        }
507    };
508    // Fold leaf-ward → root-ward, splicing EMPTY for absent sibling levels.
509    for level in (0..td).rev() {
510        let sib = proof
511            .siblings
512            .iter()
513            .find(|(l, _)| *l as usize == level)
514            .map(|(_, h)| *h)
515            .unwrap_or(EMPTY);
516        cur = if bit(key, level) == 0 {
517            branch_hash::<D>(&cur, &sib)
518        } else {
519            branch_hash::<D>(&sib, &cur)
520        };
521    }
522    if &cur == root { Some(verdict) } else { None }
523}
524
525// --------------------------------------------------------------------------
526// Wire format: (entries_offset: u32 = 4, List<(ByteVector[KEY_BYTES], MissingOr<V>)>).
527//
528// Mirrors `SparseList`'s entry-list encoding with the u64 key replaced by a
529// fixed KEY_BYTES key and the per-entry inner value-offset = KEY_BYTES + 4.
530// There is no leading `len` (a map has no logical length).
531// --------------------------------------------------------------------------
532
533impl<V: Encode, const KEY_BYTES: usize> Encode for RadixMap<V, KEY_BYTES> {
534    fn is_ssz_fixed_len() -> bool {
535        false
536    }
537    fn ssz_fixed_len() -> usize {
538        BYTES_PER_LENGTH_OFFSET
539    }
540    fn ssz_bytes_len(&self) -> usize {
541        // 4 (entries_offset) + offset table (n*4) + per-entry (key + 4 + value).
542        let n = self.entries.len();
543        let var: usize = self.entries.iter().map(|(_, v)| v.ssz_bytes_len()).sum();
544        4 + n * (BYTES_PER_LENGTH_OFFSET + KEY_BYTES + BYTES_PER_LENGTH_OFFSET) + var
545    }
546    fn ssz_append(&self, buf: &mut Vec<u8>) {
547        // entries_offset = 4.
548        buf.extend_from_slice(&4u32.to_le_bytes());
549        encode_entries_list::<V, KEY_BYTES>(&self.entries, buf);
550    }
551}
552
553fn encode_entries_list<V: Encode, const KEY_BYTES: usize>(
554    entries: &[Entry<V, KEY_BYTES>],
555    buf: &mut Vec<u8>,
556) {
557    let n = entries.len();
558    let header_size = n * BYTES_PER_LENGTH_OFFSET;
559    let start = buf.len();
560    buf.resize(start + header_size, 0u8);
561
562    let mut running = header_size as u32;
563    for (i, (key, val)) in entries.iter().enumerate() {
564        let off_pos = start + i * BYTES_PER_LENGTH_OFFSET;
565        buf[off_pos..off_pos + 4].copy_from_slice(&running.to_le_bytes());
566
567        let entry_start = buf.len();
568        // key (KEY_BYTES fixed)
569        buf.extend_from_slice(key);
570        // value-offset within this entry container = KEY_BYTES + 4
571        buf.extend_from_slice(&((KEY_BYTES + BYTES_PER_LENGTH_OFFSET) as u32).to_le_bytes());
572        // value payload
573        val.ssz_append(buf);
574
575        let entry_end = buf.len();
576        running = running
577            .checked_add((entry_end - entry_start) as u32)
578            .expect("ssz offset overflow");
579    }
580}
581
582impl<V: Decode, const KEY_BYTES: usize> Decode for RadixMap<V, KEY_BYTES> {
583    fn is_ssz_fixed_len() -> bool {
584        false
585    }
586    fn ssz_fixed_len() -> usize {
587        BYTES_PER_LENGTH_OFFSET
588    }
589    fn from_ssz_bytes(bytes: &[u8]) -> Result<Self, DecodeError> {
590        if bytes.len() < 4 {
591            return Err(DecodeError::UnexpectedEof {
592                expected: 4,
593                actual: bytes.len(),
594            });
595        }
596        let entries_offset = u32::from_le_bytes(bytes[0..4].try_into().unwrap()) as usize;
597        if entries_offset != 4 {
598            return Err(DecodeError::InvalidOffset {
599                offset: entries_offset,
600                len: bytes.len(),
601                fixed: 4,
602            });
603        }
604        let entries = decode_entries_list::<V, KEY_BYTES>(&bytes[4..])?;
605        // Enforce strictly-ascending keys (rejects duplicates + bad order).
606        let mut prev: Option<&[u8; KEY_BYTES]> = None;
607        for (k, _) in &entries {
608            if let Some(p) = prev
609                && k.as_slice() <= p.as_slice()
610            {
611                return Err(DecodeError::NotSorted);
612            }
613            prev = Some(k);
614        }
615        Ok(Self { entries })
616    }
617}
618
619fn decode_entries_list<V: Decode, const KEY_BYTES: usize>(
620    bytes: &[u8],
621) -> Result<Vec<Entry<V, KEY_BYTES>>, DecodeError> {
622    if bytes.is_empty() {
623        return Ok(Vec::new());
624    }
625    if bytes.len() < BYTES_PER_LENGTH_OFFSET {
626        return Err(DecodeError::UnexpectedEof {
627            expected: BYTES_PER_LENGTH_OFFSET,
628            actual: bytes.len(),
629        });
630    }
631    let first = u32::from_le_bytes(bytes[0..4].try_into().unwrap()) as usize;
632    if !first.is_multiple_of(BYTES_PER_LENGTH_OFFSET) || first > bytes.len() {
633        return Err(DecodeError::InvalidOffset {
634            offset: first,
635            len: bytes.len(),
636            fixed: 0,
637        });
638    }
639    let n = first / BYTES_PER_LENGTH_OFFSET;
640    let mut offsets = Vec::with_capacity(n + 1);
641    offsets.push(first);
642    for i in 1..n {
643        if bytes.len() < (i + 1) * BYTES_PER_LENGTH_OFFSET {
644            return Err(DecodeError::UnexpectedEof {
645                expected: (i + 1) * BYTES_PER_LENGTH_OFFSET,
646                actual: bytes.len(),
647            });
648        }
649        let off = u32::from_le_bytes(
650            bytes[i * BYTES_PER_LENGTH_OFFSET..(i + 1) * BYTES_PER_LENGTH_OFFSET]
651                .try_into()
652                .unwrap(),
653        ) as usize;
654        if off < *offsets.last().unwrap() {
655            return Err(DecodeError::OffsetsNotMonotonic {
656                prev: *offsets.last().unwrap(),
657                curr: off,
658            });
659        }
660        if off > bytes.len() {
661            return Err(DecodeError::InvalidOffset {
662                offset: off,
663                len: bytes.len(),
664                fixed: first,
665            });
666        }
667        offsets.push(off);
668    }
669    offsets.push(bytes.len());
670
671    let mut out = Vec::with_capacity(n);
672    for i in 0..n {
673        let entry = &bytes[offsets[i]..offsets[i + 1]];
674        // key (KEY_BYTES) + value-offset (4, must be KEY_BYTES+4) + MissingOr payload.
675        if entry.len() < KEY_BYTES + BYTES_PER_LENGTH_OFFSET {
676            return Err(DecodeError::UnexpectedEof {
677                expected: KEY_BYTES + BYTES_PER_LENGTH_OFFSET,
678                actual: entry.len(),
679            });
680        }
681        let mut key = [0u8; KEY_BYTES];
682        key.copy_from_slice(&entry[0..KEY_BYTES]);
683        let value_offset = u32::from_le_bytes(
684            entry[KEY_BYTES..KEY_BYTES + BYTES_PER_LENGTH_OFFSET]
685                .try_into()
686                .unwrap(),
687        ) as usize;
688        if value_offset != KEY_BYTES + BYTES_PER_LENGTH_OFFSET {
689            return Err(DecodeError::InvalidOffset {
690                offset: value_offset,
691                len: entry.len(),
692                fixed: KEY_BYTES + BYTES_PER_LENGTH_OFFSET,
693            });
694        }
695        let value = MissingOr::<V>::from_ssz_bytes(&entry[KEY_BYTES + BYTES_PER_LENGTH_OFFSET..])?;
696        out.push((key, value));
697    }
698    Ok(out)
699}
700
701// --------------------------------------------------------------------------
702// rkyv: hand-rolled via delegation to `RadixMapRepr` (mirroring
703// `SparseListRepr`). On deserialize we re-sort the entries so the in-memory
704// canonical (strictly-ascending) order is restored regardless of the
705// archived order — a defensive guard against a non-canonical blob.
706// --------------------------------------------------------------------------
707
708#[derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
709pub struct RadixMapRepr<V, const KEY_BYTES: usize>
710where
711    V: rkyv::Archive,
712    MissingOr<V>: rkyv::Archive,
713{
714    pub entries: Vec<([u8; KEY_BYTES], MissingOr<V>)>,
715}
716
717impl<V, const KEY_BYTES: usize> rkyv::Archive for RadixMap<V, KEY_BYTES>
718where
719    V: rkyv::Archive + Clone,
720    MissingOr<V>: rkyv::Archive,
721{
722    type Archived = <RadixMapRepr<V, KEY_BYTES> as rkyv::Archive>::Archived;
723    type Resolver = <RadixMapRepr<V, KEY_BYTES> as rkyv::Archive>::Resolver;
724
725    fn resolve(&self, resolver: Self::Resolver, out: rkyv::Place<Self::Archived>) {
726        let repr = RadixMapRepr {
727            entries: self.entries.clone(),
728        };
729        <RadixMapRepr<V, KEY_BYTES> as rkyv::Archive>::resolve(&repr, resolver, out)
730    }
731}
732
733impl<V, S, const KEY_BYTES: usize> rkyv::Serialize<S> for RadixMap<V, KEY_BYTES>
734where
735    V: rkyv::Archive + Clone,
736    MissingOr<V>: rkyv::Archive,
737    RadixMapRepr<V, KEY_BYTES>: rkyv::Serialize<S>,
738    S: rkyv::rancor::Fallible + ?Sized,
739{
740    fn serialize(
741        &self,
742        serializer: &mut S,
743    ) -> Result<Self::Resolver, <S as rkyv::rancor::Fallible>::Error> {
744        let repr = RadixMapRepr {
745            entries: self.entries.clone(),
746        };
747        <RadixMapRepr<V, KEY_BYTES> as rkyv::Serialize<S>>::serialize(&repr, serializer)
748    }
749}
750
751impl<V, D, const KEY_BYTES: usize> rkyv::Deserialize<RadixMap<V, KEY_BYTES>, D>
752    for <RadixMapRepr<V, KEY_BYTES> as rkyv::Archive>::Archived
753where
754    V: rkyv::Archive + Clone,
755    MissingOr<V>: rkyv::Archive,
756    <RadixMapRepr<V, KEY_BYTES> as rkyv::Archive>::Archived:
757        rkyv::Deserialize<RadixMapRepr<V, KEY_BYTES>, D>,
758    D: rkyv::rancor::Fallible + ?Sized,
759{
760    fn deserialize(
761        &self,
762        deserializer: &mut D,
763    ) -> Result<RadixMap<V, KEY_BYTES>, <D as rkyv::rancor::Fallible>::Error> {
764        let repr: RadixMapRepr<V, KEY_BYTES> =
765            rkyv::Deserialize::<RadixMapRepr<V, KEY_BYTES>, D>::deserialize(self, deserializer)?;
766        let mut entries = repr.entries;
767        // Restore the canonical strictly-ascending order (defensive against a
768        // non-canonically-ordered archived blob). Duplicate keys would be a
769        // corrupt blob; `radix_node` handles them deterministically without
770        // panicking.
771        entries.sort_by(|(a, _), (b, _)| a.as_slice().cmp(b.as_slice()));
772        Ok(RadixMap { entries })
773    }
774}