{"record":{"id":"caafa666f3c24550","repo":"linera-io/linera-protocol","slug":"justificationunlockingroundmismatch","errorCode":"JustificationUnlockingRoundMismatch","errorMessage":"Certificate unlocking round does not match the top of its justification chain","messagePattern":"Certificate unlocking round does not match the top of its justification chain","errorType":"validation","errorClass":"ChainError","httpStatus":null,"severity":"error","filePath":"linera-chain/src/justification/mod.rs","lineNumber":620,"sourceCode":"/// the round the vote's payload grounds on, and its signatures form a genuine quorum of\n/// `committee` over the reconstructed `ValidatedBlock` payload. These are exactly the checks a\n/// voter performs before signing the opening's commitment, so their failure on a signed opening\n/// convicts the signer.\nfn check_cited_quorum(\n    header: &BlockHeader,\n    round: Round,\n    kind: CertificateKind,\n    unlocking_round: Option<Round>,\n    opening: &CommittedQuorum,\n    committee: &Committee,\n) -> Result<(), ChainError> {\n    ensure!(\n        opening.value_hash == CryptoHash::new(header),\n        ChainError::JustificationCommitmentMismatch\n    );\n    match kind {\n        // A validated vote cites the quorum grounding its unlocking round, in a lower round.\n        CertificateKind::Validated => ensure!(\n            unlocking_round == Some(opening.round) && opening.round < round,\n            ChainError::JustificationUnlockingRoundMismatch\n        ),\n        // A confirmed vote cites the quorum that validated the block in the same round.\n        CertificateKind::Confirmed => ensure!(\n            opening.round == round,\n            ChainError::JustificationUnlockingRoundMismatch\n        ),\n        // Timeout votes cite nothing; any commitment is dishonest.\n        CertificateKind::Timeout => ensure!(false, ChainError::JustificationCommitmentMismatch),\n    }\n    // A quorum with an unlocking round cites a quorum itself, and vice versa: its own commitment\n    // and unlocking round come from one chain, so they are both present or both absent.\n    ensure!(\n        opening.unlocking_round.is_some() == opening.previous.is_some(),\n        ChainError::JustificationUnlockingRoundMismatch\n    );\n    let value = VoteValue(","sourceCodeStart":602,"sourceCodeEnd":638,"githubUrl":"https://github.com/linera-io/linera-protocol/blob/6c226ddcb332ef55118dc8d0aafbd093d5420899/linera-chain/src/justification/mod.rs#L602-L638","documentation":"Thrown by check_cited_quorum for a Validated-kind vote when the opening does not ground the vote's unlocking round: the vote must sign unlocking_round == Some(opening.round) and the opening's round must be strictly lower than the vote's round (ChainError::JustificationUnlockingRoundMismatch). A validated vote's unlocking round is defined by the quorum it cites in a lower round; an opening at the same or higher round, or a mismatched unlocking round, does not honestly ground the claim.","triggerScenarios":"check_cited_quorum is called with kind == CertificateKind::Validated where unlocking_round != Some(opening.round) or opening.round >= round. Arises when a voter signs an unlocking round not equal to the round of the quorum it cites, or cites a same-round/higher-round quorum; when seen via InvalidJustification proof checking, it is the fault being proven.","commonSituations":"Misimplemented voters that set unlocking_round from a different chain link than the cited opening; certificate builders mixing links from different validation attempts; protocol-version drift changing which link defines the unlocking round.","solutions":["As a voter, set the signed unlocking_round from the top link of the exact chain you verified (chain.top_unlocking_round()) and cite that chain's commitment.","Ensure the cited quorum's round is strictly below the vote's round — re-validate with a lower round chain.","As a verifier, report the mismatch as an InvalidJustification fault with the opening as evidence.","Check for version skew between voter and verifier if the grounding rule changed."],"exampleFix":"// before\nlet value = VoteValue(hash, round, Kind::Validated, Some(other_round), false, Some(commitment));\n// other_round != opening.round -> check_cited_quorum fails\n\n// after\nlet unlocking = chain.top_unlocking_round(); // == opening's round, < round\nlet value = VoteValue(hash, round, Kind::Validated, unlocking, false, chain.commitment(hash));","handlingStrategy":"validation","validationCode":"// voter-side: ground the unlocking round in the cited chain's top link\nlet unlocking = chain.top_unlocking_round();\nensure!(unlocking == Some(opening.round) && opening.round < round, \"opening does not ground the unlocking round\");","typeGuard":null,"tryCatchPattern":"match check_cited_quorum(header, round, kind, unlocking_round, &opening, committee) {\n    Err(ChainError::JustificationUnlockingRoundMismatch) => {\n        // attributable fault: report InvalidJustification with the opening as evidence\n        Err(anyhow::anyhow!(\"unlocking round not grounded by cited quorum\"))\n    }\n    other => other,\n}","preventionTips":["Derive unlocking_round from chain.top_unlocking_round() and cite that same chain's commitment.","Ensure the cited quorum's round is strictly below the vote's round.","Add invariant checks in the voter before signing a VoteValue with a commitment."],"tags":["consensus","justification","rounds","unlocking-round"],"backgroundTag":"justification-unlocking-round-mismatch","analyzedSha":"6c226ddcb332ef55118dc8d0aafbd093d5420899","analyzedAt":"2026-08-22T22:49:09.787Z","schemaVersion":2},"datasetVersion":"2026-08-23T01:17:44.959Z"}