Skip to main content

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}