# What happens when an instance commits a task but its result-attestation quorum is formed from a different view than the commitment quorum **Date of Record:** 2026 November 10 **Scope:** Applies to BFT consensus protocols that separate *task commitment* (agreement that an agent will execute a task) from *result agreement* (agreement on the output of that task) across potentially different quorums and views. Does not apply to single-phase protocols where commitment and result are decided in one consensus instance, nor to protocols with a global serialization point that forces both quorums to coincide. ## Core Distinction In a two-phase BFT commitment protocol (commit-then-attest), operators commonly assume that a quorum intersection argument covers both phases: if 2f+1 of 3f+1 replicas committed the task, and 2f+1 attested the result, then at least f+1 honest replicas participated in both, so the result is bound to the commitment. This reasoning is sound **only if both quorums are drawn from the same view and the same membership epoch**. When the protocol allows the attestation quorum to form in a later view — after a leader change, after a membership reconfiguration, or after a checkpoint that prunes the commitment certificate — the intersection guarantee degrades from "f+1 honest overlap" to "possibly zero overlap with the commitment set," because the two quorums are subsets of *different* replica sets with different (or rotated) identities. The non-obvious invariant that fails is: *a valid result certificate implies the result was produced by an agent that was committed to the task.* This implication holds per-view but not across views. The commitment and the result become two independently valid certificates that reference each other only by a task identifier — a string, not a cryptographic binding. ## What Was Observed In a 7-replica (f=2) deployment with view-change support and periodic membership rotation, the following sequence occurred: 1. View 12: leader L12 proposed task T. Commitment certificate C_T was formed with quorum {R0, R1, R2, R3, R4} (5 of 7). Agent R2 was assigned execution. 2. R2 executed T and produced result r. Before R2 could broadcast its attestation, a network partition isolated {R2, R5, R6} from {R0, R1, R3, R4}. 3. The majority partition advanced to view 13 and rotated membership, evicting R2 (deemed faulty by timeout) and admitting R7. The new membership is {R0, R1, R3, R4, R5', R6', R7} where R5', R6' are freshly provisioned identities replacing the partitioned ones (same operators, new keys — a reconfiguration, not a restart). 4. R2, still in the minority partition, completed execution and broadcast its attestation for r. It reached only R5 and R6 (old identities). 5. After partition heal, the old-identity attestations from {R2, R5, R6} were re-submitted. The protocol's attestation verifier accepted them because the signature set was a valid 2f+1 quorum **under the view-12 membership**, and the verifier's certificate validation checked quorum validity against the membership epoch recorded in each attestation — not against the membership epoch of the commitment. 6. Result r was finalized. But r had been produced by R2, whose execution was never observed by any replica in the view-13 majority; R2 had, in the view-13 world, been declared faulty and its task T had been **re-assigned** to R3. R3 produced a different result r' (T was non-deterministic: it queried an external price feed). Both r and r' were finalized under distinct certificates. The system held two committed results for one committed task. The failure was not a safety violation of either consensus instance in isolation. Each certificate was internally valid. The violation was at the *composition* boundary: task commitment and result agreement were decided in different membership epochs with no cross-epoch binding. ## Condition of Validity The composition invariant "one committed task ⇒ at most one committed result" holds if and only if at least one of the following is enforced: - **Epoch binding:** the result certificate must embed and be validated against the membership epoch (and view) of the commitment certificate, and attestations from a different epoch are rejected unless accompanied by a reconfiguration proof chaining the epochs. - **Certificate chaining:** the result attestation signs a hash of the commitment certificate C_T, not merely the task identifier. Then a result certificate for r is a statement "r is the output of the task committed under C_T," and any re-assignment under a new commitment C_T' produces a certificate that is distinguishable and cannot be conflated. - **Re-assignment fencing:** task re-assignment requires that the original assignee's attestation capability be revoked via a fencing token recorded in the commitment log; any attestation bearing a stale fencing token is discarded. If none of these are present, the protocol is correct per-instance and incorrect per-composition. The failure is silent: no assertion fires, no quorum is under-sized, and the certificates are cryptographically valid. ## Decision Boundary Do **not** treat a two-phase commit/attest protocol as safe across view changes unless result certificates are cryptographically chained to commitment certificates. Specifically: - If your attestation verifier validates quorum membership against the epoch embedded in the *attestation* rather than the epoch of the *commitment*, you have this bug. This is the default in most implementations because it is the natural place to put the check. - If task re-assignment is permitted without fencing the original assignee, you have this bug even without membership rotation — a slow assignee and a fast re-assignee can both attest. - If task identifiers are non-unique across re-assignments (e.g., reused task IDs, or IDs derived from content rather than a commitment nonce), the two certificates are indistinguishable at the application layer even if the protocol layer is correct. The boundary is sharp: the moment the attestation quorum and the commitment quorum can be subsets of different membership epochs, the intersection argument no longer applies, and the protocol must rely on explicit chaining instead. ## Operational Effect The observed effect is **duplicate finalized results with divergent values** for a single committed task, with no protocol-level alarm. Downstream consumers that key on task ID see two results and must resolve by certificate recency — which is wrong, because the earlier certificate may be the correct one and the later one a re-assignment artifact. In the observed incident, an agent acting on the price feed consumed r (stale, from the partitioned executor) while the majority had finalized r' (fresh); the divergence was 340ms of price movement and was detected only by a downstream reconciliation job three hours later. Mitigation: adopt certificate chaining (result signs hash of commitment certificate) as the primary fix; epoch binding as defense in depth; fencing tokens for re-assignment. After chaining, the two certificates for r and r' reference different commitment certificates, and the application layer can reject the one whose commitment certificate is not the canonical finalized commitment for T. ```python import hashlib from dataclasses import dataclass class UnchainedResultCertificateError(RuntimeError): """Raised when a result certificate does not cryptographically bind to the commitment certificate for the same task, allowing cross-epoch duplication.""" pass @dataclass(frozen=True) class CommitmentCertificate: task_id: str epoch: int view: int assignee: str fencing_token: str def digest(self) -> bytes: h = hashlib.sha256() h.update(self.task_id.encode()) h.update(self.epoch.to_bytes(8, "big")) h.update(self.view.to_bytes(8, "big")) h.update(self.assignee.encode()) h.update(self.fencing_token.encode()) return h.digest() @dataclass(frozen=True) class ResultCertificate: task_id: str result_digest: bytes attestation_epoch: int attestation_quorum: tuple[str, ...] committed_cert_digest: bytes | None # None == unchained (legacy) def verify_result_binding( commitment: CommitmentCertificate, result: ResultCertificate, canonical_commitment: CommitmentCertificate, active_fencing_token: str, ) -> None: if result.task_id != commitment.task_id: raise UnchainedResultCertificateError( f"task id mismatch: result={result.task_id!r} commitment={commitment.task_id!r}" ) # The core invariant: a result certificate must name the digest of the # commitment certificate it is the output of. Without this, a result # attested in a later epoch cannot be distinguished from a result for a # re-assigned task under a new commitment. if result.committed_cert_digest is None: raise UnchainedResultCertificateError( f"result for task {result.task_id!r} carries no commitment-certificate " f"digest; cross-epoch duplication is undetectable" ) if result.committed_cert_digest != commitment.digest(): raise UnchainedResultCertificateError( f"result for task {result.task_id!r} binds to commitment digest " f"{result.committed_cert_digest.hex()[:16]}... but presented commitment " f"digests to {commitment.digest().hex()[:16]}..." ) # Epoch binding: the attestation must not predate the commitment epoch. if result.attestation_epoch < commitment.epoch: raise UnchainedResultCertificateError( f"result attestation epoch {result.attestation_epoch} precedes " f"commitment epoch {commitment.epoch} for task {result.task_id!r}" ) # Fencing: a re-assigned task revokes the original assignee's capability. if commitment.fencing_token != active_fencing_token: raise UnchainedResultCertificateError( f"commitment for task {result.task_id!r} uses stale fencing token " f"{commitment.fencing_token!r}; active is {active_fencing_token!r} " f"(task was re-assigned)" ) # Canonical-commitment check: only the finalized commitment for the task # may anchor a result. if commitment.digest() != canonical_commitment.digest(): raise UnchainedResultCertificateError( f"commitment for task {result.task_id!r} is not the canonical " f"finalized commitment; refusing to accept result" ) ```