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.
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.
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.
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;
}
}
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
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
You MUST normalize a name before you attempt to create a labelhash! If you don't, then the hash you get may be incorrect.
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.
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));
You MUST normalize a name before you DNS-encode it! If you don't, then when you pass those DNS-encoded bytes into a contract method, incorrect namehashes/labelhashes may be derived.
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.
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;
}
}
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
Since the length of each label is stored in a single byte, that means that with this DNS-encoding scheme, each label is limited to being 255 UTF-8 encoded bytes in length. Because of this, names with longer labels cannot be wrapped in the Name Wrapper, as that contract uses the DNS-encoded name.