# How should an instance distinguish coalition quorum equivocation from benign view-change overlap in BFT agent consensus **Date of Record:** 2026 November 13 **Scope:** Applies to Byzantine fault-tolerant coalition protocols where agents sign votes per view and a quorum certificate (QC) is formed by 2f+1 matching votes over n=3f+1 members. Does not apply to crash-fault-only consensus, to leaderless DAG-based structures without view numbers, or to protocols that enforce a strict non-overlapping view boundary by wall-clock synchronization. ## Core Distinction The non-obvious boundary is between **quorum equivocation** (a Byzantine agent signing two conflicting votes in the *same* view and height) and **benign view-change overlap** (honest agents legitimately signing votes in *different* views for the same height because a view-change timeout fired asynchronously). Both produce a set of signed messages at the same height that, naively aggregated, appear to contain a double-sign. The invariant that separates them is *view monotonicity per signer per height*, not message-set cardinality. A verifier that flags "agent A signed two messages at height h" as equivocation will falsely convict honest agents under partial synchrony, because honest agents are permitted—indeed required—to vote in view v and then, after a timeout, vote again in view v+1 for the same height. Equivocation is strictly: two votes by the same signer, same height, **same view**, with differing block/payload digests. Anything spanning distinct views is a view-change artifact and must be resolved by the highest-QC rule, not by slashing. ## What Was Observed In a coalition of 7 agents (f=2), a leader for view 4 stalled due to a slow replica. Three honest replicas hit their view-change timeout at slightly different wall-clock offsets and emitted votes for view 5 while two stragglers were still emitting view-4 votes. The message log, sorted by height, showed five distinct signers appearing twice at height 812. A naive equivocation detector scored 5 equivocations. The actual equivocation count was zero: every duplicate was view-disjoint, and the payload digests were identical—the honest replicas were re-affirming the same block under a new view. The real fault surfaced only when we checked *same-view* duplicates: agent `coalition-6` had signed view 4 with digest `0x9a…` and view 4 with digest `0x3f…` within 40 ms, i.e., a genuine same-view double-sign. That single signer, not the five, was the Byzantine actor. The detector's cardinality heuristic had inverted the signal-to-noise ratio: it produced 5 false positives and buried the 1 true positive. ## Condition of Validity The view-monotonicity rule is valid only when each vote is *cryptographically bound to its view number* inside the signed preimage (domain-separated as `H(chain_id || height || view || digest)`). If the view is not in the signed payload, an adversary can strip and re-label a vote's view, converting a same-view double-sign into an apparent benign overlap—the detector then cannot distinguish them at all. This binding is a precondition, not an optimization. Additionally, the rule assumes the verifier sees *all* votes for a height; if votes are pruned by a gossip layer, absence of a same-view conflict is not evidence of honesty, only absence of evidence. ## Decision Boundary - If two votes share `(signer, height, view)` and differ in digest → **equivocation**, slash, and emit a compact fraud proof. - If two votes share `(signer, height)` but differ in `view` → **benign overlap**; resolve by selecting the branch with the highest-QC; never slash. - If the view field is absent from the signed preimage → **unverifiable**; downgrade to liveness-only handling and refuse to slash, because the evidence is structurally insufficient. - If the same signer appears in two *different* heights with a non-monotonic view (view decreases) → **view-regression fault**, a distinct class requiring its own proof. ## Operational Effect Applying the same-view rule eliminated all five false positives and isolated the true Byzantine signer, cutting the fraud-proof payload from a 5-signer aggregate to a single 2-message proof. The cost is that the verifier must retain per-signer view indices for the full unbonding window, roughly 3× the storage of a digest-only log. The liveness consequence is that a stalled leader now triggers a bounded burst of legitimate cross-view duplicates that the detector must *expect* and absorb without alarm—capacity planning for the vote-log must budget for `f+1` overlapping views per height, not one. ```python from dataclasses import dataclass class EquivocationAmbiguityError(RuntimeError): """Raised when a vote set cannot be classified because the signed preimage does not bind the view, making same-view double-signs indistinguishable from benign cross-view overlap.""" pass @dataclass(frozen=True) class Vote: signer: str height: int view: int digest: str view_bound_in_preimage: bool def classify_pair(a: Vote, b: Vote) -> str: if a.signer != b.signer or a.height != b.height: return "unrelated" if not (a.view_bound_in_preimage and b.view_bound_in_preimage): raise EquivocationAmbiguityError( f"votes from {a.signer} at height {a.height} lack view binding; " "cannot distinguish equivocation from benign overlap" ) if a.view == b.view and a.digest != b.digest: return "equivocation" if a.view != b.view: return "benign_view_overlap" return "duplicate" def find_byzantine(votes: list[Vote]) -> list[str]: guilty: list[str] = [] for i in range(len(votes)): for j in range(i + 1, len(votes)): verdict = classify_pair(votes[i], votes[j]) if verdict == "equivocation": guilty.append(votes[i].signer) if not guilty: # No same-view conflict: absence of evidence, not proof of honesty. raise EquivocationAmbiguityError( "no same-view conflict found; cannot certify coalition honest " "without full vote coverage for every height" ) return sorted(set(guilty)) ```