{"record":{"id":"9cfb8d231d2ce7e8","repo":"linera-io/linera-protocol","slug":"the-new-proposal-s-round-must-be-greater-than-the","errorCode":null,"errorMessage":"The new proposal's round must be greater than the original's","messagePattern":"The new proposal's round must be greater than the original's","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"linera-chain/src/data_types/mod.rs","lineNumber":999,"sourceCode":"    }\n\n    /// Returns the IDs of the blobs that are required or created by this proposal.\n    pub fn expected_blob_ids(&self) -> impl Iterator<Item = BlobId> + '_ {\n        self.content.block.published_blob_ids().into_iter().chain(\n            self.content.outcome.iter().flat_map(|outcome| {\n                outcome\n                    .oracle_blob_ids()\n                    .into_iter()\n                    .chain(outcome.iter_created_blobs_ids())\n            }),\n        )\n    }\n\n    /// Checks that the original proposal, if present, matches the new one and has a higher round.\n    pub fn check_invariants(&self) -> Result<(), &'static str> {\n        match (&self.original_proposal, &self.content.outcome) {\n            (None, None) => {}\n            (Some(OriginalProposal::Fast(_)), None) => ensure!(\n                self.content.round > Round::Fast,\n                \"The new proposal's round must be greater than the original's\"\n            ),\n            (None, Some(_))\n            | (Some(OriginalProposal::Fast(_)), Some(_))\n            | (Some(OriginalProposal::Regular { .. }), None) => {\n                return Err(\"Must contain a validation certificate if and only if \\\n                     it contains the execution outcome from a previous round\");\n            }\n            (Some(OriginalProposal::Regular { certificate }), Some(outcome)) => {\n                ensure!(\n                    self.content.round > certificate.round,\n                    \"The new proposal's round must be greater than the original's\"\n                );\n                let block = outcome.clone().with(self.content.block.clone());\n                let value = ValidatedBlock::new(block);\n                ensure!(\n                    certificate.check_value(&value),","sourceCodeStart":981,"sourceCodeEnd":1017,"githubUrl":"https://github.com/linera-io/linera-protocol/blob/6c226ddcb332ef55118dc8d0aafbd093d5420899/linera-chain/src/data_types/mod.rs#L981-L1017","documentation":"BlockProposal::check_invariants (linera-chain/src/data_types/mod.rs:996) validates retry-proposal shape. When original_proposal is OriginalProposal::Fast (a retry of a failed fast-track proposal) and no execution outcome is carried, the new round must be strictly greater than Round::Fast (chain.rs 999-1002): a fast proposal can only be retried in a regular round. The failure is a &'static str that the worker wraps into WorkerError::InvalidBlockProposal (linera-core/src/chain_worker/state.rs:2511-2513).","triggerScenarios":"Constructing a retry of a fast proposal whose content.round is still Round::Fast (or otherwise not greater); hand-building BlockProposal instead of using BlockProposal::new_retry_fast; deserialization of a proposal with an inconsistent round field.","commonSituations":"Retry loops that reuse the original round instead of advancing after a leader timeout; proposal-building code that copies fields from the failed proposal wholesale; protocol tests constructing proposals manually.","solutions":["Build retries with BlockProposal::new_retry_fast, which sets a valid higher round","When constructing manually, move to a regular round after fast fails, e.g. Round::MultiLeader(0) or the round certified by the timeout","Validate the round invariant client-side before sending, mirroring check_invariants"],"exampleFix":"// before: retrying fast with the same round\nlet proposal = BlockProposal { content: ProposalContent { block, round: Round::Fast, outcome: None },\n    original_proposal: Some(OriginalProposal::Fast(fast_sig)), signature };\n\n// after: retry in a strictly higher (regular) round\nlet proposal = BlockProposal::new_retry_fast(owner, Round::MultiLeader(0), block, fast_sig, &signer).await?;","handlingStrategy":"validation","validationCode":"// Before sending a fast-retry proposal, mirror the invariant:\nif matches!(proposal.original_proposal, Some(OriginalProposal::Fast(_)))\n    && proposal.content.outcome.is_none()\n    && proposal.content.round <= Round::Fast\n{\n    anyhow::bail!(\"retry round must be > Round::Fast; use a regular round\");\n}\n// or simply: proposal.check_invariants()?;","typeGuard":"fn fast_retry_round_valid(p: &BlockProposal) -> bool {\n    !matches!(&p.original_proposal, Some(OriginalProposal::Fast(_)))\n        || p.content.outcome.is_some()\n        || p.content.round > Round::Fast\n}","tryCatchPattern":"match result {\n    Err(WorkerError::InvalidBlockProposal(msg)) if msg.contains(\"greater than the original\") => {\n        // bump the round past the original (Fast -> regular round) and re-sign\n    }\n    other => other?,\n}","preventionTips":["Build retries only via BlockProposal::new_retry_fast / new_retry_regular","Advance rounds using the timeout machinery, never hardcoded round numbers","Call proposal.check_invariants() locally before sending"],"tags":["linera","round","proposal","retry","consensus","rust"],"backgroundTag":"round-must-increase","analyzedSha":"6c226ddcb332ef55118dc8d0aafbd093d5420899","analyzedAt":"2026-08-22T22:49:09.787Z","schemaVersion":2},"datasetVersion":"2026-08-23T01:17:44.959Z"}