Skip to main content

javm_cap/
image_cap.rs

1//! `ImageCap` — Image cap.
2//!
3//! Stores code, bitmask, jump_table, endpoints, mappings, and slot
4//! references as separate `Vec<T>` allocations. Allocation count
5//! per ImageCap is bounded (seven Vecs, regardless of content size);
6//! we accept that in exchange for direct field accessors.
7
8use alloc::vec::Vec;
9
10use crate::slot::SlotIdx;
11
12use super::cap::{CapHash, MAX_ENDPOINTS, MAX_SOURCE_DEPTH, NUM_REGS};
13
14#[derive(Clone, Debug, ssz_derive::HashTreeRoot)]
15pub struct ImageCap {
16    /// Bytecode bytes.
17    pub code: Vec<u8>,
18    /// Packed bit-per-byte instruction-start bitmask. Same layout
19    /// as `crate::image::Image::packed_bitmask`.
20    pub bitmask: Vec<u8>,
21    /// Jump-table entries (PVM PCs).
22    pub jump_table: Vec<u32>,
23    /// Endpoint definitions. Stored as a dense array keyed by
24    /// endpoint index — `endpoints[i].entry_pc == 0` means the
25    /// endpoint at index `i` is not defined.
26    pub endpoints: Vec<EndpointDef>,
27    /// Memory mappings.
28    pub mappings: Vec<MemoryMapping>,
29    /// Pinned read-only slots (Cap::Data / Cap::Image). Images only
30    /// ever reference content-addressed caps, so the target is a
31    /// plain `CapHash`.
32    pub pinned: Vec<ImageSlotEntry>,
33    /// Initial mutable slot state for non-pinned slots.
34    pub initial: Vec<ImageSlotEntry>,
35    /// Slot holding `Cap::Instance[YieldCatcher]`, if any.
36    pub yield_marker_slot: Option<SlotIdx>,
37}
38
39/// Endpoint definition. Dense `initial_regs` array; index `i`
40/// corresponds to PVM register `φ[i]`. `0` is "use default" (same
41/// semantics as the spec's old `BTreeMap<u8, u64>` when the key is
42/// absent).
43#[derive(
44    Clone,
45    Copy,
46    Debug,
47    PartialEq,
48    Eq,
49    ssz_derive::Encode,
50    ssz_derive::Decode,
51    ssz_derive::HashTreeRoot,
52)]
53pub struct EndpointDef {
54    pub entry_pc: u64,
55    pub stack_top: u64,
56    pub arg_cnode_slot: SlotIdx,
57    pub arg_cnode_size: u8,
58    pub initial_regs: [u64; NUM_REGS],
59}
60
61impl EndpointDef {
62    /// Empty endpoint — `entry_pc == 0` is the canonical sentinel
63    /// for "not defined" (since a real entry PC is never zero — PC 0
64    /// is reserved as the fallback PC in our convention).
65    pub const fn empty() -> Self {
66        Self {
67            entry_pc: 0,
68            stack_top: 0,
69            arg_cnode_slot: SlotIdx(0),
70            arg_cnode_size: 0,
71            initial_regs: [0; NUM_REGS],
72        }
73    }
74}
75
76/// One mapped region. The kernel resolves `source_path` at instance
77/// start, reads the bytes from the resulting `Cap::Data`, and lays
78/// them at `[start, start + size)`. `source_path` is a fixed-cap
79/// array; `source_path_len` is the actual depth.
80///
81/// **SSZ note**: `Encode`/`Decode`/`HashTreeRoot` are hand-written
82/// because the `source_path` field is `[SlotIdx; MAX_SOURCE_DEPTH]` —
83/// an array of a local type, which Rust's orphan rules block from
84/// receiving a blanket impl in either `ssz` or `javm-cap`. The encoded
85/// form is field-by-field SSZ: `u64 || u64 || (MAX_SOURCE_DEPTH * 4
86/// LE bytes) || u8`. All fields are fixed-length so the container is
87/// fixed-length too.
88#[derive(Clone, Copy, Debug, PartialEq, Eq)]
89pub struct MemoryMapping {
90    pub start: u64,
91    pub size: u64,
92    pub source_path: [SlotIdx; MAX_SOURCE_DEPTH],
93    pub source_path_len: u8,
94}
95
96impl MemoryMapping {
97    /// SSZ fixed encoded length: 8 + 8 + (MAX_SOURCE_DEPTH * 4) + 1.
98    const SSZ_LEN: usize = 8 + 8 + MAX_SOURCE_DEPTH * 4 + 1;
99}
100
101impl ssz::Encode for MemoryMapping {
102    fn is_ssz_fixed_len() -> bool {
103        true
104    }
105    fn ssz_fixed_len() -> usize {
106        Self::SSZ_LEN
107    }
108    fn ssz_bytes_len(&self) -> usize {
109        Self::SSZ_LEN
110    }
111    fn ssz_append(&self, buf: &mut alloc::vec::Vec<u8>) {
112        buf.extend_from_slice(&self.start.to_le_bytes());
113        buf.extend_from_slice(&self.size.to_le_bytes());
114        for s in &self.source_path {
115            buf.extend_from_slice(&s.get().to_le_bytes());
116        }
117        buf.push(self.source_path_len);
118    }
119}
120
121impl ssz::Decode for MemoryMapping {
122    fn is_ssz_fixed_len() -> bool {
123        true
124    }
125    fn ssz_fixed_len() -> usize {
126        Self::SSZ_LEN
127    }
128    fn from_ssz_bytes(bytes: &[u8]) -> Result<Self, ssz::DecodeError> {
129        if bytes.len() != Self::SSZ_LEN {
130            return Err(ssz::DecodeError::UnexpectedEof {
131                expected: Self::SSZ_LEN,
132                actual: bytes.len(),
133            });
134        }
135        let start = u64::from_le_bytes(bytes[0..8].try_into().expect("len checked"));
136        let size = u64::from_le_bytes(bytes[8..16].try_into().expect("len checked"));
137        let mut source_path = [SlotIdx(0); MAX_SOURCE_DEPTH];
138        for (i, slot) in source_path.iter_mut().enumerate() {
139            let s = 16 + i * 4;
140            let arr: [u8; 4] = bytes[s..s + 4].try_into().expect("len checked");
141            *slot = SlotIdx(u32::from_le_bytes(arr));
142        }
143        let source_path_len = bytes[16 + MAX_SOURCE_DEPTH * 4];
144        Ok(Self {
145            start,
146            size,
147            source_path,
148            source_path_len,
149        })
150    }
151}
152
153impl ssz::HashTreeRoot for MemoryMapping {
154    fn hash_tree_root<D: ::ssz::digest::Digest<OutputSize = ::ssz::digest::typenum::U32>>(
155        &self,
156    ) -> [u8; 32] {
157        // SSZ container root: merkleize the per-field roots with
158        // limit = number of fields (4). All four are fixed-size leaves.
159        let path_root = {
160            // Treat the fixed-length path array as a `Vector<u32,
161            // MAX_SOURCE_DEPTH>` for hashing: pack to bytes, merkleize
162            // with `ceil(N*4/32)` chunks.
163            let mut buf: alloc::vec::Vec<u8> = alloc::vec::Vec::with_capacity(MAX_SOURCE_DEPTH * 4);
164            for s in &self.source_path {
165                buf.extend_from_slice(&s.get().to_le_bytes());
166            }
167            let chunks = ssz::pack_bytes(&buf);
168            let limit = (MAX_SOURCE_DEPTH * 4).div_ceil(32).max(1);
169            ssz::merkleize::<D>(&chunks, limit)
170        };
171        let roots = [
172            ssz::HashTreeRoot::hash_tree_root::<D>(&self.start),
173            ssz::HashTreeRoot::hash_tree_root::<D>(&self.size),
174            path_root,
175            ssz::HashTreeRoot::hash_tree_root::<D>(&self.source_path_len),
176        ];
177        ssz::merkleize::<D>(&roots, 4)
178    }
179}
180
181impl MemoryMapping {
182    /// Live slot indices (length = `source_path_len`).
183    pub fn path(&self) -> &[SlotIdx] {
184        &self.source_path[..self.source_path_len as usize]
185    }
186}
187
188/// `(slot_idx, cap_hash)` pair used by Image's `pinned` and
189/// `initial` arrays. References content-addressed caps only.
190#[derive(
191    Clone,
192    Copy,
193    Debug,
194    PartialEq,
195    Eq,
196    ssz_derive::Encode,
197    ssz_derive::Decode,
198    ssz_derive::HashTreeRoot,
199)]
200pub struct ImageSlotEntry {
201    pub slot: SlotIdx,
202    pub cap_hash: CapHash,
203}
204
205/// Failure modes when converting a SCALE-encoded [`crate::image::Image`]
206/// into an [`ImageCap`]. The conversion is lossy in fields the v3 cap
207/// shape no longer carries (`gas_slots`, `quota_slots`, per-endpoint
208/// `arg_registers`) and constrained in others — these errors flag the
209/// constraint violations.
210#[derive(Debug, thiserror::Error)]
211pub enum ImageConvertError {
212    #[error("memory mapping source path empty")]
213    SourcePathEmpty,
214    #[error("memory mapping source path too deep (steps={0} > MAX_SOURCE_DEPTH)")]
215    SourcePathTooDeep(usize),
216    #[error("endpoint index {0} >= MAX_ENDPOINTS")]
217    EndpointIndexOutOfRange(u8),
218    #[error("register index {0} >= NUM_REGS")]
219    RegisterIndexOutOfRange(u8),
220}
221
222/// Build an [`ImageCap`] from the SCALE-encoded [`crate::image::Image`]
223/// shape. The Data content referenced by pinned and initial slots must
224/// already be published — pass the resolved `(SlotIdx, CapHash)` pairs
225/// in `pinned_hashes` and `initial_hashes`. The builder sorts both lists
226/// by slot index.
227///
228/// **Lossy fields (intentionally dropped):**
229/// - `gas_slots` / `quota_slots`: gas is now tracked on
230///   [`super::instance::InstanceCap::gas_remaining`]; the v3 cap shape
231///   no longer pins gas/quota slots in the Image.
232/// - per-endpoint `arg_registers`: the calling convention is implicit
233///   in the new shape.
234///
235/// **Field mappings:**
236/// - Endpoints are stored in a dense `MAX_ENDPOINTS`-sized array,
237///   indexed by endpoint id. Empty slots use [`EndpointDef::empty`].
238///   `stack_top` is extracted from the old `initial_regs[1]` (RISC-V
239///   SP convention); `arg_cnode_slot` defaults to `SlotIdx(0)`.
240/// - `MemoryMapping.source: SlotPath` becomes `source_path: [SlotIdx;
241///   MAX_SOURCE_DEPTH] + source_path_len`; paths deeper than 8 error.
242pub fn image_cap(
243    image: &crate::image::Image,
244    pinned_hashes: &[(SlotIdx, CapHash)],
245    initial_hashes: &[(SlotIdx, CapHash)],
246) -> Result<ImageCap, ImageConvertError> {
247    let mut code = Vec::with_capacity(image.code.len());
248    code.extend_from_slice(&image.code);
249
250    let mut bitmask = Vec::with_capacity(image.packed_bitmask.len());
251    bitmask.extend_from_slice(&image.packed_bitmask);
252
253    let mut jump_table = Vec::with_capacity(image.jump_table.len());
254    for &j in &image.jump_table {
255        jump_table.push(j);
256    }
257
258    // Endpoints: dense `MAX_ENDPOINTS`-sized array; empty entries have
259    // `entry_pc == 0`.
260    let mut endpoints = Vec::with_capacity(MAX_ENDPOINTS);
261    for _ in 0..MAX_ENDPOINTS {
262        endpoints.push(EndpointDef::empty());
263    }
264    for (&idx, ep) in &image.endpoints {
265        if (idx as usize) >= MAX_ENDPOINTS {
266            return Err(ImageConvertError::EndpointIndexOutOfRange(idx));
267        }
268        let mut initial_regs = [0u64; NUM_REGS];
269        for (&reg_idx, &val) in &ep.initial_regs {
270            if (reg_idx as usize) >= NUM_REGS {
271                return Err(ImageConvertError::RegisterIndexOutOfRange(reg_idx));
272            }
273            initial_regs[reg_idx as usize] = val;
274        }
275        // RISC-V SP convention: φ[1] = stack pointer.
276        let stack_top = ep.initial_regs.get(&1).copied().unwrap_or(0);
277        endpoints[idx as usize] = EndpointDef {
278            entry_pc: ep.entry_pc,
279            stack_top,
280            arg_cnode_slot: SlotIdx(0),
281            arg_cnode_size: ep.arg_cnode_size,
282            initial_regs,
283        };
284    }
285
286    let mut mappings = Vec::with_capacity(image.memory_mappings.len());
287    for m in &image.memory_mappings {
288        let steps = &m.source.steps;
289        if steps.is_empty() {
290            return Err(ImageConvertError::SourcePathEmpty);
291        }
292        if steps.len() > MAX_SOURCE_DEPTH {
293            return Err(ImageConvertError::SourcePathTooDeep(steps.len()));
294        }
295        let mut source_path = [SlotIdx(0); MAX_SOURCE_DEPTH];
296        for (i, s) in steps.iter().enumerate() {
297            source_path[i] = *s;
298        }
299        mappings.push(MemoryMapping {
300            start: m.start,
301            size: m.size,
302            source_path,
303            source_path_len: steps.len() as u8,
304        });
305    }
306
307    let pinned = build_image_slot_vec(pinned_hashes);
308    let initial = build_image_slot_vec(initial_hashes);
309
310    Ok(ImageCap {
311        code,
312        bitmask,
313        jump_table,
314        endpoints,
315        mappings,
316        pinned,
317        initial,
318        yield_marker_slot: image.yield_marker_slot,
319    })
320}
321
322fn build_image_slot_vec(pairs: &[(SlotIdx, CapHash)]) -> Vec<ImageSlotEntry> {
323    let mut sorted: Vec<(SlotIdx, CapHash)> = pairs.to_vec();
324    sorted.sort_by_key(|(s, _)| *s);
325    let mut out = Vec::with_capacity(sorted.len());
326    for (slot, cap_hash) in &sorted {
327        out.push(ImageSlotEntry {
328            slot: *slot,
329            cap_hash: *cap_hash,
330        });
331    }
332    out
333}