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, Debug, Deserialize, Eq, PartialEq, Serialize)]
12pub struct CommittedGenerationBytes {
13 pub generation: u64,
15 pub commit_marker: u64,
17 pub checksum: u64,
19 pub payload: Vec<u8>,
21}
22
23impl CommittedGenerationBytes {
24 #[must_use]
26 pub fn new(generation: u64, payload: Vec<u8>) -> Self {
27 let mut record = Self {
28 generation,
29 commit_marker: COMMIT_MARKER,
30 checksum: 0,
31 payload,
32 };
33 record.checksum = generation_checksum(&record);
34 record
35 }
36
37 #[must_use]
39 pub fn validates(&self) -> bool {
40 self.commit_marker == COMMIT_MARKER && self.checksum == generation_checksum(self)
41 }
42}
43
44#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
53pub struct DualCommitStore {
54 pub slot0: Option<CommittedGenerationBytes>,
56 pub slot1: Option<CommittedGenerationBytes>,
58}
59
60impl DualCommitStore {
61 #[must_use]
63 pub const fn is_uninitialized(&self) -> bool {
64 self.slot0.is_none() && self.slot1.is_none()
65 }
66
67 pub fn authoritative(&self) -> Result<&CommittedGenerationBytes, CommitRecoveryError> {
69 let slot0 = self.slot0.as_ref().filter(|slot| slot.validates());
70 let slot1 = self.slot1.as_ref().filter(|slot| slot.validates());
71
72 match (slot0, slot1) {
73 (Some(left), Some(right)) if right.generation > left.generation => Ok(right),
74 (Some(left), Some(_) | None) => Ok(left),
75 (None, Some(right)) => Ok(right),
76 (None, None) => Err(CommitRecoveryError::NoValidGeneration),
77 }
78 }
79
80 #[must_use]
82 pub fn diagnostic(&self) -> CommitStoreDiagnostic {
83 let authoritative = self.authoritative();
84 CommitStoreDiagnostic {
85 slot0: CommitSlotDiagnostic::from_slot(self.slot0.as_ref()),
86 slot1: CommitSlotDiagnostic::from_slot(self.slot1.as_ref()),
87 authoritative_generation: authoritative.ok().map(|record| record.generation),
88 recovery_error: authoritative.err(),
89 }
90 }
91
92 pub fn commit_payload(
98 &mut self,
99 payload: Vec<u8>,
100 ) -> Result<&CommittedGenerationBytes, CommitRecoveryError> {
101 let next_generation = self
102 .authoritative()
103 .map_or(0, |record| record.generation.saturating_add(1));
104 let next = CommittedGenerationBytes::new(next_generation, payload);
105
106 if self.inactive_slot_index() == 0 {
107 self.slot0 = Some(next);
108 } else {
109 self.slot1 = Some(next);
110 }
111
112 self.authoritative()
113 }
114
115 pub fn write_corrupt_inactive_slot(&mut self, generation: u64, payload: Vec<u8>) {
120 let mut corrupt = CommittedGenerationBytes::new(generation, payload);
121 corrupt.checksum = corrupt.checksum.wrapping_add(1);
122
123 if self.inactive_slot_index() == 0 {
124 self.slot0 = Some(corrupt);
125 } else {
126 self.slot1 = Some(corrupt);
127 }
128 }
129
130 fn inactive_slot_index(&self) -> u8 {
131 match self.authoritative() {
132 Ok(record) if self.slot0.as_ref() == Some(record) => 1,
133 Ok(_) | Err(_) => 0,
134 }
135 }
136}
137
138#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
143pub struct CommitStoreDiagnostic {
144 pub slot0: CommitSlotDiagnostic,
146 pub slot1: CommitSlotDiagnostic,
148 pub authoritative_generation: Option<u64>,
150 pub recovery_error: Option<CommitRecoveryError>,
152}
153
154#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
159pub struct CommitSlotDiagnostic {
160 pub present: bool,
162 pub generation: Option<u64>,
164 pub valid: bool,
166}
167
168impl CommitSlotDiagnostic {
169 fn from_slot(slot: Option<&CommittedGenerationBytes>) -> Self {
170 match slot {
171 Some(record) => Self {
172 present: true,
173 generation: Some(record.generation),
174 valid: record.validates(),
175 },
176 None => Self {
177 present: false,
178 generation: None,
179 valid: false,
180 },
181 }
182 }
183}
184
185#[derive(Clone, Copy, Debug, Deserialize, Eq, thiserror::Error, PartialEq, Serialize)]
190pub enum CommitRecoveryError {
191 #[error("no valid committed ledger generation")]
193 NoValidGeneration,
194}
195
196fn generation_checksum(generation: &CommittedGenerationBytes) -> u64 {
197 let mut hash = FNV_OFFSET;
198 hash = hash_u64(hash, generation.generation);
199 hash = hash_u64(hash, generation.commit_marker);
200 hash = hash_usize(hash, generation.payload.len());
201 for byte in &generation.payload {
202 hash = hash_byte(hash, *byte);
203 }
204 hash
205}
206
207fn hash_usize(hash: u64, value: usize) -> u64 {
208 hash_u64(hash, value as u64)
209}
210
211fn hash_u64(mut hash: u64, value: u64) -> u64 {
212 for byte in value.to_le_bytes() {
213 hash = hash_byte(hash, byte);
214 }
215 hash
216}
217
218const fn hash_byte(hash: u64, byte: u8) -> u64 {
219 (hash ^ byte as u64).wrapping_mul(FNV_PRIME)
220}
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225
226 fn payload(value: u8) -> Vec<u8> {
227 vec![value; 4]
228 }
229
230 #[test]
231 fn committed_generation_validates_marker_and_checksum() {
232 let mut generation = CommittedGenerationBytes::new(7, payload(1));
233 assert!(generation.validates());
234
235 generation.checksum = generation.checksum.wrapping_add(1);
236 assert!(!generation.validates());
237 }
238
239 #[test]
240 fn authoritative_selects_highest_valid_generation() {
241 let mut store = DualCommitStore::default();
242 store.commit_payload(payload(1)).expect("first commit");
243 store.commit_payload(payload(2)).expect("second commit");
244
245 let authoritative = store.authoritative().expect("authoritative");
246
247 assert_eq!(authoritative.generation, 1);
248 assert_eq!(authoritative.payload, payload(2));
249 }
250
251 #[test]
252 fn corrupt_newer_slot_leaves_prior_generation_authoritative() {
253 let mut store = DualCommitStore::default();
254 store.commit_payload(payload(1)).expect("first commit");
255 store.write_corrupt_inactive_slot(1, payload(2));
256
257 let authoritative = store.authoritative().expect("authoritative");
258
259 assert_eq!(authoritative.generation, 0);
260 assert_eq!(authoritative.payload, payload(1));
261 }
262
263 #[test]
264 fn no_valid_generation_fails_closed() {
265 let mut store = DualCommitStore::default();
266 store.write_corrupt_inactive_slot(0, payload(1));
267 store.write_corrupt_inactive_slot(1, payload(2));
268
269 let err = store.authoritative().expect_err("no valid slot");
270
271 assert_eq!(err, CommitRecoveryError::NoValidGeneration);
272 }
273
274 #[test]
275 fn diagnostic_reports_authoritative_generation_and_corrupt_slots() {
276 let mut store = DualCommitStore::default();
277 store.commit_payload(payload(1)).expect("first commit");
278 store.write_corrupt_inactive_slot(1, payload(2));
279
280 let diagnostic = store.diagnostic();
281
282 assert_eq!(diagnostic.authoritative_generation, Some(0));
283 assert_eq!(diagnostic.recovery_error, None);
284 assert_eq!(diagnostic.slot0.generation, Some(0));
285 assert!(diagnostic.slot0.valid);
286 assert_eq!(diagnostic.slot1.generation, Some(1));
287 assert!(!diagnostic.slot1.valid);
288 }
289
290 #[test]
291 fn diagnostic_reports_no_valid_generation_for_empty_store() {
292 let diagnostic = DualCommitStore::default().diagnostic();
293
294 assert_eq!(diagnostic.authoritative_generation, None);
295 assert_eq!(
296 diagnostic.recovery_error,
297 Some(CommitRecoveryError::NoValidGeneration)
298 );
299 assert!(!diagnostic.slot0.present);
300 assert!(!diagnostic.slot1.present);
301 }
302
303 #[test]
304 fn uninitialized_distinguishes_empty_from_corrupt() {
305 let mut store = DualCommitStore::default();
306 assert!(store.is_uninitialized());
307
308 store.write_corrupt_inactive_slot(0, payload(1));
309
310 assert!(!store.is_uninitialized());
311 }
312
313 #[test]
314 fn commit_after_corrupt_slot_advances_from_prior_valid_generation() {
315 let mut store = DualCommitStore::default();
316 store.commit_payload(payload(1)).expect("first commit");
317 store.write_corrupt_inactive_slot(1, payload(2));
318 store.commit_payload(payload(3)).expect("third commit");
319
320 let authoritative = store.authoritative().expect("authoritative");
321
322 assert_eq!(authoritative.generation, 1);
323 assert_eq!(authoritative.payload, payload(3));
324 }
325}