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:
node = namehash(name) on the raw string
- DNS-encode the same raw string for
resolve(bytes,bytes)
- 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
References
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) withnamehash(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:node = namehash(name)on the raw stringresolve(bytes,bytes)normalize()beforehandsrc/ens/namehash.zigonly implements EIP-137namehashand ENSIP-10dnsEncode(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
namehashanddnsEncodeon the same normalized string. Reject names that fail normalization (disallowed characters, empty labels, confusables, illegal mixtures, etc.).Suggested call shape:
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:
DisallowedCharacter,WholeConfusable,EmptyLabel, …)Things to validate before adopting:
build.zig.zonAcceptance criteria
namehashanddnsEncodealways use the same normalized stringNick.ETH→nick.eth), emoji names, and known confusable / invalid inputsReferences