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
19#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
24pub enum CommitSlotIndex {
25 Slot0,
27 Slot1,
29}
30
31impl CommitSlotIndex {
32 #[must_use]
34 pub const fn opposite(self) -> Self {
35 match self {
36 Self::Slot0 => Self::Slot1,
37 Self::Slot1 => Self::Slot0,
38 }
39 }
40}
41
42#[derive(Clone, Copy, Debug, Eq, PartialEq)]
47pub struct AuthoritativeSlot<'slot, T> {
48 pub index: CommitSlotIndex,
50 pub record: &'slot T,
52}
53
54pub fn select_authoritative_slot<'slot, T: ProtectedGenerationSlot>(
56 slot0: Option<&'slot T>,
57 slot1: Option<&'slot T>,
58) -> Result<AuthoritativeSlot<'slot, T>, CommitRecoveryError> {
59 let slot0 = slot0
60 .filter(|slot| slot.validates())
61 .map(|record| AuthoritativeSlot {
62 index: CommitSlotIndex::Slot0,
63 record,
64 });
65 let slot1 = slot1
66 .filter(|slot| slot.validates())
67 .map(|record| AuthoritativeSlot {
68 index: CommitSlotIndex::Slot1,
69 record,
70 });
71
72 match (slot0, slot1) {
73 (Some(left), Some(right)) if right.record.generation() > left.record.generation() => {
74 Ok(right)
75 }
76 (Some(left), Some(_) | None) => Ok(left),
77 (None, Some(right)) => Ok(right),
78 (None, None) => Err(CommitRecoveryError::NoValidGeneration),
79 }
80}
81
82#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
87pub struct CommittedGenerationBytes {
88 pub generation: u64,
90 pub commit_marker: u64,
92 pub checksum: u64,
94 pub payload: Vec<u8>,
96}
97
98impl CommittedGenerationBytes {
99 #[must_use]
101 pub fn new(generation: u64, payload: Vec<u8>) -> Self {
102 let mut record = Self {
103 generation,
104 commit_marker: COMMIT_MARKER,
105 checksum: 0,
106 payload,
107 };
108 record.checksum = generation_checksum(&record);
109 record
110 }
111
112 #[must_use]
114 pub fn validates(&self) -> bool {
115 self.commit_marker == COMMIT_MARKER && self.checksum == generation_checksum(self)
116 }
117}
118
119impl ProtectedGenerationSlot for CommittedGenerationBytes {
120 fn generation(&self) -> u64 {
121 self.generation
122 }
123
124 fn validates(&self) -> bool {
125 self.validates()
126 }
127}
128
129#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
138pub struct DualCommitStore {
139 pub slot0: Option<CommittedGenerationBytes>,
141 pub slot1: Option<CommittedGenerationBytes>,
143}
144
145impl DualCommitStore {
146 #[must_use]
148 pub const fn is_uninitialized(&self) -> bool {
149 self.slot0.is_none() && self.slot1.is_none()
150 }
151
152 pub fn authoritative(&self) -> Result<&CommittedGenerationBytes, CommitRecoveryError> {
154 select_authoritative_slot(self.slot0.as_ref(), self.slot1.as_ref())
155 .map(|authoritative| authoritative.record)
156 }
157
158 #[must_use]
160 pub fn diagnostic(&self) -> CommitStoreDiagnostic {
161 let authoritative = self.authoritative();
162 CommitStoreDiagnostic {
163 slot0: CommitSlotDiagnostic::from_slot(self.slot0.as_ref()),
164 slot1: CommitSlotDiagnostic::from_slot(self.slot1.as_ref()),
165 authoritative_generation: authoritative.ok().map(|record| record.generation),
166 recovery_error: authoritative.err(),
167 }
168 }
169
170 pub fn commit_payload(
176 &mut self,
177 payload: Vec<u8>,
178 ) -> Result<&CommittedGenerationBytes, CommitRecoveryError> {
179 let next_generation = self
180 .authoritative()
181 .map_or(0, |record| record.generation.saturating_add(1));
182 let next = CommittedGenerationBytes::new(next_generation, payload);
183
184 if self.inactive_slot_index() == 0 {
185 self.slot0 = Some(next);
186 } else {
187 self.slot1 = Some(next);
188 }
189
190 self.authoritative()
191 }
192
193 pub fn write_corrupt_inactive_slot(&mut self, generation: u64, payload: Vec<u8>) {
198 let mut corrupt = CommittedGenerationBytes::new(generation, payload);
199 corrupt.checksum = corrupt.checksum.wrapping_add(1);
200
201 if self.inactive_slot_index() == 0 {
202 self.slot0 = Some(corrupt);
203 } else {
204 self.slot1 = Some(corrupt);
205 }
206 }
207
208 fn inactive_slot_index(&self) -> u8 {
209 match select_authoritative_slot(self.slot0.as_ref(), self.slot1.as_ref()) {
210 Ok(authoritative) if authoritative.index == CommitSlotIndex::Slot0 => 1,
211 Ok(_) | Err(_) => 0,
212 }
213 }
214}
215
216#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
221pub struct CommitStoreDiagnostic {
222 pub slot0: CommitSlotDiagnostic,
224 pub slot1: CommitSlotDiagnostic,
226 pub authoritative_generation: Option<u64>,
228 pub recovery_error: Option<CommitRecoveryError>,
230}
231
232#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
237pub struct CommitSlotDiagnostic {
238 pub present: bool,
240 pub generation: Option<u64>,
242 pub valid: bool,
244}
245
246impl CommitSlotDiagnostic {
247 fn from_slot(slot: Option<&CommittedGenerationBytes>) -> Self {
248 match slot {
249 Some(record) => Self {
250 present: true,
251 generation: Some(record.generation),
252 valid: record.validates(),
253 },
254 None => Self {
255 present: false,
256 generation: None,
257 valid: false,
258 },
259 }
260 }
261}
262
263#[derive(Clone, Copy, Debug, Deserialize, Eq, thiserror::Error, PartialEq, Serialize)]
268pub enum CommitRecoveryError {
269 #[error("no valid committed ledger generation")]
271 NoValidGeneration,
272}
273
274fn generation_checksum(generation: &CommittedGenerationBytes) -> u64 {
275 let mut hash = FNV_OFFSET;
276 hash = hash_u64(hash, generation.generation);
277 hash = hash_u64(hash, generation.commit_marker);
278 hash = hash_usize(hash, generation.payload.len());
279 for byte in &generation.payload {
280 hash = hash_byte(hash, *byte);
281 }
282 hash
283}
284
285fn hash_usize(hash: u64, value: usize) -> u64 {
286 hash_u64(hash, value as u64)
287}
288
289fn hash_u64(mut hash: u64, value: u64) -> u64 {
290 for byte in value.to_le_bytes() {
291 hash = hash_byte(hash, byte);
292 }
293 hash
294}
295
296const fn hash_byte(hash: u64, byte: u8) -> u64 {
297 (hash ^ byte as u64).wrapping_mul(FNV_PRIME)
298}
299
300#[cfg(test)]
301mod tests {
302 use super::*;
303
304 fn payload(value: u8) -> Vec<u8> {
305 vec![value; 4]
306 }
307
308 #[test]
309 fn committed_generation_validates_marker_and_checksum() {
310 let mut generation = CommittedGenerationBytes::new(7, payload(1));
311 assert!(generation.validates());
312
313 generation.checksum = generation.checksum.wrapping_add(1);
314 assert!(!generation.validates());
315 }
316
317 #[test]
318 fn authoritative_selects_highest_valid_generation() {
319 let mut store = DualCommitStore::default();
320 store.commit_payload(payload(1)).expect("first commit");
321 store.commit_payload(payload(2)).expect("second commit");
322
323 let authoritative = store.authoritative().expect("authoritative");
324 let authoritative_slot =
325 select_authoritative_slot(store.slot0.as_ref(), store.slot1.as_ref())
326 .expect("authoritative slot");
327
328 assert_eq!(authoritative.generation, 1);
329 assert_eq!(authoritative.payload, payload(2));
330 assert_eq!(authoritative_slot.index, CommitSlotIndex::Slot1);
331 assert_eq!(authoritative_slot.record.payload, payload(2));
332 }
333
334 #[test]
335 fn corrupt_newer_slot_leaves_prior_generation_authoritative() {
336 let mut store = DualCommitStore::default();
337 store.commit_payload(payload(1)).expect("first commit");
338 store.write_corrupt_inactive_slot(1, payload(2));
339
340 let authoritative = store.authoritative().expect("authoritative");
341
342 assert_eq!(authoritative.generation, 0);
343 assert_eq!(authoritative.payload, payload(1));
344 }
345
346 #[test]
347 fn no_valid_generation_fails_closed() {
348 let mut store = DualCommitStore::default();
349 store.write_corrupt_inactive_slot(0, payload(1));
350 store.write_corrupt_inactive_slot(1, payload(2));
351
352 let err = store.authoritative().expect_err("no valid slot");
353
354 assert_eq!(err, CommitRecoveryError::NoValidGeneration);
355 }
356
357 #[test]
358 fn diagnostic_reports_authoritative_generation_and_corrupt_slots() {
359 let mut store = DualCommitStore::default();
360 store.commit_payload(payload(1)).expect("first commit");
361 store.write_corrupt_inactive_slot(1, payload(2));
362
363 let diagnostic = store.diagnostic();
364
365 assert_eq!(diagnostic.authoritative_generation, Some(0));
366 assert_eq!(diagnostic.recovery_error, None);
367 assert_eq!(diagnostic.slot0.generation, Some(0));
368 assert!(diagnostic.slot0.valid);
369 assert_eq!(diagnostic.slot1.generation, Some(1));
370 assert!(!diagnostic.slot1.valid);
371 }
372
373 #[test]
374 fn diagnostic_reports_no_valid_generation_for_empty_store() {
375 let diagnostic = DualCommitStore::default().diagnostic();
376
377 assert_eq!(diagnostic.authoritative_generation, None);
378 assert_eq!(
379 diagnostic.recovery_error,
380 Some(CommitRecoveryError::NoValidGeneration)
381 );
382 assert!(!diagnostic.slot0.present);
383 assert!(!diagnostic.slot1.present);
384 }
385
386 #[test]
387 fn uninitialized_distinguishes_empty_from_corrupt() {
388 let mut store = DualCommitStore::default();
389 assert!(store.is_uninitialized());
390
391 store.write_corrupt_inactive_slot(0, payload(1));
392
393 assert!(!store.is_uninitialized());
394 }
395
396 #[test]
397 fn commit_after_corrupt_slot_advances_from_prior_valid_generation() {
398 let mut store = DualCommitStore::default();
399 store.commit_payload(payload(1)).expect("first commit");
400 store.write_corrupt_inactive_slot(1, payload(2));
401 store.commit_payload(payload(3)).expect("third commit");
402
403 let authoritative = store.authoritative().expect("authoritative");
404
405 assert_eq!(authoritative.generation, 1);
406 assert_eq!(authoritative.payload, payload(3));
407 }
408}