{"record":{"id":"612246b721a79655","repo":"linera-io/linera-protocol","slug":"falsefirstroundattestation","errorCode":"FalseFirstRoundAttestation","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-core/src/chain_worker/state.rs","lineNumber":1300,"sourceCode":"        // This should always be true for valid certificates.\n        ensure!(\n            tip.block_hash == block.header.previous_block_hash,\n            WorkerError::InvalidBlockChaining\n        );\n\n        // Verify that the chain is active and that the epoch we used for verifying\n        // the certificate is actually the active one on the chain.\n        self.initialize_and_save_if_needed().await?;\n        let (epoch, _) = self.chain.current_committee().await?;\n        check_block_epoch(epoch, chain_id, block.header.epoch)?;\n\n        // The chain is initialized and this block has not executed yet, so the current ownership\n        // is the configuration the block was proposed under — even for the chain's first block,\n        // whose ownership comes from the just-applied chain description. This is the point where\n        // the first-round attestation can be checked against the actual first round; blocks that\n        // are only preprocessed skip it and rely on the nodes that execute the chain in order.\n        if certificate.first_round() {\n            ensure!(\n                certificate.round() == self.chain.ownership().await?.first_round(),\n                ChainError::FalseFirstRoundAttestation\n            );\n        }\n\n        let published_blobs = block\n            .published_blob_ids()\n            .iter()\n            .filter_map(|blob_id| blobs.remove(blob_id))\n            .collect::<Vec<_>>();\n\n        let local_time = self.storage.clock().current_time();\n        if block.header.timestamp.duration_since(local_time) > self.config.block_time_grace_period {\n            warn!(\n                block_timestamp = %block.header.timestamp,\n                %local_time,\n                \"Confirmed block has a timestamp in the future beyond the block time grace period\"\n            );","sourceCodeStart":1282,"sourceCodeEnd":1318,"githubUrl":"https://github.com/linera-io/linera-protocol/blob/6c226ddcb332ef55118dc8d0aafbd093d5420899/linera-core/src/chain_worker/state.rs#L1282-L1318","documentation":"A ConfirmedBlockCertificate can carry a first-round attestation (certificate.first_round()), a cheap proof that the block was finalized in the chain's very first consensus round. When that attestation is present, execute_contiguous_block checks certificate.round() against ChainOwnership::first_round() (Round::Fast if super owners exist, Validator(0) with no owners, MultiLeader(0) when multi_leader_rounds > 0, else SingleLeader(0)). The error means the certificate claims the attestation but was actually certified in a different round, so the attestation is false.","triggerScenarios":"A client that re-proposes a timed-out fast block in a higher round but keeps the first_round flag set in the confirmation; ownership changed between proposal and confirmation (super owners removed or multi_leader_rounds changed) so first_round() now computes a different round; hand-crafted or malformed certificates in tests.","commonSituations":"Client/wallet and validator versions mismatched after the first-round-attestation feature landed; chains whose ownership was migrated while a block was in flight; test fixtures that build certificates manually instead of via the standard propose/confirm flow.","solutions":["Regenerate the certificate so first_round is set only when round == ownership.first_round(), then resubmit.","Update the client/wallet (linera-service) and validators to matching versions.","Verify the chain's current ownership (super owners, multi_leader_rounds) matches what the proposer assumed when it built the attestation.","If neither side is wrong, treat it as a protocol violation and inspect the certificate's round and signature justification."],"exampleFix":"// before: attestation copied from the original fast proposal\nlet first_round = true;\n\n// after: claim the attestation only when the round really is the first\nlet first_round = round == ownership.first_round();\nlet cert = make_certificate(value, round, first_round, justification);","handlingStrategy":"validation","validationCode":"// Client-side mirror of the validator check, before submitting.\nif certificate.first_round() {\n    let ownership = client.ownership(chain_id).await?;\n    ensure!(\n        certificate.round() == ownership.first_round(),\n        ChainError::FalseFirstRoundAttestation\n    );\n}","typeGuard":null,"tryCatchPattern":"match result {\n    Err(NodeError::WorkerError(ref err))\n        if matches!(**err, WorkerError::ChainError(ref e)\n            if matches!(**e, ChainError::FalseFirstRoundAttestation)) =>\n    {\n        // Drop the certificate, rebuild it without the first-round attestation, resubmit.\n    }\n    result => result,\n}","preventionTips":["Set the first-round attestation only when the certification round equals ChainOwnership::first_round().","After any ownership change, discard in-flight certificates and re-propose.","Keep client and validator versions in lockstep when consensus certificate formats change.","Never hand-assemble certificates in tests; use the same builders as production code."],"tags":["rust","linera","blockchain","consensus","certificate","attestation"],"backgroundTag":"invalid-consensus-attestation","analyzedSha":"6c226ddcb332ef55118dc8d0aafbd093d5420899","analyzedAt":"2026-08-22T22:49:09.787Z","schemaVersion":2},"datasetVersion":"2026-08-23T01:17:44.959Z"}