linera-io/linera-protocol · critical · ChainError
Certificate carries the first-round attestation but was not
Error message
Certificate carries the first-round attestation but was not confirmed in the chain's first round
What it means
For confirmed certificates, the first_round attestation asserts the block was confirmed without a preceding validation (direct first-round confirmation), which is only possible in rounds that can start a chain: Round::Fast, MultiLeader(0), SingleLeader(0), or Validator(0). LiteCertificate::check enforces this at linera-chain/src/certificate/lite.rs:190 and raises ChainError::FalseFirstRoundAttestation when first_round is set in any higher round. A first-round claim in a later round is contradictory and typically signals a malformed or forged certificate.
Source
Thrown at linera-chain/src/certificate/lite.rs:190
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)
| Round::Validator(0)
),
ChainError::FalseFirstRoundAttestation
);
}
match top {
// An absent chain is allowed only for a first-round confirmation.
None => ensure!(
self.first_round,
ChainError::JustificationUnlockingRoundMismatch
),
// Otherwise the chain's top link is the validation in the confirmation round.
Some(top) => ensure!(View on GitHub (pinned to 6c226ddcb3)
Solutions
- Reject the certificate — the flag and round are irreconcilable.
- In test/fuzz code, derive first_round from the actual round and justification instead of hard-coding it.
- Ensure all validators run a version that signs and verifies the first_round flag identically.
- When assembling certificates manually, use the constructors (LiteCertificate::new_with_payload / try_from_votes) with values taken from real votes only.
Example fix
// before (test helper) let cert = LiteCertificate::new_with_payload(value, Round::MultiLeader(2), ur, /* first_round */ true, jc, sigs); // after let first_round = matches!(round, Round::Fast | Round::MultiLeader(0) | Round::SingleLeader(0) | Round::Validator(0)); let cert = LiteCertificate::new_with_payload(value, round, ur, first_round, jc, sigs);
Defensive patterns
Strategy: validation
Validate before calling
fn first_round_is_possible(round: &Round) -> bool {
matches!(round, Round::Fast | Round::MultiLeader(0) | Round::SingleLeader(0) | Round::Validator(0))
}
// Before checking/propagating a certificate with first_round set:
if certificate.first_round && !first_round_is_possible(&certificate.round) {
tracing::warn!(round = ?certificate.round, "first-round attestation impossible in this round; discarding");
return discard(certificate);
} Type guard
fn first_round_is_possible(round: &Round) -> bool {
matches!(round, Round::Fast | Round::MultiLeader(0) | Round::SingleLeader(0) | Round::Validator(0))
} Try / catch
match certificate.check(&committee) {
Ok(value) => value,
Err(ChainError::FalseFirstRoundAttestation) => {
tracing::warn!(round = ?certificate.round, "first-round attestation in a non-first round; discarding certificate");
self.request_full_certificate(certificate.value.value_hash).await?
}
Err(e) => return Err(e),
} Prevention
- Derive first_round from the round and justification instead of defaulting flags in constructors.
- Keep signer and verifier versions aligned whenever attestation semantics change.
- Only assemble certificates from genuine votes; never synthesize flags for tests without deriving them.
When it happens
Trigger: A certificate in Round::MultiLeader(3) (or any non-zero/non-fast round) carrying first_round = true; test code defaulting first_round to true when synthesizing certificates; a buggy proposer copying the flag from a prior certificate in a chain.
Common situations: Certificate synthesis helpers with wrong defaults in tests and fuzzer corpora; version skew after the first-round attestation feature was introduced, with old signers omitting/new verifiers expecting the flag; adversarial attempts to skip justification requirements by claiming first round.
Understand the failure class
- SSL/TLS and certificate errors — how TLS handshakes and certificate validation fail.
Related errors
- Certificate justification commitment does not match its just
- Certificate unlocking round does not match the top of its ju
- Justification chain must lie in rounds strictly below the ce
- FalseFirstRoundAttestation
- CannotRejectMessage
AI-assisted analysis of linera-io/linera-protocol@6c226ddcb3 (2026-08-22).
Data as JSON: /api/errors/61171b59ddf3633c.
Report an issue: GitHub.