linera-io/linera-protocol · error

invalid topic: {e}

Error message

invalid topic: {e}

What it means

One of the entries inside the topics list did not decode as a B256 (32-byte) value. Every Ethereum topic must be an RLP string of exactly 32 bytes; anything else — wrong length, a list, or a truncated tail — fails here.

Source

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

    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)
        .map_err(|e| anyhow!("invalid log data RLP: {e}"))?;
    ensure!(
        !data_header.list,
        "log data must be a byte string, not a list"
    );
    ensure!(
        log_data_buf.len() >= data_header.payload_length,
        "log data extends past log boundary"
    );
    let log_bytes = log_data_buf[..data_header.payload_length].to_vec();
    log_data_buf = &log_data_buf[data_header.payload_length..];

    ensure!(

View on GitHub (pinned to 6c226ddcb3)

Solutions

  1. Pre-validate with alloy_primitives::Log::decode, which enforces 32-byte topics exactly.
  2. Inspect the failing topic's RLP prefix: 0xa0 means a 32-byte string — anything else indicates a malformed topic.
  3. Fix the event-encoding side so indexed parameters are abi.encode()-padded to 32 bytes.
  4. Rebuild the receipt proof from the chain.

Example fix

// before: every remaining byte-range is treated as a topic
let topic = <B256 as alloy_rlp::Decodable>::decode(&mut topics_data)
    .map_err(|e| anyhow!("invalid topic: {e}"))?;

// after: only accept logs whose topics are canonical 32-byte words
use alloy_rlp::Decodable;
anyhow::ensure!(
    alloy_primitives::Log::decode(&mut &log_entry_bytes[..]).is_ok(),
    "non-32-byte topic in log — rejecting entry"
);
Defensive patterns

Strategy: validation

Validate before calling

use alloy_rlp::Decodable;
fn topics_are_canonical(rlp: &[u8]) -> bool {
    alloy_primitives::Log::decode(&mut &rlp[..]).is_ok() // every topic must be 32 bytes
}

Try / catch

Err(e) if e.to_string().contains("invalid topic") => reject_proof(e), // permanent data error

Prevention

When it happens

Trigger: decode_log iterating topics_data: a topic shorter/longer than 32 bytes, a nested list where a string was expected, or a topics payload whose boundary leaves a partial topic at the end.

Common situations: Producers that encode topics with variable-length integers instead of fixed 32-byte words; a topic count/payload length mismatch in hand-built fixtures; bytes shifted by one after an off-by-one in the topics header.

Related errors


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