Skip to main content

javm_cap/
image.rs

1//! Image: the smallest unit of program specification.
2//!
3//! An `Image` is content-addressed (its `image_id` is the hash of
4//! its serialized content). An Instance's `image_hash` is the
5//! cumulative chain hash tracking the lineage of `set_image` /
6//! `host_derive_spawn` extensions from genesis.
7//!
8//! ```text
9//! genesis (host_derive_spawn from no source):
10//!     image_hash = hash(image)
11//!
12//! after set_image(new):
13//!     image_hash = hash(prev_chain || hash(new))
14//!
15//! after host_derive_spawn(new, cnode) by a spawner:
16//!     spawned.image_hash = hash(spawner.image_hash || hash(new))
17//!
18//! after MGMT_COPY of a Cap::Instance:
19//!     copy.image_hash = source.image_hash   (preserved)
20//! ```
21//!
22//! This module provides the pure data structures + the chain-hash
23//! computations. Image *content hashing* is done by serializing the
24//! Image canonically and feeding the bytes to `H::hash`; we provide
25//! a simple deterministic encoder here so the v3 implementation
26//! has one canonical form.
27
28use crate::hash::Hash;
29use crate::slot::SlotIdx;
30use alloc::collections::BTreeMap;
31use alloc::vec::Vec;
32use ssz_derive::{Decode, Encode};
33
34/// Image: the program spec (code, endpoints, memory layout, slot
35/// declarations, pinned ro caps).
36///
37/// `pinned_slots` and `yield_marker_slot` reference cnode slots; the
38/// kernel installs declared pinned content into the Instance's cnode
39/// at `set_image` / `host_derive_spawn` time and treats them as
40/// read-only thereafter.
41#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, ssz_derive::HashTreeRoot)]
42pub struct Image {
43    /// Bytecode bytes (validated at construction; see `host_make_image`).
44    pub code: Vec<u8>,
45    /// Packed bitmask, one bit per `code` byte, LSB-first.
46    /// `packed_bitmask.len() == code.len().div_ceil(8)`. A `1` bit
47    /// marks the start of an instruction; a `0` bit marks a
48    /// continuation byte. Use `javm_exec::unpack_bitmask` to
49    /// recover the unpacked form at decode time.
50    pub packed_bitmask: Vec<u8>,
51    /// Jump-table entries (PVM PCs into `code`). Indexed by
52    /// `djump` immediates.
53    pub jump_table: Vec<u32>,
54    /// Endpoints addressable by `endpoint_idx` (u8). Sparse — only
55    /// declared endpoints are present.
56    pub endpoints: BTreeMap<u8, EndpointDef>,
57    /// Memory layout. Each entry maps a `Cap::Data` (resolved
58    /// through `source`) into the address space at `[start, start
59    /// + size)`. Permissions are derived from whether the target
60    /// slot appears in `pinned_slots` (RO) or not (RW).
61    pub memory_mappings: Vec<MemoryMapping>,
62    /// Cnode slots holding `Cap::Instance[Gas{meter_id}]`. Active
63    /// gas debit comes from the first slot's meter; the rest are
64    /// fallback reserves (chain-spec policy).
65    pub gas_slots: Vec<SlotIdx>,
66    /// Cnode slots holding `Cap::Instance[Quota{quota_id}]`.
67    /// Symmetric with `gas_slots`.
68    pub quota_slots: Vec<SlotIdx>,
69    /// Pinned read-only caps (Cap::Data or Cap::Image) baked into
70    /// the spec. The kernel rejects mutations to these slots.
71    pub pinned_slots: BTreeMap<SlotIdx, PinnedCap>,
72    /// Initial cnode state for non-pinned mutable slots. Only
73    /// honored at standalone (root) Instance bootstrap — a
74    /// parented Instance receives its cnode from the spawner.
75    pub initial_slots: BTreeMap<SlotIdx, InitialDataCap>,
76    /// Slot holding `Cap::Instance[YieldCatcher]`, if this Instance
77    /// catches yields. None = no catcher.
78    pub yield_marker_slot: Option<SlotIdx>,
79}
80
81/// Endpoint definition: entry PC + register conventions.
82#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, ssz_derive::HashTreeRoot)]
83pub struct EndpointDef {
84    /// Bytecode address to jump to.
85    pub entry_pc: u64,
86    /// Number of register args supplied by the caller (0..=4
87    /// per spec convention; we store as u8 for flexibility).
88    pub arg_registers: u8,
89    /// Size of the arg cnode the caller may attach.
90    pub arg_cnode_size: u8,
91    /// PVM registers to seed before entering the endpoint. Keyed
92    /// by register index (0..=12). Common usage: φ\[1\] (RISC-V SP)
93    /// ← `stack_top`. The kernel applies these on top of the
94    /// calling-convention defaults (φ\[11\] = endpoint_idx).
95    pub initial_regs: BTreeMap<u8, u64>,
96}
97
98/// One mapped region. The kernel resolves `source` at instance
99/// start, reads the bytes from the resulting `Cap::Data`, and lays
100/// them at `[start, start + size)` in the address space. Whether
101/// the region is RO or RW is derived from whether `source.target()`
102/// is in `Image.pinned_slots`.
103#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, ssz_derive::HashTreeRoot)]
104pub struct MemoryMapping {
105    pub start: u64,
106    pub size: u64,
107    pub source: crate::slot::SlotPath,
108}
109
110/// Pinned slot content. Only content-addressed cap kinds can be
111/// pinned (Data or Image). `Cap::Data` bytes are inlined in the
112/// Image; a future optimisation can add a hash-only variant for
113/// content that lives in σ.data_payloads.
114#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, ssz_derive::HashTreeRoot)]
115pub enum PinnedCap {
116    #[ssz(selector = 0)]
117    /// Pinned `Cap::Data` with bytes baked into the Image. `size`
118    /// may be larger than `content.len()`; trailing bytes are
119    /// zero-filled per the DataCap canonical form.
120    Data { content: Vec<u8>, size: u64 },
121    #[ssz(selector = 1)]
122    /// Pinned `Cap::Image` by content hash. Cap::Image is itself
123    /// content-addressed; inlining a whole sub-Image makes less
124    /// sense than for Data.
125    Image { content_hash: [u8; 32] },
126}
127
128/// Initial `Cap::Data` content for a non-pinned mutable slot. Used
129/// at standalone (root) Instance bootstrap to seed the cnode. A
130/// parented Instance receives its slots from the spawner and
131/// ignores this field.
132#[derive(Debug, Clone, Default, PartialEq, Eq, Encode, Decode, ssz_derive::HashTreeRoot)]
133pub struct InitialDataCap {
134    /// Initial bytes. May be empty for zero-filled regions like
135    /// stack and heap.
136    pub content: Vec<u8>,
137    /// Logical size of the cap. `size` may be larger than
138    /// `content.len()`; trailing bytes are zero-filled when the
139    /// cap is mapped.
140    pub size: u64,
141}
142
143impl Image {
144    /// Empty Image: no code, no endpoints, no mappings, no slots.
145    /// Useful for tests and as a starting point.
146    pub fn empty() -> Self {
147        Self {
148            code: Vec::new(),
149            packed_bitmask: Vec::new(),
150            jump_table: Vec::new(),
151            endpoints: BTreeMap::new(),
152            memory_mappings: Vec::new(),
153            gas_slots: Vec::new(),
154            quota_slots: Vec::new(),
155            pinned_slots: BTreeMap::new(),
156            initial_slots: BTreeMap::new(),
157            yield_marker_slot: None,
158        }
159    }
160}
161
162/// Content hash of an Image: SSZ `hash_tree_root` (SHA-256 merkleization
163/// of the derived SSZ container). The canonical encoding/merkleization is
164/// defined by `Image`'s `ssz-derive` impl.
165pub fn image_content_hash(image: &Image) -> [u8; 32] {
166    ssz::hash_tree_root(image)
167}
168
169/// Genesis image-hash chain: a freshly-derived Instance (with no
170/// prior chain) has `image_hash = image_content_hash`.
171///
172/// This is the case for the very first Instance the chain spec
173/// produces. Subsequent Instances always derive from some spawner
174/// via `chain_extend`.
175pub fn chain_genesis<H: Hash>(image: &Image) -> H::Out
176where
177    H::Out: From<[u8; 32]>,
178{
179    image_content_hash(image).into()
180}
181
182/// Extend an image-hash chain with a new image:
183/// `result = H(prev_chain || image_content_hash(new_image))`.
184///
185/// Used for both `set_image(new)` on an existing Instance and
186/// `host_derive_spawn(new, cnode)` from a spawner.
187pub fn chain_extend<H: Hash>(prev_chain: &H::Out, new_image: &Image) -> H::Out
188where
189    H::Out: AsRef<[u8]>,
190{
191    let new_image_hash = image_content_hash(new_image);
192    H::hash_pair(prev_chain.as_ref(), &new_image_hash)
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198    use crate::hash::Blake2b256;
199    use ssz::Decode as _;
200
201    type H = Blake2b256;
202
203    #[test]
204    fn empty_image_hashes_deterministically() {
205        let img = Image::empty();
206        let h1 = image_content_hash(&img);
207        let h2 = image_content_hash(&img);
208        assert_eq!(h1, h2);
209    }
210
211    #[test]
212    fn image_ssz_roundtrip() {
213        let mut img = Image::empty();
214        img.code = b"sample code".to_vec();
215        img.packed_bitmask = vec![0xFF, 0x07]; // 11 bits set, all-1s
216        img.jump_table = vec![0u32, 4, 8];
217        img.endpoints.insert(
218            0,
219            EndpointDef {
220                entry_pc: 0x100,
221                arg_registers: 1,
222                arg_cnode_size: 0,
223                initial_regs: BTreeMap::new(),
224            },
225        );
226        let mut initial_regs = BTreeMap::new();
227        initial_regs.insert(1u8, 0x4000);
228        img.endpoints.insert(
229            255,
230            EndpointDef {
231                entry_pc: 0xDEADBEEF,
232                arg_registers: 4,
233                arg_cnode_size: 8,
234                initial_regs,
235            },
236        );
237        img.memory_mappings.push(MemoryMapping {
238            start: 0x1000,
239            size: 0x4000,
240            source: crate::slot::SlotPath::root(SlotIdx(65)),
241        });
242        img.memory_mappings.push(MemoryMapping {
243            start: 0x5000,
244            size: 0x2000,
245            source: crate::slot::SlotPath::root(SlotIdx(3)),
246        });
247        img.gas_slots = vec![SlotIdx(7)];
248        img.quota_slots = vec![SlotIdx(8)];
249        img.pinned_slots.insert(
250            SlotIdx(11),
251            PinnedCap::Data {
252                content: vec![0xAB; 1024],
253                size: 4096,
254            },
255        );
256        img.initial_slots.insert(
257            SlotIdx(65),
258            InitialDataCap {
259                content: Vec::new(),
260                size: 0x4000,
261            },
262        );
263        img.yield_marker_slot = Some(SlotIdx(9));
264
265        let bytes = ssz::Encode::as_ssz_bytes(&img);
266        let decoded = Image::from_ssz_bytes(&bytes).expect("decode");
267        assert_eq!(decoded, img);
268    }
269
270    #[test]
271    fn different_code_different_hash() {
272        let mut a = Image::empty();
273        a.code = b"AAAA".to_vec();
274        let mut b = Image::empty();
275        b.code = b"BBBB".to_vec();
276        assert_ne!(image_content_hash(&a), image_content_hash(&b));
277    }
278
279    #[test]
280    fn endpoints_affect_hash() {
281        let a = Image::empty();
282        let mut b = Image::empty();
283        b.endpoints.insert(
284            7,
285            EndpointDef {
286                entry_pc: 0x1000,
287                arg_registers: 2,
288                arg_cnode_size: 0,
289                initial_regs: BTreeMap::new(),
290            },
291        );
292        assert_ne!(image_content_hash(&a), image_content_hash(&b));
293    }
294
295    #[test]
296    fn pinned_slots_order_independent() {
297        // BTreeMap iteration is deterministic; insertion order
298        // shouldn't matter for the resulting hash.
299        let mut a = Image::empty();
300        a.pinned_slots.insert(
301            SlotIdx(3),
302            PinnedCap::Data {
303                content: vec![0xAA; 100],
304                size: 100,
305            },
306        );
307        a.pinned_slots.insert(
308            SlotIdx(7),
309            PinnedCap::Data {
310                content: vec![0xBB; 200],
311                size: 200,
312            },
313        );
314
315        let mut b = Image::empty();
316        // Different insertion order.
317        b.pinned_slots.insert(
318            SlotIdx(7),
319            PinnedCap::Data {
320                content: vec![0xBB; 200],
321                size: 200,
322            },
323        );
324        b.pinned_slots.insert(
325            SlotIdx(3),
326            PinnedCap::Data {
327                content: vec![0xAA; 100],
328                size: 100,
329            },
330        );
331
332        assert_eq!(image_content_hash(&a), image_content_hash(&b));
333    }
334
335    #[test]
336    fn chain_genesis_equals_content_hash() {
337        let img = Image::empty();
338        assert_eq!(chain_genesis::<H>(&img), image_content_hash(&img));
339    }
340
341    #[test]
342    fn chain_extend_changes_with_new_image() {
343        let img_a = Image::empty();
344        let mut img_b = Image::empty();
345        img_b.code = b"B".to_vec();
346        let prev = chain_genesis::<H>(&img_a);
347        let extended_b = chain_extend::<H>(&prev, &img_b);
348        let mut img_c = Image::empty();
349        img_c.code = b"C".to_vec();
350        let extended_c = chain_extend::<H>(&prev, &img_c);
351        assert_ne!(extended_b, extended_c);
352    }
353
354    #[test]
355    fn chain_extend_is_associative_under_sequence() {
356        // Extending twice with [A then B] yields a single deterministic
357        // chain hash. Calling chain_extend twice in different orders
358        // gives different chains (as expected — chain order matters).
359        let img_a = Image::empty();
360        let mut img_b = Image::empty();
361        img_b.code = b"B".to_vec();
362        let mut img_c = Image::empty();
363        img_c.code = b"C".to_vec();
364
365        let chain_abc = {
366            let g = chain_genesis::<H>(&img_a);
367            let g_b = chain_extend::<H>(&g, &img_b);
368            chain_extend::<H>(&g_b, &img_c)
369        };
370        let chain_acb = {
371            let g = chain_genesis::<H>(&img_a);
372            let g_c = chain_extend::<H>(&g, &img_c);
373            chain_extend::<H>(&g_c, &img_b)
374        };
375        // Order matters.
376        assert_ne!(chain_abc, chain_acb);
377
378        // Re-running the same sequence gives the same result.
379        let chain_abc_2 = {
380            let g = chain_genesis::<H>(&img_a);
381            let g_b = chain_extend::<H>(&g, &img_b);
382            chain_extend::<H>(&g_b, &img_c)
383        };
384        assert_eq!(chain_abc, chain_abc_2);
385    }
386
387    #[test]
388    fn mgmt_copy_preserves_chain_hash() {
389        // MGMT_COPY of a Cap::Instance preserves image_hash; this is
390        // a function-level invariant: equality of the same H::Out
391        // value. Just a sanity test that H::Out is Copy and equal.
392        let img = Image::empty();
393        let chain = chain_genesis::<H>(&img);
394        let copy = chain;
395        assert_eq!(chain, copy);
396    }
397}