1use serde::{Deserialize, Serialize};
2
3const COMMIT_MARKER: u64 = 0x4943_4D45_4D43_4F4D;
4const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
5const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
6
7#[derive(Clone, Copy, Debug, Eq, PartialEq)]
8enum CommitSlotIndex {
9 Slot0,
10 Slot1,
11}
12
13impl CommitSlotIndex {
14 const fn opposite(self) -> Self {
15 match self {
16 Self::Slot0 => Self::Slot1,
17 Self::Slot1 => Self::Slot0,
18 }
19 }
20}
21
22#[derive(Clone, Copy, Debug, Eq, PartialEq)]
23struct AuthoritativeSlot<'slot> {
24 index: CommitSlotIndex,
25 record: &'slot CommittedGenerationBytes,
26}
27
28fn select_authoritative_slot<'slot>(
29 slot0: Option<&'slot CommittedGenerationBytes>,
30 slot1: Option<&'slot CommittedGenerationBytes>,
31) -> Result<AuthoritativeSlot<'slot>, CommitRecoveryError> {
32 let slot0 = slot0
33 .filter(|slot| slot.validates())
34 .map(|record| AuthoritativeSlot {
35 index: CommitSlotIndex::Slot0,
36 record,
37 });
38 let slot1 = slot1
39 .filter(|slot| slot.validates())
40 .map(|record| AuthoritativeSlot {
41 index: CommitSlotIndex::Slot1,
42 record,
43 });
44
45 match (slot0, slot1) {
46 (Some(left), Some(right))
47 if left.record.generation() == right.record.generation()
48 && left.record != right.record =>
49 {
50 Err(CommitRecoveryError::AmbiguousGeneration {
51 generation: left.record.generation(),
52 })
53 }
54 (Some(left), Some(right)) if right.record.generation() > left.record.generation() => {
55 Ok(right)
56 }
57 (Some(left), Some(_) | None) => Ok(left),
58 (None, Some(right)) => Ok(right),
59 (None, None) => Err(CommitRecoveryError::NoValidGeneration),
60 }
61}
62
63#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
72#[serde(deny_unknown_fields)]
73pub struct CommittedGenerationBytes {
74 pub(crate) generation: u64,
76 pub(crate) commit_marker: u64,
78 pub(crate) checksum: u64,
80 pub(crate) payload: Vec<u8>,
82}
83
84impl CommittedGenerationBytes {
85 #[must_use]
87 pub fn new(generation: u64, payload: Vec<u8>) -> Self {
88 let mut record = Self {
89 generation,
90 commit_marker: COMMIT_MARKER,
91 checksum: 0,
92 payload,
93 };
94 record.checksum = generation_checksum(&record);
95 record
96 }
97
98 #[must_use]
100 pub const fn generation(&self) -> u64 {
101 self.generation
102 }
103
104 #[must_use]
110 pub const fn commit_marker(&self) -> u64 {
111 self.commit_marker
112 }
113
114 #[must_use]
119 pub const fn checksum(&self) -> u64 {
120 self.checksum
121 }
122
123 #[must_use]
125 pub fn payload(&self) -> &[u8] {
126 &self.payload
127 }
128
129 #[must_use]
131 pub fn validates(&self) -> bool {
132 self.commit_marker == COMMIT_MARKER && self.checksum == generation_checksum(self)
133 }
134}
135
136#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
156#[serde(deny_unknown_fields)]
157pub struct DualCommitStore {
158 pub(crate) slot0: Option<CommittedGenerationBytes>,
160 pub(crate) slot1: Option<CommittedGenerationBytes>,
162}
163
164impl DualCommitStore {
165 #[must_use]
167 pub const fn is_uninitialized(&self) -> bool {
168 self.slot0.is_none() && self.slot1.is_none()
169 }
170
171 #[must_use]
176 pub const fn slot0(&self) -> Option<&CommittedGenerationBytes> {
177 self.slot0.as_ref()
178 }
179
180 #[must_use]
185 pub const fn slot1(&self) -> Option<&CommittedGenerationBytes> {
186 self.slot1.as_ref()
187 }
188
189 fn authoritative_slot(&self) -> Result<AuthoritativeSlot<'_>, CommitRecoveryError> {
190 select_authoritative_slot(self.slot0(), self.slot1())
191 }
192
193 fn inactive_slot_index(&self) -> CommitSlotIndex {
194 match self.authoritative_slot() {
195 Ok(authoritative) => authoritative.index.opposite(),
196 Err(_) => CommitSlotIndex::Slot0,
197 }
198 }
199
200 pub fn authoritative(&self) -> Result<&CommittedGenerationBytes, CommitRecoveryError> {
202 self.authoritative_slot()
203 .map(|authoritative| authoritative.record)
204 }
205
206 #[must_use]
208 pub fn diagnostic(&self) -> CommitStoreDiagnostic {
209 CommitStoreDiagnostic::from_store(self)
210 }
211
212 pub fn commit_payload(
217 &mut self,
218 payload: Vec<u8>,
219 ) -> Result<&CommittedGenerationBytes, CommitRecoveryError> {
220 let next_generation =
221 match self.authoritative() {
222 Ok(record) => record.generation.checked_add(1).ok_or(
223 CommitRecoveryError::GenerationOverflow {
224 generation: record.generation,
225 },
226 )?,
227 Err(CommitRecoveryError::NoValidGeneration) if self.is_uninitialized() => 0,
228 Err(err) => return Err(err),
229 };
230
231 self.commit_payload_at_generation(next_generation, payload)
232 }
233
234 pub fn commit_payload_at_generation(
245 &mut self,
246 generation: u64,
247 payload: Vec<u8>,
248 ) -> Result<&CommittedGenerationBytes, CommitRecoveryError> {
249 match self.authoritative() {
250 Ok(record) => {
251 let expected = record.generation.checked_add(1).ok_or(
252 CommitRecoveryError::GenerationOverflow {
253 generation: record.generation,
254 },
255 )?;
256 if generation != expected {
257 return Err(CommitRecoveryError::UnexpectedGeneration {
258 expected,
259 actual: generation,
260 });
261 }
262 }
263 Err(CommitRecoveryError::NoValidGeneration) if self.is_uninitialized() => {}
264 Err(err) => return Err(err),
265 }
266
267 let next = CommittedGenerationBytes::new(generation, payload);
268
269 if self.inactive_slot_index() == CommitSlotIndex::Slot0 {
270 self.slot0 = Some(next);
271 } else {
272 self.slot1 = Some(next);
273 }
274
275 self.authoritative()
276 }
277
278 #[cfg(test)]
283 pub fn write_corrupt_inactive_slot(&mut self, generation: u64, payload: Vec<u8>) {
284 let mut corrupt = CommittedGenerationBytes::new(generation, payload);
285 corrupt.checksum = corrupt.checksum.wrapping_add(1);
286
287 if self.inactive_slot_index() == CommitSlotIndex::Slot0 {
288 self.slot0 = Some(corrupt);
289 } else {
290 self.slot1 = Some(corrupt);
291 }
292 }
293}
294
295#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
300#[serde(deny_unknown_fields)]
301pub struct CommitStoreDiagnostic {
302 pub slot0: CommitSlotDiagnostic,
304 pub slot1: CommitSlotDiagnostic,
306 pub authoritative_generation: Option<u64>,
308 pub recovery_error: Option<CommitRecoveryError>,
310}
311
312impl CommitStoreDiagnostic {
313 #[must_use]
315 pub fn from_store(store: &DualCommitStore) -> Self {
316 let (authoritative_generation, recovery_error) = match store.authoritative_slot() {
317 Ok(slot) => (Some(slot.record.generation()), None),
318 Err(err) => (None, Some(err)),
319 };
320 Self {
321 slot0: CommitSlotDiagnostic::from_slot(store.slot0()),
322 slot1: CommitSlotDiagnostic::from_slot(store.slot1()),
323 authoritative_generation,
324 recovery_error,
325 }
326 }
327}
328
329#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
334#[serde(deny_unknown_fields)]
335pub struct CommitSlotDiagnostic {
336 pub present: bool,
338 pub generation: Option<u64>,
340 pub valid: bool,
342}
343
344impl CommitSlotDiagnostic {
345 fn from_slot(slot: Option<&CommittedGenerationBytes>) -> Self {
346 match slot {
347 Some(record) => Self {
348 present: true,
349 generation: Some(record.generation()),
350 valid: record.validates(),
351 },
352 None => Self {
353 present: false,
354 generation: None,
355 valid: false,
356 },
357 }
358 }
359}
360
361#[non_exhaustive]
366#[derive(Clone, Copy, Debug, Deserialize, Eq, thiserror::Error, PartialEq, Serialize)]
367pub enum CommitRecoveryError {
368 #[error("no valid committed ledger generation")]
370 NoValidGeneration,
371 #[error("ambiguous committed ledger generation {generation}")]
373 AmbiguousGeneration {
374 generation: u64,
376 },
377 #[error("committed ledger generation {generation} cannot be advanced without overflow")]
379 GenerationOverflow {
380 generation: u64,
382 },
383 #[error("expected committed ledger generation {expected}, got {actual}")]
385 UnexpectedGeneration {
386 expected: u64,
388 actual: u64,
390 },
391}
392
393fn generation_checksum(generation: &CommittedGenerationBytes) -> u64 {
394 let mut hash = FNV_OFFSET;
395 hash = hash_u64(hash, generation.generation);
396 hash = hash_u64(hash, generation.commit_marker);
397 hash = hash_usize(hash, generation.payload.len());
398 for byte in &generation.payload {
399 hash = hash_byte(hash, *byte);
400 }
401 hash
402}
403
404fn hash_usize(hash: u64, value: usize) -> u64 {
405 hash_u64(hash, value as u64)
406}
407
408fn hash_u64(mut hash: u64, value: u64) -> u64 {
409 for byte in value.to_le_bytes() {
410 hash = hash_byte(hash, byte);
411 }
412 hash
413}
414
415const fn hash_byte(hash: u64, byte: u8) -> u64 {
416 (hash ^ byte as u64).wrapping_mul(FNV_PRIME)
417}
418
419#[cfg(test)]
420mod tests {
421 use super::*;
422
423 fn payload(value: u8) -> Vec<u8> {
424 vec![value; 4]
425 }
426
427 #[test]
428 fn committed_generation_validates_marker_and_checksum() {
429 let mut generation = CommittedGenerationBytes::new(7, payload(1));
430 assert!(generation.validates());
431
432 generation.checksum = generation.checksum.wrapping_add(1);
433 assert!(!generation.validates());
434 }
435
436 #[test]
437 fn physical_commit_accessors_expose_read_only_state() {
438 let mut store = DualCommitStore::default();
439 store.commit_payload(payload(1)).expect("first commit");
440
441 let slot = store.slot0().expect("first slot");
442
443 assert_eq!(slot.generation(), 0);
444 assert_eq!(slot.payload(), payload(1).as_slice());
445 assert_eq!(slot.commit_marker(), COMMIT_MARKER);
446 assert_eq!(slot.checksum(), generation_checksum(slot));
447 assert!(store.slot1().is_none());
448 }
449
450 #[test]
451 fn authoritative_selects_highest_valid_generation() {
452 let mut store = DualCommitStore::default();
453 store.commit_payload(payload(1)).expect("first commit");
454 store.commit_payload(payload(2)).expect("second commit");
455
456 let authoritative = store.authoritative().expect("authoritative");
457 let authoritative_slot =
458 select_authoritative_slot(store.slot0.as_ref(), store.slot1.as_ref())
459 .expect("authoritative slot");
460
461 assert_eq!(authoritative.generation, 1);
462 assert_eq!(authoritative.payload, payload(2));
463 assert_eq!(authoritative_slot.index, CommitSlotIndex::Slot1);
464 assert_eq!(authoritative_slot.record.payload, payload(2));
465 }
466
467 #[test]
468 fn corrupt_newer_slot_leaves_prior_generation_authoritative() {
469 let mut store = DualCommitStore::default();
470 store.commit_payload(payload(1)).expect("first commit");
471 store.write_corrupt_inactive_slot(1, payload(2));
472
473 let authoritative = store.authoritative().expect("authoritative");
474
475 assert_eq!(authoritative.generation, 0);
476 assert_eq!(authoritative.payload, payload(1));
477 }
478
479 #[test]
480 fn no_valid_generation_fails_closed() {
481 let mut store = DualCommitStore::default();
482 store.write_corrupt_inactive_slot(0, payload(1));
483 store.write_corrupt_inactive_slot(1, payload(2));
484
485 let err = store.authoritative().expect_err("no valid slot");
486
487 assert_eq!(err, CommitRecoveryError::NoValidGeneration);
488 }
489
490 #[test]
491 fn same_generation_identical_slots_recover_deterministically() {
492 let committed = CommittedGenerationBytes::new(7, payload(1));
493 let store = DualCommitStore {
494 slot0: Some(committed.clone()),
495 slot1: Some(committed),
496 };
497
498 let authoritative = store.authoritative_slot().expect("authoritative");
499
500 assert_eq!(authoritative.index, CommitSlotIndex::Slot0);
501 assert_eq!(authoritative.record.generation, 7);
502 }
503
504 #[test]
505 fn same_generation_divergent_slots_fail_closed() {
506 let store = DualCommitStore {
507 slot0: Some(CommittedGenerationBytes::new(7, payload(1))),
508 slot1: Some(CommittedGenerationBytes::new(7, payload(2))),
509 };
510
511 let err = store.authoritative().expect_err("ambiguous generation");
512
513 assert_eq!(
514 err,
515 CommitRecoveryError::AmbiguousGeneration { generation: 7 }
516 );
517 }
518
519 #[test]
520 fn physical_generation_overflow_fails_closed() {
521 let mut store = DualCommitStore {
522 slot0: Some(CommittedGenerationBytes::new(u64::MAX, payload(1))),
523 slot1: None,
524 };
525
526 let err = store
527 .commit_payload(payload(2))
528 .expect_err("overflow must fail");
529
530 assert_eq!(
531 err,
532 CommitRecoveryError::GenerationOverflow {
533 generation: u64::MAX
534 }
535 );
536 }
537
538 #[test]
539 fn diagnostic_reports_authoritative_generation_and_corrupt_slots() {
540 let mut store = DualCommitStore::default();
541 store.commit_payload(payload(1)).expect("first commit");
542 store.write_corrupt_inactive_slot(1, payload(2));
543
544 let diagnostic = store.diagnostic();
545
546 assert_eq!(diagnostic.authoritative_generation, Some(0));
547 assert_eq!(diagnostic.recovery_error, None);
548 assert_eq!(diagnostic.slot0.generation, Some(0));
549 assert!(diagnostic.slot0.valid);
550 assert_eq!(diagnostic.slot1.generation, Some(1));
551 assert!(!diagnostic.slot1.valid);
552 }
553
554 #[test]
555 fn diagnostic_reports_no_valid_generation_for_empty_store() {
556 let diagnostic = DualCommitStore::default().diagnostic();
557
558 assert_eq!(diagnostic.authoritative_generation, None);
559 assert_eq!(
560 diagnostic.recovery_error,
561 Some(CommitRecoveryError::NoValidGeneration)
562 );
563 assert!(!diagnostic.slot0.present);
564 assert!(!diagnostic.slot1.present);
565 }
566
567 #[test]
568 fn uninitialized_distinguishes_empty_from_corrupt() {
569 let mut store = DualCommitStore::default();
570 assert!(store.is_uninitialized());
571
572 store.write_corrupt_inactive_slot(0, payload(1));
573
574 assert!(!store.is_uninitialized());
575 }
576
577 #[test]
578 fn commit_after_corrupt_slot_advances_from_prior_valid_generation() {
579 let mut store = DualCommitStore::default();
580 store.commit_payload(payload(1)).expect("first commit");
581 store.write_corrupt_inactive_slot(1, payload(2));
582 store.commit_payload(payload(3)).expect("third commit");
583
584 let authoritative = store.authoritative().expect("authoritative");
585
586 assert_eq!(authoritative.generation, 1);
587 assert_eq!(authoritative.payload, payload(3));
588 }
589}