libgrite_core/
integrity.rs1use crate::hash::compute_event_id;
6use crate::signing::verify_signature;
7use crate::store::GritStore;
8use crate::types::event::Event;
9use crate::types::ids::{EventId, id_to_hex};
10use crate::GriteError;
11
12#[derive(Debug, Default)]
14pub struct IntegrityReport {
15 pub events_checked: usize,
17 pub events_valid: usize,
19 pub corrupt_events: Vec<CorruptEvent>,
21 pub signatures_checked: usize,
23 pub signatures_valid: usize,
25 pub signature_errors: Vec<SignatureError>,
27}
28
29#[derive(Debug)]
31pub struct CorruptEvent {
32 pub event_id: EventId,
33 pub issue_id: String,
34 pub kind: CorruptionKind,
35}
36
37#[derive(Debug)]
39pub enum CorruptionKind {
40 HashMismatch {
42 expected: EventId,
43 computed: EventId,
44 },
45 MissingParent {
47 parent_id: EventId,
48 },
49}
50
51#[derive(Debug)]
53pub struct SignatureError {
54 pub event_id: EventId,
55 pub actor_id: String,
56 pub error: String,
57}
58
59impl IntegrityReport {
60 pub fn is_healthy(&self) -> bool {
62 self.corrupt_events.is_empty() && self.signature_errors.is_empty()
63 }
64
65 pub fn corruption_count(&self) -> usize {
67 self.corrupt_events.len()
68 }
69
70 pub fn signature_error_count(&self) -> usize {
72 self.signature_errors.len()
73 }
74}
75
76pub fn verify_event_hash(event: &Event) -> Result<(), CorruptionKind> {
78 let computed = compute_event_id(
79 &event.issue_id,
80 &event.actor,
81 event.ts_unix_ms,
82 event.parent.as_ref(),
83 &event.kind,
84 );
85
86 if computed != event.event_id {
87 return Err(CorruptionKind::HashMismatch {
88 expected: event.event_id,
89 computed,
90 });
91 }
92
93 Ok(())
94}
95
96pub fn check_store_integrity(
102 store: &GritStore,
103 verify_parents: bool,
104) -> Result<IntegrityReport, GriteError> {
105 let mut report = IntegrityReport::default();
106
107 let issues = store.list_issues(&Default::default())?;
109
110 for issue_summary in &issues {
111 let events = store.get_issue_events(&issue_summary.issue_id)?;
112 let event_ids: std::collections::HashSet<EventId> =
113 events.iter().map(|e| e.event_id).collect();
114
115 for event in &events {
116 report.events_checked += 1;
117
118 match verify_event_hash(event) {
120 Ok(()) => {
121 report.events_valid += 1;
122 }
123 Err(kind) => {
124 report.corrupt_events.push(CorruptEvent {
125 event_id: event.event_id,
126 issue_id: id_to_hex(&event.issue_id),
127 kind,
128 });
129 continue;
130 }
131 }
132
133 if verify_parents {
135 if let Some(parent_id) = &event.parent {
136 if !event_ids.contains(parent_id) {
137 report.corrupt_events.push(CorruptEvent {
138 event_id: event.event_id,
139 issue_id: id_to_hex(&event.issue_id),
140 kind: CorruptionKind::MissingParent {
141 parent_id: *parent_id,
142 },
143 });
144 }
145 }
146 }
147 }
148 }
149
150 Ok(report)
151}
152
153pub fn verify_store_signatures<F>(
157 store: &GritStore,
158 get_public_key: F,
159) -> Result<IntegrityReport, GriteError>
160where
161 F: Fn(&str) -> Option<String>,
162{
163 let mut report = IntegrityReport::default();
164
165 let issues = store.list_issues(&Default::default())?;
166
167 for issue_summary in &issues {
168 let events = store.get_issue_events(&issue_summary.issue_id)?;
169
170 for event in &events {
171 report.events_checked += 1;
172
173 if event.sig.is_none() {
175 report.signature_errors.push(SignatureError {
176 event_id: event.event_id,
177 actor_id: id_to_hex(&event.actor),
178 error: "signature missing".to_string(),
179 });
180 continue;
181 }
182
183 report.signatures_checked += 1;
184
185 let actor_hex = id_to_hex(&event.actor);
187 let public_key = match get_public_key(&actor_hex) {
188 Some(pk) => pk,
189 None => {
190 report.signature_errors.push(SignatureError {
191 event_id: event.event_id,
192 actor_id: actor_hex,
193 error: "public key not found".to_string(),
194 });
195 continue;
196 }
197 };
198
199 match verify_signature(event, &public_key) {
201 Ok(()) => {
202 report.signatures_valid += 1;
203 report.events_valid += 1;
204 }
205 Err(e) => {
206 report.signature_errors.push(SignatureError {
207 event_id: event.event_id,
208 actor_id: actor_hex,
209 error: e.to_string(),
210 });
211 }
212 }
213 }
214 }
215
216 Ok(report)
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222 use crate::types::event::EventKind;
223
224 #[test]
225 fn test_verify_event_hash_valid() {
226 let issue_id = [1u8; 16];
227 let actor = [2u8; 16];
228 let ts = 1700000000000u64;
229 let kind = EventKind::IssueCreated {
230 title: "Test".to_string(),
231 body: "Body".to_string(),
232 labels: vec![],
233 };
234
235 let event_id = compute_event_id(&issue_id, &actor, ts, None, &kind);
236 let event = Event::new(event_id, issue_id, actor, ts, None, kind);
237
238 assert!(verify_event_hash(&event).is_ok());
239 }
240
241 #[test]
242 fn test_verify_event_hash_invalid() {
243 let issue_id = [1u8; 16];
244 let actor = [2u8; 16];
245 let ts = 1700000000000u64;
246 let kind = EventKind::IssueCreated {
247 title: "Test".to_string(),
248 body: "Body".to_string(),
249 labels: vec![],
250 };
251
252 let event = Event::new([0u8; 32], issue_id, actor, ts, None, kind);
254
255 let result = verify_event_hash(&event);
256 assert!(matches!(result, Err(CorruptionKind::HashMismatch { .. })));
257 }
258
259 #[test]
260 fn test_integrity_report_is_healthy() {
261 let report = IntegrityReport::default();
262 assert!(report.is_healthy());
263
264 let mut report_with_error = IntegrityReport::default();
265 report_with_error.corrupt_events.push(CorruptEvent {
266 event_id: [0u8; 32],
267 issue_id: "test".to_string(),
268 kind: CorruptionKind::HashMismatch {
269 expected: [0u8; 32],
270 computed: [1u8; 32],
271 },
272 });
273 assert!(!report_with_error.is_healthy());
274 }
275}