Skip to content

Commit

Permalink
Misc improvements, L2 primary names (#336)
Browse files Browse the repository at this point in the history
Co-authored-by: Luc van Kampen <[email protected]>
  • Loading branch information
gskril and lucemans authored Nov 15, 2024
1 parent bb09aa8 commit 34596e9
Show file tree
Hide file tree
Showing 9 changed files with 153 additions and 124 deletions.
3 changes: 0 additions & 3 deletions app/local/content/demos/listnames/ListNamesDemo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,6 @@ const Demo = () => {
</label>
<div className="flex gap-2">
<Button onClick={() => {}}>Subgraph</Button>
<Button onClick={() => {}} disabled>
Airstack
</Button>
<Button onClick={() => {}} disabled>
Alchemy
</Button>
Expand Down
1 change: 1 addition & 0 deletions app/public/_redirects
Original file line number Diff line number Diff line change
Expand Up @@ -122,3 +122,4 @@
/v/governance/governance-proposals/term-4/ep4.9-social-select-providers-for-ep4.7-streams /dao/proposals/4.9
/v/governance/governance-proposals/term-4/ep4.10-social-transfer-ens-root-key-ownership-to-the-ens-dao /dao/proposals/4.10
/dao/airdrop /dao/token
/web/namehash /resolution/names#namehash
4 changes: 4 additions & 0 deletions app/public/edgerc.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
"trailing_slash": "never"
},
"redirects": [
{
"pattern": "^/web/namehash$",
"destination": "/resolution/names#namehash"
},
{
"pattern": "^/dao/airdrop$",
"destination": "/dao/token"
Expand Down
150 changes: 93 additions & 57 deletions docs/resolution/names.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ export const meta = {
emoji: '⚙️',
contributors: [
'luc.eth',
'serenae.eth'
'serenae.eth',
'gregskril.eth',
]
};

Expand All @@ -27,82 +28,74 @@ ENS names are validated and normalized using the [ENSIP-15](/ensip/15) normaliza

Previously, [UTS-46](https://www.unicode.org/reports/tr46/) was used, but that is insufficient for emoji sequences. Correct emoji processing is only possible with [UTS-51](https://www.unicode.org/reports/tr51/). The [ENSIP-15](/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](https://support.ens.domains/en/articles/7901658-homoglyphs)

A standard implementation of the algorithm is available here: https://github.com/adraffy/ens-normalize.js. This library is also [included in ENSjs](https://github.com/ensdomains/ensjs/blob/main/packages/ensjs/src/utils/normalise.ts#L27).

To normalize a name, simply call `ens_normalize`:
A standard implementation of the algorithm is available at [@adraffy/ens-normalize](https://github.com/adraffy/ens-normalize.js). This library is used under the hood in [viem](https://viem.sh/docs/ens/utilities/normalize), [ENSjs](https://github.com/ensdomains/ensjs/blob/main/packages/ensjs/src/utils/normalise.ts#L27), and others.

```js
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"
import { normalize } from 'viem/ens';
// Uses @adraffy/ens-normalize under the hood

// note: does not enforce .eth registrar 3-character minimum
const normalized = normalize('RaFFY🚴‍♂️.eTh');
// => "raffy🚴‍♂.eth"
```

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.
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 {{ title: "Namehash", id: "namehash" }}

<Note>
You **MUST** [normalize](#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](https://github.com/ensdomains/ensjs) will automatically do this for you.
You **MUST** [normalize](#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](https://github.com/ensdomains/ensjs) will automatically do this for you.
</Note>

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.
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](/ensip/1).

### Code Examples
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.

<CodeGroup>
<CodeGroup title="Calculating Namehash">
```tsx {{ title: 'Viem (TS)', language: 'ts', variant: 'viem', link: 'https://viem.sh/docs/ens/utilities/namehash' }}
import { namehash, normalize } from "viem/ens";

```jsx {{ title: "ensjs" }}
// https://github.com/ensdomains/ensjs
const normalizedName = normalize("name.eth");
const node = namehash(normalizedName);
```

import { namehash } from '@ensdomains/ensjs/utils';
```ts {{ title: 'Ethers.js (TS)', language: 'ts', variant: 'ethers-v6', link: 'https://docs.ethers.org/v6/api/hashing/#namehash' }}
import { ensNormalize, namehash } from "ethers/hash";

const node = namehash('name.eth');
const normalizedName = ensNormalize('name.eth')
const node = namehash(normalizedName)
```

```jsx {{ title: "ens-namehash-py" }}
// https://github.com/ConsenSysMesh/ens-namehash-py

```python {{ title: "ens-namehash-py", variant: 'python', link: 'https://github.com/ConsenSysMesh/ens-namehash-py' }}
from namehash import namehash

node = namehash('name.eth')
```

```rust {{ title: "namehash-rust" }}
// https://github.com/InstateDev/namehash-rust

```rust {{ title: "namehash-rust", link: "https://github.com/InstateDev/namehash-rust" }}
fn main() {
let node = &namehash("name.eth");
let s = hex::encode(&node);
}
```

</CodeGroup>
```solidity {{ title: 'Solidity', variant: 'solidity' }}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
[ENSjs](https://github.com/ensdomains/ensjs/blob/main/packages/ensjs/src/utils/normalise.ts#L29)
https://github.com/ConsenSysMesh/ens-namehash-py
https://github.com/InstateDev/namehash-rust
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;
}
}
```
</CodeGroup>

### Algorithm

The specification for the namehash algorithm is here: https://eips.ethereum.org/EIPS/eip-137#namehash-algorithm
The specification for the namehash algorithm was originally defined in [EIP-137](https://eips.ethereum.org/EIPS/eip-137#namehash-algorithm) (same as [ENSIP-1](/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:

Expand Down Expand Up @@ -159,22 +152,72 @@ And the resulting namehash for the reverse node is:
You **MUST** [normalize](#normalize) a name before you attempt to create a labelhash! If you don't, then the hash you get may be incorrect.
</Note>

The labelhash is just the [Keccak-256](https://en.wikipedia.org/wiki/SHA-3) output for a particular label.
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.

Labelhashes are used to construct [namehashes](#namehash), and often times a labelhash (rather than the raw label) will be the required input for various contract methods.
One example of where labelhash is used is in the [BaseRegistar](/registry/eth), 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.

```js
// https://www.npmjs.com/package/js-sha3
const labelhash = '0x' + require('js-sha3').keccak_256('name')
<CodeGroup title="Calculating Labelhash">
```tsx {{ title: 'Viem (TS)', language: 'ts', variant: 'viem', link: 'https://viem.sh/docs/ens/utilities/labelhash' }}
import { labelhash, normalize } from "viem/ens";

const normalizedLabel = normalize("label");
const hash = labelhash(normalizedLabel);
```

```tsx {{ title: 'Ethers (TS)', language: 'ts', variant: 'ethers-v6', link: 'https://docs.ethers.org/v6/api/crypto/#keccak256' }}
import { keccak256 } from "ethers/crypto";
import { ensNormalize } from "ethers/hash";
import { toUtf8Bytes } from "ethers/utils";

const normalizedLabel = ensNormalize('label')
const labelhash = keccak256(toUtf8Bytes(normalizedLabel))
```

```ts {{ title: 'Solidity', variant: 'solidity' }}
string constant label = "label";
bytes32 constant labelhash = keccak256(bytes(label));
```
</CodeGroup>

## DNS Encoding {{ title: "DNS Encoding", id: "dns" }}

<Note>
You **MUST** [normalize](#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.
</Note>

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.
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](/wrapper/overview) or resolving data with [ENSIP-10](/ensip/10).

<CodeGroup title="DNS Encoding">
```tsx {{ title: 'Viem (TS)', language: 'ts', variant: 'viem', link: 'https://viem.sh/docs/ens/utilities/labelhash' }}
import { toHex } from 'viem/utils'
import { packetToBytes } from 'viem/ens'

const name = 'name.eth'
const dnsEncodedName = toHex(packetToBytes(name))
```

```tsx {{ title: 'Ethers (TS)', language: 'ts', variant: 'ethers-v6', link: 'https://docs.ethers.org/v6/api/hashing/#dnsEncode' }}
import { dnsEncode } from 'ethers/lib/utils'

const dnsEncodedName = dnsEncode('name.eth')
```

```ts {{ title: 'Solidity', variant: 'solidity' }}
// 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;
}
}
```
</CodeGroup>

### Algorithm

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

Expand All @@ -194,13 +237,6 @@ For example, to DNS-encode `my.name.eth`:

Final result: `0x026d79046e616d650365746800`

```js
// 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
```

<Note>
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](/wrapper/overview), as that contract uses the DNS-encoded name.
</Note>
9 changes: 4 additions & 5 deletions docs/resolvers/ccip-read.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,13 @@ This example application allows you to claim a subname for 24 hours.
The name [offchaindemo.eth](https://ens.app/offchaindemo.eth) with resolver [0xDB3...4D27](https://etherscan.io/address/0xDB34Da70Cfd694190742E94B7f17769Bc3d84D27#code), reverts with [OffchainLookup](https://eips.ethereum.org/EIPS/eip-3668) and directs the client to a gateway url.
The [gateway url](https://ens-gateway.gregskril.workers.dev/lookup/{sender}/{data}.json) returns the information and loads it from a temporary database.

## EVMGateway
## Unruggable Gateway

The EVMGateway is a gateway that allows you to load data from specific Layer 2's whose proofs are verifyable on L1.
This means if you are looking to load data from Optimism or Arbitrum, the EVMGateway allows you to trustlessly do so.
The Unruggable Gateway is a gateway that allows you to load data from specific Layer 2's whose proofs are verifyable on L1.
This means if you are looking to load data from Optimism or Arbitrum, the Unruggable Gateway allows you to trustlessly do so.

<div>
<Repository src="ensdomains/evmgateway" />
<Repository src="unruggable-labs/unruggable-gateways" />
</div>

## CCIP Read Flow
Expand All @@ -86,7 +86,6 @@ The URL for this gateway is determined by the `OffchainLookup` error, and is pas
<div>
<Repository src="ensdomains/offchain-resolver" description="CCIP Read Offchain ENS Resolver with Cloudflare Workers" />
<Repository src="ensdomains/offchain-gateway-rs" description="Offchain CCIP Read Gateway Resolver implementation in Rust (& postgres)" />
<Repository src="ensdomains/offchain-resolver-example" />
<Repository src="gskril/ens-offchain-resolver-read-from-api" />
</div>

Expand Down
19 changes: 0 additions & 19 deletions docs/web/enumerate.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -58,25 +58,6 @@ ENSjs makes it easy to run common queries on the subgraph with strong type safet
}
```

## Airstack {{ title: "Airstack" }}

Airstack is an API provider that includes a GraphQL endpoint to query all ENS names owned by a given address.

```graphql {{ title: 'GraphQL', language: 'gql', variant: 'gql', link: "https://app.airstack.xyz/api-studio" }}
{
Domains(
input: {
filter: { owner: { _eq: "0x179A862703a4adfb29896552DF9e307980D19285" } }
blockchain: ethereum
}
) {
Domain {
name
}
}
}
```

## Alchemy {{ title: "Alchemy" }}

Alchemy has several API endpoints for fetching NFTs, which we can use to query a list of names owned by a given address.
Expand Down
11 changes: 0 additions & 11 deletions docs/web/namehash.mdx

This file was deleted.

25 changes: 20 additions & 5 deletions docs/web/reverse.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,12 @@ This allows us to turn any address into a human-readable name.

```tsx {{ meta: 'focus=4:8', variant: 'wagmi', link: 'https://wagmi.sh/react/hooks/useEnsName', stackblitz: 'https://stackblitz.com/edit/ens-wagmi-use-ens-name' }}
import { useEnsName } from 'wagmi';
import { mainnet } from 'wagmi/chains';

export const Name = () => {
const { data: name } = useEnsName({
address: '0x225f137127d9067788314bc7fcc1f36746a3c3B5',
chainId: mainnet.id, // resolution always starts from L1
});

return <div>Name: {name}</div>;
Expand Down Expand Up @@ -114,16 +116,29 @@ func main() {

In some cases you might want to encourage users to set their primary name.
This might be in the event you are issuing names, or want people to be part of a community.
Most users are expected to do this through the ENS Manager, however it is totally doable from 3rd-party platforms as well.

<CodeGroup title="Setting Primary Name">
Currently, primary names are only support on L1 mainnet. Soon, primary names are also coming to L2s and are already available on testnets. The examples below use the testnet deployments, for which the (latest code can be found here)[https://github.com/ensdomains/ens-contracts/pull/379].

Deployments for the latest L2 reverse registrars, the contracts that power L2 primary names,

| L2 Testnet Chain | Address |
|------------------|------------------------------------------- |
| Base Sepolia | 0xa12159e5131b1eEf6B4857EEE3e1954744b5033A |
| OP Sepolia | 0x74E20Bd2A1fE0cdbe45b9A1d89cb7e0a45b36376 |
| Arbitrum Sepolia | 0x74E20Bd2A1fE0cdbe45b9A1d89cb7e0a45b36376 |
| Scroll Sepolia | 0xc0497E381f536Be9ce14B0dD3817cBcAe57d2F62 |
| Linea Sepolia | 0x74E20Bd2A1fE0cdbe45b9A1d89cb7e0a45b36376 |

On these chains, you can set a primary name for the sender via `setName()` most simply, or via signature.

`setNameForAddrWithSignature()` can be used for EOAs or smart contracts with an ERC-1271 signature, while `setNameForAddrWithSignatureAndOwnable()` can be used when a smart contract has an explicit `owner()`.

{/* <CodeGroup title="Setting Primary Name">
```tsx {{ meta: 'focus=4:9', variant: 'wagmi' }}
// TODO: Write Code Snippet
```
```ts {{ variant: 'ethers-v5' }}

```
```ts {{ variant: 'ensjs' }}
Expand All @@ -150,7 +165,7 @@ from ens.auto import ns
ns.setup_name('myname.eth', my_address)
```
</CodeGroup>
</CodeGroup> */}

### Do's and Dont's

Expand Down
Loading

0 comments on commit 34596e9

Please sign in to comment.