javm_cap/page.rs
1//! `PageSlot` and `PageRef` — DataCap page storage.
2//!
3//! Each page is owned by the DataCap that holds it. Sharing across
4//! DataCap CoW clones is done via [`PageRef`], a refcounted handle
5//! over [`PageBytes`] backed by the global allocator. The cache
6//! subsystem doesn't index pages by hash — pages aren't first-class
7//! caps. They're internal to the DataCap layer.
8
9use alloc::sync::Arc;
10use alloc::vec::Vec;
11
12use super::cap::CapHash;
13
14/// Sparse representation of a paged DataCap's pages. `Empty` is the
15/// canonical zero page; `Loaded` holds a refcounted byte slab;
16/// `Missing` records the page's content hash so a host callback can
17/// later resolve it (V1: never observed — we always pre-publish).
18#[derive(Clone, Debug)]
19pub enum PageSlot {
20 Empty,
21 Loaded(PageRef),
22 Missing(CapHash),
23}
24
25/// Refcounted handle to a [`PageBytes`] allocated by the global
26/// allocator. Plain `std::sync::Arc` alias for cap-layer readability.
27pub type PageRef = Arc<PageBytes>;
28
29/// One page's bytes plus its precomputed content hash.
30///
31/// Sharing across DataCap CoW clones is via [`PageRef`] (= `Arc`),
32/// which carries its own refcount — `PageBytes` itself is not
33/// refcounted.
34#[derive(Debug)]
35pub struct PageBytes {
36 pub hash: CapHash,
37 pub bytes: Vec<u8>,
38}
39
40// --------------------------------------------------------------------------
41// Hand-written SSZ impls for `PageSlot` and `PageBytes`.
42//
43// `HashTreeRoot` is deliberately not derived: the pass-through semantics
44// are load-bearing for the substitution invariant. A `Loaded(page)` slot
45// must hash identically to a `Missing(h)` slot when `h == page.hash`, and
46// a `Loaded(page)` slot's root must equal `page.hash` (the precomputed
47// page digest). A `derive(HashTreeRoot)` would mix in a selector byte and
48// break that equality.
49//
50// --------------------------------------------------------------------------
51
52impl ssz::HashTreeRoot for PageSlot {
53 fn hash_tree_root<D: ::ssz::digest::Digest<OutputSize = ::ssz::digest::typenum::U32>>(
54 &self,
55 ) -> [u8; 32] {
56 match self {
57 // Canonical zero-page sentinel. Under SSZ, an empty page's
58 // root is the empty 32-byte chunk.
59 PageSlot::Empty => [0u8; 32],
60 PageSlot::Loaded(pr) => (**pr).hash_tree_root::<D>(),
61 PageSlot::Missing(h) => *h,
62 }
63 }
64}
65
66impl ssz::HashTreeRoot for PageBytes {
67 fn hash_tree_root<D: ::ssz::digest::Digest<OutputSize = ::ssz::digest::typenum::U32>>(
68 &self,
69 ) -> [u8; 32] {
70 // `self.hash` is the precomputed page-content identity (kept
71 // consistent with `bytes` by `cache.rs`). Returning it directly
72 // preserves substitution: a `Loaded(page)` slot is
73 // indistinguishable from `Missing(page.hash)` at the SSZ
74 // merkleization level.
75 self.hash
76 }
77}
78
79impl ssz::Encode for PageSlot {
80 fn is_ssz_fixed_len() -> bool {
81 false
82 }
83 fn ssz_fixed_len() -> usize {
84 ssz::BYTES_PER_LENGTH_OFFSET
85 }
86 fn ssz_bytes_len(&self) -> usize {
87 match self {
88 PageSlot::Empty => 1,
89 PageSlot::Loaded(pr) => 1 + (**pr).ssz_bytes_len(),
90 PageSlot::Missing(_) => 1 + 32,
91 }
92 }
93 fn ssz_append(&self, buf: &mut Vec<u8>) {
94 match self {
95 PageSlot::Empty => buf.push(0),
96 PageSlot::Loaded(pr) => {
97 buf.push(1);
98 (**pr).ssz_append(buf);
99 }
100 PageSlot::Missing(h) => {
101 buf.push(2);
102 buf.extend_from_slice(h);
103 }
104 }
105 }
106}
107
108impl ssz::Encode for PageBytes {
109 fn is_ssz_fixed_len() -> bool {
110 false
111 }
112 fn ssz_fixed_len() -> usize {
113 ssz::BYTES_PER_LENGTH_OFFSET
114 }
115 fn ssz_bytes_len(&self) -> usize {
116 // SSZ container with one fixed (hash) and one variable (bytes):
117 // fixed-region = 32 (hash) + 4 (offset slot) = 36; variable
118 // payload = bytes.len().
119 32 + 4 + self.bytes.len()
120 }
121 fn ssz_append(&self, buf: &mut Vec<u8>) {
122 // Field 0: hash (fixed, 32 bytes).
123 // Field 1: bytes (variable, offset slot + payload).
124 let fixed_region = 32 + 4;
125 buf.extend_from_slice(&self.hash);
126 // Offset to the variable payload = fixed_region size.
127 buf.extend_from_slice(&(fixed_region as u32).to_le_bytes());
128 buf.extend_from_slice(self.bytes.as_slice());
129 }
130}