linera-io/linera-protocol · error
invalid log RLP: {e}
Error message
invalid log RLP: {e} What it means
Thrown while decoding an Ethereum receipt log entry inside a bridge deposit proof: the very first RLP header of the entry cannot be parsed. decode_log walks the receipt's log list, and each entry must start with a well-formed RLP list header. Corrupt, truncated, or misaligned bytes at the cursor position fail here before any field is read.
Source
Thrown at linera-bridge/src/proof/mod.rs:551
/// Skips one RLP item (string or list) by reading its header and advancing past the payload.
fn skip_rlp_item(data: &mut &[u8]) -> Result<()> {
let header = alloy_rlp::Header::decode(data).map_err(|e| anyhow!("invalid RLP item: {e}"))?;
ensure!(
data.len() >= header.payload_length,
"not enough data to skip RLP item"
);
*data = &data[header.payload_length..];
Ok(())
}
/// Decodes a single log entry from RLP.
///
/// Enforces the declared payload boundary: after decoding address, topics, and data,
/// verifies that exactly `payload_length` bytes were consumed.
fn decode_log(data: &mut &[u8]) -> Result<ReceiptLog> {
let log_header =
alloy_rlp::Header::decode(data).map_err(|e| anyhow!("invalid log RLP: {e}"))?;
ensure!(log_header.list, "log must be an RLP list");
ensure!(
data.len() >= log_header.payload_length,
"log payload extends past available data"
);
// Limit reads to the declared payload boundary.
let mut log_data_buf = &data[..log_header.payload_length];
*data = &data[log_header.payload_length..];
let address = <Address as alloy_rlp::Decodable>::decode(&mut log_data_buf)
.map_err(|e| anyhow!("invalid log address: {e}"))?;
// Decode topics list
let topics_header = alloy_rlp::Header::decode(&mut log_data_buf)
.map_err(|e| anyhow!("invalid topics list RLP: {e}"))?;
ensure!(topics_header.list, "topics must be an RLP list");
ensure!(View on GitHub (pinned to 6c226ddcb3)
Solutions
- Decode the same receipt bytes with a reference implementation (e.g. alloy_consensus::Receipt or an RPC eth_getTransactionReceipt) to confirm they are canonical receipt RLP.
- Check the slicing/skipping logic that positions the cursor before the log loop (status byte, cumulativeGasUsed, logsBloom) for off-by-one errors.
- Re-fetch the receipt and Merkle proof from the EVM RPC and retry verification with fresh bytes.
- Confirm the proof was produced for the same chain/hardfork the light client tracks.
Example fix
// before: raw bytes from the proof are handed straight to the log decoder
let logs = decode_receipt_logs(&receipt_bytes)?; // fails: "invalid log RLP: ..."
// after: pre-validate the whole log with the reference decoder, reject the proof on mismatch
use alloy_rlp::Decodable;
if alloy_primitives::Log::decode(&mut &log_bytes[..]).is_err() {
anyhow::bail!("rejecting receipt proof: log entry is not canonical RLP");
}
let logs = decode_receipt_logs(&receipt_bytes)?; Defensive patterns
Strategy: validation
Validate before calling
// Cheap pre-check: the reference decoder must accept the whole log entry
// before the custom field-by-field proof parser runs.
use alloy_rlp::Decodable;
fn is_canonical_log(rlp: &[u8]) -> bool {
alloy_primitives::Log::decode(&mut &rlp[..]).is_ok()
} Type guard
fn is_canonical_log(rlp: &[u8]) -> bool {
use alloy_rlp::Decodable;
alloy_primitives::Log::decode(&mut &rlp[..]).is_ok()
} Try / catch
match decode_receipt_logs(&receipt_bytes) {
Ok(logs) => { /* proceed: find_deposit_log_indices, verify proof */ }
Err(e) if e.to_string().contains("invalid log") => {
// Malformed proof data: reject the proof; never retry with the same bytes.
return Ok(ProofOutcome::Rejected);
}
Err(e) => return Err(e),
} Prevention
- Only feed proof bytes obtained from verified Merkle proofs against the light client's receipts root.
- Round-trip test every producer's receipt encoding against alloy_primitives::Log::decode.
- Reject, don't retry: a decode failure means the bytes are bad, not that the system is busy.
When it happens
Trigger: decode_receipt_logs is called on receipt bytes where the cursor is not at the start of a valid RLP header: truncated proof payload, a receipt that is not canonical Ethereum receipt RLP ([status, cumulativeGasUsed, logsBloom, logs]), or an off-by-N cursor left by the preceding receipt-field decoding (status/bloom).
Common situations: Receipts re-encoded by a non-canonical RLP encoder; byte offsets shifted because the logsBloom or status field was skipped incorrectly; proofs fetched from an incompatible chain or hardfork; partially copied hex blobs (odd length, 0x prefix left in).
Related errors
- receipt must be an RLP list
- invalid receipt RLP: {e}
- failed to skip receipt field {i}: {e}
- invalid logs list RLP: {e}
- invalid RLP item: {e}
AI-assisted analysis of linera-io/linera-protocol@6c226ddcb3 (2026-08-22).
Data as JSON: /api/errors/abedde0a9861bbe7.
Report an issue: GitHub.