{"record":{"id":"61171b59ddf3633c","repo":"linera-io/linera-protocol","slug":"certificate-carries-the-first-round-attestation-bu","errorCode":null,"errorMessage":"Certificate carries the first-round attestation but was not confirmed in the chain's first round","messagePattern":"Certificate carries the first-round attestation but was not confirmed in the chain's first round","errorType":"validation","errorClass":"ChainError","httpStatus":null,"severity":"critical","filePath":"linera-chain/src/certificate/lite.rs","lineNumber":190,"sourceCode":"        let top = self.justification.top_unlocking_round();\n        match self.value.kind {\n            CertificateKind::Validated => {\n                // The signed unlocking round must be the top of the chain, which must lie strictly\n                // below the certified round.\n                ensure!(\n                    self.unlocking_round == top,\n                    ChainError::JustificationUnlockingRoundMismatch\n                );\n                ensure!(\n                    top.is_none_or(|top| top < self.round),\n                    ChainError::JustificationChainNotBelowCertificate\n                );\n            }\n            CertificateKind::Confirmed => {\n                // The first-round attestation can only be set in a round that could be a chain's\n                // first one.\n                if self.first_round {\n                    ensure!(\n                        matches!(\n                            self.round,\n                            Round::Fast\n                                | Round::MultiLeader(0)\n                                | Round::SingleLeader(0)\n                                | Round::Validator(0)\n                        ),\n                        ChainError::FalseFirstRoundAttestation\n                    );\n                }\n                match top {\n                    // An absent chain is allowed only for a first-round confirmation.\n                    None => ensure!(\n                        self.first_round,\n                        ChainError::JustificationUnlockingRoundMismatch\n                    ),\n                    // Otherwise the chain's top link is the validation in the confirmation round.\n                    Some(top) => ensure!(","sourceCodeStart":172,"sourceCodeEnd":208,"githubUrl":"https://github.com/linera-io/linera-protocol/blob/6c226ddcb332ef55118dc8d0aafbd093d5420899/linera-chain/src/certificate/lite.rs#L172-L208","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before (test helper)\nlet cert = LiteCertificate::new_with_payload(value, Round::MultiLeader(2), ur, /* first_round */ true, jc, sigs);\n\n// after\nlet first_round = matches!(round, Round::Fast | Round::MultiLeader(0) | Round::SingleLeader(0) | Round::Validator(0));\nlet cert = LiteCertificate::new_with_payload(value, round, ur, first_round, jc, sigs);","handlingStrategy":"validation","validationCode":"fn first_round_is_possible(round: &Round) -> bool {\n    matches!(round, Round::Fast | Round::MultiLeader(0) | Round::SingleLeader(0) | Round::Validator(0))\n}\n\n// Before checking/propagating a certificate with first_round set:\nif certificate.first_round && !first_round_is_possible(&certificate.round) {\n    tracing::warn!(round = ?certificate.round, \"first-round attestation impossible in this round; discarding\");\n    return discard(certificate);\n}","typeGuard":"fn first_round_is_possible(round: &Round) -> bool {\n    matches!(round, Round::Fast | Round::MultiLeader(0) | Round::SingleLeader(0) | Round::Validator(0))\n}","tryCatchPattern":"match certificate.check(&committee) {\n    Ok(value) => value,\n    Err(ChainError::FalseFirstRoundAttestation) => {\n        tracing::warn!(round = ?certificate.round, \"first-round attestation in a non-first round; discarding certificate\");\n        self.request_full_certificate(certificate.value.value_hash).await?\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["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."],"tags":["linera","consensus","certificate","fast-path","rust"],"backgroundTag":"consensus-certificate-invalid","analyzedSha":"6c226ddcb332ef55118dc8d0aafbd093d5420899","analyzedAt":"2026-08-22T22:49:09.787Z","schemaVersion":2},"datasetVersion":"2026-08-23T01:17:44.959Z"}