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