linera-io/linera-protocol · warning · ChainError

WrongRound

WrongRound

Error message

Round number should be {0:?}

What it means

In check_proposed_block, a MultiLeader(_) or SingleLeader(0) proposal is rejected while the chain's current round is still Round::Fast and the proposer is not a super owner. This protects the fast round from being preempted: ordinary owners must wait for the fast round to time out before opening a later round; only super owners may bypass it.

Source

Thrown at linera-chain/src/manager.rs:308

        if let Some(old_proposal) = self.proposed.get() {
            if old_proposal.content == proposal.content {
                return Ok(Outcome::Skip); // We have already seen this proposal; nothing to do.
            }
        }
        // When a block is certified, incrementing its height must succeed.
        ensure!(
            new_block.height < BlockHeight::MAX,
            ChainError::BlockHeightOverflow
        );
        let current_round = self.current_round();
        match new_round {
            // The proposal from the fast round may still be relevant as a locking block, so
            // we don't compare against the current round here.
            Round::Fast => {}
            Round::MultiLeader(_) | Round::SingleLeader(0) => {
                // If the fast round has not timed out yet, only a super owner is allowed to open
                // a later round by making a proposal.
                ensure!(
                    self.is_super(&proposal.owner()) || !current_round.is_fast(),
                    ChainError::WrongRound(current_round)
                );
                // After the fast round, proposals older than the current round are obsolete.
                ensure!(
                    new_round >= current_round,
                    ChainError::InsufficientRound(new_round)
                );
            }
            Round::SingleLeader(_) | Round::Validator(_) => {
                // After the first single-leader round, only proposals from the current round are relevant.
                ensure!(
                    new_round == current_round,
                    ChainError::WrongRound(current_round)
                );
            }
        }
        // The round of our validation votes is only allowed to increase.

View on GitHub (pinned to 6c226ddcb3)

Solutions

  1. Wait for the fast round to time out, fetch fresh chain info, and rebuild the proposal for the new current round
  2. Have a super owner sign and submit the proposal if the fast round genuinely must be preempted
  3. Re-query the chain (ChainInfoQuery / request_leader_timeout) to learn the current round and ownership before proposing

Example fix

// before: proposal built from stale state, submitted while fast round is open
let proposal = BlockProposal::new(Round::MultiLeader(0), block, ...);
client.submit_proposal(proposal).await?; // WrongRound(Fast)

// after: wait for the fast-round timeout, then propose in the fresh current round
let info = client.chain_info(chain_id).await?;
let round = wait_for_next_round(&client, info).await?; // uses request_leader_timeout
let proposal = BlockProposal::new(round, block, ...);
client.submit_proposal(proposal).await?;
Defensive patterns

Strategy: validation

Validate before calling

// Fetch fresh state before proposing in a post-fast round.
let info = client.chain_info(chain_id).await?;
let current = info.manager.current_round;
let is_super = info.manager.ownership.is_super_owner(&owner);
if current.is_fast() && !is_super {
    // Fast round still open: wait for its timeout instead of submitting.
    return wait_for_round_change(&client, chain_id).await;
}
let round = current.max(Round::MultiLeader(0));
let proposal = BlockProposal::new(round, block, /* ... */);

Type guard

fn is_wrong_round(e: &ChainError) -> bool {
    matches!(e, ChainError::WrongRound(_))
}

Try / catch

match client.submit_proposal(proposal).await {
    Err(e) if matches!(e, ref x if x.is_wrong_round()) => {
        // Re-query chain info and rebuild the proposal at the returned round.
    }
    other => other?,
}

Prevention

When it happens

Trigger: Calling try_handle_block_proposal with a MultiLeader(0) or higher MultiLeader proposal when manager.current_round() is Round::Fast and ownership does not list proposal.owner() as a super owner.

Common situations: Client built its proposal from stale chain info and missed that the chain is still in the fast round; a leader retrying immediately after block confirmation instead of waiting out the fast-round timeout; tests that skip round bookkeeping.

Related errors


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