1use crate::jcs::{canonicalize, JcsError};
11use sha2::{Digest, Sha256};
12
13#[derive(Debug, Clone)]
15pub struct EventChain {
16 head: [u8; 32],
17 count: u64,
18}
19
20impl EventChain {
21 pub fn new(session_id: &str) -> Self {
23 let mut h = Sha256::new();
24 h.update(b"av-genesis");
27 h.update(session_id.as_bytes());
28 Self {
29 head: h.finalize().into(),
30 count: 0,
31 }
32 }
33
34 pub fn append(&mut self, event: &serde_json::Value) -> Result<(), JcsError> {
36 let canon = canonicalize(event)?;
37 let mut h = Sha256::new();
38 h.update(self.head);
39 h.update(canon.as_bytes());
40 self.head = h.finalize().into();
41 self.count += 1;
42 Ok(())
43 }
44
45 pub fn head_hex(&self) -> String {
47 hex::encode(self.head)
48 }
49
50 pub fn count(&self) -> u64 {
52 self.count
53 }
54
55 pub fn compute(session_id: &str, events: &[serde_json::Value]) -> Result<Self, JcsError> {
57 let mut chain = Self::new(session_id);
58 for e in events {
59 chain.append(e)?;
60 }
61 Ok(chain)
62 }
63}
64
65#[cfg(test)]
66mod tests {
67 #![allow(
68 clippy::unwrap_used,
69 clippy::expect_used,
70 clippy::panic,
71 clippy::indexing_slicing
72 )]
73
74 use super::*;
75 use serde_json::json;
76
77 fn events() -> Vec<serde_json::Value> {
78 (0..5)
79 .map(|i| json!({"seq": i, "payload": format!("event-{i}")}))
80 .collect()
81 }
82
83 #[test]
84 fn deterministic() {
85 let a = EventChain::compute("sess", &events()).unwrap();
86 let b = EventChain::compute("sess", &events()).unwrap();
87 assert_eq!(a.head_hex(), b.head_hex());
88 assert_eq!(a.count(), 5);
89 }
90
91 #[test]
92 fn session_id_binds_the_genesis() {
93 let a = EventChain::compute("sess-1", &events()).unwrap();
94 let b = EventChain::compute("sess-2", &events()).unwrap();
95 assert_ne!(a.head_hex(), b.head_hex());
96 }
97
98 #[test]
99 fn tamper_any_event_changes_head() {
100 let baseline = EventChain::compute("s", &events()).unwrap().head_hex();
101 for i in 0..5 {
102 let mut evs = events();
103 evs[i]["payload"] = json!("tampered");
104 let h = EventChain::compute("s", &evs).unwrap().head_hex();
105 assert_ne!(h, baseline, "tamper at index {i} undetected");
106 }
107 }
108
109 #[test]
110 fn reorder_detected() {
111 let baseline = EventChain::compute("s", &events()).unwrap().head_hex();
112 let mut evs = events();
113 evs.swap(1, 3);
114 assert_ne!(EventChain::compute("s", &evs).unwrap().head_hex(), baseline);
115 }
116
117 #[test]
118 fn drop_detected() {
119 let baseline = EventChain::compute("s", &events()).unwrap().head_hex();
120 let mut evs = events();
121 evs.remove(2);
122 assert_ne!(EventChain::compute("s", &evs).unwrap().head_hex(), baseline);
123 }
124
125 #[test]
126 fn key_order_of_event_json_is_irrelevant() {
127 let a = vec![serde_json::from_str::<serde_json::Value>(r#"{"x":1,"y":2}"#).unwrap()];
128 let b = vec![serde_json::from_str::<serde_json::Value>(r#"{"y":2,"x":1}"#).unwrap()];
129 assert_eq!(
130 EventChain::compute("s", &a).unwrap().head_hex(),
131 EventChain::compute("s", &b).unwrap().head_hex(),
132 "JCS must make key order irrelevant"
133 );
134 }
135
136 #[test]
137 fn empty_chain_is_genesis_only() {
138 let c = EventChain::new("s");
139 assert_eq!(c.count(), 0);
140 assert_eq!(c.head_hex().len(), 64);
141 }
142}