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 here: https://github.com/adraffy/ens-normalize.js. This library is also included in ENSjs.
To normalize a name, simply call ens_normalize
:
import {ens_normalize} from '@adraffy/ens-normalize'; // or require()
// npm i @adraffy/ens-normalize
// browser: https://cdn.jsdelivr.net/npm/@adraffy/ens-normalize@latest/dist/index.min.mjs (or .cjs)
// *** ALL errors thrown by this library are safe to print ***
// - characters are shown as {HEX} if should_escape()
// - potentially different bidi directions inside "quotes"
// - 200E is used near "quotes" to prevent spillover
// - an "error type" can be extracted by slicing up to the first (:)
// - labels are middle-truncated with ellipsis (…) at 63 cps
// string -> string
// throws on invalid names
// output ready for namehash
let normalized = ens_normalize('RaFFY🚴♂️.eTh');
// => "raffy🚴♂.eth"
// note: does not enforce .eth registrar 3-character minimum
If the name was not able to be normalized, then that method will throw a descriptive error. A name is valid if it is able to be normalized.
In order for us to interface with our nice readable names there needs to be a way we communicate them to smart-contracts. ENS stores names in a uint256 encoded format we call a "namehash". This is done to optimize for gas, performance, and more.
// https://github.com/ensdomains/ensjs-v3
import { namehash } from '@ensdomains/ensjs/utils';
const node = namehash('name.eth');
// https://github.com/ConsenSysMesh/ens-namehash-py
from namehash import namehash
node = namehash('name.eth')
// https://github.com/InstateDev/namehash-rust
fn main() {
let node = &namehash("name.eth");
let s = hex::encode(&node);
}
ENSjs https://github.com/ConsenSysMesh/ens-namehash-py https://github.com/InstateDev/namehash-rust
The specification for the namehash algorithm is here: https://eips.ethereum.org/EIPS/eip-137#namehash-algorithm
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.
The labelhash is just the Keccak-256 output for a particular label.
Labelhashes are used to construct namehashes, and often times a labelhash (rather than the raw label) will be the required input for various contract methods.
// https://www.npmjs.com/package/js-sha3
const labelhash = '0x' + require('js-sha3').keccak_256('name')
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 a subname or DNS name using the Name Wrapper.
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
// https://npmjs.com/package/dns-packet
const dnsEncodedBytes = require('dns-packet').name.encode('name.eth');
const dnsEncodedHexStr = '0x' + require('dns-packet').name.encode('name.eth').toString('hex');
// => 0x046e616d650365746800
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.