From 34596e968e3ca8a53dd2048d6fe867c9649afec9 Mon Sep 17 00:00:00 2001 From: Greg <35093316+gskril@users.noreply.github.com> Date: Fri, 15 Nov 2024 14:44:42 +0700 Subject: [PATCH] Misc improvements, L2 primary names (#336) Co-authored-by: Luc van Kampen --- .../content/demos/listnames/ListNamesDemo.tsx | 3 - app/public/_redirects | 1 + app/public/edgerc.json | 4 + docs/resolution/names.mdx | 150 +++++++++++------- docs/resolvers/ccip-read.mdx | 9 +- docs/web/enumerate.mdx | 19 --- docs/web/namehash.mdx | 11 -- docs/web/reverse.mdx | 25 ++- docs/web/subdomains.mdx | 55 ++++--- 9 files changed, 153 insertions(+), 124 deletions(-) delete mode 100644 docs/web/namehash.mdx diff --git a/app/local/content/demos/listnames/ListNamesDemo.tsx b/app/local/content/demos/listnames/ListNamesDemo.tsx index f0018bdf..53dcee70 100644 --- a/app/local/content/demos/listnames/ListNamesDemo.tsx +++ b/app/local/content/demos/listnames/ListNamesDemo.tsx @@ -86,9 +86,6 @@ const Demo = () => {
- diff --git a/app/public/_redirects b/app/public/_redirects index 8dbffae7..00a421ce 100644 --- a/app/public/_redirects +++ b/app/public/_redirects @@ -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 diff --git a/app/public/edgerc.json b/app/public/edgerc.json index f4706864..f39e7e33 100644 --- a/app/public/edgerc.json +++ b/app/public/edgerc.json @@ -4,6 +4,10 @@ "trailing_slash": "never" }, "redirects": [ + { + "pattern": "^/web/namehash$", + "destination": "/resolution/names#namehash" + }, { "pattern": "^/dao/airdrop$", "destination": "/dao/token" diff --git a/docs/resolution/names.mdx b/docs/resolution/names.mdx index 7955aa47..71af9665 100644 --- a/docs/resolution/names.mdx +++ b/docs/resolution/names.mdx @@ -6,7 +6,8 @@ export const meta = { emoji: '⚙️', contributors: [ 'luc.eth', - 'serenae.eth' + 'serenae.eth', + 'gregskril.eth', ] }; @@ -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" }} - 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. -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. - + +```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); } ``` - +```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; + } +} +``` + ### 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: @@ -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. -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') + +```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)); +``` + + ## DNS Encoding {{ title: "DNS Encoding", id: "dns" }} 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. -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). + + +```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; + } +} +``` + + +### Algorithm To DNS-encode a name, first split the name into labels (delimited by `.`). Then for each label from left-to-right: @@ -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 -``` - 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. diff --git a/docs/resolvers/ccip-read.mdx b/docs/resolvers/ccip-read.mdx index 9b6ccea7..c8bf4c53 100644 --- a/docs/resolvers/ccip-read.mdx +++ b/docs/resolvers/ccip-read.mdx @@ -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.
- +
## CCIP Read Flow @@ -86,7 +86,6 @@ The URL for this gateway is determined by the `OffchainLookup` error, and is pas
-
diff --git a/docs/web/enumerate.mdx b/docs/web/enumerate.mdx index 425dc904..0f6bdeeb 100644 --- a/docs/web/enumerate.mdx +++ b/docs/web/enumerate.mdx @@ -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. diff --git a/docs/web/namehash.mdx b/docs/web/namehash.mdx deleted file mode 100644 index a273ed95..00000000 --- a/docs/web/namehash.mdx +++ /dev/null @@ -1,11 +0,0 @@ -import { WIP } from '@/components/wip/WIP'; - -{/** @type {import('@/lib/mdxPageProps').MdxMetaProps} */} -export const meta = { - description: 'Learn about namehash and how it works', - contributors: [] -}; - -# Namehash - - diff --git a/docs/web/reverse.mdx b/docs/web/reverse.mdx index ad3823e5..b480d665 100644 --- a/docs/web/reverse.mdx +++ b/docs/web/reverse.mdx @@ -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
Name: {name}
; @@ -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. - +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()`. + +{/* ```tsx {{ meta: 'focus=4:9', variant: 'wagmi' }} -// TODO: Write Code Snippet ``` ```ts {{ variant: 'ethers-v5' }} - ``` ```ts {{ variant: 'ensjs' }} @@ -150,7 +165,7 @@ from ens.auto import ns ns.setup_name('myname.eth', my_address) ``` - + */} ### Do's and Dont's diff --git a/docs/web/subdomains.mdx b/docs/web/subdomains.mdx index 51edbc06..2aa830c5 100644 --- a/docs/web/subdomains.mdx +++ b/docs/web/subdomains.mdx @@ -1,12 +1,12 @@ -import { WIP } from '@/components/wip/WIP'; +import { WIP } from '@/components/wip/WIP' -{/** @type {import('@/lib/mdxPageProps').MdxMetaProps} */} +{/* * @type {import('@/lib/mdxPageProps').MdxMetaProps} */} export const meta = { - description: 'Issue subdomains to your users, yourself, or your friends. Program your own resolver, or use one of the many existing ones.', - emoji: '⚙️', - contributors: [ - 'luc.eth' - ] +description: 'Issue subdomains to your users, yourself, or your friends. Program your own resolver, or use one of the many existing ones.', +emoji: '⚙️', +contributors: [ +'luc.eth' +] }; # Subdomains @@ -15,14 +15,18 @@ Names come in handy in many situations. We believe that any place an address is The smart contracts you interact with have names, the deposit address for your favorite exchange has a name, your favorite DAO has a name, or maybe you use subnames to keep your wallets organized.
-
- { - ["vault", "domico", "subname", "nick", "cold"].map((subname, i) => ( -
{subname}
- )) +
+ {['vault', 'domico', 'subname', 'nick', 'cold'].map((subname, i) => ( +
-
.nick.eth
+ > + {subname} +
+ ))} +
+
.nick.eth
Luckily, the ENS Protocol has so much to offer for you to play with. There are a variety of ways you can give out subdomains to your apps users, set them up for yourself, or more. @@ -31,20 +35,24 @@ If you are interested in naming smart-contracts specifically, check out the [Nam ## Pre-built Tooling +### Durin (new!) + +[Durin](https://durin.dev/) is an opinionated approach to issuing ENS subnames on L2. It takes care of the L1 Resolver and offchain gateway parts of the [CCIP Read stack](/resolvers/ccip-read) for you, so you can focus on the business logic of your L2 smart contracts. + ### NameWrapper The [NameWrapper](/wrapper/overview) is a smart contract that allows you to issue trustless on-chain subdomains. -NameWrapper subdomains come in the form of [ERC1155](https://eips.ethereum.org/EIPS/eip-1155) NFTs, which can be transferred, sold, or traded. - -In addition NameWrapper subnames have fuzes which allow you to limit permissions, or even emancipate a subname entirely. -You can interact directly with the namewrapper from the [ENS Manager App](https://ens.app/). +NameWrapper subdomains come in the form of [ERC1155](https://eips.ethereum.org/EIPS/eip-1155) NFTs, which can be transferred, sold, or traded. Read our guide to [creating an onchain subname registrar](/wrapper/creating-subname-registrar) to learn more. - + ### Legacy Subdomains -In the previous version of the ETHRegistrar you could register subdomains for a name. These subdomains are always owner-controlled, and can be transferred. -They are also stored on-chain, but are not in the form of an ERC1155. +In the previous version of the ETHRegistrar, you could register subdomains for a name. These subdomains are always owner-controlled, and can be transferred. They are also stored on-chain, but are not in the form of a standard NFT. You can register a legacy subdomain by calling the `setSubnodeRecord` function on the [ENS Registry](/registry/ens) contract. @@ -60,10 +68,9 @@ A resolver can leverage [CCIP Read](/resolvers/ccip-read) to load data from an o Tooling has been built to allow you to easily deploy a template resolver, or leverage a library to load data from offchain. -#### EVM Gateway +#### Unruggable Gateway -The [EVM Gateway](https://github.com/ensdomains/evmgateway) is a set of solidity tooling that allows you to write a resolver that can trustlessly load storage slots from an EVM-compatible chain. -It currently supports *Optimism* and *Arbitrum*. +The [Unruggable Gateway](https://gateway-docs.unruggable.com/) is a set of solidity tooling that allows you to write a resolver that can trustlessly load storage slots from an EVM-compatible chain. #### CCIP Tools