linera-io/linera-protocol · error · ChainError

Chain is expecting a next block at height {expected_block_he

Error message

Chain is expecting a next block at height {expected_block_height} but the given block is at height {found_block_height} instead

What it means

ChainTipState::verify_block_chaining (linera-chain/src/chain.rs:412) requires a proposed block's height to equal the chain tip's next_block_height exactly — blocks must extend the chain contiguously, no gaps and no replays. The validator rejects the whole proposal with UnexpectedBlockHeight via try_handle_block_proposal (linera-core/src/chain_worker/state.rs:2524), and process_validated_block re-checks it (state.rs:883). Hitting it almost always means your view of the chain is stale: someone else's block at that height was already processed.

Source

Thrown at linera-chain/src/chain.rs:413

    /// no entry is queried before its first push; ones that leave the committee are pruned.
    pub exported_heights: RegisterView<C, NonCanonicalBTreeMap<ValidatorPublicKey, BlockHeight>>,
}

/// Block-chaining state.
#[cfg_attr(with_graphql, derive(async_graphql::SimpleObject))]
#[derive(Debug, Default, Clone, Eq, PartialEq, Serialize, Deserialize, Allocative)]
pub struct ChainTipState {
    /// Hash of the latest certified block in this chain, if any.
    pub block_hash: Option<CryptoHash>,
    /// Sequence number tracking blocks.
    pub next_block_height: BlockHeight,
}

impl ChainTipState {
    /// Checks that the proposed block is suitable, i.e. at the expected height and with the
    /// expected parent.
    pub fn verify_block_chaining(&self, new_block: &ProposedBlock) -> Result<(), ChainError> {
        ensure!(
            new_block.height == self.next_block_height,
            ChainError::UnexpectedBlockHeight {
                expected_block_height: self.next_block_height,
                found_block_height: new_block.height
            }
        );
        ensure!(
            new_block.previous_block_hash == self.block_hash,
            ChainError::UnexpectedPreviousBlockHash
        );
        Ok(())
    }

    /// Returns `true` if the validated block's height is below the tip height. Returns an error if
    /// it is higher than the tip.
    pub fn already_validated_block(&self, height: BlockHeight) -> Result<bool, ChainError> {
        ensure!(
            self.next_block_height >= height,

View on GitHub (pinned to 6c226ddcb3)

Solutions

  1. Re-fetch the chain's ChainInfoResponse and rebuild the block at the returned next_block_height, then resubmit
  2. On multi-owner chains, ensure only the current round's leader proposes, or handle the loss by re-deriving transactions on the new tip
  3. If a single-owner chain hits this, check for two processes using the same owner key and stop one
  4. In retry loops, always refresh height and parent hash together instead of assuming the previous values still hold

Example fix

// before: building from a cached height
let block = Block::new(chain_id, cached_height, cached_parent, transactions);
client.submit_block(block).await?;

// after: refresh the tip first
let info = client.chain_info(chain_id).await?;
let tip = info.info.chain_tip();
let block = ProposedBlock {
    height: tip.next_block_height,
    previous_block_hash: tip.block_hash,
    // ...
};
client.submit_block(block).await?;
Defensive patterns

Strategy: validation

Validate before calling

// Before submitting, make sure the height matches the chain's tip:
let info = client.chain_info(chain_id).await?;
let next = info.info.next_block_height; // tip's next_block_height
if block.height != next {
    anyhow::bail!(
        "block height {} != expected {}; refresh the tip and rebuild",
        block.height, next
    );
}

Try / catch

match result {
    Err(WorkerError::ChainError(ChainError::UnexpectedBlockHeight { expected_block_height, found_block_height })) => {
        // stale view: refresh chain info, rebuild at `expected_block_height`, resubmit ONCE
    }
    other => other?,
}

Prevention

When it happens

Trigger: Submitting a BlockProposal built from a cached ChainInfo while another block at the same height was already confirmed (concurrent proposers on a multi-owner chain); replaying an old proposal after the chain advanced; two clients sharing one owner key proposing in parallel.

Common situations: Client SDK caches chain height across successive block submissions instead of re-querying; multi-owner chains where another owner proposed first in the same round; a lagging node catching up and receiving proposals newer than its tip; tests replaying recorded proposals against a chain that has moved on.

Related errors


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