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
- Treat the certificate as invalid: drop it and re-request the certificate (with its full original justification) from the source validator.
- Verify version compatibility between the sender and receiver — a chain serialized by a different linera version may hash differently.
- If producing certificates yourself, always attach justification via full_justification() so the chain and commitment are derived from the same data.
- 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
- Forward certificates verbatim through relays and storage; never rebuild or prune chains.
- When constructing certificates, derive chain and commitment from the same source (full_justification).
- Keep linera versions aligned across the network so serialization hashes agree.
- Fail closed on any certificate inconsistency: re-request, do not repair.
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
- SSL/TLS and certificate errors — how TLS handshakes and certificate validation fail.
Related errors
- Certificate unlocking round does not match the top of its ju
- Justification chain must lie in rounds strictly below the ce
- Certificate carries the first-round attestation but was not
- FalseFirstRoundAttestation
- CannotRejectMessage
AI-assisted analysis of linera-io/linera-protocol@6c226ddcb3 (2026-08-22).
Data as JSON: /api/errors/7d4dd0f687466a11.
Report an issue: GitHub.