Skip to content

fjall state backend: UTxO tag lookup by native asset does prefix match, not exact match #1338

Description

@adrian1-dot

Summary

utxos_by_tag on the fjall state-store backend does a prefix scan over the state-tags keyspace. For most dimensions (address, payment credential, stake credential, policy ID) the lookup key is a fixed-length credential, so prefix-scan and exact-match coincide and this is harmless. For the asset dimension, the lookup key is policy_id (28 bytes, fixed) ++ asset_name (0–32 bytes, variable) with no delimiter before the following txo_ref field — so a query for asset X also matches any stored asset whose name has X as a byte-prefix.

Where

crates/fjall/src/state/tags.rs:

  • build_utxo_tag_key — key format [dim_hash:8][lookup_key:var][txo_ref:36], no length prefix or terminator between lookup_key and txo_ref.
  • get_by_key — builds prefix = [dim_hash:8][lookup_key] and does readable.prefix(keyspace, prefix), a raw byte-prefix scan.

Asset subject construction: crates/cardano/src/indexes/delta.rs:107-109 (and the archive-side equivalent at :182-184) — subject = policy.to_vec() ++ asset.name().

state/README.md documents the tags keyspace as "Prefix scans by dimension and lookup key" and says the lookup key is "stored verbatim... because a live-UTxO query knows the key it is asking about and wants the exact refs back" — i.e. the intent is exact-match semantics achieved via prefix-scan-over-fixed-length-keys. That assumption holds for every other dimension but not for asset, where the key has a variable-length component. No test in the module covers the "shorter key is a byte-prefix of a longer one" case — the existing test_utxo_tag_prefix test only checks that a key starts with its own prefix.

Confirmed this is still present on main, not something already fixed since a released tag.

Repro

Mint two native assets under the same policy where one name is a byte-prefix of the other, e.g. bob (hex 626f62) and bob2 (hex 626f6232). Querying UTxOs holding asset bob (via utxos_by_asset, surfaced through UTxORPC SearchUtxos) returns both UTxOs — the one actually holding bob and the one holding bob2 — because the stored key for bob2's tag entry is [dim_hash][policy||"bob2"][txo_ref], which starts with the queried prefix [dim_hash][policy||"bob"]. The spurious entry reports quantity 0 for the asset actually requested when its value is inspected.

The redb3 backend does not exhibit this — its get_by_key does a MultimapTable::get(key) exact lookup, not a range/prefix scan.

Impact

Any caller asking "which UTxOs hold exact asset X" via a fjall-backed store gets extra, unrelated UTxOs back whenever another asset under the same policy has a name that is a byte-prefix of X — silently, no error. For short human-readable asset names (incrementing/numbered identifiers being an obvious case: bob, bob2, bob3), this isn't an edge case restricted to adversarial input.

Suggested fix

Delimit the variable-length lookup_key from the fixed-length txo_ref suffix in the stored key — e.g. a length prefix ([dim_hash:8][key_len:2][lookup_key:var][txo_ref:36]) or a reserved separator byte that cannot appear in lookup_key — so a prefix scan on [dim_hash][lookup_key] can no longer match a longer stored lookup_key. Worth checking whether slots_by_tag (archive side, same key shape) needs the identical fix — not checked here.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions