Skip to main content

ic_memory/ledger/
payload.rs

1const LEDGER_PAYLOAD_MAGIC: &[u8; 8] = b"ICMEMLED";
2const LEDGER_PAYLOAD_FORMAT_MARKER: &[u8; 4] = b"ICMS";
3/// Current durable ledger payload format version.
4pub const LEDGER_PAYLOAD_FORMAT_VERSION: u32 = 1;
5use crate::constants::LEDGER_PAYLOAD_HEADER_LEN;
6
7///
8/// LedgerPayloadEnvelope
9///
10/// Owned byte fixture for logical envelopes inside committed generations.
11///
12/// Production recovery borrows the payload through the private decoder. Tests
13/// use this wrapper to construct valid and deliberately malformed wire inputs;
14/// it does not establish committed ledger authority.
15///
16
17#[cfg(test)]
18#[derive(Clone, Debug, Eq, PartialEq)]
19pub struct LedgerPayloadEnvelope {
20    payload: Vec<u8>,
21}
22
23#[cfg(test)]
24impl LedgerPayloadEnvelope {
25    /// Wrap a current logical ledger payload.
26    #[must_use]
27    pub const fn current(payload: Vec<u8>) -> Self {
28        Self { payload }
29    }
30
31    /// Manually encode the logical payload envelope.
32    ///
33    /// # Panics
34    ///
35    /// Panics if the payload exceeds the current ledger byte ceiling. Use
36    /// `try_encode` for typed errors.
37    #[must_use]
38    pub fn encode(&self) -> Vec<u8> {
39        self.try_encode()
40            .expect("payload exceeds the ledger byte ceiling")
41    }
42
43    /// Try to encode the logical payload envelope.
44    pub fn try_encode(&self) -> Result<Vec<u8>, LedgerPayloadEnvelopeError> {
45        if self.payload.len() > crate::constants::MAX_LEDGER_BYTES {
46            return Err(LedgerPayloadEnvelopeError::PayloadTooLarge {
47                len: self.payload.len() as u64,
48            });
49        }
50        // The byte ceiling bounds both the total usize length and the u64 header.
51        let total_len = LEDGER_PAYLOAD_HEADER_LEN + self.payload.len();
52        let mut bytes = Vec::with_capacity(total_len);
53        bytes.extend_from_slice(&encoded_header(self.payload.len()));
54        bytes.extend_from_slice(&self.payload);
55        Ok(bytes)
56    }
57
58    /// Manually decode the logical payload envelope.
59    pub fn decode(bytes: &[u8]) -> Result<Self, LedgerPayloadEnvelopeError> {
60        Ok(Self {
61            payload: decode_payload(bytes)?.to_vec(),
62        })
63    }
64
65    /// Borrow the logical ledger payload bytes.
66    #[must_use]
67    pub fn payload(&self) -> &[u8] {
68        &self.payload
69    }
70}
71
72// Serialize directly into the final envelope buffer. The commit boundary
73// retains its ledger-byte integrity error before any physical mutation.
74pub(super) fn encode_ledger(
75    ledger: &super::AllocationLedger,
76) -> Result<Vec<u8>, super::LedgerIntegrityError> {
77    let mut writer = crate::bounded_writer::BoundedWriter::new(
78        vec![0; LEDGER_PAYLOAD_HEADER_LEN],
79        crate::constants::MAX_COMMITTED_PAYLOAD_BYTES,
80    );
81    match ciborium::into_writer(ledger, &mut writer) {
82        Ok(()) => (),
83        Err(ciborium::ser::Error::Io(err)) if err.kind() == std::io::ErrorKind::WriteZero => {
84            return Err(super::LedgerIntegrityError::LimitExceeded {
85                resource: "ledger bytes",
86                limit: crate::constants::MAX_LEDGER_BYTES,
87            });
88        }
89        // Concrete derived serializers have no other recoverable failures.
90        Err(ciborium::ser::Error::Io(_) | ciborium::ser::Error::Value(_)) => {
91            panic!("allocation ledger serialization failed");
92        }
93    }
94    let mut bytes = writer.bytes;
95    let payload_len = bytes.len() - LEDGER_PAYLOAD_HEADER_LEN;
96    bytes[..LEDGER_PAYLOAD_HEADER_LEN].copy_from_slice(&encoded_header(payload_len));
97    Ok(bytes)
98}
99
100// Recovery already owns the committed bytes. Validate the same envelope
101// without copying its bounded payload before logical ledger decoding.
102pub(super) fn decode_payload(bytes: &[u8]) -> Result<&[u8], LedgerPayloadEnvelopeError> {
103    let Some(magic) = bytes.get(0..8).and_then(|bytes| bytes.try_into().ok()) else {
104        return Err(LedgerPayloadEnvelopeError::Truncated {
105            actual: bytes.len(),
106            minimum: LEDGER_PAYLOAD_HEADER_LEN,
107        });
108    };
109    if &magic != LEDGER_PAYLOAD_MAGIC {
110        return Err(LedgerPayloadEnvelopeError::BadMagic { found: magic });
111    }
112
113    let Some(format_marker) = bytes.get(8..12).and_then(|bytes| bytes.try_into().ok()) else {
114        return Err(LedgerPayloadEnvelopeError::Truncated {
115            actual: bytes.len(),
116            minimum: LEDGER_PAYLOAD_HEADER_LEN,
117        });
118    };
119    if &format_marker != LEDGER_PAYLOAD_FORMAT_MARKER {
120        return Err(LedgerPayloadEnvelopeError::UnsupportedFormat {
121            marker: format_marker,
122            version: None,
123        });
124    }
125
126    let Some(format_version) = bytes.get(12..16).and_then(|bytes| bytes.try_into().ok()) else {
127        return Err(LedgerPayloadEnvelopeError::Truncated {
128            actual: bytes.len(),
129            minimum: LEDGER_PAYLOAD_HEADER_LEN,
130        });
131    };
132    let format_version = u32::from_le_bytes(format_version);
133    if format_version != LEDGER_PAYLOAD_FORMAT_VERSION {
134        return Err(LedgerPayloadEnvelopeError::UnsupportedFormat {
135            marker: format_marker,
136            version: Some(format_version),
137        });
138    }
139
140    let Some(payload_len) = bytes.get(16..24).and_then(|bytes| bytes.try_into().ok()) else {
141        return Err(LedgerPayloadEnvelopeError::Truncated {
142            actual: bytes.len(),
143            minimum: LEDGER_PAYLOAD_HEADER_LEN,
144        });
145    };
146    let payload_len = u64::from_le_bytes(payload_len);
147    let payload_len = usize::try_from(payload_len)
148        .map_err(|_| LedgerPayloadEnvelopeError::PayloadTooLarge { len: payload_len })?;
149    if payload_len > crate::constants::MAX_LEDGER_BYTES {
150        return Err(LedgerPayloadEnvelopeError::PayloadTooLarge {
151            len: payload_len as u64,
152        });
153    }
154    let expected_len = LEDGER_PAYLOAD_HEADER_LEN + payload_len;
155    if bytes.len() != expected_len {
156        return Err(LedgerPayloadEnvelopeError::LengthMismatch {
157            declared: payload_len,
158            actual: bytes.len().saturating_sub(LEDGER_PAYLOAD_HEADER_LEN),
159        });
160    }
161
162    Ok(&bytes[LEDGER_PAYLOAD_HEADER_LEN..])
163}
164
165// Both writers establish the payload bound before constructing this header.
166fn encoded_header(payload_len: usize) -> [u8; LEDGER_PAYLOAD_HEADER_LEN] {
167    let mut header = [0; LEDGER_PAYLOAD_HEADER_LEN];
168    header[..8].copy_from_slice(LEDGER_PAYLOAD_MAGIC);
169    header[8..12].copy_from_slice(LEDGER_PAYLOAD_FORMAT_MARKER);
170    header[12..16].copy_from_slice(&LEDGER_PAYLOAD_FORMAT_VERSION.to_le_bytes());
171    header[16..24].copy_from_slice(&(payload_len as u64).to_le_bytes());
172    header
173}
174
175///
176/// LedgerPayloadEnvelopeError
177///
178/// Logical payload envelope could not be classified before ledger decode.
179///
180
181#[non_exhaustive]
182#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
183pub enum LedgerPayloadEnvelopeError {
184    /// Not enough bytes for an envelope header.
185    #[error("ledger payload envelope is truncated: {actual} bytes, need at least {minimum}")]
186    Truncated {
187        /// Bytes present.
188        actual: usize,
189        /// Minimum bytes required.
190        minimum: usize,
191    },
192    /// Magic bytes do not identify an `ic-memory` ledger payload.
193    #[error("ledger payload envelope has bad magic {found:?}")]
194    BadMagic {
195        /// Magic bytes found.
196        found: [u8; 8],
197    },
198    /// The payload belongs to the `ic-memory` ledger family but does not carry
199    /// the current format discriminator.
200    #[error("unsupported ic-memory ledger payload format (marker={marker:?}, version={version:?})")]
201    UnsupportedFormat {
202        /// Format marker found after the ledger-family magic.
203        marker: [u8; 4],
204        /// Format version when the current marker was present.
205        version: Option<u32>,
206    },
207    /// Payload length exceeds the ledger byte ceiling or cannot fit in this
208    /// platform's address space.
209    #[error("ledger payload envelope length {len} is too large")]
210    PayloadTooLarge {
211        /// Declared payload length.
212        len: u64,
213    },
214    /// Declared payload length does not match the bytes present.
215    #[error("ledger payload envelope declared {declared} payload bytes but contained {actual}")]
216    LengthMismatch {
217        /// Declared payload length.
218        declared: usize,
219        /// Actual payload length.
220        actual: usize,
221    },
222}
223
224#[cfg(test)]
225mod tests {
226    use super::*;
227
228    #[test]
229    fn ledger_writer_admits_exact_ceiling_and_refuses_without_partial_append() {
230        use std::io::Write;
231
232        let mut writer = crate::bounded_writer::BoundedWriter::new(
233            vec![0; LEDGER_PAYLOAD_HEADER_LEN],
234            crate::constants::MAX_COMMITTED_PAYLOAD_BYTES,
235        );
236        let chunk = [42; 4096];
237        for _ in 0..crate::constants::MAX_LEDGER_BYTES / chunk.len() {
238            writer.write_all(&chunk).unwrap();
239        }
240        assert_eq!(
241            writer.bytes.len(),
242            crate::constants::MAX_COMMITTED_PAYLOAD_BYTES
243        );
244        writer.write_all(&[]).unwrap();
245        assert_eq!(
246            writer.write(&[42]).unwrap_err().kind(),
247            std::io::ErrorKind::WriteZero
248        );
249        assert_eq!(
250            writer.bytes.len(),
251            crate::constants::MAX_COMMITTED_PAYLOAD_BYTES
252        );
253        assert!(
254            writer.bytes[LEDGER_PAYLOAD_HEADER_LEN..]
255                .iter()
256                .all(|&byte| byte == 42)
257        );
258
259        let mut writer = crate::bounded_writer::BoundedWriter::new(
260            vec![0; LEDGER_PAYLOAD_HEADER_LEN],
261            crate::constants::MAX_COMMITTED_PAYLOAD_BYTES,
262        );
263        let before = writer.bytes.clone();
264        assert!(
265            writer
266                .write(&vec![42; crate::constants::MAX_LEDGER_BYTES + 1])
267                .is_err()
268        );
269        assert_eq!(writer.bytes, before);
270    }
271
272    #[test]
273    fn bounded_ledger_encoding_matches_canonical_cbor() {
274        for generations in [0, 1, 24, 256, 1024] {
275            let ledger = super::super::AllocationLedger {
276                current_generation: generations,
277                records: Vec::new(),
278            };
279            let mut payload = Vec::new();
280            ciborium::into_writer(&ledger, &mut payload).unwrap();
281            let expected = LedgerPayloadEnvelope::current(payload)
282                .try_encode()
283                .unwrap();
284            assert_eq!(encode_ledger(&ledger).unwrap(), expected);
285        }
286    }
287
288    #[test]
289    fn oversized_payload_is_rejected_before_encoding() {
290        let len = crate::constants::MAX_LEDGER_BYTES + 1;
291        assert_eq!(
292            LedgerPayloadEnvelope::current(vec![0; len]).try_encode(),
293            Err(LedgerPayloadEnvelopeError::PayloadTooLarge { len: len as u64 })
294        );
295    }
296
297    #[test]
298    fn malformed_lengths_are_classified_before_payload_copy() {
299        let mut bytes = LedgerPayloadEnvelope::current(Vec::new()).encode();
300        for len in 0..LEDGER_PAYLOAD_HEADER_LEN {
301            assert_eq!(
302                LedgerPayloadEnvelope::decode(&bytes[..len]),
303                Err(LedgerPayloadEnvelopeError::Truncated {
304                    actual: len,
305                    minimum: LEDGER_PAYLOAD_HEADER_LEN,
306                })
307            );
308        }
309        for len in [crate::constants::MAX_LEDGER_BYTES as u64 + 1, u64::MAX] {
310            bytes[16..24].copy_from_slice(&len.to_le_bytes());
311            assert_eq!(
312                LedgerPayloadEnvelope::decode(&bytes),
313                Err(LedgerPayloadEnvelopeError::PayloadTooLarge { len })
314            );
315        }
316        bytes[16..24].copy_from_slice(&1_u64.to_le_bytes());
317        assert_eq!(
318            LedgerPayloadEnvelope::decode(&bytes),
319            Err(LedgerPayloadEnvelopeError::LengthMismatch {
320                declared: 1,
321                actual: 0,
322            })
323        );
324        bytes[16..24].copy_from_slice(&0_u64.to_le_bytes());
325        bytes.push(42);
326        assert_eq!(
327            LedgerPayloadEnvelope::decode(&bytes),
328            Err(LedgerPayloadEnvelopeError::LengthMismatch {
329                declared: 0,
330                actual: 1,
331            })
332        );
333    }
334
335    #[test]
336    fn borrowed_payload_decode_shares_storage_through_the_byte_ceiling() {
337        for len in [0, 3, crate::constants::MAX_LEDGER_BYTES] {
338            let bytes = LedgerPayloadEnvelope::current(vec![42; len])
339                .try_encode()
340                .expect("bounded envelope");
341            let payload = decode_payload(&bytes).expect("valid envelope");
342
343            assert_eq!(payload.len(), len);
344            assert_eq!(
345                payload.as_ptr(),
346                bytes[LEDGER_PAYLOAD_HEADER_LEN..].as_ptr()
347            );
348            assert!(payload.iter().all(|byte| *byte == 42));
349        }
350    }
351}