Skip to main content

javm/
kernel_assist.rs

1//! `KernelAssist` trait: the integration point for kernel-assisted
2//! Instances (GasMeter, StorageQuota, YieldCatcher).
3//!
4//! Per the v3 spec (README §22 "Kernel-assisted Instances"), certain
5//! Cap::Instance values are recognized by their `image_hash_chain` as
6//! kernel-internal. The kernel short-circuits their state access — no
7//! bytecode dispatch, no endpoint walk. From userspace, those caps
8//! still look like ordinary Cap::Instance values; the
9//! special-cased path is invisible.
10//!
11//! After the move to the `javm_cap::Cap<A>` cache model, image / data
12//! / file lookups are cache operations rather than KernelAssist
13//! hooks — the cache holds the content, the assist holds only the
14//! per-block ephemeral kernel state (gas meters, storage quotas,
15//! yield catchers, file id allocation).
16
17use core::fmt;
18use std::collections::HashMap;
19
20use javm_cap::{Blake2b256, CapHash, CapHashOrRef, Hash};
21
22/// Identifier for a row in the kernel-internal GasMeter table.
23/// Chain-chosen, not kernel-assigned (per spec §22).
24pub type MeterId = u64;
25
26/// Identifier for a row in the kernel-internal StorageQuota table.
27pub type QuotaId = u64;
28
29/// The integration point for kernel-assisted Instances.
30///
31/// Stage 3 wires the Vm to call these methods at:
32/// - per-instruction gas debit (gas_meter_*),
33/// - host_yield routing (yield_catcher_*),
34/// - host_mint_data_cap quota debit (storage_quota_*),
35/// - host_open / host_save resolved via cache references.
36///
37/// Methods on `&self` are reads; methods on `&mut self` are state
38/// mutations. Atomic semantics where the spec requires it
39/// (`*_set` returns the previous value).
40pub trait KernelAssist {
41    // ---- GasMeter (kernel:gasmeter) ----
42
43    /// Read the remaining gas for `meter_id`. Missing entry → 0.
44    fn gas_meter_get(&self, meter_id: MeterId) -> u64;
45
46    /// Atomically `GasMeter[meter_id] := value`; return previous value
47    /// (or 0 if no entry existed).
48    fn gas_meter_set(&mut self, meter_id: MeterId, value: u64) -> u64;
49
50    // ---- StorageQuota (kernel:storagequota) ----
51
52    fn storage_quota_get(&self, quota_id: QuotaId) -> u64;
53    fn storage_quota_set(&mut self, quota_id: QuotaId, value: u64) -> u64;
54
55    // ---- YieldCatcher (kernel:yieldcatcher) ----
56
57    /// Read the marker list for a YieldCatcher instance identified by
58    /// `catcher_hash`. Order matters: routing walks the list and takes
59    /// the first match.
60    fn yield_catcher_markers(&self, catcher_hash: CapHash) -> Vec<CapHash>;
61
62    /// Add a marker template to the catcher's list.
63    fn yield_catcher_add(&mut self, catcher_hash: CapHash, marker_instance_hash: CapHash);
64
65    /// Remove a marker template. No-op if absent.
66    fn yield_catcher_remove(&mut self, catcher_hash: CapHash, marker_instance_hash: CapHash);
67
68    /// Mint a fresh empty YieldCatcher. Returns its content hash
69    /// (which the caller stores as a Cap::Instance\[YieldCatcher\]).
70    fn yield_catcher_new(&mut self) -> CapHash;
71
72    // ---- σ-resident File registry ----
73    //
74    // A v3 "FileCap" is a `Cap::Instance` with the well-known
75    // `KernelImage::File` chain hash. After the cache migration the
76    // file's bytes live in the cache as a `Cap::Data`; this trait
77    // maps `file_id ↔ CapHashOrRef` so host_open/host_save can route
78    // requests to the σ-resident registry without knowing the cap
79    // layout.
80
81    /// Materialize a σ-resident file as a cache reference (typically
82    /// a `CapHashOrRef::Hash` of a published `Cap::Data`). `None` if
83    /// the file_id isn't registered.
84    fn host_open(&mut self, _file_id: u64) -> Option<CapHashOrRef> {
85        None
86    }
87
88    /// Mint a new file from the cache reference `data` after debiting
89    /// `quota_id` by the resolved DataCap size. Returns the new
90    /// `file_id`. Default returns None (no file registry).
91    fn host_save(&mut self, _data: CapHashOrRef, _quota_id: u64, _size: u64) -> Option<u64> {
92        None
93    }
94}
95
96/// In-process, in-memory `KernelAssist` impl. State lives in plain
97/// `HashMap`s. Used by Stage 3's tests and as a runnable default
98/// before `jar-kernel-v3` lands its σ-backed implementation.
99///
100/// Per spec §22, kernel-internal state is reset per block (the kernel
101/// is stateless across blocks). This impl persists across `Vm`
102/// invocations; the chain orchestrator that owns the `Vm` is
103/// responsible for the block-boundary reset (see `reset_block_state`).
104pub struct InProcessKernelAssist {
105    gas_meters: HashMap<MeterId, u64>,
106    storage_quotas: HashMap<QuotaId, u64>,
107    yield_catchers: HashMap<CapHash, Vec<CapHash>>,
108    /// Counter for fresh YieldCatcher hashes. Real impl would compute
109    /// `Blake2b256::hash(epoch || nonce)` or similar; here we use a
110    /// trivial monotonic counter (test-only).
111    next_yc_nonce: u64,
112    /// σ-style file registry (file_id → cache reference). `host_open`
113    /// reads through this; `host_save` mints monotonic file_ids.
114    files: HashMap<u64, CapHashOrRef>,
115    next_file_id: u64,
116}
117
118impl InProcessKernelAssist {
119    pub fn new() -> Self {
120        Self {
121            gas_meters: HashMap::new(),
122            storage_quotas: HashMap::new(),
123            yield_catchers: HashMap::new(),
124            next_yc_nonce: 0,
125            files: HashMap::new(),
126            next_file_id: 1,
127        }
128    }
129
130    /// Reset all kernel-assisted state. The chain orchestrator calls
131    /// this at block end per the v3 design (kernel is stateless across
132    /// blocks).
133    pub fn reset_block_state(&mut self) {
134        self.gas_meters.clear();
135        self.storage_quotas.clear();
136        self.yield_catchers.clear();
137        // Note: next_yc_nonce intentionally not reset — same-process
138        // fresh catchers stay distinct even after block reset to
139        // simplify test diagnostics.
140    }
141
142    /// Register a FileId → cache reference mapping. `host_open` of the
143    /// file_id returns the cache reference. Useful when seeding fixtures.
144    pub fn register_file(&mut self, file_id: u64, data: CapHashOrRef) {
145        self.files.insert(file_id, data);
146        if file_id >= self.next_file_id {
147            self.next_file_id = file_id + 1;
148        }
149    }
150}
151
152impl Default for InProcessKernelAssist {
153    fn default() -> Self {
154        Self::new()
155    }
156}
157
158impl fmt::Debug for InProcessKernelAssist {
159    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
160        f.debug_struct("InProcessKernelAssist")
161            .field("gas_meters", &self.gas_meters.len())
162            .field("storage_quotas", &self.storage_quotas.len())
163            .field("yield_catchers", &self.yield_catchers.len())
164            .field("files", &self.files.len())
165            .finish()
166    }
167}
168
169impl KernelAssist for InProcessKernelAssist {
170    fn gas_meter_get(&self, meter_id: MeterId) -> u64 {
171        self.gas_meters.get(&meter_id).copied().unwrap_or(0)
172    }
173
174    fn gas_meter_set(&mut self, meter_id: MeterId, value: u64) -> u64 {
175        self.gas_meters.insert(meter_id, value).unwrap_or(0)
176    }
177
178    fn storage_quota_get(&self, quota_id: QuotaId) -> u64 {
179        self.storage_quotas.get(&quota_id).copied().unwrap_or(0)
180    }
181
182    fn storage_quota_set(&mut self, quota_id: QuotaId, value: u64) -> u64 {
183        self.storage_quotas.insert(quota_id, value).unwrap_or(0)
184    }
185
186    fn yield_catcher_markers(&self, catcher_hash: CapHash) -> Vec<CapHash> {
187        self.yield_catchers
188            .get(&catcher_hash)
189            .cloned()
190            .unwrap_or_default()
191    }
192
193    fn yield_catcher_add(&mut self, catcher_hash: CapHash, marker_instance_hash: CapHash) {
194        let entry = self.yield_catchers.entry(catcher_hash).or_default();
195        if !entry.contains(&marker_instance_hash) {
196            entry.push(marker_instance_hash);
197        }
198    }
199
200    fn yield_catcher_remove(&mut self, catcher_hash: CapHash, marker_instance_hash: CapHash) {
201        if let Some(entry) = self.yield_catchers.get_mut(&catcher_hash) {
202            entry.retain(|m| *m != marker_instance_hash);
203        }
204    }
205
206    fn yield_catcher_new(&mut self) -> CapHash {
207        let nonce = self.next_yc_nonce;
208        self.next_yc_nonce += 1;
209        // Synthesize a fresh content hash by hashing the nonce; real
210        // impl would derive from the chain context.
211        let hash = Blake2b256::hash(&nonce.to_le_bytes());
212        self.yield_catchers.insert(hash, Vec::new());
213        hash
214    }
215
216    fn host_open(&mut self, file_id: u64) -> Option<CapHashOrRef> {
217        self.files.get(&file_id).cloned()
218    }
219
220    fn host_save(&mut self, data: CapHashOrRef, quota_id: u64, size: u64) -> Option<u64> {
221        let q = self.storage_quotas.get(&quota_id).copied().unwrap_or(0);
222        if q < size {
223            return None;
224        }
225        self.storage_quotas.insert(quota_id, q - size);
226        let id = self.next_file_id;
227        self.next_file_id += 1;
228        self.files.insert(id, data);
229        Some(id)
230    }
231}
232
233// ---------------------------------------------------------------------
234// Kernel image registry
235// ---------------------------------------------------------------------
236
237/// Identifies which kernel-assisted Image a given image_hash chain
238/// refers to. Recognized by the registry's content-hash lookup.
239///
240/// The well-known image_hash values are placeholders for Stage 3 —
241/// `Blake2b256::hash(b"kernel:name")`. Stage 4 jar-kernel-v3 will
242/// finalize the canonical encoding when it actually constructs the
243/// kernel-known `Image` values at chain genesis.
244#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
245pub enum KernelImage {
246    GasMeter,
247    StorageQuota,
248    YieldCatcher,
249    /// Per-Instance Gas{meter_id} unit handle. State: `meter_id: u64`.
250    Gas,
251    /// Per-Instance Quota{quota_id} unit handle.
252    Quota,
253    SetGasMeter,
254    SetStorageQuota,
255    MintGas,
256    MintQuota,
257    CreateYieldCatcher,
258    OogMarker,
259    StorageExhaustedMarker,
260    /// Per-Instance File{file_id} handle: stable σ-resident reference
261    /// produced by host_save / consumed by host_open. State:
262    /// `file_id: u64`.
263    File,
264    /// Per-Instance HostOpen handle: kernel-issued cap that resolves
265    /// to the `host_open` host call dispatch.
266    HostOpen,
267    /// Per-Instance HostSave handle: symmetric counterpart.
268    HostSave,
269}
270
271/// Compute the placeholder image_hash for a kernel-assisted Image.
272/// Stage 4 will replace these with the real chain-genesis-derived
273/// hashes; for now they're stable strings hashed via Blake2b256 so the
274/// Vm has a concrete value to match against.
275const fn const_kernel_image_label(kind: KernelImage) -> &'static [u8] {
276    match kind {
277        KernelImage::GasMeter => b"kernel:gasmeter",
278        KernelImage::StorageQuota => b"kernel:storagequota",
279        KernelImage::YieldCatcher => b"kernel:yieldcatcher",
280        KernelImage::Gas => b"kernel:gas",
281        KernelImage::Quota => b"kernel:quota",
282        KernelImage::SetGasMeter => b"kernel:set_gas_meter",
283        KernelImage::SetStorageQuota => b"kernel:set_storage_quota",
284        KernelImage::MintGas => b"kernel:mint_gas",
285        KernelImage::MintQuota => b"kernel:mint_quota",
286        KernelImage::CreateYieldCatcher => b"kernel:create_yield_catcher",
287        KernelImage::OogMarker => b"kernel:oog_marker",
288        KernelImage::StorageExhaustedMarker => b"kernel:storage_exhausted_marker",
289        KernelImage::File => b"kernel:file",
290        KernelImage::HostOpen => b"kernel:host_open",
291        KernelImage::HostSave => b"kernel:host_save",
292    }
293}
294
295/// Compute the well-known image_hash for a kernel-assisted Image.
296pub fn kernel_image_hash(kind: KernelImage) -> CapHash {
297    Blake2b256::hash(const_kernel_image_label(kind))
298}
299
300/// Look up a known kernel-assisted Image by its image_hash chain.
301/// Returns `None` for a hash that doesn't match any kernel-known
302/// Image (the common case — user Images are not kernel-assisted).
303pub fn recognize_kernel_image(hash: CapHash) -> Option<KernelImage> {
304    // Linear scan is fine: the registry has ~15 entries, lookup is
305    // not on the hot path (only at Instance-entry / yield-route).
306    [
307        KernelImage::GasMeter,
308        KernelImage::StorageQuota,
309        KernelImage::YieldCatcher,
310        KernelImage::Gas,
311        KernelImage::Quota,
312        KernelImage::SetGasMeter,
313        KernelImage::SetStorageQuota,
314        KernelImage::MintGas,
315        KernelImage::MintQuota,
316        KernelImage::CreateYieldCatcher,
317        KernelImage::OogMarker,
318        KernelImage::StorageExhaustedMarker,
319        KernelImage::File,
320        KernelImage::HostOpen,
321        KernelImage::HostSave,
322    ]
323    .into_iter()
324    .find(|kind| kernel_image_hash(*kind) == hash)
325}
326
327#[cfg(test)]
328mod tests {
329    use super::*;
330
331    #[test]
332    fn gas_meter_get_missing_returns_zero() {
333        let k = InProcessKernelAssist::new();
334        assert_eq!(k.gas_meter_get(42), 0);
335    }
336
337    #[test]
338    fn gas_meter_set_returns_previous() {
339        let mut k = InProcessKernelAssist::new();
340        assert_eq!(k.gas_meter_set(1, 100), 0);
341        assert_eq!(k.gas_meter_set(1, 200), 100);
342        assert_eq!(k.gas_meter_get(1), 200);
343    }
344
345    #[test]
346    fn storage_quota_round_trip() {
347        let mut k = InProcessKernelAssist::new();
348        assert_eq!(k.storage_quota_set(7, 1024), 0);
349        assert_eq!(k.storage_quota_get(7), 1024);
350        assert_eq!(k.storage_quota_set(7, 2048), 1024);
351    }
352
353    #[test]
354    fn yield_catcher_add_and_read_markers() {
355        let mut k = InProcessKernelAssist::new();
356        let yc = k.yield_catcher_new();
357        let m1 = [1u8; 32];
358        let m2 = [2u8; 32];
359        k.yield_catcher_add(yc, m1);
360        k.yield_catcher_add(yc, m2);
361        assert_eq!(k.yield_catcher_markers(yc), vec![m1, m2]);
362    }
363
364    #[test]
365    fn yield_catcher_add_is_set_semantics() {
366        let mut k = InProcessKernelAssist::new();
367        let yc = k.yield_catcher_new();
368        let m = [9u8; 32];
369        k.yield_catcher_add(yc, m);
370        k.yield_catcher_add(yc, m); // duplicate
371        assert_eq!(k.yield_catcher_markers(yc).len(), 1);
372    }
373
374    #[test]
375    fn yield_catcher_remove() {
376        let mut k = InProcessKernelAssist::new();
377        let yc = k.yield_catcher_new();
378        let m = [3u8; 32];
379        k.yield_catcher_add(yc, m);
380        k.yield_catcher_remove(yc, m);
381        assert!(k.yield_catcher_markers(yc).is_empty());
382        // Removing absent is a no-op.
383        k.yield_catcher_remove(yc, [9u8; 32]);
384    }
385
386    #[test]
387    fn yield_catcher_new_returns_distinct_hashes() {
388        let mut k = InProcessKernelAssist::new();
389        let a = k.yield_catcher_new();
390        let b = k.yield_catcher_new();
391        assert_ne!(a, b);
392    }
393
394    #[test]
395    fn reset_block_state_clears_meters_and_catchers() {
396        let mut k = InProcessKernelAssist::new();
397        k.gas_meter_set(1, 100);
398        k.storage_quota_set(2, 200);
399        let yc = k.yield_catcher_new();
400        k.yield_catcher_add(yc, [4u8; 32]);
401
402        k.reset_block_state();
403
404        assert_eq!(k.gas_meter_get(1), 0);
405        assert_eq!(k.storage_quota_get(2), 0);
406        assert!(k.yield_catcher_markers(yc).is_empty());
407    }
408
409    #[test]
410    fn kernel_image_hash_is_deterministic() {
411        assert_eq!(
412            kernel_image_hash(KernelImage::GasMeter),
413            kernel_image_hash(KernelImage::GasMeter)
414        );
415    }
416
417    #[test]
418    fn kernel_images_have_distinct_hashes() {
419        let all = [
420            KernelImage::GasMeter,
421            KernelImage::StorageQuota,
422            KernelImage::YieldCatcher,
423            KernelImage::Gas,
424            KernelImage::Quota,
425            KernelImage::SetGasMeter,
426            KernelImage::SetStorageQuota,
427            KernelImage::MintGas,
428            KernelImage::MintQuota,
429            KernelImage::CreateYieldCatcher,
430            KernelImage::OogMarker,
431            KernelImage::StorageExhaustedMarker,
432            KernelImage::File,
433            KernelImage::HostOpen,
434            KernelImage::HostSave,
435        ];
436        for (i, a) in all.iter().enumerate() {
437            for b in &all[i + 1..] {
438                assert_ne!(
439                    kernel_image_hash(*a),
440                    kernel_image_hash(*b),
441                    "{:?} vs {:?}",
442                    a,
443                    b
444                );
445            }
446        }
447    }
448
449    #[test]
450    fn recognize_known_image() {
451        let h = kernel_image_hash(KernelImage::OogMarker);
452        assert_eq!(recognize_kernel_image(h), Some(KernelImage::OogMarker));
453    }
454
455    #[test]
456    fn recognize_unknown_image_is_none() {
457        let h = Blake2b256::hash(b"user:my-cool-image");
458        assert_eq!(recognize_kernel_image(h), None);
459    }
460}