linera-io/linera-protocol · error

block header must be an RLP list

Error message

block header must be an RLP list

What it means

decode_block_header RLP-decodes the outer item of an Ethereum block header and requires it to be a list (headers are a 15-field RLP list). If alloy_rlp::Header decodes successfully but reports a string (single value) instead, this ensure! fails — the input bytes are well-formed RLP but not a block header.

Source

Thrown at linera-bridge/src/proof/mod.rs:306

        receipts: &[(u64, Vec<u8>)],
        target_tx_index: u64,
    ) -> (B256, Vec<Bytes>) {
        let (root, proof) = super::build_receipt_proof(receipts, target_tx_index);
        (root, proof.into_iter().map(Into::into).collect())
    }
}

/// Decodes an RLP-encoded Ethereum block header, returning `(block_hash, receipts_root)`.
///
/// The block hash is `keccak256(header_rlp)`. The receipts root is field index 5
/// in the RLP list.
pub fn decode_block_header(header_rlp: &[u8]) -> Result<(B256, B256)> {
    let block_hash = keccak256(header_rlp);

    let mut data = header_rlp;
    let list_header =
        alloy_rlp::Header::decode(&mut data).map_err(|e| anyhow!("invalid RLP header: {e}"))?;
    ensure!(list_header.list, "block header must be an RLP list");

    // Limit reads to the header's declared payload.
    ensure!(
        data.len() >= list_header.payload_length,
        "block header payload extends past available data"
    );
    let mut data = &data[..list_header.payload_length];

    // Skip first 5 fields: parentHash, ommersHash, beneficiary, stateRoot, transactionsRoot
    for i in 0..5 {
        skip_rlp_item(&mut data).map_err(|e| anyhow!("failed to skip header field {i}: {e}"))?;
    }

    // Field index 5: receiptsRoot (B256)
    let receipts_root = <B256 as alloy_rlp::Decodable>::decode(&mut data)
        .map_err(|e| anyhow!("failed to decode receipts_root: {e}"))?;

    Ok((block_hash, receipts_root))

View on GitHub (pinned to 6c226ddcb3)

Solutions

  1. Verify you are passing the raw RLP-encoded block header bytes, not its hash, the header JSON, or another trie node
  2. Cross-check against a known-good source: eth_getBlockByHash(..., false). Then re-encode header.raw and compare bytes
  3. For test vectors, build the header as an RLP list of all 15 fields (alloy_primitives::Header struct serializes correctly)

Example fix

// before
let (hash, root) = decode_block_header(&block_hash_bytes)?; // passing the hash -> error

// after
let (hash, root) = decode_block_header(&header_rlp_bytes)?; // full header RLP list
Defensive patterns

Strategy: validation

Validate before calling

// Sanity-check the source before decoding
assert_eq!(header_rlp.first(), Some(&0xf9), "block header RLP must start with a list header (0xf9)");
let (hash, root) = decode_block_header(header_rlp)?;

Try / catch

match decode_block_header(bytes) {
    Ok(pair) => pair,
    Err(e) if e.to_string().contains("must be an RLP list") => {
        anyhow::bail!("input is not a block header (got a non-list RLP item); verify RPC field")
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Passing a receipt, transaction, or arbitrary RLP string where a block header is expected; feeding truncated data whose first bytes happen to decode as an RLP string header; wrong endianness or double-hashed input.

Common situations: Indexer/bridge code fetching headers via an RPC proxy that returns the wrong field (e.g. transactionsRoot instead of the header); hand-built test vectors that RLP-encode a flat byte array rather than a field list; feeding the keccak256 hash of the header rather than the header itself.

Related errors


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