sim_lib_journal/
verify.rs1use crate::{JournalEntry, JournalHead, JournalObject, StoredState};
2use sim_kernel::ContentId;
3use std::collections::{BTreeMap, BTreeSet};
4use thiserror::Error;
5
6#[derive(Clone, Debug, PartialEq, Eq)]
7pub struct Verification {
8 pub head: Option<JournalHead>,
9 pub entries: Vec<JournalEntry>,
10 pub object_ids: BTreeSet<ContentId>,
11}
12
13#[derive(Clone, Debug, PartialEq, Eq, Error)]
14pub enum JournalError {
15 #[error("journal batch is empty")]
16 EmptyBatch,
17 #[error("journal head does not match the expected head")]
18 WrongHead,
19 #[error("writer lease is stale")]
20 StaleLease,
21 #[error("sequence is not gapless")]
22 SequenceGap,
23 #[error("entry has the wrong previous id")]
24 WrongPrevious,
25 #[error("entry identity is not canonical")]
26 CorruptEntry,
27 #[error("object {0:?} has bytes that do not match its id")]
28 CorruptObject(ContentId),
29 #[error("payload object {0:?} is missing")]
30 MissingPayload(ContentId),
31 #[error("content id was redelivered with conflicting bytes")]
32 ConflictingObject,
33 #[error("sequence was redelivered with a conflicting entry")]
34 ConflictingDelivery,
35 #[error("backend state is corrupt: {0}")]
36 CorruptState(&'static str),
37 #[error("backend failure: {0}")]
38 Backend(String),
39 #[error("backend cannot satisfy durable journal writes: {0}")]
40 WriteRefused(&'static str),
41 #[error("journal verification exceeded its caller-supplied work bound")]
42 WorkBoundExceeded,
43 #[error("injected crash at {0}")]
44 InjectedCrash(&'static str),
45}
46
47pub(crate) fn verify_batch(
48 state: &StoredState,
49 expected: Option<&JournalHead>,
50 objects: &[JournalObject],
51 entries: &[JournalEntry],
52) -> Result<(), JournalError> {
53 let mut available: BTreeMap<ContentId, Vec<u8>> = state.objects.clone();
54 for object in objects {
55 object.verify()?;
56 if let Some(bytes) = available.get(&object.id)
57 && bytes != &object.bytes
58 {
59 return Err(JournalError::ConflictingObject);
60 }
61 available.insert(object.id.clone(), object.bytes.clone());
62 }
63 let first_sequence = expected.map_or(0, |h| h.sequence + 1);
64 let mut previous = expected.map(|h| h.entry.clone());
65 for (sequence, entry) in (first_sequence..).zip(entries) {
66 if entry.canonical_id() != entry.id {
67 return Err(JournalError::CorruptEntry);
68 }
69 if entry.sequence != sequence {
70 return Err(JournalError::SequenceGap);
71 }
72 if entry.previous != previous {
73 return Err(JournalError::WrongPrevious);
74 }
75 for payload in &entry.payloads {
76 if !available.contains_key(payload) {
77 return Err(JournalError::MissingPayload(payload.clone()));
78 }
79 }
80 if let Some(existing) = state.entries.get(&entry.sequence)
81 && existing != entry
82 {
83 return Err(JournalError::ConflictingDelivery);
84 }
85 previous = Some(entry.id.clone());
86 }
87 Ok(())
88}
89
90pub(crate) fn verify_state(state: &StoredState) -> Result<Verification, JournalError> {
91 let mut prior = None;
92 for (expected_sequence, entry) in state.entries.values().enumerate() {
93 if entry.sequence != expected_sequence as u64 {
94 return Err(JournalError::CorruptState("sequence gap"));
95 }
96 if entry.previous != prior {
97 return Err(JournalError::CorruptState("previous id"));
98 }
99 if entry.canonical_id() != entry.id {
100 return Err(JournalError::CorruptEntry);
101 }
102 for payload in &entry.payloads {
103 let bytes = state
104 .objects
105 .get(payload)
106 .ok_or_else(|| JournalError::MissingPayload(payload.clone()))?;
107 JournalObject {
108 id: payload.clone(),
109 bytes: bytes.clone(),
110 }
111 .verify()?;
112 }
113 prior = Some(entry.id.clone());
114 }
115 let computed = state.entries.last_key_value().map(|(_, e)| JournalHead {
116 sequence: e.sequence,
117 entry: e.id.clone(),
118 });
119 if computed != state.head {
120 return Err(JournalError::CorruptState("head"));
121 }
122 Ok(Verification {
123 head: computed,
124 entries: state.entries.values().cloned().collect(),
125 object_ids: state.objects.keys().cloned().collect(),
126 })
127}