1use std::{fmt, io::Write};
24
25use serde::{Deserialize, Serialize};
26
27use crate::{
28 crypto,
29 types::{CorrelationId, Did, PqPublicKey, PublicKey, Signature, Timestamp},
30};
31
32pub const EVENT_SIGNING_DOMAIN: &str = "exo.core.event.signable.v1";
34const EVENT_SIGNING_SCHEMA_VERSION: u16 = 1;
35
36#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
42pub enum EventType {
43 TransactionStateChanged,
45 IdentityResolved,
47 ConsentGranted,
49 ConsentRevoked,
51 InvariantChecked,
53 InvariantViolated,
55 GovernanceDecision,
57 EscalationTriggered,
59 SybilAlert,
61 KeyRotated,
63 EntityRegistered,
65 AuditEntry,
67 Custom(String),
69}
70
71#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
77pub struct Event {
78 pub id: CorrelationId,
80 pub timestamp: Timestamp,
82 pub event_type: EventType,
84 pub payload: Vec<u8>,
86 pub source_did: Did,
88 pub signature: Signature,
90}
91
92impl fmt::Debug for Event {
93 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94 f.debug_struct("Event")
95 .field("id", &self.id)
96 .field("timestamp", &self.timestamp)
97 .field("event_type", &self.event_type)
98 .field("payload_len", &self.payload.len())
99 .field("source_did", &self.source_did)
100 .field("signature_algorithm", &self.signature.algorithm())
101 .field("signature", &"[REDACTED]")
102 .finish()
103 }
104}
105
106impl Event {
107 pub fn write_signable_bytes<W: Write>(&self, writer: W) -> crate::Result<()> {
117 #[derive(Serialize)]
118 struct Signable<'a> {
119 domain: &'static str,
120 schema_version: u16,
121 id: &'a CorrelationId,
122 timestamp: &'a Timestamp,
123 event_type: &'a EventType,
124 payload: &'a [u8],
125 source_did: &'a Did,
126 }
127 let s = Signable {
128 domain: EVENT_SIGNING_DOMAIN,
129 schema_version: EVENT_SIGNING_SCHEMA_VERSION,
130 id: &self.id,
131 timestamp: &self.timestamp,
132 event_type: &self.event_type,
133 payload: &self.payload,
134 source_did: &self.source_did,
135 };
136 ciborium::into_writer(&s, writer)?;
137 Ok(())
138 }
139
140 pub fn signable_bytes(&self) -> crate::Result<Vec<u8>> {
149 let mut buf = Vec::new();
150 self.write_signable_bytes(&mut buf)?;
151 Ok(buf)
152 }
153}
154
155#[must_use]
157pub fn verify_event(event: &Event, public_key: &PublicKey) -> bool {
158 let Ok(bytes) = event.signable_bytes() else {
159 return false;
160 };
161 crypto::verify(&bytes, &event.signature, public_key)
162}
163
164#[must_use]
166pub fn verify_event_pq(event: &Event, public_key: &PqPublicKey) -> bool {
167 let Ok(bytes) = event.signable_bytes() else {
168 return false;
169 };
170 crypto::verify_pq(&bytes, &event.signature, public_key)
171}
172
173#[must_use]
178pub fn verify_event_hybrid(
179 event: &Event,
180 classical_public_key: &PublicKey,
181 pq_public_key: &PqPublicKey,
182) -> bool {
183 let Ok(bytes) = event.signable_bytes() else {
184 return false;
185 };
186 crypto::verify_hybrid(
187 &bytes,
188 &event.signature,
189 classical_public_key,
190 pq_public_key,
191 )
192}
193
194pub fn create_signed_event(
200 id: CorrelationId,
201 timestamp: Timestamp,
202 event_type: EventType,
203 payload: Vec<u8>,
204 source_did: Did,
205 secret_key: &crate::types::SecretKey,
206) -> crate::Result<Event> {
207 let mut event = Event {
209 id,
210 timestamp,
211 event_type,
212 payload,
213 source_did,
214 signature: Signature::from_bytes([0u8; 64]),
215 };
216 let bytes = event.signable_bytes()?;
217 event.signature = crypto::sign(&bytes, secret_key);
218 Ok(event)
219}
220
221#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
234pub enum EventPayload {
235 Genesis { network_id: String },
237 IdentityCreated { did_doc_cid: String },
239 DecisionCreated {
242 decision_id: crate::Hash256,
243 title: String,
244 decision_class: String,
245 constitution_hash: crate::Hash256,
246 },
247 DecisionAdvanced {
249 decision_id: crate::Hash256,
250 from_status: String,
251 to_status: String,
252 },
253 VoteCast {
255 decision_id: crate::Hash256,
256 voter: Did,
257 choice: String,
258 },
259 DelegationGranted {
261 delegation_id: crate::Hash256,
262 delegator: Did,
263 delegatee: Did,
264 expires_at: u64,
265 },
266 DelegationRevoked {
268 delegation_id: crate::Hash256,
269 revoked_at: u64,
270 },
271 ConstitutionAmended {
273 from_version: String,
274 to_version: String,
275 amendment_hash: crate::Hash256,
276 },
277 ChallengeRaised {
279 challenge_id: crate::Hash256,
280 contested_decision_id: crate::Hash256,
281 grounds: String,
282 },
283 EmergencyActionTaken {
285 emergency_id: crate::Hash256,
286 decision_id: crate::Hash256,
287 ratification_deadline: u64,
288 },
289 ConflictDisclosed {
291 decision_id: crate::Hash256,
292 discloser: Did,
293 },
294 HolonCreated {
297 holon_did: Did,
298 sponsor_did: Did,
299 genesis_model_cid: crate::Hash256,
300 },
301 HolonActivated {
303 holon_did: Did,
304 approver_did: Did,
305 approval_level: u32,
306 },
307 HolonActionProposed {
309 holon_did: Did,
310 action_hash: crate::Hash256,
311 reasoning_trace_cid: crate::Hash256,
312 },
313 HolonActionVerified {
315 holon_did: Did,
316 action_hash: crate::Hash256,
317 cgr_proof_hash: crate::Hash256,
318 },
319 HolonActionExecuted {
321 holon_did: Did,
322 action_hash: crate::Hash256,
323 outcome_hash: crate::Hash256,
324 },
325 HolonSuspended {
327 holon_did: Did,
328 reason: String,
329 suspended_by: Did,
330 },
331 HolonReinstated {
333 holon_did: Did,
334 reinstated_by: Did,
335 remediation_evidence_cid: crate::Hash256,
336 },
337 HolonSunset {
339 holon_did: Did,
340 reason: String,
341 initiated_by: Did,
342 },
343 CgrProofIssued {
346 proof_id: u64,
347 invariants_checked: u32,
348 registry_hash: crate::Hash256,
349 },
350 Opaque(Vec<u8>),
352}
353
354pub fn compute_event_id<T: serde::Serialize>(envelope: &T) -> crate::Result<crate::Hash256> {
365 crate::hash::hash_structured(envelope)
366}
367
368#[cfg(test)]
373mod tests {
374 use super::*;
375 use crate::{
376 crypto::{self, KeyPair, PqKeyPair},
377 types::{CorrelationId, Did, Timestamp},
378 };
379
380 macro_rules! correlation_id {
381 () => {
382 CorrelationId::from_uuid(uuid::Uuid::from_u128(u128::from(line!())))
383 };
384 }
385
386 fn make_event(kp: &KeyPair) -> Event {
387 let did = Did::new("did:exo:test-source").expect("valid");
388 create_signed_event(
389 correlation_id!(),
390 Timestamp::new(1000, 0),
391 EventType::AuditEntry,
392 b"test payload".to_vec(),
393 did,
394 kp.secret_key(),
395 )
396 .expect("sign event")
397 }
398
399 fn make_unsigned_event(source_did: Did) -> Event {
400 Event {
401 id: correlation_id!(),
402 timestamp: Timestamp::new(1000, 0),
403 event_type: EventType::AuditEntry,
404 payload: b"test payload".to_vec(),
405 source_did,
406 signature: Signature::Empty,
407 }
408 }
409
410 #[test]
411 fn create_and_verify_event() {
412 let kp = KeyPair::generate();
413 let event = make_event(&kp);
414 assert!(verify_event(&event, kp.public_key()));
415 }
416
417 #[test]
418 fn verify_fails_wrong_key() {
419 let kp1 = KeyPair::generate();
420 let kp2 = KeyPair::generate();
421 let event = make_event(&kp1);
422 assert!(!verify_event(&event, kp2.public_key()));
423 }
424
425 #[test]
426 fn verify_fails_tampered_payload() {
427 let kp = KeyPair::generate();
428 let mut event = make_event(&kp);
429 event.payload = b"tampered".to_vec();
430 assert!(!verify_event(&event, kp.public_key()));
431 }
432
433 #[test]
434 fn verify_fails_tampered_timestamp() {
435 let kp = KeyPair::generate();
436 let mut event = make_event(&kp);
437 event.timestamp = Timestamp::new(9999, 99);
438 assert!(!verify_event(&event, kp.public_key()));
439 }
440
441 #[test]
442 fn verify_fails_tampered_event_type() {
443 let kp = KeyPair::generate();
444 let mut event = make_event(&kp);
445 event.event_type = EventType::SybilAlert;
446 assert!(!verify_event(&event, kp.public_key()));
447 }
448
449 #[test]
450 fn verify_event_pq_accepts_valid_post_quantum_signature() {
451 let pq = PqKeyPair::generate();
452 let did = Did::new("did:exo:pq-source").expect("valid");
453 let mut event = make_unsigned_event(did);
454 let bytes = event.signable_bytes().expect("serialize signable bytes");
455 event.signature = pq.sign(&bytes).expect("sign pq event");
456
457 assert!(verify_event_pq(&event, pq.public_key()));
458 assert!(
459 !verify_event(&event, &PublicKey::from_bytes([7u8; 32])),
460 "classical verifier must not accept a PQ event signature"
461 );
462 }
463
464 #[test]
465 fn verify_event_pq_rejects_wrong_key_and_tamper() {
466 let pq = PqKeyPair::generate();
467 let wrong_pq = PqKeyPair::generate();
468 let did = Did::new("did:exo:pq-source").expect("valid");
469 let mut event = make_unsigned_event(did);
470 let bytes = event.signable_bytes().expect("serialize signable bytes");
471 event.signature = pq.sign(&bytes).expect("sign pq event");
472
473 assert!(!verify_event_pq(&event, wrong_pq.public_key()));
474
475 event.payload = b"tampered".to_vec();
476 assert!(!verify_event_pq(&event, pq.public_key()));
477 }
478
479 #[test]
480 fn verify_event_hybrid_accepts_valid_dual_signature() {
481 let classical = KeyPair::generate();
482 let (pq_public, pq_secret) = crypto::generate_pq_keypair();
483 let did = Did::new("did:exo:hybrid-source").expect("valid");
484 let mut event = make_unsigned_event(did);
485 let bytes = event.signable_bytes().expect("serialize signable bytes");
486 event.signature = crypto::sign_hybrid(&bytes, classical.secret_key(), &pq_secret)
487 .expect("sign hybrid event");
488
489 assert!(verify_event_hybrid(
490 &event,
491 classical.public_key(),
492 &pq_public
493 ));
494 assert!(
495 !verify_event(&event, classical.public_key()),
496 "classical verifier must not accept a hybrid event signature"
497 );
498 }
499
500 #[test]
501 fn verify_event_hybrid_rejects_wrong_keys_and_tamper() {
502 let classical = KeyPair::generate();
503 let wrong_classical = KeyPair::generate();
504 let (pq_public, pq_secret) = crypto::generate_pq_keypair();
505 let (wrong_pq_public, _) = crypto::generate_pq_keypair();
506 let did = Did::new("did:exo:hybrid-source").expect("valid");
507 let mut event = make_unsigned_event(did);
508 let bytes = event.signable_bytes().expect("serialize signable bytes");
509 event.signature = crypto::sign_hybrid(&bytes, classical.secret_key(), &pq_secret)
510 .expect("sign hybrid event");
511
512 assert!(!verify_event_hybrid(
513 &event,
514 wrong_classical.public_key(),
515 &pq_public
516 ));
517 assert!(!verify_event_hybrid(
518 &event,
519 classical.public_key(),
520 &wrong_pq_public
521 ));
522
523 event.event_type = EventType::SybilAlert;
524 assert!(!verify_event_hybrid(
525 &event,
526 classical.public_key(),
527 &pq_public
528 ));
529 }
530
531 #[test]
532 fn event_type_serde_roundtrip() {
533 let types = vec![
534 EventType::TransactionStateChanged,
535 EventType::IdentityResolved,
536 EventType::ConsentGranted,
537 EventType::ConsentRevoked,
538 EventType::InvariantChecked,
539 EventType::InvariantViolated,
540 EventType::GovernanceDecision,
541 EventType::EscalationTriggered,
542 EventType::SybilAlert,
543 EventType::KeyRotated,
544 EventType::EntityRegistered,
545 EventType::AuditEntry,
546 EventType::Custom("my-event".into()),
547 ];
548 for t in &types {
549 let json = serde_json::to_string(t).expect("ser");
550 let t2: EventType = serde_json::from_str(&json).expect("de");
551 assert_eq!(t, &t2);
552 }
553 }
554
555 #[test]
556 fn event_serde_roundtrip() {
557 let kp = KeyPair::generate();
558 let event = make_event(&kp);
559 let json = serde_json::to_string(&event).expect("ser");
560 let event2: Event = serde_json::from_str(&json).expect("de");
561 assert_eq!(event, event2);
562 assert!(verify_event(&event2, kp.public_key()));
564 }
565
566 #[test]
567 fn signable_bytes_deterministic() {
568 let kp = KeyPair::generate();
569 let event = make_event(&kp);
570 let b1 = event.signable_bytes().expect("serialize signable bytes");
571 let b2 = event.signable_bytes().expect("serialize signable bytes");
572 assert_eq!(b1, b2);
573 }
574
575 #[test]
576 fn signable_bytes_are_domain_separated_and_versioned_cbor() {
577 #[derive(Deserialize)]
578 struct EventSignableEnvelope {
579 domain: String,
580 schema_version: u16,
581 id: CorrelationId,
582 timestamp: Timestamp,
583 event_type: EventType,
584 payload: Vec<u8>,
585 source_did: Did,
586 }
587
588 let kp = KeyPair::generate();
589 let event = make_event(&kp);
590 let bytes = event.signable_bytes().expect("serialize signable bytes");
591 let envelope: EventSignableEnvelope =
592 ciborium::from_reader(&bytes[..]).expect("domain-separated event signing payload");
593
594 assert_eq!(envelope.domain, "exo.core.event.signable.v1");
595 assert_eq!(envelope.schema_version, 1);
596 assert_eq!(envelope.id, event.id);
597 assert_eq!(envelope.timestamp, event.timestamp);
598 assert_eq!(envelope.event_type, event.event_type);
599 assert_eq!(envelope.payload, event.payload);
600 assert_eq!(envelope.source_did, event.source_did);
601 }
602
603 #[test]
604 fn signable_bytes_writer_error_is_returned() {
605 struct FailingWriter;
606
607 impl std::io::Write for FailingWriter {
608 fn write(&mut self, _buf: &[u8]) -> std::io::Result<usize> {
609 Err(std::io::Error::other("intentional signable writer failure"))
610 }
611
612 fn flush(&mut self) -> std::io::Result<()> {
613 Ok(())
614 }
615 }
616
617 let kp = KeyPair::generate();
618 let event = make_event(&kp);
619 let error = event.write_signable_bytes(FailingWriter).unwrap_err();
620 assert!(matches!(error, crate::ExoError::SerializationError { .. }));
621 }
622
623 #[test]
624 fn event_type_ord() {
625 let a = EventType::AuditEntry;
626 let b = EventType::SybilAlert;
627 let _ = a.cmp(&b);
629 }
630
631 #[test]
632 fn event_type_hash() {
633 use std::hash::{Hash, Hasher};
634 let t = EventType::KeyRotated;
635 let mut h = std::hash::DefaultHasher::new();
636 t.hash(&mut h);
637 let _ = h.finish();
638 }
639
640 #[test]
641 fn event_with_empty_payload() {
642 let kp = KeyPair::generate();
643 let did = Did::new("did:exo:empty-payload").expect("valid");
644 let event = create_signed_event(
645 correlation_id!(),
646 Timestamp::new(500, 1),
647 EventType::Custom("empty".into()),
648 Vec::new(),
649 did,
650 kp.secret_key(),
651 )
652 .expect("sign event");
653 assert!(verify_event(&event, kp.public_key()));
654 }
655
656 #[test]
657 fn event_with_large_payload() {
658 let kp = KeyPair::generate();
659 let did = Did::new("did:exo:large-payload").expect("valid");
660 let payload = vec![0xab_u8; 10_000];
661 let event = create_signed_event(
662 correlation_id!(),
663 Timestamp::new(500, 1),
664 EventType::AuditEntry,
665 payload,
666 did,
667 kp.secret_key(),
668 )
669 .expect("sign event");
670 assert!(verify_event(&event, kp.public_key()));
671 }
672
673 #[test]
674 fn event_debug_format() {
675 let kp = KeyPair::generate();
676 let event = make_event(&kp);
677 let dbg = format!("{event:?}");
678 let raw_payload_debug = format!("{:?}", event.payload);
679 let raw_signature_debug = format!("{:?}", event.signature);
680
681 assert!(dbg.contains("Event"));
682 assert!(dbg.contains("payload_len"));
683 assert!(!dbg.contains(&raw_payload_debug));
684 assert!(!dbg.contains(&raw_signature_debug));
685 assert!(!dbg.contains("signature: Signature"));
686 }
687
688 #[test]
693 fn event_payload_serde_roundtrip() {
694 let payloads = vec![
695 EventPayload::Genesis {
696 network_id: "exochain-mainnet".into(),
697 },
698 EventPayload::IdentityCreated {
699 did_doc_cid: "bafy...".into(),
700 },
701 EventPayload::DecisionCreated {
702 decision_id: crate::Hash256::digest(b"decision-1"),
703 title: "Governance Reform".into(),
704 decision_class: "Constitutional".into(),
705 constitution_hash: crate::Hash256::digest(b"constitution"),
706 },
707 EventPayload::VoteCast {
708 decision_id: crate::Hash256::digest(b"decision-1"),
709 voter: Did::new("did:exo:voter").expect("valid"),
710 choice: "approve".into(),
711 },
712 EventPayload::HolonCreated {
713 holon_did: Did::new("did:exo:holon-1").expect("valid"),
714 sponsor_did: Did::new("did:exo:sponsor").expect("valid"),
715 genesis_model_cid: crate::Hash256::digest(b"model"),
716 },
717 EventPayload::CgrProofIssued {
718 proof_id: 42,
719 invariants_checked: 8,
720 registry_hash: crate::Hash256::digest(b"registry"),
721 },
722 EventPayload::Opaque(vec![1, 2, 3]),
723 ];
724 for payload in &payloads {
725 let json = serde_json::to_string(payload).expect("serialize");
726 let deserialized: EventPayload = serde_json::from_str(&json).expect("deserialize");
727 assert_eq!(payload, &deserialized);
728 }
729 }
730
731 #[test]
732 fn compute_event_id_deterministic() {
733 let payload = EventPayload::Genesis {
734 network_id: "test-net".into(),
735 };
736 let id1 = compute_event_id(&payload).expect("compute");
737 let id2 = compute_event_id(&payload).expect("compute");
738 assert_eq!(id1, id2);
739 }
740
741 #[test]
742 fn compute_event_id_different_payloads() {
743 let p1 = EventPayload::Genesis {
744 network_id: "net-a".into(),
745 };
746 let p2 = EventPayload::Genesis {
747 network_id: "net-b".into(),
748 };
749 let id1 = compute_event_id(&p1).expect("compute");
750 let id2 = compute_event_id(&p2).expect("compute");
751 assert_ne!(id1, id2);
752 }
753
754 #[test]
755 fn event_payload_all_governance_variants() {
756 let variants: Vec<EventPayload> = vec![
758 EventPayload::DecisionAdvanced {
759 decision_id: crate::Hash256::ZERO,
760 from_status: "Draft".into(),
761 to_status: "Submitted".into(),
762 },
763 EventPayload::DelegationGranted {
764 delegation_id: crate::Hash256::ZERO,
765 delegator: Did::new("did:exo:alice").expect("valid"),
766 delegatee: Did::new("did:exo:bob").expect("valid"),
767 expires_at: 1_000_000,
768 },
769 EventPayload::DelegationRevoked {
770 delegation_id: crate::Hash256::ZERO,
771 revoked_at: 2_000_000,
772 },
773 EventPayload::ConstitutionAmended {
774 from_version: "1.0.0".into(),
775 to_version: "1.1.0".into(),
776 amendment_hash: crate::Hash256::ZERO,
777 },
778 EventPayload::ChallengeRaised {
779 challenge_id: crate::Hash256::ZERO,
780 contested_decision_id: crate::Hash256::ZERO,
781 grounds: "Procedural violation".into(),
782 },
783 EventPayload::EmergencyActionTaken {
784 emergency_id: crate::Hash256::ZERO,
785 decision_id: crate::Hash256::ZERO,
786 ratification_deadline: 86400,
787 },
788 EventPayload::ConflictDisclosed {
789 decision_id: crate::Hash256::ZERO,
790 discloser: Did::new("did:exo:discloser").expect("valid"),
791 },
792 ];
793 for v in &variants {
794 let json = serde_json::to_string(v).expect("ser");
795 let _: EventPayload = serde_json::from_str(&json).expect("de");
796 }
797 }
798
799 #[test]
800 fn event_payload_all_holon_variants() {
801 let holon = Did::new("did:exo:holon").expect("valid");
802 let actor = Did::new("did:exo:actor").expect("valid");
803 let variants: Vec<EventPayload> = vec![
804 EventPayload::HolonActivated {
805 holon_did: holon.clone(),
806 approver_did: actor.clone(),
807 approval_level: 3,
808 },
809 EventPayload::HolonActionProposed {
810 holon_did: holon.clone(),
811 action_hash: crate::Hash256::ZERO,
812 reasoning_trace_cid: crate::Hash256::ZERO,
813 },
814 EventPayload::HolonActionVerified {
815 holon_did: holon.clone(),
816 action_hash: crate::Hash256::ZERO,
817 cgr_proof_hash: crate::Hash256::ZERO,
818 },
819 EventPayload::HolonActionExecuted {
820 holon_did: holon.clone(),
821 action_hash: crate::Hash256::ZERO,
822 outcome_hash: crate::Hash256::ZERO,
823 },
824 EventPayload::HolonSuspended {
825 holon_did: holon.clone(),
826 reason: "anomaly detected".into(),
827 suspended_by: actor.clone(),
828 },
829 EventPayload::HolonReinstated {
830 holon_did: holon.clone(),
831 reinstated_by: actor.clone(),
832 remediation_evidence_cid: crate::Hash256::ZERO,
833 },
834 EventPayload::HolonSunset {
835 holon_did: holon,
836 reason: "end of lifecycle".into(),
837 initiated_by: actor,
838 },
839 ];
840 for v in &variants {
841 let json = serde_json::to_string(v).expect("ser");
842 let _: EventPayload = serde_json::from_str(&json).expect("de");
843 }
844 }
845}