linera-io/linera-protocol · error
receipt must be an RLP list
Error message
receipt must be an RLP list
What it means
After optionally stripping the EIP-2718 type byte, decode_receipt_logs decodes the outer RLP header and requires it to be a list: a legacy/typed receipt encodes as an RLP list of [status, cumulativeGasUsed, bloom, logs]. Well-formed RLP that is a string (or garbage whose first bytes decode as a string header) fails here.
Source
Thrown at linera-bridge/src/proof/mod.rs:386
/// Handles EIP-2718 typed receipts (type byte prefix < 0x80).
///
/// This variant exists because when tests build MPT tries
/// with multiple receipts (e.g. test_build_receipt_proof_multiple_receipts),
/// each receipt needs a distinct cumulative_gas_used to produce different
/// RLP encodings. Without that, all empty-log receipts would be byte-identical,
/// making the trie degenerate.
pub fn decode_receipt_logs(receipt_rlp: &[u8]) -> Result<Vec<ReceiptLog>> {
ensure!(!receipt_rlp.is_empty(), "empty receipt RLP");
let mut data: &[u8] = receipt_rlp;
// EIP-2718: if first byte < 0x80, it's a transaction type prefix
if data[0] < 0x80 {
data = &data[1..];
}
let list_header =
alloy_rlp::Header::decode(&mut data).map_err(|e| anyhow!("invalid receipt RLP: {e}"))?;
ensure!(list_header.list, "receipt must be an RLP list");
// Limit reads to the receipt's declared payload.
ensure!(
data.len() >= list_header.payload_length,
"receipt payload extends past available data"
);
let mut data = &data[..list_header.payload_length];
// Skip: status (0), cumulative_gas_used (1), logs_bloom (2)
for i in 0..3 {
skip_rlp_item(&mut data).map_err(|e| anyhow!("failed to skip receipt field {i}: {e}"))?;
}
// Decode the logs list
let logs_header =
alloy_rlp::Header::decode(&mut data).map_err(|e| anyhow!("invalid logs list RLP: {e}"))?;
ensure!(logs_header.list, "logs must be an RLP list");
ensure!(View on GitHub (pinned to 6c226ddcb3)
Solutions
- Use the value verified by alloy_trie proof verification (the leaf value at key tx_index) as the receipt RLP
- Re-encode fixtures with alloy_consensus Receipt types instead of hand-written RLP
- Confirm the EIP-2718 prefix handling: type byte only stripped when first byte < 0x80
Defensive patterns
Strategy: validation
Validate before calling
// Structural pre-check: outer item after optional type byte must be an RLP list
let d = if bytes[0] < 0x80 { &bytes[1..] } else { bytes };
let (h, _) = alloy_rlp::Header::decode(d)?;
anyhow::ensure!(h.list, "not a receipt: outer item is an RLP string");
let logs = decode_receipt_logs(bytes)?; Try / catch
match decode_receipt_logs(bytes) {
Ok(logs) => logs,
Err(e) if e.to_string().contains("receipt must be an RLP list") => {
anyhow::bail!("input is not a receipt RLP; check proof leaf value extraction")
}
Err(e) => return Err(e),
} Prevention
- Use the exact leaf value returned by verify_proof as the receipt RLP
- Encode fixtures with alloy_consensus Receipt types, never hand-rolled bytes
- Keep type-byte stripping consistent with EIP-2718 (only when < 0x80)
When it happens
Trigger: Passing a single receipt field (e.g. just the bloom or logs) instead of the whole receipt; passing a trie node or transaction where the receipt belongs; input where the type byte was stripped incorrectly.
Common situations: Extracting the wrong element from the proof's verified value; test vectors that RLP-encode a bare bytes value; mixing up legacy and typed receipt encodings when hand-assembling fixtures.
Related errors
- invalid log RLP: {e}
- block header must be an RLP list
- empty receipt RLP
- receipt payload extends past available data
- logs must be an RLP list
AI-assisted analysis of linera-io/linera-protocol@6c226ddcb3 (2026-08-22).
Data as JSON: /api/errors/65ceefb33106ab9b.
Report an issue: GitHub.