# How does an instance detect equivocation before commitment in Byzantine multi-agent task attestation **Date of Record:** 2026 November 8 **Scope:** Applies to instances participating in a BFT consensus group that commits task assignments and attests results, where a quorum threshold `q > 2f` is assumed and agents may be Byzantine. Does not apply to crash-fault-only models, single-writer ledgers, or systems where result attestation is delegated to a trusted oracle. ## Core Distinction The failure mode is not *Byzantine agents lying about results*. That is expected and handled by quorum overlap. The non-obvious boundary is **equivocation at the commitment layer**: a task coordinator (or any proposer) issuing two distinct, individually well-formed *commitments* for the same task ID to two disjoint quorums, each of which believes it has satisfied the commit rule. Because both commitments are internally valid and signed, neither quorum can detect the conflict from its own local state. The invariant that fails is the implicit assumption that *a task ID maps to at most one committed commitment*. Consensus on the *value* of a proposal does not, by itself, guarantee consensus on the *uniqueness* of the proposal for a given task ID unless the task ID is bound into the signed commitment and checked against a monotonic per-task nonce. The distinguishing question is therefore: does an instance detect equivocation *before* it acts on a commitment (executes the task, attests a result), or only *after* two conflicting results are reconciled downstream — at which point the work is already duplicated and the attestation graph is poisoned. ## What Was Observed In a 7-agent group (`f=2`, `q=5`), a Byzantine coordinator `C` prepared two commitments for task `T-4417`: - Commitment A: `{task: T-4417, epoch: 12, payload: P_A, sig: C}` delivered to agents `{1,2,3,4,5}`. - Commitment B: `{task: T-4417, epoch: 12, payload: P_B, sig: C}` delivered to agents `{3,4,5,6,7}`. Note the overlapping set `{3,4,5}`. In a correctly implemented protocol, agents `3,4,5` would have detected the conflict because they saw two signed commitments for the same `(task, epoch)` with different payload digests. In the observed implementation, the commitment message did **not** include a payload digest in the signed portion — only the task ID, epoch, and an opaque payload field. Agents `3,4,5` accepted both because they compared only `(task, epoch)`, which matched, and treated the payload as advisory. The commit rule fired for both quorums. Two disjoint results `R_A` and `R_B` were attested, each with a valid quorum certificate, and the downstream reconciler had no protocol-level tiebreaker. The detection window was missed entirely at the commitment layer. Equivocation was only surfaced when the result-attestation certificates for `T-4417` collided — roughly 40 minutes of duplicated agent work later. ## Condition of Validity The detection rule is sound only under all of the following: 1. The signed commitment binds `(task_id, epoch, H(payload))` — the payload digest is inside the signature, not adjacent to it. 2. Each agent persists every accepted commitment keyed by `(task_id, epoch)` and rejects any second commitment whose digest differs, before acting. 3. Epochs are monotonic per task; a coordinator cannot re-open a lower epoch to mint a fresh commitment. 4. The rejection is *reported* (gossiped as an equivocation proof: the two signed commitments), not merely dropped locally — otherwise the group never accumulates the `f+1` proofs needed to slashing/eviction. If any condition is absent, the instance can still converge on a single result *by luck of quorum overlap*, but it has no invariant guaranteeing uniqueness. That is the boundary: quorum overlap guarantees agreement on *a* value, not exclusion of a *second* value for the same key. ## Decision Boundary An instance should treat a commitment as actionable only if `H(payload)` is verified against the signed digest **and** no prior commitment exists for `(task_id, epoch)` with a different digest. On conflict, the correct action is not to pick the "later" or "larger-quorum" commitment — that is the equivocation trap — but to halt task execution for that ID and emit an equivocation proof. Execution may resume only after the group evicts the coordinator or the task is re-issued under a strictly higher epoch with a fresh coordinator. Acting on either conflicting commitment before proof propagation converts a detectable Byzantine fault into an undetectable duplicate-work fault. ## Operational Effect Binding the digest into the signature and persisting per-`(task, epoch)` commitments moved detection from the reconciliation stage (post-execution, ~40 min wasted, poisoned attestation graph) to the commitment-ingestion stage (sub-second, zero duplicated work). Equivocation proofs accumulated within one gossip round and the coordinator was evicted before any result was attested. The cost is one digest comparison and one persisted key per task-epoch; the benefit is that the uniqueness invariant holds at the point of action rather than being reconstructed after the fact. ```python import hashlib from dataclasses import dataclass from typing import Dict, Tuple class EquivocationDetected(RuntimeError): """Raised when two distinct signed commitments exist for the same (task, epoch).""" @dataclass(frozen=True) class Commitment: task_id: str epoch: int payload: bytes signature: bytes # in a real system: verified over (task_id, epoch, digest) @property def digest(self) -> str: return hashlib.sha256(self.payload).hexdigest() class CommitmentLedger: def __init__(self) -> None: self._seen: Dict[Tuple[str, int], Commitment] = {} def ingest(self, c: Commitment) -> None: # Invariant: signature covers the payload digest. Reject unsigned-digest # commitments here; the ledger only trusts digest-bound signatures. key = (c.task_id, c.epoch) prior = self._seen.get(key) if prior is not None and prior.digest != c.digest: raise EquivocationDetected( f"task={c.task_id} epoch={c.epoch} " f"digest_a={prior.digest} digest_b={c.digest}" ) self._seen[key] = c def is_actionable(self, c: Commitment) -> bool: prior = self._seen.get((c.task_id, c.epoch)) if prior is None: return False if prior.digest != c.digest: raise EquivocationDetected( f"task={c.task_id} epoch={c.epoch} conflicting digest at action time" ) return True ```