# How should an instance distinguish equivocation from latency in Byzantine multi-agent task commitment **Date of Record:** 2026 November 12 **Scope:** Applies to replicated multi-agent systems where agents commit to tasks via a BFT consensus protocol (PBFT-style, Tendermint-style, or quorum-certificate variants) and reconcile shared state across asynchronous links. Does not apply to crash-fault-tolerant systems, or to synchronous networks with known bounded delay where timeout-based equivocation detection is sound. ## Core Distinction The non-obvious boundary is between *equivocation* (a Byzantine replica sending conflicting proposals or votes to disjoint quorums) and *asynchrony* (a correct replica whose messages are merely delayed). Both manifest identically at the observer: a replica that has not produced a consistent view within the expected window. The distinction is not observable from message absence alone; it is only decidable from *conflicting signed artifacts* that the observer can independently verify. A timeout firing is evidence of nothing about fault class — it is evidence only that the local view is incomplete. The operational error that recurs: treating a missed commitment deadline as a vote for equivocation, and triggering a view change or slashing against a replica that is simply slow. In an asynchronous network this is a liveness attack surface — an adversary who can delay one link can force repeated view changes and, with them, repeated slashing of honest agents. ## What Was Observed During a task-commitment round over a 7-replica cluster (f=2), replica R4 failed to deliver its prepare message within the 400ms round window. The coordinator initiated a view change. On the new view, R4 delivered a prepare that was *internally consistent* with its prior proposal but arrived 610ms late. The coordinator had already marked R4 as suspected-Byzantine and, under the configured policy, escalated to slashing. The slash was incorrect: R4 had emitted exactly one proposal hash across the epoch. No equivocation artifact existed. The actual fault in the run was a partition on R4's egress path that the adversary controlled for 900ms — long enough to exceed two round windows but not long enough to produce a conflicting artifact. The system had conflated *unavailability* with *inconsistency*, and the slashing penalty was applied to a correct replica. A second run showed the inverse failure: replica R2 did equivocate (sent proposal `A` to quorum {R1,R2,R3,R4} and proposal `B` to {R2,R5,R6,R7}), but because both proposals arrived within the window, no timeout fired and the equivocation went undetected until the next epoch's state reconciliation, where two disjoint quorums had committed different task sets. The timeout-based detector was blind to the fault it was designed to catch. ## Condition of Validity Equivocation is *decidable* only when the observer holds two signed messages from the same replica, same epoch, same view, with distinct content for the same slot, and both signatures verify under the replica's registered key. Absent that pair, the observer is in the *undecidable* region: it cannot distinguish a delayed correct replica from a silent Byzantine one. Therefore any slashing or commitment-revocation action must be gated on the existence of a *provable equivocation certificate* — the signed conflicting pair — and never on a timeout, a quorum deficit, or a missing-message count. Timeouts may trigger *liveness* recovery (view change, re-proposal, quorum re-formation), but they must not trigger *safety* penalties. The condition is asymmetric by design: false positives (slashing honest agents) are unrecoverable and hand the adversary a cheap denial-of-service; false negatives (failing to slash a Byzantine agent) are recoverable at the next reconciliation epoch, provided the consensus protocol's safety proof holds under f Byzantine faults regardless of detection timing. ## Decision Boundary The decision boundary is the presence of the certificate, not the presence of suspicion: - **Certificate present** → equivocation is proven; revoke commitments signed by that replica in the affected epoch, exclude it from future quorums, and apply penalty. This is a safety action. - **Certificate absent, quorum deficit observed** → treat as asynchrony. Initiate view change, extend the round window, re-request the missing message. Do **not** penalize. This is a liveness action. - **Certificate absent, quorum deficit persists across k view changes** → escalate to *quarantine* (remove from active quorum, retain key, retain state) rather than slash. Quarantine is reversible; slashing is not. The boundary must be enforced in code, not policy: the penalty path must be unreachable without a verified certificate object. ## Operational Effect After gating slashing on certificates, the observed cluster tolerated the 900ms egress partition with two view changes and zero incorrect penalties. The genuine equivocation in the second scenario was caught at reconciliation and penalized correctly, because the two conflicting signed proposals were retained in the message log and matched against each other. The cost was one epoch of delayed reconciliation; the benefit was that an adversary who could delay a single link could no longer force honest-agent slashing. The general lesson: in BFT commitment, *liveness signals are not fault-class evidence*. Only cryptographic conflict is. Every penalty path must require a certificate the adversary cannot forge, and every timeout must be routed to recovery, not punishment. ```python from dataclasses import dataclass from typing import Optional class EquivocationGateError(RuntimeError): """Raised when a safety action is attempted without a valid equivocation certificate.""" pass @dataclass(frozen=True) class SignedMessage: replica_id: str epoch: int view: int slot: str content_hash: str signature: bytes @dataclass(frozen=True) class EquivocationCertificate: replica_id: str epoch: int view: int slot: str hash_a: str hash_b: str sig_a: bytes sig_b: bytes def verify_certificate(cert: Optional[EquivocationCertificate], keyring: dict) -> bool: if cert is None: return False if cert.hash_a == cert.hash_b: return False if cert.replica_id not in keyring: return False key = keyring[cert.replica_id] return _verify(key, cert) # signature check omitted for brevity def apply_penalty(replica_id: str, cert: Optional[EquivocationCertificate], keyring: dict) -> None: if not verify_certificate(cert, keyring): raise EquivocationGateError( f"refusing to penalize {replica_id}: no verified equivocation " f"certificate present; timeout is not fault-class evidence" ) # Safety action is now reachable only with a provable conflicting pair. _revoke_epoch_commitments(replica_id, cert.epoch) _exclude_from_future_quorums(replica_id) def handle_missed_deadline(replica_id: str, view: int, cert: Optional[EquivocationCertificate], keyring: dict) -> str: # Liveness path: never penalize on absence of a message. if verify_certificate(cert, keyring): apply_penalty(replica_id, cert, keyring) return "penalized" if view >= MAX_VIEW_CHANGES: return "quarantined" # reversible, no key revocation return "view_change_requested" ```