linera-io/linera-protocol · warning

The new proposal's round must be greater than the original's

Error message

The new proposal's round must be greater than the original's

What it means

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).

Source

Thrown at linera-chain/src/data_types/mod.rs:999

    }

    /// Returns the IDs of the blobs that are required or created by this proposal.
    pub fn expected_blob_ids(&self) -> impl Iterator<Item = BlobId> + '_ {
        self.content.block.published_blob_ids().into_iter().chain(
            self.content.outcome.iter().flat_map(|outcome| {
                outcome
                    .oracle_blob_ids()
                    .into_iter()
                    .chain(outcome.iter_created_blobs_ids())
            }),
        )
    }

    /// Checks that the original proposal, if present, matches the new one and has a higher round.
    pub fn check_invariants(&self) -> Result<(), &'static str> {
        match (&self.original_proposal, &self.content.outcome) {
            (None, None) => {}
            (Some(OriginalProposal::Fast(_)), None) => ensure!(
                self.content.round > Round::Fast,
                "The new proposal's round must be greater than the original's"
            ),
            (None, Some(_))
            | (Some(OriginalProposal::Fast(_)), Some(_))
            | (Some(OriginalProposal::Regular { .. }), None) => {
                return Err("Must contain a validation certificate if and only if \
                     it contains the execution outcome from a previous round");
            }
            (Some(OriginalProposal::Regular { certificate }), Some(outcome)) => {
                ensure!(
                    self.content.round > certificate.round,
                    "The new proposal's round must be greater than the original's"
                );
                let block = outcome.clone().with(self.content.block.clone());
                let value = ValidatedBlock::new(block);
                ensure!(
                    certificate.check_value(&value),

View on GitHub (pinned to 6c226ddcb3)

Solutions

  1. Build retries with BlockProposal::new_retry_fast, which sets a valid higher round
  2. When constructing manually, move to a regular round after fast fails, e.g. Round::MultiLeader(0) or the round certified by the timeout
  3. Validate the round invariant client-side before sending, mirroring check_invariants

Example fix

// before: retrying fast with the same round
let proposal = BlockProposal { content: ProposalContent { block, round: Round::Fast, outcome: None },
    original_proposal: Some(OriginalProposal::Fast(fast_sig)), signature };

// after: retry in a strictly higher (regular) round
let proposal = BlockProposal::new_retry_fast(owner, Round::MultiLeader(0), block, fast_sig, &signer).await?;
Defensive patterns

Strategy: validation

Validate before calling

// Before sending a fast-retry proposal, mirror the invariant:
if matches!(proposal.original_proposal, Some(OriginalProposal::Fast(_)))
    && proposal.content.outcome.is_none()
    && proposal.content.round <= Round::Fast
{
    anyhow::bail!("retry round must be > Round::Fast; use a regular round");
}
// or simply: proposal.check_invariants()?;

Type guard

fn fast_retry_round_valid(p: &BlockProposal) -> bool {
    !matches!(&p.original_proposal, Some(OriginalProposal::Fast(_)))
        || p.content.outcome.is_some()
        || p.content.round > Round::Fast
}

Try / catch

match result {
    Err(WorkerError::InvalidBlockProposal(msg)) if msg.contains("greater than the original") => {
        // bump the round past the original (Fast -> regular round) and re-sign
    }
    other => other?,
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


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