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
- Verify you are passing the raw RLP-encoded block header bytes, not its hash, the header JSON, or another trie node
- Cross-check against a known-good source: eth_getBlockByHash(..., false). Then re-encode header.raw and compare bytes
- 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
- Fetch headers from a trusted RPC endpoint and pass header.raw bytes unchanged
- Never substitute the header's keccak hash or the JSON representation for the RLP bytes
- Build test vectors with alloy's Header type so they are lists by construction
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
- block header payload extends past available data
- receipt must be an RLP list
- invalid log RLP: {e}
- invalid log address: {e}
- invalid topics list RLP: {e}
AI-assisted analysis of linera-io/linera-protocol@6c226ddcb3 (2026-08-22).
Data as JSON: /api/errors/6033e719c65b57f7.
Report an issue: GitHub.