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

  1. Use the value verified by alloy_trie proof verification (the leaf value at key tx_index) as the receipt RLP
  2. Re-encode fixtures with alloy_consensus Receipt types instead of hand-written RLP
  3. 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

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


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