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}