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 = LedgerWriter(vec![0; LEDGER_PAYLOAD_HEADER_LEN]);
78    match ciborium::into_writer(ledger, &mut writer) {
79        Ok(()) => (),
80        Err(ciborium::ser::Error::Io(err)) if err.kind() == std::io::ErrorKind::WriteZero => {
81            return Err(super::LedgerIntegrityError::LimitExceeded {
82                resource: "ledger bytes",
83                limit: crate::constants::MAX_LEDGER_BYTES,
84            });
85        }
86        // Concrete derived serializers have no other recoverable failures.
87        Err(ciborium::ser::Error::Io(_) | ciborium::ser::Error::Value(_)) => {
88            panic!("allocation ledger serialization failed");
89        }
90    }
91    let mut bytes = writer.0;
92    let payload_len = bytes.len() - LEDGER_PAYLOAD_HEADER_LEN;
93    bytes[..LEDGER_PAYLOAD_HEADER_LEN].copy_from_slice(&encoded_header(payload_len));
94    Ok(bytes)
95}
96
97// Recovery already owns the committed bytes. Validate the same envelope
98// without copying its bounded payload before logical ledger decoding.
99pub(super) fn decode_payload(bytes: &[u8]) -> Result<&[u8], LedgerPayloadEnvelopeError> {
100    let Some(magic) = bytes.get(0..8).and_then(|bytes| bytes.try_into().ok()) else {
101        return Err(LedgerPayloadEnvelopeError::Truncated {
102            actual: bytes.len(),
103            minimum: LEDGER_PAYLOAD_HEADER_LEN,
104        });
105    };
106    if &magic != LEDGER_PAYLOAD_MAGIC {
107        return Err(LedgerPayloadEnvelopeError::BadMagic { found: magic });
108    }
109
110    let Some(format_marker) = bytes.get(8..12).and_then(|bytes| bytes.try_into().ok()) else {
111        return Err(LedgerPayloadEnvelopeError::Truncated {
112            actual: bytes.len(),
113            minimum: LEDGER_PAYLOAD_HEADER_LEN,
114        });
115    };
116    if &format_marker != LEDGER_PAYLOAD_FORMAT_MARKER {
117        return Err(LedgerPayloadEnvelopeError::UnsupportedFormat {
118            marker: format_marker,
119            version: None,
120        });
121    }
122
123    let Some(format_version) = bytes.get(12..16).and_then(|bytes| bytes.try_into().ok()) else {
124        return Err(LedgerPayloadEnvelopeError::Truncated {
125            actual: bytes.len(),
126            minimum: LEDGER_PAYLOAD_HEADER_LEN,
127        });
128    };
129    let format_version = u32::from_le_bytes(format_version);
130    if format_version != LEDGER_PAYLOAD_FORMAT_VERSION {
131        return Err(LedgerPayloadEnvelopeError::UnsupportedFormat {
132            marker: format_marker,
133            version: Some(format_version),
134        });
135    }
136
137    let Some(payload_len) = bytes.get(16..24).and_then(|bytes| bytes.try_into().ok()) else {
138        return Err(LedgerPayloadEnvelopeError::Truncated {
139            actual: bytes.len(),
140            minimum: LEDGER_PAYLOAD_HEADER_LEN,
141        });
142    };
143    let payload_len = u64::from_le_bytes(payload_len);
144    let payload_len = usize::try_from(payload_len)
145        .map_err(|_| LedgerPayloadEnvelopeError::PayloadTooLarge { len: payload_len })?;
146    if payload_len > crate::constants::MAX_LEDGER_BYTES {
147        return Err(LedgerPayloadEnvelopeError::PayloadTooLarge {
148            len: payload_len as u64,
149        });
150    }
151    let expected_len = LEDGER_PAYLOAD_HEADER_LEN + payload_len;
152    if bytes.len() != expected_len {
153        return Err(LedgerPayloadEnvelopeError::LengthMismatch {
154            declared: payload_len,
155            actual: bytes.len().saturating_sub(LEDGER_PAYLOAD_HEADER_LEN),
156        });
157    }
158
159    Ok(&bytes[LEDGER_PAYLOAD_HEADER_LEN..])
160}
161
162// Keep a single serialization pass, but stop before an oversized write or a
163// geometric capacity reservation can exceed the final envelope's byte ceiling.
164// Partial output is local and discarded on refusal, before physical mutation.
165struct LedgerWriter(Vec<u8>);
166
167impl std::io::Write for LedgerWriter {
168    fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
169        self.write_all(bytes)?;
170        Ok(bytes.len())
171    }
172
173    // Ciborium uses write_all; this buffer accepts an entire slice or refuses
174    // it, so it needs no partial-write retry loop from the default adapter.
175    #[inline]
176    fn write_all(&mut self, bytes: &[u8]) -> std::io::Result<()> {
177        let limit = crate::constants::MAX_COMMITTED_PAYLOAD_BYTES;
178        if bytes.len() > limit - self.0.len() {
179            return Err(std::io::ErrorKind::WriteZero.into());
180        }
181        let required = self.0.len() + bytes.len();
182        if required > self.0.capacity() {
183            let capacity = self.0.capacity().saturating_mul(2).max(required).min(limit);
184            self.0.reserve_exact(capacity - self.0.len());
185        }
186        self.0.extend_from_slice(bytes);
187        Ok(())
188    }
189
190    fn flush(&mut self) -> std::io::Result<()> {
191        Ok(())
192    }
193}
194
195// Both writers establish the payload bound before constructing this header.
196fn encoded_header(payload_len: usize) -> [u8; LEDGER_PAYLOAD_HEADER_LEN] {
197    let mut header = [0; LEDGER_PAYLOAD_HEADER_LEN];
198    header[..8].copy_from_slice(LEDGER_PAYLOAD_MAGIC);
199    header[8..12].copy_from_slice(LEDGER_PAYLOAD_FORMAT_MARKER);
200    header[12..16].copy_from_slice(&LEDGER_PAYLOAD_FORMAT_VERSION.to_le_bytes());
201    header[16..24].copy_from_slice(&(payload_len as u64).to_le_bytes());
202    header
203}
204
205///
206/// LedgerPayloadEnvelopeError
207///
208/// Logical payload envelope could not be classified before ledger decode.
209///
210
211#[non_exhaustive]
212#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
213pub enum LedgerPayloadEnvelopeError {
214    /// Not enough bytes for an envelope header.
215    #[error("ledger payload envelope is truncated: {actual} bytes, need at least {minimum}")]
216    Truncated {
217        /// Bytes present.
218        actual: usize,
219        /// Minimum bytes required.
220        minimum: usize,
221    },
222    /// Magic bytes do not identify an `ic-memory` ledger payload.
223    #[error("ledger payload envelope has bad magic {found:?}")]
224    BadMagic {
225        /// Magic bytes found.
226        found: [u8; 8],
227    },
228    /// The payload belongs to the `ic-memory` ledger family but does not carry
229    /// the current format discriminator.
230    #[error("unsupported ic-memory ledger payload format (marker={marker:?}, version={version:?})")]
231    UnsupportedFormat {
232        /// Format marker found after the ledger-family magic.
233        marker: [u8; 4],
234        /// Format version when the current marker was present.
235        version: Option<u32>,
236    },
237    /// Payload length exceeds the ledger byte ceiling or cannot fit in this
238    /// platform's address space.
239    #[error("ledger payload envelope length {len} is too large")]
240    PayloadTooLarge {
241        /// Declared payload length.
242        len: u64,
243    },
244    /// Declared payload length does not match the bytes present.
245    #[error("ledger payload envelope declared {declared} payload bytes but contained {actual}")]
246    LengthMismatch {
247        /// Declared payload length.
248        declared: usize,
249        /// Actual payload length.
250        actual: usize,
251    },
252}
253
254#[cfg(test)]
255mod tests {
256    use super::*;
257
258    #[test]
259    fn ledger_writer_admits_exact_ceiling_and_refuses_without_partial_append() {
260        use std::io::Write;
261
262        let mut writer = LedgerWriter(vec![0; LEDGER_PAYLOAD_HEADER_LEN]);
263        let chunk = [42; 4096];
264        for _ in 0..crate::constants::MAX_LEDGER_BYTES / chunk.len() {
265            writer.write_all(&chunk).unwrap();
266        }
267        assert_eq!(
268            writer.0.len(),
269            crate::constants::MAX_COMMITTED_PAYLOAD_BYTES
270        );
271        writer.write_all(&[]).unwrap();
272        assert_eq!(
273            writer.write(&[42]).unwrap_err().kind(),
274            std::io::ErrorKind::WriteZero
275        );
276        assert_eq!(
277            writer.0.len(),
278            crate::constants::MAX_COMMITTED_PAYLOAD_BYTES
279        );
280        assert!(
281            writer.0[LEDGER_PAYLOAD_HEADER_LEN..]
282                .iter()
283                .all(|&byte| byte == 42)
284        );
285
286        let mut writer = LedgerWriter(vec![0; LEDGER_PAYLOAD_HEADER_LEN]);
287        let before = writer.0.clone();
288        assert!(
289            writer
290                .write(&vec![42; crate::constants::MAX_LEDGER_BYTES + 1])
291                .is_err()
292        );
293        assert_eq!(writer.0, before);
294    }
295
296    #[test]
297    fn bounded_ledger_encoding_matches_canonical_cbor() {
298        for generations in [0, 1, 24, 256, 1024] {
299            let ledger = super::super::AllocationLedger {
300                current_generation: generations,
301                records: Vec::new(),
302            };
303            let mut payload = Vec::new();
304            ciborium::into_writer(&ledger, &mut payload).unwrap();
305            let expected = LedgerPayloadEnvelope::current(payload)
306                .try_encode()
307                .unwrap();
308            assert_eq!(encode_ledger(&ledger).unwrap(), expected);
309        }
310    }
311
312    #[test]
313    fn oversized_payload_is_rejected_before_encoding() {
314        let len = crate::constants::MAX_LEDGER_BYTES + 1;
315        assert_eq!(
316            LedgerPayloadEnvelope::current(vec![0; len]).try_encode(),
317            Err(LedgerPayloadEnvelopeError::PayloadTooLarge { len: len as u64 })
318        );
319    }
320
321    #[test]
322    fn malformed_lengths_are_classified_before_payload_copy() {
323        let mut bytes = LedgerPayloadEnvelope::current(Vec::new()).encode();
324        for len in 0..LEDGER_PAYLOAD_HEADER_LEN {
325            assert_eq!(
326                LedgerPayloadEnvelope::decode(&bytes[..len]),
327                Err(LedgerPayloadEnvelopeError::Truncated {
328                    actual: len,
329                    minimum: LEDGER_PAYLOAD_HEADER_LEN,
330                })
331            );
332        }
333        for len in [crate::constants::MAX_LEDGER_BYTES as u64 + 1, u64::MAX] {
334            bytes[16..24].copy_from_slice(&len.to_le_bytes());
335            assert_eq!(
336                LedgerPayloadEnvelope::decode(&bytes),
337                Err(LedgerPayloadEnvelopeError::PayloadTooLarge { len })
338            );
339        }
340        bytes[16..24].copy_from_slice(&1_u64.to_le_bytes());
341        assert_eq!(
342            LedgerPayloadEnvelope::decode(&bytes),
343            Err(LedgerPayloadEnvelopeError::LengthMismatch {
344                declared: 1,
345                actual: 0,
346            })
347        );
348        bytes[16..24].copy_from_slice(&0_u64.to_le_bytes());
349        bytes.push(42);
350        assert_eq!(
351            LedgerPayloadEnvelope::decode(&bytes),
352            Err(LedgerPayloadEnvelopeError::LengthMismatch {
353                declared: 0,
354                actual: 1,
355            })
356        );
357    }
358
359    #[test]
360    fn borrowed_payload_decode_shares_storage_through_the_byte_ceiling() {
361        for len in [0, 3, crate::constants::MAX_LEDGER_BYTES] {
362            let bytes = LedgerPayloadEnvelope::current(vec![42; len])
363                .try_encode()
364                .expect("bounded envelope");
365            let payload = decode_payload(&bytes).expect("valid envelope");
366
367            assert_eq!(payload.len(), len);
368            assert_eq!(
369                payload.as_ptr(),
370                bytes[LEDGER_PAYLOAD_HEADER_LEN..].as_ptr()
371            );
372            assert!(payload.iter().all(|byte| *byte == 42));
373        }
374    }
375}