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 {
12 fn generation(&self) -> u64;
14
15 fn validates(&self) -> bool;
17}
18
19pub trait DualProtectedCommitStore {
24 type Slot: ProtectedGenerationSlot;
26
27 fn slot0(&self) -> Option<&Self::Slot>;
29
30 fn slot1(&self) -> Option<&Self::Slot>;
32
33 fn is_uninitialized(&self) -> bool {
35 self.slot0().is_none() && self.slot1().is_none()
36 }
37
38 fn authoritative_slot(&self) -> Result<AuthoritativeSlot<'_, Self::Slot>, CommitRecoveryError> {
40 select_authoritative_slot(self.slot0(), self.slot1())
41 }
42
43 fn inactive_slot_index(&self) -> CommitSlotIndex {
48 match self.authoritative_slot() {
49 Ok(authoritative) => authoritative.index.opposite(),
50 Err(_) => CommitSlotIndex::Slot0,
51 }
52 }
53}
54
55#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
60pub enum CommitSlotIndex {
61 Slot0,
63 Slot1,
65}
66
67impl CommitSlotIndex {
68 #[must_use]
70 pub const fn opposite(self) -> Self {
71 match self {
72 Self::Slot0 => Self::Slot1,
73 Self::Slot1 => Self::Slot0,
74 }
75 }
76}
77
78#[derive(Clone, Copy, Debug, Eq, PartialEq)]
83pub struct AuthoritativeSlot<'slot, T> {
84 pub index: CommitSlotIndex,
86 pub record: &'slot T,
88}
89
90pub fn select_authoritative_slot<'slot, T: ProtectedGenerationSlot>(
92 slot0: Option<&'slot T>,
93 slot1: Option<&'slot T>,
94) -> Result<AuthoritativeSlot<'slot, T>, CommitRecoveryError> {
95 let slot0 = slot0
96 .filter(|slot| slot.validates())
97 .map(|record| AuthoritativeSlot {
98 index: CommitSlotIndex::Slot0,
99 record,
100 });
101 let slot1 = slot1
102 .filter(|slot| slot.validates())
103 .map(|record| AuthoritativeSlot {
104 index: CommitSlotIndex::Slot1,
105 record,
106 });
107
108 match (slot0, slot1) {
109 (Some(left), Some(right)) if right.record.generation() > left.record.generation() => {
110 Ok(right)
111 }
112 (Some(left), Some(_) | None) => Ok(left),
113 (None, Some(right)) => Ok(right),
114 (None, None) => Err(CommitRecoveryError::NoValidGeneration),
115 }
116}
117
118#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
123pub struct CommittedGenerationBytes {
124 pub generation: u64,
126 pub commit_marker: u64,
128 pub checksum: u64,
130 pub payload: Vec<u8>,
132}
133
134impl CommittedGenerationBytes {
135 #[must_use]
137 pub fn new(generation: u64, payload: Vec<u8>) -> Self {
138 let mut record = Self {
139 generation,
140 commit_marker: COMMIT_MARKER,
141 checksum: 0,
142 payload,
143 };
144 record.checksum = generation_checksum(&record);
145 record
146 }
147
148 #[must_use]
150 pub fn validates(&self) -> bool {
151 self.commit_marker == COMMIT_MARKER && self.checksum == generation_checksum(self)
152 }
153}
154
155impl ProtectedGenerationSlot for CommittedGenerationBytes {
156 fn generation(&self) -> u64 {
157 self.generation
158 }
159
160 fn validates(&self) -> bool {
161 self.validates()
162 }
163}
164
165#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
174pub struct DualCommitStore {
175 pub slot0: Option<CommittedGenerationBytes>,
177 pub slot1: Option<CommittedGenerationBytes>,
179}
180
181impl DualCommitStore {
182 #[must_use]
184 pub const fn is_uninitialized(&self) -> bool {
185 self.slot0.is_none() && self.slot1.is_none()
186 }
187
188 pub fn authoritative(&self) -> Result<&CommittedGenerationBytes, CommitRecoveryError> {
190 self.authoritative_slot()
191 .map(|authoritative| authoritative.record)
192 }
193
194 #[must_use]
196 pub fn diagnostic(&self) -> CommitStoreDiagnostic {
197 CommitStoreDiagnostic::from_store(self)
198 }
199
200 pub fn commit_payload(
206 &mut self,
207 payload: Vec<u8>,
208 ) -> Result<&CommittedGenerationBytes, CommitRecoveryError> {
209 let next_generation = self
210 .authoritative()
211 .map_or(0, |record| record.generation.saturating_add(1));
212 let next = CommittedGenerationBytes::new(next_generation, payload);
213
214 if self.inactive_slot_index() == CommitSlotIndex::Slot0 {
215 self.slot0 = Some(next);
216 } else {
217 self.slot1 = Some(next);
218 }
219
220 self.authoritative()
221 }
222
223 pub fn write_corrupt_inactive_slot(&mut self, generation: u64, payload: Vec<u8>) {
228 let mut corrupt = CommittedGenerationBytes::new(generation, payload);
229 corrupt.checksum = corrupt.checksum.wrapping_add(1);
230
231 if self.inactive_slot_index() == CommitSlotIndex::Slot0 {
232 self.slot0 = Some(corrupt);
233 } else {
234 self.slot1 = Some(corrupt);
235 }
236 }
237}
238
239impl DualProtectedCommitStore for DualCommitStore {
240 type Slot = CommittedGenerationBytes;
241
242 fn slot0(&self) -> Option<&Self::Slot> {
243 self.slot0.as_ref()
244 }
245
246 fn slot1(&self) -> Option<&Self::Slot> {
247 self.slot1.as_ref()
248 }
249}
250
251#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
256pub struct CommitStoreDiagnostic {
257 pub slot0: CommitSlotDiagnostic,
259 pub slot1: CommitSlotDiagnostic,
261 pub authoritative_generation: Option<u64>,
263 pub recovery_error: Option<CommitRecoveryError>,
265}
266
267impl CommitStoreDiagnostic {
268 #[must_use]
270 pub fn from_store<S: DualProtectedCommitStore>(store: &S) -> Self {
271 let (authoritative_generation, recovery_error) = match store.authoritative_slot() {
272 Ok(slot) => (Some(slot.record.generation()), None),
273 Err(err) => (None, Some(err)),
274 };
275 Self {
276 slot0: CommitSlotDiagnostic::from_slot(store.slot0()),
277 slot1: CommitSlotDiagnostic::from_slot(store.slot1()),
278 authoritative_generation,
279 recovery_error,
280 }
281 }
282}
283
284#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
289pub struct CommitSlotDiagnostic {
290 pub present: bool,
292 pub generation: Option<u64>,
294 pub valid: bool,
296}
297
298impl CommitSlotDiagnostic {
299 fn from_slot<T: ProtectedGenerationSlot>(slot: Option<&T>) -> Self {
300 match slot {
301 Some(record) => Self {
302 present: true,
303 generation: Some(record.generation()),
304 valid: record.validates(),
305 },
306 None => Self {
307 present: false,
308 generation: None,
309 valid: false,
310 },
311 }
312 }
313}
314
315#[derive(Clone, Copy, Debug, Deserialize, Eq, thiserror::Error, PartialEq, Serialize)]
320pub enum CommitRecoveryError {
321 #[error("no valid committed ledger generation")]
323 NoValidGeneration,
324}
325
326fn generation_checksum(generation: &CommittedGenerationBytes) -> u64 {
327 let mut hash = FNV_OFFSET;
328 hash = hash_u64(hash, generation.generation);
329 hash = hash_u64(hash, generation.commit_marker);
330 hash = hash_usize(hash, generation.payload.len());
331 for byte in &generation.payload {
332 hash = hash_byte(hash, *byte);
333 }
334 hash
335}
336
337fn hash_usize(hash: u64, value: usize) -> u64 {
338 hash_u64(hash, value as u64)
339}
340
341fn hash_u64(mut hash: u64, value: u64) -> u64 {
342 for byte in value.to_le_bytes() {
343 hash = hash_byte(hash, byte);
344 }
345 hash
346}
347
348const fn hash_byte(hash: u64, byte: u8) -> u64 {
349 (hash ^ byte as u64).wrapping_mul(FNV_PRIME)
350}
351
352#[cfg(test)]
353mod tests {
354 use super::*;
355
356 fn payload(value: u8) -> Vec<u8> {
357 vec![value; 4]
358 }
359
360 #[test]
361 fn committed_generation_validates_marker_and_checksum() {
362 let mut generation = CommittedGenerationBytes::new(7, payload(1));
363 assert!(generation.validates());
364
365 generation.checksum = generation.checksum.wrapping_add(1);
366 assert!(!generation.validates());
367 }
368
369 #[test]
370 fn authoritative_selects_highest_valid_generation() {
371 let mut store = DualCommitStore::default();
372 store.commit_payload(payload(1)).expect("first commit");
373 store.commit_payload(payload(2)).expect("second commit");
374
375 let authoritative = store.authoritative().expect("authoritative");
376 let authoritative_slot =
377 select_authoritative_slot(store.slot0.as_ref(), store.slot1.as_ref())
378 .expect("authoritative slot");
379
380 assert_eq!(authoritative.generation, 1);
381 assert_eq!(authoritative.payload, payload(2));
382 assert_eq!(authoritative_slot.index, CommitSlotIndex::Slot1);
383 assert_eq!(authoritative_slot.record.payload, payload(2));
384 }
385
386 #[test]
387 fn corrupt_newer_slot_leaves_prior_generation_authoritative() {
388 let mut store = DualCommitStore::default();
389 store.commit_payload(payload(1)).expect("first commit");
390 store.write_corrupt_inactive_slot(1, payload(2));
391
392 let authoritative = store.authoritative().expect("authoritative");
393
394 assert_eq!(authoritative.generation, 0);
395 assert_eq!(authoritative.payload, payload(1));
396 }
397
398 #[test]
399 fn no_valid_generation_fails_closed() {
400 let mut store = DualCommitStore::default();
401 store.write_corrupt_inactive_slot(0, payload(1));
402 store.write_corrupt_inactive_slot(1, payload(2));
403
404 let err = store.authoritative().expect_err("no valid slot");
405
406 assert_eq!(err, CommitRecoveryError::NoValidGeneration);
407 }
408
409 #[test]
410 fn diagnostic_reports_authoritative_generation_and_corrupt_slots() {
411 let mut store = DualCommitStore::default();
412 store.commit_payload(payload(1)).expect("first commit");
413 store.write_corrupt_inactive_slot(1, payload(2));
414
415 let diagnostic = store.diagnostic();
416
417 assert_eq!(diagnostic.authoritative_generation, Some(0));
418 assert_eq!(diagnostic.recovery_error, None);
419 assert_eq!(diagnostic.slot0.generation, Some(0));
420 assert!(diagnostic.slot0.valid);
421 assert_eq!(diagnostic.slot1.generation, Some(1));
422 assert!(!diagnostic.slot1.valid);
423 }
424
425 #[test]
426 fn diagnostic_reports_no_valid_generation_for_empty_store() {
427 let diagnostic = DualCommitStore::default().diagnostic();
428
429 assert_eq!(diagnostic.authoritative_generation, None);
430 assert_eq!(
431 diagnostic.recovery_error,
432 Some(CommitRecoveryError::NoValidGeneration)
433 );
434 assert!(!diagnostic.slot0.present);
435 assert!(!diagnostic.slot1.present);
436 }
437
438 #[test]
439 fn diagnostic_builds_from_any_dual_protected_store() {
440 struct TestSlot {
441 generation: u64,
442 valid: bool,
443 }
444
445 impl ProtectedGenerationSlot for TestSlot {
446 fn generation(&self) -> u64 {
447 self.generation
448 }
449
450 fn validates(&self) -> bool {
451 self.valid
452 }
453 }
454
455 struct TestStore {
456 slot0: Option<TestSlot>,
457 slot1: Option<TestSlot>,
458 }
459
460 impl DualProtectedCommitStore for TestStore {
461 type Slot = TestSlot;
462
463 fn slot0(&self) -> Option<&Self::Slot> {
464 self.slot0.as_ref()
465 }
466
467 fn slot1(&self) -> Option<&Self::Slot> {
468 self.slot1.as_ref()
469 }
470 }
471
472 let diagnostic = CommitStoreDiagnostic::from_store(&TestStore {
473 slot0: Some(TestSlot {
474 generation: 8,
475 valid: true,
476 }),
477 slot1: Some(TestSlot {
478 generation: 9,
479 valid: false,
480 }),
481 });
482
483 assert_eq!(diagnostic.authoritative_generation, Some(8));
484 assert!(diagnostic.slot0.valid);
485 assert_eq!(diagnostic.slot1.generation, Some(9));
486 assert!(!diagnostic.slot1.valid);
487 }
488
489 #[test]
490 fn uninitialized_distinguishes_empty_from_corrupt() {
491 let mut store = DualCommitStore::default();
492 assert!(store.is_uninitialized());
493
494 store.write_corrupt_inactive_slot(0, payload(1));
495
496 assert!(!store.is_uninitialized());
497 }
498
499 #[test]
500 fn commit_after_corrupt_slot_advances_from_prior_valid_generation() {
501 let mut store = DualCommitStore::default();
502 store.commit_payload(payload(1)).expect("first commit");
503 store.write_corrupt_inactive_slot(1, payload(2));
504 store.commit_payload(payload(3)).expect("third commit");
505
506 let authoritative = store.authoritative().expect("authoritative");
507
508 assert_eq!(authoritative.generation, 1);
509 assert_eq!(authoritative.payload, payload(3));
510 }
511}