Skip to main content

ic_memory/
stable_cell.rs

1use crate::LedgerCommitStore;
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;
15const WASM_PAGE_SIZE: u64 = 65_536;
16
17///
18/// StableCellLedgerRecord
19///
20/// `ic-stable-structures::Cell` record containing an `ic-memory` allocation
21/// ledger commit store.
22///
23/// This is a substrate adapter DTO. It owns no framework policy and does not
24/// open application allocations.
25///
26
27#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
28#[serde(deny_unknown_fields)]
29pub struct StableCellLedgerRecord {
30    store: LedgerCommitStore,
31}
32
33impl StableCellLedgerRecord {
34    /// Construct a record from a commit store.
35    #[must_use]
36    pub const fn new(store: LedgerCommitStore) -> Self {
37        Self { store }
38    }
39
40    /// Borrow the embedded commit store.
41    #[must_use]
42    pub const fn store(&self) -> &LedgerCommitStore {
43        &self.store
44    }
45
46    /// Mutably borrow the embedded commit store.
47    pub const fn store_mut(&mut self) -> &mut LedgerCommitStore {
48        &mut self.store
49    }
50
51    /// Consume this record and return the embedded commit store.
52    #[must_use]
53    pub fn into_store(self) -> LedgerCommitStore {
54        self.store
55    }
56}
57
58impl Storable for StableCellLedgerRecord {
59    const BOUND: Bound = Bound::Unbounded;
60
61    fn to_bytes(&self) -> Cow<'_, [u8]> {
62        Cow::Owned(serialize_record(self))
63    }
64
65    fn into_bytes(self) -> Vec<u8> {
66        serialize_record(&self)
67    }
68
69    fn from_bytes(bytes: Cow<'_, [u8]>) -> Self {
70        decode_stable_cell_ledger_record(&bytes).unwrap_or_else(|err| {
71            panic!("StableCellLedgerRecord deserialize failed: {err}");
72        })
73    }
74}
75
76///
77/// StableCellPayloadError
78///
79/// Stable-cell payload decode failure.
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 format version is not supported.
86    #[error("unsupported stable-cell layout version {version}")]
87    UnsupportedVersion {
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#[derive(Debug, Error)]
112pub enum StableCellLedgerError {
113    /// Stable-cell envelope is corrupt or unsupported.
114    #[error(transparent)]
115    Payload(#[from] StableCellPayloadError),
116    /// Stable-cell value bytes are not a valid ledger record.
117    #[error("stable-cell ledger record decode failed")]
118    Record(#[source] serde_cbor::Error),
119}
120
121/// Decode the raw value payload from an `ic-stable-structures::Cell` memory.
122///
123/// This helper is intentionally narrow: it recognizes the physical stable-cell
124/// envelope and returns the value bytes. It does not deserialize those bytes or
125/// decide whether they represent a valid allocation ledger.
126pub fn decode_stable_cell_payload<M: Memory>(
127    memory: &M,
128) -> Result<Vec<u8>, StableCellPayloadError> {
129    let mut header = [0; STABLE_CELL_HEADER_SIZE];
130    memory.read(0, &mut header);
131    if &header[0..3] != STABLE_CELL_MAGIC {
132        return Err(StableCellPayloadError::NotStableCell);
133    }
134    if header[3] != STABLE_CELL_LAYOUT_VERSION {
135        return Err(StableCellPayloadError::UnsupportedVersion { version: header[3] });
136    }
137
138    let value_len = u64::from(u32::from_le_bytes([
139        header[4], header[5], header[6], header[7],
140    ]));
141    let available_bytes = memory.size().saturating_mul(WASM_PAGE_SIZE);
142    let payload_capacity = available_bytes.saturating_sub(STABLE_CELL_VALUE_OFFSET);
143    if value_len > payload_capacity {
144        return Err(StableCellPayloadError::InvalidLength {
145            value_len,
146            available_bytes: payload_capacity,
147        });
148    }
149    let value_len = usize::try_from(value_len)
150        .map_err(|_| StableCellPayloadError::LengthOverflow { value_len })?;
151
152    let mut bytes = vec![0; value_len];
153    memory.read(STABLE_CELL_VALUE_OFFSET, &mut bytes);
154    Ok(bytes)
155}
156
157/// Decode a `StableCellLedgerRecord` from stable-cell value bytes.
158///
159/// This decodes only the cell value payload, not the enclosing stable-cell
160/// header. Use [`decode_stable_cell_payload`] first when inspecting raw stable
161/// memory.
162///
163/// The returned record is decoded DTO state, not authority. Recover through the
164/// embedded [`LedgerCommitStore`] before trusting any ledger payload.
165pub fn decode_stable_cell_ledger_record(
166    bytes: &[u8],
167) -> Result<StableCellLedgerRecord, serde_cbor::Error> {
168    serde_cbor::from_slice(bytes)
169}
170
171/// Validate an existing stable-cell ledger record before opening it with
172/// `ic-stable-structures::Cell`.
173///
174/// `Cell::init` decodes the existing value through [`Storable::from_bytes`].
175/// That trait is panic-based, so the runtime preflights the raw memory with
176/// this fallible helper first. Empty memory is treated as uninitialized and is
177/// safe for `Cell::init` to create.
178pub fn validate_stable_cell_ledger_memory<M: Memory>(
179    memory: &M,
180) -> Result<(), StableCellLedgerError> {
181    if memory.size() == 0 {
182        return Ok(());
183    }
184
185    let payload = decode_stable_cell_payload(memory)?;
186    decode_stable_cell_ledger_record(&payload).map_err(StableCellLedgerError::Record)?;
187    Ok(())
188}
189
190fn serialize_record(record: &StableCellLedgerRecord) -> Vec<u8> {
191    serde_cbor::to_vec(record).unwrap_or_else(|err| {
192        panic!("StableCellLedgerRecord serialize failed: {err}");
193    })
194}
195
196#[cfg(test)]
197mod tests {
198    use super::*;
199    use ic_stable_structures::{Cell, VectorMemory};
200
201    fn hex_fixture(contents: &str) -> Vec<u8> {
202        let hex = contents
203            .chars()
204            .filter(|char| !char.is_whitespace())
205            .collect::<String>();
206        assert_eq!(hex.len() % 2, 0, "fixture hex must have byte pairs");
207        hex.as_bytes()
208            .chunks_exact(2)
209            .map(|pair| {
210                let pair = std::str::from_utf8(pair).expect("fixture hex is utf8");
211                u8::from_str_radix(pair, 16).expect("fixture hex byte")
212            })
213            .collect()
214    }
215
216    #[test]
217    fn stable_cell_ledger_record_round_trips_through_cell() {
218        let memory = VectorMemory::default();
219        let record = StableCellLedgerRecord::default();
220        let cell = Cell::init(memory.clone(), record.clone());
221
222        assert_eq!(cell.get(), &record);
223        let payload = decode_stable_cell_payload(&memory).expect("decode stable cell payload");
224        let decoded = StableCellLedgerRecord::from_bytes(Cow::Owned(payload));
225        assert_eq!(decoded, record);
226    }
227
228    #[test]
229    fn v1_stable_cell_record_fixture_recovers() {
230        let bytes = hex_fixture(include_str!("../fixtures/v1/stable_cell_record.cbor.hex"));
231        let record = decode_stable_cell_ledger_record(&bytes).expect("stable-cell fixture");
232
233        assert_eq!(
234            bytes,
235            serde_cbor::to_vec(&record).expect("re-encoded stable-cell fixture")
236        );
237        assert_eq!(
238            record
239                .store()
240                .recover()
241                .expect("fixture store recovers")
242                .current_generation(),
243            1
244        );
245    }
246
247    #[test]
248    fn stable_cell_payload_rejects_non_cell_memory() {
249        let memory = VectorMemory::default();
250        memory.grow(1);
251        memory.write(0, b"BAD");
252
253        assert_eq!(
254            decode_stable_cell_payload(&memory),
255            Err(StableCellPayloadError::NotStableCell)
256        );
257    }
258
259    #[test]
260    fn stable_cell_ledger_preflight_classifies_bad_record_without_panic() {
261        let memory = VectorMemory::default();
262        memory.grow(1);
263        memory.write(0, STABLE_CELL_MAGIC);
264        memory.write(3, &[STABLE_CELL_LAYOUT_VERSION]);
265        memory.write(4, &1_u32.to_le_bytes());
266        memory.write(STABLE_CELL_VALUE_OFFSET, &[0xff]);
267
268        let err =
269            validate_stable_cell_ledger_memory(&memory).expect_err("bad record must be classified");
270
271        assert!(matches!(err, StableCellLedgerError::Record(_)));
272    }
273}