Skip to main content

eventgraph/
event.rs

1use sha2::{Sha256, Digest};
2use serde_json::Value;
3use std::collections::BTreeMap;
4use uuid::Uuid;
5
6use crate::types::*;
7
8// ── Signer trait ───────────────────────────────────────────────────────
9
10pub trait Signer {
11    fn sign(&self, data: &[u8]) -> Signature;
12}
13
14pub struct NoopSigner;
15
16impl Signer for NoopSigner {
17    fn sign(&self, _data: &[u8]) -> Signature { Signature::zero() }
18}
19
20// ── Canonical form ─────────────────────────────────────────────────────
21
22pub fn canonical_content_json(content: &BTreeMap<String, Value>) -> String {
23    // Omit null values, match Go's number formatting (1.0 → 1)
24    let filtered: BTreeMap<&String, &Value> = content
25        .iter()
26        .filter(|(_, v)| !v.is_null())
27        .collect();
28    let mut s = String::from("{");
29    let mut first = true;
30    for (k, v) in &filtered {
31        if !first { s.push(','); }
32        first = false;
33        s.push('"');
34        s.push_str(k);
35        s.push_str("\":");
36        canonical_value_to_string(v, &mut s);
37    }
38    s.push('}');
39    s
40}
41
42fn canonical_value_to_string(v: &Value, s: &mut String) {
43    match v {
44        Value::Object(map) => {
45            // Recursively sort keys and omit nulls in nested objects
46            let filtered: BTreeMap<&String, &Value> = map
47                .iter()
48                .filter(|(_, v)| !v.is_null())
49                .collect();
50            s.push('{');
51            let mut first = true;
52            for (k, val) in &filtered {
53                if !first { s.push(','); }
54                first = false;
55                s.push('"');
56                s.push_str(k);
57                s.push_str("\":");
58                canonical_value_to_string(val, s);
59            }
60            s.push('}');
61        }
62        Value::Number(n) => {
63            if let Some(f) = n.as_f64() {
64                if f == (f as i64) as f64 && f.is_finite() {
65                    s.push_str(&(f as i64).to_string());
66                } else {
67                    s.push_str(&serde_json::to_string(v).unwrap());
68                }
69            } else {
70                s.push_str(&serde_json::to_string(v).unwrap());
71            }
72        }
73        Value::Array(arr) => {
74            s.push('[');
75            for (i, item) in arr.iter().enumerate() {
76                if i > 0 { s.push(','); }
77                canonical_value_to_string(item, s);
78            }
79            s.push(']');
80        }
81        _ => s.push_str(&serde_json::to_string(v).unwrap()),
82    }
83}
84
85pub fn canonical_form(
86    version: u32,
87    prev_hash: &str,
88    causes: &[&str],
89    event_id: &str,
90    event_type: &str,
91    source: &str,
92    conversation_id: &str,
93    timestamp_nanos: u64,
94    content_json: &str,
95) -> String {
96    let mut sorted: Vec<&str> = causes.to_vec();
97    sorted.sort();
98    let causes_str = sorted.join(",");
99    format!("{version}|{prev_hash}|{causes_str}|{event_id}|{event_type}|{source}|{conversation_id}|{timestamp_nanos}|{content_json}")
100}
101
102pub fn compute_hash(canonical: &str) -> Hash {
103    let mut hasher = Sha256::new();
104    hasher.update(canonical.as_bytes());
105    let result = hasher.finalize();
106    let hex: String = result.iter().map(|b| format!("{b:02x}")).collect();
107    Hash::new(hex).expect("SHA-256 always produces 64 hex chars")
108}
109
110// ── Event ──────────────────────────────────────────────────────────────
111
112#[derive(Debug, Clone)]
113pub struct Event {
114    pub version: u32,
115    pub id: EventId,
116    pub event_type: EventType,
117    pub timestamp_nanos: u64,
118    pub source: ActorId,
119    pub(crate) content: BTreeMap<String, Value>,
120    pub causes: NonEmpty<EventId>,
121    pub conversation_id: ConversationId,
122    pub hash: Hash,
123    pub prev_hash: Hash,
124    pub signature: Signature,
125}
126
127impl Event {
128    pub fn content(&self) -> BTreeMap<String, Value> {
129        self.content.clone()
130    }
131}
132
133// ── UUID v7 ────────────────────────────────────────────────────────────
134
135pub fn new_event_id() -> EventId {
136    let id = Uuid::now_v7();
137    EventId::new(id.to_string()).expect("UUID v7 is always valid")
138}
139
140// ── Event factories ────────────────────────────────────────────────────
141
142pub fn create_event(
143    event_type: EventType,
144    source: ActorId,
145    content: BTreeMap<String, Value>,
146    causes: Vec<EventId>,
147    conversation_id: ConversationId,
148    prev_hash: Hash,
149    signer: &dyn Signer,
150    version: u32,
151) -> Event {
152    let event_id = new_event_id();
153    let timestamp_nanos = std::time::SystemTime::now()
154        .duration_since(std::time::UNIX_EPOCH)
155        .unwrap()
156        .as_nanos() as u64;
157    let content_json = canonical_content_json(&content);
158
159    let cause_strs: Vec<&str> = causes.iter().map(|c| c.value()).collect();
160    let canon = canonical_form(
161        version, prev_hash.value(), &cause_strs,
162        event_id.value(), event_type.value(),
163        source.value(), conversation_id.value(),
164        timestamp_nanos, &content_json,
165    );
166
167    let hash = compute_hash(&canon);
168    let sig = signer.sign(canon.as_bytes());
169
170    Event {
171        version,
172        id: event_id,
173        event_type,
174        timestamp_nanos,
175        source,
176        content,
177        causes: NonEmpty::of(causes).expect("causes must be non-empty"),
178        conversation_id,
179        hash,
180        prev_hash,
181        signature: sig,
182    }
183}
184
185pub fn create_bootstrap(source: ActorId, signer: &dyn Signer, version: u32) -> Event {
186    let event_id = new_event_id();
187    let timestamp_nanos = std::time::SystemTime::now()
188        .duration_since(std::time::UNIX_EPOCH)
189        .unwrap()
190        .as_nanos() as u64;
191    let conversation_id = ConversationId::new(format!("conv_{}", source.value())).unwrap();
192
193    let now = chrono_like_utc();
194    let mut content = BTreeMap::new();
195    content.insert("ActorID".to_string(), Value::String(source.value().to_string()));
196    content.insert("ChainGenesis".to_string(), Value::String(Hash::zero().value().to_string()));
197    content.insert("Timestamp".to_string(), Value::String(now));
198
199    let content_json = canonical_content_json(&content);
200
201    let canon = canonical_form(
202        version, "", &[],
203        event_id.value(), "system.bootstrapped",
204        source.value(), conversation_id.value(),
205        timestamp_nanos, &content_json,
206    );
207
208    let hash = compute_hash(&canon);
209    let sig = signer.sign(canon.as_bytes());
210
211    Event {
212        version,
213        id: event_id.clone(),
214        event_type: EventType::new("system.bootstrapped").unwrap(),
215        timestamp_nanos,
216        source,
217        content,
218        causes: NonEmpty::of(vec![event_id]).expect("self-ref is non-empty"),
219        conversation_id,
220        hash,
221        prev_hash: Hash::zero(),
222        signature: sig,
223    }
224}
225
226fn chrono_like_utc() -> String {
227    use std::time::SystemTime;
228    let d = SystemTime::now()
229        .duration_since(std::time::UNIX_EPOCH)
230        .unwrap();
231    let secs = d.as_secs();
232    let days = secs / 86400;
233    let rem = secs % 86400;
234    let h = rem / 3600;
235    let m = (rem % 3600) / 60;
236    let s = rem % 60;
237
238    // Simplified date from days since epoch
239    let (year, month, day) = days_to_ymd(days);
240    format!("{year:04}-{month:02}-{day:02}T{h:02}:{m:02}:{s:02}Z")
241}
242
243fn days_to_ymd(days: u64) -> (u64, u64, u64) {
244    // Civil calendar from days since 1970-01-01
245    let z = days + 719468;
246    let era = z / 146097;
247    let doe = z - era * 146097;
248    let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
249    let y = yoe + era * 400;
250    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
251    let mp = (5 * doy + 2) / 153;
252    let d = doy - (153 * mp + 2) / 5 + 1;
253    let m = if mp < 10 { mp + 3 } else { mp - 9 };
254    let y = if m <= 2 { y + 1 } else { y };
255    (y, m, d)
256}