linera-io/linera-protocol · warning · ProofError

block not found for hash {block_hash}

Error message

block not found for hash {block_hash}

What it means

eth_getBlockByHash returned null for the block hash taken from the receipt. Classified ProofError::Transient. The RPC node does not know this block: most commonly the block was pruned (node keeps limited history), is on a reorged-out branch, or the node is not synced to that height.

Source

Thrown at linera-bridge/src/proof/gen.rs:120

                    "transaction receipt not found for {tx_hash}"
                ))
            })?;

        let block_hash = receipt.inner.block_hash.ok_or_else(|| {
            ProofError::Transient(anyhow::anyhow!("receipt missing block_hash (pending tx?)"))
        })?;
        let tx_index = receipt.inner.transaction_index.ok_or_else(|| {
            ProofError::Transient(anyhow::anyhow!("receipt missing transaction_index"))
        })?;

        // 2. Get full block → header RLP
        let block = self
            .provider
            .get_block_by_hash(block_hash)
            .await
            .map_err(|e| ProofError::Transient(e.into()))?
            .ok_or_else(|| {
                ProofError::Transient(anyhow::anyhow!("block not found for hash {block_hash}"))
            })?;

        let mut block_header_rlp = Vec::new();
        block.header.inner.encode(&mut block_header_rlp);

        // Sanity check: header RLP hashes to the expected block hash
        let computed_hash = alloy_primitives::keccak256(&block_header_rlp);
        if computed_hash != block_hash {
            return Err(ProofError::Permanent(anyhow::anyhow!(
                "header RLP hash mismatch: computed {computed_hash}, expected {block_hash}. \
                 This may indicate the RPC returned non-standard header fields."
            )));
        }

        // 3. Get all block receipts
        let all_receipts = self
            .provider
            .get_block_receipts(block.header.number.into())

View on GitHub (pinned to 6c226ddcb3)

Solutions

  1. Use an archive or full-history RPC endpoint that serves the deposit's block
  2. Generate proofs for recent transactions before they age out of the node's history
  3. Retry once to rule out transient routing; if consistently null, the history is pruned
  4. Verify the block hash on an explorer and compare with the block number the RPC can serve
Defensive patterns

Strategy: retry

Validate before calling

// Preflight: check the RPC can serve the block for this receipt.
let receipt = provider.get_transaction_receipt(tx_hash).await?.expect("mined");
let bh = receipt.inner.block_hash.expect("confirmed");
if provider.get_block_by_hash(bh).await.ok().flatten().is_none() {
    // history pruned: switch to an archive RPC before generating proofs
}

Try / catch

Err(ProofError::Transient(e)) if e.to_string().contains("block not found") => {
    // once retried: if still failing, history is pruned — move to an archive endpoint
}

Prevention

When it happens

Trigger: Calling generate_deposit_proof for an older deposit against a pruned node (public/free Base endpoints often keep only recent blocks); a reorg between fetching the receipt and fetching the block; an archive gap on a load-balanced cluster.

Common situations: Free or default RPC endpoints with short block-history retention; batch-processing historical deposits; RPC providers that route eth_getBlockByHash to nodes with different prunes.

Related errors


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