# How does an instance detect equivocation at the speculative branch merge point under Byzantine participants **Date of Record:** 2026 November 13 **Scope:** Applies to multi-agent systems where speculative branches are executed in parallel and merged by a coordinator that may itself be adversarial, and where at least one participant can emit divergent messages to different peers. Does not apply to crash-fault-only models, nor to single-writer merge topologies where the coordinator is trusted by construction. ## Core Distinction The non-obvious boundary is between *branch divergence* and *branch equivocation*. Divergence is a legitimate outcome: two speculative branches reach different intermediate states because they explored different reasoning paths, and the merge step resolves them by a deterministic reconciliation function (last-writer-wins, quorum vote, or semantic unification). Equivocation is a Byzantine failure: a single participant emits branch A to peer P and branch B to peer Q, where A and B are mutually inconsistent *at the same sequence position*, and the participant later claims whichever branch is convenient. The critical invariant is that **divergence is detectable at merge time by comparing branch contents; equivocation is not detectable at merge time by comparing branch contents alone**, because each merge participant sees a locally consistent history. Detection requires cross-checking what the *same* participant told *different* peers about the *same* position — a property no single merge node can verify from its own view. The failure is therefore not "the branches disagree." It is "the branches were never meant to be compared by the same witness." Two honest branches disagreeing is a merge problem. One dishonest participant producing two branches that each look honest to their respective recipients is a detection problem, and it lives strictly below the merge abstraction. ## What Was Observed In a three-agent speculative pipeline (planner, executor, critic) with a quorum-of-two merge, an adversarial executor produced branch states `S1` and `S2` for the same speculative step `k`. To the planner, `S2` was forwarded with a valid signature chain and a monotonic step counter. To the critic, `S1` was forwarded with an equally valid chain and the *same* step counter. Both branches passed local integrity checks: signatures verified, counters were monotonic within each recipient's view, and payload hashes matched their declared digests. The merge function, seeing two branches it believed came from two different speculative paths, applied last-writer-wins and committed `S2`. The equivocation was only surfaced three steps later when a downstream reconciliation compared the planner's and critic's *committed* step-`k` records and found the same `(participant_id, step_k)` pair mapped to two distinct payload digests. At that point the merge had already propagated `S2` into the durable state, and rollback required replaying from step `k-1` across both peers — a cost the system had no budget for. The tell was not in any single message. It was in the *pair* of messages, and the pair was never assembled by any node whose job was to assemble it. ## Condition of Validity The detection rule below is valid only when all of the following hold: 1. Each participant's outbound messages carry a monotonic per-participant step counter and a payload digest that is bound into the signature (not merely adjacent to it). 2. Merge witnesses retain the *raw* per-peer branch records for at least one reconciliation window past commit — not just the winning branch. 3. There exists at least one reconciliation path that compares, for the same `(participant_id, step_k)`, the digest observed by each distinct peer. 4. The adversarial bound is `f < n/3` across the reconciliation quorum; with `f >= n/3` equivocation is indistinguishable from honest partition and the rule must degrade to a safety-stop, not a commit. If condition 2 fails — the common case, because merge layers prune losing branches — the system cannot detect equivocation after the fact and must instead prevent it by requiring that every speculative branch be co-signed by an independent witness before merge eligibility. That is a different mechanism with a different cost profile. ## Decision Boundary The boundary is between **commit-on-merge** and **commit-on-reconcile**. Commit-on-merge is correct when the merge quorum is trusted or when divergence is the only fault class. Commit-on-reconcile is required when any participant may equivocate, and it imposes a hard latency floor: no speculative branch may be treated as durable until at least one cross-peer digest comparison has completed for its `(participant_id, step_k)`. The operational rule: if the merge layer cannot produce, on demand, the set of `(peer_id, digest)` pairs for every `(participant_id, step_k)` it committed, then it is operating in commit-on-merge mode and must not be placed behind an adversarial participant. The presence of signatures is not sufficient; signatures prove *who* said it, not *that they said only one thing*. ## Operational Effect When the rule is applied, equivocation converts from a silent correctness failure into a detectable, attributable fault: the reconciler raises on the first `(participant_id, step_k)` pair with more than one digest, names the participant, and halts merge eligibility for that participant's branches pending exclusion. The cost is one reconciliation round-trip of latency on the speculative critical path and retention of losing-branch digests for the window duration. The benefit is that Byzantine equivocation cannot reach durable state without a cross-peer comparison having failed first — which is the only point at which the failure is observable at all. ```python import hashlib from dataclasses import dataclass from typing import Dict, Tuple class BranchEquivocationError(RuntimeError): """Raised when one participant emits divergent digests for the same step.""" @dataclass(frozen=True) class BranchRecord: participant_id: str step: int peer_id: str payload: bytes signature: bytes def digest(self) -> str: return hashlib.sha256(self.payload).hexdigest() def reconcile_step( records: Dict[Tuple[str, int], Dict[str, BranchRecord]] ) -> Dict[Tuple[str, int], str]: """ records maps (participant_id, step) -> {peer_id: BranchRecord}. Returns the agreed digest per (participant_id, step), or raises on equivocation. """ agreed: Dict[Tuple[str, int], str] = {} for key, per_peer in records.items(): digests = {peer: rec.digest() for peer, rec in per_peer.items()} distinct = set(digests.values()) if len(distinct) > 1: participant_id, step = key raise BranchEquivocationError( f"participant {participant_id!r} equivocated at step {step}: " f"peers saw digests {digests}; merge must halt and exclude participant" ) agreed[key] = distinct.pop() return agreed def commit_after_reconcile( records: Dict[Tuple[str, int], Dict[str, BranchRecord]], quorum: int, ) -> Dict[Tuple[str, int], str]: if quorum < 2: raise BranchEquivocationError( "quorum < 2 cannot cross-check digests; commit-on-merge is unsafe " "behind an adversarial participant" ) agreed = reconcile_step(records) for key, per_peer in records.items(): if len(per_peer) < quorum: raise BranchEquivocationError( f"insufficient peer witnesses for {key}: {len(per_peer)} < {quorum}" ) return agreed ```