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}