linera-io/linera-protocol · error · async_graphql::Error

{label} must be exactly {N} bytes ({} hex chars)

Error message

{label} must be exactly {N} bytes ({} hex chars)

What it means

After decode_hex succeeds, decode_hex_array converts the decoded bytes into a fixed-size [u8; N] for typed fields (32-byte tx hashes or topics, 20-byte addresses, etc.). try_into fails when the decoded length differs from N, so this error means the hex was valid but the wrong length. The message states the exact requirement: N bytes, i.e. N*2 hex characters.

Source

Thrown at linera-bridge/contracts/evm-bridge/src/service.rs:78

                runtime: self.runtime.clone(),
            },
            EmptySubscription,
        )
        .finish();
        schema.execute(request).await
    }
}

/// Decodes a hex string (optional `0x` prefix) into bytes.
fn decode_hex(label: &str, s: &str) -> async_graphql::Result<Vec<u8>> {
    hex::decode(s.strip_prefix("0x").unwrap_or(s))
        .map_err(|e| async_graphql::Error::new(format!("invalid {label} hex: {e}")))
}

/// Decodes a hex string into a fixed-size byte array.
fn decode_hex_array<const N: usize>(label: &str, s: &str) -> async_graphql::Result<[u8; N]> {
    decode_hex(label, s)?.as_slice().try_into().map_err(|_| {
        async_graphql::Error::new(format!(
            "{label} must be exactly {N} bytes ({} hex chars)",
            N * 2
        ))
    })
}

/// GraphQL mutation root: schedules bridge operations submitted by a client (the
/// demo UI, an admin tool, or a custom relayer). The application bytecode — and
/// thus this interface — cannot change after deployment, so every
/// [`BridgeOperation`] is exposed; on-chain authorization still gates each.
/// Byte-array arguments are hex-encoded strings (with or without `0x`).
pub struct MutationRoot {
    runtime: Arc<ServiceRuntime<EvmBridgeService>>,
}

#[Object]
impl MutationRoot {
    /// Burns `amount` wrapped tokens from the operation's authenticated signer

View on GitHub (pinned to 6c226ddcb3)

Solutions

  1. Count the hex characters (after the optional 0x): it must be exactly 2N — 64 chars for 32-byte hashes/topics, 40 for 20-byte addresses — and match the field named in the message.
  2. If the value is short, left-pad with zeros to the required length; if long, verify you have the right value (e.g. a full keccak hash, not its truncated form).
  3. Double-check you are putting the value in the right argument (address vs hash vs topic) per the field's label in the message.

Example fix

// before
// 32-byte field, but only 4 hex chars given
topic: "0xabcd"  // -> "topic must be exactly 32 bytes (64 hex chars)"

// after
topic: "0x000000000000000000000000000000000000000000000000000000000000abcd"  // 64 hex chars
Defensive patterns

Strategy: validation

Validate before calling

fn has_hex_len(s: &str, n_bytes: usize) -> bool {
    let body = s.strip_prefix("0x").unwrap_or(s);
    body.len() == n_bytes * 2 && body.bytes().all(|b| b.is_ascii_hexdigit())
}

// e.g. has_hex_len(topic, 32) before sending it to the bridge service

Type guard

fn is_32_byte_hex(s: &str) -> bool {
    let body = s.strip_prefix("0x").unwrap_or(s);
    body.len() == 64 && body.bytes().all(|b| b.is_ascii_hexdigit())
}

Prevention

When it happens

Trigger: Calling a bridge-service GraphQL field routed through decode_hex_array with a valid-hex string whose length is not 2N hex chars: e.g. a 40-hex-char (20-byte) EVM address where 64 hex chars (32 bytes) are required, a truncated hash, or an over-long value.

Common situations: Mixing up field types: passing an Ethereum address (20 bytes) as a transaction hash (32 bytes) or vice versa; using a topic hash without left-padding to 32 bytes; pasting a hash that lost leading zeros when converted through a decimal intermediate; confusing byte count with hex-char count (sending 32 hex chars thinking that is 32 bytes).

Related errors


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