Skip to main content

pv_receipts/
chain.rs

1//! Session event-chain hashing.
2//!
3//! `h₀ = SHA-256("av-genesis" ‖ session_id)`;
4//! `hᵢ = SHA-256(hᵢ₋₁ ‖ JCS(eventᵢ))`.
5//!
6//! Ordering is driven by the per-session sequence number (events are fed in
7//! seq order) — never by wall-clock timestamps (D13.6). Any tamper, drop,
8//! reorder, or substitution changes the head hash and breaks verification.
9
10use crate::jcs::{canonicalize, JcsError};
11use sha2::{Digest, Sha256};
12
13/// Incrementally computed hash chain over a session's OCSF events.
14#[derive(Debug, Clone)]
15pub struct EventChain {
16    head: [u8; 32],
17    count: u64,
18}
19
20impl EventChain {
21    /// Start a chain for `session_id`.
22    pub fn new(session_id: &str) -> Self {
23        let mut h = Sha256::new();
24        // Wire format: every chain head ever sealed starts here; the
25        // label predates the ProofVisor name and must never be renamed.
26        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    /// Append an event (as JSON) to the chain.
35    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    /// Current head hash, hex-encoded.
46    pub fn head_hex(&self) -> String {
47        hex::encode(self.head)
48    }
49
50    /// Number of appended events.
51    pub fn count(&self) -> u64 {
52        self.count
53    }
54
55    /// Recompute a chain from scratch over `events` (offline verification).
56    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}