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