Skip to main content

Try it Live

Run ENS examples in the interactive playground
Conceptual Guide - For API reference and method documentation, see ENS API.
ENS (Ethereum Name Service) is the decentralized naming system for Ethereum. This guide teaches ENS fundamentals using Tevm.

What is ENS?

ENS provides human-readable names for Ethereum addresses, content hashes, and other resources. Instead of 0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb2, use vitalik.eth. ENS operates on-chain via smart contracts - no centralized DNS servers. Names are NFTs (ERC-721) that can be owned, transferred, and renewed.

Why ENS Exists

Addresses are hostile to humans:
  • 42 hex characters are error-prone
  • No way to verify correctness by inspection
  • Difficult to share verbally or remember
Names solve this:
  • alice.eth0x1234...
  • token.uniswap.eth → Contract address
  • dao.eth → IPFS content hash

Name Structure

ENS names are hierarchical labels separated by dots, read right-to-left:
vitalik.eth
└─┬──┘ └┬┘
  │     └─ TLD (Top-Level Domain)
  └─ Label (owned by user)

wallet.vitalik.eth
└──┬──┘ └─┬──┘ └┬┘
   │      │     └─ TLD
   │      └─ Parent label
   └─ Subdomain

Labels

  • Separated by . (U+002E FULL STOP)
  • Can contain letters, numbers, emoji, non-Latin scripts
  • Maximum 255 characters per label
  • Must be normalized before use (see below)

Hierarchy

  • .eth - Primary ENS TLD (controlled by ENS DAO)
  • name.eth - Second-level domain (what users register)
  • subdomain.name.eth - Unlimited subdomains (owner controls)

Normalization

Critical: ENS names must be normalized to prevent homograph attacks and ensure canonical representation.
import { Ens } from 'tevm';

// Different Unicode, same appearance
const name1 = "аpple.eth";  // Cyrillic 'а' (U+0430)
const name2 = "apple.eth";  // Latin 'a' (U+0061)

// Without normalization - UNSAFE
console.log(name1 === name2);  // false ❌

// With normalization - SAFE
console.log(Ens.normalize(name1)); // Error: mixed scripts
console.log(Ens.normalize(name2)); // "apple.eth"

Normalization Process

  1. Lowercase - Convert ASCII uppercase to lowercase (A-Za-z)
  2. Unicode NFC - Normalize to composed form (é not e + ´)
  3. UTS-46 Processing - Map/disallow characters per Unicode standard
  4. Script Validation - Reject mixed scripts (e.g., Latin + Cyrillic)
  5. Confusable Detection - Reject characters that look identical
import { Ens } from 'tevm';

// Case normalization
Ens.normalize("ALICE.eth");        // "alice.eth"
Ens.normalize("Alice.ETH");        // "alice.eth"

// Unicode normalization
Ens.normalize("café.eth");         // "café.eth" (NFC composed)

// Emoji preservation
Ens.normalize("💩.eth");           // "💩.eth" (emoji unchanged)

// Script mixing (rejected)
Ens.normalize("алісе.eth");        // Error: Cyrillic not allowed in .eth

// Confusables (rejected)
Ens.normalize("nic‍k.eth");        // Error: zero-width joiner

Name Resolution

ENS resolution is a multi-step process from human-readable name to on-chain data:

1. Normalize Name

import { Ens } from 'tevm';

const input = "Vitalik.ETH";
const normalized = Ens.normalize(input);
// "vitalik.eth"

2. Compute Namehash

Namehash converts names to deterministic 32-byte identifiers:
import { Ens, Hash } from 'tevm';

const name = "vitalik.eth";
const node = Ens.namehash(name);
// Hash representing the name in ENS registry
console.log(Hash.toHex(node));
// "0xee6c4522aab0003e8d14cd40a6af439055fd2577951148c14b6cea9a53475835"
Namehash Algorithm:
namehash('') = 0x0000...0000
namehash(label + '.' + domain) = keccak256(namehash(domain) + keccak256(label))

Example: namehash('vitalik.eth')
1. namehash('') = 0x00...00
2. namehash('eth') = keccak256(0x00...00 + keccak256('eth'))
3. namehash('vitalik.eth') = keccak256(namehash('eth') + keccak256('vitalik'))

3. Lookup Resolver

Query ENS registry for resolver contract address:
// Pseudo-code (requires web3 provider)
const registry = new ENSRegistry(registryAddress);
const resolverAddress = await registry.resolver(node);

4. Query Records

Call resolver contract for specific data:
// Pseudo-code
const resolver = new Resolver(resolverAddress);
const address = await resolver.addr(node);  // Ethereum address
const contentHash = await resolver.contenthash(node);  // IPFS/IPNS
const avatar = await resolver.text(node, "avatar");  // Avatar URL

Complete Example

import { Ens, Hash, Keccak256 } from 'tevm';

// Step 1: Normalize user input
const userInput = "Alice.ETH";
const normalized = Ens.normalize(userInput);
console.log(normalized); // "alice.eth"

// Step 2: Compute namehash for on-chain lookup
const node = Ens.namehash(normalized);
console.log(Hash.toHex(node));
// "0x787192fc5378cc32aa956ddfdedbf26b24e8d78e40109add0eea2c1a012c3dec"

// Step 3: Manual namehash verification
// namehash('alice.eth') = keccak256(namehash('eth') + keccak256('alice'))
const ethNode = Ens.namehash("eth");
const aliceLabel = Keccak256.hash(new TextEncoder().encode("alice"));
const computed = Keccak256.hash(new Uint8Array([...ethNode, ...aliceLabel]));
console.log(Hash.toHex(computed) === Hash.toHex(node)); // true

// This node is used to query ENS registry and resolver contracts

Common Use Cases

Wallet Addresses

Most common ENS usage - map names to Ethereum addresses:
import { Ens } from 'tevm';

const name = Ens.normalize("vitalik.eth");
const node = Ens.namehash(name);

// On-chain: resolver.addr(node) returns address
// Result: 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045

Content Hashes

Point names to IPFS content:
import { Ens } from 'tevm';

const name = Ens.normalize("vitalik.eth");
const node = Ens.namehash(name);

// On-chain: resolver.contenthash(node) returns IPFS/IPNS hash
// Use case: Decentralized websites, DAOs, documentation

Text Records

Store arbitrary key-value data:
import { Ens } from 'tevm';

const name = Ens.normalize("vitalik.eth");
const node = Ens.namehash(name);

// Common text records:
// - "avatar" → Profile picture URL
// - "description" → Bio
// - "url" → Website
// - "com.twitter" → Twitter handle
// - "com.github" → GitHub username

Reverse Resolution

Resolve addresses back to primary ENS name:
import { Ens, Address } from 'tevm';

const addr = Address("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045");

// Format: <address>.addr.reverse
const reverseNode = Ens.namehash(
  Address.toHex(addr).slice(2).toLowerCase() + ".addr.reverse"
);

// On-chain: resolver.name(reverseNode) returns "vitalik.eth"

Security

Homograph Attacks

Problem: Visually identical characters from different scripts
import { Ens } from 'tevm';

// These look identical but are different:
const latin = "apple.eth";       // Latin 'a' (U+0061)
const cyrillic = "аpple.eth";    // Cyrillic 'а' (U+0430)

// Normalization prevents this:
Ens.normalize(latin);    // "apple.eth" ✓
Ens.normalize(cyrillic); // Error: mixed scripts ✓

Confusable Characters

Problem: Characters that look similar (I vs l vs 1)
import { Ens } from 'tevm';

// Zero-width joiners, invisible characters
const normal = "nick.eth";
const sneaky = "nic‍k.eth";  // Contains U+200D (zero-width joiner)

Ens.normalize(normal);  // "nick.eth" ✓
Ens.normalize(sneaky);  // Error: invalid character ✓

Best Practices

  1. Always normalize before use
    const safe = Ens.normalize(userInput);
    
  2. Display normalized form to users
    // Show user what they're actually signing
    console.log(`Resolving: ${Ens.normalize(input)}`);
    
  3. Validate on both client and server
    // Never trust client-side validation alone
    if (!Ens.isValid(input)) {
      throw new Error("Invalid ENS name");
    }
    
  4. Use beautify for display, normalize for logic
    const display = Ens.beautify(input);    // "💩.eth" (preserves emoji)
    const canonical = Ens.normalize(input); // "💩.eth" (validated)
    

Resources

Next Steps