javm_cap/cnode.rs
1//! `CNodeCap` — CNode cap.
2//!
3//! Slot table is a [`SparseList`] of [`MissingOr`] entries: a sparse
4//! materialized-on-demand map from `SlotIdx` to [`CapHashOrRef`]. The
5//! merkle tree shape is fixed at depth 16 (= ceil_log2(MAX_CNODE_SLOTS))
6//! regardless of `size_log`; `size_log` is runtime metadata used for
7//! bounds-checking slot indices.
8//!
9//! Empty slots contribute `zero_hash` at the depth-16 leaf level; a
10//! `Missing(h)` placeholder substitutes losslessly for the materialized
11//! contents whose `hash_tree_root` equals `h`. This is the load-bearing
12//! property for sparse cnode loading from cold storage.
13//!
14//! `size_log` is permitted in `0..=16` (the spec's hard ceiling).
15
16use ssz::{MissingOr, SparseList};
17
18use crate::error::CapError;
19use crate::slot::SlotIdx;
20
21use super::cap::CapHashOrRef;
22
23/// Maximum cnode capacity (`2^16` slots). The SSZ merkle tree depth is
24/// fixed at 16 regardless of an individual cnode's declared `size_log`.
25pub const MAX_CNODE_SLOTS: u64 = 1u64 << 16;
26
27#[derive(Clone, Debug, ssz_derive::HashTreeRoot)]
28pub struct CNodeCap {
29 pub size_log: u8,
30 /// Sparse slot table keyed by slot index. Missing keys are absent
31 /// slots (contribute `zero_hash` to the merkle root). The merkle
32 /// tree is always size `MAX_CNODE_SLOTS = 2^16`; `size_log` bounds
33 /// the addressable range.
34 pub slots: SparseList<CapHashOrRef, MAX_CNODE_SLOTS>,
35}
36
37impl CNodeCap {
38 /// Construct an empty cnode of `2^size_log` slots.
39 /// Rejects `size_log > 16`.
40 pub fn new(size_log: u8) -> Result<Self, CapError> {
41 if size_log > 16 {
42 return Err(CapError::InvalidCNodeSize(size_log));
43 }
44 Ok(Self {
45 size_log,
46 slots: SparseList::new(),
47 })
48 }
49
50 /// Number of slots in the cnode (`2^size_log`).
51 pub fn capacity(&self) -> u64 {
52 1u64 << self.size_log
53 }
54
55 /// Look up a slot by index. Returns `None` for empty (unmaterialized)
56 /// slots; returns the materialized `CapHashOrRef` otherwise.
57 ///
58 /// For a `MissingOr::Missing(_)` placeholder slot (used when a
59 /// subtree was loaded by hash without contents), this returns
60 /// `None` — callers needing to distinguish "absent" from "missing
61 /// placeholder" should inspect `self.slots.get(...)` directly.
62 pub fn get(&self, slot: SlotIdx) -> Option<CapHashOrRef> {
63 match self.slots.get(slot.get() as u64)? {
64 MissingOr::Materialized(t) => Some(t.clone()),
65 MissingOr::Missing(_) => None,
66 }
67 }
68
69 /// Bind `slot` to `target`, or clear the binding if `target` is
70 /// `None`. Rejects slot indices outside the cnode's `2^size_log`
71 /// range. Returns the prior materialized target at `slot`, if any.
72 pub fn set(
73 &mut self,
74 slot: SlotIdx,
75 target: Option<CapHashOrRef>,
76 ) -> Result<Option<CapHashOrRef>, CapError> {
77 if !slot.fits(self.size_log) {
78 return Err(CapError::SlotOutOfRange(slot.get(), self.size_log));
79 }
80 let key = slot.get() as u64;
81 let prior = match self.slots.get(key) {
82 Some(MissingOr::Materialized(t)) => Some(t.clone()),
83 Some(MissingOr::Missing(_)) | None => None,
84 };
85 match target {
86 Some(t) => {
87 // `MAX_CNODE_SLOTS = 2^16` and `slot.fits(size_log)` with
88 // `size_log <= 16` guarantee `key < MAX_CNODE_SLOTS`, so
89 // the bound check inside `SparseList::insert` cannot fail.
90 self.slots
91 .insert(key, MissingOr::Materialized(t))
92 .expect("slot index fits cnode capacity (checked above)");
93 }
94 None => {
95 self.slots.remove(key);
96 }
97 }
98 Ok(prior)
99 }
100
101 /// Take the binding at `slot`, leaving the slot empty. Returns the
102 /// prior target (or `None` if the slot was already empty).
103 pub fn take(&mut self, slot: SlotIdx) -> Result<Option<CapHashOrRef>, CapError> {
104 self.set(slot, None)
105 }
106
107 /// Alias of `set(slot, None)`.
108 pub fn remove(&mut self, slot: SlotIdx) -> Result<Option<CapHashOrRef>, CapError> {
109 self.set(slot, None)
110 }
111}
112
113/// One populated slot — retained as a serialisation helper for callers
114/// that need a flat `(slot, target)` pair (e.g., `CacheDirectory::publish_cnode`).
115///
116/// The on-the-wire/hash representation of `CNodeCap` no longer uses this
117/// type; the cnode is encoded directly as a `SparseList<CapHashOrRef, ...>`.
118#[derive(Clone, Debug, PartialEq, Eq)]
119pub struct CNodeSlotEntry {
120 pub slot: SlotIdx,
121 pub target: CapHashOrRef,
122}