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