Buidler DAO:以ENS为例深度分析Web3域名系统的技术设计
原文标题:《Buidler DAO:以 ENS 为例深度分析 Web3 域名系统的技术设计》
原文作者:@axtrur,Buidler DAO 研究员
Web3 域名系统,简而言之就是基于区块链的分布式、去中心化的命名系统,与 DNS(互联网名称服务)类似,将地址(钱包地址或智能合约地址)解析成可读性的名称,本文以 ENS 为例从整体架构到合约细节,深度剖析 web3 域名系统的设计。
ENS 前置概念
域名层级
类似 DNS,ENS 域名层级同样分为
根域名,即"“域名
一级域名,类似.com,.cn,在 ens 中有.eth 和.reverse( 用来记录反向解析,后面会提到 )
二级域名,即用户注册的域名,比如 axtrur.eth
三级域名,用户注册了二级域名之后,可以创建或修改该二级域名下的三级域名,比如 app.axtrur.eth
NameHash 算法
由于智能合约直接与可读的域名进行交互效率低,因此 ENS 采用固定长度的 256 位加密哈希作为域名记录。namehash 是一个递归算法,可从任意域名的 NameHash 值推导出任意子域名的值,而无需知道原域名的真实文本字符串,同时符合域名的多层级特性。
Namehash(”") = “0x0000000000000000000000000000000000000000000000000000000000000000” Namehash(“eth”) = keccak256(Namehash(""), keccak256(“eth”)) Namehash(“axtrur.eth”) = keccak256(Namehash(“eth”), keccak256(“axtrur”)) Namehash(“app.axtrur.eth”) = keccak256(Namehash(“axtrur.eth”), keccak256(“app”))
node
在 ens 中,用户注册的域名比如 axtrur.eth,会采用 namehash 算法生成的哈希 node 去记录链上数据,后文中我们将提到的 node 理解为一个域名名称比如 axtrur 即可,关于 ENS 名称的处理请参考 https://ensuser.com/docs/contract-api-reference/name-processing.html
ENS 模块概念
注册器合约(绿色部分):负责分配名称的合约,有正向注册器,反向注册器,DNS 注册器。
解析器合约(蓝色部分):负责记录域名映射关系的合约,分正向、反向解析器,其中正向解析器合约也可自定义实现:
正向解析(主网上有默认的公共解析器合约):负责记录域名所绑定的内容,即域名的 nameNode(比如 axtrur.eth,app.axtrur.eth)到(包括不限于 name, addr, txt, contenthash 等内容)的解析,可设置各类币种的钱包地址,还可以设置 IPFS 的内容哈希,甚至记录邮箱等第三方账号作为文本记录。
反向解析(主网上有默认的反向解析器合约):负责记录钱包地址所绑定的域名,即反向域名的 nameNode(比如 {{钱包地址}}.addr.reverse) 到 域名名称的解析
根合约:是根域名的 owner,拥有一级域名的管理权限
控制器合约:官网的注册入口合约(如果需要实现不同的玩法合约,则统一归为控制器合约模块,需要将对应注册器合约地址设置给注册器,才有权限操作注册器进行域名 NFT 注册与记录反向解析)
价格预言机:ENS 定价采用的是 U 本位(usdt),所以需要 USDT 预言机来计算某一时刻的注册费的 eth 换算值,ENS 主网上的注册费为:
长度为 5+ 个字符的域名:每年支付 5 美元
长度为 4 个字符的域名:每年支付 160 美元
长度为 3 个字符的域名:每年支付 640 美元
DNSSEC 预言机:DNS 安全扩展预言机合约,负责校验证明 web3 域名的所有权和有效性
ENS 模块解析
注册表合约 (EnsRegistry.sol)
注册表是 ENS 最核心的合约,上图为注册表合约内部的 records 结构,维护着域名层级 node 对应的 owner、解析器、ttl 信息注册表是 ENS 最核心的合约,上图为注册表合约内部的 records 结构,维护着域名层级 node 对应的 owner、解析器、ttl 信息。
除了注册表信息 records 维护,合约还维护 owner 的委托管理者信息 operators,owner 可以通过添加设置委托管理者地址(可以是用户地址,也可以是合约地址)来共同管理域名信息 合约中相关管理设置接口(比如设置解析器,ttl,以及创建和修改子域名),都会通过修饰器`authorised(node)`来限制调用权限;该修饰器将判断该接口的交易请求者是否为当前域名的 owner,或者委托管理者地址,保证了仅有域名的 owner 或委托者才有创建下一级子域名的权限。同时这里部署初始化的时候将`」」`根域名的 node 的 owner 设置为部署者,只有这样,部署者才能将根域名的 owner 设置给 Root 合约根合约。
(Root.sol)根合约是根域名的 owner,同时根合约作为根域名的 owner,有权限调用注册表合约的 setSubnodeOwner 接口,将域名.eth 的 owner 指向基础注册器合约。
基础注册器合约(BaseRegistrarImplementation.sol)
由于 Root 合约将域名.eth 的 owner 指向基础注册器合约(又称正向注册器合约),从而基础注册器拥有.eth 底下的二级域名的设置权限,使得用户可以通过基础注册器合约进行域名注册;同时该注册器合约继承了 ERC721 协议标准,这也就是为什么 ENS 域名可以作为 NFT 在交易市场比如 opensea 上买卖的原因。除此之外,基础注册器合约还维护着每个域名的过期时间 expiries,注册器为每个域名设置了 90 天的保护期,当域名过期后且在保护期内,域名拥有者可以通过调用续期 renew 接口进行续期,如果超过了保护期,则需要重新注册(这里重新注册会先销毁 NFT 在重新 mint)。同时在 ENS 设计中,注册器合约(不管是正向注册器还是反向注册器)基本上都有 controllers 结构,维护着可信的 controller 注册器合约,只有可信合约才可进行调用。
控制器合约(ETHRegsiterController.sol)
用户在官网中,将要注册的域名等注册信息传给控制器合约,控制器合约通过预言机计算该域名的价格,同时将域名通过 namehash 转成 node 后传给基础注册器进行域名 NFT 的注册,同时将域名相关注册表信息写入注册表合约完成注册,同时域名的 owner 可以在官网通过注册表合约进行管理操作,官网中的注册页面如下:
核心注册流程:
ENS 注册采用「请求 - 提交」两阶段注册模式 ENS 注册采用「请求 - 提交」两阶段注册模式,为什么需要两阶段提交?我们知道以太坊节点从交易池 pool 中捞取交易是会按照交易给的 gas 费进行优先级排序;在注册者携带待注册域名构造的交易提交上链前,在整个网络是公开透明的,恶意的攻击者可以监听并解析此类待上链交易,并构造相同域名的注册交易,通过提高 gas 费的方式抢先上链注册控制器合约注册。
为了防止此类域名抢注问题,ENS 采用了先请求,后提交的注册模式。在第一阶段并不直接提交域名,而是先调用
makeCommitment 接口根据待申请域名 name、待申请地址 owner、随机值 secret 进行哈希后生成一条特殊的 commitment 后,通过 commit 提交上链。
提交阶段的 commitment 记录着当前时间戳,同时 ENS 设置 commitment 的有效期为 60s 到 86400s 之间;第二阶段注册的时候合约会重新计算 commitment,判断是否与第一阶段提交的一致,同时检查 Commitment 的有效期,保证跟第一阶段的链上处理时间间隔 1 分钟以上,保证记录了第一阶段交易的区块经过了至少 5 个后续区块的确认。(此时攻击者虽然可以获取域名值,但由于只有第一阶段的 owner 需要根第二阶段的 owner 一致才能生成一致的 commitment,从而避免了被抢注的风险)
用户在官网的第二阶段注册流程实际上是代码中的 resolver != address(0) 逻辑分支,因为 ENS 默认会将注册的 resolver 解析器设置为默认的公共正向解析器(publicResolver 后面会提到),这里为什么需要将域名注册给合约本身然后在转移给用户呢?因为上文中我们提到注册表合约中只有 owner 或者委托管理者才有权限设置解析器或更新 owner,所以为了帮用户设置好解析器,需要通过基础注册器注册(register)给合约自身,再通过注册表合约设置解析器(setResolver),然后声明所有权(reclaim),最后才转移给注册者(transferFrom)。
解析器(Resolver)
ENS 中的解析器合约分为正向解析和反向解析,解析记录是 ENS 比较重要的内容,只有定义好规范,生态才能方便的即成 ENS 这类 web3 域名系统。
正向解析(ENS 默认的正向解析器合约 PublicResolver.sol 或者自定义解析器合约。)
负责将域名映射为对应用户设置的内容(包括币种地址,ipfs 内容 hash,通用 text 记录等等。
首先 metamask 会通过注册表合约获取域名 node 设置的解析器地址(默认的公共解析器,也可以是用户自定义的解析器合约地址),然后与该解析器地址交互,获取用户设置的 eth 的币种地址(官网注册默认会设置成注册者,注册者后续可自由更改)进行转账操作。
反向解析(ENS 默认反向解析器合约 DefaultReverseResolver.sol)
负责将用户钱包地址映射为对应的域名。
反向解析实际上是对用户不透明的,用户也无法像正向解析器合约那样可以自定义。用户也可以通过反向注册器(ReverseRegistrar.sol)的 setName 方法设置当前钱包地址要绑定的域名,反向记录同样在 ENS 注册表合约维护,用户注册的反向记录在三级域名记录中,格式为:具体用户地址.addr.reverse
设置反向解析之后,opensea 用户界面会将用户钱包地址展示为可读的 ENS 域名,则是反向解析的过程解析器结构以及 node 对应的注册表信息。
根域名的 owner 是根域名:
.eth 一级域名的 owner 是正向注册器也就是(BaseRegistrarImplement.sol)
用户注册的二级、三级域名的 owner 是用户本身,同时可以自由设置解析器合约地址
.reserve 一级域名 owner 是 ENS 的多签钱包地址
.addr.reserve 二级域名 owner 是反向注册器,ens 当前主网版本控制器合约注册的时候默认通过反向注册器(ReverseRegistrar.sol)设置反向解析记录(比如具体用户地址.addr.reverse 指向 axtrur.eth),用户无需提供 gas 之外的反向注册费。
用户注册域名的反向三级域名的 owner 都默认指向反向注册器合约,同时 resolver 默认指向反向解析器合约
上面我们已经把 ENS 域名合约设计以及主要的模块梳理完了,ENS 在设计上比如模块拆分,权限拆分方面都是值得我们借鉴的,但是目前主网上的 ENS 也存在一些问题。
ENS 存在问题与解决
1、零宽问题:这是 ENS 目前比较麻烦的问题,因为合约设计之初并没有限制零宽字符(关于零宽问题解释 https://mirror.xyz/0xc952fE149b640097054CFa53cAf7aC2bfd0162C5/RW6psQ2mnxyzmQx08PUXgLXOZc0kjvfXm8RGRVu8s0Y),比如可以官网注册某个域名的时候,如果该域名已被注册,此时用 https://unicode-table.com/en/200B/ 拷贝对应某种零宽字符串插入到要注册的域名中间某个位置,则可以注册对应的域名了。
2、特殊字符:ENS 合约并没有限制.,emoji 表情等特殊字符过滤,以致于目前交易市场存在太多冗杂非规范的域名。
目前 ENS 官网已经对特殊字符进行过滤,并给予必要的警告提示(但是合约本身并没有限制,所以科学家一样可以通过合约进行注册)
3、transfer 问题:ENS 目前有个比较麻烦的问题就是域名 NFT 在转移的时候,owner 没有同步转移,所以当你在交易市场买了一个 ENS 域名 NFT 的时候,你需要通过基础注册器合约的 reclaim 接口,消耗一定的 gas 费声明 NFT 所有权后,才能到 ens 官网上看到自己拥有的域。
4、tokenURI 问题:ENS 的基础注册器合约并没有即成 ERC721 标准的 tokenURI,可能是设计之初没有考虑好,所以目前我们在交易市场比如 opensea 上的 ENS 的 NFT 的 metadata,是交易市场特殊对 ENS 即成了 ENS 中心化的 metaservice 的 API(比如:https://metadata.ens.domains/mainnet/0x57f1887a8bf19b14fc0df6fd9b2acc9af147ea85/92165218023603606815515740961699344403389102980529428548045197994533319339809)。
5、保留字:这是我觉得.bit 这方面做得比较好的方面,.bit 官方会把 web2 世界中的机构或公司名称保留下来,便于后续 web2 与 web3 之间的连接(https://github.com/dotbitHQ/Documents/blob/main/Reserved_DAS/Reserved_DAS_List.md)这对于 web3 域名生态发展是有意义。
6、基础合约可升级,ENS 目前对于基础模块并没有采用代理模式支持合约可升级,这样当未来需要对基础模块升级的时候是比较麻烦的,一种是 fallback 一种是迁移数据,但这都只能解决部分问题,这个方面 ENS 是有改善空间的。
我们可以怎么解决上面那些问题呢?
1、字符问题,我们可以在控制器合约的 valid 函数修改逻辑,一种实现方式是限制零宽等特殊字符比如 spaceid(https://github.com/Space-ID/SpaceIDContract-Audit/blob/main/contracts/bnbregistrar/BNBRegistrarControllerV9.sol#L88),另一种就是只允许符合规范的字符。
2、transfer 问题:我们可以在基础注册器里复写 transferFrom 和 saveTransferFrom 函数,在转移 nft 的同时调用 setSubnodeOwner 转移 owner。
3、tokenURI 问题:这个比较简单我们只要继承 ECR721 的 tokenURI 标准呢就可以了,那怎么实现在图片中动态的域名的展示呢?我们可以采用 svg 上链(可以看文章后面改造后的合约代码的 TokenURIBuilder.sol)。
4、保留字问题:可以将保留字以及对应要保留的钱包地址上链,先保留给合约本身,后面可以通过 apply 接口申请给某个特定地址。
5、基础合约可升级:我们可以采用代理模式 (eip-1967) 对基础模块合约进行改造,感兴趣可以参考 lens-protocol 的合约设计(https://github.com/lens-protocol/core/tree/main/contracts)。
DNS 模块
ENS 的 DNS 能力并不是我们说的 web2 域名系统比如.com 可以实现在浏览器里访问域名来访问你的 ens 域名,ENS 的 dns 注册实际上只是基于 DNS 安全扩展,通过相关的证明,校验算法证明你对于该 web2 的域名的所有权,然后在链上做一个(web2 域名到钱包地址)的记录,使得我们可以用 web2 域名进行链上转账。详见(https://ensuser.com/docs/dns-registrar-guide.html)
但是本文为什么我们没有详细讲 ENS 的 DNS 模块呢?是因为 ENS 虽然花了大部分精力在实现 DNS,但是这个功能放在 ENS 比较鸡肋,用的人很少。其实这实际上是一个 did 聚合的范畴,类似的能力个人觉得更适合放到聚合 DID 中去实现,比如像 mask network 的 nextid,cloak network 的 zkid。可以把 proof 做好,向即成 twitter 等 web2 的 handler 一样,去集成 web2 域名。
部署自己的 web3 域名
本文最后给大家提供一个改造后的 ENS 域名合约版本 ( https://github.com/axtrur/xens-contracts 改造内容以及部署方式详见 readme),方便大家自行部署自己的 web3 域名,深入理解 web3 域名系统的设计原理 部署 goerli 测试网命令
OWNER_KEY={{account private key}} INFURA_ID=c03713652e3c4ef6a3c09ea7dbf58711 npx hardhat deploy –network goerli (INFURA_ID 可以替换成自己的 infuraid,执行前删除 deployment/goerli 文件夹以及 deployment/goerli_result.json)
部署测试网 goerli 后,执行注册脚本 ens.js 注册域名
OWNKEY={{account private key}} INFURA=https://goerli.infura.io/v3/c03713652e3c4ef6a3c09ea7dbf58711 node ens.js
就可以到 opensea 测试网查看已经部署的 nft 了,比如我部署的.buidlerdao 后缀的域名就可以到 opensea 测试网查看已经部署的 nft 了,比如我部署的.buidlerdao 后缀的域名
https://testnets.opensea.io/collection/buildlerdao-name-service
总结
ENS 域名作为 web3 域名的先行者,在设计上有很多值得借鉴的地方,我们看到的.bnb,.nft 也都是基于 ens 合约基础上搭建的。希望通过本文大家对 ENS 的设计从整体到细节有个深入的深入,web3 域名不仅仅是一个 NFT,他有着更深远的意义。同时 web 域名只是一个很小的开始,相信随之普及、生态集成以及大家对 did 的探索,原生链上的可读的 web3 域名将会被聚合起来,使得每个用户在加密世界里都有个统一的名片描述,更好地去连接多链生态,连接用户。
ENS 域名系统相关 EIP 标准
EIP 137 - 注册表 https://eips.ethereum.org/EIPS/eip-137
EIP 181 - 反向注册器 https://eips.ethereum.org/EIPS/eip-181
EIP 205 - ABI 解析 (ABI()).
EIP 619 - SECP256k1 公钥解析 (pubkey()).
EIP 634 - 文本记录解析 (text()).
EIP 1577 - 内容 hash 解析 (contenthash()).
EIP 2304 - 多Token地址解析 (addr()). 新的记录类型可以随时通过 EIP 标准化程序进行定义
主网部署的 ENS 合约
注册表合约:0x00000000000c2e074ec69a0dfb2997ba6c7d2e1e
根合约:0xab528d626ec275e3fad363ff1393a41f581c5897
基础正向注册器合约:0x57f1887a8BF19b14fC0dF6Fd9B2acc9Af147eA85
反向注册器合约:0x084b1c3c81545d370f3634392de611caabff8148
默认正向解析器合约:0x4976fb03c32e5b8cfe2b6ccb31c09ba78ebaba41
默认反向解析器合约:0xa2c122be93b0074270ebee7f6b7292c7deb45047
控制器合约:0x283Af0B28c62C092C9727F1Ee09c02CA627EB7F5
ENS 域名资料
github: https://github.com/ensdomains
合约代码:https://github.com/ensdomains/ens-contracts/tree/master/contracts
线网部署版本:https://etherscan.io/accounts/label/ens
文档:https://ensuser.com/docs/contract-api-reference/ens-contracts-overview.html
交易市场:https://opensea.io/collection/ens
其他 web3 域名系统资料
.bnb
github: https://github.com/Space-ID
交易市场:https://www.element.market/collections/space-id-bnb
.bit
官网:https://www.did.id/
github:https://github.com/dotbitHQ
交易市场:https://opensea.io/collection/dotbit
.nft
官网:https://nft.space/
交易市场:https://www.element.market/collections/nft-name-service