linera-io/linera-protocol · critical · WorkerError
InvalidBlockChaining
InvalidBlockChaining
Error message
The block does not contain the hash that we expected for the previous block
What it means
Linera only executes a confirmed block that extends the exact chain tip supplied with the request: execute_contiguous_block verifies tip.block_hash == block.header.previous_block_hash before touching any state. InvalidBlockChaining means the certificate's parent hash is not the tip the caller reported, so the block would fork or skip a height. For certificates produced by a correctly synchronized client this never happens; it signals a stale, out-of-order, or conflicting submission.
Source
Thrown at linera-core/src/chain_worker/state.rs:1283
) -> Result<(ChainInfoResponse, NetworkActions, BlockOutcome), WorkerError> {
// Cached only when export is on: the queue holds the shared pointer, and inserting on
// every executed block would otherwise churn the dedup cache for nothing.
let (cached, plain) = if self.block_export.is_some() {
(Some(self.storage.cache_certificate(certificate)), None)
} else {
(None, Some(certificate))
};
let certificate = cached
.as_deref()
.or(plain.as_ref())
.expect("exactly one of the two is set");
let block_hash = certificate.hash();
let block = certificate.block();
let chain_id = block.header.chain_id;
let height = block.header.height;
// This should always be true for valid certificates.
ensure!(
tip.block_hash == block.header.previous_block_hash,
WorkerError::InvalidBlockChaining
);
// Verify that the chain is active and that the epoch we used for verifying
// the certificate is actually the active one on the chain.
self.initialize_and_save_if_needed().await?;
let (epoch, _) = self.chain.current_committee().await?;
check_block_epoch(epoch, chain_id, block.header.epoch)?;
// The chain is initialized and this block has not executed yet, so the current ownership
// is the configuration the block was proposed under — even for the chain's first block,
// whose ownership comes from the just-applied chain description. This is the point where
// the first-round attestation can be checked against the actual first round; blocks that
// are only preprocessed skip it and rely on the nodes that execute the chain in order.
if certificate.first_round() {
ensure!(
certificate.round() == self.chain.ownership().await?.first_round(),View on GitHub (pinned to 6c226ddcb3)
Solutions
- Re-query the chain (fresh ChainInfoResponse) and resubmit the certificate with the tip from that response.
- Process confirmed blocks strictly in height order: execute the parent block first, then its child.
- Ensure a single writer confirms blocks per chain; serialize concurrent clients with a per-chain lock.
- If it persists with a single writer, compare block.header.previous_block_hash with the tip hash by hand and check for a fork or corrupted storage.
Example fix
// before: stale cached tip let tip = cached_tip; worker.process_confirmed_block(cert, blobs, tip, None).await?; // after: pair the certificate with the validator's current tip let info = client.request_chain_info(chain_id).await?; let tip = *info.info.tip_state(); worker.process_confirmed_block(cert, blobs, tip, None).await?;
Defensive patterns
Strategy: retry
Validate before calling
// Before submitting, verify the tip still matches the parent block.
let info = client.request_chain_info(chain_id).await?;
let tip = *info.info.tip_state();
if tip.block_hash != cert.block().header.previous_block_hash {
// Fetch and execute the missing parent blocks first, then resubmit.
} Try / catch
match worker.process_confirmed_block(cert, blobs, tip, None).await {
Err(WorkerError::InvalidBlockChaining) => {
// Tip moved on: re-query and retry once with the fresh tip.
let info = client.request_chain_info(chain_id).await?;
worker.process_confirmed_block(cert, blobs, *info.info.tip_state(), None).await
}
result => result,
} Prevention
- Never cache ChainTipState across confirmations; fetch it in the same step as the submission.
- Confirm blocks in ascending height order, one at a time.
- Run one confirming client per chain, or wrap confirmations in a per-chain lock.
- Treat InvalidBlockChaining as a sync signal, not a permanent failure: resynchronize before retrying.
When it happens
Trigger: Calling process_confirmed_block or execute_block_with_checkpoint_restore with a ChainTipState fetched before the parent block was executed; submitting the certificate for height N+1 while the validator's tip is older or newer; two clients concurrently confirming blocks on the same chain; replaying an old certificate after the chain advanced.
Common situations: A custom client or off-chain worker that caches ChainInfo across confirmations; concurrent writers on one chain; a validator restored from an older snapshot while the client kept a newer tip; certificates processed out of height order.
Related errors
- CannotRejectMessage
- FalseFirstRoundAttestation
- UnexpectedBlob
- InvalidOwner
- Fast blocks cannot query oracles
AI-assisted analysis of linera-io/linera-protocol@6c226ddcb3 (2026-08-22).
Data as JSON: /api/errors/225334836650b404.
Report an issue: GitHub.