//! Operation envelopes, their canonical hash, and the well-formedness contract //! (Chapter 6 §"Operation Envelopes", §"Envelope Acceptance"). //! //! An operation is transmitted, stored, and reduced as an *envelope*: the //! payload together with its identity, ordering stamp, causal context, and //! optional transaction grouping. The [`EnvelopeHash`] is the BLAKE3-256 of the //! envelope's canonical serialization under the `MUSCENVH` domain tag; it is //! what the [`OperationSlot`](crate::OperationSlot) compares to detect //! equivocation (two distinct canonical envelopes under one //! [`OperationId`](epiphany_core::OperationId)). //! //! [`well_formed`] is the reception gate (Chapter 6 §6.4): an envelope that is //! not well-formed is rejected and never enters the canonical operation set. //! Crucially, acceptance does **not** trust stamp *content* — a peer may emit //! implausible future times — it only checks the structural invariants, //! foremost `stamp.id == id`. use epiphany_core::{OperationId, TransactionId}; use epiphany_determinism::{CanonicalDecode, CanonicalEncode, DomainTag, Preimage}; use crate::causal::CausalContext; use crate::encode::{push_canon, push_tag}; use crate::payload::OperationPayload; use crate::stamp::OperationStamp; use crate::support::AuthorId; /// The BLAKE3-256 hash of a canonical envelope serialization under the /// `MUSCENVH` domain tag (Chapter 6 §6.5). Ordered lexicographically on the 32 /// bytes — the order an `Equivocated` slot's `candidates` set enumerates in. #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] pub struct EnvelopeHash(pub [u8; 32]); impl EnvelopeHash { /// The raw 32 bytes. #[inline] pub const fn as_bytes(&self) -> &[u8; 32] { &self.0 } /// Lowercase hex (64 chars), for diagnostics. pub fn to_hex(&self) -> String { const HEX: &[u8; 16] = b"0123456789abcdef"; let mut s = String::with_capacity(64); for &b in &self.0 { s.push(HEX[(b >> 4) as usize] as char); s.push(HEX[(b & 0x0f) as usize] as char); } s } } impl core::fmt::Debug for EnvelopeHash { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { write!(f, "EnvelopeHash({}…)", &self.to_hex()[..8]) } } impl CanonicalEncode for EnvelopeHash { #[inline] fn encode_canonical(&self, out: &mut Vec) { out.extend_from_slice(&self.0); } } /// An operation envelope (Chapter 6 §"Operation Envelopes"). #[derive(Clone, PartialEq, Eq, Debug)] pub struct OperationEnvelope { /// Stable identifier of this operation. pub id: OperationId, /// Author (may differ from the replica in shared authoring sessions). pub author: AuthorId, /// Ordering stamp. For canonical reduction order; never for identity. pub stamp: OperationStamp, /// Compact causal context (dotted version vector). pub causal_context: CausalContext, /// Optional transaction grouping; members sharing a `TransactionId` reduce /// atomically. pub transaction: Option, /// The mutation itself. pub payload: OperationPayload, } impl OperationEnvelope { /// The canonical [`EnvelopeHash`] (`BLAKE3(MUSCENVH || canonical_bytes)`). pub fn envelope_hash(&self) -> EnvelopeHash { let mut p = Preimage::new(DomainTag::ENVELOPE); p.push_bytes(&self.to_canonical_bytes()); EnvelopeHash(*p.finish().as_bytes()) } /// The binary-format schema major this envelope's canonical bytes require /// ([`OperationPayload::schema_major`]). An op-envelope block's schema major /// is the maximum over the envelopes it carries. pub fn schema_major(&self) -> u16 { self.payload.schema_major() } } impl CanonicalEncode for OperationEnvelope { fn encode_canonical(&self, out: &mut Vec) { push_canon(out, &self.id); push_canon(out, &self.author); push_canon(out, &self.stamp); push_canon(out, &self.causal_context); match &self.transaction { None => push_tag(out, 0), Some(t) => { push_tag(out, 1); push_canon(out, t); } } push_canon(out, &self.payload); } } /// Reads the [`OperationId`] a canonically encoded envelope declares from its /// **leading 16 bytes** alone, without decoding the rest of the envelope. /// /// The canonical envelope encoding leads with the id /// ([`OperationEnvelope::encode_canonical`] pushes `id` first, and an /// `OperationId`'s canonical form is exactly 16 big-endian bytes), so this is a /// total, allocation-free peek. Returns `None` when fewer than 16 bytes are /// present. It deliberately performs **no** validation of the remaining bytes — /// full decoding stays the caller's job — which is exactly the contract the /// bundle's operation index (Chapter 8 §"The Operation Index") needs: the /// bundle layer keys index entries on the raw 16 id bytes without ever /// interpreting an envelope, and this helper is the one place the ops layer /// vouches that those bytes really are the id. pub fn peek_operation_id(envelope_bytes: &[u8]) -> Option { let head = envelope_bytes.get(..16)?; OperationId::decode_canonical(head).ok() } /// Why an envelope failed the well-formedness check (Chapter 6 §6.4). A /// rejected envelope is recorded in the local diagnostic log but does not enter /// the canonical operation set. #[derive(Copy, Clone, PartialEq, Eq, Debug)] pub enum WellFormednessError { /// The operation id's replica is the reserved `SYSTEM_DERIVED` namespace; /// operations are not authored by the system namespace. SystemDerivedReplica, /// `stamp.id` is not byte-identical to the top-level `id`: the stamp /// addresses a different operation than the envelope's identity declares. StampIdMismatch, /// `stamp.hlc.physical_time` is negative (it must be finite and /// non-negative; the integer encoding is always finite). NegativePhysicalTime, /// The causal context's vector references the reserved `SYSTEM_DERIVED` /// replica namespace, which never authors operations. MalformedCausalReplica, /// A causal dot is not a well-formed `OperationId` (its replica is the /// reserved `SYSTEM_DERIVED` namespace). MalformedDot, } impl core::fmt::Display for WellFormednessError { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { let msg = match self { WellFormednessError::SystemDerivedReplica => { "operation id uses the reserved SYSTEM_DERIVED replica namespace" } WellFormednessError::StampIdMismatch => "stamp.id does not equal the envelope id", WellFormednessError::NegativePhysicalTime => "stamp physical_time is negative", WellFormednessError::MalformedCausalReplica => { "causal context references the SYSTEM_DERIVED replica namespace" } WellFormednessError::MalformedDot => { "a causal dot uses the SYSTEM_DERIVED replica namespace" } }; f.write_str(msg) } } impl std::error::Error for WellFormednessError {} /// Checks an incoming envelope for well-formedness (Chapter 6 §6.4). Returns /// `Ok(())` if it may enter the operation set, or the first structural failure. /// /// The check is *structural only*: it never validates the plausibility of the /// HLC content (a peer may legitimately emit implausible future times), and it /// never depends on arrival order — two replicas observing the same envelope /// reach the same verdict. pub fn well_formed(env: &OperationEnvelope) -> Result<(), WellFormednessError> { // OperationId replica is not the system namespace. if env.id.replica.is_system_derived() { return Err(WellFormednessError::SystemDerivedReplica); } // stamp.id is byte-identical to the top-level id. if env.stamp.id != env.id { return Err(WellFormednessError::StampIdMismatch); } // physical_time is finite (always, for the integer encoding) and non-negative. if env.stamp.hlc.physical_time.0 < 0 { return Err(WellFormednessError::NegativePhysicalTime); } // Causal context references only well-formed replica identifiers … if env .causal_context .vector .keys() .any(|r| r.is_system_derived()) { return Err(WellFormednessError::MalformedCausalReplica); } // … and its dots are well-formed OperationIds. if env .causal_context .dots() .any(|d| d.replica.is_system_derived()) { return Err(WellFormednessError::MalformedDot); } Ok(()) } #[cfg(test)] mod tests { use super::*; use crate::payload::{OperationKind, RespellPitchOp}; use crate::stamp::HybridLogicalClock; use epiphany_core::{PitchId, ReplicaId, WallClockTime}; fn env(id: OperationId, stamp_id: OperationId) -> OperationEnvelope { OperationEnvelope { id, author: AuthorId(1), stamp: OperationStamp::new(HybridLogicalClock::new(WallClockTime(10), 0), stamp_id), causal_context: CausalContext::new(), transaction: None, payload: OperationPayload::Primitive(OperationKind::RespellPitch(RespellPitchOp { pitch: PitchId::new(ReplicaId(1), 1), spelling: crate::valuegen::spelling(7), })), } } #[test] fn well_formed_requires_stamp_id_equals_id() { let good = env( OperationId::new(ReplicaId(1), 5), OperationId::new(ReplicaId(1), 5), ); assert_eq!(well_formed(&good), Ok(())); let bad = env( OperationId::new(ReplicaId(1), 5), OperationId::new(ReplicaId(1), 6), ); assert_eq!(well_formed(&bad), Err(WellFormednessError::StampIdMismatch)); } #[test] fn well_formed_rejects_system_derived_author_replica() { let id = OperationId::new(ReplicaId::SYSTEM_DERIVED, 1); let e = env(id, id); assert_eq!( well_formed(&e), Err(WellFormednessError::SystemDerivedReplica) ); } #[test] fn well_formed_rejects_negative_physical_time() { let mut e = env( OperationId::new(ReplicaId(1), 5), OperationId::new(ReplicaId(1), 5), ); e.stamp.hlc.physical_time = WallClockTime(-1); assert_eq!( well_formed(&e), Err(WellFormednessError::NegativePhysicalTime) ); } #[test] fn peek_operation_id_reads_the_leading_canonical_bytes() { let id = OperationId::new(ReplicaId(0x0102_0304_0506_0708), 0x1122_3344_5566_7788); let e = env(id, id); let bytes = e.to_canonical_bytes(); // The canonical envelope encoding truly *leads* with the id's 16 // canonical bytes — the invariant `peek_operation_id` (and the bundle's // operation index built on it) depends on. assert_eq!(&bytes[..16], &id.canonical_bytes()); assert_eq!(peek_operation_id(&bytes), Some(id)); // A short buffer has no id to peek. assert_eq!(peek_operation_id(&bytes[..15]), None); assert_eq!(peek_operation_id(&[]), None); } #[test] fn envelope_hash_changes_with_payload_bytes() { let mut a = env( OperationId::new(ReplicaId(1), 5), OperationId::new(ReplicaId(1), 5), ); let ha = a.envelope_hash(); a.payload = OperationPayload::Primitive(OperationKind::RespellPitch(RespellPitchOp { pitch: PitchId::new(ReplicaId(1), 1), spelling: crate::valuegen::spelling(9), // different spelling })); assert_ne!(ha, a.envelope_hash()); } }