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

  1. Reject the certificate — the flag and round are irreconcilable.
  2. In test/fuzz code, derive first_round from the actual round and justification instead of hard-coding it.
  3. Ensure all validators run a version that signs and verifies the first_round flag identically.
  4. 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

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

Related errors


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