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)]
147#[serde(deny_unknown_fields)]
148pub struct CommittedGenerationBytes {
149 pub(crate) generation: u64,
151 pub(crate) commit_marker: u64,
153 pub(crate) checksum: u64,
155 pub(crate) payload: Vec<u8>,
157}
158
159impl CommittedGenerationBytes {
160 #[must_use]
162 pub fn new(generation: u64, payload: Vec<u8>) -> Self {
163 let mut record = Self {
164 generation,
165 commit_marker: COMMIT_MARKER,
166 checksum: 0,
167 payload,
168 };
169 record.checksum = generation_checksum(&record);
170 record
171 }
172
173 #[must_use]
175 pub const fn generation(&self) -> u64 {
176 self.generation
177 }
178
179 #[must_use]
185 pub const fn commit_marker(&self) -> u64 {
186 self.commit_marker
187 }
188
189 #[must_use]
194 pub const fn checksum(&self) -> u64 {
195 self.checksum
196 }
197
198 #[must_use]
200 pub fn payload(&self) -> &[u8] {
201 &self.payload
202 }
203
204 #[must_use]
206 pub fn validates(&self) -> bool {
207 self.commit_marker == COMMIT_MARKER && self.checksum == generation_checksum(self)
208 }
209}
210
211impl ProtectedGenerationSlot for CommittedGenerationBytes {
212 fn generation(&self) -> u64 {
213 self.generation
214 }
215
216 fn validates(&self) -> bool {
217 self.validates()
218 }
219}
220
221#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
238#[serde(deny_unknown_fields)]
239pub struct DualCommitStore {
240 pub(crate) slot0: Option<CommittedGenerationBytes>,
242 pub(crate) slot1: Option<CommittedGenerationBytes>,
244}
245
246impl DualCommitStore {
247 #[must_use]
249 pub const fn is_uninitialized(&self) -> bool {
250 self.slot0.is_none() && self.slot1.is_none()
251 }
252
253 #[must_use]
258 pub const fn slot0(&self) -> Option<&CommittedGenerationBytes> {
259 self.slot0.as_ref()
260 }
261
262 #[must_use]
267 pub const fn slot1(&self) -> Option<&CommittedGenerationBytes> {
268 self.slot1.as_ref()
269 }
270
271 pub fn authoritative(&self) -> Result<&CommittedGenerationBytes, CommitRecoveryError> {
273 self.authoritative_slot()
274 .map(|authoritative| authoritative.record)
275 }
276
277 #[must_use]
279 pub fn diagnostic(&self) -> CommitStoreDiagnostic {
280 CommitStoreDiagnostic::from_store(self)
281 }
282
283 pub fn commit_payload(
289 &mut self,
290 payload: Vec<u8>,
291 ) -> Result<&CommittedGenerationBytes, CommitRecoveryError> {
292 let next_generation =
293 match self.authoritative() {
294 Ok(record) => record.generation.checked_add(1).ok_or(
295 CommitRecoveryError::GenerationOverflow {
296 generation: record.generation,
297 },
298 )?,
299 Err(CommitRecoveryError::NoValidGeneration) if self.is_uninitialized() => 0,
300 Err(err) => return Err(err),
301 };
302
303 self.commit_payload_at_generation(next_generation, payload)
304 }
305
306 pub fn commit_payload_at_generation(
317 &mut self,
318 generation: u64,
319 payload: Vec<u8>,
320 ) -> Result<&CommittedGenerationBytes, CommitRecoveryError> {
321 match self.authoritative() {
322 Ok(record) => {
323 let expected = record.generation.checked_add(1).ok_or(
324 CommitRecoveryError::GenerationOverflow {
325 generation: record.generation,
326 },
327 )?;
328 if generation != expected {
329 return Err(CommitRecoveryError::UnexpectedGeneration {
330 expected,
331 actual: generation,
332 });
333 }
334 }
335 Err(CommitRecoveryError::NoValidGeneration) if self.is_uninitialized() => {}
336 Err(err) => return Err(err),
337 }
338
339 let next = CommittedGenerationBytes::new(generation, payload);
340
341 if self.inactive_slot_index() == CommitSlotIndex::Slot0 {
342 self.slot0 = Some(next);
343 } else {
344 self.slot1 = Some(next);
345 }
346
347 self.authoritative()
348 }
349
350 #[cfg(test)]
355 pub fn write_corrupt_inactive_slot(&mut self, generation: u64, payload: Vec<u8>) {
356 let mut corrupt = CommittedGenerationBytes::new(generation, payload);
357 corrupt.checksum = corrupt.checksum.wrapping_add(1);
358
359 if self.inactive_slot_index() == CommitSlotIndex::Slot0 {
360 self.slot0 = Some(corrupt);
361 } else {
362 self.slot1 = Some(corrupt);
363 }
364 }
365}
366
367impl DualProtectedCommitStore for DualCommitStore {
368 type Slot = CommittedGenerationBytes;
369
370 fn slot0(&self) -> Option<&Self::Slot> {
371 self.slot0.as_ref()
372 }
373
374 fn slot1(&self) -> Option<&Self::Slot> {
375 self.slot1.as_ref()
376 }
377}
378
379#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
384#[serde(deny_unknown_fields)]
385pub struct CommitStoreDiagnostic {
386 pub slot0: CommitSlotDiagnostic,
388 pub slot1: CommitSlotDiagnostic,
390 pub authoritative_generation: Option<u64>,
392 pub recovery_error: Option<CommitRecoveryError>,
394}
395
396impl CommitStoreDiagnostic {
397 #[must_use]
399 pub fn from_store<S: DualProtectedCommitStore>(store: &S) -> Self {
400 let (authoritative_generation, recovery_error) = match store.authoritative_slot() {
401 Ok(slot) => (Some(slot.record.generation()), None),
402 Err(err) => (None, Some(err)),
403 };
404 Self {
405 slot0: CommitSlotDiagnostic::from_slot(store.slot0()),
406 slot1: CommitSlotDiagnostic::from_slot(store.slot1()),
407 authoritative_generation,
408 recovery_error,
409 }
410 }
411}
412
413#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
418#[serde(deny_unknown_fields)]
419pub struct CommitSlotDiagnostic {
420 pub present: bool,
422 pub generation: Option<u64>,
424 pub valid: bool,
426}
427
428impl CommitSlotDiagnostic {
429 fn from_slot<T: ProtectedGenerationSlot>(slot: Option<&T>) -> Self {
430 match slot {
431 Some(record) => Self {
432 present: true,
433 generation: Some(record.generation()),
434 valid: record.validates(),
435 },
436 None => Self {
437 present: false,
438 generation: None,
439 valid: false,
440 },
441 }
442 }
443}
444
445#[non_exhaustive]
450#[derive(Clone, Copy, Debug, Deserialize, Eq, thiserror::Error, PartialEq, Serialize)]
451pub enum CommitRecoveryError {
452 #[error("no valid committed ledger generation")]
454 NoValidGeneration,
455 #[error("ambiguous committed ledger generation {generation}")]
457 AmbiguousGeneration {
458 generation: u64,
460 },
461 #[error("committed ledger generation {generation} cannot be advanced without overflow")]
463 GenerationOverflow {
464 generation: u64,
466 },
467 #[error("expected committed ledger generation {expected}, got {actual}")]
469 UnexpectedGeneration {
470 expected: u64,
472 actual: u64,
474 },
475}
476
477fn generation_checksum(generation: &CommittedGenerationBytes) -> u64 {
478 let mut hash = FNV_OFFSET;
479 hash = hash_u64(hash, generation.generation);
480 hash = hash_u64(hash, generation.commit_marker);
481 hash = hash_usize(hash, generation.payload.len());
482 for byte in &generation.payload {
483 hash = hash_byte(hash, *byte);
484 }
485 hash
486}
487
488fn hash_usize(hash: u64, value: usize) -> u64 {
489 hash_u64(hash, value as u64)
490}
491
492fn hash_u64(mut hash: u64, value: u64) -> u64 {
493 for byte in value.to_le_bytes() {
494 hash = hash_byte(hash, byte);
495 }
496 hash
497}
498
499const fn hash_byte(hash: u64, byte: u8) -> u64 {
500 (hash ^ byte as u64).wrapping_mul(FNV_PRIME)
501}
502
503#[cfg(test)]
504mod tests {
505 use super::*;
506
507 fn payload(value: u8) -> Vec<u8> {
508 vec![value; 4]
509 }
510
511 #[test]
512 fn committed_generation_validates_marker_and_checksum() {
513 let mut generation = CommittedGenerationBytes::new(7, payload(1));
514 assert!(generation.validates());
515
516 generation.checksum = generation.checksum.wrapping_add(1);
517 assert!(!generation.validates());
518 }
519
520 #[test]
521 fn physical_commit_accessors_expose_read_only_state() {
522 let mut store = DualCommitStore::default();
523 store.commit_payload(payload(1)).expect("first commit");
524
525 let slot = store.slot0().expect("first slot");
526
527 assert_eq!(slot.generation(), 0);
528 assert_eq!(slot.payload(), payload(1).as_slice());
529 assert_eq!(slot.commit_marker(), COMMIT_MARKER);
530 assert_eq!(slot.checksum(), generation_checksum(slot));
531 assert!(store.slot1().is_none());
532 }
533
534 #[test]
535 fn authoritative_selects_highest_valid_generation() {
536 let mut store = DualCommitStore::default();
537 store.commit_payload(payload(1)).expect("first commit");
538 store.commit_payload(payload(2)).expect("second commit");
539
540 let authoritative = store.authoritative().expect("authoritative");
541 let authoritative_slot =
542 select_authoritative_slot(store.slot0.as_ref(), store.slot1.as_ref())
543 .expect("authoritative slot");
544
545 assert_eq!(authoritative.generation, 1);
546 assert_eq!(authoritative.payload, payload(2));
547 assert_eq!(authoritative_slot.index, CommitSlotIndex::Slot1);
548 assert_eq!(authoritative_slot.record.payload, payload(2));
549 }
550
551 #[test]
552 fn corrupt_newer_slot_leaves_prior_generation_authoritative() {
553 let mut store = DualCommitStore::default();
554 store.commit_payload(payload(1)).expect("first commit");
555 store.write_corrupt_inactive_slot(1, payload(2));
556
557 let authoritative = store.authoritative().expect("authoritative");
558
559 assert_eq!(authoritative.generation, 0);
560 assert_eq!(authoritative.payload, payload(1));
561 }
562
563 #[test]
564 fn no_valid_generation_fails_closed() {
565 let mut store = DualCommitStore::default();
566 store.write_corrupt_inactive_slot(0, payload(1));
567 store.write_corrupt_inactive_slot(1, payload(2));
568
569 let err = store.authoritative().expect_err("no valid slot");
570
571 assert_eq!(err, CommitRecoveryError::NoValidGeneration);
572 }
573
574 #[test]
575 fn same_generation_identical_slots_recover_deterministically() {
576 let committed = CommittedGenerationBytes::new(7, payload(1));
577 let store = DualCommitStore {
578 slot0: Some(committed.clone()),
579 slot1: Some(committed),
580 };
581
582 let authoritative = store.authoritative_slot().expect("authoritative");
583
584 assert_eq!(authoritative.index, CommitSlotIndex::Slot0);
585 assert_eq!(authoritative.record.generation, 7);
586 }
587
588 #[test]
589 fn same_generation_divergent_slots_fail_closed() {
590 let store = DualCommitStore {
591 slot0: Some(CommittedGenerationBytes::new(7, payload(1))),
592 slot1: Some(CommittedGenerationBytes::new(7, payload(2))),
593 };
594
595 let err = store.authoritative().expect_err("ambiguous generation");
596
597 assert_eq!(
598 err,
599 CommitRecoveryError::AmbiguousGeneration { generation: 7 }
600 );
601 }
602
603 #[test]
604 fn physical_generation_overflow_fails_closed() {
605 let mut store = DualCommitStore {
606 slot0: Some(CommittedGenerationBytes::new(u64::MAX, payload(1))),
607 slot1: None,
608 };
609
610 let err = store
611 .commit_payload(payload(2))
612 .expect_err("overflow must fail");
613
614 assert_eq!(
615 err,
616 CommitRecoveryError::GenerationOverflow {
617 generation: u64::MAX
618 }
619 );
620 }
621
622 #[test]
623 fn diagnostic_reports_authoritative_generation_and_corrupt_slots() {
624 let mut store = DualCommitStore::default();
625 store.commit_payload(payload(1)).expect("first commit");
626 store.write_corrupt_inactive_slot(1, payload(2));
627
628 let diagnostic = store.diagnostic();
629
630 assert_eq!(diagnostic.authoritative_generation, Some(0));
631 assert_eq!(diagnostic.recovery_error, None);
632 assert_eq!(diagnostic.slot0.generation, Some(0));
633 assert!(diagnostic.slot0.valid);
634 assert_eq!(diagnostic.slot1.generation, Some(1));
635 assert!(!diagnostic.slot1.valid);
636 }
637
638 #[test]
639 fn diagnostic_reports_no_valid_generation_for_empty_store() {
640 let diagnostic = DualCommitStore::default().diagnostic();
641
642 assert_eq!(diagnostic.authoritative_generation, None);
643 assert_eq!(
644 diagnostic.recovery_error,
645 Some(CommitRecoveryError::NoValidGeneration)
646 );
647 assert!(!diagnostic.slot0.present);
648 assert!(!diagnostic.slot1.present);
649 }
650
651 #[test]
652 fn diagnostic_builds_from_any_dual_protected_store() {
653 #[derive(Eq, PartialEq)]
654 struct TestSlot {
655 generation: u64,
656 valid: bool,
657 }
658
659 impl ProtectedGenerationSlot for TestSlot {
660 fn generation(&self) -> u64 {
661 self.generation
662 }
663
664 fn validates(&self) -> bool {
665 self.valid
666 }
667 }
668
669 struct TestStore {
670 slot0: Option<TestSlot>,
671 slot1: Option<TestSlot>,
672 }
673
674 impl DualProtectedCommitStore for TestStore {
675 type Slot = TestSlot;
676
677 fn slot0(&self) -> Option<&Self::Slot> {
678 self.slot0.as_ref()
679 }
680
681 fn slot1(&self) -> Option<&Self::Slot> {
682 self.slot1.as_ref()
683 }
684 }
685
686 let diagnostic = CommitStoreDiagnostic::from_store(&TestStore {
687 slot0: Some(TestSlot {
688 generation: 8,
689 valid: true,
690 }),
691 slot1: Some(TestSlot {
692 generation: 9,
693 valid: false,
694 }),
695 });
696
697 assert_eq!(diagnostic.authoritative_generation, Some(8));
698 assert!(diagnostic.slot0.valid);
699 assert_eq!(diagnostic.slot1.generation, Some(9));
700 assert!(!diagnostic.slot1.valid);
701 }
702
703 #[test]
704 fn uninitialized_distinguishes_empty_from_corrupt() {
705 let mut store = DualCommitStore::default();
706 assert!(store.is_uninitialized());
707
708 store.write_corrupt_inactive_slot(0, payload(1));
709
710 assert!(!store.is_uninitialized());
711 }
712
713 #[test]
714 fn commit_after_corrupt_slot_advances_from_prior_valid_generation() {
715 let mut store = DualCommitStore::default();
716 store.commit_payload(payload(1)).expect("first commit");
717 store.write_corrupt_inactive_slot(1, payload(2));
718 store.commit_payload(payload(3)).expect("third commit");
719
720 let authoritative = store.authoritative().expect("authoritative");
721
722 assert_eq!(authoritative.generation, 1);
723 assert_eq!(authoritative.payload, payload(3));
724 }
725}