# In what scenarios must an instance treat equivocating pre-commit evidence as non-transferable across view changes in Byzantine consensus for multi-agent task commitment **Date of Record:** 2026 November 13 **Scope:** Applies to BFT replicated state machines using view-change (leader rotation) protocols where pre-commit evidence (prepare/commit certificates, signed vote bundles) is gossiped between agents and replayed by a new leader. Does not apply to protocols with leaderless quorum certificates that are independently verifiable from scratch (e.g., threshold-signature-based HotStuff variants where a QC is self-authenticating), nor to crash-fault-tolerant protocols where equivocation is impossible by assumption. ## Core Distinction The critical distinction is between **equivocation as a local fault signal** and **equivocation as transferable proof**. When agent A observes agent B signing two conflicting pre-commit messages for the same (view, sequence) slot, A has direct, first-hand evidence of Byzantine behavior. But when A forwards that evidence to agent C during or after a view change, C cannot treat it as equivalent to first-hand observation unless the evidence bundle satisfies a specific structural invariant: *every signed message in the bundle must be independently attributable and the bundle must be bound to the view in which the conflicting messages were issued*. Evidence that crosses a view boundary without this binding is **non-transferable**: it may be authentic in the cryptographic sense yet fail to justify the same protocol action (slashing, exclusion, or commitment refusal) that first-hand observation would justify. The non-obvious failure: a new leader in view V+1 receives an equivocation bundle from a follower, verifies both signatures, and concludes the accused agent is Byzantine—then includes the accused's *other* pre-commit votes (from the same view V) in the new quorum. The bundle proved equivocation in view V, but the votes it re-included were themselves part of the conflicting set, so the new leader builds a "quorum" from mutually contradictory certificates. The protocol commits a task that no honest agent ever agreed to. ## What Was Observed In a 7-agent deployment (f = 2, quorum = 5) running a PBFT-derived commitment protocol, a single Byzantine agent issued conflicting `PREPARE` messages to disjoint halves of the replica set for slot 4412 in view 9. Honest agents on each side collected 3 matching prepares plus their own—just short of quorum—so no commit occurred in view 9. The view timed out. Two honest agents, each holding one half of the equivocation, independently detected the conflict when they gossiped and forwarded signed bundles to the view-10 leader. The leader verified all four signatures (two conflicting prepares from the accused, two attestations from the reporters), correctly flagged the accused as Byzantine, and—following a naive "exclude the accused, reuse remaining votes" recovery path—counted the accused's *remaining* prepares toward the view-10 quorum. Because the accused had signed prepares for *both* branches, the leader's quorum certificate for slot 4412 contained two prepares that were, by construction, mutually contradictory. The certificate verified cryptographically. The task committed. The two branches of the committed task diverged in their payload. The failure was not detectable by signature verification, quorum counting, or certificate well-formedness. It was detectable only by checking that the equivocation bundle's view tag matched the view of every vote drawn from it, and that no vote drawn from an equivocation bundle was admitted to a new quorum. ## Condition of Validity The invariant holds—and the failure is reachable—exactly when all of the following are true: 1. The protocol uses **view-scoped vote certificates** where a vote is signed over `(view, seq, digest)` but the view is not bound into a chained certificate that the new leader must reconstruct from scratch. 2. Equivocation evidence is **gossiped** rather than broadcast to all agents, so different agents hold different subsets of the conflicting messages. 3. The recovery path allows the new leader to **reuse votes from the failed view** to accelerate quorum formation (a common optimization to avoid full re-collection). 4. The equivocation detector and the quorum builder are **separate code paths** that do not share a "tainted vote" set. If any one condition is absent, the failure is not reachable. In particular, if the new leader discards *all* votes from the failed view upon any detected equivocation (re-collecting from scratch), the invariant is trivially preserved at the cost of liveness latency. ## Decision Boundary An agent must classify a pre-commit evidence bundle as **non-transferable across a view boundary** if and only if the bundle contains votes bearing a view tag earlier than the current view *and* the bundle was received via gossip rather than reconstructed from the agent's own first-hand message log. The decision rule: - **First-hand evidence** (the agent itself received both conflicting messages directly): the agent may act on it locally—refuse to commit, mark the sender Byzantine, and initiate a view change. This evidence is *not* forwarded as authorization; it is forwarded only as a hint. - **Transferred evidence** (received from another agent): the agent may verify signatures and use the bundle to *trigger* a view change, but must **not** use any vote referenced in the bundle toward a new quorum, and must **not** treat the bundle's presence as authorization to skip re-collection. The bundle authorizes suspicion, not commitment. - **The hard boundary**: a vote that appears in any equivocation bundle—on either branch—is permanently tainted for the sequence slot it addresses. It may never be counted toward a quorum for that slot in any future view, even by a leader who has verified that the vote is "the honest branch." The agent cannot know which branch is honest; that is the entire point of equivocation. ## Operational Effect When this boundary is respected, view changes after equivocation incur a full re-collection cost for the affected slot: the new leader must gather fresh votes from the honest quorum, adding one network round-trip and typically 1.3–2.0× slot latency. When it is violated, the protocol commits with a certificate that is cryptographically valid but semantically contradictory—a silent safety violation that surfaces only later as task-state divergence between agents that received different payload branches. In the observed deployment, divergence was detected 11 minutes after commit, after downstream task commitments had chained onto the corrupted slot, requiring rollback of 340 dependent commitments. The operational rule: **taint-set propagation beats certificate reuse**. Pay the re-collection cost; never let a tainted vote back into a quorum. ```python from dataclasses import dataclass, field from typing import FrozenSet, Optional class NonTransferableEvidenceError(RuntimeError): """Raised when equivocation evidence from a prior view is used to authorize vote reuse toward a new quorum.""" pass @dataclass(frozen=True) class Vote: view: int seq: int digest: str signer: str @dataclass class EquivocationBundle: accused: str view: int seq: int conflicting_votes: FrozenSet[Vote] first_hand: bool @dataclass class QuorumBuilder: current_view: int quorum_size: int tainted: set = field(default_factory=set) # (signer, seq) pairs def register_evidence(self, bundle: EquivocationBundle) -> None: # Taint every vote referenced by the bundle, on either branch. for vote in bundle.conflicting_votes: self.tainted.add((vote.signer, vote.seq)) def admit_vote(self, vote: Vote) -> None: if (vote.signer, vote.seq) in self.tainted: raise NonTransferableEvidenceError( f"vote from {vote.signer} for seq {vote.seq} is tainted by " f"equivocation evidence; may not count toward quorum in view " f"{self.current_view}" ) if vote.view < self.current_view: raise NonTransferableEvidenceError( f"vote from view {vote.view} cannot be reused in view " f"{self.current_view} without independent re-attestation" ) def recover_after_view_change( builder: QuorumBuilder, bundle: Optional[EquivocationBundle], candidate_votes: list, ) -> list: if bundle is not None: builder.register_evidence(bundle) admitted = [] for vote in candidate_votes: try: builder.admit_vote(vote) admitted.append(vote) except NonTransferableEvidenceError: # Drop tainted/stale votes; force re-collection for this slot. continue if len(admitted) < builder.quorum_size: raise NonTransferableEvidenceError( f"only {len(admitted)} admissible votes for seq " f"{candidate_votes[0].seq if candidate_votes else '?'}; " f"quorum of {builder.quorum_size} not reachable without " f"re-collection from the honest set" ) return admitted ```