1use crate::hash::{FNV_OFFSET, fnv64};
2use serde::{Deserialize, Serialize};
3
4const COMMIT_MARKER: u64 = 0x4943_4D57_4952_4531;
6
7#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
22#[serde(deny_unknown_fields)]
23pub struct CommittedGenerationBytes {
24 pub(crate) generation: u64,
26 pub(crate) commit_marker: u64,
28 pub(crate) checksum: u64,
30 #[serde(with = "opaque_payload")]
32 pub(crate) payload: Vec<u8>,
33}
34
35mod 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 #[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 #[must_use]
68 pub const fn generation(&self) -> u64 {
69 self.generation
70 }
71
72 #[must_use]
78 #[cfg(test)]
79 pub const fn commit_marker(&self) -> u64 {
80 self.commit_marker
81 }
82
83 #[must_use]
88 #[cfg(test)]
89 pub const fn checksum(&self) -> u64 {
90 self.checksum
91 }
92
93 #[must_use]
95 pub fn payload(&self) -> &[u8] {
96 &self.payload
97 }
98
99 #[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 pub(crate) const fn current(&self) -> Option<&CommittedGenerationBytes> {
109 self.current.as_ref()
110 }
111
112 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 pub(crate) fn diagnostic(&self) -> CommitSnapshotDiagnostic {
151 self.authoritative_with_diagnostic().1
152 }
153
154 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 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#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
206pub enum CommitSnapshotDiagnostic {
207 Empty,
209 Valid {
211 generation: u64,
213 },
214 Invalid {
216 generation: u64,
218 },
219}
220
221#[non_exhaustive]
228#[derive(Clone, Copy, Debug, Deserialize, Eq, thiserror::Error, PartialEq, Serialize)]
229pub enum CommitRecoveryError {
230 #[error("committed payload length {len} exceeds byte limit {limit}")]
232 PayloadTooLarge {
233 len: usize,
235 limit: usize,
237 },
238 #[error("no committed ledger generation is present")]
240 NoValidGeneration,
241 #[error("unsupported physical ledger format marker {marker:#x}")]
243 UnsupportedFormat { marker: u64 },
244 #[error("current ledger snapshot validation failed at generation {generation}")]
246 InvalidSnapshot {
247 generation: u64,
249 },
250 #[error("committed ledger generation {generation} cannot be advanced without overflow")]
252 GenerationOverflow {
253 generation: u64,
255 },
256 #[error("expected committed ledger generation {expected}, got {actual}")]
258 UnexpectedGeneration {
259 expected: u64,
261 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}