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 fourteen Core-range Events
6//! (`0x0003_0F01..=0x0003_0F0E`): `HookModuleRegister` anchors hook-module
7//! ingestion receipts, `ObserverQuarantine` anchors observer-host trap
8//! quarantines, and the `ReplicaIdAllocation` + `AuditReceiptKeyPolicy`
9//! pair reserves the forward-looking event surface for federation /
10//! long-term audit activation.
11//!
12//! ## Forward-looking events — 0-emission posture
13//!
14//! These events are **define-only**: type + `ArkheEvent`
15//! derive + `TypeCode` reservation, but **no production code path emits
16//! them**. A 3-layer 0-emission defense:
17//!
18//! - **(a) `emit()` not called** — runtime never invokes
19//!   `emit_event::<…>` for either type. A workspace grep test verifies
20//!   0 production occurrences.
21//! - **(b) Cargo feature gate on type definition** — each type sits
22//!   behind a semantic feature flag (`federation-archive-hardened` /
23//!   `audit-receipt-key-identified`) so default builds do not even
24//!   compile the type.
25//! - **(c) Registry test under default features** — the runtime registry
26//!   inventory does not contain the reserved TypeCodes when the feature
27//!   gates are off.
28
29use arkhe_kernel::abi::{ExternalId, Tick, TypeCode};
30use bytes::Bytes;
31use serde::{Deserialize, Serialize};
32
33use crate::actor::ActorId;
34use crate::brand::ShellId;
35use crate::component::BoundedString;
36use crate::pii::DekId;
37use crate::user::UserId;
38// `ArkheEvent` here refers to the runtime-derive macro (re-exported by the
39// crate root from `arkhe-forge-macros`); the type-level `ArkheEvent` trait
40// defined below lives in the trait namespace, so both names coexist.
41use crate::ArkheEvent;
42
43/// Sealed marker trait for runtime Event types. Implementations come only
44/// from `#[derive(ArkheEvent)]`.
45pub trait ArkheEvent:
46    crate::__sealed::__Sealed + Serialize + for<'de> Deserialize<'de> + 'static
47{
48    /// Runtime `TypeCode` registry pin — Core Events live in
49    /// `0x0003_0F00..=0x0003_FFFF` (TypeCode sub-range split).
50    const TYPE_CODE: u32;
51
52    /// Monotone schema version — same rules as `ArkheComponent`.
53    const SCHEMA_VERSION: u16;
54
55    /// Convenience `TypeCode` accessor.
56    fn type_code() -> TypeCode {
57        TypeCode(Self::TYPE_CODE)
58    }
59}
60
61// ===================== Support types =====================
62
63/// Runtime SemVer — fixed-layout 3-tuple, postcard-stable.
64///
65/// `semver` crate's pre-release / build metadata strings are variable-width;
66/// the Runtime reserves a minimal 6-byte canonical shape instead.
67#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Serialize, Deserialize)]
68pub struct SemVer {
69    /// Major version.
70    pub major: u16,
71    /// Minor version.
72    pub minor: u16,
73    /// Patch version.
74    pub patch: u16,
75}
76
77impl SemVer {
78    /// Construct from components.
79    #[inline]
80    #[must_use]
81    pub const fn new(major: u16, minor: u16, patch: u16) -> Self {
82        Self {
83            major,
84            minor,
85            patch,
86        }
87    }
88}
89
90/// Runtime-only wire-format class tag for audit receipts.
91///
92/// Distinct from L0 `arkhe_kernel::persist::SignatureClass` (which
93/// holds key material). The L0 type is unserializable by design; this type
94/// is the serializable projection.
95#[non_exhaustive]
96#[repr(u8)]
97#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
98pub enum RuntimeSignatureClass {
99    /// No signature attached.
100    None = 0,
101    /// Classical Ed25519.
102    Ed25519 = 1,
103    /// Post-quantum ML-DSA-65 (Dilithium, FIPS 204).
104    MlDsa65 = 2,
105    /// Hybrid Ed25519 + ML-DSA-65 dual-sign.
106    Hybrid = 3,
107}
108
109/// Compliance tier classifier — crypto-erasure protection level
110/// Compliance tier indicator.
111#[non_exhaustive]
112#[repr(u8)]
113#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
114pub enum ComplianceTier {
115    /// Tier-0 — software KEK (dev / non-production).
116    Tier0 = 0,
117    /// Tier-1 — single KMS free-tier.
118    Tier1 = 1,
119    /// Tier-2 — production Multi-KMS + threshold HSM (t-of-n Shamir).
120    Tier2 = 2,
121}
122
123/// Trap classification for [`ObserverQuarantine`] — surfaced into the
124/// chain-anchored receipt so replay + audit can distinguish each
125/// sandbox-boundary failure mode.
126///
127/// **Wire-stable enum** (mirrors `RuntimeSignatureClass` / `ComplianceTier`):
128/// `#[repr(u8)]` + `#[non_exhaustive]` so additive expansion is
129/// non-breaking. Each variant has a fixed discriminant so the postcard
130/// wire format stays stable across schema-version bumps.
131///
132/// Mirrored as a host-internal type by `arkhe_forge_platform::observer_host`
133/// (re-exports this same enum). Single source of truth lives here in
134/// `arkhe-forge-core` because the value enters the L0 chain via the
135/// `ObserverQuarantine` event.
136#[non_exhaustive]
137#[repr(u8)]
138#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
139pub enum ObserverTrapClass {
140    /// wasm panic / cranelift trap — observer code itself faulted.
141    Panic = 0,
142    /// Fuel exhaustion — observer exceeded the per-invocation budget.
143    BudgetExceeded = 1,
144    /// Observer attempted to call a host-fn for which the active
145    /// per-invocation capability set lacks the matching token.
146    CapabilityDenied = 2,
147    /// Catch-all for cranelift trap variants not classified above
148    /// (incl. operator host-config errors like "no capability impl
149    /// registered" — distinguished only at audit-log granularity).
150    Other = 3,
151}
152
153/// Progress scope selector for multi-region / multi-KMS erasure progress
154/// (TypeCode reservation).
155#[non_exhaustive]
156#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
157pub enum ProgressScope {
158    /// Region scope — geographic / cloud region identifier.
159    Region(BoundedString<64>),
160    /// KMS identifier scope.
161    KmsIdentifier(BoundedString<64>),
162}
163
164// ===================== Core Events =====================
165
166/// `RuntimeBootstrap` — chain-anchored bootstrap receipt (the E12 axiom).
167///
168/// Emitted at instance first-tick, manifest change, and runtime semver bump.
169/// The `manifest_digest` + `typecode_pins` pair is how WAL replay validates
170/// that the runtime environment matches what produced the log.
171#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, ArkheEvent)]
172#[arkhe(type_code = 0x0003_0F01, schema_version = 1)]
173pub struct RuntimeBootstrap {
174    /// Wire schema version.
175    pub schema_version: u16,
176    /// L0 kernel semver at the bootstrap tick.
177    pub l0_semver: SemVer,
178    /// Runtime semver at the bootstrap tick.
179    pub runtime_semver: SemVer,
180    /// Canonical BLAKE3 digest of the manifest TOML.
181    pub manifest_digest: [u8; 32],
182    /// Active TypeCode registry snapshot — derive injects
183    /// canonical ascending sort before serialize.
184    #[arkhe(canonical_sort)]
185    pub typecode_pins: Vec<TypeCode>,
186    /// Tick at which bootstrap was recorded.
187    pub bootstrap_tick: Tick,
188}
189
190/// `UserErasureScheduled` — GDPR erasure lease accepted; cascade observer
191/// will complete the crypto-shred.
192#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, ArkheEvent)]
193#[arkhe(type_code = 0x0003_0F02, schema_version = 1)]
194pub struct UserErasureScheduled {
195    /// Wire schema version.
196    pub schema_version: u16,
197    /// Target User.
198    pub user: UserId,
199    /// Tick at which erasure was scheduled.
200    pub scheduled_tick: Tick,
201}
202
203/// `UserErasureCompleted` — crypto-erasure completion receipt
204/// (chain-anchored transparency).
205#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, ArkheEvent)]
206#[arkhe(type_code = 0x0003_0F03, schema_version = 1)]
207pub struct UserErasureCompleted {
208    /// Wire schema version.
209    pub schema_version: u16,
210    /// Target User.
211    pub user: UserId,
212    /// Tick at which the DEK was shredded.
213    pub dek_shred_tick: Tick,
214    /// Signature class used for the attestation payload.
215    pub attestation_class: RuntimeSignatureClass,
216    /// HSM attestation bytes (typically 64 or 128 B).
217    pub attestation_bytes: Bytes,
218    /// Transparency-log entry index.
219    pub transparency_log_index: u64,
220}
221
222/// `BackupErasurePropagated` — per-region offsite tombstone evidence
223/// Restore must refuse if any region is missing.
224#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, ArkheEvent)]
225#[arkhe(type_code = 0x0003_0F04, schema_version = 1)]
226pub struct BackupErasurePropagated {
227    /// Wire schema version.
228    pub schema_version: u16,
229    /// Target User.
230    pub user: UserId,
231    /// Region identifier (e.g. `"eu-west-1"`).
232    pub region: BoundedString<32>,
233    /// Tick at which the tombstone was applied.
234    pub applied_tick: Tick,
235    /// Signature class used for the receipt.
236    pub receipt_class: RuntimeSignatureClass,
237    /// Receipt payload bytes.
238    pub receipt_bytes: Bytes,
239}
240
241/// `GdprPolicyViolation` — audit trail for an actor-originated Action that
242/// targeted an ErasurePending User (L1 compute MC gate).
243#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, ArkheEvent)]
244#[arkhe(type_code = 0x0003_0F05, schema_version = 1)]
245pub struct GdprPolicyViolation {
246    /// Wire schema version.
247    pub schema_version: u16,
248    /// Acting actor.
249    pub actor: ActorId,
250    /// Tick at which the violating Action was attempted.
251    pub attempted_tick: Tick,
252    /// TypeCode of the rejected Action.
253    pub action_type_code: TypeCode,
254}
255
256/// `SignatureClassPolicy` — chain-anchored shell audit signature policy
257/// (the E13 axiom). 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/// `HookModuleRegister` — chain-anchored Hook host v2 module-registration
344/// receipt (E14.L2 axiom).
345///
346/// Emitted by the wasmtime hook host on every successful
347/// `register_module(bytes, expected_digest)`. Pairs the operator's
348/// manifest digest (which pins the expected module digest) with the
349/// actually-registered module digest — replay validates the host
350/// instantiated the module the manifest demanded, not a substitute.
351///
352/// **3-tier ingestion** anchored here:
353///
354/// - **Tier 1 (BLAKE3 digest pin)** — `module_digest` matches the value
355///   the operator pinned in `manifest_digest`-anchored manifest TOML.
356///   Catches operator config typos + accidental file substitution.
357///   This tier ships fully.
358/// - **Tier 2 (sigstore sign-before-load)** — recorded via
359///   `attestation_class` field; the verification closure is provided
360///   by an integration layer above this crate. Tier 1 alone is
361///   sufficient for the runtime contract: operator config is the trust
362///   root.
363/// - **Tier 3 (cargo-vet provenance)** — build-time check; runtime only
364///   records the attestation hash for chain-anchored audit.
365#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, ArkheEvent)]
366#[arkhe(type_code = 0x0003_0F0B, schema_version = 1)]
367pub struct HookModuleRegister {
368    /// Wire schema version.
369    pub schema_version: u16,
370    /// BLAKE3 digest of the manifest TOML that pinned the expected
371    /// module digest. Replay uses this to verify the manifest itself
372    /// was unchanged between registration time and replay time.
373    ///
374    /// **Caller responsibility**: this field is host-side recorded but
375    /// NOT host-side enforced. The integration layer
376    /// (`arkhe-forge-platform/src/manifest.rs`) is responsible for
377    /// hashing the operator's manifest TOML and passing the result
378    /// through to the event emission, alongside the manifest-signature
379    /// verification closure that makes the field cryptographically
380    /// meaningful.
381    pub manifest_digest: [u8; 32],
382    /// BLAKE3 digest of the registered wasm module bytes. Equals the
383    /// `expected_digest` parameter the operator passed; recorded so
384    /// replay can re-verify the module bytes against the same hash.
385    pub module_digest: [u8; 32],
386    /// Tick at which the module was registered.
387    pub register_tick: Tick,
388    /// Attestation class signalling Tier 2/3 presence. The default
389    /// path is [`RuntimeSignatureClass::None`] (Tier 1 BLAKE3 digest
390    /// pin only); Tier 2 sigstore integrations set the field to
391    /// `Ed25519` / `MlDsa65` / `HybridEd25519MlDsa65` once a
392    /// verification closure is wired in.
393    ///
394    /// **Semantics distinction**: in this `HookModuleRegister` context
395    /// `None` means "Tier 1 BLAKE3 digest pin only; no Tier 2/3
396    /// attestation present". Distinct from the audit-receipt
397    /// `None` (= "no signature class") which carries different
398    /// operational semantics. Same enum, context-specific reading.
399    pub attestation_class: RuntimeSignatureClass,
400}
401
402/// `ObserverQuarantine` — chain-anchored Observer host v2 trap-
403/// quarantine receipt (E15 axiom).
404///
405/// Emitted by the runtime supervisor when an observer wasm execution
406/// trips a sandbox-boundary failure (panic / budget / capability
407/// denial / other trap). The receipt anchors the operator's audit
408/// trail without observer wasm authorship — chain-non-affecting
409/// clause 3: the *host* supervises emission.
410///
411/// **Trigger boundary**: only `ObserverError` variants from the host
412/// trip Quarantine emission. `CapabilityExecutionError` (PG unreachable
413/// etc.) is **operational, NOT chain-anchored** — those surface via
414/// metric / `runtime_doctor_journal` instead.
415///
416/// **Replay-side verification**: replay re-checks the
417/// `observer_module_digest` against the bytes the manifest pinned at
418/// registration time (mirrors `HookModuleRegister`'s replay
419/// verification). Mismatch indicates manifest tampering or operator
420/// mis-deployment.
421///
422/// **3-tier ingestion mirror**: `attestation_class` records the
423/// observer module's ingestion attestation tier (Tier 1 BLAKE3 digest
424/// pin active by default; Tier 2 sigstore + Tier 3 cargo-vet
425/// scaffolded). Per-Quarantine the `attestation_class` reflects the
426/// state at registration time so audit logs distinguish "trapped after
427/// Tier-1-only ingestion" from Tier-2/3 paths.
428#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, ArkheEvent)]
429#[arkhe(type_code = 0x0003_0F0C, schema_version = 1)]
430pub struct ObserverQuarantine {
431    /// Wire schema version.
432    pub schema_version: u16,
433    /// BLAKE3 digest of the registered observer module bytes that
434    /// trapped. Equals the `expected_digest` the operator pinned at
435    /// registration; recorded so replay can re-verify the module
436    /// bytes against the same hash.
437    pub observer_module_digest: [u8; 32],
438    /// Tick at which the trap occurred + Quarantine was emitted by
439    /// the host supervisor.
440    pub quarantine_tick: Tick,
441    /// Trap classification — distinguishes panic / budget / cap-
442    /// deny / other for forensic + operator triage.
443    pub trap_class: ObserverTrapClass,
444    /// Attestation class signalling the Tier 2/3 ingestion state at
445    /// registration time. The default path is
446    /// [`RuntimeSignatureClass::None`] (Tier 1 BLAKE3 digest pin
447    /// only); Tier 2/3 paths set Ed25519 / MlDsa65 / Hybrid.
448    ///
449    /// **Semantics distinction**: in this `ObserverQuarantine`
450    /// context the value records the *observer module ingestion*
451    /// attestation tier — NOT the event-signing class. The
452    /// Quarantine event itself is chain-anchored under the runtime's
453    /// standard signing path (E13 shell-per-tick
454    /// `SignatureClassPolicy`), independent of this field.
455    pub attestation_class: RuntimeSignatureClass,
456}
457
458// ============================================================================
459// Cryptographic primitives — Attestation newtype +
460// AttestationSignerPolicy enum.
461// ============================================================================
462
463/// Length-sealed 64-byte attestation signature wrapper.
464///
465/// Constructed only via [`Attestation::from_bytes`] (which takes a
466/// `[u8; 64]` literal — length statically enforced by the Rust type
467/// system). The wire format is a postcard length-prefixed byte
468/// sequence (`Vec<u8>`-equivalent) with a strict 64-byte length check
469/// on deserialize. This combination produces a *length-sealed* type:
470///
471/// - **Constructor side**: any caller producing an `Attestation` does
472///   so via the `[u8; 64]` constructor — the type system rejects any
473///   other byte width at compile time.
474/// - **Deserialize side**: any wire bytes whose payload length is
475///   not exactly 64 produce a `serde::de::Error`, never a panic.
476///
477/// **Why a custom serde impl** (rather than `#[derive]` over `[u8; 64]`):
478/// serde's stock array deserializer caps at 32 bytes — the L0
479/// `WalRecord.signature` workaround uses `Vec<u8>` with length
480/// validation at the application layer. `Attestation` lifts the
481/// length invariant from convention to the type itself: any value of
482/// type `Attestation` that exists has 64 bytes, and the Deserialize
483/// impl is the exhaustive admission check.
484///
485/// Used by `ReplicaIdAllocation::registry_attestation` and
486/// `AuditReceiptKeyPolicy::attestation`.
487#[derive(Debug, Clone, Eq, PartialEq)]
488pub struct Attestation {
489    /// 64-byte payload. Private to enforce the constructor invariant —
490    /// any `Attestation` value that exists has `inner.len() == 64`.
491    inner: Vec<u8>,
492}
493
494impl Attestation {
495    /// Construct an [`Attestation`] from a fixed 64-byte signature.
496    ///
497    /// The `[u8; 64]` parameter type makes the length invariant
498    /// statically enforced by the Rust type system — any caller
499    /// passing a non-64-byte input is rejected at compile time.
500    #[must_use]
501    pub fn from_bytes(bytes: [u8; 64]) -> Self {
502        Self {
503            inner: bytes.to_vec(),
504        }
505    }
506
507    /// Borrow the underlying 64-byte payload as a slice.
508    #[must_use]
509    pub fn as_bytes(&self) -> &[u8] {
510        &self.inner
511    }
512}
513
514impl Serialize for Attestation {
515    /// Serializes as the underlying `Vec<u8>` (postcard length-prefix
516    /// plus bytes). Wire format is identical to a bare `Vec<u8>` of
517    /// length 64, so the wire baseline is preserved byte-for-byte.
518    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
519        self.inner.serialize(serializer)
520    }
521}
522
523impl<'de> Deserialize<'de> for Attestation {
524    /// Deserializes from a `Vec<u8>` and rejects (with `serde::de::Error`,
525    /// never a panic) any payload whose length is not exactly 64. This
526    /// makes `Attestation` the single admission check for the 64-byte
527    /// invariant on the wire side.
528    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
529        let inner: Vec<u8> = Vec::deserialize(deserializer)?;
530        if inner.len() != 64 {
531            return Err(serde::de::Error::custom(format!(
532                "Attestation: expected 64 bytes, got {}",
533                inner.len()
534            )));
535        }
536        Ok(Self { inner })
537    }
538}
539
540/// Signer policy for `AuditReceiptKeyPolicy::attestation`.
541///
542/// Each `AuditReceiptKeyPolicy` entry's attestation signature binds
543/// the inventory entry to *some* signing authority — but "which
544/// authority" is an operator-policy choice the runtime merely records.
545/// Three variants cover the expected operator topologies;
546/// `#[non_exhaustive]` lets additive variants land without breaking
547/// existing wire bytes.
548///
549/// The enum is paired with `AuditReceiptKeyPolicy::attestation` at
550/// the same struct level — currently no other event references it,
551/// so cohesion is preferred over abstraction. If a second user
552/// emerges, the enum can be lifted to a shared type with no
553/// wire-format change (additive non-breaking refactor).
554///
555/// **`Copy` derive — forward-compat constraint**: the derive
556/// constrains future variants to be field-less. A variant carrying
557/// data (e.g., `HardwareAttestation { tpm_version: u32 }` or
558/// threshold-signature parameters) would require the `Copy` derive
559/// to be removed, which is a breaking API change. Field-less policy
560/// reservation is the contract; data-bearing variants would arrive
561/// alongside the `Copy` removal as a coordinated breaking change.
562#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
563#[non_exhaustive]
564pub enum AttestationSignerPolicy {
565    /// Successor key signed by predecessor key — rotation chain
566    /// integrity. The recipient verifies the attestation against
567    /// the predecessor entry's `public_key`.
568    Predecessor,
569    /// Direct signature by an operator-root authority (HW-signed
570    /// or air-gapped key per `docs/release-keys.md §3` co-custody).
571    /// The recipient verifies against the operator-root public key
572    /// pinned in the runtime's release-keys metadata.
573    OperatorRoot,
574    /// Genesis self-signed proof-of-possession — the signing key
575    /// signs its own inventory entry. Reserved for the very first
576    /// inventory entry (no predecessor, no operator-root yet
577    /// pinned). Recipient verification = fixed-point check against
578    /// the entry's own `public_key`.
579    SelfSigned,
580}
581
582/// `ReplicaIdAllocation` — federation-replica registration receipt
583/// **Define-only**: the type sits behind the
584/// `federation-archive-hardened` Cargo feature so default builds do
585/// not compile the type at all (3-layer 0-emission defense, layer
586/// (b)).
587///
588/// Activation: when a federation registry signs off on a new replica
589/// entering the federation, the runtime would emit one
590/// `ReplicaIdAllocation` event into the chain so cross-replica audit
591/// can trace the membership lineage. The wire surface (TypeCode +
592/// schema) is reserved here. Activation gate: federation prerequisites
593/// complete (archive-hardening + `SignedArkheUri` + identity
594/// federation layer).
595///
596/// **0-emission posture**: no production code path calls
597/// `emit_event::<ReplicaIdAllocation>(..)`. A workspace grep test
598/// confirms zero emission sites; Cargo feature gating provides
599/// compile-time exclusion.
600#[cfg(feature = "federation-archive-hardened")]
601#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, ArkheEvent)]
602#[arkhe(type_code = 0x0003_0F0D, schema_version = 1)]
603pub struct ReplicaIdAllocation {
604    /// Wire schema version.
605    pub schema_version: u16,
606    /// Federation identifier (128-bit). `[16]` is collision-resistant
607    /// for any plausible federation count.
608    pub federation_id: [u8; 16],
609    /// Replica identifier within the federation. The width is u64
610    /// (federation-scale collision-resistant — ~10^19
611    /// replicas/federation, well beyond any plausible ultra-scale
612    /// federation deployment). A narrower u32 would cap at ~4B
613    /// replicas, an order-of-magnitude tight bound on the long tail
614    /// of federated systems.
615    pub replica_id: u64,
616    /// 32-bit nonce drawn at allocation time, fed into the
617    /// registry attestation signature alongside the (federation,
618    /// replica, tick) tuple. Defends against replay of older
619    /// allocation requests.
620    pub allocation_nonce: u32,
621    /// Tick at which the allocation became effective (chain-anchor
622    /// for ordering relative to other federation events).
623    pub effective_tick: Tick,
624    /// Federation-registry attestation over (federation_id,
625    /// replica_id, allocation_nonce, effective_tick). 64-byte
626    /// signature (Ed25519 width) wrapped in [`Attestation`] —
627    /// length-sealed at construction (`from_bytes([u8; 64])`) and
628    /// strictly verified on deserialize (length invariant lifted from
629    /// convention to type). The signer is the federation registry;
630    /// the signer-policy enum reservation lives on
631    /// `AuditReceiptKeyPolicy::signer_policy` (audit-receipt key
632    /// rotation context where signer choice is operator-variable).
633    pub registry_attestation: Attestation,
634}
635
636/// `AuditReceiptKeyPolicy` — audit-receipt key inventory + rotation
637/// manifest (the E13 axiom). **Define-only**: the type sits
638/// behind the `audit-receipt-key-identified` Cargo feature so default
639/// builds do not compile the type (3-layer 0-emission defense, layer
640/// (b)).
641///
642/// Activation: when the operator rotates the audit-receipt signing key
643/// (or initially declares the genesis key in the
644/// `docs/release-keys.md §1` inventory), the runtime would emit one
645/// `AuditReceiptKeyPolicy` event into the chain so audit-trail
646/// consumers can verify which key was active at which tick. The wire
647/// surface is reserved here. Activation gate: operator-side carry-over
648/// (g) "audit-receipt key identity declared in `docs/release-keys.md`
649/// inventory anchor.
650///
651/// **0-emission posture**: identical to `ReplicaIdAllocation` —
652/// production code path emits zero, a workspace grep test verifies,
653/// Cargo feature gating closes the compile.
654#[cfg(feature = "audit-receipt-key-identified")]
655#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, ArkheEvent)]
656#[arkhe(type_code = 0x0003_0F0E, schema_version = 1)]
657pub struct AuditReceiptKeyPolicy {
658    /// Wire schema version.
659    pub schema_version: u16,
660    /// Audit-receipt key identifier. The width is `[u8; 16]` (128-bit,
661    /// UUID-class collision space). A narrower 8-byte width would give
662    /// only a 2^32 birthday bound, tight at federation scale where
663    /// multiple operators independently mint keys; 16 bytes raises the
664    /// bound to 2^64 = computationally infeasible across any plausible
665    /// key population. The `docs/release-keys.md §1` inventory entry
666    /// maps this identifier to the physical key material.
667    pub key_id: [u8; 16],
668    /// Signature class for receipts under this key. Reuses the
669    /// `RuntimeSignatureClass` enum so the wire
670    /// tagging is consistent with `SignatureClassPolicy` (E13).
671    pub algorithm: RuntimeSignatureClass,
672    /// Public-key wire bytes. Variable length to accommodate
673    /// classical (Ed25519, 32 bytes), post-quantum (ML-DSA-65,
674    /// ~1952 bytes), and hybrid representations. Bounded by the
675    /// runtime's per-event size cap on encode.
676    pub public_key: Bytes,
677    /// Predecessor `key_id` if this entry succeeds an earlier key
678    /// (rotation chain). `None` = genesis (first entry in the
679    /// inventory) or an operator-policy-determined unrelated key.
680    /// Width matches `key_id` ([u8; 16]).
681    pub predecessor_key_id: Option<[u8; 16]>,
682    /// Tick at which this key entry becomes effective for new
683    /// audit-receipt signatures.
684    pub effective_tick: Tick,
685    /// Tick at which this key entry retires (no further new
686    /// signatures, but historical receipts remain valid). `None`
687    /// for the currently-active entry.
688    pub retirement_tick: Option<Tick>,
689    /// Signer policy for the [`Self::attestation`] field —
690    /// declares whether the signature was produced by the
691    /// predecessor key (rotation chain), an operator-root authority,
692    /// or as a genesis self-signed proof-of-possession. The enum +
693    /// field reservation lets emission code populate the value per
694    /// operator policy without further schema work.
695    /// `#[non_exhaustive]` keeps additive variants forward-compat.
696    pub signer_policy: AttestationSignerPolicy,
697    /// Attestation signature binding the inventory entry to the
698    /// operator's signing authority. 64-byte signature (Ed25519
699    /// width) wrapped in [`Attestation`] — length-sealed at
700    /// construction and strictly verified on deserialize (length
701    /// invariant lifted from convention to type). Recipient
702    /// verification path is selected by [`Self::signer_policy`].
703    pub attestation: Attestation,
704}
705
706#[cfg(test)]
707#[allow(clippy::unwrap_used, clippy::expect_used)]
708mod tests {
709    use super::*;
710
711    #[test]
712    fn runtime_bootstrap_serde_roundtrip() {
713        let rb = RuntimeBootstrap {
714            schema_version: 1,
715            l0_semver: SemVer::new(0, 11, 0),
716            runtime_semver: SemVer::new(0, 11, 0),
717            manifest_digest: [0xABu8; 32],
718            typecode_pins: vec![TypeCode(0x0003_0001), TypeCode(0x0003_0002)],
719            bootstrap_tick: Tick(1),
720        };
721        let bytes = postcard::to_stdvec(&rb).unwrap();
722        let back: RuntimeBootstrap = postcard::from_bytes(&bytes).unwrap();
723        assert_eq!(rb, back);
724    }
725
726    #[test]
727    fn per_region_progress_with_region_scope_roundtrip() {
728        let ev = PerRegionErasureProgress {
729            schema_version: 1,
730            user: crate::user::UserId::new(arkhe_kernel::abi::EntityId::new(42).unwrap()),
731            scope: ProgressScope::Region(BoundedString::<64>::new("eu-west-1").unwrap()),
732            shred_tick: Tick(100),
733            attestation_class: RuntimeSignatureClass::Ed25519,
734            attestation_bytes: Bytes::from_static(&[0u8; 64]),
735        };
736        let bytes = postcard::to_stdvec(&ev).unwrap();
737        let back: PerRegionErasureProgress = postcard::from_bytes(&bytes).unwrap();
738        assert_eq!(ev, back);
739    }
740
741    #[test]
742    fn core_event_type_code_pins_match_spec() {
743        assert_eq!(RuntimeBootstrap::TYPE_CODE, 0x0003_0F01);
744        assert_eq!(UserErasureScheduled::TYPE_CODE, 0x0003_0F02);
745        assert_eq!(UserErasureCompleted::TYPE_CODE, 0x0003_0F03);
746        assert_eq!(BackupErasurePropagated::TYPE_CODE, 0x0003_0F04);
747        assert_eq!(GdprPolicyViolation::TYPE_CODE, 0x0003_0F05);
748        assert_eq!(SignatureClassPolicy::TYPE_CODE, 0x0003_0F06);
749        assert_eq!(CrossShellActivity::TYPE_CODE, 0x0003_0F07);
750        assert_eq!(PerRegionErasureProgress::TYPE_CODE, 0x0003_0F08);
751        assert_eq!(DekMigrationCompleted::TYPE_CODE, 0x0003_0F09);
752        assert_eq!(ComplianceTierChange::TYPE_CODE, 0x0003_0F0A);
753        assert_eq!(HookModuleRegister::TYPE_CODE, 0x0003_0F0B);
754        assert_eq!(ObserverQuarantine::TYPE_CODE, 0x0003_0F0C);
755        // Forward-looking event TypeCode pins are verified only when
756        // the activation feature is enabled (cfg-gate per 3-layer
757        // 0-emission defense, layer (b)). The TypeCode constants in
758        // `typecode::core_event` remain unconditional anchors.
759        #[cfg(feature = "federation-archive-hardened")]
760        assert_eq!(ReplicaIdAllocation::TYPE_CODE, 0x0003_0F0D);
761        #[cfg(feature = "audit-receipt-key-identified")]
762        assert_eq!(AuditReceiptKeyPolicy::TYPE_CODE, 0x0003_0F0E);
763    }
764
765    #[test]
766    fn hook_module_register_serde_roundtrip() {
767        let ev = HookModuleRegister {
768            schema_version: 1,
769            manifest_digest: [0xAAu8; 32],
770            module_digest: [0xBBu8; 32],
771            register_tick: Tick(123),
772            attestation_class: RuntimeSignatureClass::None,
773        };
774        let bytes = postcard::to_stdvec(&ev).unwrap();
775        let back: HookModuleRegister = postcard::from_bytes(&bytes).unwrap();
776        assert_eq!(ev, back);
777    }
778
779    #[test]
780    fn hook_module_register_type_code_matches_typecode_constant() {
781        // The typecode.rs core_event::HOOK_MODULE_REGISTER constant and
782        // the #[arkhe(type_code = ...)] derive must agree — guards against
783        // accidental drift between the catalog and the struct attribute.
784        assert_eq!(
785            HookModuleRegister::TYPE_CODE,
786            crate::typecode::core_event::HOOK_MODULE_REGISTER
787        );
788    }
789
790    #[test]
791    fn observer_quarantine_serde_roundtrip() {
792        let ev = ObserverQuarantine {
793            schema_version: 1,
794            observer_module_digest: [0xCCu8; 32],
795            quarantine_tick: Tick(456),
796            trap_class: ObserverTrapClass::Panic,
797            attestation_class: RuntimeSignatureClass::None,
798        };
799        let bytes = postcard::to_stdvec(&ev).unwrap();
800        let back: ObserverQuarantine = postcard::from_bytes(&bytes).unwrap();
801        assert_eq!(ev, back);
802    }
803
804    #[test]
805    fn observer_quarantine_type_code_matches_typecode_constant() {
806        assert_eq!(
807            ObserverQuarantine::TYPE_CODE,
808            crate::typecode::core_event::OBSERVER_QUARANTINE
809        );
810    }
811
812    #[test]
813    fn observer_trap_class_wire_discriminants_stable() {
814        // Verify each variant's wire-stable discriminant — `#[repr(u8)]`
815        // pins these so the postcard format stays bit-identical across
816        // schema-version bumps. Drift here = wire breakage.
817        for (variant, expected_disc) in [
818            (ObserverTrapClass::Panic, 0u8),
819            (ObserverTrapClass::BudgetExceeded, 1u8),
820            (ObserverTrapClass::CapabilityDenied, 2u8),
821            (ObserverTrapClass::Other, 3u8),
822        ] {
823            // Round-trip through postcard verifies the wire byte
824            // matches the declared discriminant. (postcard encodes
825            // unit-variant enums as a single varint of the
826            // discriminant.)
827            let bytes = postcard::to_stdvec(&variant).unwrap();
828            assert_eq!(
829                bytes,
830                vec![expected_disc],
831                "ObserverTrapClass::{variant:?} discriminant drift"
832            );
833        }
834    }
835
836    #[test]
837    fn observer_quarantine_with_each_trap_class_roundtrips() {
838        for trap_class in [
839            ObserverTrapClass::Panic,
840            ObserverTrapClass::BudgetExceeded,
841            ObserverTrapClass::CapabilityDenied,
842            ObserverTrapClass::Other,
843        ] {
844            let ev = ObserverQuarantine {
845                schema_version: 1,
846                observer_module_digest: [0u8; 32],
847                quarantine_tick: Tick(1),
848                trap_class,
849                attestation_class: RuntimeSignatureClass::None,
850            };
851            let bytes = postcard::to_stdvec(&ev).unwrap();
852            let back: ObserverQuarantine = postcard::from_bytes(&bytes).unwrap();
853            assert_eq!(ev.trap_class, back.trap_class);
854        }
855    }
856
857    #[test]
858    fn semver_roundtrip_is_stable() {
859        let v = SemVer::new(0, 11, 0);
860        let a = postcard::to_stdvec(&v).unwrap();
861        let b = postcard::to_stdvec(&v).unwrap();
862        assert_eq!(a, b);
863        let back: SemVer = postcard::from_bytes(&a).unwrap();
864        assert_eq!(back, v);
865    }
866
867    // ----- Forward-looking event tests -----
868    //
869    // Each test is feature-gated to its activation flag (3-layer
870    // 0-emission defense, layer (b)). The default-features build skips
871    // these tests because the underlying type is not compiled.
872
873    #[cfg(feature = "federation-archive-hardened")]
874    #[test]
875    fn replica_id_allocation_serde_roundtrip() {
876        let ev = ReplicaIdAllocation {
877            schema_version: 1,
878            federation_id: [0xF1u8; 16],
879            replica_id: 7,
880            allocation_nonce: 0xCAFE_BABE,
881            effective_tick: Tick(1234),
882            registry_attestation: Attestation::from_bytes([0x55u8; 64]),
883        };
884        let bytes = postcard::to_stdvec(&ev).unwrap();
885        let back: ReplicaIdAllocation = postcard::from_bytes(&bytes).unwrap();
886        assert_eq!(ev, back);
887    }
888
889    /// Verify u64 width preserved for replica_id values above
890    /// `u32::MAX`. Regression sentinel against any schema width
891    /// change that would silently truncate the high 32 bits
892    /// (federation-scale concern). Pin chosen to span the upper
893    /// half of u64 so byte-level postcard varint
894    /// encoding exercises the >5-byte continuation path.
895    #[cfg(feature = "federation-archive-hardened")]
896    #[test]
897    fn replica_id_allocation_high_replica_id_preserves_u64_width() {
898        let high_replica = 0x1234_5678_9ABC_DEF0u64;
899        assert!(high_replica > u64::from(u32::MAX));
900        let ev = ReplicaIdAllocation {
901            schema_version: 1,
902            federation_id: [0xA5u8; 16],
903            replica_id: high_replica,
904            allocation_nonce: 0xDEAD_BEEF,
905            effective_tick: Tick(99_999),
906            registry_attestation: Attestation::from_bytes([0x33u8; 64]),
907        };
908        let bytes = postcard::to_stdvec(&ev).unwrap();
909        let back: ReplicaIdAllocation = postcard::from_bytes(&bytes).unwrap();
910        assert_eq!(ev, back);
911        assert_eq!(back.replica_id, high_replica);
912    }
913
914    #[cfg(feature = "federation-archive-hardened")]
915    #[test]
916    fn replica_id_allocation_type_code_matches_typecode_constant() {
917        assert_eq!(
918            ReplicaIdAllocation::TYPE_CODE,
919            crate::typecode::core_event::REPLICA_ID_ALLOCATION
920        );
921    }
922
923    #[cfg(feature = "audit-receipt-key-identified")]
924    #[test]
925    fn audit_receipt_key_policy_serde_roundtrip_with_genesis_entry() {
926        let ev = AuditReceiptKeyPolicy {
927            schema_version: 1,
928            key_id: [0xABu8; 16],
929            algorithm: RuntimeSignatureClass::Ed25519,
930            public_key: Bytes::from_static(&[0u8; 32]),
931            predecessor_key_id: None,
932            effective_tick: Tick(0),
933            retirement_tick: None,
934            signer_policy: AttestationSignerPolicy::SelfSigned,
935            attestation: Attestation::from_bytes([0x77u8; 64]),
936        };
937        let bytes = postcard::to_stdvec(&ev).unwrap();
938        let back: AuditReceiptKeyPolicy = postcard::from_bytes(&bytes).unwrap();
939        assert_eq!(ev, back);
940    }
941
942    #[cfg(feature = "audit-receipt-key-identified")]
943    #[test]
944    fn audit_receipt_key_policy_serde_roundtrip_with_rotation_entry() {
945        let ev = AuditReceiptKeyPolicy {
946            schema_version: 1,
947            key_id: [0xCDu8; 16],
948            algorithm: RuntimeSignatureClass::MlDsa65,
949            public_key: Bytes::from_static(&[0xEEu8; 1952]), // ML-DSA-65 wire size
950            predecessor_key_id: Some([0xABu8; 16]),
951            effective_tick: Tick(100),
952            retirement_tick: Some(Tick(1000)),
953            signer_policy: AttestationSignerPolicy::Predecessor,
954            attestation: Attestation::from_bytes([0x99u8; 64]),
955        };
956        let bytes = postcard::to_stdvec(&ev).unwrap();
957        let back: AuditReceiptKeyPolicy = postcard::from_bytes(&bytes).unwrap();
958        assert_eq!(ev, back);
959    }
960
961    /// Verify [u8; 16] width preserved with high-entropy distinct
962    /// bytes in both `key_id` and `predecessor_key_id`. Repeated-byte
963    /// literals like `[0xABu8; 16]` / `[0xCDu8; 16]` would silently
964    /// round-trip through any narrower
965    /// width that happens to truncate to the same repeated byte. This
966    /// test pins distinct bytes per position so a regression to
967    /// `[u8; 8]` would catch on the upper-half `key_id[8..16]` and
968    /// `predecessor_key_id[8..16]` byte mismatch. Belt-and-suspenders
969    /// against silent width truncation.
970    #[cfg(feature = "audit-receipt-key-identified")]
971    #[test]
972    fn audit_receipt_key_policy_distinct_bytes_preserve_full_16_byte_widths() {
973        let key_id: [u8; 16] = [
974            0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, 0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54,
975            0x32, 0x10,
976        ];
977        let predecessor_key_id: [u8; 16] = [
978            0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE, 0xBA, 0xBE, 0xF0, 0x0D, 0xFA, 0xCE, 0x12, 0x34,
979            0x56, 0x78,
980        ];
981        let ev = AuditReceiptKeyPolicy {
982            schema_version: 1,
983            key_id,
984            algorithm: RuntimeSignatureClass::Ed25519,
985            public_key: Bytes::from_static(&[0xC3u8; 32]),
986            predecessor_key_id: Some(predecessor_key_id),
987            effective_tick: Tick(2_500),
988            retirement_tick: Some(Tick(5_000)),
989            signer_policy: AttestationSignerPolicy::OperatorRoot,
990            attestation: Attestation::from_bytes([0x44u8; 64]),
991        };
992        let bytes = postcard::to_stdvec(&ev).unwrap();
993        let back: AuditReceiptKeyPolicy = postcard::from_bytes(&bytes).unwrap();
994        assert_eq!(ev, back);
995        assert_eq!(back.key_id, key_id);
996        assert_eq!(back.predecessor_key_id, Some(predecessor_key_id));
997        // Explicit upper-half spot-check (catches truncation regression
998        // even if compiler accepted [u8; 8] literal somehow).
999        assert_eq!(back.key_id[8..16], key_id[8..16]);
1000    }
1001
1002    #[cfg(feature = "audit-receipt-key-identified")]
1003    #[test]
1004    fn audit_receipt_key_policy_type_code_matches_typecode_constant() {
1005        assert_eq!(
1006            AuditReceiptKeyPolicy::TYPE_CODE,
1007            crate::typecode::core_event::AUDIT_RECEIPT_KEY_POLICY
1008        );
1009    }
1010
1011    /// 0-emission posture confirmation (layer (a) — `emit()` not
1012    /// called). The `ArkheEvent` trait makes the type *eligible* for
1013    /// chain emission via `Op::EmitEvent`, but **no production code**
1014    /// calls `emit_event::<ReplicaIdAllocation>(..)` or
1015    /// `emit_event::<AuditReceiptKeyPolicy>(..)`. A workspace grep
1016    /// verification scans the source tree and asserts 0 occurrences.
1017    /// This test is the structural anchor — type definitions exist,
1018    /// but the trait constants alone do not constitute emission.
1019    ///
1020    /// Feature-gated under both activation flags so the test compiles
1021    /// only when the types compile (cfg-gate, layer (b)).
1022    #[cfg(all(
1023        feature = "federation-archive-hardened",
1024        feature = "audit-receipt-key-identified"
1025    ))]
1026    #[test]
1027    fn forward_looking_events_are_define_only() {
1028        // The TYPE_CODE constants exist + are pinned. Schema version
1029        // is 1 (initial wire format). No emission entry point is
1030        // defined for either type — verified structurally by Track
1031        // H.3 grep. This test is the architecture anchor.
1032        assert_eq!(ReplicaIdAllocation::SCHEMA_VERSION, 1);
1033        assert_eq!(AuditReceiptKeyPolicy::SCHEMA_VERSION, 1);
1034        assert_eq!(ReplicaIdAllocation::TYPE_CODE, 0x0003_0F0D);
1035        assert_eq!(AuditReceiptKeyPolicy::TYPE_CODE, 0x0003_0F0E);
1036    }
1037
1038    // ----- Attestation newtype + AttestationSignerPolicy tests.
1039    // These are unconditional (no cfg-gate) because the types themselves
1040    // live unconditionally in the module — only the events that
1041    // *consume* them are cfg-gated under the activation flags.
1042
1043    /// `Attestation` round-trips through postcard byte-identical to a
1044    /// bare `Vec<u8>` of length 64 (wire baseline preservation).
1045    #[test]
1046    fn attestation_serde_round_trip_preserves_64_bytes() {
1047        let payload: [u8; 64] = [0xA1; 64];
1048        let att = Attestation::from_bytes(payload);
1049        let bytes = postcard::to_stdvec(&att).unwrap();
1050        let back: Attestation = postcard::from_bytes(&bytes).unwrap();
1051        assert_eq!(att, back);
1052        assert_eq!(back.as_bytes(), &payload);
1053        assert_eq!(back.as_bytes().len(), 64);
1054    }
1055
1056    /// Wire format invariance with bare `Vec<u8>` of length 64. The
1057    /// custom `Serialize` impl delegates to `Vec<u8>::serialize`, so
1058    /// an `Attestation` and an equivalent `vec![0x..; 64]` produce
1059    /// byte-for-byte identical postcard output. This preserves the
1060    /// sealed wire baseline — any emitted bytes decode equivalently
1061    /// whether the receiver expects `Attestation` or `Vec<u8>`.
1062    #[test]
1063    fn attestation_wire_format_byte_identical_to_vec_u8_length_64() {
1064        let payload: [u8; 64] = [0xC7; 64];
1065        let att_bytes = postcard::to_stdvec(&Attestation::from_bytes(payload)).unwrap();
1066        let vec_bytes = postcard::to_stdvec(&payload.to_vec()).unwrap();
1067        assert_eq!(att_bytes, vec_bytes);
1068    }
1069
1070    /// `Attestation` deserialize **rejects** payloads whose length is
1071    /// not exactly 64 with a `serde::de::Error`, never a panic. This is
1072    /// the single admission check that lifts the 64-byte invariant from
1073    /// convention to type.
1074    #[test]
1075    fn attestation_deserialize_rejects_short_payload() {
1076        // Postcard-encode a 32-byte Vec<u8> (still serde-valid, but
1077        // shorter than the Attestation contract).
1078        let short_payload: Vec<u8> = vec![0xBB; 32];
1079        let bytes = postcard::to_stdvec(&short_payload).unwrap();
1080        let result: Result<Attestation, _> = postcard::from_bytes(&bytes);
1081        assert!(
1082            result.is_err(),
1083            "32-byte payload must be rejected as not-64-bytes"
1084        );
1085    }
1086
1087    #[test]
1088    fn attestation_deserialize_rejects_long_payload() {
1089        let long_payload: Vec<u8> = vec![0xCC; 65];
1090        let bytes = postcard::to_stdvec(&long_payload).unwrap();
1091        let result: Result<Attestation, _> = postcard::from_bytes(&bytes);
1092        assert!(
1093            result.is_err(),
1094            "65-byte payload must be rejected as not-64-bytes"
1095        );
1096    }
1097
1098    #[test]
1099    fn attestation_deserialize_rejects_empty_payload() {
1100        let empty_payload: Vec<u8> = Vec::new();
1101        let bytes = postcard::to_stdvec(&empty_payload).unwrap();
1102        let result: Result<Attestation, _> = postcard::from_bytes(&bytes);
1103        assert!(
1104            result.is_err(),
1105            "empty payload must be rejected as not-64-bytes"
1106        );
1107    }
1108
1109    /// All three [`AttestationSignerPolicy`] variants round-trip
1110    /// through postcard. The variant tag is a postcard varint; an
1111    /// additive variant (operator-policy expansion) lands as a new
1112    /// tag without disturbing existing tags (`#[non_exhaustive]` on
1113    /// the enum + variant order preservation).
1114    #[test]
1115    fn attestation_signer_policy_round_trip_all_three_variants() {
1116        for variant in [
1117            AttestationSignerPolicy::Predecessor,
1118            AttestationSignerPolicy::OperatorRoot,
1119            AttestationSignerPolicy::SelfSigned,
1120        ] {
1121            let bytes = postcard::to_stdvec(&variant).unwrap();
1122            let back: AttestationSignerPolicy = postcard::from_bytes(&bytes).unwrap();
1123            assert_eq!(variant, back);
1124        }
1125    }
1126}