Name Processing

When interacting with the MNS 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.

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.MoN normalizes to name.mon. This ensures that the correct Registry node is used, no matter how the user types in the name.

MNS 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 viemENSjs, and others.

  • Viem (TS)
import { normalize } from 'viem/ens';
// Uses @adraffy/ens-normalize under the hood
 
const normalized = normalize('ALiCe🚴‍♂️.mOn');
// => "alice🚴‍♂.mon"

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

You MUST normalize a name before you attempt to create a namehash! If you don't, then the hash you get may be incorrect. Some libraries like ensjs will automatically do this for you.

In the core MNS 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. Because 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.

  • View (TS)
  • Ethers.js (TS)
  • Solidity
import { namehash, normalize } from "viem/ens";

const normalizedName = normalize("name.mon");
const node = namehash(normalizedName);

import { ensNormalize as mnsNormalize, namehash } from "ethers/hash";

const normalizedName = mnsNormalize('name.mon')
const node = namehash(normalizedName)

// 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 mns.mon, the algorithm works like so:

namehash('mns.mon') = keccak256(namehash('mon') + labelhash('mns'))
namehash('mon') = keccak256(namehash('') + labelhash('mon'))
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('mon') =
    • keccak256('mon') =
    • 0x7d074ff60790193d6f1639a7404e70caff96bb1ae486f61939ce4e42695b49a1
  • namehash('mon') =
    • keccak256(namehash('') + labelhash('mon')) =
    • keccak256(0x00000000000000000000000000000000000000000000000000000000000000007d074ff60790193d6f1639a7404e70caff96bb1ae486f61939ce4e42695b49a1) =
    • 0xc6467acde3662083e12f3fbcf8aef57155a035e49629628eb9453948d1afb379
  • labelhash('mns') =
    • keccak256('mns') =
    • 0xcca126147e5135c88a70e21a189c19200274d40391a772ac7b953f836468b45f
  • namehash('mns.mon') =
    • keccak256(namehash('mon') + labelhash('mns')) =
      • keccak256(0xc6467acde3662083e12f3fbcf8aef57155a035e49629628eb9453948d1afb379cca126147e5135c88a70e21a189c19200274d40391a772ac7b953f836468b45f) =
    • 0x4b0f178dddfa60cd4f0830ec9094ce87b72f28057b2802f16213c759aa3d8df4

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

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.mon), 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.mon) and not 3LDs+ (e.g. sub.name.mon). The token ID of a second-level .mon name in the BaseRegistar is the uint256 of the labelhash.

  • Viem (TS)
  • Ethers.js (TS)
  • Solidity
import { labelhash, normalize } from "viem/ens";

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

const normalizedLabel = mnsNormalize('label')
const labelhash = keccak256(toUtf8Bytes(normalizedLabel))


string constant label = "label";
bytes32 constant labelhash = keccak256(bytes(label));