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"ICMF";
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 payload_len = self.payload.len() as u64;
53
54        let mut bytes = Vec::with_capacity(total_len);
55        bytes.extend_from_slice(LEDGER_PAYLOAD_MAGIC);
56        bytes.extend_from_slice(LEDGER_PAYLOAD_FORMAT_MARKER);
57        bytes.extend_from_slice(&LEDGER_PAYLOAD_FORMAT_VERSION.to_le_bytes());
58        bytes.extend_from_slice(&payload_len.to_le_bytes());
59        bytes.extend_from_slice(&self.payload);
60        Ok(bytes)
61    }
62
63    /// Manually decode the logical payload envelope.
64    pub fn decode(bytes: &[u8]) -> Result<Self, LedgerPayloadEnvelopeError> {
65        Ok(Self {
66            payload: Self::decode_payload(bytes)?.to_vec(),
67        })
68    }
69
70    // Recovery already owns the committed bytes. Validate the same envelope
71    // without copying its bounded payload before logical ledger decoding.
72    pub(super) fn decode_payload(bytes: &[u8]) -> Result<&[u8], LedgerPayloadEnvelopeError> {
73        if bytes.len() < LEDGER_PAYLOAD_MAGIC.len() {
74            return Err(LedgerPayloadEnvelopeError::Truncated {
75                actual: bytes.len(),
76                minimum: LEDGER_PAYLOAD_HEADER_LEN,
77            });
78        }
79
80        let Some(magic) = bytes.get(0..8).and_then(|bytes| bytes.try_into().ok()) else {
81            return Err(LedgerPayloadEnvelopeError::Truncated {
82                actual: bytes.len(),
83                minimum: LEDGER_PAYLOAD_HEADER_LEN,
84            });
85        };
86        if &magic != LEDGER_PAYLOAD_MAGIC {
87            return Err(LedgerPayloadEnvelopeError::BadMagic { found: magic });
88        }
89
90        let Some(format_marker) = bytes.get(8..12).and_then(|bytes| bytes.try_into().ok()) else {
91            return Err(LedgerPayloadEnvelopeError::Truncated {
92                actual: bytes.len(),
93                minimum: LEDGER_PAYLOAD_HEADER_LEN,
94            });
95        };
96        if &format_marker != LEDGER_PAYLOAD_FORMAT_MARKER {
97            return Err(LedgerPayloadEnvelopeError::UnsupportedFormat {
98                marker: format_marker,
99                version: None,
100            });
101        }
102
103        let Some(format_version) = bytes.get(12..16).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        let format_version = u32::from_le_bytes(format_version);
110        if format_version != LEDGER_PAYLOAD_FORMAT_VERSION {
111            return Err(LedgerPayloadEnvelopeError::UnsupportedFormat {
112                marker: format_marker,
113                version: Some(format_version),
114            });
115        }
116
117        let Some(payload_len) = bytes.get(16..24).and_then(|bytes| bytes.try_into().ok()) else {
118            return Err(LedgerPayloadEnvelopeError::Truncated {
119                actual: bytes.len(),
120                minimum: LEDGER_PAYLOAD_HEADER_LEN,
121            });
122        };
123        let payload_len = u64::from_le_bytes(payload_len);
124        let payload_len = usize::try_from(payload_len)
125            .map_err(|_| LedgerPayloadEnvelopeError::PayloadTooLarge { len: payload_len })?;
126        if payload_len > crate::constants::MAX_LEDGER_BYTES {
127            return Err(LedgerPayloadEnvelopeError::PayloadTooLarge {
128                len: payload_len as u64,
129            });
130        }
131        let expected_len = LEDGER_PAYLOAD_HEADER_LEN + payload_len;
132        if bytes.len() != expected_len {
133            return Err(LedgerPayloadEnvelopeError::LengthMismatch {
134                declared: payload_len,
135                actual: bytes.len().saturating_sub(LEDGER_PAYLOAD_HEADER_LEN),
136            });
137        }
138
139        Ok(&bytes[LEDGER_PAYLOAD_HEADER_LEN..])
140    }
141
142    /// Borrow the logical ledger payload bytes.
143    #[must_use]
144    pub fn payload(&self) -> &[u8] {
145        &self.payload
146    }
147}
148
149///
150/// LedgerPayloadEnvelopeError
151///
152/// Logical payload envelope could not be classified before ledger decode.
153///
154
155#[non_exhaustive]
156#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
157pub enum LedgerPayloadEnvelopeError {
158    /// Not enough bytes for an envelope header.
159    #[error("ledger payload envelope is truncated: {actual} bytes, need at least {minimum}")]
160    Truncated {
161        /// Bytes present.
162        actual: usize,
163        /// Minimum bytes required.
164        minimum: usize,
165    },
166    /// Magic bytes do not identify an `ic-memory` ledger payload.
167    #[error("ledger payload envelope has bad magic {found:?}")]
168    BadMagic {
169        /// Magic bytes found.
170        found: [u8; 8],
171    },
172    /// The payload belongs to the `ic-memory` ledger family but does not carry
173    /// the current format discriminator.
174    #[error("unsupported ic-memory ledger payload format (marker={marker:?}, version={version:?})")]
175    UnsupportedFormat {
176        /// Format marker found after the ledger-family magic.
177        marker: [u8; 4],
178        /// Format version when the current marker was present.
179        version: Option<u32>,
180    },
181    /// Payload length exceeds the ledger byte ceiling or cannot fit in this
182    /// platform's address space.
183    #[error("ledger payload envelope length {len} is too large")]
184    PayloadTooLarge {
185        /// Declared payload length.
186        len: u64,
187    },
188    /// Declared payload length does not match the bytes present.
189    #[error("ledger payload envelope declared {declared} payload bytes but contained {actual}")]
190    LengthMismatch {
191        /// Declared payload length.
192        declared: usize,
193        /// Actual payload length.
194        actual: usize,
195    },
196}
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201
202    #[test]
203    fn oversized_payload_is_rejected_before_encoding() {
204        let len = crate::constants::MAX_LEDGER_BYTES + 1;
205        assert_eq!(
206            LedgerPayloadEnvelope::current(vec![0; len]).try_encode(),
207            Err(LedgerPayloadEnvelopeError::PayloadTooLarge { len: len as u64 })
208        );
209    }
210
211    #[test]
212    fn malformed_lengths_are_classified_before_payload_copy() {
213        let mut bytes = LedgerPayloadEnvelope::current(Vec::new()).encode();
214        for len in 0..LEDGER_PAYLOAD_HEADER_LEN {
215            assert_eq!(
216                LedgerPayloadEnvelope::decode(&bytes[..len]),
217                Err(LedgerPayloadEnvelopeError::Truncated {
218                    actual: len,
219                    minimum: LEDGER_PAYLOAD_HEADER_LEN,
220                })
221            );
222        }
223        for len in [crate::constants::MAX_LEDGER_BYTES as u64 + 1, u64::MAX] {
224            bytes[16..24].copy_from_slice(&len.to_le_bytes());
225            assert_eq!(
226                LedgerPayloadEnvelope::decode(&bytes),
227                Err(LedgerPayloadEnvelopeError::PayloadTooLarge { len })
228            );
229        }
230        bytes[16..24].copy_from_slice(&1_u64.to_le_bytes());
231        assert_eq!(
232            LedgerPayloadEnvelope::decode(&bytes),
233            Err(LedgerPayloadEnvelopeError::LengthMismatch {
234                declared: 1,
235                actual: 0,
236            })
237        );
238        bytes[16..24].copy_from_slice(&0_u64.to_le_bytes());
239        bytes.push(42);
240        assert_eq!(
241            LedgerPayloadEnvelope::decode(&bytes),
242            Err(LedgerPayloadEnvelopeError::LengthMismatch {
243                declared: 0,
244                actual: 1,
245            })
246        );
247    }
248
249    #[test]
250    fn borrowed_payload_decode_shares_storage_through_the_byte_ceiling() {
251        for len in [0, 3, crate::constants::MAX_LEDGER_BYTES] {
252            let bytes = LedgerPayloadEnvelope::current(vec![42; len])
253                .try_encode()
254                .expect("bounded envelope");
255            let payload = LedgerPayloadEnvelope::decode_payload(&bytes).expect("valid envelope");
256
257            assert_eq!(payload.len(), len);
258            assert_eq!(
259                payload.as_ptr(),
260                bytes[LEDGER_PAYLOAD_HEADER_LEN..].as_ptr()
261            );
262            assert!(payload.iter().all(|byte| *byte == 42));
263        }
264    }
265}