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
- Re-fetch the chain's ChainInfoResponse and rebuild the block at the returned next_block_height, then resubmit
- On multi-owner chains, ensure only the current round's leader proposes, or handle the loss by re-deriving transactions on the new tip
- If a single-owner chain hits this, check for two processes using the same owner key and stop one
- 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
- Re-query next_block_height for every new proposal; never cache it across submissions
- On multi-owner chains, propose only in rounds you lead
- Keep (height, parent_hash) as one atomic snapshot of the tip
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
- The previous block hash of a new block should match the last
- no signer found for owner ${owner}
- CannotRejectMessage
- Certificate justification commitment does not match its just
- Certificate unlocking round does not match the top of its ju
AI-assisted analysis of linera-io/linera-protocol@6c226ddcb3 (2026-08-22).
Data as JSON: /api/errors/0ddab96de0fdf9bc.
Report an issue: GitHub.