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;
5const LEDGER_PAYLOAD_HEADER_LEN: usize = 8 + 4 + 4 + 8;
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 only on a platform where an in-memory payload length cannot fit
36    /// into the envelope's `u64` length field.
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        let total_len = LEDGER_PAYLOAD_HEADER_LEN
46            .checked_add(self.payload.len())
47            .ok_or(LedgerPayloadEnvelopeError::PayloadLengthOverflow {
48                len: self.payload.len(),
49            })?;
50        let payload_len = u64::try_from(self.payload.len()).map_err(|_| {
51            LedgerPayloadEnvelopeError::PayloadLengthOverflow {
52                len: self.payload.len(),
53            }
54        })?;
55
56        let mut bytes = Vec::with_capacity(total_len);
57        bytes.extend_from_slice(LEDGER_PAYLOAD_MAGIC);
58        bytes.extend_from_slice(LEDGER_PAYLOAD_FORMAT_MARKER);
59        bytes.extend_from_slice(&LEDGER_PAYLOAD_FORMAT_VERSION.to_le_bytes());
60        bytes.extend_from_slice(&payload_len.to_le_bytes());
61        bytes.extend_from_slice(&self.payload);
62        Ok(bytes)
63    }
64
65    /// Manually decode the logical payload envelope.
66    pub fn decode(bytes: &[u8]) -> Result<Self, LedgerPayloadEnvelopeError> {
67        if bytes.len() < LEDGER_PAYLOAD_MAGIC.len() {
68            return Err(LedgerPayloadEnvelopeError::Truncated {
69                actual: bytes.len(),
70                minimum: LEDGER_PAYLOAD_HEADER_LEN,
71            });
72        }
73
74        let Some(magic) = bytes.get(0..8).and_then(|bytes| bytes.try_into().ok()) else {
75            return Err(LedgerPayloadEnvelopeError::Truncated {
76                actual: bytes.len(),
77                minimum: LEDGER_PAYLOAD_HEADER_LEN,
78            });
79        };
80        if &magic != LEDGER_PAYLOAD_MAGIC {
81            return Err(LedgerPayloadEnvelopeError::BadMagic { found: magic });
82        }
83
84        let Some(format_marker) = bytes.get(8..12).and_then(|bytes| bytes.try_into().ok()) else {
85            return Err(LedgerPayloadEnvelopeError::Truncated {
86                actual: bytes.len(),
87                minimum: LEDGER_PAYLOAD_HEADER_LEN,
88            });
89        };
90        if &format_marker != LEDGER_PAYLOAD_FORMAT_MARKER {
91            return Err(LedgerPayloadEnvelopeError::UnsupportedFormat {
92                marker: format_marker,
93                version: None,
94            });
95        }
96
97        let Some(format_version) = bytes.get(12..16).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        let format_version = u32::from_le_bytes(format_version);
104        if format_version != LEDGER_PAYLOAD_FORMAT_VERSION {
105            return Err(LedgerPayloadEnvelopeError::UnsupportedFormat {
106                marker: format_marker,
107                version: Some(format_version),
108            });
109        }
110
111        let Some(payload_len) = bytes.get(16..24).and_then(|bytes| bytes.try_into().ok()) else {
112            return Err(LedgerPayloadEnvelopeError::Truncated {
113                actual: bytes.len(),
114                minimum: LEDGER_PAYLOAD_HEADER_LEN,
115            });
116        };
117        let payload_len = u64::from_le_bytes(payload_len);
118        let payload_len = usize::try_from(payload_len)
119            .map_err(|_| LedgerPayloadEnvelopeError::PayloadTooLarge { len: payload_len })?;
120        let expected_len = LEDGER_PAYLOAD_HEADER_LEN
121            .checked_add(payload_len)
122            .ok_or(LedgerPayloadEnvelopeError::PayloadLengthOverflow { len: payload_len })?;
123        if bytes.len() != expected_len {
124            return Err(LedgerPayloadEnvelopeError::LengthMismatch {
125                declared: payload_len,
126                actual: bytes.len().saturating_sub(LEDGER_PAYLOAD_HEADER_LEN),
127            });
128        }
129
130        Ok(Self {
131            payload: bytes[LEDGER_PAYLOAD_HEADER_LEN..].to_vec(),
132        })
133    }
134
135    /// Borrow the logical ledger payload bytes.
136    #[must_use]
137    pub fn payload(&self) -> &[u8] {
138        &self.payload
139    }
140}
141
142///
143/// LedgerPayloadEnvelopeError
144///
145/// Logical payload envelope could not be classified before ledger decode.
146#[non_exhaustive]
147#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
148pub enum LedgerPayloadEnvelopeError {
149    /// Not enough bytes for an envelope header.
150    #[error("ledger payload envelope is truncated: {actual} bytes, need at least {minimum}")]
151    Truncated {
152        /// Bytes present.
153        actual: usize,
154        /// Minimum bytes required.
155        minimum: usize,
156    },
157    /// Magic bytes do not identify an `ic-memory` ledger payload.
158    #[error("ledger payload envelope has bad magic {found:?}")]
159    BadMagic {
160        /// Magic bytes found.
161        found: [u8; 8],
162    },
163    /// The payload belongs to the `ic-memory` ledger family but does not carry
164    /// the current format discriminator.
165    #[error("unsupported ic-memory ledger payload format (marker={marker:?}, version={version:?})")]
166    UnsupportedFormat {
167        /// Format marker found after the ledger-family magic.
168        marker: [u8; 4],
169        /// Format version when the current marker was present.
170        version: Option<u32>,
171    },
172    /// Declared payload length does not fit in this platform's address space.
173    #[error("ledger payload envelope length {len} is too large")]
174    PayloadTooLarge {
175        /// Declared payload length.
176        len: u64,
177    },
178    /// Declared payload length overflowed the total envelope length.
179    #[error("ledger payload envelope length {len} overflows total length")]
180    PayloadLengthOverflow {
181        /// Declared payload length.
182        len: usize,
183    },
184    /// Declared payload length does not match the bytes present.
185    #[error("ledger payload envelope declared {declared} payload bytes but contained {actual}")]
186    LengthMismatch {
187        /// Declared payload length.
188        declared: usize,
189        /// Actual payload length.
190        actual: usize,
191    },
192}