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        if bytes.len() < LEDGER_PAYLOAD_MAGIC.len() {
73            return Err(LedgerPayloadEnvelopeError::Truncated {
74                actual: bytes.len(),
75                minimum: LEDGER_PAYLOAD_HEADER_LEN,
76            });
77        }
78
79        let Some(magic) = bytes.get(0..8).and_then(|bytes| bytes.try_into().ok()) else {
80            return Err(LedgerPayloadEnvelopeError::Truncated {
81                actual: bytes.len(),
82                minimum: LEDGER_PAYLOAD_HEADER_LEN,
83            });
84        };
85        if &magic != LEDGER_PAYLOAD_MAGIC {
86            return Err(LedgerPayloadEnvelopeError::BadMagic { found: magic });
87        }
88
89        let Some(format_marker) = bytes.get(8..12).and_then(|bytes| bytes.try_into().ok()) else {
90            return Err(LedgerPayloadEnvelopeError::Truncated {
91                actual: bytes.len(),
92                minimum: LEDGER_PAYLOAD_HEADER_LEN,
93            });
94        };
95        if &format_marker != LEDGER_PAYLOAD_FORMAT_MARKER {
96            return Err(LedgerPayloadEnvelopeError::UnsupportedFormat {
97                marker: format_marker,
98                version: None,
99            });
100        }
101
102        let Some(format_version) = bytes.get(12..16).and_then(|bytes| bytes.try_into().ok()) else {
103            return Err(LedgerPayloadEnvelopeError::Truncated {
104                actual: bytes.len(),
105                minimum: LEDGER_PAYLOAD_HEADER_LEN,
106            });
107        };
108        let format_version = u32::from_le_bytes(format_version);
109        if format_version != LEDGER_PAYLOAD_FORMAT_VERSION {
110            return Err(LedgerPayloadEnvelopeError::UnsupportedFormat {
111                marker: format_marker,
112                version: Some(format_version),
113            });
114        }
115
116        let Some(payload_len) = bytes.get(16..24).and_then(|bytes| bytes.try_into().ok()) else {
117            return Err(LedgerPayloadEnvelopeError::Truncated {
118                actual: bytes.len(),
119                minimum: LEDGER_PAYLOAD_HEADER_LEN,
120            });
121        };
122        let payload_len = u64::from_le_bytes(payload_len);
123        let payload_len = usize::try_from(payload_len)
124            .map_err(|_| LedgerPayloadEnvelopeError::PayloadTooLarge { len: payload_len })?;
125        if payload_len > crate::constants::MAX_LEDGER_BYTES {
126            return Err(LedgerPayloadEnvelopeError::PayloadTooLarge {
127                len: payload_len as u64,
128            });
129        }
130        let expected_len = LEDGER_PAYLOAD_HEADER_LEN
131            .checked_add(payload_len)
132            .ok_or(LedgerPayloadEnvelopeError::PayloadLengthOverflow { len: payload_len })?;
133        if bytes.len() != expected_len {
134            return Err(LedgerPayloadEnvelopeError::LengthMismatch {
135                declared: payload_len,
136                actual: bytes.len().saturating_sub(LEDGER_PAYLOAD_HEADER_LEN),
137            });
138        }
139
140        Ok(Self {
141            payload: bytes[LEDGER_PAYLOAD_HEADER_LEN..].to_vec(),
142        })
143    }
144
145    /// Borrow the logical ledger payload bytes.
146    #[must_use]
147    pub fn payload(&self) -> &[u8] {
148        &self.payload
149    }
150}
151
152///
153/// LedgerPayloadEnvelopeError
154///
155/// Logical payload envelope could not be classified before ledger decode.
156#[non_exhaustive]
157#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
158pub enum LedgerPayloadEnvelopeError {
159    /// Not enough bytes for an envelope header.
160    #[error("ledger payload envelope is truncated: {actual} bytes, need at least {minimum}")]
161    Truncated {
162        /// Bytes present.
163        actual: usize,
164        /// Minimum bytes required.
165        minimum: usize,
166    },
167    /// Magic bytes do not identify an `ic-memory` ledger payload.
168    #[error("ledger payload envelope has bad magic {found:?}")]
169    BadMagic {
170        /// Magic bytes found.
171        found: [u8; 8],
172    },
173    /// The payload belongs to the `ic-memory` ledger family but does not carry
174    /// the current format discriminator.
175    #[error("unsupported ic-memory ledger payload format (marker={marker:?}, version={version:?})")]
176    UnsupportedFormat {
177        /// Format marker found after the ledger-family magic.
178        marker: [u8; 4],
179        /// Format version when the current marker was present.
180        version: Option<u32>,
181    },
182    /// Declared payload length does not fit in this 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 overflowed the total envelope length.
189    #[error("ledger payload envelope length {len} overflows total length")]
190    PayloadLengthOverflow {
191        /// Declared payload length.
192        len: usize,
193    },
194    /// Declared payload length does not match the bytes present.
195    #[error("ledger payload envelope declared {declared} payload bytes but contained {actual}")]
196    LengthMismatch {
197        /// Declared payload length.
198        declared: usize,
199        /// Actual payload length.
200        actual: usize,
201    },
202}