1use crate::hash::{FNV_OFFSET, fnv64};
2use serde::{Deserialize, Serialize};
3
4const COMMIT_MARKER: u64 = 0x4943_4D53_4E41_5031;
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::{Deserializer, Serializer, de::Visitor};
39
40 pub fn serialize<S: Serializer>(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error> {
41 if bytes.len() > MAX_COMMITTED_PAYLOAD_BYTES {
42 return Err(serde::ser::Error::custom(
43 "ledger byte string exceeds payload bound",
44 ));
45 }
46 serializer.serialize_bytes(bytes)
47 }
48
49 pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Vec<u8>, D::Error> {
50 struct Bytes;
51 impl Visitor<'_> for Bytes {
52 type Value = Vec<u8>;
53
54 fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55 f.write_str("a bounded opaque ledger byte string")
56 }
57
58 fn visit_bytes<E: serde::de::Error>(self, bytes: &[u8]) -> Result<Self::Value, E> {
59 if bytes.len() > MAX_COMMITTED_PAYLOAD_BYTES {
60 return Err(E::custom("ledger byte string exceeds payload bound"));
61 }
62 Ok(bytes.to_vec())
63 }
64
65 fn visit_byte_buf<E: serde::de::Error>(self, bytes: Vec<u8>) -> Result<Self::Value, E> {
66 if bytes.len() > MAX_COMMITTED_PAYLOAD_BYTES {
67 return Err(E::custom("ledger byte string exceeds payload bound"));
68 }
69 Ok(bytes)
70 }
71 }
72 deserializer.deserialize_byte_buf(Bytes)
73 }
74}
75
76impl CommittedGenerationBytes {
77 #[must_use]
79 pub fn new(generation: u64, payload: Vec<u8>) -> Self {
80 let mut record = Self {
81 generation,
82 commit_marker: COMMIT_MARKER,
83 checksum: 0,
84 payload,
85 };
86 record.checksum = generation_checksum(&record);
87 record
88 }
89
90 #[must_use]
92 pub const fn generation(&self) -> u64 {
93 self.generation
94 }
95
96 #[must_use]
102 #[cfg(test)]
103 pub const fn commit_marker(&self) -> u64 {
104 self.commit_marker
105 }
106
107 #[must_use]
112 #[cfg(test)]
113 pub const fn checksum(&self) -> u64 {
114 self.checksum
115 }
116
117 #[must_use]
119 pub fn payload(&self) -> &[u8] {
120 &self.payload
121 }
122
123 #[must_use]
125 pub fn validates(&self) -> bool {
126 self.commit_marker == COMMIT_MARKER && self.checksum == generation_checksum(self)
127 }
128}
129
130impl crate::ledger::LedgerCommitStore {
131 pub(crate) const fn current(&self) -> Option<&CommittedGenerationBytes> {
133 self.current.as_ref()
134 }
135
136 pub(crate) fn authoritative(&self) -> Result<&CommittedGenerationBytes, CommitRecoveryError> {
138 let current = self
139 .current()
140 .ok_or(CommitRecoveryError::NoValidGeneration)?;
141 if current.commit_marker != COMMIT_MARKER {
142 return Err(CommitRecoveryError::UnsupportedFormat {
143 marker: current.commit_marker,
144 });
145 }
146 if !current.validates() {
147 return Err(CommitRecoveryError::InvalidSnapshot {
148 generation: current.generation(),
149 });
150 }
151 Ok(current)
152 }
153
154 pub(crate) fn authoritative_with_diagnostic(
155 &self,
156 ) -> (
157 Result<&CommittedGenerationBytes, CommitRecoveryError>,
158 CommitSnapshotDiagnostic,
159 ) {
160 let recovery = self.authoritative();
161 let current = match self.current() {
162 None => CommitSnapshotDiagnostic::Empty,
163 Some(record) if recovery.is_ok() => CommitSnapshotDiagnostic::Valid {
164 generation: record.generation(),
165 },
166 Some(record) => CommitSnapshotDiagnostic::Invalid {
167 generation: record.generation(),
168 },
169 };
170 (recovery, current)
171 }
172
173 pub(crate) fn diagnostic(&self) -> CommitSnapshotDiagnostic {
175 self.authoritative_with_diagnostic().1
176 }
177
178 pub(crate) fn commit_payload_at_generation(
181 &mut self,
182 generation: u64,
183 payload: Vec<u8>,
184 ) -> Result<&CommittedGenerationBytes, CommitRecoveryError> {
185 if payload.len() > crate::constants::MAX_COMMITTED_PAYLOAD_BYTES {
186 return Err(CommitRecoveryError::PayloadTooLarge {
187 len: payload.len(),
188 limit: crate::constants::MAX_COMMITTED_PAYLOAD_BYTES,
189 });
190 }
191 match self.authoritative() {
192 Ok(prior) => {
193 let prior_generation = prior.generation();
194 let expected = prior_generation.checked_add(1).ok_or(
195 CommitRecoveryError::GenerationOverflow {
196 generation: prior_generation,
197 },
198 )?;
199 if generation != expected {
200 return Err(CommitRecoveryError::UnexpectedGeneration {
201 expected,
202 actual: generation,
203 });
204 }
205 }
206 Err(CommitRecoveryError::NoValidGeneration) => (),
208 Err(error) => return Err(error),
209 }
210 Ok(self
211 .current
212 .insert(CommittedGenerationBytes::new(generation, payload)))
213 }
214
215 #[cfg(test)]
216 pub(crate) fn write_corrupt_snapshot(&mut self, generation: u64, payload: Vec<u8>) {
217 let mut corrupt = CommittedGenerationBytes::new(generation, payload);
218 corrupt.checksum ^= 1;
219 self.current = Some(corrupt);
220 }
221}
222
223#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
230pub enum CommitSnapshotDiagnostic {
231 Empty,
233 Valid {
235 generation: u64,
237 },
238 Invalid {
240 generation: u64,
242 },
243}
244
245#[non_exhaustive]
252#[derive(Clone, Copy, Debug, Deserialize, Eq, thiserror::Error, PartialEq, Serialize)]
253pub enum CommitRecoveryError {
254 #[error("committed payload length {len} exceeds byte limit {limit}")]
256 PayloadTooLarge {
257 len: usize,
259 limit: usize,
261 },
262 #[error("no committed ledger generation is present")]
264 NoValidGeneration,
265 #[error("unsupported physical ledger format marker {marker:#x}")]
267 UnsupportedFormat { marker: u64 },
268 #[error("current ledger snapshot validation failed at generation {generation}")]
270 InvalidSnapshot {
271 generation: u64,
273 },
274 #[error("committed ledger generation {generation} cannot be advanced without overflow")]
276 GenerationOverflow {
277 generation: u64,
279 },
280 #[error("expected committed ledger generation {expected}, got {actual}")]
282 UnexpectedGeneration {
283 expected: u64,
285 actual: u64,
287 },
288}
289
290fn generation_checksum(generation: &CommittedGenerationBytes) -> u64 {
291 let header = [
292 generation.generation.to_le_bytes(),
293 generation.commit_marker.to_le_bytes(),
294 (generation.payload.len() as u64).to_le_bytes(),
295 ];
296 let hash = fnv64(FNV_OFFSET, header.as_flattened());
297 fnv64(hash, &generation.payload)
298}
299
300#[cfg(test)]
301mod tests {
302 use super::*;
303
304 #[test]
305 fn rejected_successors_and_oversized_payloads_preserve_current_and_allow_retry() {
306 let limit = crate::constants::MAX_COMMITTED_PAYLOAD_BYTES;
307 for populated in [false, true] {
308 let mut store = crate::LedgerCommitStore::default();
309 if populated {
310 store.commit_payload_at_generation(7, vec![1]).unwrap();
311 }
312 let before = store.clone();
313 let generation = if populated { 8 } else { 0 };
314 assert_eq!(
315 store.commit_payload_at_generation(generation, vec![0; limit + 1]),
316 Err(CommitRecoveryError::PayloadTooLarge {
317 len: limit + 1,
318 limit
319 })
320 );
321 assert_eq!(store, before);
322 if populated {
323 assert_eq!(
324 store.commit_payload_at_generation(9, vec![2]),
325 Err(CommitRecoveryError::UnexpectedGeneration {
326 expected: 8,
327 actual: 9
328 })
329 );
330 assert_eq!(store, before);
331 }
332 store
333 .commit_payload_at_generation(generation, vec![3])
334 .unwrap();
335 assert_eq!(store.authoritative().unwrap().generation(), generation);
336 let bytes = crate::test_cbor::to_vec(&store).unwrap();
337 assert_eq!(
338 crate::cbor::from_slice_exact::<crate::LedgerCommitStore>(&bytes).unwrap(),
339 store
340 );
341 }
342 }
343
344 #[test]
345 fn commits_replace_the_snapshot_and_diagnostics_share_recovery() {
346 let mut store = crate::LedgerCommitStore::default();
347 assert_eq!(store.diagnostic(), CommitSnapshotDiagnostic::Empty);
348 for generation in 0..4 {
349 let payload = vec![u8::try_from(generation).unwrap(); 4];
350 store
351 .commit_payload_at_generation(generation, payload.clone())
352 .unwrap();
353 let record = store.current().unwrap();
354 assert_eq!(record.payload(), payload);
355 assert_eq!(record.commit_marker(), COMMIT_MARKER);
356 assert_eq!(record.checksum(), generation_checksum(record));
357 let diagnostic = store.diagnostic();
358 assert_eq!(diagnostic, CommitSnapshotDiagnostic::Valid { generation });
359 assert_eq!(
360 serde_json::to_value(diagnostic).unwrap(),
361 serde_json::json!({"Valid": {"generation": generation}})
362 );
363 }
364 }
365
366 #[test]
367 fn corrupt_current_fails_recovery_and_commit_without_mutation() {
368 for field in ["checksum", "marker", "payload", "generation"] {
369 let mut store = crate::LedgerCommitStore::default();
370 store
371 .commit_payload_at_generation(7, vec![1, 2, 3])
372 .unwrap();
373 let current = store.current.as_mut().unwrap();
374 match field {
375 "checksum" => current.checksum ^= 1,
376 "marker" => {
377 current.commit_marker ^= 1;
378 current.checksum = generation_checksum(current);
379 }
380 "payload" => current.payload[0] ^= 1,
381 _ => current.generation ^= 1,
382 }
383 let generation = current.generation();
384 let expected = if field == "marker" {
385 CommitRecoveryError::UnsupportedFormat {
386 marker: current.commit_marker,
387 }
388 } else {
389 CommitRecoveryError::InvalidSnapshot { generation }
390 };
391 let before = store.clone();
392 assert_eq!(store.authoritative(), Err(expected));
393 assert_eq!(
394 store.diagnostic(),
395 CommitSnapshotDiagnostic::Invalid { generation }
396 );
397 assert_eq!(
398 store.commit_payload_at_generation(8, vec![4]),
399 Err(expected)
400 );
401 assert_eq!(store, before);
402 }
403 }
404
405 #[test]
406 fn physical_generation_overflow_preserves_current() {
407 let mut store = crate::LedgerCommitStore::default();
408 store
409 .commit_payload_at_generation(u64::MAX, vec![1])
410 .unwrap();
411 let before = store.clone();
412 assert_eq!(
413 store.commit_payload_at_generation(0, vec![2]),
414 Err(CommitRecoveryError::GenerationOverflow {
415 generation: u64::MAX
416 })
417 );
418 assert_eq!(store, before);
419 }
420
421 #[test]
422 fn maximum_payload_writer_reader_agree() {
423 let mut store = crate::LedgerCommitStore::default();
424 store
425 .commit_payload_at_generation(0, vec![0; crate::constants::MAX_COMMITTED_PAYLOAD_BYTES])
426 .unwrap();
427 let bytes = crate::test_cbor::to_vec(&store).unwrap();
428 assert!(bytes.len() <= crate::constants::MAX_LEDGER_RECORD_BYTES);
429 let decoded: crate::LedgerCommitStore = crate::cbor::from_slice_exact(&bytes).unwrap();
430 assert_eq!(decoded, store);
431 assert!(decoded.authoritative().is_ok());
432 let oversized = CommittedGenerationBytes::new(
433 0,
434 vec![0; crate::constants::MAX_COMMITTED_PAYLOAD_BYTES + 1],
435 );
436 assert!(crate::test_cbor::to_vec(&oversized).is_err());
437 }
438
439 #[test]
440 fn payload_uses_bounded_binary_bytes() {
441 let record = CommittedGenerationBytes::new(7, vec![0, 24, 255]);
442 let bytes = crate::test_cbor::to_vec(&record).unwrap();
443 let value: ciborium::Value = crate::cbor::from_slice_exact(&bytes).unwrap();
444 let ciborium::Value::Map(fields) = value else {
445 panic!("record map")
446 };
447 let (_, payload) = fields
448 .iter()
449 .find(|(key, _)| key.as_text() == Some("payload"))
450 .unwrap();
451 assert_eq!(*payload, ciborium::Value::Bytes(vec![0, 24, 255]));
452 assert_eq!(
453 crate::cbor::from_slice_exact::<CommittedGenerationBytes>(&bytes).unwrap(),
454 record
455 );
456 }
457}