Skip to main content

arkhe_forge_core/
event.rs

1//! `ArkheEvent` sealed trait + Core Event catalog.
2//!
3//! The trait is the runtime's wire-contract marker — `#[derive(ArkheEvent)]`
4//! in `arkhe-forge-macros` is the only way to satisfy it. The catalog in
5//! this module defines all twelve Core-range Events
6//! (`0x0003_0F01..=0x0003_0F0E`): the `ReplicaIdAllocation` +
7//! `AuditReceiptKeyPolicy` pair reserves the forward-looking event
8//! surface for federation / long-term audit activation.
9//!
10//! ## Forward-looking events — 0-emission posture
11//!
12//! These events are **define-only**: type + `ArkheEvent`
13//! derive + `TypeCode` reservation, but **no production code path emits
14//! them**. A 3-layer 0-emission defense:
15//!
16//! - **(a) `emit()` not called** — runtime never invokes
17//!   `emit_event::<…>` for either type. A workspace grep test verifies
18//!   0 production occurrences.
19//! - **(b) Cargo feature gate on type definition** — each type sits
20//!   behind a semantic feature flag (`federation-archive-hardened` /
21//!   `audit-receipt-key-identified`) so default builds do not even
22//!   compile the type.
23//! - **(c) Registry test under default features** — the runtime registry
24//!   inventory does not contain the reserved TypeCodes when the feature
25//!   gates are off.
26
27use arkhe_kernel::abi::{ExternalId, Tick, TypeCode};
28use bytes::Bytes;
29use serde::{Deserialize, Serialize};
30
31use crate::actor::ActorId;
32use crate::brand::ShellId;
33use crate::component::BoundedString;
34use crate::pii::DekId;
35use crate::user::UserId;
36// `ArkheEvent` here refers to the runtime-derive macro (re-exported by the
37// crate root from `arkhe-forge-macros`); the type-level `ArkheEvent` trait
38// defined below lives in the trait namespace, so both names coexist.
39use crate::ArkheEvent;
40
41/// Sealed marker trait for runtime Event types. Implementations come only
42/// from `#[derive(ArkheEvent)]`.
43pub trait ArkheEvent:
44    crate::__sealed::__Sealed + Serialize + for<'de> Deserialize<'de> + 'static
45{
46    /// Runtime `TypeCode` registry pin — Core Events live in
47    /// `0x0003_0F00..=0x0003_FFFF` (TypeCode sub-range split).
48    const TYPE_CODE: u32;
49
50    /// Monotone schema version — same rules as `ArkheComponent`.
51    const SCHEMA_VERSION: u16;
52
53    /// Convenience `TypeCode` accessor.
54    fn type_code() -> TypeCode {
55        TypeCode(Self::TYPE_CODE)
56    }
57}
58
59// ===================== Support types =====================
60
61/// Runtime SemVer — fixed-layout 3-tuple, postcard-stable.
62///
63/// `semver` crate's pre-release / build metadata strings are variable-width;
64/// the Runtime reserves a minimal 6-byte canonical shape instead.
65#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Serialize, Deserialize)]
66pub struct SemVer {
67    /// Major version.
68    pub major: u16,
69    /// Minor version.
70    pub minor: u16,
71    /// Patch version.
72    pub patch: u16,
73}
74
75impl SemVer {
76    /// Construct from components.
77    #[inline]
78    #[must_use]
79    pub const fn new(major: u16, minor: u16, patch: u16) -> Self {
80        Self {
81            major,
82            minor,
83            patch,
84        }
85    }
86}
87
88/// Runtime-only wire-format class tag for audit receipts.
89///
90/// Distinct from L0 `arkhe_kernel::persist::SignatureClass` (which
91/// holds key material). The L0 type is unserializable by design; this type
92/// is the serializable projection.
93///
94/// On-wire tag is the postcard VARIANT INDEX (0..N in declaration order), not
95/// the `repr(u8)` discriminant — the explicit values are a C-ABI hint, never
96/// the serialized byte.
97#[non_exhaustive]
98#[repr(u8)]
99#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
100pub enum RuntimeSignatureClass {
101    /// No signature attached.
102    None = 0,
103    /// Classical Ed25519.
104    Ed25519 = 1,
105    /// Post-quantum ML-DSA-65 (Dilithium, FIPS 204).
106    MlDsa65 = 2,
107    /// Hybrid Ed25519 + ML-DSA-65 dual-sign.
108    Hybrid = 3,
109}
110
111impl RuntimeSignatureClass {
112    /// Parse the manifest `audit.signature_class` token.
113    ///
114    /// Canonical tokens: `"none"`, `"ed25519"`, `"ml-dsa-65"`,
115    /// `"hybrid"`. Any other string yields `None` so the manifest
116    /// validator can reject an unknown class with a typed error rather
117    /// than silently defaulting.
118    #[must_use]
119    pub fn from_manifest_str(s: &str) -> Option<Self> {
120        match s {
121            "none" => Some(Self::None),
122            "ed25519" => Some(Self::Ed25519),
123            "ml-dsa-65" => Some(Self::MlDsa65),
124            "hybrid" => Some(Self::Hybrid),
125            _ => None,
126        }
127    }
128}
129
130/// Compliance tier classifier — crypto-erasure protection level
131/// Compliance tier indicator.
132///
133/// On-wire tag is the postcard VARIANT INDEX (0..N in declaration order), not
134/// the `repr(u8)` discriminant — the explicit values are a C-ABI hint, never
135/// the serialized byte.
136#[non_exhaustive]
137#[repr(u8)]
138#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
139pub enum ComplianceTier {
140    /// Tier-0 — software KEK (dev / non-production).
141    Tier0 = 0,
142    /// Tier-1 — single KMS free-tier.
143    Tier1 = 1,
144    /// Tier-2 — production Multi-KMS + threshold HSM (t-of-n Shamir).
145    Tier2 = 2,
146}
147
148/// Progress scope selector for multi-region / multi-KMS erasure progress
149/// (TypeCode reservation).
150#[non_exhaustive]
151#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
152pub enum ProgressScope {
153    /// Region scope — geographic / cloud region identifier.
154    Region(BoundedString<64>),
155    /// KMS identifier scope.
156    KmsIdentifier(BoundedString<64>),
157}
158
159// ===================== Core Events =====================
160
161/// `RuntimeBootstrap` — replay-anchored bootstrap receipt (the E12 axiom).
162///
163/// Emitted at instance first-tick, manifest change, and runtime semver bump.
164/// The receipt is re-derived bit-identically from the chain-recorded
165/// `Submit` inputs on every replay, and the kernel's default `replay_into`
166/// validates the WAL header's `manifest_digest`
167/// (`ReplayError::ManifestDigestMismatch`) — together these pin the runtime
168/// environment to what produced the log.
169#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, ArkheEvent)]
170#[arkhe(type_code = 0x0003_0F01, schema_version = 1)]
171pub struct RuntimeBootstrap {
172    /// Wire schema version.
173    pub schema_version: u16,
174    /// L0 kernel semver at the bootstrap tick.
175    pub l0_semver: SemVer,
176    /// Runtime semver at the bootstrap tick.
177    pub runtime_semver: SemVer,
178    /// Canonical BLAKE3 digest of the manifest TOML.
179    pub manifest_digest: [u8; 32],
180    /// Active TypeCode registry snapshot — derive injects
181    /// canonical ascending sort before serialize.
182    #[arkhe(canonical_sort)]
183    pub typecode_pins: Vec<TypeCode>,
184    /// Tick at which bootstrap was recorded.
185    pub bootstrap_tick: Tick,
186}
187
188/// `UserErasureScheduled` — GDPR erasure lease accepted; cascade observer
189/// will complete the crypto-shred.
190#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, ArkheEvent)]
191#[arkhe(type_code = 0x0003_0F02, schema_version = 1)]
192pub struct UserErasureScheduled {
193    /// Wire schema version.
194    pub schema_version: u16,
195    /// Target User.
196    pub user: UserId,
197    /// Tick at which erasure was scheduled.
198    pub scheduled_tick: Tick,
199}
200
201/// `UserErasureCompleted` — crypto-erasure completion receipt
202/// (replay-anchored transparency — re-derived bit-identically from
203/// chain-recorded `Submit` inputs).
204#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, ArkheEvent)]
205#[arkhe(type_code = 0x0003_0F03, schema_version = 1)]
206pub struct UserErasureCompleted {
207    /// Wire schema version.
208    pub schema_version: u16,
209    /// Target User.
210    pub user: UserId,
211    /// Tick at which the DEK was shredded.
212    pub dek_shred_tick: Tick,
213    /// Signature class used for the attestation payload.
214    pub attestation_class: RuntimeSignatureClass,
215    /// HSM attestation bytes (typically 64 or 128 B).
216    pub attestation_bytes: Bytes,
217    /// Transparency-log entry index.
218    pub transparency_log_index: u64,
219}
220
221/// `BackupErasurePropagated` — per-region offsite tombstone evidence
222/// Restore must refuse if any region is missing.
223#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, ArkheEvent)]
224#[arkhe(type_code = 0x0003_0F04, schema_version = 1)]
225pub struct BackupErasurePropagated {
226    /// Wire schema version.
227    pub schema_version: u16,
228    /// Target User.
229    pub user: UserId,
230    /// Region identifier (e.g. `"eu-west-1"`).
231    pub region: BoundedString<32>,
232    /// Tick at which the tombstone was applied.
233    pub applied_tick: Tick,
234    /// Signature class used for the receipt.
235    pub receipt_class: RuntimeSignatureClass,
236    /// Receipt payload bytes.
237    pub receipt_bytes: Bytes,
238}
239
240/// `GdprPolicyViolation` — audit trail for an actor-originated Action that
241/// targeted an ErasurePending User (L1 compute MC gate).
242#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, ArkheEvent)]
243#[arkhe(type_code = 0x0003_0F05, schema_version = 1)]
244pub struct GdprPolicyViolation {
245    /// Wire schema version.
246    pub schema_version: u16,
247    /// Acting actor.
248    pub actor: ActorId,
249    /// Tick at which the violating Action was attempted.
250    pub attempted_tick: Tick,
251    /// TypeCode of the rejected Action.
252    pub action_type_code: TypeCode,
253}
254
255/// `SignatureClassPolicy` — replay-anchored shell audit signature policy
256/// (the E13 axiom; re-derived bit-identically from chain-recorded
257/// `Submit` inputs). Downgrade-resistant by construction.
258#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, ArkheEvent)]
259#[arkhe(type_code = 0x0003_0F06, schema_version = 1)]
260pub struct SignatureClassPolicy {
261    /// Wire schema version.
262    pub schema_version: u16,
263    /// Shell for which this policy applies.
264    pub shell_id: ShellId,
265    /// Required signature class.
266    pub class: RuntimeSignatureClass,
267    /// Tick at which the policy becomes effective.
268    pub effective_tick: Tick,
269}
270
271/// `CrossShellActivity` — audit trail for a replay/admin path that
272/// observed a record whose `shell_id` mismatched the actor's shell
273/// (E-act-2 dual-tier RA side).
274#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, ArkheEvent)]
275#[arkhe(type_code = 0x0003_0F07, schema_version = 1)]
276pub struct CrossShellActivity {
277    /// Wire schema version.
278    pub schema_version: u16,
279    /// Acting actor.
280    pub actor: ActorId,
281    /// Shell the target entity actually belongs to.
282    pub target_shell_id: ShellId,
283    /// Shell the record claimed the activity belongs to.
284    pub record_shell_id: ShellId,
285    /// Tick at which the mismatch was detected.
286    pub detected_tick: Tick,
287}
288
289/// `PerRegionErasureProgress` — multi-region or multi-KMS DEK-shred progress
290/// record (two-phase-commit).
291#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, ArkheEvent)]
292#[arkhe(type_code = 0x0003_0F08, schema_version = 1)]
293pub struct PerRegionErasureProgress {
294    /// Wire schema version.
295    pub schema_version: u16,
296    /// Target User.
297    pub user: UserId,
298    /// Progress scope — region or KMS identifier.
299    pub scope: ProgressScope,
300    /// Tick at which this scope's shred completed.
301    pub shred_tick: Tick,
302    /// Signature class used for the attestation payload.
303    pub attestation_class: RuntimeSignatureClass,
304    /// HSM attestation bytes for this scope.
305    pub attestation_bytes: Bytes,
306}
307
308/// `DekMigrationCompleted` — alpha→beta DEK rotation receipt
309/// Emitted when `runtime-doctor pqc-reseal` or similar
310/// rotation completes for a user.
311#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, ArkheEvent)]
312#[arkhe(type_code = 0x0003_0F09, schema_version = 1)]
313pub struct DekMigrationCompleted {
314    /// Wire schema version.
315    pub schema_version: u16,
316    /// Target User.
317    pub user: UserId,
318    /// Previous DEK identifier.
319    pub old_dek_id: DekId,
320    /// New DEK identifier after rotation.
321    pub new_dek_id: DekId,
322    /// Tick at which the migration completed.
323    pub migrated_tick: Tick,
324}
325
326/// `ComplianceTierChange` — operator-driven Tier transition record
327/// Compliance tier indicator.
328#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, ArkheEvent)]
329#[arkhe(type_code = 0x0003_0F0A, schema_version = 1)]
330pub struct ComplianceTierChange {
331    /// Wire schema version.
332    pub schema_version: u16,
333    /// Previous compliance tier.
334    pub old_tier: ComplianceTier,
335    /// New compliance tier.
336    pub new_tier: ComplianceTier,
337    /// Tick at which the transition becomes effective.
338    pub effective_tick: Tick,
339    /// External identity of the operator who authorized the change.
340    pub operator: ExternalId,
341}
342
343// ============================================================================
344// Cryptographic primitives — Attestation newtype +
345// AttestationSignerPolicy enum.
346// ============================================================================
347
348/// Length-sealed 64-byte attestation signature wrapper.
349///
350/// Constructed only via [`Attestation::from_bytes`] (which takes a
351/// `[u8; 64]` literal — length statically enforced by the Rust type
352/// system). The wire format is a postcard length-prefixed byte
353/// sequence (`Vec<u8>`-equivalent) with a strict 64-byte length check
354/// on deserialize. This combination produces a *length-sealed* type:
355///
356/// - **Constructor side**: any caller producing an `Attestation` does
357///   so via the `[u8; 64]` constructor — the type system rejects any
358///   other byte width at compile time.
359/// - **Deserialize side**: any wire bytes whose payload length is
360///   not exactly 64 produce a `serde::de::Error`, never a panic.
361///
362/// **Why a custom serde impl** (rather than `#[derive]` over `[u8; 64]`):
363/// serde's stock array deserializer caps at 32 bytes — the L0
364/// `WalRecord.signature` workaround uses `Vec<u8>` with length
365/// validation at the application layer. `Attestation` lifts the
366/// length invariant from convention to the type itself: any value of
367/// type `Attestation` that exists has 64 bytes, and the Deserialize
368/// impl is the exhaustive admission check.
369///
370/// Used by `ReplicaIdAllocation::registry_attestation` and
371/// `AuditReceiptKeyPolicy::attestation`.
372#[derive(Debug, Clone, Eq, PartialEq)]
373pub struct Attestation {
374    /// 64-byte payload. Private to enforce the constructor invariant —
375    /// any `Attestation` value that exists has `inner.len() == 64`.
376    inner: Vec<u8>,
377}
378
379impl Attestation {
380    /// Construct an [`Attestation`] from a fixed 64-byte signature.
381    ///
382    /// The `[u8; 64]` parameter type makes the length invariant
383    /// statically enforced by the Rust type system — any caller
384    /// passing a non-64-byte input is rejected at compile time.
385    #[must_use]
386    pub fn from_bytes(bytes: [u8; 64]) -> Self {
387        Self {
388            inner: bytes.to_vec(),
389        }
390    }
391
392    /// Borrow the underlying 64-byte payload as a slice.
393    #[must_use]
394    pub fn as_bytes(&self) -> &[u8] {
395        &self.inner
396    }
397}
398
399impl Serialize for Attestation {
400    /// Serializes as the underlying `Vec<u8>` (postcard length-prefix
401    /// plus bytes). Wire format is identical to a bare `Vec<u8>` of
402    /// length 64, so the wire baseline is preserved byte-for-byte.
403    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
404        self.inner.serialize(serializer)
405    }
406}
407
408impl<'de> Deserialize<'de> for Attestation {
409    /// Deserializes from a `Vec<u8>` and rejects (with `serde::de::Error`,
410    /// never a panic) any payload whose length is not exactly 64. This
411    /// makes `Attestation` the single admission check for the 64-byte
412    /// invariant on the wire side.
413    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
414        let inner: Vec<u8> = Vec::deserialize(deserializer)?;
415        if inner.len() != 64 {
416            return Err(serde::de::Error::custom(format!(
417                "Attestation: expected 64 bytes, got {}",
418                inner.len()
419            )));
420        }
421        Ok(Self { inner })
422    }
423}
424
425/// Signer policy for `AuditReceiptKeyPolicy::attestation`.
426///
427/// Each `AuditReceiptKeyPolicy` entry's attestation signature binds
428/// the inventory entry to *some* signing authority — but "which
429/// authority" is an operator-policy choice the runtime merely records.
430/// Three variants cover the expected operator topologies;
431/// `#[non_exhaustive]` lets additive variants land without breaking
432/// existing wire bytes.
433///
434/// The enum is paired with `AuditReceiptKeyPolicy::attestation` at
435/// the same struct level — currently no other event references it,
436/// so cohesion is preferred over abstraction. If a second user
437/// emerges, the enum can be lifted to a shared type with no
438/// wire-format change (additive non-breaking refactor).
439///
440/// **`Copy` derive — forward-compat constraint**: the derive
441/// constrains future variants to be field-less. A variant carrying
442/// data (e.g., `HardwareAttestation { tpm_version: u32 }` or
443/// threshold-signature parameters) would require the `Copy` derive
444/// to be removed, which is a breaking API change. Field-less policy
445/// reservation is the contract; data-bearing variants would arrive
446/// alongside the `Copy` removal as a coordinated breaking change.
447#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
448#[non_exhaustive]
449pub enum AttestationSignerPolicy {
450    /// Successor key signed by predecessor key — rotation chain
451    /// integrity. The recipient verifies the attestation against
452    /// the predecessor entry's `public_key`.
453    Predecessor,
454    /// Direct signature by an operator-root authority (HW-signed
455    /// or air-gapped key per `docs/release-keys.md §3` co-custody).
456    /// The recipient verifies against the operator-root public key
457    /// pinned in the runtime's release-keys metadata.
458    OperatorRoot,
459    /// Genesis self-signed proof-of-possession — the signing key
460    /// signs its own inventory entry. Reserved for the very first
461    /// inventory entry (no predecessor, no operator-root yet
462    /// pinned). Recipient verification = fixed-point check against
463    /// the entry's own `public_key`.
464    SelfSigned,
465}
466
467/// `ReplicaIdAllocation` — federation-replica registration receipt
468/// **Define-only**: the type sits behind the
469/// `federation-archive-hardened` Cargo feature so default builds do
470/// not compile the type at all (3-layer 0-emission defense, layer
471/// (b)).
472///
473/// Activation: when a federation registry signs off on a new replica
474/// entering the federation, the runtime would emit one
475/// `ReplicaIdAllocation` event into the chain so cross-replica audit
476/// can trace the membership lineage. The wire surface (TypeCode +
477/// schema) is reserved here. Activation gate: federation prerequisites
478/// complete (archive-hardening + `SignedArkheUri` + identity
479/// federation layer).
480///
481/// **0-emission posture**: no production code path calls
482/// `emit_event::<ReplicaIdAllocation>(..)`. A workspace grep test
483/// confirms zero emission sites; Cargo feature gating provides
484/// compile-time exclusion.
485#[cfg(feature = "federation-archive-hardened")]
486#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, ArkheEvent)]
487#[arkhe(type_code = 0x0003_0F0D, schema_version = 1)]
488pub struct ReplicaIdAllocation {
489    /// Wire schema version.
490    pub schema_version: u16,
491    /// Federation identifier (128-bit). `[16]` is collision-resistant
492    /// for any plausible federation count.
493    pub federation_id: [u8; 16],
494    /// Replica identifier within the federation. The width is u64
495    /// (federation-scale collision-resistant — ~10^19
496    /// replicas/federation, well beyond any plausible ultra-scale
497    /// federation deployment). A narrower u32 would cap at ~4B
498    /// replicas, an order-of-magnitude tight bound on the long tail
499    /// of federated systems.
500    pub replica_id: u64,
501    /// 32-bit nonce drawn at allocation time, fed into the
502    /// registry attestation signature alongside the (federation,
503    /// replica, tick) tuple. Defends against replay of older
504    /// allocation requests.
505    pub allocation_nonce: u32,
506    /// Tick at which the allocation became effective (replay-derived
507    /// ordering anchor relative to other federation events).
508    pub effective_tick: Tick,
509    /// Federation-registry attestation over (federation_id,
510    /// replica_id, allocation_nonce, effective_tick). 64-byte
511    /// signature (Ed25519 width) wrapped in [`Attestation`] —
512    /// length-sealed at construction (`from_bytes([u8; 64])`) and
513    /// strictly verified on deserialize (length invariant lifted from
514    /// convention to type). The signer is the federation registry;
515    /// the signer-policy enum reservation lives on
516    /// `AuditReceiptKeyPolicy::signer_policy` (audit-receipt key
517    /// rotation context where signer choice is operator-variable).
518    pub registry_attestation: Attestation,
519}
520
521/// `AuditReceiptKeyPolicy` — audit-receipt key inventory + rotation
522/// manifest (the E13 axiom). **Define-only**: the type sits
523/// behind the `audit-receipt-key-identified` Cargo feature so default
524/// builds do not compile the type (3-layer 0-emission defense, layer
525/// (b)).
526///
527/// Activation: when the operator rotates the audit-receipt signing key
528/// (or initially declares the genesis key in the
529/// `docs/release-keys.md §1` inventory), the runtime would emit one
530/// `AuditReceiptKeyPolicy` event into the chain so audit-trail
531/// consumers can verify which key was active at which tick. The wire
532/// surface is reserved here. Activation gate: operator-side carry-over
533/// (g) "audit-receipt key identity declared in `docs/release-keys.md`
534/// inventory anchor.
535///
536/// **0-emission posture**: identical to `ReplicaIdAllocation` —
537/// production code path emits zero, a workspace grep test verifies,
538/// Cargo feature gating closes the compile.
539#[cfg(feature = "audit-receipt-key-identified")]
540#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, ArkheEvent)]
541#[arkhe(type_code = 0x0003_0F0E, schema_version = 2)]
542pub struct AuditReceiptKeyPolicy {
543    /// Wire schema version.
544    pub schema_version: u16,
545    /// Audit-receipt key identifier. The width is `[u8; 16]` (128-bit,
546    /// UUID-class collision space). A narrower 8-byte width would give
547    /// only a 2^32 birthday bound, tight at federation scale where
548    /// multiple operators independently mint keys; 16 bytes raises the
549    /// bound to 2^64 = computationally infeasible across any plausible
550    /// key population. The `docs/release-keys.md §1` inventory entry
551    /// maps this identifier to the physical key material.
552    pub key_id: [u8; 16],
553    /// Signature class for receipts under this key. Reuses the
554    /// `RuntimeSignatureClass` enum so the wire
555    /// tagging is consistent with `SignatureClassPolicy` (E13).
556    pub algorithm: RuntimeSignatureClass,
557    /// Public-key wire bytes. Variable length to accommodate
558    /// classical (Ed25519, 32 bytes), post-quantum (ML-DSA-65,
559    /// ~1952 bytes), and hybrid representations. Bounded by the
560    /// runtime's per-event size cap on encode.
561    pub public_key: Bytes,
562    /// Predecessor `key_id` if this entry succeeds an earlier key
563    /// (rotation chain). `None` = genesis (first entry in the
564    /// inventory) or an operator-policy-determined unrelated key.
565    /// Width matches `key_id` ([u8; 16]).
566    pub predecessor_key_id: Option<[u8; 16]>,
567    /// Tick at which this key entry becomes effective for new
568    /// audit-receipt signatures.
569    pub effective_tick: Tick,
570    /// Tick at which this key entry retires (no further new
571    /// signatures, but historical receipts remain valid). `None`
572    /// for the currently-active entry.
573    pub retirement_tick: Option<Tick>,
574    /// Signer policy for the [`Self::attestation`] field —
575    /// declares whether the signature was produced by the
576    /// predecessor key (rotation chain), an operator-root authority,
577    /// or as a genesis self-signed proof-of-possession. The enum +
578    /// field reservation lets emission code populate the value per
579    /// operator policy without further schema work.
580    /// `#[non_exhaustive]` keeps additive variants forward-compat.
581    pub signer_policy: AttestationSignerPolicy,
582    /// Attestation signature binding the inventory entry to the
583    /// operator's signing authority. 64-byte signature (Ed25519
584    /// width) wrapped in [`Attestation`] — length-sealed at
585    /// construction and strictly verified on deserialize (length
586    /// invariant lifted from convention to type). Recipient
587    /// verification path is selected by [`Self::signer_policy`].
588    pub attestation: Attestation,
589    /// Post-quantum half of the attestation envelope (schema v2).
590    ///
591    /// Coherence with [`Self::algorithm`]: this field MUST be `Some`
592    /// for the PQC-bearing classes (`MlDsa65`, `Hybrid`) and `None`
593    /// otherwise (`None`, `Ed25519`). For `MlDsa65` it carries the
594    /// 3309-byte ML-DSA-65 signature; for `Hybrid` the 3309-byte
595    /// ML-DSA-65 half (the Ed25519 half lives in [`Self::attestation`]).
596    /// The verifier (`arkhe-forge-platform` `verify_receipt_envelope`)
597    /// enforces this algorithm<->slot coherence before dispatching.
598    ///
599    /// Wire compatibility: postcard encodes `None` as a single trailing
600    /// `0x00`, so a schema-1 `None`/`Ed25519` receipt re-encodes
601    /// byte-identically under schema-2 when this field is left `None`.
602    pub attestation_pqc: Option<Vec<u8>>,
603}
604
605#[cfg(test)]
606#[allow(clippy::unwrap_used, clippy::expect_used)]
607mod tests {
608    use super::*;
609
610    #[test]
611    fn runtime_bootstrap_serde_roundtrip() {
612        let rb = RuntimeBootstrap {
613            schema_version: 1,
614            l0_semver: SemVer::new(0, 11, 0),
615            runtime_semver: SemVer::new(0, 11, 0),
616            manifest_digest: [0xABu8; 32],
617            typecode_pins: vec![TypeCode(0x0003_0001), TypeCode(0x0003_0002)],
618            bootstrap_tick: Tick(1),
619        };
620        let bytes = postcard::to_stdvec(&rb).unwrap();
621        let back: RuntimeBootstrap = postcard::from_bytes(&bytes).unwrap();
622        assert_eq!(rb, back);
623    }
624
625    #[test]
626    fn per_region_progress_with_region_scope_roundtrip() {
627        let ev = PerRegionErasureProgress {
628            schema_version: 1,
629            user: crate::user::UserId::new(arkhe_kernel::abi::EntityId::new(42).unwrap()),
630            scope: ProgressScope::Region(BoundedString::<64>::new("eu-west-1").unwrap()),
631            shred_tick: Tick(100),
632            attestation_class: RuntimeSignatureClass::Ed25519,
633            attestation_bytes: Bytes::from_static(&[0u8; 64]),
634        };
635        let bytes = postcard::to_stdvec(&ev).unwrap();
636        let back: PerRegionErasureProgress = postcard::from_bytes(&bytes).unwrap();
637        assert_eq!(ev, back);
638    }
639
640    #[test]
641    fn core_event_type_code_pins_match_spec() {
642        assert_eq!(RuntimeBootstrap::TYPE_CODE, 0x0003_0F01);
643        assert_eq!(UserErasureScheduled::TYPE_CODE, 0x0003_0F02);
644        assert_eq!(UserErasureCompleted::TYPE_CODE, 0x0003_0F03);
645        assert_eq!(BackupErasurePropagated::TYPE_CODE, 0x0003_0F04);
646        assert_eq!(GdprPolicyViolation::TYPE_CODE, 0x0003_0F05);
647        assert_eq!(SignatureClassPolicy::TYPE_CODE, 0x0003_0F06);
648        assert_eq!(CrossShellActivity::TYPE_CODE, 0x0003_0F07);
649        assert_eq!(PerRegionErasureProgress::TYPE_CODE, 0x0003_0F08);
650        assert_eq!(DekMigrationCompleted::TYPE_CODE, 0x0003_0F09);
651        assert_eq!(ComplianceTierChange::TYPE_CODE, 0x0003_0F0A);
652        // Forward-looking event TypeCode pins are verified only when
653        // the activation feature is enabled (cfg-gate per 3-layer
654        // 0-emission defense, layer (b)). The TypeCode constants in
655        // `typecode::core_event` remain unconditional anchors.
656        #[cfg(feature = "federation-archive-hardened")]
657        assert_eq!(ReplicaIdAllocation::TYPE_CODE, 0x0003_0F0D);
658        #[cfg(feature = "audit-receipt-key-identified")]
659        assert_eq!(AuditReceiptKeyPolicy::TYPE_CODE, 0x0003_0F0E);
660    }
661
662    #[test]
663    fn from_manifest_str_parses_all() {
664        assert_eq!(
665            RuntimeSignatureClass::from_manifest_str("none"),
666            Some(RuntimeSignatureClass::None)
667        );
668        assert_eq!(
669            RuntimeSignatureClass::from_manifest_str("ed25519"),
670            Some(RuntimeSignatureClass::Ed25519)
671        );
672        assert_eq!(
673            RuntimeSignatureClass::from_manifest_str("ml-dsa-65"),
674            Some(RuntimeSignatureClass::MlDsa65)
675        );
676        assert_eq!(
677            RuntimeSignatureClass::from_manifest_str("hybrid"),
678            Some(RuntimeSignatureClass::Hybrid)
679        );
680        assert_eq!(RuntimeSignatureClass::from_manifest_str("ML-DSA-65"), None);
681        assert_eq!(RuntimeSignatureClass::from_manifest_str("dilithium"), None);
682    }
683
684    #[test]
685    fn semver_roundtrip_is_stable() {
686        let v = SemVer::new(0, 11, 0);
687        let a = postcard::to_stdvec(&v).unwrap();
688        let b = postcard::to_stdvec(&v).unwrap();
689        assert_eq!(a, b);
690        let back: SemVer = postcard::from_bytes(&a).unwrap();
691        assert_eq!(back, v);
692    }
693
694    // ----- Forward-looking event tests -----
695    //
696    // Each test is feature-gated to its activation flag (3-layer
697    // 0-emission defense, layer (b)). The default-features build skips
698    // these tests because the underlying type is not compiled.
699
700    #[cfg(feature = "federation-archive-hardened")]
701    #[test]
702    fn replica_id_allocation_serde_roundtrip() {
703        let ev = ReplicaIdAllocation {
704            schema_version: 1,
705            federation_id: [0xF1u8; 16],
706            replica_id: 7,
707            allocation_nonce: 0xCAFE_BABE,
708            effective_tick: Tick(1234),
709            registry_attestation: Attestation::from_bytes([0x55u8; 64]),
710        };
711        let bytes = postcard::to_stdvec(&ev).unwrap();
712        let back: ReplicaIdAllocation = postcard::from_bytes(&bytes).unwrap();
713        assert_eq!(ev, back);
714    }
715
716    /// Verify u64 width preserved for replica_id values above
717    /// `u32::MAX`. Regression sentinel against any schema width
718    /// change that would silently truncate the high 32 bits
719    /// (federation-scale concern). Pin chosen to span the upper
720    /// half of u64 so byte-level postcard varint
721    /// encoding exercises the >5-byte continuation path.
722    #[cfg(feature = "federation-archive-hardened")]
723    #[test]
724    fn replica_id_allocation_high_replica_id_preserves_u64_width() {
725        let high_replica = 0x1234_5678_9ABC_DEF0u64;
726        assert!(high_replica > u64::from(u32::MAX));
727        let ev = ReplicaIdAllocation {
728            schema_version: 1,
729            federation_id: [0xA5u8; 16],
730            replica_id: high_replica,
731            allocation_nonce: 0xDEAD_BEEF,
732            effective_tick: Tick(99_999),
733            registry_attestation: Attestation::from_bytes([0x33u8; 64]),
734        };
735        let bytes = postcard::to_stdvec(&ev).unwrap();
736        let back: ReplicaIdAllocation = postcard::from_bytes(&bytes).unwrap();
737        assert_eq!(ev, back);
738        assert_eq!(back.replica_id, high_replica);
739    }
740
741    #[cfg(feature = "federation-archive-hardened")]
742    #[test]
743    fn replica_id_allocation_type_code_matches_typecode_constant() {
744        assert_eq!(
745            ReplicaIdAllocation::TYPE_CODE,
746            crate::typecode::core_event::REPLICA_ID_ALLOCATION
747        );
748    }
749
750    #[cfg(feature = "audit-receipt-key-identified")]
751    #[test]
752    fn audit_receipt_key_policy_serde_roundtrip_with_genesis_entry() {
753        let ev = AuditReceiptKeyPolicy {
754            schema_version: 2,
755            key_id: [0xABu8; 16],
756            algorithm: RuntimeSignatureClass::Ed25519,
757            public_key: Bytes::from_static(&[0u8; 32]),
758            predecessor_key_id: None,
759            effective_tick: Tick(0),
760            retirement_tick: None,
761            signer_policy: AttestationSignerPolicy::SelfSigned,
762            attestation: Attestation::from_bytes([0x77u8; 64]),
763            attestation_pqc: None,
764        };
765        let bytes = postcard::to_stdvec(&ev).unwrap();
766        let back: AuditReceiptKeyPolicy = postcard::from_bytes(&bytes).unwrap();
767        assert_eq!(ev, back);
768    }
769
770    #[cfg(feature = "audit-receipt-key-identified")]
771    #[test]
772    fn audit_receipt_key_policy_serde_roundtrip_with_rotation_entry() {
773        let ev = AuditReceiptKeyPolicy {
774            schema_version: 2,
775            key_id: [0xCDu8; 16],
776            algorithm: RuntimeSignatureClass::MlDsa65,
777            public_key: Bytes::from_static(&[0xEEu8; 1952]), // ML-DSA-65 wire size
778            predecessor_key_id: Some([0xABu8; 16]),
779            effective_tick: Tick(100),
780            retirement_tick: Some(Tick(1000)),
781            signer_policy: AttestationSignerPolicy::Predecessor,
782            attestation: Attestation::from_bytes([0x99u8; 64]),
783            attestation_pqc: Some(vec![0xEEu8; 3309]),
784        };
785        let bytes = postcard::to_stdvec(&ev).unwrap();
786        let back: AuditReceiptKeyPolicy = postcard::from_bytes(&bytes).unwrap();
787        assert_eq!(ev, back);
788    }
789
790    /// Verify [u8; 16] width preserved with high-entropy distinct
791    /// bytes in both `key_id` and `predecessor_key_id`. Repeated-byte
792    /// literals like `[0xABu8; 16]` / `[0xCDu8; 16]` would silently
793    /// round-trip through any narrower
794    /// width that happens to truncate to the same repeated byte. This
795    /// test pins distinct bytes per position so a regression to
796    /// `[u8; 8]` would catch on the upper-half `key_id[8..16]` and
797    /// `predecessor_key_id[8..16]` byte mismatch. Belt-and-suspenders
798    /// against silent width truncation.
799    #[cfg(feature = "audit-receipt-key-identified")]
800    #[test]
801    fn audit_receipt_key_policy_distinct_bytes_preserve_full_16_byte_widths() {
802        let key_id: [u8; 16] = [
803            0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, 0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54,
804            0x32, 0x10,
805        ];
806        let predecessor_key_id: [u8; 16] = [
807            0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE, 0xBA, 0xBE, 0xF0, 0x0D, 0xFA, 0xCE, 0x12, 0x34,
808            0x56, 0x78,
809        ];
810        let ev = AuditReceiptKeyPolicy {
811            schema_version: 2,
812            key_id,
813            algorithm: RuntimeSignatureClass::Ed25519,
814            public_key: Bytes::from_static(&[0xC3u8; 32]),
815            predecessor_key_id: Some(predecessor_key_id),
816            effective_tick: Tick(2_500),
817            retirement_tick: Some(Tick(5_000)),
818            signer_policy: AttestationSignerPolicy::OperatorRoot,
819            attestation: Attestation::from_bytes([0x44u8; 64]),
820            attestation_pqc: None,
821        };
822        let bytes = postcard::to_stdvec(&ev).unwrap();
823        let back: AuditReceiptKeyPolicy = postcard::from_bytes(&bytes).unwrap();
824        assert_eq!(ev, back);
825        assert_eq!(back.key_id, key_id);
826        assert_eq!(back.predecessor_key_id, Some(predecessor_key_id));
827        // Explicit upper-half spot-check (catches truncation regression
828        // even if compiler accepted [u8; 8] literal somehow).
829        assert_eq!(back.key_id[8..16], key_id[8..16]);
830    }
831
832    /// Schema-1 vs schema-2 byte-identity for the `None` pqc slot.
833    ///
834    /// postcard has no struct framing — a struct encodes as the bare
835    /// concatenation of its fields, and `Option::None` is a single
836    /// trailing `0x00`. So a schema-2 receipt with `attestation_pqc:
837    /// None` encodes as the schema-1 field run (reproduced here as the
838    /// matching field tuple) followed by exactly one `0x00`. This is
839    /// the wire-compat guarantee: schema-1 `None`/`Ed25519` receipts
840    /// re-encode byte-identical under schema-2 modulo that one byte.
841    #[cfg(feature = "audit-receipt-key-identified")]
842    #[test]
843    fn audit_receipt_key_policy_none_pqc_appends_single_zero_byte() {
844        let ev = AuditReceiptKeyPolicy {
845            schema_version: 2,
846            key_id: [0xABu8; 16],
847            algorithm: RuntimeSignatureClass::Ed25519,
848            public_key: Bytes::from_static(&[0u8; 32]),
849            predecessor_key_id: None,
850            effective_tick: Tick(0),
851            retirement_tick: None,
852            signer_policy: AttestationSignerPolicy::SelfSigned,
853            attestation: Attestation::from_bytes([0x77u8; 64]),
854            attestation_pqc: None,
855        };
856        let full = postcard::to_stdvec(&ev).unwrap();
857
858        // The schema-1 field run = the same fields WITHOUT the pqc slot.
859        // postcard encodes a tuple as the concatenation of its elements,
860        // identical to a struct's field run, so this reproduces the
861        // pre-field byte layout.
862        let pre_field = postcard::to_stdvec(&(
863            ev.schema_version,
864            ev.key_id,
865            ev.algorithm,
866            ev.public_key.clone(),
867            ev.predecessor_key_id,
868            ev.effective_tick,
869            ev.retirement_tick,
870            ev.signer_policy,
871            ev.attestation.clone(),
872        ))
873        .unwrap();
874
875        // `None` appends exactly one trailing 0x00 to the pre-field run.
876        assert_eq!(full.len(), pre_field.len() + 1);
877        assert_eq!(&full[..pre_field.len()], pre_field.as_slice());
878        assert_eq!(full[full.len() - 1], 0x00);
879    }
880
881    /// PQC slot round-trip — a 3309-byte ML-DSA-65 signature in the
882    /// `attestation_pqc` slot survives a postcard round-trip intact.
883    #[cfg(feature = "audit-receipt-key-identified")]
884    #[test]
885    fn audit_receipt_key_policy_pqc_slot_round_trip() {
886        let ev = AuditReceiptKeyPolicy {
887            schema_version: 2,
888            key_id: [0x5Au8; 16],
889            algorithm: RuntimeSignatureClass::MlDsa65,
890            public_key: Bytes::from_static(&[0x11u8; 1952]),
891            predecessor_key_id: None,
892            effective_tick: Tick(7),
893            retirement_tick: None,
894            signer_policy: AttestationSignerPolicy::SelfSigned,
895            attestation: Attestation::from_bytes([0u8; 64]),
896            attestation_pqc: Some(vec![0xABu8; 3309]),
897        };
898        let bytes = postcard::to_stdvec(&ev).unwrap();
899        let back: AuditReceiptKeyPolicy = postcard::from_bytes(&bytes).unwrap();
900        assert_eq!(ev, back);
901        assert_eq!(back.attestation_pqc.as_deref().map(<[u8]>::len), Some(3309));
902    }
903
904    #[cfg(feature = "audit-receipt-key-identified")]
905    #[test]
906    fn audit_receipt_key_policy_type_code_matches_typecode_constant() {
907        assert_eq!(
908            AuditReceiptKeyPolicy::TYPE_CODE,
909            crate::typecode::core_event::AUDIT_RECEIPT_KEY_POLICY
910        );
911    }
912
913    /// 0-emission posture confirmation (layer (a) — `emit()` not
914    /// called). The `ArkheEvent` trait makes the type *eligible* for
915    /// chain emission via `Op::EmitEvent`, but **no production code**
916    /// calls `emit_event::<ReplicaIdAllocation>(..)` or
917    /// `emit_event::<AuditReceiptKeyPolicy>(..)`. A workspace grep
918    /// verification scans the source tree and asserts 0 occurrences.
919    /// This test is the structural anchor — type definitions exist,
920    /// but the trait constants alone do not constitute emission.
921    ///
922    /// Feature-gated under both activation flags so the test compiles
923    /// only when the types compile (cfg-gate, layer (b)).
924    #[cfg(all(
925        feature = "federation-archive-hardened",
926        feature = "audit-receipt-key-identified"
927    ))]
928    #[test]
929    fn forward_looking_events_are_define_only() {
930        // The TYPE_CODE constants exist + are pinned. ReplicaIdAllocation
931        // stays at schema 1; AuditReceiptKeyPolicy is at schema 2 (the
932        // additive attestation_pqc slot). No emission entry point is
933        // defined for either type. This test is the structural anchor.
934        assert_eq!(ReplicaIdAllocation::SCHEMA_VERSION, 1);
935        assert_eq!(AuditReceiptKeyPolicy::SCHEMA_VERSION, 2);
936        assert_eq!(ReplicaIdAllocation::TYPE_CODE, 0x0003_0F0D);
937        assert_eq!(AuditReceiptKeyPolicy::TYPE_CODE, 0x0003_0F0E);
938    }
939
940    // ----- Attestation newtype + AttestationSignerPolicy tests.
941    // These are unconditional (no cfg-gate) because the types themselves
942    // live unconditionally in the module — only the events that
943    // *consume* them are cfg-gated under the activation flags.
944
945    /// `Attestation` round-trips through postcard byte-identical to a
946    /// bare `Vec<u8>` of length 64 (wire baseline preservation).
947    #[test]
948    fn attestation_serde_round_trip_preserves_64_bytes() {
949        let payload: [u8; 64] = [0xA1; 64];
950        let att = Attestation::from_bytes(payload);
951        let bytes = postcard::to_stdvec(&att).unwrap();
952        let back: Attestation = postcard::from_bytes(&bytes).unwrap();
953        assert_eq!(att, back);
954        assert_eq!(back.as_bytes(), &payload);
955        assert_eq!(back.as_bytes().len(), 64);
956    }
957
958    /// Wire format invariance with bare `Vec<u8>` of length 64. The
959    /// custom `Serialize` impl delegates to `Vec<u8>::serialize`, so
960    /// an `Attestation` and an equivalent `vec![0x..; 64]` produce
961    /// byte-for-byte identical postcard output. This preserves the
962    /// sealed wire baseline — any emitted bytes decode equivalently
963    /// whether the receiver expects `Attestation` or `Vec<u8>`.
964    #[test]
965    fn attestation_wire_format_byte_identical_to_vec_u8_length_64() {
966        let payload: [u8; 64] = [0xC7; 64];
967        let att_bytes = postcard::to_stdvec(&Attestation::from_bytes(payload)).unwrap();
968        let vec_bytes = postcard::to_stdvec(&payload.to_vec()).unwrap();
969        assert_eq!(att_bytes, vec_bytes);
970    }
971
972    /// `Attestation` deserialize **rejects** payloads whose length is
973    /// not exactly 64 with a `serde::de::Error`, never a panic. This is
974    /// the single admission check that lifts the 64-byte invariant from
975    /// convention to type.
976    #[test]
977    fn attestation_deserialize_rejects_short_payload() {
978        // Postcard-encode a 32-byte Vec<u8> (still serde-valid, but
979        // shorter than the Attestation contract).
980        let short_payload: Vec<u8> = vec![0xBB; 32];
981        let bytes = postcard::to_stdvec(&short_payload).unwrap();
982        let result: Result<Attestation, _> = postcard::from_bytes(&bytes);
983        assert!(
984            result.is_err(),
985            "32-byte payload must be rejected as not-64-bytes"
986        );
987    }
988
989    #[test]
990    fn attestation_deserialize_rejects_long_payload() {
991        let long_payload: Vec<u8> = vec![0xCC; 65];
992        let bytes = postcard::to_stdvec(&long_payload).unwrap();
993        let result: Result<Attestation, _> = postcard::from_bytes(&bytes);
994        assert!(
995            result.is_err(),
996            "65-byte payload must be rejected as not-64-bytes"
997        );
998    }
999
1000    #[test]
1001    fn attestation_deserialize_rejects_empty_payload() {
1002        let empty_payload: Vec<u8> = Vec::new();
1003        let bytes = postcard::to_stdvec(&empty_payload).unwrap();
1004        let result: Result<Attestation, _> = postcard::from_bytes(&bytes);
1005        assert!(
1006            result.is_err(),
1007            "empty payload must be rejected as not-64-bytes"
1008        );
1009    }
1010
1011    /// All three [`AttestationSignerPolicy`] variants round-trip
1012    /// through postcard. The variant tag is a postcard varint; an
1013    /// additive variant (operator-policy expansion) lands as a new
1014    /// tag without disturbing existing tags (`#[non_exhaustive]` on
1015    /// the enum + variant order preservation).
1016    #[test]
1017    fn attestation_signer_policy_round_trip_all_three_variants() {
1018        for variant in [
1019            AttestationSignerPolicy::Predecessor,
1020            AttestationSignerPolicy::OperatorRoot,
1021            AttestationSignerPolicy::SelfSigned,
1022        ] {
1023            let bytes = postcard::to_stdvec(&variant).unwrap();
1024            let back: AttestationSignerPolicy = postcard::from_bytes(&bytes).unwrap();
1025            assert_eq!(variant, back);
1026        }
1027    }
1028}