linera-io/linera-protocol · error · ChainError

JustificationUnlockingRoundMismatch

JustificationUnlockingRoundMismatch

Error message

Certificate unlocking round does not match the top of its justification chain

What it means

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.

Source

Thrown at linera-chain/src/justification/mod.rs:620

/// the round the vote's payload grounds on, and its signatures form a genuine quorum of
/// `committee` over the reconstructed `ValidatedBlock` payload. These are exactly the checks a
/// voter performs before signing the opening's commitment, so their failure on a signed opening
/// convicts the signer.
fn check_cited_quorum(
    header: &BlockHeader,
    round: Round,
    kind: CertificateKind,
    unlocking_round: Option<Round>,
    opening: &CommittedQuorum,
    committee: &Committee,
) -> Result<(), ChainError> {
    ensure!(
        opening.value_hash == CryptoHash::new(header),
        ChainError::JustificationCommitmentMismatch
    );
    match kind {
        // A validated vote cites the quorum grounding its unlocking round, in a lower round.
        CertificateKind::Validated => ensure!(
            unlocking_round == Some(opening.round) && opening.round < round,
            ChainError::JustificationUnlockingRoundMismatch
        ),
        // A confirmed vote cites the quorum that validated the block in the same round.
        CertificateKind::Confirmed => ensure!(
            opening.round == round,
            ChainError::JustificationUnlockingRoundMismatch
        ),
        // Timeout votes cite nothing; any commitment is dishonest.
        CertificateKind::Timeout => ensure!(false, ChainError::JustificationCommitmentMismatch),
    }
    // A quorum with an unlocking round cites a quorum itself, and vice versa: its own commitment
    // and unlocking round come from one chain, so they are both present or both absent.
    ensure!(
        opening.unlocking_round.is_some() == opening.previous.is_some(),
        ChainError::JustificationUnlockingRoundMismatch
    );
    let value = VoteValue(

View on GitHub (pinned to 6c226ddcb3)

Solutions

  1. 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.
  2. Ensure the cited quorum's round is strictly below the vote's round — re-validate with a lower round chain.
  3. As a verifier, report the mismatch as an InvalidJustification fault with the opening as evidence.
  4. Check for version skew between voter and verifier if the grounding rule changed.

Example fix

// before
let value = VoteValue(hash, round, Kind::Validated, Some(other_round), false, Some(commitment));
// other_round != opening.round -> check_cited_quorum fails

// after
let unlocking = chain.top_unlocking_round(); // == opening's round, < round
let value = VoteValue(hash, round, Kind::Validated, unlocking, false, chain.commitment(hash));
Defensive patterns

Strategy: validation

Validate before calling

// voter-side: ground the unlocking round in the cited chain's top link
let unlocking = chain.top_unlocking_round();
ensure!(unlocking == Some(opening.round) && opening.round < round, "opening does not ground the unlocking round");

Try / catch

match check_cited_quorum(header, round, kind, unlocking_round, &opening, committee) {
    Err(ChainError::JustificationUnlockingRoundMismatch) => {
        // attributable fault: report InvalidJustification with the opening as evidence
        Err(anyhow::anyhow!("unlocking round not grounded by cited quorum"))
    }
    other => other,
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Related errors


AI-assisted analysis of linera-io/linera-protocol@6c226ddcb3 (2026-08-22). Data as JSON: /api/errors/caafa666f3c24550. Report an issue: GitHub.