Zero-dependency Zig implementation of ENSIP-15: ENS Name Normalization Standard
A complete port of go-ens-normalize to Zig, providing ENS (Ethereum Name Service) domain name normalization according to ENSIP-15 specification.
- Zero Dependencies - No external packages required
- 100% ENSIP-15 Compliant - Passes all official validation tests
- Embedded Data - Compressed specification data built into the binary
- Thread-Safe - Singleton pattern with lazy initialization via
std.once() - Memory Efficient - Explicit allocator parameters for full control
- Unicode 16.0.0 - Latest Unicode standard support
- C FFI Compatible - Full C bindings for interoperability
- WebAssembly Ready - Browser and Node.js WASM support
Add to your build.zig.zon:
.{
.name = "my-project",
.version = "0.1.0",
.dependencies = .{
.z_ens_normalize = .{
.url = "https://github.com/YOUR_USERNAME/z-ens-normalize/archive/refs/tags/v0.1.0.tar.gz",
// Use zig fetch to get the correct hash
.hash = "...",
},
},
}const ens = b.dependency("z_ens_normalize", .{
.target = target,
.optimize = optimize,
});
exe.root_module.addImport("z_ens_normalize", ens.module("z_ens_normalize"));const std = @import("std");
const ens = @import("z_ens_normalize");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// Normalize a name
const normalized = try ens.normalize(allocator, "Nick.ETH");
defer allocator.free(normalized);
std.debug.print("Normalized: {s}\n", .{normalized});
// Output: "nick.eth"
// Beautify a name (preserves emoji presentation)
const beautified = try ens.beautify(allocator, "🚀RaFFY🚴♂️.eTh");
defer allocator.free(beautified);
std.debug.print("Beautified: {s}\n", .{beautified});
// Output: "🚀raffy🚴♂️.eth"
}These functions use a thread-safe singleton instance initialized lazily on first use:
Normalizes an ENS name according to ENSIP-15 specification.
Parameters:
allocator- Memory allocator for the resultname- Input name as UTF-8 bytes
Returns: Normalized name (caller owns memory, must free)
Example:
const result = try ens.normalize(allocator, "VITALIK.eth");
defer allocator.free(result);
// result: "vitalik.eth"Beautifies an ENS name with visual enhancements while maintaining normalization.
Differences from normalize():
- Preserves FE0F variation selectors for emoji presentation
- Converts lowercase Greek xi (ξ) to uppercase Xi (Ξ) in non-Greek labels
- More visually appealing for UI display
Example:
const result = try ens.beautify(allocator, "🏴☠️nick.eth");
defer allocator.free(result);
// result: "🏴☠️nick.eth" (with proper emoji presentation)For more control, you can use the singleton directly or create your own instance:
Returns the thread-safe singleton instance.
const instance = ens.shared();
const result = try instance.normalize(allocator, "test.eth");
defer allocator.free(result);Creates a new ENSIP15 normalizer instance.
var normalizer = try ens.Ensip15.init(allocator);
defer normalizer.deinit();
const result = try normalizer.normalize(allocator, "test.eth");
defer allocator.free(result);All normalization functions return errors for invalid input:
const result = ens.normalize(allocator, "invalid..name") catch |err| switch (err) {
error.EmptyLabel => std.debug.print("Label cannot be empty\n", .{}),
error.DisallowedCharacter => std.debug.print("Contains disallowed character\n", .{}),
error.IllegalMixture => std.debug.print("Illegal script mixture\n", .{}),
error.WholeConfusable => std.debug.print("Confusable with another name\n", .{}),
else => return err,
};The library defines the following error types:
InvalidLabelExtension- Label has--at positions 2-3 (e.g., "ab--test")IllegalMixture- Mixed scripts not allowed togetherWholeConfusable- Label looks like a different scriptLeadingUnderscore- Underscore appears after label startFencedLeading- Zero-width joiner at label startFencedAdjacent- Adjacent zero-width charactersFencedTrailing- Zero-width joiner at label endDisallowedCharacter- Character not allowed in ENS namesEmptyLabel- Zero-length labelCMLeading- Combining mark at label startCMAfterEmoji- Combining mark after emojiNSMDuplicate- Duplicate non-spacing marksNSMExcessive- Too many non-spacing marksOutOfMemory- Allocation failureInvalidUtf8- Invalid UTF-8 encoding
The library also exposes Unicode normalization functions:
const nf = ens.NF.init();
// NFC (Canonical Composition)
const composed = try nf.nfc(allocator, &[_]u21{ 0x61, 0x300 }); // "à"
defer allocator.free(composed);
// NFD (Canonical Decomposition)
const decomposed = try nf.nfd(allocator, &[_]u21{ 0xE0 }); // "a" + "̀"
defer allocator.free(decomposed);The library includes comprehensive test suites:
zig build test-
ENSIP-15 Validation Tests (
tests/ensip15_test.zig)- 100% pass rate on official ENSIP-15 test suite
- Tests normalization, beautification, and error cases
-
Unicode Normalization Tests (
tests/nf_test.zig)- 100% pass rate on Unicode normalization test cases
- Tests NFC, NFD, and Hangul composition
-
Initialization Tests (
tests/init_test.zig)- Tests data loading from embedded binary
- Validates spec.bin and nf.bin decompression
Test data is automatically copied from the reference implementation:
zig build copy-test-dataThis downloads:
ensip15-tests.json- ENSIP-15 validation test casesnf-tests.json- Unicode normalization test cases
The library provides a complete C API for interoperability with C/C++ and other languages.
# Build C FFI library
zig build c-lib
# Output: zig-out/lib/libz_ens_normalize_c.a
# Header: zig-out/include/z_ens_normalize.h#include <stdio.h>
#include "z_ens_normalize.h"
int main(void) {
// Initialize library (optional)
zens_init();
// Normalize a name
ZensResult result = zens_normalize("Nick.ETH", 0);
if (result.error_code == ZENS_SUCCESS) {
printf("Normalized: %.*s\n", (int)result.len, result.data);
zens_free(result);
} else {
printf("Error: %s\n", zens_error_message(result.error_code));
}
// Cleanup (optional)
zens_deinit();
return 0;
}# Using GCC
gcc your_program.c -I./zig-out/include -L./zig-out/lib -lz_ens_normalize_c -o your_program
# Using Clang
clang your_program.c -I./zig-out/include -L./zig-out/lib -lz_ens_normalize_c -o your_programint32_t zens_init(void)
- Initialize the library (optional but recommended)
- Returns 0 on success
void zens_deinit(void)
- Cleanup library resources
- Call at program exit
ZensResult zens_normalize(const uint8_t *input, size_t input_len)
- Normalize an ENS name
input_lencan be 0 to use strlen()- Returns
ZensResultwith normalized name or error
ZensResult zens_beautify(const uint8_t *input, size_t input_len)
- Beautify an ENS name with visual enhancements
- Same parameters as
zens_normalize()
void zens_free(ZensResult result)
- Free memory allocated by normalize/beautify
- Must be called for successful results
const char* zens_error_message(int32_t error_code)
- Get human-readable error message
- Returns static string (do not free)
typedef enum {
ZENS_SUCCESS = 0,
ZENS_ERROR_OUT_OF_MEMORY = -1,
ZENS_ERROR_INVALID_UTF8 = -2,
ZENS_ERROR_INVALID_LABEL_EXTENSION = -3,
ZENS_ERROR_ILLEGAL_MIXTURE = -4,
ZENS_ERROR_WHOLE_CONFUSABLE = -5,
ZENS_ERROR_LEADING_UNDERSCORE = -6,
ZENS_ERROR_DISALLOWED_CHARACTER = -10,
ZENS_ERROR_EMPTY_LABEL = -11,
// ... more error codes
} ZensErrorCode;See include/z_ens_normalize.h for complete API documentation.
The library can be compiled to WebAssembly for use in browsers and Node.js.
# Build for browsers/Node.js (freestanding)
zig build wasm
# Output: zig-out/bin/z_ens_normalize.wasm
# Build with WASI support
zig build wasi
# Output: zig-out/bin/z_ens_normalize_wasi.wasm
# Build both
zig build wasm-all<!DOCTYPE html>
<html>
<body>
<script type="module">
// Load WASM module
const response = await fetch('z_ens_normalize.wasm');
const bytes = await response.arrayBuffer();
const { instance } = await WebAssembly.instantiate(bytes, {});
// Initialize
instance.exports.zens_init();
// Helper to encode string
const encoder = new TextEncoder();
function normalize(name) {
const bytes = encoder.encode(name);
const ptr = instance.exports.malloc(bytes.length);
const memory = new Uint8Array(instance.exports.memory.buffer);
memory.set(bytes, ptr);
const resultPtr = instance.exports.zens_normalize(ptr, bytes.length);
// ... read result from memory
}
console.log(normalize("Nick.ETH")); // "nick.eth"
</script>
</body>
</html>import { readFile } from 'fs/promises';
// Load WASM
const wasmBuffer = await readFile('z_ens_normalize.wasm');
const { instance } = await WebAssembly.instantiate(wasmBuffer, {});
// Initialize
instance.exports.zens_init();
// Use normalize/beautify functions (see examples/example_node.mjs)Complete examples are provided in the examples/ directory:
examples/example.html- Browser example with interactive UIexamples/example_node.mjs- Node.js example with ES modulesexamples/example.c- C API example
Run the examples:
# C example
zig build c-lib
gcc examples/example.c -I./zig-out/include -L./zig-out/lib -lz_ens_normalize_c -o example
./example
# Node.js example
zig build wasm
node examples/example_node.mjs
# Browser example
zig build wasm
# Serve examples/ directory with HTTP server
python -m http.server 8000
# Open http://localhost:8000/examples/example.html# Build library
zig build
# Run tests
zig build test
# Build with optimizations
zig build -Doptimize=ReleaseFast# Build for specific target
zig build -Dtarget=x86_64-linux
# Build static library for all targets
zig build --summary allzig build # Default library
zig build test # Run tests
zig build c-lib # C FFI library
zig build wasm # WebAssembly (freestanding)
zig build wasi # WebAssembly (WASI)
zig build wasm-all # All WASM variants-
Sync with reference implementation:
# Update test data from go-ens-normalize zig build copy-test-data -
Run tests:
zig build test -
Build library:
zig build # Output: zig-out/lib/libz_ens_normalize.a
z-ens-normalize/
├── src/
│ ├── root.zig # Public Zig API & singleton
│ ├── root_c.zig # C FFI bindings
│ ├── ensip15/
│ │ ├── ensip15.zig # Main normalization logic
│ │ ├── init.zig # Data initialization
│ │ ├── types.zig # Core data structures
│ │ ├── errors.zig # Error definitions
│ │ ├── utils.zig # Helper utilities
│ │ └── spec.bin # Embedded ENSIP-15 data
│ ├── nf/
│ │ ├── nf.zig # Unicode normalization
│ │ └── nf.bin # Embedded normalization data
│ └── util/
│ ├── decoder.zig # Binary data decoder
│ └── runeset.zig # Efficient rune set
├── include/
│ └── z_ens_normalize.h # C API header
├── examples/
│ ├── example.c # C API example
│ ├── example.html # Browser WASM example
│ └── example_node.mjs # Node.js WASM example
├── tests/
│ ├── ensip15_test.zig # ENSIP-15 validation tests
│ ├── nf_test.zig # Unicode normalization tests
│ └── init_test.zig # Initialization tests
├── test-data/
│ ├── ensip15-tests.json # ENSIP-15 test cases
│ └── nf-tests.json # NF test cases
├── build.zig # Build configuration
└── README.md # This file
This library was developed using AI-assisted implementation with Claude Code, following a structured, multi-phase approach:
.claude/commands/ens.md- Complete ENS specification context including ENSIP-1 (ENS Protocol) and ENSIP-15 (Name Normalization) standardsprompts/- 19 detailed implementation guides (tasks 01-19) providing step-by-step instructions for porting each component from the Go reference implementation
The development followed a staged approach outlined in prompts/00-meta-guide.md:
Stage 1: Skeleton Setup (Tasks 01-19)
- Created project structure with all type definitions and function signatures
- Stubbed all logic with
@panic("TODO")to achieve compilation - Result:
zig buildsucceeds, tests exist but fail
Stage 2: Implementation (Dependency order)
- Implemented actual logic following the Go reference implementation
- Three parallel phases:
- Phase 1 (Foundation): 8 concurrent tasks - decoder, runeset, types, binaries, test data
- Phase 2 (Core): 8 concurrent tasks - NF initialization, normalization, ENSIP15 validation
- Phase 3 (Tests): 3 concurrent tasks - test infrastructure for NF and ENSIP15
- Result:
zig build testshows 100% pass rate
Each prompt file in prompts/ includes:
- Complete Go reference code to port
- Zig type mappings and patterns
- Step-by-step implementation guidance
- Success criteria checklist
- Validation commands
Example tasks:
01-util-decoder.md- Binary data decoder for compressed spec files09-nf-init.md- Unicode normalization data initialization13-ensip15-normalize.md- Core ENSIP-15 normalization pipeline18-ensip15-tests.md- Comprehensive validation test suite
This approach enabled systematic development with clear milestones, parallel workstreams, and automated validation at each stage.
The library follows Zig best practices for memory management:
- Explicit Allocators - All allocation-requiring functions take
Allocatorparameter - Caller Owns Memory - Functions return owned slices that must be freed
- No Hidden Allocations - No global allocator usage
- Zero-Copy Initialization - Embedded data is referenced, not copied
Example memory pattern:
// Caller provides allocator and owns result
const result = try ens.normalize(allocator, "test.eth");
defer allocator.free(result); // Caller frees memory
// Internal operations use the provided allocator
// No global state or hidden allocationsThe library is designed for efficiency:
- Compressed Data - Spec data is bit-packed and compressed
- Embedded Binary - No file I/O at runtime
- Lazy Initialization - Singleton initialized only when first used
- Zero-Copy Where Possible - References embedded data directly
- Zig Version: 0.13.0 or later
- Unicode Version: 16.0.0
- ENSIP-15: Final specification
- Reference Implementation: go-ens-normalize v0.1.1
Contributions are welcome! This implementation aims to maintain 100% compatibility with the reference Go implementation.
- Run tests before submitting PR:
zig build test - Follow Zig style conventions
- Add tests for new functionality
- Update documentation as needed
MIT License - see LICENSE file for details
- Reference Implementation: adraffy/go-ens-normalize
- JavaScript Reference: adraffy/ens-normalize.js
- ENSIP-15 Specification: ENS Improvement Proposals
- Zig Port: William Cory
- ENSIP-15 Specification
- ENS Documentation
- Unicode Technical Report #15 (Normalization Forms)
- Unicode Technical Report #46 (IDNA Compatibility)
- Zig Language Reference
- Issues: GitHub Issues
- ENS Discord: discord.gg/ensdomains
Built with Zig 🦎