linera-io/linera-protocol · critical · ChainError

Certificate unlocking round does not match the top of its ju

Error message

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

What it means

For a validated certificate, LiteCertificate::check requires at linera-chain/src/certificate/lite.rs:177 that the signed unlocking_round equals the top unlocking round of the carried justification chain (top_unlocking_round()). The signed value must describe exactly the chain being presented; a mismatch means the quorum signed an unlocking round that is not the head of the attached chain — an inconsistent or manipulated certificate.

Source

Thrown at linera-chain/src/certificate/lite.rs:177

        ensure!(
            self.justification_commitment == derived_commitment,
            ChainError::JustificationCommitmentMismatch
        );
        let value = VoteValue(
            self.value.value_hash,
            self.round,
            self.value.kind,
            self.unlocking_round,
            self.first_round,
            self.justification_commitment,
        );
        check_signatures(&value, &self.signatures, committee)?;
        let top = self.justification.top_unlocking_round();
        match self.value.kind {
            CertificateKind::Validated => {
                // The signed unlocking round must be the top of the chain, which must lie strictly
                // below the certified round.
                ensure!(
                    self.unlocking_round == top,
                    ChainError::JustificationUnlockingRoundMismatch
                );
                ensure!(
                    top.is_none_or(|top| top < self.round),
                    ChainError::JustificationChainNotBelowCertificate
                );
            }
            CertificateKind::Confirmed => {
                // The first-round attestation can only be set in a round that could be a chain's
                // first one.
                if self.first_round {
                    ensure!(
                        matches!(
                            self.round,
                            Round::Fast
                                | Round::MultiLeader(0)
                                | Round::SingleLeader(0)

View on GitHub (pinned to 6c226ddcb3)

Solutions

  1. Reject the certificate and re-fetch it from the source; consistent certificates verify as a unit.
  2. If you build certificates in tests, construct them from a single vote set via try_from_votes so round, unlocking_round, and justification come from the same votes.
  3. Check for software version mismatch between the certificate producer and verifier.
  4. Persist certificates unmodified once checked; do not rebuild them from parts.
Defensive patterns

Strategy: try-catch

Try / catch

match certificate.check(&committee) {
    Ok(value) => value,
    Err(ChainError::JustificationUnlockingRoundMismatch) => {
        tracing::warn!(
            round = ?certificate.round,
            signed_unlocking = ?certificate.unlocking_round,
            "signed unlocking round inconsistent with justification chain; discarding certificate"
        );
        self.request_full_certificate(certificate.value.value_hash).await?
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: A certificate assembled from votes citing one justification but carrying a different (e.g., longer or shorter) chain; a buggy validator copying unlocking_round from an older vote; retry proposals where the chain was refreshed but the signed unlocking_round was not regenerated.

Common situations: Version skew in how unlocking_round is computed from chains; test code mixing fields from different certificates; a Byzantine validator crafting certificates to unlock locked values prematurely.

Understand the failure class

Related errors


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