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