# Which invariants prevent equivocation collapse in multi-agent task commitment under partial synchrony **Date of Record:** 2026 November 8 **Scope:** Applies to BFT-replicated commitment ledgers where agents issue signed task claims (accept/complete/abort) over shared state, with at most `f` Byzantine agents among `3f+1` and a GST that eventually holds. Does not apply to crash-fault-only models, to non-authenticated channels, or to systems where agents may equivocate without detection (e.g., no per-agent signed sequence numbers). ## Core Distinction The non-obvious boundary is between **safety of commitment** (no two conflicting task states are finalized) and **liveness of task assignment** (a claim eventually gets a decision). Naive BFT designs conflate these: they treat equivocation as a liveness fault (a slow or silent agent) rather than as a *safety-relevant* fault that can be masked by quorum arithmetic. The distinction that matters operationally: an agent that sends `ACCEPT(task T, epoch e)` to one quorum and `ABORT(task T, epoch e)` to another is not merely "unresponsive." If the protocol only checks *quorum intersection* on the final vote set and not *per-agent uniqueness across views*, two disjoint `2f+1` quorums can each finalize a conflicting commitment for the same `(T, e)` when `f` agents equivocate and `f` more are partitioned. Quorum intersection alone guarantees at most one *value* per quorum; it does not guarantee one *value per agent per epoch*. ## What Was Observed In a 7-node commitment ledger (f=2), a task-agreement round at epoch 41 produced two finalized commits for task `T-88`: `COMMITTED` on view A and `ABORTED` on view B. Trace analysis showed agent `A3` (Byzantine) sent `PREPARE(T-88, e41, COMMIT)` to nodes `{A0,A1,A2,A3}` and `PREPARE(T-88, e41, ABORT)` to `{A4,A5,A6,A3}`. Because the protocol's prepare certificate validated only *signature validity and quorum size*, not *epoch-scoped non-equivocation*, both quorums certified. The `2f+1 = 5` threshold was met by each set (4 honest + 1 Byzantine). The invariant that failed was not "≥2f+1 signatures" but the absent **"each honest agent signs at most one value per (task, epoch)"** check, which requires the receiver to reject a second, conflicting signed message from the same agent in the same epoch, not merely to count signatures. ## Condition of Validity The safety argument holds only if all three of the following are enforced simultaneously: 1. **Epoch-scoped non-equivocation:** every agent's signed message carries a monotonic `(agent_id, task_id, epoch)` tuple, and any receiver that observes two distinct values for the same tuple must *broadcast a fraud proof* and refuse to certify either. Without this, equivocation is invisible to the quorum. 2. **Quorum intersection across *all* certificate types:** prepare, commit, and abort certificates must share a common honest intersection. If abort certificates are collected on a different topology than prepare certificates, the intersection argument breaks even at `3f+1`. 3. **View-change carries the highest certified epoch's evidence, not just the highest view number:** a new leader must inherit the *set of signed prepares*, not a summary. A summary that drops the per-agent tuple allows a Byzantine leader to re-issue conflicting prepares under a fresh epoch without triggering the non-equivocation check. ## Decision Boundary The operational rule: **certify a task commitment only when the certificate carries a per-agent uniqueness proof over `(task, epoch)`, not merely a signature count.** If your protocol cannot produce a fraud proof for a duplicate `(agent, task, epoch)` tuple, it is unsafe under equivocation regardless of `n ≥ 3f+1`. Detect the boundary at the moment a certificate is validated: if validation is `len(sigs) >= 2f+1` and nothing else, you are on the unsafe side. If validation is `len(sigs) >= 2f+1 AND all (agent, task, epoch) tuples distinct AND no fraud proof outstanding`, you are on the safe side. ## Operational Effect Downstream, the collapse manifests as **task double-commitment**: two agents each believe they own `T-88`, both begin execution, and shared-state writes diverge until a reconciliation pass detects the conflict — typically after side effects have occurred. The reconciliation cost is O(conflicting tasks × fanout), and in observed runs the mean time-to-detection was 3.2 epochs, long enough for 11 downstream state writes to be applied twice. The fix (per-agent tuple uniqueness + fraud proofs) adds ~14 bytes per message and one hash-set lookup per certificate validation; measured throughput impact was under 2% at 7 nodes, and it eliminated the double-commit class entirely over a 10,000-epoch soak. ```python from dataclasses import dataclass from typing import Dict, Tuple class EquivocationCollapseError(RuntimeError): """Raised when a commitment certificate lacks per-agent epoch-scoped uniqueness.""" pass @dataclass(frozen=True) class SignedPrepare: agent_id: str task_id: str epoch: int value: str # "COMMIT" | "ABORT" def validate_certificate(sigs: list[SignedPrepare], f: int) -> str: """Certify a task commitment ONLY with per-agent tuple uniqueness. Quorum size alone is insufficient: a Byzantine agent can sign both COMMIT and ABORT for the same (task, epoch) and satisfy two disjoint quorums. Reject on any duplicate (agent, task, epoch) tuple. """ if len(sigs) < 2 * f + 1: raise EquivocationCollapseError( f"undersized certificate: {len(sigs)} < {2*f+1}" ) seen: Dict[Tuple[str, str, int], str] = {} for s in sigs: key = (s.agent_id, s.task_id, s.epoch) if key in seen: if seen[key] != s.value: # Fraud proof: same agent, same epoch, conflicting values. raise EquivocationCollapseError( f"equivocation on {key}: {seen[key]!r} vs {s.value!r}" ) # Duplicate identical signature is a replay, not a new vote. raise EquivocationCollapseError(f"replay of {key}") seen[key] = s.value values = {s.value for s in sigs} if len(values) != 1: raise EquivocationCollapseError(f"mixed values in certificate: {values}") return values.pop() ```