Skip to main content

exo_core/
events.rs

1// Copyright 2026 Exochain Foundation
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at:
6//
7//     https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14//
15// SPDX-License-Identifier: Apache-2.0
16
17//! Event system for EXOCHAIN.
18//!
19//! Every significant action produces a signed, timestamped event that can
20//! be verified independently.  Events carry a CBOR-encoded payload and are
21//! attributed to a DID via an Ed25519 signature.
22
23use std::{fmt, io::Write};
24
25use serde::{Deserialize, Serialize};
26
27use crate::{
28    crypto,
29    types::{CorrelationId, Did, PqPublicKey, PublicKey, Signature, Timestamp},
30};
31
32/// Domain separation tag for EXOCHAIN event signatures.
33pub const EVENT_SIGNING_DOMAIN: &str = "exo.core.event.signable.v1";
34const EVENT_SIGNING_SCHEMA_VERSION: u16 = 1;
35
36// ---------------------------------------------------------------------------
37// EventType
38// ---------------------------------------------------------------------------
39
40/// Classification of system events.
41#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
42pub enum EventType {
43    /// A BCTS transaction changed state.
44    TransactionStateChanged,
45    /// An identity was resolved.
46    IdentityResolved,
47    /// Consent was granted.
48    ConsentGranted,
49    /// Consent was revoked.
50    ConsentRevoked,
51    /// An invariant was checked.
52    InvariantChecked,
53    /// An invariant was violated.
54    InvariantViolated,
55    /// A governance decision was made.
56    GovernanceDecision,
57    /// An escalation was triggered.
58    EscalationTriggered,
59    /// A sybil detection alert was raised.
60    SybilAlert,
61    /// A cryptographic key was rotated.
62    KeyRotated,
63    /// A new entity was registered.
64    EntityRegistered,
65    /// An audit log entry.
66    AuditEntry,
67    /// Custom / extension event.
68    Custom(String),
69}
70
71// ---------------------------------------------------------------------------
72// Event
73// ---------------------------------------------------------------------------
74
75/// A signed, timestamped event in the EXOCHAIN system.
76#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
77pub struct Event {
78    /// Unique identifier for this event.
79    pub id: CorrelationId,
80    /// HLC timestamp of when the event was created.
81    pub timestamp: Timestamp,
82    /// Classification of the event.
83    pub event_type: EventType,
84    /// CBOR-encoded payload (opaque bytes).
85    pub payload: Vec<u8>,
86    /// DID of the entity that produced the event.
87    pub source_did: Did,
88    /// Ed25519 signature over the canonical event content.
89    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    /// Construct the canonical bytes that are signed.
108    ///
109    /// The signed content is a domain-separated, schema-versioned CBOR
110    /// envelope over `id`, `timestamp`, `event_type`, `payload`, and
111    /// `source_did`.
112    ///
113    /// # Errors
114    ///
115    /// Returns `ExoError::SerializationError` if CBOR encoding fails.
116    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    /// Construct the canonical bytes that are signed.
141    ///
142    /// The signed content is: `id || timestamp || event_type || payload || source_did`
143    /// serialized as CBOR.
144    ///
145    /// # Errors
146    ///
147    /// Returns `ExoError::SerializationError` if CBOR encoding fails.
148    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/// Verify that an event's signature is valid for the given public key.
156#[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/// Verify that an event's post-quantum signature is valid for the given ML-DSA public key.
165#[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/// Verify that an event's hybrid signature is valid for both public keys.
174///
175/// Both Ed25519 and ML-DSA components must verify. Use [`verify_event`] only
176/// for Ed25519-only events.
177#[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
194/// Helper: create a signed event.
195///
196/// # Errors
197///
198/// Returns `ExoError::SerializationError` if canonical event serialization fails.
199pub 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    // Build a temporary event with a dummy signature to compute signable bytes
208    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// ---------------------------------------------------------------------------
222// Typed Event Payloads — merged from orphan event.rs per council review
223// ---------------------------------------------------------------------------
224
225/// Typed event payload variants for structured governance, identity, and
226/// Holon lifecycle events.
227///
228/// These typed variants provide compile-time enforcement of payload structure,
229/// complementing the opaque `payload: Vec<u8>` on [`Event`] for cases that
230/// require structured payloads with DAG linkage.
231///
232/// Per EXOCHAIN Specification v2.2 §3A (Holon lifecycle) and decision.forum governance.
233#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
234pub enum EventPayload {
235    /// Genesis event for a new network.
236    Genesis { network_id: String },
237    /// A new DID document was created.
238    IdentityCreated { did_doc_cid: String },
239    // --- decision.forum governance events ---
240    /// A new decision record was created.
241    DecisionCreated {
242        decision_id: crate::Hash256,
243        title: String,
244        decision_class: String,
245        constitution_hash: crate::Hash256,
246    },
247    /// A decision was advanced to a new status.
248    DecisionAdvanced {
249        decision_id: crate::Hash256,
250        from_status: String,
251        to_status: String,
252    },
253    /// A vote was cast on a decision.
254    VoteCast {
255        decision_id: crate::Hash256,
256        voter: Did,
257        choice: String,
258    },
259    /// Delegation authority was granted.
260    DelegationGranted {
261        delegation_id: crate::Hash256,
262        delegator: Did,
263        delegatee: Did,
264        expires_at: u64,
265    },
266    /// Delegation authority was revoked.
267    DelegationRevoked {
268        delegation_id: crate::Hash256,
269        revoked_at: u64,
270    },
271    /// The constitution was amended.
272    ConstitutionAmended {
273        from_version: String,
274        to_version: String,
275        amendment_hash: crate::Hash256,
276    },
277    /// A challenge was raised against a decision.
278    ChallengeRaised {
279        challenge_id: crate::Hash256,
280        contested_decision_id: crate::Hash256,
281        grounds: String,
282    },
283    /// An emergency action was taken.
284    EmergencyActionTaken {
285        emergency_id: crate::Hash256,
286        decision_id: crate::Hash256,
287        ratification_deadline: u64,
288    },
289    /// A conflict of interest was disclosed.
290    ConflictDisclosed {
291        decision_id: crate::Hash256,
292        discloser: Did,
293    },
294    // --- Holon lifecycle events (per EXOCHAIN Specification v2.2 §3A) ---
295    /// A new Holon was created.
296    HolonCreated {
297        holon_did: Did,
298        sponsor_did: Did,
299        genesis_model_cid: crate::Hash256,
300    },
301    /// A Holon was activated.
302    HolonActivated {
303        holon_did: Did,
304        approver_did: Did,
305        approval_level: u32,
306    },
307    /// A Holon action was proposed.
308    HolonActionProposed {
309        holon_did: Did,
310        action_hash: crate::Hash256,
311        reasoning_trace_cid: crate::Hash256,
312    },
313    /// A Holon action was verified.
314    HolonActionVerified {
315        holon_did: Did,
316        action_hash: crate::Hash256,
317        cgr_proof_hash: crate::Hash256,
318    },
319    /// A Holon action was executed.
320    HolonActionExecuted {
321        holon_did: Did,
322        action_hash: crate::Hash256,
323        outcome_hash: crate::Hash256,
324    },
325    /// A Holon was suspended.
326    HolonSuspended {
327        holon_did: Did,
328        reason: String,
329        suspended_by: Did,
330    },
331    /// A Holon was reinstated after suspension.
332    HolonReinstated {
333        holon_did: Did,
334        reinstated_by: Did,
335        remediation_evidence_cid: crate::Hash256,
336    },
337    /// A Holon was permanently retired.
338    HolonSunset {
339        holon_did: Did,
340        reason: String,
341        initiated_by: Did,
342    },
343    // --- CGR Kernel events ---
344    /// A Compact Governance Representation proof was issued.
345    CgrProofIssued {
346        proof_id: u64,
347        invariants_checked: u32,
348        registry_hash: crate::Hash256,
349    },
350    /// Opaque payload — extension point for domain-specific events.
351    Opaque(Vec<u8>),
352}
353
354/// Compute a canonical event identifier by hashing the CBOR-encoded
355/// representation with blake3.
356///
357/// Any serializable event structure can be identified this way, ensuring
358/// that identical logical events produce identical IDs regardless of
359/// serialization context.
360///
361/// # Errors
362///
363/// Returns `ExoError::SerializationError` if CBOR encoding fails.
364pub fn compute_event_id<T: serde::Serialize>(envelope: &T) -> crate::Result<crate::Hash256> {
365    crate::hash::hash_structured(envelope)
366}
367
368// ===========================================================================
369// Tests
370// ===========================================================================
371
372#[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        // Signature should still verify after deserialization
563        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        // Just verify Ord doesn't panic
628        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    // -----------------------------------------------------------------------
689    // EventPayload tests (merged from orphan event.rs)
690    // -----------------------------------------------------------------------
691
692    #[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        // Ensure all governance variants can be created and serialized
757        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}