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
7pub trait ProtectedGenerationSlot: Eq {
16 fn generation(&self) -> u64;
18
19 fn validates(&self) -> bool;
21}
22
23pub trait DualProtectedCommitStore {
32 type Slot: ProtectedGenerationSlot;
34
35 fn slot0(&self) -> Option<&Self::Slot>;
37
38 fn slot1(&self) -> Option<&Self::Slot>;
40
41 fn is_uninitialized(&self) -> bool {
43 self.slot0().is_none() && self.slot1().is_none()
44 }
45
46 fn authoritative_slot(&self) -> Result<AuthoritativeSlot<'_, Self::Slot>, CommitRecoveryError> {
48 select_authoritative_slot(self.slot0(), self.slot1())
49 }
50
51 fn inactive_slot_index(&self) -> CommitSlotIndex {
56 match self.authoritative_slot() {
57 Ok(authoritative) => authoritative.index.opposite(),
58 Err(_) => CommitSlotIndex::Slot0,
59 }
60 }
61}
62
63#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
68pub enum CommitSlotIndex {
69 Slot0,
71 Slot1,
73}
74
75impl CommitSlotIndex {
76 #[must_use]
78 pub const fn opposite(self) -> Self {
79 match self {
80 Self::Slot0 => Self::Slot1,
81 Self::Slot1 => Self::Slot0,
82 }
83 }
84}
85
86#[derive(Clone, Copy, Debug, Eq, PartialEq)]
91pub struct AuthoritativeSlot<'slot, T> {
92 pub index: CommitSlotIndex,
94 pub record: &'slot T,
96}
97
98pub fn select_authoritative_slot<'slot, T: ProtectedGenerationSlot>(
104 slot0: Option<&'slot T>,
105 slot1: Option<&'slot T>,
106) -> Result<AuthoritativeSlot<'slot, T>, CommitRecoveryError> {
107 let slot0 = slot0
108 .filter(|slot| slot.validates())
109 .map(|record| AuthoritativeSlot {
110 index: CommitSlotIndex::Slot0,
111 record,
112 });
113 let slot1 = slot1
114 .filter(|slot| slot.validates())
115 .map(|record| AuthoritativeSlot {
116 index: CommitSlotIndex::Slot1,
117 record,
118 });
119
120 match (slot0, slot1) {
121 (Some(left), Some(right))
122 if left.record.generation() == right.record.generation()
123 && left.record != right.record =>
124 {
125 Err(CommitRecoveryError::AmbiguousGeneration {
126 generation: left.record.generation(),
127 })
128 }
129 (Some(left), Some(right)) if right.record.generation() > left.record.generation() => {
130 Ok(right)
131 }
132 (Some(left), Some(_) | None) => Ok(left),
133 (None, Some(right)) => Ok(right),
134 (None, None) => Err(CommitRecoveryError::NoValidGeneration),
135 }
136}
137
138#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
147pub struct CommittedGenerationBytes {
148 pub(crate) generation: u64,
150 pub(crate) commit_marker: u64,
152 pub(crate) checksum: u64,
154 pub(crate) payload: Vec<u8>,
156}
157
158impl CommittedGenerationBytes {
159 #[must_use]
161 pub fn new(generation: u64, payload: Vec<u8>) -> Self {
162 let mut record = Self {
163 generation,
164 commit_marker: COMMIT_MARKER,
165 checksum: 0,
166 payload,
167 };
168 record.checksum = generation_checksum(&record);
169 record
170 }
171
172 #[must_use]
174 pub const fn generation(&self) -> u64 {
175 self.generation
176 }
177
178 #[must_use]
184 pub const fn commit_marker(&self) -> u64 {
185 self.commit_marker
186 }
187
188 #[must_use]
193 pub const fn checksum(&self) -> u64 {
194 self.checksum
195 }
196
197 #[must_use]
199 pub fn payload(&self) -> &[u8] {
200 &self.payload
201 }
202
203 #[must_use]
205 pub fn validates(&self) -> bool {
206 self.commit_marker == COMMIT_MARKER && self.checksum == generation_checksum(self)
207 }
208}
209
210impl ProtectedGenerationSlot for CommittedGenerationBytes {
211 fn generation(&self) -> u64 {
212 self.generation
213 }
214
215 fn validates(&self) -> bool {
216 self.validates()
217 }
218}
219
220#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
238pub struct DualCommitStore {
239 pub(crate) slot0: Option<CommittedGenerationBytes>,
241 pub(crate) slot1: Option<CommittedGenerationBytes>,
243}
244
245impl DualCommitStore {
246 #[must_use]
248 pub const fn is_uninitialized(&self) -> bool {
249 self.slot0.is_none() && self.slot1.is_none()
250 }
251
252 #[must_use]
257 pub const fn slot0(&self) -> Option<&CommittedGenerationBytes> {
258 self.slot0.as_ref()
259 }
260
261 #[must_use]
266 pub const fn slot1(&self) -> Option<&CommittedGenerationBytes> {
267 self.slot1.as_ref()
268 }
269
270 pub fn authoritative(&self) -> Result<&CommittedGenerationBytes, CommitRecoveryError> {
272 self.authoritative_slot()
273 .map(|authoritative| authoritative.record)
274 }
275
276 #[must_use]
278 pub fn diagnostic(&self) -> CommitStoreDiagnostic {
279 CommitStoreDiagnostic::from_store(self)
280 }
281
282 pub fn commit_payload(
288 &mut self,
289 payload: Vec<u8>,
290 ) -> Result<&CommittedGenerationBytes, CommitRecoveryError> {
291 let next_generation =
292 match self.authoritative() {
293 Ok(record) => record.generation.checked_add(1).ok_or(
294 CommitRecoveryError::GenerationOverflow {
295 generation: record.generation,
296 },
297 )?,
298 Err(CommitRecoveryError::NoValidGeneration) if self.is_uninitialized() => 0,
299 Err(err) => return Err(err),
300 };
301
302 self.commit_payload_at_generation(next_generation, payload)
303 }
304
305 pub fn commit_payload_at_generation(
311 &mut self,
312 generation: u64,
313 payload: Vec<u8>,
314 ) -> Result<&CommittedGenerationBytes, CommitRecoveryError> {
315 match self.authoritative() {
316 Ok(record) => {
317 let expected = record.generation.checked_add(1).ok_or(
318 CommitRecoveryError::GenerationOverflow {
319 generation: record.generation,
320 },
321 )?;
322 if generation != expected {
323 return Err(CommitRecoveryError::UnexpectedGeneration {
324 expected,
325 actual: generation,
326 });
327 }
328 }
329 Err(CommitRecoveryError::NoValidGeneration) if self.is_uninitialized() => {}
330 Err(err) => return Err(err),
331 }
332
333 let next = CommittedGenerationBytes::new(generation, payload);
334
335 if self.inactive_slot_index() == CommitSlotIndex::Slot0 {
336 self.slot0 = Some(next);
337 } else {
338 self.slot1 = Some(next);
339 }
340
341 self.authoritative()
342 }
343
344 #[cfg(test)]
349 pub fn write_corrupt_inactive_slot(&mut self, generation: u64, payload: Vec<u8>) {
350 let mut corrupt = CommittedGenerationBytes::new(generation, payload);
351 corrupt.checksum = corrupt.checksum.wrapping_add(1);
352
353 if self.inactive_slot_index() == CommitSlotIndex::Slot0 {
354 self.slot0 = Some(corrupt);
355 } else {
356 self.slot1 = Some(corrupt);
357 }
358 }
359}
360
361impl DualProtectedCommitStore for DualCommitStore {
362 type Slot = CommittedGenerationBytes;
363
364 fn slot0(&self) -> Option<&Self::Slot> {
365 self.slot0.as_ref()
366 }
367
368 fn slot1(&self) -> Option<&Self::Slot> {
369 self.slot1.as_ref()
370 }
371}
372
373#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
378pub struct CommitStoreDiagnostic {
379 pub slot0: CommitSlotDiagnostic,
381 pub slot1: CommitSlotDiagnostic,
383 pub authoritative_generation: Option<u64>,
385 pub recovery_error: Option<CommitRecoveryError>,
387}
388
389impl CommitStoreDiagnostic {
390 #[must_use]
392 pub fn from_store<S: DualProtectedCommitStore>(store: &S) -> Self {
393 let (authoritative_generation, recovery_error) = match store.authoritative_slot() {
394 Ok(slot) => (Some(slot.record.generation()), None),
395 Err(err) => (None, Some(err)),
396 };
397 Self {
398 slot0: CommitSlotDiagnostic::from_slot(store.slot0()),
399 slot1: CommitSlotDiagnostic::from_slot(store.slot1()),
400 authoritative_generation,
401 recovery_error,
402 }
403 }
404}
405
406#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
411pub struct CommitSlotDiagnostic {
412 pub present: bool,
414 pub generation: Option<u64>,
416 pub valid: bool,
418}
419
420impl CommitSlotDiagnostic {
421 fn from_slot<T: ProtectedGenerationSlot>(slot: Option<&T>) -> Self {
422 match slot {
423 Some(record) => Self {
424 present: true,
425 generation: Some(record.generation()),
426 valid: record.validates(),
427 },
428 None => Self {
429 present: false,
430 generation: None,
431 valid: false,
432 },
433 }
434 }
435}
436
437#[derive(Clone, Copy, Debug, Deserialize, Eq, thiserror::Error, PartialEq, Serialize)]
442pub enum CommitRecoveryError {
443 #[error("no valid committed ledger generation")]
445 NoValidGeneration,
446 #[error("ambiguous committed ledger generation {generation}")]
448 AmbiguousGeneration {
449 generation: u64,
451 },
452 #[error("committed ledger generation {generation} cannot be advanced without overflow")]
454 GenerationOverflow {
455 generation: u64,
457 },
458 #[error("expected committed ledger generation {expected}, got {actual}")]
460 UnexpectedGeneration {
461 expected: u64,
463 actual: u64,
465 },
466}
467
468fn generation_checksum(generation: &CommittedGenerationBytes) -> u64 {
469 let mut hash = FNV_OFFSET;
470 hash = hash_u64(hash, generation.generation);
471 hash = hash_u64(hash, generation.commit_marker);
472 hash = hash_usize(hash, generation.payload.len());
473 for byte in &generation.payload {
474 hash = hash_byte(hash, *byte);
475 }
476 hash
477}
478
479fn hash_usize(hash: u64, value: usize) -> u64 {
480 hash_u64(hash, value as u64)
481}
482
483fn hash_u64(mut hash: u64, value: u64) -> u64 {
484 for byte in value.to_le_bytes() {
485 hash = hash_byte(hash, byte);
486 }
487 hash
488}
489
490const fn hash_byte(hash: u64, byte: u8) -> u64 {
491 (hash ^ byte as u64).wrapping_mul(FNV_PRIME)
492}
493
494#[cfg(test)]
495mod tests {
496 use super::*;
497
498 fn payload(value: u8) -> Vec<u8> {
499 vec![value; 4]
500 }
501
502 #[test]
503 fn committed_generation_validates_marker_and_checksum() {
504 let mut generation = CommittedGenerationBytes::new(7, payload(1));
505 assert!(generation.validates());
506
507 generation.checksum = generation.checksum.wrapping_add(1);
508 assert!(!generation.validates());
509 }
510
511 #[test]
512 fn physical_commit_accessors_expose_read_only_state() {
513 let mut store = DualCommitStore::default();
514 store.commit_payload(payload(1)).expect("first commit");
515
516 let slot = store.slot0().expect("first slot");
517
518 assert_eq!(slot.generation(), 0);
519 assert_eq!(slot.payload(), payload(1).as_slice());
520 assert_eq!(slot.commit_marker(), COMMIT_MARKER);
521 assert_eq!(slot.checksum(), generation_checksum(slot));
522 assert!(store.slot1().is_none());
523 }
524
525 #[test]
526 fn authoritative_selects_highest_valid_generation() {
527 let mut store = DualCommitStore::default();
528 store.commit_payload(payload(1)).expect("first commit");
529 store.commit_payload(payload(2)).expect("second commit");
530
531 let authoritative = store.authoritative().expect("authoritative");
532 let authoritative_slot =
533 select_authoritative_slot(store.slot0.as_ref(), store.slot1.as_ref())
534 .expect("authoritative slot");
535
536 assert_eq!(authoritative.generation, 1);
537 assert_eq!(authoritative.payload, payload(2));
538 assert_eq!(authoritative_slot.index, CommitSlotIndex::Slot1);
539 assert_eq!(authoritative_slot.record.payload, payload(2));
540 }
541
542 #[test]
543 fn corrupt_newer_slot_leaves_prior_generation_authoritative() {
544 let mut store = DualCommitStore::default();
545 store.commit_payload(payload(1)).expect("first commit");
546 store.write_corrupt_inactive_slot(1, payload(2));
547
548 let authoritative = store.authoritative().expect("authoritative");
549
550 assert_eq!(authoritative.generation, 0);
551 assert_eq!(authoritative.payload, payload(1));
552 }
553
554 #[test]
555 fn no_valid_generation_fails_closed() {
556 let mut store = DualCommitStore::default();
557 store.write_corrupt_inactive_slot(0, payload(1));
558 store.write_corrupt_inactive_slot(1, payload(2));
559
560 let err = store.authoritative().expect_err("no valid slot");
561
562 assert_eq!(err, CommitRecoveryError::NoValidGeneration);
563 }
564
565 #[test]
566 fn same_generation_identical_slots_recover_deterministically() {
567 let committed = CommittedGenerationBytes::new(7, payload(1));
568 let store = DualCommitStore {
569 slot0: Some(committed.clone()),
570 slot1: Some(committed),
571 };
572
573 let authoritative = store.authoritative_slot().expect("authoritative");
574
575 assert_eq!(authoritative.index, CommitSlotIndex::Slot0);
576 assert_eq!(authoritative.record.generation, 7);
577 }
578
579 #[test]
580 fn same_generation_divergent_slots_fail_closed() {
581 let store = DualCommitStore {
582 slot0: Some(CommittedGenerationBytes::new(7, payload(1))),
583 slot1: Some(CommittedGenerationBytes::new(7, payload(2))),
584 };
585
586 let err = store.authoritative().expect_err("ambiguous generation");
587
588 assert_eq!(
589 err,
590 CommitRecoveryError::AmbiguousGeneration { generation: 7 }
591 );
592 }
593
594 #[test]
595 fn physical_generation_overflow_fails_closed() {
596 let mut store = DualCommitStore {
597 slot0: Some(CommittedGenerationBytes::new(u64::MAX, payload(1))),
598 slot1: None,
599 };
600
601 let err = store
602 .commit_payload(payload(2))
603 .expect_err("overflow must fail");
604
605 assert_eq!(
606 err,
607 CommitRecoveryError::GenerationOverflow {
608 generation: u64::MAX
609 }
610 );
611 }
612
613 #[test]
614 fn diagnostic_reports_authoritative_generation_and_corrupt_slots() {
615 let mut store = DualCommitStore::default();
616 store.commit_payload(payload(1)).expect("first commit");
617 store.write_corrupt_inactive_slot(1, payload(2));
618
619 let diagnostic = store.diagnostic();
620
621 assert_eq!(diagnostic.authoritative_generation, Some(0));
622 assert_eq!(diagnostic.recovery_error, None);
623 assert_eq!(diagnostic.slot0.generation, Some(0));
624 assert!(diagnostic.slot0.valid);
625 assert_eq!(diagnostic.slot1.generation, Some(1));
626 assert!(!diagnostic.slot1.valid);
627 }
628
629 #[test]
630 fn diagnostic_reports_no_valid_generation_for_empty_store() {
631 let diagnostic = DualCommitStore::default().diagnostic();
632
633 assert_eq!(diagnostic.authoritative_generation, None);
634 assert_eq!(
635 diagnostic.recovery_error,
636 Some(CommitRecoveryError::NoValidGeneration)
637 );
638 assert!(!diagnostic.slot0.present);
639 assert!(!diagnostic.slot1.present);
640 }
641
642 #[test]
643 fn diagnostic_builds_from_any_dual_protected_store() {
644 #[derive(Eq, PartialEq)]
645 struct TestSlot {
646 generation: u64,
647 valid: bool,
648 }
649
650 impl ProtectedGenerationSlot for TestSlot {
651 fn generation(&self) -> u64 {
652 self.generation
653 }
654
655 fn validates(&self) -> bool {
656 self.valid
657 }
658 }
659
660 struct TestStore {
661 slot0: Option<TestSlot>,
662 slot1: Option<TestSlot>,
663 }
664
665 impl DualProtectedCommitStore for TestStore {
666 type Slot = TestSlot;
667
668 fn slot0(&self) -> Option<&Self::Slot> {
669 self.slot0.as_ref()
670 }
671
672 fn slot1(&self) -> Option<&Self::Slot> {
673 self.slot1.as_ref()
674 }
675 }
676
677 let diagnostic = CommitStoreDiagnostic::from_store(&TestStore {
678 slot0: Some(TestSlot {
679 generation: 8,
680 valid: true,
681 }),
682 slot1: Some(TestSlot {
683 generation: 9,
684 valid: false,
685 }),
686 });
687
688 assert_eq!(diagnostic.authoritative_generation, Some(8));
689 assert!(diagnostic.slot0.valid);
690 assert_eq!(diagnostic.slot1.generation, Some(9));
691 assert!(!diagnostic.slot1.valid);
692 }
693
694 #[test]
695 fn uninitialized_distinguishes_empty_from_corrupt() {
696 let mut store = DualCommitStore::default();
697 assert!(store.is_uninitialized());
698
699 store.write_corrupt_inactive_slot(0, payload(1));
700
701 assert!(!store.is_uninitialized());
702 }
703
704 #[test]
705 fn commit_after_corrupt_slot_advances_from_prior_valid_generation() {
706 let mut store = DualCommitStore::default();
707 store.commit_payload(payload(1)).expect("first commit");
708 store.write_corrupt_inactive_slot(1, payload(2));
709 store.commit_payload(payload(3)).expect("third commit");
710
711 let authoritative = store.authoritative().expect("authoritative");
712
713 assert_eq!(authoritative.generation, 1);
714 assert_eq!(authoritative.payload, payload(3));
715 }
716}