feat: Universal Resolver migration with full ENSIP-15 normalization - #113
feat: Universal Resolver migration with full ENSIP-15 normalization#113koko1123 wants to merge 13 commits into
Conversation
From adraffy/go-ens-normalize @ 165fa80 (MIT). spec.bin/nf.bin are the compressed ENSIP-15 and Unicode NF tables (36 KB total, shipped in the package); the JSON vectors live under tests/ and are not packaged.
Port of go-ens-normalize util/ (decoder.go, runeset.go) at 165fa80. Both hand-computed test buffers in the brief (0x09, 0x83) were verified bit-by-bit against decoder.go's LSB-first readBit/readUnary/readBinary and are correct as written; no test corrections were needed. Fixed one Zig-specific memory-safety issue not present in the Go source: a literal translation of readUnique would wrap the errdefer-tracked sorted-ascending prefix in an ArrayList via fromOwnedSlice and grow it in place, so an allocation failure mid-append could free that buffer twice (once via the list's own errdefer, once via the outer errdefer on the original slice). Reworked to build the appended ranges in a separate buffer and copy once at the end; added a regression test that exercises the append path end to end.
Port of go-ens-normalize nf/nf.go with embedded nf.bin tables.
Port of go-ens-normalize ensip15/ at 165fa80: tokenization, NFC, script groups, whole-script confusables, emoji sequences, and the label validation rules. Public API: ens_normalize.normalize.
zig build vector-test runs the full upstream suites (ens-normalize 1.11.1, Unicode 17) in CI.
An unconditional stderr summary made the Zig build runner print a "failed command" trailer on successful vector-test runs. Print diagnostics only when failures occur; the expectEqual assertion still fails the build on any mismatch.
lastError() now exposes the error.data hex payload so callers can decode contract custom errors (needed for Universal Resolver).
resolve/getText/getContentHash now normalize per ENSIP-15, then build namehash and ENSIP-10 dnsEncode from the same normalized string and call resolve(bytes,bytes) on the UR (same address on mainnet and Sepolia). UR custom-error reverts map to null / OffchainLookupRequired. Adds EIP-1577 contenthash decoding (ipfs/ipns/swarm).
reverse(bytes,uint256) with on-chain forward verification replaces the namehash-of-addr.reverse registry flow. Removes the now-unused ens_registry chain metadata.
Integration tests are gated on ETH_RPC_URL and skip when unset.
Final-review cleanup: NoResolver was declared but never produced (the no-resolver case returns null) and its docs contradicted the null contract; parseSelector existed verbatim in resolver.zig and reverse.zig; provider revert-data buffer comment and the ens docs normalization-timing claim were imprecise.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughThis change adds ENSIP-15 normalization, ENSIP-10 Universal Resolver support, DNS encoding, EIP-1577 contenthash decoding, reverse lookup verification, provider revert-data capture, conformance tests, integration tests, and updated ENS documentation. ChangesENS resolution support
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant Caller
participant ENSResolver
participant Provider
participant UniversalResolver
Caller->>ENSResolver: normalize name
ENSResolver->>ENSResolver: namehash and DNS-encode normalized name
ENSResolver->>Provider: call Universal Resolver
Provider->>UniversalResolver: resolve(bytes,bytes)
UniversalResolver-->>Provider: response or revert data
Provider-->>ENSResolver: result and ErrorInfo.data
ENSResolver-->>Caller: record, null, or ResolveError
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 7
🧹 Nitpick comments (5)
tests/ens_vectors_test.zig (1)
71-76: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueGuard the case-array length before indexing.
v[0],v[1], andv[2]assume every case holds at least three elements. A refreshednf-tests.jsonwith a shorter row causes an index-out-of-bounds panic, and the panic does not identify the offending group or row. A length check turns that into a readable test failure.♻️ Proposed guard
for (entry.value_ptr.array.items) |case| { const v = case.array.items; + if (v.len < 3) { + fail += 1; + std.debug.print("malformed NF case in {s}: {d} fields\n", .{ entry.key_ptr.*, v.len }); + continue; + } const input = try utf8ToCps(allocator, v[0].string);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/ens_vectors_test.zig` around lines 71 - 76, In the test loop over cases, validate that case.array.items contains at least three elements before assigning v[0], v[1], or v[2]. Turn insufficient rows into a readable test failure that identifies the current group and row, then continue indexing only after the check passes.src/ens/normalize/nf.zig (1)
159-195: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low valueFree the pending row when the map insertion fails.
oneat Line 165 andpairat Line 185 are allocated beforedecomps.put. Ifputreturnserror.OutOfMemory, the slice is not yet a map value, sofreeDecompsin theerrdefercannot free it. The result is a leaked row on the allocation-failure path. The same allocator is used for both, so anerrdeferinside each loop body closes the gap.🛡️ Proposed fix for the OOM path
for (decomp1, 0..) |cp_raw, i| { const one = try allocator.alloc(u21, 1); + errdefer allocator.free(one); one[0] = `@intCast`(decomp1a[i]); try decomps.put(allocator, `@intCast`(cp_raw), one); }const pair = try allocator.alloc(u21, 2); + errdefer allocator.free(pair); pair[0] = cp_b; pair[1] = cp_a; try decomps.put(allocator, cp, pair);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/ens/normalize/nf.zig` around lines 159 - 195, Add an errdefer immediately after allocating each pending decomposition row in the single-codepoint loop and the two-codepoint loop, covering one and pair respectively, so failed decomps.put calls free the allocation while successful insertion transfers ownership to decomps.src/ens/normalize/ensip15.zig (2)
886-897: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueAdd a negative-path unit test in this module.
The tests in
src/ens/normalize.zigcover rejection cases, andtests/ens_vectors_test.zigcovers the official vectors. This file has only one positive test. A single confusable or illegal-mixture case here keeps the core module self-checking when the public wrapper changes, and it exercisescheckWholeanddetermineGroup, which no test in this module reaches today.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/ens/normalize/ensip15.zig` around lines 886 - 897, Add a negative-path unit test alongside the existing normalize test in this module, using a confusable or illegal mixed-script name that normalize rejects. Assert the call returns the expected NormalizeError, exercising the internal checkWhole and determineGroup validation path while leaving the existing positive test unchanged.
860-884: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winReplace the busy spin with
std.Io.Mutex.
Ensip15.initcan take a long time. Use a module-levelstd.Io.Mutexwith an atomicreadyfast path. Re-checkreadyafterlockUncancelable(runtime.blockingIo()), initialize under the lock, publishtables, and then release the lock.std.Thread.Futexis not required; a futex implementation would also need a 32-bit wait word instead of the currentu8atomic.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/ens/normalize/ensip15.zig` around lines 860 - 884, Replace the initializing-state CAS and spin loop in getTables with a module-level std.Io.Mutex and an atomic ready fast path. When not ready, acquire the mutex via lockUncancelable(runtime.blockingIo()), re-check readiness after locking, initialize Ensip15 only if still needed, publish tables, mark ready with release ordering, and unlock; preserve the existing page_allocator initialization and catch behavior.src/ens/normalize.zig (1)
23-25: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider limiting the public surface for the NF re-export.
testing_nfexposes the internal NF implementation through the publishedeth.ens_normalizeAPI. Only the vector suite needs it. The name marks the intent, but consumers can still depend on it, and later table refactors then become breaking changes. An alternative is to importsrc/ens/normalize/nf.zigdirectly fromtests/ens_vectors_test.zigthrough a dedicated build module, and keepnormalize.ziglimited toNormalizeErrorandnormalize.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/ens/normalize.zig` around lines 23 - 25, Remove the public testing_nf re-export from normalize.zig and update the vector suite to import normalize/nf.zig directly through its dedicated build module. Keep normalize.zig’s public surface limited to NormalizeError and normalize, while preserving the vector tests’ direct access to the NF implementation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/ens/contenthash.zig`:
- Around line 125-133: Update decodeIpns to validate the payload as a CID before
calling baseNEncode, matching the validation performed by the IPFS and Swarm
decoding paths; reject empty or otherwise invalid payloads instead of
constructing an unresolved ipns://k URI.
In `@src/ens/normalize/data/NOTICE`:
- Around line 1-7: Update src/ens/normalize/data/NOTICE to attribute
adraffy/go-ens-normalize as Copyright (c) 2024 Andrew Raffensperger, and add the
complete upstream MIT license text. Place the license text in an appropriate
repository license file since no aggregate third-party license file exists,
while preserving the existing provenance details.
In `@src/ens/normalize/decoder.zig`:
- Around line 109-140: Update the documentation for readUnsortedDeltas to remove
the claim that intermediate sums may be negative and state that decoded
accumulated values must remain non-negative for valid input. Keep the existing
i64 accumulator implementation in readArray unchanged.
In `@src/ens/resolver.zig`:
- Around line 170-172: Update the catch block around provider.call in the
resolver flow to inspect the caught error and call mapRevert(provider) only when
it is error.RpcError; propagate all other errors directly so allocation failures
cannot consume stale provider.lastError() data.
In `@src/ens/reverse.zig`:
- Around line 156-159: Update the encodeReverseTuple documentation and parameter
names to match the UR tuple order: primary name, forward resolver address, and
reverseResolver address. Rename the resolved parameter and its corresponding
AbiValue usage without changing tuple encoding behavior or lookupAddress.
- Around line 89-92: Update the primary-name handling after decoding in the
reverse-resolution function: normalize the decoded name only for comparison,
return null when the normalized form differs from the stored name, and return
the original name when they match. Update the test covering “Vitalik.ETH” to
expect null instead of the rewritten lowercase name.
In `@tests/integration_tests.zig`:
- Line 196: Update the private-key wallet initialization calls in README.md,
docs/content/docs/, and examples/04_send_transaction.zig to use
eth.wallet.Wallet.initLocal rather than Wallet.init; preserve Wallet.init for
signer_mod.Signer inputs, and leave the already-correct
tests/integration_tests.zig call unchanged.
---
Nitpick comments:
In `@src/ens/normalize.zig`:
- Around line 23-25: Remove the public testing_nf re-export from normalize.zig
and update the vector suite to import normalize/nf.zig directly through its
dedicated build module. Keep normalize.zig’s public surface limited to
NormalizeError and normalize, while preserving the vector tests’ direct access
to the NF implementation.
In `@src/ens/normalize/ensip15.zig`:
- Around line 886-897: Add a negative-path unit test alongside the existing
normalize test in this module, using a confusable or illegal mixed-script name
that normalize rejects. Assert the call returns the expected NormalizeError,
exercising the internal checkWhole and determineGroup validation path while
leaving the existing positive test unchanged.
- Around line 860-884: Replace the initializing-state CAS and spin loop in
getTables with a module-level std.Io.Mutex and an atomic ready fast path. When
not ready, acquire the mutex via lockUncancelable(runtime.blockingIo()),
re-check readiness after locking, initialize Ensip15 only if still needed,
publish tables, mark ready with release ordering, and unlock; preserve the
existing page_allocator initialization and catch behavior.
In `@src/ens/normalize/nf.zig`:
- Around line 159-195: Add an errdefer immediately after allocating each pending
decomposition row in the single-codepoint loop and the two-codepoint loop,
covering one and pair respectively, so failed decomps.put calls free the
allocation while successful insertion transfers ownership to decomps.
In `@tests/ens_vectors_test.zig`:
- Around line 71-76: In the test loop over cases, validate that case.array.items
contains at least three elements before assigning v[0], v[1], or v[2]. Turn
insufficient rows into a readable test failure that identifies the current group
and row, then continue indexing only after the check passes.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 7cf36218-7bb2-4cb5-bda4-8458dd738df7
⛔ Files ignored due to path filters (2)
src/ens/normalize/data/nf.binis excluded by!**/*.binsrc/ens/normalize/data/spec.binis excluded by!**/*.bin
📒 Files selected for processing (28)
.github/workflows/ci.ymlMakefileREADME.mdbuild.zigdocs/content/docs/ens.mdxdocs/content/docs/modules.mdxsrc/chains/arbitrum.zigsrc/chains/base.zigsrc/chains/chain.zigsrc/chains/ethereum.zigsrc/chains/optimism.zigsrc/chains/polygon.zigsrc/ens/contenthash.zigsrc/ens/namehash.zigsrc/ens/normalize.zigsrc/ens/normalize/data/NOTICEsrc/ens/normalize/decoder.zigsrc/ens/normalize/ensip15.zigsrc/ens/normalize/nf.zigsrc/ens/normalize/rune_set.zigsrc/ens/resolver.zigsrc/ens/reverse.zigsrc/provider.zigsrc/root.zigtests/data/ens-normalize/nf-tests.jsontests/data/ens-normalize/tests.jsontests/ens_vectors_test.zigtests/integration_tests.zig
💤 Files with no reviewable changes (6)
- src/chains/base.zig
- src/chains/optimism.zig
- src/chains/polygon.zig
- src/chains/arbitrum.zig
- src/chains/ethereum.zig
- src/chains/chain.zig
Strict-reject unnormalized primary names in reverse resolution (an unnormalized stored primary is treated as no-primary-set rather than rewritten to a name the UR never verified); consult lastError only for RpcError reverts; validate ipns contenthash payloads; vendor the upstream MIT license text with corrected attribution; fix decoder doc precision and reverse test-helper naming; migrate remaining raw-key Wallet.init callers in docs and examples to initLocal.
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/ens/resolver.zig (1)
198-207: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winValidate custom-error payloads before mapping selectors.
mapRevertmaps selector-onlyResolverNotFoundandResolverErrorpayloads tonull, and maps0x556f1830deadbeeftoerror.OffchainLookupRequired. These payloads omit required ABI arguments. Decode and validate every recognized error payload, returnerror.ProviderErroron failure, and update the fixtures.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/ens/resolver.zig` around lines 198 - 207, Update mapRevert to ABI-decode and validate the payload for every recognized selector before returning its mapped result, including required arguments for ResolverNotFound, ResolverError, and OffchainLookup; return error.ProviderError when decoding or validation fails. Preserve the existing mappings only for valid payloads, and update the related fixtures to use complete valid error payloads plus invalid selector-only/malformed cases.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/ens/resolver.zig`:
- Around line 170-176: Update the error handling around provider.call in the
resolver flow so mapRevert(provider) is invoked only when e is error.RpcError
and provider.lastError().code equals 3. Return error.OutOfMemory unchanged, map
other RPC errors to error.ProviderError, and preserve the existing fallback for
non-RPC failures.
---
Outside diff comments:
In `@src/ens/resolver.zig`:
- Around line 198-207: Update mapRevert to ABI-decode and validate the payload
for every recognized selector before returning its mapped result, including
required arguments for ResolverNotFound, ResolverError, and OffchainLookup;
return error.ProviderError when decoding or validation fails. Preserve the
existing mappings only for valid payloads, and update the related fixtures to
use complete valid error payloads plus invalid selector-only/malformed cases.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: fe091f4f-07a7-4ba2-b257-c3687c9aeb93
📒 Files selected for processing (11)
README.mddocs/content/docs/examples.mdxdocs/content/docs/introduction.mdxdocs/content/docs/nonce-manager.mdxdocs/content/docs/transactions.mdxexamples/04_send_transaction.zigsrc/ens/contenthash.zigsrc/ens/normalize/data/NOTICEsrc/ens/normalize/decoder.zigsrc/ens/resolver.zigsrc/ens/reverse.zig
🚧 Files skipped from review as they are similar to previous changes (5)
- src/ens/normalize/data/NOTICE
- README.md
- src/ens/contenthash.zig
- src/ens/normalize/decoder.zig
- src/ens/reverse.zig
| const response = provider.call(UNIVERSAL_RESOLVER, outer) catch |e| { | ||
| // Only a revert carries diagnostics; any other failure (including an | ||
| // allocation failure) must not read a stale `lastError()` from an | ||
| // earlier call. | ||
| if (e == error.OutOfMemory) return error.OutOfMemory; | ||
| if (e == error.RpcError) return mapRevert(provider); | ||
| return error.ProviderError; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/sh
set -eu
rg -n -C 6 'pub const ErrorInfo|pub fn lastError|error\.RpcError|captureRpcError|code.*3|execution reverted' src/provider.zigRepository: StrobeLabs/eth.zig
Length of output: 10346
🏁 Script executed:
#!/bin/sh
set -eu
printf '%s\n' '--- resolver symbols ---'
rg -n -C 12 'mapRevert|provider\.call|OffchainLookupRequired|lastError' src/ens/resolver.zig
printf '%s\n' '--- provider call and error capture ---'
rg -n -C 14 'pub fn call|fn call|last_error|captureRpcError|rpcCall|error\.RpcError' src/provider.zigRepository: StrobeLabs/eth.zig
Length of output: 36076
Check lastError().code == 3 before calling mapRevert. error.RpcError covers every JSON-RPC error, not only execution reverts. Otherwise, mapRevert can interpret server-error data as resolver revert data.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/ens/resolver.zig` around lines 170 - 176, Update the error handling
around provider.call in the resolver flow so mapRevert(provider) is invoked only
when e is error.RpcError and provider.lastError().code equals 3. Return
error.OutOfMemory unchanged, map other RPC errors to error.ProviderError, and
preserve the existing fallback for non-RPC failures.
Summary
zig build vector-test.resolve,getText, newgetContentHash) migrated to the Universal Resolver (0xeEeEEEeE14D718C2B47D9923Deab1335E144EeEe, mainnet + Sepolia) viaresolve(bytes,bytes), with UR custom-error reverts mapped tonull/ typed errors (selectors verified withcast sig).reverse(bytes,uint256)with on-chain forward verification; returned primary names are normalized before being handed to callers.Qm...) per EIP-1577's canonical example; see the docs for the cross-library string-compat caveat.lastError()now captures JSON-RPC revert data so contract custom errors can be decoded.ens_registrychain metadata; zero new external dependencies (spec tables are 36 KB of vendored MIT-licensed data).Testing
zig build vector-test).ETH_RPC_URL(skip when unset); fixtures independently verified against mainnet viacast.Not included (follow-ups)
error.OffchainLookupRequired. Follow-up issue to be filed on merge.beautify(), comptime table decode,tokenize()introspection.Closes #110. Supersedes #109 -- thanks @yashgo0018 for the report and the initial PR.
Summary by CodeRabbit
New Features
Documentation
Tests