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#[non_exhaustive]
80#[derive(Clone, Debug, Eq, Error, PartialEq)]
81pub enum StableCellPayloadError {
82    /// Memory contents do not start with the stable-cell marker.
83    #[error("memory is not an ic-stable-structures Cell")]
84    NotStableCell,
85    /// Stable-cell layout version does not match the adapter's current shape.
86    #[error("unexpected stable-cell layout version {version}")]
87    UnexpectedLayoutVersion {
88        /// Observed stable-cell version.
89        version: u8,
90    },
91    /// Stable-cell header length does not fit inside the memory.
92    #[error("stable-cell payload length {value_len} exceeds available bytes {available_bytes}")]
93    InvalidLength {
94        /// Encoded value length.
95        value_len: u64,
96        /// Available payload bytes in memory.
97        available_bytes: u64,
98    },
99    /// Stable-cell length cannot be represented on the current host.
100    #[error("stable-cell payload length {value_len} cannot fit in usize")]
101    LengthOverflow {
102        /// Encoded value length.
103        value_len: u64,
104    },
105}
106
107///
108/// StableCellLedgerError
109///
110/// Stable-cell ledger record validation failure.
111#[non_exhaustive]
112#[derive(Debug, Error)]
113pub enum StableCellLedgerError {
114    /// Stable-cell envelope is corrupt or unexpected.
115    #[error(transparent)]
116    Payload(#[from] StableCellPayloadError),
117    /// Stable-cell value bytes are not a valid ledger record.
118    #[error("stable-cell ledger record decode failed")]
119    Record(#[source] ciborium::de::Error<std::io::Error>),
120}
121
122/// Decode the raw value payload from an `ic-stable-structures::Cell` memory.
123///
124/// This helper is intentionally narrow: it recognizes the physical stable-cell
125/// envelope and returns the value bytes. It does not deserialize those bytes or
126/// decide whether they represent a valid allocation ledger.
127pub fn decode_stable_cell_payload<M: Memory>(
128    memory: &M,
129) -> Result<Vec<u8>, StableCellPayloadError> {
130    if memory.size() == 0 {
131        return Err(StableCellPayloadError::NotStableCell);
132    }
133
134    let mut header = [0; STABLE_CELL_HEADER_SIZE];
135    memory.read(0, &mut header);
136    if &header[0..3] != STABLE_CELL_MAGIC {
137        return Err(StableCellPayloadError::NotStableCell);
138    }
139    if header[3] != STABLE_CELL_LAYOUT_VERSION {
140        return Err(StableCellPayloadError::UnexpectedLayoutVersion { version: header[3] });
141    }
142
143    let value_len = u64::from(u32::from_le_bytes([
144        header[4], header[5], header[6], header[7],
145    ]));
146    let available_bytes = memory.size().saturating_mul(WASM_PAGE_SIZE_BYTES);
147    let payload_capacity = available_bytes.saturating_sub(STABLE_CELL_VALUE_OFFSET);
148    if value_len > payload_capacity {
149        return Err(StableCellPayloadError::InvalidLength {
150            value_len,
151            available_bytes: payload_capacity,
152        });
153    }
154    let value_len = usize::try_from(value_len)
155        .map_err(|_| StableCellPayloadError::LengthOverflow { value_len })?;
156
157    let mut bytes = vec![0; value_len];
158    memory.read(STABLE_CELL_VALUE_OFFSET, &mut bytes);
159    Ok(bytes)
160}
161
162/// Decode a `StableCellLedgerRecord` from stable-cell value bytes.
163///
164/// This decodes only the cell value payload, not the enclosing stable-cell
165/// header. Use [`decode_stable_cell_payload`] first when inspecting raw stable
166/// memory.
167///
168/// The returned record is decoded DTO state, not authority. Recover through the
169/// embedded [`LedgerCommitStore`] before trusting any ledger payload.
170pub fn decode_stable_cell_ledger_record(
171    bytes: &[u8],
172) -> Result<StableCellLedgerRecord, ciborium::de::Error<std::io::Error>> {
173    crate::cbor::from_slice_exact(bytes)
174}
175
176pub fn decode_stable_cell_ledger_record_from_memory<M: Memory>(
177    memory: &M,
178) -> Result<StableCellLedgerRecord, StableCellLedgerError> {
179    if memory.size() == 0 {
180        return Ok(StableCellLedgerRecord::default());
181    }
182
183    let payload = decode_stable_cell_payload(memory)?;
184    decode_stable_cell_ledger_record(&payload).map_err(StableCellLedgerError::Record)
185}
186
187/// Validate an existing stable-cell ledger record before opening it with
188/// `ic-stable-structures::Cell`.
189///
190/// `Cell::init` decodes the existing value through [`Storable::from_bytes`].
191/// That trait is panic-based, so the runtime preflights the raw memory with
192/// this fallible helper first. Empty memory is treated as uninitialized and is
193/// safe for `Cell::init` to create.
194pub fn validate_stable_cell_ledger_memory<M: Memory>(
195    memory: &M,
196) -> Result<(), StableCellLedgerError> {
197    decode_stable_cell_ledger_record_from_memory(memory)?;
198    Ok(())
199}
200
201fn serialize_record(record: &StableCellLedgerRecord) -> Vec<u8> {
202    let mut bytes = Vec::new();
203    ciborium::into_writer(record, &mut bytes).unwrap_or_else(|err| {
204        panic!("StableCellLedgerRecord serialize failed: {err}");
205    });
206    bytes
207}
208
209#[cfg(test)]
210mod tests {
211    use super::*;
212    use ic_stable_structures::{Cell, VectorMemory};
213
214    fn hex_fixture(contents: &str) -> Vec<u8> {
215        let hex = contents
216            .chars()
217            .filter(|char| !char.is_whitespace())
218            .collect::<String>();
219        assert_eq!(hex.len() % 2, 0, "fixture hex must have byte pairs");
220        hex.as_bytes()
221            .chunks_exact(2)
222            .map(|pair| {
223                let pair = std::str::from_utf8(pair).expect("fixture hex is utf8");
224                u8::from_str_radix(pair, 16).expect("fixture hex byte")
225            })
226            .collect()
227    }
228
229    #[test]
230    fn stable_cell_ledger_record_round_trips_through_cell() {
231        let memory = VectorMemory::default();
232        let record = StableCellLedgerRecord::default();
233        let cell = Cell::init(memory.clone(), record.clone());
234
235        assert_eq!(cell.get(), &record);
236        let payload = decode_stable_cell_payload(&memory).expect("decode stable cell payload");
237        let decoded = StableCellLedgerRecord::from_bytes(Cow::Owned(payload));
238        assert_eq!(decoded, record);
239    }
240
241    #[test]
242    fn current_stable_cell_record_fixture_recovers() {
243        let bytes = hex_fixture(include_str!(
244            "../fixtures/current/stable_cell_record.cbor.hex"
245        ));
246        let record = decode_stable_cell_ledger_record(&bytes).expect("stable-cell fixture");
247
248        assert_eq!(
249            bytes,
250            crate::test_cbor::to_vec(&record).expect("re-encoded stable-cell fixture")
251        );
252        assert_eq!(
253            record
254                .store()
255                .recover()
256                .expect("fixture store recovers")
257                .current_generation(),
258            1
259        );
260    }
261
262    #[test]
263    fn stable_cell_ledger_record_rejects_trailing_bytes() {
264        let mut bytes = serialize_record(&StableCellLedgerRecord::default());
265        bytes.push(0);
266
267        let err =
268            decode_stable_cell_ledger_record(&bytes).expect_err("trailing bytes must fail closed");
269
270        assert!(err.to_string().contains("trailing bytes"));
271    }
272
273    #[test]
274    fn stable_cell_ledger_record_rejects_unknown_top_level_fields() {
275        use crate::test_cbor::Value;
276
277        let mut map = Vec::new();
278        crate::test_cbor::map_insert(
279            &mut map,
280            Value::Text("store".to_string()),
281            crate::test_cbor::to_value(LedgerCommitStore::default()).expect("store value"),
282        );
283        crate::test_cbor::map_insert(
284            &mut map,
285            Value::Text("future_field".to_string()),
286            Value::Bool(true),
287        );
288        let bytes = crate::test_cbor::to_vec(&Value::Map(map)).expect("unknown-field stable cell");
289
290        let err = decode_stable_cell_ledger_record(&bytes)
291            .expect_err("unknown stable-cell record field must fail closed");
292
293        assert!(err.to_string().contains("future_field"));
294    }
295
296    #[test]
297    fn stable_cell_ledger_record_requires_both_commit_slot_fields() {
298        use crate::test_cbor::Value;
299
300        for (missing, present) in [("slot0", "slot1"), ("slot1", "slot0")] {
301            let mut physical = Vec::new();
302            crate::test_cbor::map_insert(
303                &mut physical,
304                Value::Text(present.to_string()),
305                Value::Null,
306            );
307            let mut store = Vec::new();
308            crate::test_cbor::map_insert(
309                &mut store,
310                Value::Text("physical".to_string()),
311                Value::Map(physical),
312            );
313            let mut record = Vec::new();
314            crate::test_cbor::map_insert(
315                &mut record,
316                Value::Text("store".to_string()),
317                Value::Map(store),
318            );
319            let bytes = crate::test_cbor::to_vec(&Value::Map(record)).expect("record bytes");
320
321            let err = decode_stable_cell_ledger_record(&bytes)
322                .expect_err("missing commit slot must fail closed");
323
324            assert!(err.to_string().contains(missing));
325        }
326    }
327
328    #[test]
329    fn stable_cell_payload_rejects_non_cell_memory() {
330        let memory = VectorMemory::default();
331        memory.grow(1);
332        memory.write(0, b"BAD");
333
334        assert_eq!(
335            decode_stable_cell_payload(&memory),
336            Err(StableCellPayloadError::NotStableCell)
337        );
338    }
339
340    #[test]
341    fn stable_cell_payload_rejects_empty_memory_without_panic() {
342        let memory = VectorMemory::default();
343
344        assert_eq!(
345            decode_stable_cell_payload(&memory),
346            Err(StableCellPayloadError::NotStableCell)
347        );
348    }
349
350    #[test]
351    fn stable_cell_ledger_preflight_classifies_bad_record_without_panic() {
352        let memory = VectorMemory::default();
353        memory.grow(1);
354        memory.write(0, STABLE_CELL_MAGIC);
355        memory.write(3, &[STABLE_CELL_LAYOUT_VERSION]);
356        memory.write(4, &1_u32.to_le_bytes());
357        memory.write(STABLE_CELL_VALUE_OFFSET, &[0xff]);
358
359        let err =
360            validate_stable_cell_ledger_memory(&memory).expect_err("bad record must be classified");
361
362        assert!(matches!(err, StableCellLedgerError::Record(_)));
363    }
364}