ENS Logo

Name Processing

When interacting with the ENS protocol smart-contracts directly it is important to note that names are not stored in their human readable format. In fact there are a few steps a name undergoes before it can be used by a smart-contract.

When building a dApp most of the time you don't have to worry about name processing, as most libraries will handle this for you.

Name Normalization

Normalization is the process of canonicalizing a name before running it through the Namehash algorithm. It is important to always normalize all input, because even one little difference (like a capital vs lowercase character) will cause the namehash to be completely different.

For example, NaMe.EtH normalizes to name.eth. This ensures that the correct Registry node is used, no matter how the user types in the name.

ENS names are validated and normalized using the ENSIP-15 normalization algorithm.

Previously, UTS-46 was used, but that is insufficient for emoji sequences. Correct emoji processing is only possible with UTS-51. The ENSIP-15 normalization algorithm draws from those older Unicode standards, but also adds many other validation rules to prevent common spoofing techniques like inserting zero-width characters, or using confusable (look-alike) characters. See here for additional discussion on this: Homogylphs

A standard implementation of the algorithm is available at @adraffy/ens-normalize. This library is used under the hood in viem, ENSjs, and others.

import { normalize } from 'viem/ens';
// Uses @adraffy/ens-normalize under the hood

const normalized = normalize('RaFFY🚴‍♂️.eTh');
// => "raffy🚴‍♂.eth"

If the name was not able to be normalized, then that method will throw an error. A name is valid if it is able to be normalized.

Namehash

In the core ENS registry, names are stored as a hash instead of the raw string to optimize for gas, performance, and more. This hashed value is typically referred to as a node. The node is a hex-encoded 32-byte value that is derived from the name using the namehash algorithm defined in ENSIP-1.

Namehash is a recursive algorithm that hashes each part of the name, then hashes the results together. Beacuse recursive functions aren't very efficient in Solidity, it's usually best to derive the namehash offchain and pass to it a contract. Luckily, there are libraries that do this for us.

Calculating Namehash
import { namehash, normalize } from "viem/ens";

const normalizedName = normalize("name.eth");
const node = namehash(normalizedName);
import { ensNormalize, namehash } from "ethers/hash";

const normalizedName = ensNormalize('name.eth')
const node = namehash(normalizedName)
from namehash import namehash

node = namehash('name.eth')
fn main() {
  let node = &namehash("name.eth");
  let s = hex::encode(&node);
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@ensdomains/ens-contracts/contracts/utils/NameEncoder.sol";

contract MyContract {
    function namehash(string calldata name) public pure returns (bytes32) {
        (, bytes32 node) = NameEncoder.dnsEncodeName(name);
        return node;
    }
}

Algorithm

The specification for the namehash algorithm was originally defined in EIP-137 (same as ENSIP-1).

It's a recursive algorithm that works its way down until you hit the root domain. For ens.eth, the algorithm works like so:

namehash('ens.eth') = keccak256(namehash('eth') + labelhash('ens'))
namehash('eth') = keccak256(namehash('') + labelhash('eth'))
namehash('') = 0x0000000000000000000000000000000000000000000000000000000000000000

That last line is a special case: The namehash for an empty string (representing the root domain) is 32 null bytes.

If you plug everything in above, you'll end up with the final namehash value:

  • namehash('') =
    • 0x0000000000000000000000000000000000000000000000000000000000000000
  • labelhash('eth') =
    • keccak256('eth') =
    • 0x4f5b812789fc606be1b3b16908db13fc7a9adf7ca72641f84d75b47069d3d7f0
  • namehash('eth') =
    • keccak256(namehash('') + labelhash('eth')) =
    • keccak256(0x00000000000000000000000000000000000000000000000000000000000000004f5b812789fc606be1b3b16908db13fc7a9adf7ca72641f84d75b47069d3d7f0) =
    • 0x93cdeb708b7545dc668eb9280176169d1c33cfd8ed6f04690a0bcc88a93fc4ae
  • labelhash('ens') =
    • keccak256('ens') =
    • 0x5cee339e13375638553bdf5a6e36ba80fb9f6a4f0783680884d92b558aa471da
  • namehash('ens.eth') =
    • keccak256(namehash('eth') + labelhash('ens')) =
    • keccak256(0x93cdeb708b7545dc668eb9280176169d1c33cfd8ed6f04690a0bcc88a93fc4ae5cee339e13375638553bdf5a6e36ba80fb9f6a4f0783680884d92b558aa471da) =
    • 0x4e34d3a81dc3a20f71bbdf2160492ddaa17ee7e5523757d47153379c13cb46df

Reverse Nodes

The Reverse Node is a node in the Registry that can be claimed for any Ethereum account. The name this node represents is [addr].addr.reverse, where [addr] is the Ethereum public address (lowercase, without the "0x"). These reverse nodes are typically used to set a Primary Name for an account.

To generate the namehash for a reverse node:

  • Take the input address and:
    • Remove the "0x" at the beginning
    • Convert all characters to lowercase
  • Add .addr.reverse to the end
  • Run this result through the namehash algorithm

For example, for address 0x481f50a5BdcCC0bc4322C4dca04301433dED50f0, the name for the reverse node is:

  • 481f50a5bdccc0bc4322c4dca04301433ded50f0.addr.reverse

And the resulting namehash for the reverse node is:

  • 0x58354ffdde6ac279f3a058aafbeeb14059bcb323a248fb338ee41f95fa544c86

Labelhash

Labelhash is the Keccak-256 hash of a single label (e.g. name in name.eth), used in places that don't require the full name.

One example of where labelhash is used is in the BaseRegistar, since it only supports registering 2LDs (second-level domains, like name.eth) and not 3LDs+ (e.g. sub.name.eth). The token ID of a second-level .eth name in the BaseRegistar is the uint256 of the labelhash.

Calculating Labelhash
import { labelhash, normalize } from "viem/ens";

const normalizedLabel = normalize("label");
const hash = labelhash(normalizedLabel);
import { keccak256 } from "ethers/crypto";
import { ensNormalize } from "ethers/hash";
import { toUtf8Bytes } from "ethers/utils";

const normalizedLabel = ensNormalize('label')
const labelhash = keccak256(toUtf8Bytes(normalizedLabel))
string constant label = "label";
bytes32 constant labelhash = keccak256(bytes(label));

DNS Encoding

This is a binary format for domain names, which encodes the length of each label along with the label itself. It is used by some of the ENS contracts, such as when wrapping names in the Name Wrapper or resolving data with ENSIP-10.

DNS Encoding
import { toHex } from 'viem/utils'
import { packetToBytes } from 'viem/ens'

const name = 'name.eth'
const dnsEncodedName = toHex(packetToBytes(name))
import { dnsEncode } from 'ethers/lib/utils'

const dnsEncodedName = dnsEncode('name.eth')
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@ensdomains/ens-contracts/contracts/utils/NameEncoder.sol";

contract MyContract {
    function dnsEncode(string calldata name) public pure returns (bytes memory) {
        (bytes memory dnsEncodedName,) = NameEncoder.dnsEncodeName(name);
        return dnsEncodedName;
    }
}

Algorithm

To DNS-encode a name, first split the name into labels (delimited by .). Then for each label from left-to-right:

  • One byte to denote the length of the label
  • The UTF-8 encoded bytes for the label
  • If this is the last label, then one final NUL (0x00) byte.

For example, to DNS-encode my.name.eth:

  • 0x02 (length of the label "my")
  • 0x6D79 (UTF-8 encoded bytes of "my")
  • 0x04 (length of the label "name")
  • 0x6E616D65 (UTF-8 encoded bytes of "name")
  • 0x03 (length of the label "eth")
  • 0x657468 (UTF-8 encoded bytes of "eth")
  • 0x00 (end of name marker)

Final result: 0x026d79046e616d650365746800