1use sha2::{Sha256, Digest};
2use serde_json::Value;
3use std::collections::BTreeMap;
4use uuid::Uuid;
5
6use crate::types::*;
7
8pub 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
20pub fn canonical_content_json(content: &BTreeMap<String, Value>) -> String {
23 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 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#[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
133pub 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
140pub 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 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 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}