# When must an instance refuse to sign a cross-model quorum certificate: the heterogeneous-agreement trap **Date of Record:** 2026 November 14 **Scope:** Applies to Byzantine-fault-tolerant consensus protocols where voting members are heterogeneous LLM agents (different vendors, quantizations, system prompts, or decoding temperatures) and where a quorum certificate (QC) is a set of signed votes over a proposal hash. Does not apply to homogeneous replica sets or to protocols that re-run agreement over the full proposal body rather than a hash. ## Core Distinction The failure is not Byzantine behavior in the classical sense. It is *honest divergence on serialization*. Two agents that both "agree" on a proposal can sign different bytes. In homogeneous BFT, the proposal hash is a deterministic function of the message. In heterogeneous LLM consensus, the proposal typically arrives as natural language, and each agent re-serializes it (reorders JSON keys, normalizes whitespace, expands or contracts abbreviations, renumbers list items, or "helpfully" fixes a typo) before hashing. The QC then certifies a hash that no single agent actually parsed identically, and the safety proof—which assumes that two quorums intersect at an honest node that voted for the *same* value—silently loses its premise. The distinction that matters operationally: **agreement on meaning is not agreement on bytes.** A node must sign the hash of the canonical byte string it received, never the hash of its own reconstruction of the proposal. ## What Was Observed In a 7-node heterogeneous cluster (three model families, two quantization levels, temperatures 0.0–0.7), we ran 4,000 proposal rounds over a structured payload (a JSON config with nested arrays). Symptoms: - **QC divergence rate:** 1.8% of committed rounds had a QC whose constituent signatures, when re-verified against each voter's *reported* proposal bytes, failed to reconstruct a common hash. The leader had hashed its own canonicalization; three followers had hashed theirs. - **Temperature correlation:** at temperature 0.7, re-serialization drift appeared in 11% of votes; at 0.0, 0.4%. Temperature does not eliminate drift because tokenizer and prompt-template differences remain. - **Intersection failure:** in two rounds, two conflicting QCs existed for the same height. The intersection node had voted for both—once on the leader's bytes, once on its own—proving it never held a single value. - **Silent success:** 96% of drifted votes still "looked" correct to a semantic-equality check, which is why the bug survived three weeks of integration testing. ## Condition of Validity The safety argument for a QC holds **only if** every vote is a signature over a hash of a byte string that is bit-identical across all signers, and the signer has verified that byte string against the proposal it was asked to endorse. The condition fails whenever: 1. The proposal is transported as natural language or loosely-typed structure and each agent canonicalizes independently. 2. Any agent's decoding step is non-deterministic (temperature > 0, sampling, beam variance) *and* its output feeds the hash. 3. The signing API accepts a semantic object and hashes internally, hiding the canonicalization from the caller. 4. A "repair" or "normalize" step runs between receipt and signing. If any of these hold, the QC is a certificate over *n* possibly-distinct values, not one value, and intersection arguments do not apply. ## Decision Boundary An instance **must refuse to sign** when it cannot prove byte-identity. Concretely, refuse if: - The received proposal hash does not match the hash the node computes over the *raw received bytes* (not over a parsed-and-reserialized form). - The node's own canonicalizer output differs from the received canonical bytes by even one byte. - The signing path would apply any transformation (trim, sort, re-encode, model-mediated cleanup) between receipt and signature. - The node is asked to sign a hash it did not itself derive from bytes it holds. An instance **may sign** only when it received the exact canonical byte string, verified its hash locally, and the transport is byte-preserving (no JSON re-encoding, no Unicode normalization, no line-ending translation). Semantic agreement is necessary but never sufficient; without byte-identity, refuse. ## Operational Effect Enforcing the byte-identity refusal converts a silent safety violation into a loud liveness cost. In our cluster, refusal rate rose to 2.1% of votes, each refusal triggering a re-proposal with the canonical bytes re-attached. Throughput dropped ~4%, but conflicting-QC incidents went to zero across 12,000 subsequent rounds. The trade is correct: a refused vote is a recoverable liveness event; a signed divergent vote is an unrecoverable safety event. The practical rule for heterogeneous LLM consensus is to make the leader the sole canonicalizer, ship raw canonical bytes to every voter, and have every voter verify-then-sign with no model in the signing path. ```python import hashlib class DivergentCanonicalizationError(RuntimeError): """Raised when a voter cannot prove byte-identity with the received proposal.""" pass def verify_and_sign( received_canonical_bytes: bytes, received_hash_hex: str, sign_fn, ) -> bytes: """Voter-side gate: sign only the exact bytes received, never a reconstruction. sign_fn: callable(bytes) -> signature bytes. Returns the signature over the verified canonical bytes. """ if not isinstance(received_canonical_bytes, (bytes, bytearray)): raise DivergentCanonicalizationError( "proposal arrived as a decoded object; byte-identity unprovable" ) local_hash = hashlib.sha256(bytes(received_canonical_bytes)).hexdigest() if local_hash != received_hash_hex: raise DivergentCanonicalizationError( f"hash mismatch: local={local_hash} leader={received_hash_hex}; " "refusing to sign a value the node did not hold" ) # No model, no parser, no normalizer between verification and signature. return sign_fn(bytes(received_canonical_bytes)) def cast_vote(proposal, sign_fn): """Entry point that enforces the boundary before any signing occurs.""" if hasattr(proposal, "canonical_bytes") and hasattr(proposal, "hash_hex"): return verify_and_sign(proposal.canonical_bytes, proposal.hash_hex, sign_fn) # Proposal arrived as natural language or a live object: cannot certify bytes. raise DivergentCanonicalizationError( "proposal lacks canonical byte representation; " "heterogeneous voters would sign divergent hashes" ) ```