Skip to main content

javm_cap/cap/
cnode.rs

1//! `CNodeCap` — CNode cap: a sparse, Key-addressed key→cap map.
2//!
3//! A CNode is a direct map from logical [`Key`] byte strings to
4//! [`CapHashOrRef`] slot targets. There is **no fixed capacity bound** — a
5//! CNode is bounded by storage quota, not a compile-time slot count.
6//!
7//! A slot is named by a [`Key`] (a short byte string); [`CNodeCap::get`]
8//! / [`CNodeCap::set`] / [`CNodeCap::take`] operate on that logical key
9//! directly. The V1 ABI uses single-byte keys (`Key::from(b)`), but the map
10//! admits arbitrary-length keys natively (the same `get`/`set`/`take`
11//! surface), so a future ABI can key e.g. `address -> Cap::Instance` with no
12//! structural change. The raw `&[u8]` convenience form is exposed via
13//! [`CNodeCap::get_key`] / [`CNodeCap::set_key`] / [`CNodeCap::take_key`].
14//!
15//! The hash-keyed radix tree is a commitment/proof representation, not the
16//! runtime storage model. [`HashTreeRoot`](ssz::HashTreeRoot) derives that
17//! view on demand by hashing each logical key into a 32-byte radix path; normal
18//! kernel execution never hashes a CNode key just to read or mutate a slot.
19//!
20//! The leaf value is **always a cap** ([`CapHashOrRef`]), never raw data —
21//! this is what lets a CNode model e.g. `address -> Cap::Instance` for
22//! native contracts. A `Missing(h)` placeholder substitutes losslessly for
23//! the materialized value whose `hash_tree_root` equals `h`, the
24//! load-bearing property for cold-loading a CNode by hash.
25//!
26//! ## The `O` (owned-payload) parameter
27//!
28//! `CNodeCap<O>` is generic over the inline `CapHashOrRef::Owned(O)` payload.
29//! The default `O = Box<Cap>` is the wire/content-addressed form: the cnode
30//! inside a serialised [`Cap`] (`Cap::CNode`, `InstanceCap.root_cnode`) is
31//! always `CNodeCap<Box<Cap>>`, so the wire type is unaffected by this
32//! parameter. An engine that needs to attach engine-private state to a
33//! resident instance instantiates the *running frame's* cnode with a richer
34//! payload (e.g. `Box<CachedCap>` in the recompiler) — that payload is
35//! deliberately **not** wire-serialisable, so a cache-carrying cnode cannot
36//! cross the host/guest boundary or be content-hashed (a compile error, not
37//! a runtime panic). See [`CapHashOrRef`] for the gate.
38
39use alloc::boxed::Box;
40use alloc::vec::Vec;
41
42use ssz::{MissingOr, RadixMap};
43
44use super::Cap;
45use crate::cache::CapHashOrRef;
46use crate::error::CapError;
47use crate::hash::{Hash, Hasher};
48use crate::slot::Key;
49
50/// Commitment radix-key width: a 32-byte digest of the logical key.
51///
52/// This width is used only when deriving a Merkle/radix commitment in
53/// [`HashTreeRoot`](ssz::HashTreeRoot). Runtime CNode access is direct by
54/// [`Key`].
55pub const CNODE_COMMITMENT_KEY_BYTES: usize = 32;
56
57/// Direct runtime slot map backing a CNode: `Key -> CapHashOrRef<O>`.
58///
59/// Stored as a sorted vector, not a `BTreeMap`: CNodes are usually tiny in the
60/// hot guest path, and avoiding per-entry tree-node allocations matters. The
61/// API is map-shaped and preserves strictly-ascending keys.
62#[derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
63pub struct CNodeSlots<O = Box<Cap>> {
64    entries: Vec<(Key, MissingOr<CapHashOrRef<O>>)>,
65}
66
67impl<O> CNodeSlots<O> {
68    pub fn new() -> Self {
69        Self {
70            entries: Vec::new(),
71        }
72    }
73
74    pub fn len(&self) -> usize {
75        self.entries.len()
76    }
77
78    pub fn is_empty(&self) -> bool {
79        self.entries.is_empty()
80    }
81
82    pub fn get(&self, key: &Key) -> Option<&MissingOr<CapHashOrRef<O>>> {
83        self.entries
84            .binary_search_by(|(k, _)| k.cmp(key))
85            .ok()
86            .map(|pos| &self.entries[pos].1)
87    }
88
89    pub fn insert(
90        &mut self,
91        key: Key,
92        value: MissingOr<CapHashOrRef<O>>,
93    ) -> Option<MissingOr<CapHashOrRef<O>>> {
94        match self.entries.binary_search_by(|(k, _)| k.cmp(&key)) {
95            Ok(pos) => Some(core::mem::replace(&mut self.entries[pos].1, value)),
96            Err(pos) => {
97                self.entries.insert(pos, (key, value));
98                None
99            }
100        }
101    }
102
103    pub fn remove(&mut self, key: &Key) -> Option<MissingOr<CapHashOrRef<O>>> {
104        match self.entries.binary_search_by(|(k, _)| k.cmp(key)) {
105            Ok(pos) => Some(self.entries.remove(pos).1),
106            Err(_) => None,
107        }
108    }
109
110    pub fn iter(&self) -> impl Iterator<Item = (&Key, &MissingOr<CapHashOrRef<O>>)> {
111        self.entries.iter().map(|(k, v)| (k, v))
112    }
113
114    pub fn iter_mut(&mut self) -> impl Iterator<Item = (&Key, &mut MissingOr<CapHashOrRef<O>>)> {
115        self.entries.iter_mut().map(|(k, v)| (&*k, v))
116    }
117}
118
119impl<O> Default for CNodeSlots<O> {
120    fn default() -> Self {
121        Self::new()
122    }
123}
124
125impl<O: Clone> Clone for CNodeSlots<O> {
126    fn clone(&self) -> Self {
127        Self {
128            entries: self.entries.clone(),
129        }
130    }
131}
132
133impl<O: core::fmt::Debug> core::fmt::Debug for CNodeSlots<O> {
134    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
135        f.debug_list().entries(self.entries.iter()).finish()
136    }
137}
138
139/// CNode cap: a sparse, direct `Key -> CapHashOrRef<O>` map.
140///
141/// rkyv (the wire form) is derived; the derive adds a `slots: Archive` field
142/// bound, which for the wire payload resolves via `CapHashOrRef<Box<Cap>>:
143/// Archive` (gated on `Box<Cap>: WireOwned`, a leaf marker — no recursion into
144/// `Cap: Archive`). A non-wire payload such as `Box<CachedCap>` does not
145/// satisfy the field bound, so a `CNodeCap<Box<CachedCap>>` has no rkyv impl
146/// and cannot cross the wire — non-serialisable by construction. `Clone` /
147/// `Debug` / `Default` / `HashTreeRoot` are hand-rolled with payload-specific
148/// bounds (a blanket derive would over-constrain `Box<Cap>`, which is not
149/// `Default`, and `ssz_derive::HashTreeRoot` adds no bound at all, so it would
150/// not compile for a generic field).
151#[derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
152pub struct CNodeCap<O = Box<Cap>> {
153    /// Sparse slot table keyed by logical [`Key`]. Absent keys are empty; a
154    /// `Missing(h)` entry substitutes losslessly for the value rooting at `h`.
155    pub slots: CNodeSlots<O>,
156}
157
158impl<O> Default for CNodeCap<O> {
159    fn default() -> Self {
160        Self::new()
161    }
162}
163
164impl<O: Clone> Clone for CNodeCap<O> {
165    fn clone(&self) -> Self {
166        Self {
167            slots: self.slots.clone(),
168        }
169    }
170}
171
172impl<O: core::fmt::Debug> core::fmt::Debug for CNodeCap<O> {
173    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
174        f.debug_struct("CNodeCap")
175            .field("slots", &self.slots)
176            .finish()
177    }
178}
179
180// Hand-rolled to match the `ssz_derive::HashTreeRoot` output for a single
181// named field (`merkleize` over the one field root), but with a
182// payload-specific bound: only an archivable `O` (the wire payload) admits a
183// content hash, so a cache-carrying cnode is non-hashable at compile time.
184impl<O> ssz::HashTreeRoot for CNodeCap<O>
185where
186    O: Clone,
187    CapHashOrRef<O>: ssz::HashTreeRoot,
188{
189    fn hash_tree_root<D: ssz::digest::Digest<OutputSize = ssz::digest::typenum::U32>>(
190        &self,
191    ) -> [u8; 32] {
192        let mut commitment = RadixMap::<CapHashOrRef<O>, CNODE_COMMITMENT_KEY_BYTES>::new();
193        for (key, value) in self.slots.iter() {
194            commitment.insert(Self::commitment_key(key), value.clone());
195        }
196        let roots: [[u8; 32]; 1] = [commitment.hash_tree_root::<D>()];
197        ssz::merkleize::<D>(&roots, 1)
198    }
199}
200
201impl<O> CNodeCap<O> {
202    /// Construct an empty CNode (no slots). A CNode grows on demand and is
203    /// bounded by storage quota, not a fixed slot count.
204    pub fn new() -> Self {
205        Self {
206            slots: CNodeSlots::new(),
207        }
208    }
209
210    /// Commitment radix key for a logical key: `Hasher(key)`.
211    ///
212    /// This is intentionally not used by ordinary `get` / `set` / `take`.
213    #[inline]
214    pub fn commitment_key(key: &Key) -> [u8; CNODE_COMMITMENT_KEY_BYTES] {
215        <Hasher as Hash>::hash(key.as_slice())
216    }
217
218    // ---- logical byte-string key API ----
219
220    /// Borrow the cap bound to logical key `k` **without cloning** — the
221    /// read-only peek used to inspect an `Owned` cap (e.g. read a callee's
222    /// `image_hash` to price a CALL) before deciding whether to
223    /// [`take_key`](Self::take_key) it. `None` for an absent key or a
224    /// `Missing(_)` placeholder.
225    pub fn peek_key(&self, k: &[u8]) -> Option<&CapHashOrRef<O>> {
226        match self.slots.get(&Key::from(k))? {
227            MissingOr::Materialized(t) => Some(t),
228            MissingOr::Missing(_) => None,
229        }
230    }
231
232    /// Bind logical key `k` to `target`, or clear the binding if `target`
233    /// is `None`. Returns the prior materialized target, if any —
234    /// **moved out**, not cloned, so a `CapHashOrRef::Owned(O)` transfers
235    /// with no deep copy (the zero-copy cnode move).
236    pub fn set_key(
237        &mut self,
238        k: &[u8],
239        target: Option<CapHashOrRef<O>>,
240    ) -> Option<CapHashOrRef<O>> {
241        let key = Key::from(k);
242        // `insert` / `remove` hand back the displaced entry by value — no
243        // clone of the prior `CapHashOrRef`.
244        let old = match target {
245            Some(t) => self.slots.insert(key, MissingOr::Materialized(t)),
246            None => self.slots.remove(&key),
247        };
248        match old {
249            Some(MissingOr::Materialized(t)) => Some(t),
250            Some(MissingOr::Missing(_)) | None => None,
251        }
252    }
253
254    /// Take the binding at logical key `k`, leaving it empty. Returns the
255    /// prior materialized target (or `None`), **moved out** — the
256    /// zero-copy half of an `Owned` cnode-to-cnode (or frame-to-frame)
257    /// move.
258    pub fn take_key(&mut self, k: &[u8]) -> Option<CapHashOrRef<O>> {
259        self.set_key(k, None)
260    }
261
262    // ---- Key API ----
263
264    /// Bind `key` to `target`, or clear it if `None`. Returns the prior
265    /// materialized target, if any. The radix map is unbounded, so this is
266    /// infallible; the `Result` is retained for ABI compatibility with the
267    /// pervasive `?`-using call sites.
268    pub fn set(
269        &mut self,
270        key: &Key,
271        target: Option<CapHashOrRef<O>>,
272    ) -> Result<Option<CapHashOrRef<O>>, CapError> {
273        // `insert` / `remove` hand back the displaced entry by value — no
274        // clone of the prior `CapHashOrRef`.
275        let old = match target {
276            Some(t) => self.slots.insert(key.clone(), MissingOr::Materialized(t)),
277            None => self.slots.remove(key),
278        };
279        Ok(match old {
280            Some(MissingOr::Materialized(t)) => Some(t),
281            Some(MissingOr::Missing(_)) | None => None,
282        })
283    }
284
285    /// Take the binding at `key`, leaving it empty. Returns the prior
286    /// materialized target (or `None`).
287    pub fn take(&mut self, key: &Key) -> Result<Option<CapHashOrRef<O>>, CapError> {
288        self.set(key, None)
289    }
290}
291
292impl<O: Clone> CNodeCap<O> {
293    /// Look up the cap bound to logical key `k`. Returns `None` for an
294    /// absent key or a `Missing(_)` placeholder (callers needing to tell
295    /// "absent" from "missing placeholder" apart inspect `self.slots`).
296    pub fn get_key(&self, k: &[u8]) -> Option<CapHashOrRef<O>> {
297        match self.slots.get(&Key::from(k))? {
298            MissingOr::Materialized(t) => Some(t.clone()),
299            MissingOr::Missing(_) => None,
300        }
301    }
302
303    /// Look up the slot named by `key`. See [`CNodeCap::get_key`] for the
304    /// placeholder semantics.
305    pub fn get(&self, key: &Key) -> Option<CapHashOrRef<O>> {
306        match self.slots.get(key)? {
307            MissingOr::Materialized(t) => Some(t.clone()),
308            MissingOr::Missing(_) => None,
309        }
310    }
311}