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