{"record":{"id":"69db7e15d903dc9f","repo":"linera-io/linera-protocol","slug":"protocol-error-within-chain-client-a-quorum-voted-69db7e","errorCode":null,"errorMessage":"Protocol error within chain client: A quorum voted with a justification commitment that does not match the proposal's justification chain","messagePattern":"Protocol error within chain client: A quorum voted with a justification commitment 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":1441,"sourceCode":"            .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\n    /// Creates a [`RemoteNodeUpdater`] for the given validator, backed by our local node.\n    fn remote_node_updater(\n        &self,\n        remote_node: RemoteNode<Env::ValidatorNode>,\n    ) -> RemoteNodeUpdater<Env::Storage, Env::ValidatorNode> {\n        RemoteNodeUpdater {\n            remote_node,","sourceCodeStart":1423,"sourceCodeEnd":1459,"githubUrl":"https://github.com/linera-io/linera-protocol/blob/6c226ddcb332ef55118dc8d0aafbd093d5420899/linera-core/src/client/mod.rs#L1423-L1459","documentation":"Companion guard to the unlocking-round check in Client::submit_block_proposal: after a quorum forms, the justification commitment signed by the votes must equal the commitment of the justification chain derived from our own proposal (the validated certificate's full chain for a regular retry, empty for fresh/fast proposals). A mismatch means the winning votes were formed over a different justification chain — e.g. a competing proposal — and assembling a certificate from them would fail verification everywhere.","triggerScenarios":"Same race as the unlocking-round variant: validators aggregated around a competing proposal that cited a different certificate while we submitted ours; our quorum finished on votes whose justification_commitment field differs from justification.commitment(quorum.hash()).","commonSituations":"Parallel proposers on one chain; client retries after leader timeouts while another proposer's validated certificate spread; inconsistent validator state after partial crashes.","solutions":["Retry the proposal cycle: re-prepare the chain and let the client rebuild the block on the now-current locking state","Serialize proposals per chain (one active client per owner)","Verify committee-wide version consistency","Report with attached certificates if it repeats without concurrency"],"exampleFix":null,"handlingStrategy":"retry","validationCode":null,"typeGuard":"fn is_proposal_justification_mismatch(err: &chain_client::Error) -> bool {\n    matches!(\n        err,\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}","tryCatchPattern":"match client.process_pending_block().await {\n    Err(e) if is_proposal_justification_mismatch(&e) => {\n        // Winning votes cited a different certificate; drop pending state and retry.\n        client.clear_pending_block();\n        client.prepare_chain().await?;\n        client.process_pending_block().await\n    }\n    other => other,\n}","preventionTips":["Single-writer discipline per chain is the main preventive measure","Ensure clients restart with synchronized state (prepare_chain) before resubmitting blocks","Pin the whole committee to one protocol version during rolling upgrades"],"tags":["linera","consensus","proposal","certificate","protocol-error"],"backgroundTag":"consensus-vote-mismatch","analyzedSha":"6c226ddcb332ef55118dc8d0aafbd093d5420899","analyzedAt":"2026-08-22T22:49:09.787Z","schemaVersion":2},"datasetVersion":"2026-08-23T01:17:44.959Z"}