linera-io/linera-protocol · critical · ChainError

Certificate justification commitment does not match its just

Error message

Certificate justification commitment does not match its justification chain

What it means

LiteCertificate::check verifies a certificate's carried justification chain against the commitment its quorum signed: justification.verify(value_hash) recomputes the hash-linked head commitment of the carried chain, and linera-chain/src/certificate/lite.rs:159 requires it equals the certificate's justification_commitment field. A mismatch means the attached chain is not the one the validators committed to — stripped, replaced, or reordered links. Per the doc comment, this single check is what lets the worker trust a retry proposal's chain, so any mismatch is fatal for the certificate.

Source

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

            signatures,
        ))
    }

    /// Verifies the certificate: its signatures, its justification chain, and that the signed
    /// unlocking round and first-round attestation are bound to that chain exactly as
    /// [`ValidatedBlockCertificate::check`] and [`ConfirmedBlockCertificate::check`] require. This
    /// is the single verification the worker applies to the certificate a retry proposal carries,
    /// so it must reject a stripped or mismatched chain, not just check the pieces in isolation.
    ///
    /// [`ValidatedBlockCertificate::check`]: super::ValidatedBlockCertificate::check
    /// [`ConfirmedBlockCertificate::check`]: super::ConfirmedBlockCertificate::check
    pub fn check(&self, committee: &Committee) -> Result<&LiteValue, ChainError> {
        // The carried chain's links are not signature-checked: the signed justification
        // commitment is the hash-linked head of the chain, so the single signature check over
        // this certificate's own quorum (below) attests every link — each link's voters verified
        // the quorum beneath them before signing over its hash.
        let derived_commitment = self.justification.verify(self.value.value_hash)?;
        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!(

View on GitHub (pinned to 6c226ddcb3)

Solutions

  1. Treat the certificate as invalid: drop it and re-request the certificate (with its full original justification) from the source validator.
  2. Verify version compatibility between the sender and receiver — a chain serialized by a different linera version may hash differently.
  3. If producing certificates yourself, always attach justification via full_justification() so the chain and commitment are derived from the same data.
  4. Audit custom relay code for any code path that reconstructs or prunes chains instead of forwarding certificates verbatim.

Example fix

// before (relay reconstructing chains)
let cert = LiteCertificate::new_with_payload(value, round, ur, fr, old_commitment, signatures);

// after (forward verbatim; derive chain and commitment together)
let cert = original_cert.clone();
cert.check(&committee)?;
Defensive patterns

Strategy: try-catch

Validate before calling

// Cheap pre-check before full verification: recompute the commitment of the carried chain.
let derived = certificate.justification.verify(certificate.value.value_hash)?;
if derived != certificate.justification_commitment {
    tracing::warn!("justification commitment mismatch; requesting original certificate");
    return request_certificate_from_validator(origin).await;
}

Try / catch

match certificate.check(&committee) {
    Ok(value) => value,
    Err(ChainError::JustificationCommitmentMismatch) => {
        // Chain was stripped/replaced relative to what was signed: unusable.
        tracing::warn!("certificate carries a mismatched justification chain; discarding");
        self.request_full_certificate(certificate.value.value_hash).await?
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: A certificate whose justification chain was stripped down (links removed to save space) while the signed justification_commitment still refers to the full chain; a malicious or buggy validator signing one chain but attaching another; deserialization/version skew that reconstructs the chain with different link ordering; network peer relaying a certificate assembled from mismatched parts.

Common situations: Interoperability between client versions that serialize justification chains differently; a relay/proxy truncating large certificate payloads; consensus attacks replaying old justification chains under new certificates; corrupted storage of certificates.

Understand the failure class

Related errors


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