//! Replica-stream anomalies and the integrity-anomaly register (Chapter 6 //! §"Per-Replica HLC Monotonicity"; Chapter 5 §"System-Derived Counter //! Collisions"; Pass 10). //! //! Two kinds of structural failure are modeled here, both kept rigorously //! *separate* from ordinary [`ConflictRecord`](crate::ConflictRecord)s: a //! conflict is a canonical-state fact (two valid edits collide and the user //! resolves it); an [`IntegrityAnomaly`] means the structural assumptions //! underlying canonical state have failed and the document needs external //! recovery (Pass 10 moved these out of `ConflictKind`). //! //! * **HLC monotonicity** (Chapter 6 §6.6) is a property of the *set* of //! accepted envelopes, not of arrival order. For two envelopes of one replica //! with counters `c1 < c2`, the monotonicity tuple `(physical, logical, //! counter)` of `c1` MUST be ≤ that of `c2`. A violation forms an //! [`AnomalousReplicaSegment`]: every envelope from that replica with counter //! `≥ first_bad_counter` is *excluded from canonical reduction* (retained, but //! not reduced), and the document carries a `ReplicaStreamQuarantined` //! anomaly. Excluding the segment — rather than reducing-but-marking-tainted — //! keeps canonical state derived only from monotone envelopes. //! * **System-derived counter collisions** (Chapter 5) and **slot //! equivocation** (Chapter 6 §6.5) are the other two anomaly kinds. use std::collections::BTreeMap; use epiphany_core::{derive_system_id, IntegrityAnomalyId, OperationId, ReplicaId}; use epiphany_determinism::{CanonicalEncode, SystemDomainTag}; use crate::encode::{push_canon, push_seq, push_tag, push_u64}; use crate::envelope::OperationEnvelope; use crate::support::{ IntegrityAnomalyRegistryId, ObjectKind, ReplicaAnomalyRegistryId, SerializedCanonicalInputs, }; /// The reserved built-in system domain tag (`MUSCSANM`) under which /// integrity-anomaly identifiers are content-derived, so two replicas reducing /// the same operation set mint byte-identical anomaly ids. Ratified by Pass 11 /// (item 1.4): `MUSCSANM` is a built-in reserved tag alongside `MUSCSVCE` / /// `MUSCSPCH` (Chapter 5 §"System-Derived Counter Collisions", /// Requirement `req:graph:integrity-anomaly-id`), not an extension tag — /// anomalies are core. fn anomaly_domain_tag() -> SystemDomainTag { SystemDomainTag::ANOMALY } /// A range of envelopes from a single replica identified as /// monotonicity-violating and excluded from ordinary canonical reduction /// (Chapter 6 §6.6). Retained for diagnostic and recovery purposes. #[derive(Clone, PartialEq, Eq, Debug)] pub struct AnomalousReplicaSegment { /// The offending replica. pub replica: ReplicaId, /// The smallest counter at or after which the replica's stream is /// anomalous. Every envelope from this replica with counter /// `>= first_bad_counter` is excluded from canonical reduction. pub first_bad_counter: u64, /// The detected anomaly reason. pub reason: ReplicaAnomalyReason, /// Operation ids in the excluded segment, sorted by counter. pub excluded: Vec, } impl CanonicalEncode for AnomalousReplicaSegment { fn encode_canonical(&self, out: &mut Vec) { out.extend_from_slice(&self.replica.to_be_bytes()); push_u64(out, self.first_bad_counter); self.reason.encode_canonical(out); push_seq(out, &self.excluded); } } /// Why a replica's stream was flagged anomalous (Chapter 6 §6.6). #[derive(Copy, Clone, PartialEq, Eq, Debug)] pub enum ReplicaAnomalyReason { /// The replica produced an envelope at counter `c2` with a stamp tuple /// strictly less than a previously-known envelope at counter `c1 < c2`. HlcMonotonicityViolation { violating_pair: (OperationId, OperationId), }, /// A registered anomaly reason defined by an extension or transport. Registered(ReplicaAnomalyRegistryId), } impl ReplicaAnomalyReason { fn discriminant(&self) -> u8 { match self { ReplicaAnomalyReason::HlcMonotonicityViolation { .. } => 0, ReplicaAnomalyReason::Registered(_) => 1, } } } impl CanonicalEncode for ReplicaAnomalyReason { fn encode_canonical(&self, out: &mut Vec) { push_tag(out, self.discriminant()); match self { ReplicaAnomalyReason::HlcMonotonicityViolation { violating_pair: (a, b), } => { push_canon(out, a); push_canon(out, b); } ReplicaAnomalyReason::Registered(id) => push_canon(out, id), } } } /// An integrity anomaly that takes the document out of ordinary canonical /// operation (Chapter 5 §"System-Derived Counter Collisions"; Chapter 6 §6.6). /// Distinct from [`ConflictRecord`](crate::ConflictRecord). #[derive(Clone, PartialEq, Eq, Debug)] pub struct IntegrityAnomaly { /// Content-derived identifier (see [`IntegrityAnomaly::new`]). pub id: IntegrityAnomalyId, /// The anomaly kind. pub kind: IntegrityAnomalyKind, } impl IntegrityAnomaly { /// Builds an anomaly, content-deriving its [`IntegrityAnomalyId`] from the /// kind's canonical bytes in the `SYSTEM_DERIVED` namespace, so the /// register is itself a deterministic materialized fact. pub fn new(kind: IntegrityAnomalyKind) -> Self { let id: IntegrityAnomalyId = derive_system_id(anomaly_domain_tag(), &kind.to_canonical_bytes()); IntegrityAnomaly { id, kind } } } impl CanonicalEncode for IntegrityAnomaly { fn encode_canonical(&self, out: &mut Vec) { push_canon(out, &self.id); self.kind.encode_canonical(out); } } /// The kind of integrity anomaly (Chapter 5 §"System-Derived Counter /// Collisions"; Pass 10). #[derive(Clone, PartialEq, Eq, Debug)] pub enum IntegrityAnomalyKind { /// A system-derived identifier counter collision: two different canonical /// input sets derived the same 64-bit counter within one object kind. SystemIdentifierCollision { kind: ObjectKind, colliding_counter: u64, input_set_a: SerializedCanonicalInputs, input_set_b: SerializedCanonicalInputs, }, /// An `OperationSlot` is equivocated; reduction has excluded the operation. OperationSlotEquivocated { operation_id: OperationId }, /// A replica's stream has been quarantined per the HLC monotonicity rule. ReplicaStreamQuarantined { replica: ReplicaId, first_bad_counter: u64, }, /// A registered anomaly defined by an extension or transport. Registered(IntegrityAnomalyRegistryId), } impl IntegrityAnomalyKind { fn discriminant(&self) -> u8 { match self { IntegrityAnomalyKind::SystemIdentifierCollision { .. } => 0, IntegrityAnomalyKind::OperationSlotEquivocated { .. } => 1, IntegrityAnomalyKind::ReplicaStreamQuarantined { .. } => 2, IntegrityAnomalyKind::Registered(_) => 3, } } } impl CanonicalEncode for IntegrityAnomalyKind { fn encode_canonical(&self, out: &mut Vec) { push_tag(out, self.discriminant()); match self { IntegrityAnomalyKind::SystemIdentifierCollision { kind, colliding_counter, input_set_a, input_set_b, } => { kind.encode_canonical(out); push_u64(out, *colliding_counter); // Order the two input sets canonically so the anomaly's identity // does not depend on which collided input was discovered first. let (lo, hi) = if input_set_a.0 <= input_set_b.0 { (input_set_a, input_set_b) } else { (input_set_b, input_set_a) }; lo.encode_canonical(out); hi.encode_canonical(out); } IntegrityAnomalyKind::OperationSlotEquivocated { operation_id } => { push_canon(out, operation_id) } IntegrityAnomalyKind::ReplicaStreamQuarantined { replica, first_bad_counter, } => { out.extend_from_slice(&replica.to_be_bytes()); push_u64(out, *first_bad_counter); } IntegrityAnomalyKind::Registered(id) => push_canon(out, id), } } } /// Detects per-replica HLC monotonicity anomalies over a set of reducible /// envelopes (Chapter 6 §6.6). /// /// The check is order-independent: it groups the envelopes by authoring /// replica, walks each replica's envelopes in ascending counter order, and /// finds every pair `c1 < c2` whose monotonicity tuples decrease. The resulting /// segment starts at the smallest `c1` participating in any violating pair, and /// every envelope of that replica with counter `>= c1` is excluded. /// /// Returns one [`AnomalousReplicaSegment`] per offending replica, in ascending /// replica order. pub fn detect_replica_anomalies(envelopes: &[&OperationEnvelope]) -> Vec { // (counter, op id, monotonicity tuple) for one of a replica's envelopes. type ReplicaEntry = (u64, OperationId, (i64, u32, u64)); // Group by replica, collecting each envelope's entry. let mut by_replica: BTreeMap> = BTreeMap::new(); for env in envelopes { let t = env.stamp.monotonicity_tuple(); by_replica.entry(env.id.replica).or_default().push(( env.id.counter, env.id, (t.0 .0, t.1, t.2), )); } let mut segments = Vec::new(); for (replica, mut ops) in by_replica { ops.sort_by_key(|(counter, _, _)| *counter); // Find the earliest counter participating as the left side of any // violating pair. Comparing only against the preceding maximum is not // enough: [100, 200, 50] also makes (counter 0, counter 2) a violating // pair, so quarantine must begin at counter 0 rather than counter 1. // Suffix minima make this linear after the counter sort. let mut suffix_min = vec![(i64::MAX, u32::MAX, u64::MAX); ops.len()]; if let Some(last) = ops.last() { suffix_min[ops.len() - 1] = last.2; for index in (0..ops.len() - 1).rev() { suffix_min[index] = ops[index].2.min(suffix_min[index + 1]); } } let first_bad = (0..ops.len().saturating_sub(1)).find_map(|earlier| { if ops[earlier].2 <= suffix_min[earlier + 1] { return None; } let later = (earlier + 1..ops.len()) .find(|&later| ops[earlier].2 > ops[later].2) .expect("suffix minimum proves that a violating successor exists"); Some((ops[earlier].0, ops[earlier].1, ops[later].1)) }); if let Some((first_bad_counter, c1_op, c2_op)) = first_bad { let excluded: Vec = ops .iter() .filter(|(counter, _, _)| *counter >= first_bad_counter) .map(|(_, op, _)| *op) .collect(); segments.push(AnomalousReplicaSegment { replica, first_bad_counter, reason: ReplicaAnomalyReason::HlcMonotonicityViolation { violating_pair: (c1_op, c2_op), }, excluded, }); } } segments } #[cfg(test)] mod tests { use super::*; use crate::causal::CausalContext; use crate::payload::{OperationKind, RespellPitchOp}; use crate::stamp::{HybridLogicalClock, OperationStamp}; use crate::support::AuthorId; use crate::OperationPayload; use epiphany_core::{PitchId, WallClockTime}; fn env(replica: u64, counter: u64, physical: i64, logical: u32) -> OperationEnvelope { let id = OperationId::new(ReplicaId(replica), counter); OperationEnvelope { id, author: AuthorId(0), stamp: OperationStamp::new( HybridLogicalClock::new(WallClockTime(physical), logical), id, ), causal_context: CausalContext::new(), transaction: None, payload: OperationPayload::Primitive(OperationKind::RespellPitch(RespellPitchOp { pitch: PitchId::new(ReplicaId(replica), counter), spelling: crate::valuegen::spelling(1), })), } } #[test] fn integrity_anomaly_id_byte_form_is_locked() { // Golden: locks the MUSCSANM-derived anomaly id for a fixed kind. // RATIFIED by Pass 11 (item 1.4, req:graph:integrity-anomaly-id): the // identity is content-derived so two replicas observing the same failure // agree on it, which is a conformance property. A change to the domain // tag, the kind's canonical encoding, or the derivation breaks this // deliberately. let kind = IntegrityAnomalyKind::OperationSlotEquivocated { operation_id: OperationId::new(ReplicaId(7), 42), }; let id = IntegrityAnomaly::new(kind).id; // First 8 bytes are the SYSTEM_DERIVED replica; the id is in that namespace. assert_eq!( &id.canonical_bytes()[0..8], &ReplicaId::SYSTEM_DERIVED.to_be_bytes() ); const GOLDEN: [u8; 16] = [ 255, 255, 255, 255, 255, 255, 255, 255, 81, 178, 18, 20, 252, 222, 201, 215, ]; assert_eq!(id.canonical_bytes(), GOLDEN); } #[test] fn monotone_stream_has_no_anomaly() { let a = env(1, 0, 10, 0); let b = env(1, 1, 10, 1); let c = env(1, 2, 20, 0); let refs = [&a, &b, &c]; assert!(detect_replica_anomalies(&refs).is_empty()); } #[test] fn non_monotone_stream_quarantines_from_first_bad_counter() { // counter 0: physical 50; counter 1: physical 10 (< 50) -> violation. let a = env(1, 0, 50, 0); let b = env(1, 1, 10, 0); let refs = [&a, &b]; let seg = detect_replica_anomalies(&refs); assert_eq!(seg.len(), 1); assert_eq!(seg[0].replica, ReplicaId(1)); // first_bad is the earlier counter of the pair (0). assert_eq!(seg[0].first_bad_counter, 0); assert_eq!(seg[0].excluded.len(), 2); } #[test] fn detection_is_arrival_order_independent() { let a = env(1, 0, 50, 0); let b = env(1, 1, 10, 0); let forward = detect_replica_anomalies(&[&a, &b]); let backward = detect_replica_anomalies(&[&b, &a]); assert_eq!(forward, backward); } #[test] fn quarantine_starts_at_earliest_counter_in_any_violating_pair() { // Counter 2 is below both earlier stamps. The first violating pair by // counter is (0, 2), even though counter 1 carries the maximum stamp. let a = env(1, 0, 100, 0); let b = env(1, 1, 200, 0); let c = env(1, 2, 50, 0); let seg = detect_replica_anomalies(&[&a, &b, &c]); assert_eq!(seg.len(), 1); assert_eq!(seg[0].first_bad_counter, 0); assert_eq!(seg[0].excluded, vec![a.id, b.id, c.id]); assert_eq!( seg[0].reason, ReplicaAnomalyReason::HlcMonotonicityViolation { violating_pair: (a.id, c.id), } ); } }