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}