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
- Check the source of the slice: log the tx_index and proof payload lengths before decoding
- 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
- 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
- Propagate Option/Result from proof extraction instead of defaulting to empty slices
- Assert non-empty inputs at the boundary where bytes enter from RPC/relayers
- Add unit tests for empty input so behavior is pinned
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
- receipt must be an RLP list
- receipt payload extends past available data
- logs must be an RLP list
- logs payload extends past receipt boundary
- invalid log RLP: {e}
AI-assisted analysis of linera-io/linera-protocol@6c226ddcb3 (2026-08-22).
Data as JSON: /api/errors/77cfd5f4cf1dfc67.
Report an issue: GitHub.