1use crate::core::event::Event;
4use crate::store::Store;
5use anyhow::Result;
6use rusqlite::{OptionalExtension, params};
7use serde::{Deserialize, Serialize};
8
9const GENESIS: &str = "blake3:genesis";
10
11#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
12pub struct HashChainReport {
13 pub checked_events: u64,
14 pub verified_events: u64,
15 pub unverifiable_events: u64,
16 pub broken_events: Vec<HashBreak>,
17}
18
19#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
20pub struct HashBreak {
21 pub session_id: String,
22 pub seq: u64,
23 pub reason: String,
24}
25
26pub fn store_event_hash(store: &Store, event: &Event) -> Result<()> {
27 let prev = previous_hash(store, &event.session_id, event.seq)?;
28 let hash = event_hash(&prev, event)?;
29 store.conn().execute(
30 "INSERT INTO event_hashes(session_id, seq, prev_hash, event_hash)
31 VALUES (?1, ?2, ?3, ?4)
32 ON CONFLICT(session_id, seq) DO UPDATE SET
33 prev_hash=excluded.prev_hash, event_hash=excluded.event_hash",
34 params![event.session_id, event.seq as i64, prev, hash],
35 )?;
36 Ok(())
37}
38
39pub fn verify(store: &Store, workspace: &str, session_id: Option<&str>) -> Result<HashChainReport> {
40 session_ids(store, workspace, session_id)?.iter().try_fold(
41 HashChainReport::default(),
42 |mut report, id| {
43 verify_session(store, id, &mut report)?;
44 Ok(report)
45 },
46 )
47}
48
49fn verify_session(store: &Store, session_id: &str, report: &mut HashChainReport) -> Result<()> {
50 let mut prev = None;
51 for event in store.list_events_for_session(session_id)? {
52 report.checked_events += 1;
53 match stored_hash(store, session_id, event.seq)? {
54 Some(row) => verify_row(report, &mut prev, &event, row)?,
55 None => report.unverifiable_events += 1,
56 }
57 }
58 Ok(())
59}
60
61fn verify_row(
62 report: &mut HashChainReport,
63 prev: &mut Option<String>,
64 event: &Event,
65 row: (String, String),
66) -> Result<()> {
67 let (stored_prev, stored_hash) = row;
68 if prev.as_deref().is_some_and(|p| p != stored_prev) {
69 push_break(report, event, "previous hash mismatch");
70 }
71 if event_hash(&stored_prev, event)? != stored_hash {
72 push_break(report, event, "event hash mismatch");
73 } else {
74 report.verified_events += 1;
75 }
76 *prev = Some(stored_hash);
77 Ok(())
78}
79
80fn push_break(report: &mut HashChainReport, event: &Event, reason: &str) {
81 report.broken_events.push(HashBreak {
82 session_id: event.session_id.clone(),
83 seq: event.seq,
84 reason: reason.to_string(),
85 });
86}
87
88fn session_ids(store: &Store, workspace: &str, session_id: Option<&str>) -> Result<Vec<String>> {
89 Ok(match session_id {
90 Some(id) => vec![id.to_string()],
91 None => store
92 .list_sessions(workspace)?
93 .into_iter()
94 .map(|session| session.id)
95 .collect(),
96 })
97}
98
99fn stored_hash(store: &Store, session_id: &str, seq: u64) -> Result<Option<(String, String)>> {
100 store
101 .conn()
102 .query_row(
103 "SELECT prev_hash, event_hash FROM event_hashes WHERE session_id=?1 AND seq=?2",
104 params![session_id, seq as i64],
105 |row| Ok((row.get(0)?, row.get(1)?)),
106 )
107 .optional()
108 .map_err(Into::into)
109}
110
111fn previous_hash(store: &Store, session_id: &str, seq: u64) -> Result<String> {
112 store
113 .conn()
114 .query_row(
115 "SELECT event_hash FROM event_hashes
116 WHERE session_id=?1 AND seq < ?2 ORDER BY seq DESC LIMIT 1",
117 params![session_id, seq as i64],
118 |row| row.get(0),
119 )
120 .optional()
121 .map(|v| v.unwrap_or_else(|| GENESIS.to_string()))
122 .map_err(Into::into)
123}
124
125fn event_hash(prev: &str, event: &Event) -> Result<String> {
126 let mut hasher = blake3::Hasher::new();
127 hasher.update(prev.as_bytes());
128 hasher.update(&serde_json::to_vec(event)?);
129 Ok(format!(
130 "blake3:{}",
131 hex::encode(hasher.finalize().as_bytes())
132 ))
133}