use crate::hash::compute_event_id;
use crate::signing::verify_signature;
use crate::store::GriteStore;
use crate::types::event::Event;
use crate::types::ids::{id_to_hex, EventId};
use crate::GriteError;
#[derive(Debug, Default)]
pub struct IntegrityReport {
pub events_checked: usize,
pub events_valid: usize,
pub corrupt_events: Vec<CorruptEvent>,
pub signatures_checked: usize,
pub signatures_valid: usize,
pub signature_errors: Vec<SignatureError>,
}
#[derive(Debug)]
pub struct CorruptEvent {
pub event_id: EventId,
pub issue_id: String,
pub kind: CorruptionKind,
}
#[derive(Debug)]
pub enum CorruptionKind {
HashMismatch {
expected: EventId,
computed: EventId,
},
MissingParent { parent_id: EventId },
}
#[derive(Debug)]
pub struct SignatureError {
pub event_id: EventId,
pub actor_id: String,
pub error: String,
}
impl IntegrityReport {
pub fn is_healthy(&self) -> bool {
self.corrupt_events.is_empty() && self.signature_errors.is_empty()
}
pub fn corruption_count(&self) -> usize {
self.corrupt_events.len()
}
pub fn signature_error_count(&self) -> usize {
self.signature_errors.len()
}
}
pub fn verify_event_hash(event: &Event) -> Result<(), CorruptionKind> {
let computed = compute_event_id(
&event.issue_id,
&event.actor,
event.ts_unix_ms,
event.parent.as_ref(),
&event.kind,
);
if computed != event.event_id {
return Err(CorruptionKind::HashMismatch {
expected: event.event_id,
computed,
});
}
Ok(())
}
pub fn check_store_integrity(
store: &GriteStore,
verify_parents: bool,
) -> Result<IntegrityReport, GriteError> {
let mut report = IntegrityReport::default();
let issues = store.list_issues(&Default::default())?;
for issue_summary in &issues {
let events = store.get_issue_events(&issue_summary.issue_id)?;
let event_ids: std::collections::HashSet<EventId> =
events.iter().map(|e| e.event_id).collect();
for event in &events {
report.events_checked += 1;
match verify_event_hash(event) {
Ok(()) => {
report.events_valid += 1;
}
Err(kind) => {
report.corrupt_events.push(CorruptEvent {
event_id: event.event_id,
issue_id: id_to_hex(&event.issue_id),
kind,
});
continue;
}
}
if verify_parents {
if let Some(parent_id) = &event.parent {
if !event_ids.contains(parent_id) {
report.corrupt_events.push(CorruptEvent {
event_id: event.event_id,
issue_id: id_to_hex(&event.issue_id),
kind: CorruptionKind::MissingParent {
parent_id: *parent_id,
},
});
}
}
}
}
}
Ok(report)
}
pub fn verify_store_signatures<F>(
store: &GriteStore,
get_public_key: F,
) -> Result<IntegrityReport, GriteError>
where
F: Fn(&str) -> Option<String>,
{
let mut report = IntegrityReport::default();
let issues = store.list_issues(&Default::default())?;
for issue_summary in &issues {
let events = store.get_issue_events(&issue_summary.issue_id)?;
for event in &events {
report.events_checked += 1;
if event.sig.is_none() {
report.signature_errors.push(SignatureError {
event_id: event.event_id,
actor_id: id_to_hex(&event.actor),
error: "signature missing".to_string(),
});
continue;
}
report.signatures_checked += 1;
let actor_hex = id_to_hex(&event.actor);
let public_key = match get_public_key(&actor_hex) {
Some(pk) => pk,
None => {
report.signature_errors.push(SignatureError {
event_id: event.event_id,
actor_id: actor_hex,
error: "public key not found".to_string(),
});
continue;
}
};
match verify_signature(event, &public_key) {
Ok(()) => {
report.signatures_valid += 1;
report.events_valid += 1;
}
Err(e) => {
report.signature_errors.push(SignatureError {
event_id: event.event_id,
actor_id: actor_hex,
error: e.to_string(),
});
}
}
}
}
Ok(report)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::event::EventKind;
#[test]
fn test_verify_event_hash_valid() {
let issue_id = [1u8; 16];
let actor = [2u8; 16];
let ts = 1700000000000u64;
let kind = EventKind::IssueCreated {
title: "Test".to_string(),
body: "Body".to_string(),
labels: vec![],
};
let event_id = compute_event_id(&issue_id, &actor, ts, None, &kind);
let event = Event::new(event_id, issue_id, actor, ts, None, kind);
assert!(verify_event_hash(&event).is_ok());
}
#[test]
fn test_verify_event_hash_invalid() {
let issue_id = [1u8; 16];
let actor = [2u8; 16];
let ts = 1700000000000u64;
let kind = EventKind::IssueCreated {
title: "Test".to_string(),
body: "Body".to_string(),
labels: vec![],
};
let event = Event::new([0u8; 32], issue_id, actor, ts, None, kind);
let result = verify_event_hash(&event);
assert!(matches!(result, Err(CorruptionKind::HashMismatch { .. })));
}
#[test]
fn test_integrity_report_is_healthy() {
let report = IntegrityReport::default();
assert!(report.is_healthy());
let mut report_with_error = IntegrityReport::default();
report_with_error.corrupt_events.push(CorruptEvent {
event_id: [0u8; 32],
issue_id: "test".to_string(),
kind: CorruptionKind::HashMismatch {
expected: [0u8; 32],
computed: [1u8; 32],
},
});
assert!(!report_with_error.is_healthy());
}
}