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

  1. Re-query the chain (fresh ChainInfoResponse) and resubmit the certificate with the tip from that response.
  2. Process confirmed blocks strictly in height order: execute the parent block first, then its child.
  3. Ensure a single writer confirms blocks per chain; serialize concurrent clients with a per-chain lock.
  4. 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

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


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