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