nautechsystems/nautilus_trader · error

Topic must be at least 32 bytes, was {}

Error message

Topic must be at least 32 bytes, was {}

What it means

This error means the decoded topic bytes were shorter than 32 bytes when extracting an indexed address. Indexed Ethereum parameters occupy a full 32-byte topic, with addresses left-padded to occupy bytes 12..32. A shorter decode implies malformed hex (odd length, truncated string) or a corrupted topic value.

Source

Thrown at crates/adapters/blockchain/src/rpc/log.rs:147

    decode_hex(hex)
}

/// Extract address from topic at index.
///
/// In Ethereum event logs, indexed address parameters are stored as 32-byte
/// values with the 20-byte address right-aligned (padded with zeros on the left).
///
/// # Errors
///
/// Returns an error if the topic is missing or the address extraction fails.
pub fn extract_address_from_topic(
    log: &RpcLog,
    index: usize,
    description: &str,
) -> anyhow::Result<Address> {
    let bytes = extract_topic_bytes(log, index)
        .map_err(|_| anyhow::anyhow!("Missing {description} address in topic{index}"))?;
    anyhow::ensure!(
        bytes.len() >= 32,
        "Topic must be at least 32 bytes, was {}",
        bytes.len()
    );
    Ok(Address::from_slice(&bytes[12..32]))
}

/// Extract data bytes from RPC log.
///
/// # Errors
///
/// Returns an error if the hex decoding fails.
pub fn extract_data_bytes(log: &RpcLog) -> anyhow::Result<Vec<u8>> {
    decode_hex(&log.data)
}

/// Validate event signature from topic0.
///

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Inspect the raw topic hex; it should be 66 characters (0x + 64 hex digits).
  2. Reject or quarantine logs with malformed topics instead of unwrapping.
  3. If your data source legitimately produces shorter encodings, left-pad to 32 bytes before extraction.
  4. Verify no intermediate transformation is trimming/hashing the topics.

Example fix

// before
let addr = extract_address_from_topic(&log, 1, "token0")?;
// after
let ok = log.topics.get(1).map_or(false, |t| t.trim_start_matches("0x").len() == 64);
anyhow::ensure!(ok, "malformed topic: {:?}", log.topics.get(1));
let addr = extract_address_from_topic(&log, 1, "token0")?;
Defensive patterns

Strategy: validation

Validate before calling

fn topic_is_32_bytes(t: &str) -> bool {
    t.trim_start_matches("0x").len() == 64
        && t.trim_start_matches("0x").chars().all(|c| c.is_ascii_hexdigit())
}

Type guard

fn full_width_topic(log: &RpcLog, i: usize) -> bool {
    log.topics.get(i).map_or(false, |t| t.trim_start_matches("0x").len() == 64)
}

Try / catch

match extract_address_from_topic(&log, 1, "token0") {
    Ok(a) => a,
    Err(e) => { quarantine(&log, e); Address::ZERO }
}

Prevention

When it happens

Trigger: Calling extract_address_from_topic on a topic whose hex decodes to <32 bytes — e.g. a truncated topic string, odd-length hex, or a nonstandard producer emitting shortened topics.

Common situations: Corrupted or hand-crafted log fixtures, log data mangled by an intermediate service, or custom chains emitting nonstandard topic widths.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08). Data as JSON: /api/errors/4383d9d3de0815fd. Report an issue: GitHub.