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

Keccak-256 tag collision between variants `{other_name}` and

Error message

Keccak-256 tag collision between variants `{other_name}` and `{name}` (tag = {tag:#010x}). Rename one of the variants.

What it means

`#[derive(StableEnum)]` assigns each enum variant a deterministic u32 tag: Keccak-256 of the variant name, first 4 bytes big-endian, masked into a 27-bit range so the ULEB128 encoding is always 4 bytes. At macro-expansion time `variant_tags` rejects duplicate tags, failing compilation with this error. With 2^27 possible tags the birthday bound makes collisions realistic only beyond roughly ten thousand variants — for hand-written enums this is essentially unreachable.

Source

Thrown at linera-sdk-derive/src/stable_enum.rs:74

/// Computes the stable tag for a variant name.
fn compute_tag(variant_name: &str) -> u32 {
    let hash = Keccak256::digest(variant_name.as_bytes());
    let val = u32::from_be_bytes([hash[0], hash[1], hash[2], hash[3]]);
    (val & 0x07FF_FFFF) | 0x0800_0000
}

/// Computes all variant tags, returning an error on a (vanishingly unlikely) collision.
fn variant_tags(input: &ItemEnum) -> Result<Vec<(String, u32, &Variant)>> {
    let mut out = Vec::with_capacity(input.variants.len());
    for variant in &input.variants {
        let name = variant.ident.to_string();
        let tag = compute_tag(&name);
        if let Some((other_name, _, _)) = out
            .iter()
            .find(|(_, t, _): &&(String, u32, &Variant)| *t == tag)
        {
            return Err(Error::new(
                variant.span(),
                format!(
                    "Keccak-256 tag collision between variants `{other_name}` and `{name}` \
                     (tag = {tag:#010x}). Rename one of the variants."
                ),
            ));
        }
        out.push((name, tag, variant));
    }
    Ok(out)
}

fn reject_generics(input: &ItemEnum) -> Result<()> {
    if !input.generics.params.is_empty() || input.generics.where_clause.is_some() {
        return Err(Error::new(
            input.generics.span(),
            "#[derive(StableEnum)] does not yet support generic enums",
        ));

View on GitHub (pinned to 6c226ddcb3)

Solutions

  1. Rename one of the two variants named in the error — the message lists both names and the shared tag value.
  2. If names are generated, make the generator produce diverse, semantic names instead of sequential indices.
  3. Re-run `cargo check` after renaming; tags are deterministic, so the same pair always collides until renamed.

Example fix

// before
#[derive(StableEnum)]
enum Action { Transfer, Transferr } // hypothetical Keccak-256 tag collision

// after
#[derive(StableEnum)]
enum Action { Transfer, TransferV2 } // renamed variant gets a fresh tag
Defensive patterns

Strategy: validation

Validate before calling

// compile-time: a CI `cargo check` on every generated enum catches this before merge
// `cargo check -p my-app` fails with the collision message listing both variant names

Prevention

When it happens

Trigger: Deriving `StableEnum` on an enum where two variant names hash to the same 27-bit tag; procedurally generated enums with low name diversity (Variant00001-style names) at scales of ~10^4 variants.

Common situations: Code-generated enums from external schemas with auto-numbered variant names; tests that deliberately exercise the collision path of the derive macro.

Related errors


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