{"record":{"id":"225334836650b404","repo":"linera-io/linera-protocol","slug":"invalidblockchaining","errorCode":"InvalidBlockChaining","errorMessage":"The block does not contain the hash that we expected for the previous block","messagePattern":"The block does not contain the hash that we expected for the previous block","errorType":"validation","errorClass":"WorkerError","httpStatus":null,"severity":"critical","filePath":"linera-core/src/chain_worker/state.rs","lineNumber":1283,"sourceCode":"    ) -> Result<(ChainInfoResponse, NetworkActions, BlockOutcome), WorkerError> {\n        // Cached only when export is on: the queue holds the shared pointer, and inserting on\n        // every executed block would otherwise churn the dedup cache for nothing.\n        let (cached, plain) = if self.block_export.is_some() {\n            (Some(self.storage.cache_certificate(certificate)), None)\n        } else {\n            (None, Some(certificate))\n        };\n        let certificate = cached\n            .as_deref()\n            .or(plain.as_ref())\n            .expect(\"exactly one of the two is set\");\n        let block_hash = certificate.hash();\n        let block = certificate.block();\n        let chain_id = block.header.chain_id;\n        let height = block.header.height;\n\n        // This should always be true for valid certificates.\n        ensure!(\n            tip.block_hash == block.header.previous_block_hash,\n            WorkerError::InvalidBlockChaining\n        );\n\n        // Verify that the chain is active and that the epoch we used for verifying\n        // the certificate is actually the active one on the chain.\n        self.initialize_and_save_if_needed().await?;\n        let (epoch, _) = self.chain.current_committee().await?;\n        check_block_epoch(epoch, chain_id, block.header.epoch)?;\n\n        // The chain is initialized and this block has not executed yet, so the current ownership\n        // is the configuration the block was proposed under — even for the chain's first block,\n        // whose ownership comes from the just-applied chain description. This is the point where\n        // the first-round attestation can be checked against the actual first round; blocks that\n        // are only preprocessed skip it and rely on the nodes that execute the chain in order.\n        if certificate.first_round() {\n            ensure!(\n                certificate.round() == self.chain.ownership().await?.first_round(),","sourceCodeStart":1265,"sourceCodeEnd":1301,"githubUrl":"https://github.com/linera-io/linera-protocol/blob/6c226ddcb332ef55118dc8d0aafbd093d5420899/linera-core/src/chain_worker/state.rs#L1265-L1301","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before: stale cached tip\nlet tip = cached_tip;\nworker.process_confirmed_block(cert, blobs, tip, None).await?;\n\n// after: pair the certificate with the validator's current tip\nlet info = client.request_chain_info(chain_id).await?;\nlet tip = *info.info.tip_state();\nworker.process_confirmed_block(cert, blobs, tip, None).await?;","handlingStrategy":"retry","validationCode":"// Before submitting, verify the tip still matches the parent block.\nlet info = client.request_chain_info(chain_id).await?;\nlet tip = *info.info.tip_state();\nif tip.block_hash != cert.block().header.previous_block_hash {\n    // Fetch and execute the missing parent blocks first, then resubmit.\n}","typeGuard":null,"tryCatchPattern":"match worker.process_confirmed_block(cert, blobs, tip, None).await {\n    Err(WorkerError::InvalidBlockChaining) => {\n        // Tip moved on: re-query and retry once with the fresh tip.\n        let info = client.request_chain_info(chain_id).await?;\n        worker.process_confirmed_block(cert, blobs, *info.info.tip_state(), None).await\n    }\n    result => result,\n}","preventionTips":["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."],"tags":["rust","linera","blockchain","consensus","chain-tip","ordering"],"backgroundTag":"previous-block-hash-mismatch","analyzedSha":"6c226ddcb332ef55118dc8d0aafbd093d5420899","analyzedAt":"2026-08-22T22:49:09.787Z","schemaVersion":2},"datasetVersion":"2026-08-23T01:17:44.959Z"}