linera-io/linera-protocol · error

empty receipt RLP

Error message

empty receipt RLP

What it means

decode_receipt_logs refuses zero-length input up front: an empty byte slice cannot contain a transaction-type prefix, an RLP header, or the receipt fields, so all later indexing (data[0]) would panic. The check converts that into a clean error.

Source

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

        "proof too large: {total_bytes} bytes (max {MAX_PROOF_BYTES})",
    );

    let key = receipt_trie_key(tx_index);
    alloy_trie::proof::verify_proof(receipts_root, key, Some(receipt_rlp.to_vec()), proof_nodes)
        .map_err(|e| anyhow!("MPT proof verification failed: {e}"))
}

/// Decodes a receipt's RLP and extracts its logs.
///
/// 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];

View on GitHub (pinned to 6c226ddcb3)

Solutions

  1. Check the source of the slice: log the tx_index and proof payload lengths before decoding
  2. If a receipt legitimately has no logs, still pass its real RLP (status/cumulative_gas/bloom present) — the RLP is never empty for a mined transaction
  3. Fix upstream slicing so the receipt bytes are passed in full

Example fix

// before
let logs = decode_receipt_logs(&receipt_opt.unwrap_or_default())?; // Vec::new() -> error

// after
let rlp = receipt_opt.as_deref().ok_or_else(|| anyhow::anyhow!("no receipt for tx {tx_index}"))?;
let logs = decode_receipt_logs(rlp)?;
Defensive patterns

Strategy: validation

Validate before calling

// Guard the input before decoding
if receipt_rlp.is_empty() {
    anyhow::bail!("no receipt bytes for tx index {tx_index}; proof value missing?");
}
let logs = decode_receipt_logs(receipt_rlp)?;

Try / catch

match decode_receipt_logs(bytes) {
    Ok(logs) => logs,
    Err(e) if e.to_string().contains("empty receipt RLP") => {
        Vec::new() // treat missing receipt as no logs, if that is valid for your flow
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Calling decode_receipt_logs(&[]) or on a slice emptied by earlier processing: a receipt value absent from the trie proof (verify_proof with None), a splitting/offset bug leaving a 0-byte remainder, or an RPC returning empty receipt data.

Common situations: Bridge code decoding a receipt whose RLP was stored as empty when the tx had no logs; upstream code that passes an Option's default Vec::new(); off-by-one slicing when extracting the receipt from a concatenated blob.

Related errors


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