Skip to content

ENS: add ENSIP-15 name normalization before namehash / Universal Resolver calls #110

Description

@yashgo0018

Summary

ENS forward resolution currently hashes and DNS-encodes the raw caller-supplied name. There is no ENSIP-15 normalization step before namehash / dnsEncode.

This matters more with the Universal Resolver path: on-chain UR normalizes the DNS name for resolver routing, while the client still builds inner calldata (addr / text / contenthash) with namehash(rawInput). That can split the outer name and the inner node, so confusable or unnormalized input may resolve records for a different node than the user expects.

Current behavior

In src/ens/resolver.zig, entry points do roughly:

  1. node = namehash(name) on the raw string
  2. DNS-encode the same raw string for resolve(bytes,bytes)
  3. No normalize() beforehand

src/ens/namehash.zig only implements EIP-137 namehash and ENSIP-10 dnsEncode (plus empty-label / trailing-dot skipping and label-length checks). There is no UTS-46 / ENSIP-15 pipeline.

Proposed fix

Normalize once with ENSIP-15, then run both namehash and dnsEncode on the same normalized string. Reject names that fail normalization (disallowed characters, empty labels, confusables, illegal mixtures, etc.).

Suggested call shape:

const normalized = try ens_normalize.normalize(allocator, name);
defer allocator.free(normalized);

const node = namehash(normalized);
const dns_name = try dnsEncode(allocator, normalized);

Apply this in all forward-resolution entry points (resolve, getText, getContentHash, and any future helpers).

Suggested library

Rather than re-implementing ENSIP-15 in-tree, consider depending on:

evmts/z-ens-normalize — zero-dependency Zig port of go-ens-normalize, claiming full ENSIP-15 test coverage, embedded spec data, explicit allocators, and a normalize(allocator, name) API that matches how viem/ethers do this.

Pros:

  • Avoid shipping / maintaining Unicode + ENSIP-15 tables ourselves
  • Aligns with the Zig ecosystem and existing ENS reference implementations
  • Clear error taxonomy for invalid names (DisallowedCharacter, WholeConfusable, EmptyLabel, …)

Things to validate before adopting:

  • Zig version compatibility with eth.zig’s toolchain
  • Binary size / init cost of embedded spec data
  • Whether we want the dependency always-on vs optional feature flag
  • Packaging: pin a tagged release / commit hash via build.zig.zon

Acceptance criteria

  • Public ENS name APIs normalize (or explicitly reject) input per ENSIP-15 before hashing / DNS-encoding
  • namehash and dnsEncode always use the same normalized string
  • Regression tests for case folding (Nick.ETHnick.eth), emoji names, and known confusable / invalid inputs
  • Docs updated to state that callers should treat unresolved / rejected names as invalid for payment / identity UX

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions