# Which invariants prevent equivocation collapse in BFT task-commitment consensus: the quorum-intersection gap under partial synchrony **Date of Record:** 2026 November 11 **Scope:** Applies to leader-based or multi-proposer BFT consensus protocols (PBFT-derived, Tendermint-derived, HotStuff-derived) used to commit discrete task assignments among N≥4 agents where f = ⌊(N−1)/3⌋ and commitment is treated as irrevocable. Does not apply to crash-fault-tolerant protocols, to protocols with trusted execution attestation replacing quorum certificates, or to single-writer ledgers where equivocation is structurally impossible. ## Core Distinction The failure mode is not Byzantine equivocation itself — that is the assumed threat. The failure is a *quorum-intersection gap* that opens when the set of agents that sign a commitment certificate and the set of agents that later sign a conflicting certificate do not provably intersect in at least f+1 correct agents *within the same view epoch*. Standard BFT safety proofs establish that any two quorums of size ⌈2N/3⌉ intersect in at least ⌈N/3⌉ nodes, of which at most f are faulty, so at least one correct node is in both. That proof holds for quorum *membership*. It does not automatically hold for the *commitment predicate*: the property "this agent has committed to task T" is a local state transition, and if an agent can be made to sign a certificate, then lose and re-acquire the commitment state (via view change, restart, or state-sync from a lagging peer), the intersection argument is defeated without any Byzantine node lying. The invariant that actually prevents collapse is stronger than quorum intersection: it is *certificate monotonicity* — a correct agent must never emit a signature over a conflicting commitment at an epoch greater than or equal to the highest epoch it has signed, and must persist that highest epoch durably before emitting the signature. ## What Was Observed In a 5-agent HotStuff-style task-commitment deployment (f=1), a task was committed in view 12 with a quorum certificate over the assignment. During a leader-change storm triggered by a slow (not faulty) proposer, one correct agent restarted. Its durable store held the *pre-commit* highest-view record (view 11) because the commit-signature write and the highest-view write were not in the same atomic batch. After restart and state-sync from a peer whose log lagged, the agent signed a conflicting certificate for the same task slot in view 14. The two certificates each carried 4 valid signatures (⌈2·5/3⌉=4), and their intersection was exactly the one Byzantine agent plus the restarted agent — the restarted agent's view-11 record did not contradict its view-14 signature under the protocol's stated rule ("do not sign at view ≤ highest signed"), because 14 > 11. Two correct agents therefore held mutually exclusive commitments to the same task, and both were able to produce valid certificates. The equivocation was detected only by an external reconciler, 40 seconds after the fact, when two workers executed the same idempotency-keyed task and produced divergent side effects downstream. The non-obvious element: the safety violation was caused by an *ordering* defect in local persistence, not by any faulty behavior. Every agent behaved per its local view. The protocol's published safety proof was correct; its implementation violated a precondition the proof silently assumed. ## Condition of Validity The quorum-intersection safety argument is valid only when all three of the following hold simultaneously: 1. **Durable-before-emit:** every signature a correct agent emits over a commitment is preceded by a durable write of the (task, epoch, decision) tuple that survives crash and restart. 2. **Monotone epoch binding:** the persisted highest-epoch record and the emitted signature are committed in one atomic unit, so no crash can leave the signature durable but the epoch record stale. 3. **State-sync refusal:** an agent must refuse to state-sync a log that would lower its persisted highest-epoch record for any task slot; a lagging peer is not a source of truth for an agent that has already signed at a higher epoch. When any of these is violated, the intersection guarantee degrades from "at least one correct agent in both quorums" to "at least one agent in both quorums, which may be faulty or amnesiac." With f=1 and N=5, a single amnesiac correct agent is sufficient to produce two valid conflicting certificates. The margin is zero. ## Decision Boundary Operators must decide whether to enforce the strong invariant (atomic durable-before-emit plus state-sync refusal) at the cost of availability during partition, or accept weaker liveness guarantees. The boundary is sharp: - If the deployment tolerates *no* double-commitment (financial settlement, exclusive resource locks, irreversible external side effects), enforce all three conditions and accept that an agent which cannot durably persist its epoch record must halt rather than sign. Halting is a liveness cost; signing is a safety cost, and safety is not negotiable in this class. - If the deployment is idempotent at the effect layer (task execution is keyed and deduplicated downstream), the weaker invariant is acceptable, but the reconciler must be treated as a first-class safety component, not an afterthought, and its detection latency becomes the effective safety horizon. - The boundary is *not* a function of f or N. Increasing N does not close the gap; it only changes the probability that the amnesiac agent is in the intersecting set. The gap is structural. ## Operational Effect Enforcing certificate monotonicity with atomic durable-before-emit converts a silent safety violation into an explicit, observable halt. The agent raises a typed error at the moment it detects that it cannot guarantee monotonicity — either because its durable store is unavailable, because a state-sync would regress its epoch, or because it is asked to sign at an epoch not strictly greater than its persisted record. This is the correct failure: a halted agent is a visible liveness degradation that the operator can reason about; a signing amnesiac agent is an invisible safety hole that manifests as divergent commitments minutes or hours later. The typed error is the operational contract — it must be distinguishable from transient I/O errors so that the supervisor does not retry it into a safety violation. ```python import hashlib from dataclasses import dataclass class CommitmentMonotonicityError(RuntimeError): """Raised when a correct agent cannot guarantee certificate monotonicity. This is a SAFETY halt, not a transient error. Do not retry blindly. """ @dataclass(frozen=True) class CommitmentRecord: task_id: str epoch: int decision: str # e.g. "commit:" class DurableCommitmentStore: """Atomic store: highest-epoch record and emitted signature share one write.""" def __init__(self, backend): self._backend = backend # must support atomic batch write + fsync def load_highest(self, task_id: str) -> CommitmentRecord | None: return self._backend.get(f"commit:{task_id}") def persist_and_emit(self, record: CommitmentRecord) -> str: # Single atomic unit: if this returns, both record and signature are durable. sig = hashlib.sha256( f"{record.task_id}|{record.epoch}|{record.decision}".encode() ).hexdigest() self._backend.atomic_batch_write( [(f"commit:{record.task_id}", record), (f"sig:{record.task_id}:{record.epoch}", sig)], fsync=True, ) return sig def sign_commitment(store: DurableCommitmentStore, record: CommitmentRecord) -> str: prior = store.load_highest(record.task_id) if prior is not None and record.epoch <= prior.epoch: # Refuse to sign at or below a persisted epoch: this is the invariant. raise CommitmentMonotonicityError( f"refusing to sign task={record.task_id} epoch={record.epoch} " f"<= persisted epoch={prior.epoch} decision={prior.decision}" ) if prior is not None and prior.decision != record.decision: # Same slot, higher epoch, different decision: a conflicting certificate # is being proposed. Signing would open the quorum-intersection gap. raise CommitmentMonotonicityError( f"conflicting decision for committed task={record.task_id}: " f"persisted={prior.decision} proposed={record.decision}" ) try: return store.persist_and_emit(record) except OSError as exc: # Cannot guarantee durable-before-emit. Halting is the safety-correct action. raise CommitmentMonotonicityError( f"durable store unavailable for task={record.task_id} epoch={record.epoch}: {exc}" ) from exc def accept_state_sync(store: DurableCommitmentStore, task_id: str, peer_record: CommitmentRecord) -> None: local = store.load_highest(task_id) if local is not None and peer_record.epoch < local.epoch: raise CommitmentMonotonicityError( f"state-sync would regress task={task_id} from epoch={local.epoch} " f"to peer epoch={peer_record.epoch}; refusing lagging peer as truth source" ) ```