前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >ENS源码分析

ENS源码分析

作者头像
Tiny熊
发布2022-04-08 14:00:49
1.3K0
发布2022-04-08 14:00:49
举报
文章被收录于专栏:深入浅出区块链技术

本文作者:hundredwz[1]

前言

2021 年 ENS 大火,有很多用户用户赚了不菲的空投,甚至部分用户赚了上千万。

但是 ENS 到底是怎么实现的?技术细节有什么?

目前笔者在中文网站暂未发现从技术角度进行全面讲解的文章,因此尝试从源码的角度来分析下。

以下仅代表自身感悟,如有错漏之处还请指正。

注:本文所参考的合约地址为ens-contracts[2]

概述

ens 的整个技术架构大致类似于下图

ens架构.png

如图所示,ens 可以从功能上进行以下划分:

  1. 用户层:注册 ens 的入口,通过外观模式,对请求进行相关校验,转发到下层进行实际处理
  2. 核心层:ens 的核心功能模块,包括 ens 的注册表(ens 和 owner 对应关系)、ens 注册器(注册一个实际的 ens 域名)、ens 反向注册器(通过 address 反向获取 ens 域名)、ens 注册价格计算、dns 注册关联等功能
  3. 解析器层:解析 ens 域名的实际处理层,支持将 ens 解析为地址、公钥、文本等。

注册

注册各个模块交互流程图如下:

ens注册.png

为了防止域名抢注情况,ens 使用了『请求-提交』二阶段注册模式。

用户在申请域名时,首先根据『待申请域名』和『秘密值(随机数)』生成 commitment,然后提交至 ENS 控制器。

在一分钟以后,将域名的『注册请求』和『秘密值(随机数)』一起提交到控制器,完成 ens 域名的注册。

commitment 处理

生成 commitment是一个计算哈希的过程,核心实现如下代码:

代码语言:javascript
复制
// 创建commitment
    function makeCommitment(string memory name, address owner, bytes32 secret) pure public returns(bytes32) {
        return makeCommitmentWithConfig(name, owner, secret, address(0), address(0));
    }

    function makeCommitmentWithConfig(string memory name, address owner, bytes32 secret, address resolver, address addr) pure public returns(bytes32) {
    		// 计算ens域名(不带.eth)的哈希
        bytes32 label = keccak256(bytes(name));
        if (resolver == address(0) && addr == address(0)) {
            // 将ens域名(不带.eth)哈希值、域名申请者地址、秘密值(随机数)组合计算哈希
            return keccak256(abi.encodePacked(label, owner, secret));
        }
        require(resolver != address(0));
        return keccak256(abi.encodePacked(label, owner, resolver, addr, secret));
    }
    // commit 过程
    function commit(bytes32 commitment) public {
        // maxCommitmentAge commitment最大有效时间,是24h
    		// 如果之前已经存在过该commitment,需要保证前commitment已超过最大有效期,即24小时
    		// 如果不存在该commitment,则该要求必定满足
        require(commitments[commitment] + maxCommitmentAge < block.timestamp);
        commitments[commitment] = block.timestamp;
    }

即生成 commitment,是将hash(ens name)addresssecret组合后求哈希,结果中包含了 ens name 和 secret,也不会泄露原始信息。

然后用户执行申请过程,就是将 commitment 记录到区块链的过程。

验证 commitment主要包含两个操作:

  1. 根据提交的 secret 判断 commitment 是否正确
  2. commitment 提交时间在预期范围内
代码语言:javascript
复制
// 根据提交的secret计算commitment
bytes32 commitment = makeCommitmentWithConfig(name, owner, secret, resolver, addr);
// 消耗commitment,也就是验证commitment逻辑
uint cost = _consumeCommitment(name, duration, commitment);
...
function _consumeCommitment(string memory name, uint duration, bytes32 commitment) internal returns (uint256) {

  // minCommitmentAge commitment最短有效时间,是1min
  // 如果刚提交完commitment就执行注册,该值会大于当前区块时间,不能注册通过
  require(commitments[commitment] + minCommitmentAge <= block.timestamp);

  // maxCommitmentAge commitment最大有效时间,是24h
  // 如果该commitment提交时间过早,该值会小于当前区块时间,不能注册通过
  require(commitments[commitment] + maxCommitmentAge > block.timestamp);
  // 保证该域名可用:1. 域名长度大于3;2. 该域名还未被注册或已超出保留时间(90天)
  require(available(name));
  // 验证通过,删除该commitment
	delete(commitments[commitment]);
  ...
}

价格计算

注册 ens 域名包含不同的价格,目前在 StablePriceOracle 定义中,不同的域名长度价格不同。

代码语言:javascript
复制
function price(string calldata name, uint expires, uint duration) external view override returns(uint) {
        // 计算待注册域名成都
        uint len = name.strlen();
        if(len > rentPrices.length) {
            len = rentPrices.length;
        }
        require(len > 0);
        // 计算域名注册时长*域名单价
        uint basePrice = rentPrices[len - 1].mul(duration);
        // 域名附加费用_premium价格目前定义为0
        basePrice = basePrice.add(_premium(name, expires, duration));
				// 将价格转换为eth价格
        return attoUSDToWei(basePrice);
    }
    function attoUSDToWei(uint amount) internal view returns(uint) {
    		// 通过预言机获取最新的eth/usd价格
        uint ethPrice = uint(usdOracle.latestAnswer());
        // 计算应当支付多少eth
        return amount.mul(1e8).div(ethPrice);
    }

可以发现,价格计算较为简单,

  1. 获取待注册的域名长度,进而计算一共需要支付多少 usd
  2. 通过预言机合约,获取当前的 eth/usd 价格汇率
  3. 计算当前需要支付多少 eth

注册

注册包含两种类型:1. 为 ens 域名设置解析器;2. 使用默认的解析器

代码语言:javascript
复制
// 全新注册入口
    function register(string calldata name, address owner, uint duration, bytes32 secret) external payable {
      registerWithConfig(name, owner, duration, secret, address(0), address(0));
    }
    // 具体注册逻辑
    function registerWithConfig(string memory name, address owner, uint duration, bytes32 secret, address resolver, address addr) public payable {
        // 校验commitment逻辑
        bytes32 commitment = makeCommitmentWithConfig(name, owner, secret, resolver, addr);
        uint cost = _consumeCommitment(name, duration, commitment);
				// 计算域名的哈希
        bytes32 label = keccak256(bytes(name));
        // 计算tokenId,方便铸造nft
        uint256 tokenId = uint256(label);

        uint expires;
        // 如果要设置新的解析器,执行设定解析器的逻辑
        if(resolver != address(0)) {
            // 临时设置域名的拥有者为合约地址,方便后续设置解析器
            expires = base.register(tokenId, address(this), duration);

            // 计算域名哈希(包含.eth)
            bytes32 nodehash = keccak256(abi.encodePacked(base.baseNode(), label));

            // 设置用户的指定解析器
            base.ens().setResolver(nodehash, resolver);

            // 配置解析器,将ens和指定的addr对应
            if (addr != address(0)) {
                Resolver(resolver).setAddr(nodehash, addr);
            }

            // 把ens拥有权转移给用户
            base.reclaim(tokenId, owner);
            // nft转移
            base.transferFrom(address(this), owner, tokenId);
        } else {
            // 不设定解析器,使用默认的解析器
            require(addr == address(0));
            expires = base.register(tokenId, owner, duration);
        }
        // 发起事件通知
        emit NameRegistered(name, label, owner, cost, expires);

        // 钱付多了,返还多的钱
        if(msg.value > cost) {
            payable(msg.sender).transfer(msg.value - cost);
        }
    }
默认解析器

首先分析下使用默认解析器的逻辑:

代码语言:javascript
复制
// 注册逻辑入口
    function register(uint256 id, address owner, uint duration) external override returns(uint) {
      return _register(id, owner, duration, true);
    }
    function _register(uint256 id, address owner, uint duration, bool updateRegistry) internal live onlyController returns(uint) {
        // 需要保证该ens域名可用
        require(available(id));
        // 防止申请时间过长等导致的数据溢出
        require(block.timestamp + duration + GRACE_PERIOD > block.timestamp + GRACE_PERIOD); // Prevent future overflow
        // 记录该域名的过期时间
        // 域名是否可用也是通过expires判断
        expiries[id] = block.timestamp + duration;
        // nft相关逻辑,之前被人拥有,重新铸造
        if(_exists(id)) {
            _burn(id);
        }
        _mint(owner, id);
        // 由于是全新注册域名,需要更新注册表
        if(updateRegistry) {
            ens.setSubnodeOwner(baseNode, bytes32(id), owner);
        }

        emit NameRegistered(id, owner, block.timestamp + duration);
        // 返回域名过期时间
        return block.timestamp + duration;
    }

从以上逻辑可知,注册域名的逻辑如下

  1. 域名统一使用了 expiries map 维护,记录域名的过期时间
  2. 在 ens 注册表中记录该域名的拥有者信息
  3. 由于 ens 兼容 ERC721,所以注册一个 ens 域名,也会铸造一个 NFT
自定义解析器

如果用户指定了解析器,相比于使用默认的解析器,逻辑要稍微复杂一些

  1. 将域名注册给合约自身,以便于有权限设置解析器
  2. 合约自身作为 owner,设置注册表中的解析器信息
  3. 用户若指定配置解析器,则在解析器中设定 ens<->addr 关系
  4. 设置域名的拥有者为用户指定的 owner
  5. 涉及到 NFT 的操作,就执行 NFT 的转移操作

可以发现,为了实现自定义解析器,需要临时赋予合约 ens 拥有权,执行设置解析器的操作,执行完成后,再重新授予用户。

到此位置,注册一个 ens 的域名就全部执行完成。

ENS 解析

正向解析

解析 ens 时各个模块交互如下:

可以发现,解析 ens 域名流程实现了注册表和解析器的解耦,注册表不维护具体内容,具体数据由解析器提供。

首先看下注册表设置和获取的核心逻辑

代码语言:javascript
复制
struct Record {
        address owner; // 域名拥有者
        address resolver; // 域名解析器
        uint64 ttl; // 域名解析存活时间
    }
		// ens和记录的映射关系
    mapping (bytes32 => Record) records;
    function setResolver(bytes32 node, address resolver) public virtual override authorised(node) {
        emit NewResolver(node, resolver);
        // 将解析器添加到records记录中
        records[node].resolver = resolver;
    }
    function resolver(bytes32 node) public virtual override view returns (address) {
        // 根据ens域名,返回解析器地址
        return records[node].resolver;
    }

我们以解析以太坊地址为例,查看下解析逻辑

代码语言:javascript
复制
uint constant private COIN_TYPE_ETH = 60;
    // 记录ens域名和地址的对应关系
    mapping(bytes32=>mapping(uint=>bytes)) _addresses;
		// 设置地址
    function setAddr(bytes32 node, address a) virtual external authorised(node) {
        setAddr(node, COIN_TYPE_ETH, addressToBytes(a));
    }
    function setAddr(bytes32 node, uint coinType, bytes memory a) virtual public authorised(node) {
        emit AddressChanged(node, coinType, a);
        if(coinType == COIN_TYPE_ETH) {
            emit AddrChanged(node, bytesToAddress(a));
        }
        // 将地址添加到address映射中
        _addresses[node][coinType] = a;
    }
    // 解析地址
    function addr(bytes32 node) virtual override public view returns (address payable) {
        bytes memory a = addr(node, COIN_TYPE_ETH);
        if(a.length == 0) {
            return payable(0);
        }
        return bytesToAddress(a);
    }
    function addr(bytes32 node, uint coinType) virtual override public view returns(bytes memory) {
        // 读取address映射关系,获取ens命名对应的地址
        return _addresses[node][coinType];
    }

反向解析

反向注册器的功能是实现从以太坊地址到 ens 域名的解析。类似正向正向注册器支持『.eth』,反向注册器支持的是『.addr.reverse』。

理解了正向解析后,反向解析就比较容易理解:

  1. msg.sender 地址求 hex
  2. 在 ens 注册表中添加namehash(hex(msg.sender).addr.reverse)=>owner的管理关系,即为反向域名设置Record
  3. 在反向解析器中设置namehash=>address

实现的代码逻辑为

代码语言:javascript
复制
// 设置反向域名解析
    function setName(string memory name) public returns (bytes32) {
        // 在ens注册表中添加反向域名的record
        bytes32 node = _claimWithResolver(
            msg.sender,
            address(this),
            address(defaultResolver)
        );
        // 在反向解析器中添加反向域名到ens域名的映射关系
        defaultResolver.setName(node, name);
        return node;
    }
    function _claimWithResolver(
        address addr,
        address owner,
        address resolver
    ) internal returns (bytes32) {
        // 求address的hex编码
        bytes32 label = sha3HexAddress(addr);
        // 计算namehash
        bytes32 node = keccak256(abi.encodePacked(ADDR_REVERSE_NODE, label));
        // 获取当前address有没有设定反向解析器,如果已设置,就判断是否需要更新
        address currentResolver = ens.resolver(node);
        bool shouldUpdateResolver = (resolver != address(0x0) &&
            resolver != currentResolver);
        address newResolver = shouldUpdateResolver ? resolver : currentResolver;
        // 在ens注册表中添加对应关系
        ens.setSubnodeRecord(ADDR_REVERSE_NODE, label, owner, newResolver, 0);

        emit ReverseClaimed(addr, node);

        return node;
    }

DNS 解析

通过前文的 ens 正向解析和反向解析分析可知,往注册表添加数据的关键是,证明自身对数据的所有权,DNS 解析亦如此。

  1. 正向解析修改注册表是通过有且仅有ENS 基础注册器具有『eth』这一 baseNode 操作权,作为唯一 eth 域名分配入口,保证分配给用户的域名一定是经过系统控制的
  2. 反向解析的输入数据是msg.sender,除了调用者自身,其他人都不可能给msg.sender设定反向解析记录

那么 DNS 解析是怎么操作的呢?

dns解析.png

从交互图可以理解 DNS 解析的交互逻辑

  1. 需要去域名提供商手动添加一条 A 记录,其中域名是_ens.{domain}.{suffix},a 记录内容是a=address
  2. 自身获取 A 记录的 proof,方便 DNS 预言机验证
  3. DNS 预言机内置了根公钥和支持的部分域名的证明,按照类似于证书验证体系,即验证树的形式完成 proof 的验证
  4. 有了证明,就可以向 DNS 注册器提交 DNS 绑定了
  5. 将该 DNS 记录添加到 ENS 注册表中

相关代码如下:

代码语言:javascript
复制
// 提交证明
    function submitRRSets(RRSetWithSignature[] memory input, bytes calldata _proof) public override returns (bytes memory) {
        bytes memory proof = _proof;
        for(uint i = 0; i < input.length; i++) {
            proof = _submitRRSet(input[i], proof);
        }
        return proof;
    }
    // 验证并存储证明
    function _submitRRSet(RRSetWithSignature memory input, bytes memory proof) internal returns (bytes memory) {
        RRUtils.SignedSet memory rrset;
        // 验证证明
        rrset = validateSignedSet(input, proof);

        RRSet storage storedSet = rrsets[keccak256(rrset.name)][rrset.typeCovered];
        if (storedSet.hash != bytes20(0)) {
            // To replace an existing rrset, the signature must be at least as new
            require(RRUtils.serialNumberGte(rrset.inception, storedSet.inception));
        }
        // 存储证明
        rrsets[keccak256(rrset.name)][rrset.typeCovered] = RRSet({
            inception: rrset.inception,
            expiration: rrset.expiration,
            hash: bytes20(keccak256(rrset.data))
        });

        emit RRSetUpdated(rrset.name, rrset.data);

        return rrset.data;
    }
    // 声明域名拥有权
    function claim(bytes memory name, bytes memory proof) public override {
        // 获取要存储到注册表的数据
        (bytes32 rootNode, bytes32 labelHash, address addr) = _claim(name, proof);
        // 添加到ens注册表
        ens.setSubnodeOwner(rootNode, labelHash, addr);
    }

其他

1. 注册表权限是如何管理的?

注册表实现了一个authorised验证逻辑,只有域名拥有者或授权操作者才能执行相关写操作。

代码语言:javascript
复制
modifier authorised(bytes32 node) {
        address owner = records[node].owner;
        require(owner == msg.sender || operators[owner][msg.sender]);
        _;
    }

合约在部署时,默认赋予了合约部署者0x0的操作权限。合约部署者后续会调用setSubnodeOwner赋予特定用户操作各个域名的权限,比如eth

代码语言:javascript
复制
// 合约部署者,提交node为0x0,label为eth,owner为指定用户,即赋予了特定用户操作eth的权限
    function setSubnodeOwner(bytes32 node, bytes32 label, address owner) public virtual override authorised(node) returns(bytes32) {
        bytes32 subnode = keccak256(abi.encodePacked(node, label));
        _setOwner(subnode, owner);
        emit NewOwner(node, label, owner);
        return subnode;
    }

调用记录可查看etherscan 交易[3]

2. 购买 eth 域名,默认没有配置解析器和 ttl?

是的

3. ens 的域名是怎么存储的?

在 ens 注册表中,所有的域名都会进行 namehash 计算,然后使用 bytes32 进行存储。

namehash 算法定义是

代码语言:javascript
复制
def namehash(name):
  if name == '':
    return '\0' * 32
  else:
    label, _, remainder = name.partition('.')
    return sha3(namehash(remainder) + sha3(label))

比如有一个域名为mysite.swarm,则计算方式为

代码语言:javascript
复制
node = '\0' * 32
node = sha3(node + sha3('swarm'))
node = sha3(node + sha3('mysite'))
# 计算结果
namehash('') = 0x0000000000000000000000000000000000000000000000000000000000000000
namehash('eth') = 0x93cdeb708b7545dc668eb9280176169d1c33cfd8ed6f04690a0bcc88a93fc4ae
namehash('foo.eth') = 0xde9b09fd7c5f901e23a3f19fecc54828e9c848539801e86591bd9801b019f84f

使用这种形式有以下几种原因:

  1. 合约不需要处理可读的文本字符串,降低不同编码的影响
  2. 从一个域名(bob.eth)的 namehash 可以推导任意子域名(alice.bob.eth)的 namehash
  3. 推导过程无需知道或处理原域名(bob.eth)

参考资料

[1]hundredwz: https://learnblockchain.cn/people/493

[2]ens-contracts: https://github.com/ensdomains/ens-contracts/tree/fb88681d476e8cabc642781262043b8d521fefd5

[3]etherscan交易: https://etherscan.io/tx/0x057a18943891fc4defd54ff6b18c4fa1e15b822f299f2f08117e4fd11d44f971

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-02-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 深入浅出区块链技术 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 概述
  • 注册
    • commitment 处理
      • 价格计算
        • 注册
          • 默认解析器
          • 自定义解析器
      • ENS 解析
        • 正向解析
          • 反向解析
            • DNS 解析
            • 其他
              • 1. 注册表权限是如何管理的?
                • 2. 购买 eth 域名,默认没有配置解析器和 ttl?
                  • 3. ens 的域名是怎么存储的?
                    • 参考资料
                    领券
                    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档