linera-io/linera-protocol · error
invalid topics list RLP: {e}
Error message
invalid topics list RLP: {e} What it means
After the emitter address was consumed, the next field — the topics list — does not begin with a parsable RLP header. In a canonical log, topics form an RLP list of 32-byte strings; if the bytes following the address are not a list header, the entry is malformed or the address decode consumed the wrong number of bytes.
Source
Thrown at linera-bridge/src/proof/mod.rs:567
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,
"topics payload extends past log boundary"
);
let mut topics_data = &log_data_buf[..topics_header.payload_length];
log_data_buf = &log_data_buf[topics_header.payload_length..];
let mut topics = Vec::new();
while !topics_data.is_empty() {
let topic = <B256 as alloy_rlp::Decodable>::decode(&mut topics_data)
.map_err(|e| anyhow!("invalid topic: {e}"))?;
topics.push(topic);
}
// Decode log data (byte string)
let data_header = alloy_rlp::Header::decode(&mut log_data_buf)View on GitHub (pinned to 6c226ddcb3)
Solutions
- Validate the full log entry with alloy_primitives::Log::decode first; rejection there means the bytes are not a canonical log.
- Verify the encoder wraps topics in an RLP list (outer header with list flag), not a concatenation of raw 32-byte strings.
- Check that payload_length from the outer log header actually covers address + topics + data.
- Regenerate the receipt proof from a trusted source.
Example fix
// before: topics parsed with a custom header read
let topics_header = alloy_rlp::Header::decode(&mut log_data_buf)
.map_err(|e| anyhow!("invalid topics list RLP: {e}"))?;
// after: gate the whole entry through the reference decoder before custom parsing
use alloy_rlp::Decodable;
anyhow::ensure!(
alloy_primitives::Log::decode(&mut &log_entry_bytes[..]).is_ok(),
"topics section is not a valid RLP list — rejecting log"
); Defensive patterns
Strategy: validation
Validate before calling
use alloy_rlp::Decodable;
fn log_has_valid_topics_list(rlp: &[u8]) -> bool {
alloy_primitives::Log::decode(&mut &rlp[..]).is_ok() // topics must be a list of B256
} Try / catch
Err(e) if e.to_string().contains("invalid topics list RLP") => reject_proof(e), // structural defect: no retry Prevention
- When building receipts, derive topics from alloy_sol_types event logs rather than manual RLP.
- Fuzz the decoder boundary so malformed topics never panic downstream.
When it happens
Trigger: decode_log with the address decoded but log_data_buf now starting mid-item: truncated payload after the address, topics encoded as a flat sequence instead of a nested list, or an address whose declared length did not match the canonical 20 bytes.
Common situations: Logs serialized by a tool that inlines topics without the outer list; payload_length that cuts off the topics header; fuzzed or hand-crafted proof inputs in tests.
Related errors
- invalid log RLP: {e}
- invalid log address: {e}
- invalid topic: {e}
- invalid log data RLP: {e}
- block header must be an RLP list
AI-assisted analysis of linera-io/linera-protocol@6c226ddcb3 (2026-08-22).
Data as JSON: /api/errors/c65f56802cf6248e.
Report an issue: GitHub.