Skip to main content

ic_memory/
stable_cell.rs

1use crate::{LedgerCommitStore, constants::WASM_PAGE_SIZE_BYTES};
2use ic_stable_structures::{Memory, Storable, storable::Bound};
3use serde::{Deserialize, Serialize};
4use std::borrow::Cow;
5use thiserror::Error;
6
7/// Stable-cell magic prefix written by `ic-stable-structures::Cell`.
8pub const STABLE_CELL_MAGIC: &[u8; 3] = b"SCL";
9/// Stable-cell layout version supported by this adapter.
10pub const STABLE_CELL_LAYOUT_VERSION: u8 = 1;
11/// Stable-cell header byte length.
12pub const STABLE_CELL_HEADER_SIZE: usize = 8;
13/// Byte offset where the stable-cell value payload starts.
14pub const STABLE_CELL_VALUE_OFFSET: u64 = 8;
15
16///
17/// StableCellLedgerRecord
18///
19/// `ic-stable-structures::Cell` record containing an `ic-memory` allocation
20/// ledger commit store.
21///
22/// This is a substrate adapter DTO. It owns no framework policy and does not
23/// open application allocations.
24///
25
26#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
27#[serde(deny_unknown_fields)]
28pub struct StableCellLedgerRecord {
29    store: LedgerCommitStore,
30}
31
32impl StableCellLedgerRecord {
33    /// Construct a record from a commit store.
34    #[must_use]
35    pub const fn new(store: LedgerCommitStore) -> Self {
36        Self { store }
37    }
38
39    /// Borrow the embedded commit store.
40    #[must_use]
41    pub const fn store(&self) -> &LedgerCommitStore {
42        &self.store
43    }
44
45    /// Mutably borrow the embedded commit store.
46    pub const fn store_mut(&mut self) -> &mut LedgerCommitStore {
47        &mut self.store
48    }
49
50    /// Consume this record and return the embedded commit store.
51    #[must_use]
52    pub fn into_store(self) -> LedgerCommitStore {
53        self.store
54    }
55}
56
57impl Storable for StableCellLedgerRecord {
58    const BOUND: Bound = Bound::Unbounded;
59
60    fn to_bytes(&self) -> Cow<'_, [u8]> {
61        Cow::Owned(serialize_record(self))
62    }
63
64    fn into_bytes(self) -> Vec<u8> {
65        serialize_record(&self)
66    }
67
68    fn from_bytes(bytes: Cow<'_, [u8]>) -> Self {
69        decode_stable_cell_ledger_record(&bytes).unwrap_or_else(|err| {
70            panic!("StableCellLedgerRecord deserialize failed: {err}");
71        })
72    }
73}
74
75///
76/// StableCellPayloadError
77///
78/// Stable-cell payload decode failure.
79#[derive(Clone, Debug, Eq, Error, PartialEq)]
80pub enum StableCellPayloadError {
81    /// Memory contents do not start with the stable-cell marker.
82    #[error("memory is not an ic-stable-structures Cell")]
83    NotStableCell,
84    /// Stable-cell layout version does not match the adapter's current shape.
85    #[error("unexpected stable-cell layout version {version}")]
86    UnexpectedLayoutVersion {
87        /// Observed stable-cell version.
88        version: u8,
89    },
90    /// Stable-cell header length does not fit inside the memory.
91    #[error("stable-cell payload length {value_len} exceeds available bytes {available_bytes}")]
92    InvalidLength {
93        /// Encoded value length.
94        value_len: u64,
95        /// Available payload bytes in memory.
96        available_bytes: u64,
97    },
98    /// Stable-cell length cannot be represented on the current host.
99    #[error("stable-cell payload length {value_len} cannot fit in usize")]
100    LengthOverflow {
101        /// Encoded value length.
102        value_len: u64,
103    },
104}
105
106///
107/// StableCellLedgerError
108///
109/// Stable-cell ledger record validation failure.
110#[derive(Debug, Error)]
111pub enum StableCellLedgerError {
112    /// Stable-cell envelope is corrupt or unexpected.
113    #[error(transparent)]
114    Payload(#[from] StableCellPayloadError),
115    /// Stable-cell value bytes are not a valid ledger record.
116    #[error("stable-cell ledger record decode failed")]
117    Record(#[source] serde_cbor::Error),
118}
119
120/// Decode the raw value payload from an `ic-stable-structures::Cell` memory.
121///
122/// This helper is intentionally narrow: it recognizes the physical stable-cell
123/// envelope and returns the value bytes. It does not deserialize those bytes or
124/// decide whether they represent a valid allocation ledger.
125pub fn decode_stable_cell_payload<M: Memory>(
126    memory: &M,
127) -> Result<Vec<u8>, StableCellPayloadError> {
128    if memory.size() == 0 {
129        return Err(StableCellPayloadError::NotStableCell);
130    }
131
132    let mut header = [0; STABLE_CELL_HEADER_SIZE];
133    memory.read(0, &mut header);
134    if &header[0..3] != STABLE_CELL_MAGIC {
135        return Err(StableCellPayloadError::NotStableCell);
136    }
137    if header[3] != STABLE_CELL_LAYOUT_VERSION {
138        return Err(StableCellPayloadError::UnexpectedLayoutVersion { version: header[3] });
139    }
140
141    let value_len = u64::from(u32::from_le_bytes([
142        header[4], header[5], header[6], header[7],
143    ]));
144    let available_bytes = memory.size().saturating_mul(WASM_PAGE_SIZE_BYTES);
145    let payload_capacity = available_bytes.saturating_sub(STABLE_CELL_VALUE_OFFSET);
146    if value_len > payload_capacity {
147        return Err(StableCellPayloadError::InvalidLength {
148            value_len,
149            available_bytes: payload_capacity,
150        });
151    }
152    let value_len = usize::try_from(value_len)
153        .map_err(|_| StableCellPayloadError::LengthOverflow { value_len })?;
154
155    let mut bytes = vec![0; value_len];
156    memory.read(STABLE_CELL_VALUE_OFFSET, &mut bytes);
157    Ok(bytes)
158}
159
160/// Decode a `StableCellLedgerRecord` from stable-cell value bytes.
161///
162/// This decodes only the cell value payload, not the enclosing stable-cell
163/// header. Use [`decode_stable_cell_payload`] first when inspecting raw stable
164/// memory.
165///
166/// The returned record is decoded DTO state, not authority. Recover through the
167/// embedded [`LedgerCommitStore`] before trusting any ledger payload.
168pub fn decode_stable_cell_ledger_record(
169    bytes: &[u8],
170) -> Result<StableCellLedgerRecord, serde_cbor::Error> {
171    serde_cbor::from_slice(bytes)
172}
173
174/// Validate an existing stable-cell ledger record before opening it with
175/// `ic-stable-structures::Cell`.
176///
177/// `Cell::init` decodes the existing value through [`Storable::from_bytes`].
178/// That trait is panic-based, so the runtime preflights the raw memory with
179/// this fallible helper first. Empty memory is treated as uninitialized and is
180/// safe for `Cell::init` to create.
181pub fn validate_stable_cell_ledger_memory<M: Memory>(
182    memory: &M,
183) -> Result<(), StableCellLedgerError> {
184    if memory.size() == 0 {
185        return Ok(());
186    }
187
188    let payload = decode_stable_cell_payload(memory)?;
189    decode_stable_cell_ledger_record(&payload).map_err(StableCellLedgerError::Record)?;
190    Ok(())
191}
192
193fn serialize_record(record: &StableCellLedgerRecord) -> Vec<u8> {
194    serde_cbor::to_vec(record).unwrap_or_else(|err| {
195        panic!("StableCellLedgerRecord serialize failed: {err}");
196    })
197}
198
199#[cfg(test)]
200mod tests {
201    use super::*;
202    use ic_stable_structures::{Cell, VectorMemory};
203
204    fn hex_fixture(contents: &str) -> Vec<u8> {
205        let hex = contents
206            .chars()
207            .filter(|char| !char.is_whitespace())
208            .collect::<String>();
209        assert_eq!(hex.len() % 2, 0, "fixture hex must have byte pairs");
210        hex.as_bytes()
211            .chunks_exact(2)
212            .map(|pair| {
213                let pair = std::str::from_utf8(pair).expect("fixture hex is utf8");
214                u8::from_str_radix(pair, 16).expect("fixture hex byte")
215            })
216            .collect()
217    }
218
219    #[test]
220    fn stable_cell_ledger_record_round_trips_through_cell() {
221        let memory = VectorMemory::default();
222        let record = StableCellLedgerRecord::default();
223        let cell = Cell::init(memory.clone(), record.clone());
224
225        assert_eq!(cell.get(), &record);
226        let payload = decode_stable_cell_payload(&memory).expect("decode stable cell payload");
227        let decoded = StableCellLedgerRecord::from_bytes(Cow::Owned(payload));
228        assert_eq!(decoded, record);
229    }
230
231    #[test]
232    fn current_stable_cell_record_fixture_recovers() {
233        let bytes = hex_fixture(include_str!(
234            "../fixtures/current/stable_cell_record.cbor.hex"
235        ));
236        let record = decode_stable_cell_ledger_record(&bytes).expect("stable-cell fixture");
237
238        assert_eq!(
239            bytes,
240            serde_cbor::to_vec(&record).expect("re-encoded stable-cell fixture")
241        );
242        assert_eq!(
243            record
244                .store()
245                .recover()
246                .expect("fixture store recovers")
247                .current_generation(),
248            1
249        );
250    }
251
252    #[test]
253    fn stable_cell_ledger_record_rejects_unknown_top_level_fields() {
254        use serde_cbor::Value;
255        use std::collections::BTreeMap;
256
257        let mut map = BTreeMap::new();
258        map.insert(
259            Value::Text("store".to_string()),
260            serde_cbor::value::to_value(LedgerCommitStore::default()).expect("store value"),
261        );
262        map.insert(Value::Text("future_field".to_string()), Value::Bool(true));
263        let bytes = serde_cbor::to_vec(&Value::Map(map)).expect("unknown-field stable cell");
264
265        let err = decode_stable_cell_ledger_record(&bytes)
266            .expect_err("unknown stable-cell record field must fail closed");
267
268        assert!(err.to_string().contains("future_field"));
269    }
270
271    #[test]
272    fn stable_cell_payload_rejects_non_cell_memory() {
273        let memory = VectorMemory::default();
274        memory.grow(1);
275        memory.write(0, b"BAD");
276
277        assert_eq!(
278            decode_stable_cell_payload(&memory),
279            Err(StableCellPayloadError::NotStableCell)
280        );
281    }
282
283    #[test]
284    fn stable_cell_payload_rejects_empty_memory_without_panic() {
285        let memory = VectorMemory::default();
286
287        assert_eq!(
288            decode_stable_cell_payload(&memory),
289            Err(StableCellPayloadError::NotStableCell)
290        );
291    }
292
293    #[test]
294    fn stable_cell_ledger_preflight_classifies_bad_record_without_panic() {
295        let memory = VectorMemory::default();
296        memory.grow(1);
297        memory.write(0, STABLE_CELL_MAGIC);
298        memory.write(3, &[STABLE_CELL_LAYOUT_VERSION]);
299        memory.write(4, &1_u32.to_le_bytes());
300        memory.write(STABLE_CELL_VALUE_OFFSET, &[0xff]);
301
302        let err =
303            validate_stable_cell_ledger_memory(&memory).expect_err("bad record must be classified");
304
305        assert!(matches!(err, StableCellLedgerError::Record(_)));
306    }
307}