linera-io/linera-protocol · error
log must be an RLP list
Error message
log must be an RLP list
What it means
decode_log decodes one Ethereum log entry, which per the receipts specification must be an RLP list of exactly three fields: address, topics list, data byte string. The ensure at linera-bridge/src/proof/mod.rs:552 fails when the outer item's header declares a string (list flag false) instead of a list. That means the bytes being parsed as a log are not a log at all — usually the wrong slice of a trie node or malformed RLP.
Source
Thrown at linera-bridge/src/proof/mod.rs:552
/// 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!(
log_data_buf.len() >= topics_header.payload_length,View on GitHub (pinned to 6c226ddcb3)
Solutions
- Hex-dump the first bytes handed to decode_log; a leading byte below 0xC0 is a string header and proves the slice is misaligned or the node is not a log.
- Verify the receipt proof (build_receipt_proof / trie verification) against the block header's logs/receipts root before attempting to decode logs.
- Check that iteration over logs_data starts exactly at the first log item after the logs-list header, not after the receipt's skipped fields.
- Re-fetch the block/receipt from a trusted provider and compare bytes to detect provider-side corruption.
Example fix
// before
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");
// after: distinguish string vs list for clearer diagnostics
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; got a byte string of payload {} at offset — node is not a log", log_header.payload_length); Defensive patterns
Strategy: try-catch
Validate before calling
fn next_item_is_list(data: &[u8]) -> bool {
alloy_rlp::Header::decode(&mut &data[..])
.map(|h| h.list)
.unwrap_or(false)
} Try / catch
match decode_receipt_logs(receipt_rlp) {
Ok(logs) => logs,
Err(e) if e.to_string().contains("log must be an RLP list") => {
tracing::warn!("proof node is not a log entry; re-fetching receipt");
Vec::new()
}
Err(e) => return Err(e),
} Prevention
- Validate trie proofs before decoding contents; a wrong node usually surfaces as schema errors like this.
- Keep cursor arithmetic (headers, payload slices) centralized instead of manual index math.
- Prefer library RLP decoding over hand-parsed byte offsets for nested structures.
When it happens
Trigger: decode_receipt_logs iterates the logs list and hands decode_log a slice that starts at a non-list item: a trie node delivered in place of the receipt, a hash-reference where inline data was expected, or a fixture that RLP-encodes a log as a flat string.
Common situations: Merkle proof returns an extension/branch node instead of the leaf containing the receipt; receipt bytes offset by one so decoding starts on a nested string; a node implementation or test helper that encodes logs inconsistently with yellow-paper rules.
Related errors
- not enough data to skip RLP item
- log payload extends past available data
- topics must be an RLP list
- topics payload extends past log boundary
- log data must be a byte string, not a list
AI-assisted analysis of linera-io/linera-protocol@6c226ddcb3 (2026-08-22).
Data as JSON: /api/errors/9bcaddbb4daccdab.
Report an issue: GitHub.