linera-io/linera-protocol · error

log data must be a byte string, not a list

Error message

log data must be a byte string, not a list

What it means

The third field of an Ethereum log is the arbitrary data byte string, and decode_log enforces at linera-bridge/src/proof/mod.rs:587 that its header declares a string (!list). A list header at that position means the bytes do not follow the log schema (address, topics list, data string) — a malformed or non-log item reached the data-field position.

Source

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

    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!(
        log_data_buf.is_empty(),
        "trailing data after log fields ({} unexpected bytes)",
        log_data_buf.len()
    );

    Ok(ReceiptLog {
        address,

View on GitHub (pinned to 6c226ddcb3)

Solutions

  1. Verify the topics payload was consumed exactly (topics_data emptied by B256 decodes) before reading the data header — a leftover topic shifts the cursor onto the wrong item.
  2. Hex-dump the data-field position: a prefix >= 0xC0 is a list header and proves schema violation; expect 0x80 (empty) or string prefixes.
  3. Re-encode fixtures using Vec<u8>::encode so data is a string item.
  4. If a legitimate emitter encodes list-shaped data, that content is opaque event bytes — it still must be a single RLP string at the receipt layer, so fix the producer.

Example fix

// before (fixture): log data encoded as a list
vec![vec![1u8, 2, 3]].encode(&mut buf);

// after: log data must be a byte string
vec![1u8, 2, 3].encode(&mut buf);
Defensive patterns

Strategy: try-catch

Try / catch

match decode_receipt_logs(receipt_rlp) {
    Ok(logs) => logs,
    Err(e) if e.to_string().contains("log data must be a byte string") => {
        tracing::warn!("log data field malformed; rejecting node");
        Vec::new()
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: A crafted log whose data field is RLP-encoded as a list (e.g., someone encoded a struct there); a misaligned slice where the previous topics payload was under-consumed so decoding resumes on a nested list; test fixtures encoding data with a helper that wraps content in a list.

Common situations: Hand-rolled fixture encoders; receipts from custom chains or tooling that pack structured data as a list in the data slot; off-by-N consumption of the topics list leaving the cursor inside another list.

Related errors


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