Skip to main content

ic_memory/
physical.rs

1use crate::hash::{FNV_OFFSET, fnv64};
2use serde::{Deserialize, Serialize};
3
4// ICMWIRE1 identifies a protected generation with a records-only Wire payload.
5const COMMIT_MARKER: u64 = 0x4943_4D57_4952_4531;
6
7///
8/// CommittedGenerationBytes
9///
10/// Committed ledger generation payload protected by a checksum.
11///
12/// This private DTO is the current physical snapshot representation. Its
13/// recovered bytes are untrusted until marker/checksum validation and ledger
14/// decoding/integrity validation have both succeeded.
15/// The records-only payload encodes as bounded opaque Wire bytes. Diagnostic
16/// JSON output is permitted, but is not a snapshot input format. Direct Serde
17/// decoding is a DTO operation; durable readers additionally admit Wire resource
18/// budgets and exact input consumption before protected recovery.
19///
20
21#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
22#[serde(deny_unknown_fields)]
23pub struct CommittedGenerationBytes {
24    /// Generation number represented by this payload.
25    pub(crate) generation: u64,
26    /// Physical commit marker. Readers reject records with an invalid marker.
27    pub(crate) commit_marker: u64,
28    /// Checksum over the generation, marker, and payload bytes.
29    pub(crate) checksum: u64,
30    /// Encoded ledger generation payload.
31    #[serde(with = "opaque_payload")]
32    pub(crate) payload: Vec<u8>,
33}
34
35// Use the shared byte bound for opaque payloads; no consumer-owned byte visitor.
36mod opaque_payload {
37    use crate::constants::MAX_COMMITTED_PAYLOAD_BYTES;
38    use serde::{Deserialize, Deserializer, Serialize, Serializer};
39
40    pub fn serialize<S: Serializer>(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error> {
41        ic_wire::BytesRef::<MAX_COMMITTED_PAYLOAD_BYTES>::new(bytes)
42            .map_err(serde::ser::Error::custom)?
43            .serialize(serializer)
44    }
45
46    pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Vec<u8>, D::Error> {
47        ic_wire::Bytes::<MAX_COMMITTED_PAYLOAD_BYTES>::deserialize(deserializer)
48            .map(ic_wire::Bytes::into_vec)
49    }
50}
51
52impl CommittedGenerationBytes {
53    /// Build a committed generation record.
54    #[must_use]
55    pub fn new(generation: u64, payload: Vec<u8>) -> Self {
56        let mut record = Self {
57            generation,
58            commit_marker: COMMIT_MARKER,
59            checksum: 0,
60            payload,
61        };
62        record.checksum = generation_checksum(&record);
63        record
64    }
65
66    /// Return the generation number represented by this payload.
67    #[must_use]
68    pub const fn generation(&self) -> u64 {
69        self.generation
70    }
71
72    /// Return the physical commit marker.
73    ///
74    /// This is diagnostic data from a recovered record. Callers should use
75    /// [`CommittedGenerationBytes::validates`] before treating the record as
76    /// authoritative.
77    #[must_use]
78    #[cfg(test)]
79    pub const fn commit_marker(&self) -> u64 {
80        self.commit_marker
81    }
82
83    /// Return the checksum over the generation, marker, and payload bytes.
84    ///
85    /// The checksum is non-cryptographic and detects accidental corruption
86    /// only.
87    #[must_use]
88    #[cfg(test)]
89    pub const fn checksum(&self) -> u64 {
90        self.checksum
91    }
92
93    /// Borrow the encoded ledger generation payload.
94    #[must_use]
95    pub fn payload(&self) -> &[u8] {
96        &self.payload
97    }
98
99    /// Return whether the marker and checksum validate.
100    #[must_use]
101    pub fn validates(&self) -> bool {
102        self.commit_marker == COMMIT_MARKER && self.checksum == generation_checksum(self)
103    }
104}
105
106impl crate::ledger::LedgerCommitStore {
107    /// Borrow untrusted bytes only for encoding and capacity planning.
108    pub(crate) const fn current(&self) -> Option<&CommittedGenerationBytes> {
109        self.current.as_ref()
110    }
111
112    /// Recover the current snapshot only after marker and checksum validation.
113    pub(crate) fn authoritative(&self) -> Result<&CommittedGenerationBytes, CommitRecoveryError> {
114        let current = self
115            .current()
116            .ok_or(CommitRecoveryError::NoValidGeneration)?;
117        if current.commit_marker != COMMIT_MARKER {
118            return Err(CommitRecoveryError::UnsupportedFormat {
119                marker: current.commit_marker,
120            });
121        }
122        if !current.validates() {
123            return Err(CommitRecoveryError::InvalidSnapshot {
124                generation: current.generation(),
125            });
126        }
127        Ok(current)
128    }
129
130    pub(crate) fn authoritative_with_diagnostic(
131        &self,
132    ) -> (
133        Result<&CommittedGenerationBytes, CommitRecoveryError>,
134        CommitSnapshotDiagnostic,
135    ) {
136        let recovery = self.authoritative();
137        let current = match self.current() {
138            None => CommitSnapshotDiagnostic::Empty,
139            Some(record) if recovery.is_ok() => CommitSnapshotDiagnostic::Valid {
140                generation: record.generation(),
141            },
142            Some(record) => CommitSnapshotDiagnostic::Invalid {
143                generation: record.generation(),
144            },
145        };
146        (recovery, current)
147    }
148
149    /// Report the same checked current record without decoding logical ownership.
150    pub(crate) fn diagnostic(&self) -> CommitSnapshotDiagnostic {
151        self.authoritative_with_diagnostic().1
152    }
153
154    /// Replace the current snapshot after checking its successor and byte bound.
155    /// The owner must persist the enclosing record before publishing authority.
156    pub(crate) fn commit_payload_at_generation(
157        &mut self,
158        generation: u64,
159        payload: Vec<u8>,
160    ) -> Result<&CommittedGenerationBytes, CommitRecoveryError> {
161        if payload.len() > crate::constants::MAX_COMMITTED_PAYLOAD_BYTES {
162            return Err(CommitRecoveryError::PayloadTooLarge {
163                len: payload.len(),
164                limit: crate::constants::MAX_COMMITTED_PAYLOAD_BYTES,
165            });
166        }
167        match self.authoritative() {
168            Ok(prior) => {
169                let prior_generation = prior.generation();
170                let expected = prior_generation.checked_add(1).ok_or(
171                    CommitRecoveryError::GenerationOverflow {
172                        generation: prior_generation,
173                    },
174                )?;
175                if generation != expected {
176                    return Err(CommitRecoveryError::UnexpectedGeneration {
177                        expected,
178                        actual: generation,
179                    });
180                }
181            }
182            // The current field was explicitly present and empty, never corrupt.
183            Err(CommitRecoveryError::NoValidGeneration) => (),
184            Err(error) => return Err(error),
185        }
186        Ok(self
187            .current
188            .insert(CommittedGenerationBytes::new(generation, payload)))
189    }
190
191    #[cfg(test)]
192    pub(crate) fn write_corrupt_snapshot(&mut self, generation: u64, payload: Vec<u8>) {
193        let mut corrupt = CommittedGenerationBytes::new(generation, payload);
194        corrupt.checksum ^= 1;
195        self.current = Some(corrupt);
196    }
197}
198
199///
200/// CommitSnapshotDiagnostic
201///
202/// Read-only marker/checksum status of the one current snapshot.
203///
204
205#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
206pub enum CommitSnapshotDiagnostic {
207    /// No current snapshot has been committed.
208    Empty,
209    /// The current snapshot passed marker/checksum validation.
210    Valid {
211        /// Current generation.
212        generation: u64,
213    },
214    /// The current snapshot failed marker/checksum validation.
215    Invalid {
216        /// Untrusted generation observed in the rejected snapshot.
217        generation: u64,
218    },
219}
220
221///
222/// CommitRecoveryError
223///
224/// Current physical snapshot recovery or successor admission failure.
225///
226
227#[non_exhaustive]
228#[derive(Clone, Copy, Debug, Deserialize, Eq, thiserror::Error, PartialEq, Serialize)]
229pub enum CommitRecoveryError {
230    /// Candidate bytes exceed the bounded opaque payload ceiling.
231    #[error("committed payload length {len} exceeds byte limit {limit}")]
232    PayloadTooLarge {
233        /// Candidate byte length.
234        len: usize,
235        /// Maximum admitted byte length.
236        limit: usize,
237    },
238    /// The current snapshot is explicitly absent.
239    #[error("no committed ledger generation is present")]
240    NoValidGeneration,
241    /// The snapshot marker does not identify the current Wire format.
242    #[error("unsupported physical ledger format marker {marker:#x}")]
243    UnsupportedFormat { marker: u64 },
244    /// A present snapshot failed marker/checksum validation.
245    #[error("current ledger snapshot validation failed at generation {generation}")]
246    InvalidSnapshot {
247        /// Untrusted generation observed in the rejected snapshot.
248        generation: u64,
249    },
250    /// The current generation cannot advance.
251    #[error("committed ledger generation {generation} cannot be advanced without overflow")]
252    GenerationOverflow {
253        /// Last checked generation.
254        generation: u64,
255    },
256    /// A commit requested a generation other than the checked successor.
257    #[error("expected committed ledger generation {expected}, got {actual}")]
258    UnexpectedGeneration {
259        /// Required successor.
260        expected: u64,
261        /// Requested generation.
262        actual: u64,
263    },
264}
265
266fn generation_checksum(generation: &CommittedGenerationBytes) -> u64 {
267    let header = [
268        generation.generation.to_le_bytes(),
269        generation.commit_marker.to_le_bytes(),
270        (generation.payload.len() as u64).to_le_bytes(),
271    ];
272    let hash = fnv64(FNV_OFFSET, header.as_flattened());
273    fnv64(hash, &generation.payload)
274}
275
276#[cfg(test)]
277mod tests {
278    use super::*;
279
280    #[test]
281    fn rejected_successors_and_oversized_payloads_preserve_current_and_allow_retry() {
282        let limit = crate::constants::MAX_COMMITTED_PAYLOAD_BYTES;
283        for populated in [false, true] {
284            let mut store = crate::LedgerCommitStore::default();
285            if populated {
286                store.commit_payload_at_generation(7, vec![1]).unwrap();
287            }
288            let before = store.clone();
289            let generation = if populated { 8 } else { 0 };
290            assert_eq!(
291                store.commit_payload_at_generation(generation, vec![0; limit + 1]),
292                Err(CommitRecoveryError::PayloadTooLarge {
293                    len: limit + 1,
294                    limit
295                })
296            );
297            assert_eq!(store, before);
298            if populated {
299                assert_eq!(
300                    store.commit_payload_at_generation(9, vec![2]),
301                    Err(CommitRecoveryError::UnexpectedGeneration {
302                        expected: 8,
303                        actual: 9
304                    })
305                );
306                assert_eq!(store, before);
307            }
308            store
309                .commit_payload_at_generation(generation, vec![3])
310                .unwrap();
311            assert_eq!(store.authoritative().unwrap().generation(), generation);
312            let bytes = crate::test_wire::to_vec(&store).unwrap();
313            assert_eq!(
314                crate::test_wire::from_slice::<crate::LedgerCommitStore>(&bytes).unwrap(),
315                store
316            );
317        }
318    }
319
320    #[test]
321    fn commits_replace_the_snapshot_and_diagnostics_share_recovery() {
322        let mut store = crate::LedgerCommitStore::default();
323        assert_eq!(store.diagnostic(), CommitSnapshotDiagnostic::Empty);
324        for generation in 0..4 {
325            let payload = vec![u8::try_from(generation).unwrap(); 4];
326            store
327                .commit_payload_at_generation(generation, payload.clone())
328                .unwrap();
329            let record = store.current().unwrap();
330            assert_eq!(record.payload(), payload);
331            assert_eq!(record.commit_marker(), COMMIT_MARKER);
332            assert_eq!(record.checksum(), generation_checksum(record));
333            let diagnostic = store.diagnostic();
334            assert_eq!(diagnostic, CommitSnapshotDiagnostic::Valid { generation });
335            assert_eq!(
336                serde_json::to_value(diagnostic).unwrap(),
337                serde_json::json!({"Valid": {"generation": generation}})
338            );
339        }
340    }
341
342    #[test]
343    fn corrupt_current_fails_recovery_and_commit_without_mutation() {
344        for field in ["checksum", "marker", "payload", "generation"] {
345            let mut store = crate::LedgerCommitStore::default();
346            store
347                .commit_payload_at_generation(7, vec![1, 2, 3])
348                .unwrap();
349            let current = store.current.as_mut().unwrap();
350            match field {
351                "checksum" => current.checksum ^= 1,
352                "marker" => {
353                    current.commit_marker ^= 1;
354                    current.checksum = generation_checksum(current);
355                }
356                "payload" => current.payload[0] ^= 1,
357                _ => current.generation ^= 1,
358            }
359            let generation = current.generation();
360            let expected = if field == "marker" {
361                CommitRecoveryError::UnsupportedFormat {
362                    marker: current.commit_marker,
363                }
364            } else {
365                CommitRecoveryError::InvalidSnapshot { generation }
366            };
367            let before = store.clone();
368            assert_eq!(store.authoritative(), Err(expected));
369            assert_eq!(
370                store.diagnostic(),
371                CommitSnapshotDiagnostic::Invalid { generation }
372            );
373            assert_eq!(
374                store.commit_payload_at_generation(8, vec![4]),
375                Err(expected)
376            );
377            assert_eq!(store, before);
378        }
379    }
380
381    #[test]
382    fn physical_generation_overflow_preserves_current() {
383        let mut store = crate::LedgerCommitStore::default();
384        store
385            .commit_payload_at_generation(u64::MAX, vec![1])
386            .unwrap();
387        let before = store.clone();
388        assert_eq!(
389            store.commit_payload_at_generation(0, vec![2]),
390            Err(CommitRecoveryError::GenerationOverflow {
391                generation: u64::MAX
392            })
393        );
394        assert_eq!(store, before);
395    }
396
397    #[test]
398    fn maximum_payload_writer_reader_agree() {
399        let mut store = crate::LedgerCommitStore::default();
400        store
401            .commit_payload_at_generation(0, vec![0; crate::constants::MAX_COMMITTED_PAYLOAD_BYTES])
402            .unwrap();
403        let bytes = crate::test_wire::to_vec(&store).unwrap();
404        assert!(bytes.len() <= crate::constants::MAX_LEDGER_RECORD_BYTES);
405        let decoded: crate::LedgerCommitStore = crate::test_wire::from_slice(&bytes).unwrap();
406        assert_eq!(decoded, store);
407        assert!(decoded.authoritative().is_ok());
408        let oversized = CommittedGenerationBytes::new(
409            0,
410            vec![0; crate::constants::MAX_COMMITTED_PAYLOAD_BYTES + 1],
411        );
412        assert!(crate::test_wire::to_vec(&oversized).is_err());
413    }
414
415    #[test]
416    fn payload_uses_bounded_binary_bytes() {
417        #[derive(serde::Deserialize)]
418        struct Borrowed<'a> {
419            generation: u64,
420            commit_marker: u64,
421            checksum: u64,
422            #[serde(borrow)]
423            payload: ic_wire::BytesRef<'a, { crate::constants::MAX_COMMITTED_PAYLOAD_BYTES }>,
424        }
425        let record = CommittedGenerationBytes::new(7, vec![0, 24, 255]);
426        let bytes = crate::test_wire::to_vec(&record).unwrap();
427        let decoded: Borrowed<'_> =
428            ic_wire::native::decode(&bytes, crate::encoding::RECORD_LIMITS).unwrap();
429        assert_eq!(decoded.generation, record.generation);
430        assert_eq!(decoded.commit_marker, record.commit_marker);
431        assert_eq!(decoded.checksum, record.checksum);
432        assert_eq!(decoded.payload.as_slice(), &[0, 24, 255]);
433        assert_eq!(
434            crate::test_wire::from_slice::<CommittedGenerationBytes>(&bytes).unwrap(),
435            record
436        );
437    }
438}