{"record":{"id":"56a9ee5d128fe5a4","repo":"linera-io/linera-protocol","slug":"protocol-error-within-chain-client-a-quorum-voted","errorCode":null,"errorMessage":"Protocol error within chain client: A quorum voted with an unlocking round that does not match the proposal's justification chain","messagePattern":"Protocol error within chain client: A quorum voted with an unlocking round that does not match the proposal's justification chain","errorType":"exception","errorClass":"ChainClientError","httpStatus":null,"severity":"error","filePath":"linera-core/src/client/mod.rs","lineNumber":1434,"sourceCode":"                    }\n                }\n            }\n        });\n\n        let quorum = self\n            .communicate_chain_action(&committee, submit_action, value)\n            .await?;\n\n        clock_skew_check_handle.await;\n\n        // The justification chain comes from our own proposal, but the winning quorum may have\n        // been formed from a competing proposal that cited a different certificate: its votes then\n        // sign a different unlocking round and justification commitment, and gluing our chain onto\n        // them would build a certificate that fails verification downstream. Reject that here with\n        // a retryable error rather than assembling a mismatched certificate. (For confirmed and\n        // timeout quorums both sides are `None`, so this only bites the validated-retry case it is\n        // meant to guard.)\n        ensure!(\n            quorum.unlocking_round() == justification.top_unlocking_round(),\n            chain_client::Error::ProtocolError(\n                \"A quorum voted with an unlocking round that does not match the proposal's \\\n                 justification chain\",\n            )\n        );\n        ensure!(\n            quorum.justification_commitment() == justification.commitment(quorum.hash()),\n            chain_client::Error::ProtocolError(\n                \"A quorum voted with a justification commitment that does not match the \\\n                 proposal's justification chain\",\n            )\n        );\n        let certificate = T::make_certificate(quorum, justification);\n        self.handle_certificate::<T>(certificate.clone()).await?;\n        Ok(certificate)\n    }\n","sourceCodeStart":1416,"sourceCodeEnd":1452,"githubUrl":"https://github.com/linera-io/linera-protocol/blob/6c226ddcb332ef55118dc8d0aafbd093d5420899/linera-core/src/client/mod.rs#L1416-L1452","documentation":"Raised in Client::submit_block_proposal on the validated-retry path: the winning quorum signed an unlocking_round that does not equal the top unlocking round of the justification chain carried by our proposal. Per the source comment, the quorum may have formed from a competing proposal that cited a different certificate, so its votes sign a different unlocking round; gluing our chain onto those votes would build a certificate that fails verification downstream. The client rejects it as a retryable error rather than assembling the mismatched certificate.","triggerScenarios":"Regular (validated) retry of a block proposal while validators concurrently validated a competing proposal citing a different certificate; two proposers racing in the same round; a validator set that latched onto a competing validated block. Confirmed/timeout quorums have unlocking_round None on both sides, so only the validated-retry case triggers this.","commonSituations":"Concurrent clients retrying a failed block on the same owner chain; leader-timeout churn where multiple proposals circulate; mixed-version validators disagreeing on justification handling during a protocol upgrade.","solutions":["Retry: the pending-block loop re-synchronizes chain state from validators and re-proposes on top of whatever was validated","Ensure a single writer per chain (one client/process proposing) to avoid competing proposals","Align validator versions across the committee","If it persists on an idle chain, collect the proposals/certificates involved and report a protocol bug"],"exampleFix":null,"handlingStrategy":"retry","validationCode":null,"typeGuard":"fn is_unlocking_round_mismatch(err: &chain_client::Error) -> bool {\n    matches!(\n        err,\n        chain_client::Error::ProtocolError(\n            \"A quorum voted with an unlocking round that does not match the proposal's \\\n             justification chain\"\n        )\n    )\n}","tryCatchPattern":"match client.process_pending_block().await {\n    Err(e) if is_unlocking_round_mismatch(&e) => {\n        // Competing proposal won the validated quorum; absorb it and rebuild on top.\n        client.synchronize_chain_state_from_all_validators().await?;\n        client.process_pending_block().await\n    }\n    other => other,\n}","preventionTips":["Serialize proposals: never run two clients proposing the same chain concurrently","After leader timeouts, always re-synchronize before re-proposing a validated retry","Treat occasional occurrences as benign contention; only persistent failures on an idle chain warrant a bug report"],"tags":["linera","consensus","proposal","retry","protocol-error"],"backgroundTag":"consensus-vote-mismatch","analyzedSha":"6c226ddcb332ef55118dc8d0aafbd093d5420899","analyzedAt":"2026-08-22T22:49:09.787Z","schemaVersion":2},"datasetVersion":"2026-08-23T01:17:44.959Z"}