# ENS注册流程解析

By [franx.eth](https://paragraph.com/@franx-2) · 2022-05-17

---

1\. 注册
------

ENS的注册分为两步，先commit预提交，再registerWithConfig注册。

先看下commit的代码，客户端会先调用makeCommitmentWithConfig获得commitment参数，再调用commit进行预提交。

    //存储预提交记录的时间戳
    mapping(bytes32=>uint) public commitments;
    //最小预提交间隔时间，单位：秒
    uint public minCommitmentAge;
    //最大预提交间隔时间，单位：秒
    uint public maxCommitmentAge;
    /**
    * @dev 生成commitment参数
    * @param name ens名
    * @param owner 注册者
    * @param secret 32随机字节
    * @param resolver 正向解析器地睛
    * @param addr 正向解析目标地址
    */
    function makeCommitmentWithConfig(string memory name, address owner, bytes32 secret, address resolver, address addr) pure public returns(bytes32) {
      bytes32 label = keccak256(bytes(name));
      if (resolver == address(0) && addr == address(0)) {
        return keccak256(abi.encodePacked(label, owner, secret));
      }
      require(resolver != address(0));
      return keccak256(abi.encodePacked(label, owner, resolver, addr, secret));
    }
    /**
    * @dev 预提交
    * @param commitment 提交参数
    */
    function commit(bytes32 commitment) public {
      require(commitments[commitment] + maxCommitmentAge < block.timestamp);//当重复预提交时，如果时间还未超过最大时间间隔，就不用再重新更新时间戳
      commitments[commitment] = block.timestamp;//记录本此提交的时间戳
    }
    

    import crypto from 'crypto'
    function randomSecret() {
      return '0x' + crypto.randomBytes(32).toString('hex')
    }//生成makeCommitmentWithConfig的secret参数，实际是32随机字节
    

此步预提交的目的是为了防止抢跑，如果没有这一步的话，直接一步就是注册域名成功，那么在mev里可以监听注册域名的tx，然后用gasPrice去抢跑，那么一些抢手的域名注册时就可能会被机器人抢走。加上这个预提交步骤后，机器人如果要抢跑域名注册也要监听commit方法，但这个方法的参数的bytes32，解读不出tx是要注册哪个域名，机器人自然就抢不了；而如果机器人直接监听第二步registerWithConfig，但没有先进行第一步预提交tx，自然也成功不了。

下面来看下registerWithConfig方法

    contract ETHRegistrarController is Ownable {
        BaseRegistrarImplementation base;
        PriceOracle prices;
        uint public minCommitmentAge;
        uint public maxCommitmentAge;
        mapping(bytes32=>uint) public commitments;  
        /**
        * @dev 注册ens并配置
        * @param name ens名
        * @param owner 注册者
        * @param duration 域名有效时间
        * @param secret 32随机字节
        * @param resolver 正向解析器地睛
        * @param addr 正向解析目标地址
        */
        function registerWithConfig(string memory name, address owner, uint duration, bytes32 secret, address resolver, address addr) public payable {
            bytes32 commitment = makeCommitmentWithConfig(name, owner, secret, resolver, addr);
            uint cost = _consumeCommitment(name, duration, commitment);
    
            bytes32 label = keccak256(bytes(name));
            uint256 tokenId = uint256(label);
    
            uint expires;
            //存在正向解析器
            if(resolver != address(0)) {
                //mint NFT,设置ens的注册者为address(this)，这里先临街把注册者设为当前合约地址，
                //因为后面要setResolver时，要求msg.sender是合约注册者，如果直接把注册者给到owner，那在setResolver时就会fail
                expires = base.register(tokenId, address(this), duration);
    
                //计算nodehash
                bytes32 nodehash = keccak256(abi.encodePacked(base.baseNode(), label));
    
                //设置正向解析器地址
                base.ens().setResolver(nodehash, resolver);
    
                //如果正向解析地址不为0，就设置正向解析地址
                if (addr != address(0)) {
                    Resolver(resolver).setAddr(nodehash, addr);
                }
    
                //用owner重新认领这个ens
                base.reclaim(tokenId, owner);
                //转移erc721 NFT给owner
                base.transferFrom(address(this), owner, tokenId);
            } else {//不存在正向解析器，直接把注册者设为owner就可以了
                require(addr == address(0));
                expires = base.register(tokenId, owner, duration);
            }
            //emit注册事件
            emit NameRegistered(name, label, owner, cost, expires);
    
            //发送过来的eth超过了费用，就退还
            if(msg.value > cost) {
                payable(msg.sender).transfer(msg.value - cost);
            }
        }
        /**
        * @dev 消费commitment
        * @param name ens名
        * @param duration 域名有效时间  
        * @param commitment 参数 
        */
        function _consumeCommitment(string memory name, uint duration, bytes32 commitment) internal returns (uint256) {
            //commit之后必须等待最小间隔时间后才能注册，当前是60秒
            require(commitments[commitment] + minCommitmentAge <= block.timestamp);
            //不能超过commit之后的最大间隔境，当前是7天
            require(commitments[commitment] + maxCommitmentAge > block.timestamp);
            require(available(name));//这个ens要可用
            delete(commitments[commitment]);//删除预提交信息，gas返还
            uint cost = rentPrice(name, duration);//根据域名购买的有效时间，计算费用
            require(duration >= MIN_REGISTRATION_DURATION);//有效时间不能小于最小时间，当前是28天
            require(msg.value >= cost);//传入的eth要>=费用
            return cost;
        }
        /**
        * @dev 价格计算
        * @param name ens名
        * @param duration 域名有效时间  
        */
        function rentPrice(string memory name, uint duration) view public returns(uint) {
            bytes32 hash = keccak256(bytes(name));
            return prices.price(name, base.nameExpires(uint256(hash)), duration);
        }
    }
    

    contract StablePriceOracle is Ownable, PriceOracle {    
        //索引2，3，4分别存储域名3,4,5位及以上的每秒的usd费用
        //现在是3位每年640美金，4位每年160美金，5位及以上每年5美金
        uint[] public rentPrices;
        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);//每秒费用*有效时间
            basePrice = basePrice.add(_premium(name, expires, duration));//这里_premium固定是0
    
            return attoUSDToWei(basePrice);//根据预言机转成wei价格，用的是MakerDAO的medianizer预言机
        }
    }
    

    contract BaseRegistrarImplementation is ERC721, BaseRegistrar  {
        // A map of expiry times
        mapping(uint256=>uint) expiries;
        modifier live {
            require(ens.owner(baseNode) == address(this));//基结点的所有者必须是本合约
            _;
        }
    
        modifier onlyController {
            require(controllers[msg.sender]);//调用方地址必须是可控制这个合约的地址，这个地址需要本合约owner事先用addController添加进来
            _;
        }
        //返回域名是否可以注册
        function available(uint256 id) public view override returns(bool) {        
            return expiries[id] + GRACE_PERIOD < block.timestamp;//过期时间戳+90天的保护期<当前时间戳，就表示过期且过了保护期了，可以注册
        }
        function _register(uint256 id, address owner, uint duration, bool updateRegistry) internal live onlyController returns(uint) {
            require(available(id));
            require(block.timestamp + duration + GRACE_PERIOD > block.timestamp + GRACE_PERIOD); //先计算一下当前时间戳+有效时间+保护期，如果溢出就会直接fail，防止这次注册成功后，后面都不可renew了
    
            expiries[id] = block.timestamp + duration;
            if(_exists(id)) {
                //之前存在这个id的nft，则燃烧，说明这个nft是过期又注册的
                _burn(id);
            }
            _mint(owner, id);
            if(updateRegistry) {//更新ens的owner
                ens.setSubnodeOwner(baseNode, bytes32(id), owner);
            }
    
            emit NameRegistered(id, owner, block.timestamp + duration);
    
            return block.timestamp + duration;
        }
        /**
         * @dev 重新认领ens
         */
        function reclaim(uint256 id, address owner) external override live {
            require(_isApprovedOrOwner(msg.sender, id));
            ens.setSubnodeOwner(baseNode, bytes32(id), owner);
        }
    }
    

讲到这里，需要说明一下nodehash的生成过程，以btc.eth为例，

keccak256(“btc”)得到0x4bac7d8baf3f4f429951de9baff555c2f70564c6a43361e09971ef219908703d，再keccak256(abi.encodePacked(base.baseNode(), “0x4bac7d8baf3f4f429951de9baff555c2f70564c6a43361e09971ef219908703d”))得到0x9b530388C920f6b1dD3d05AEFb9B4650Fe388B2F，就是实际btc.eth在ENS系统中的node，而baseNode=keccak256(abi.encodePacked(“0x00000000000000000000000000000000”, keccak256(“eth”))。如果是btc.eth的子域名就再递归下去。

    contract ENSRegistry is ENS {
        //解析器结构体
        struct Record {
            address owner;//注册者
            address resolver;//解析器合约地址
            uint64 ttl;
        }
        /**
         * @dev Sets the resolver address for the specified node.
         * @param node The node to update.
         * @param resolver The address of the resolver.
         */
        function setResolver(bytes32 node, address resolver) public virtual override authorised(node) {
            emit NewResolver(node, resolver);
            records[node].resolver = resolver;//设置node的解析器地址，这里的node就是上面讲到的nodehash生成的node
        }
    }
    

    abstract contract AddrResolver is ResolverBase {    
        uint constant private COIN_TYPE_ETH = 60;//eth地址默认60，还可以解析btc,ltc,dego地址，分别对应0，2，3
        mapping(bytes32=>mapping(uint=>bytes)) _addresses;//此map存储node=>60=>address，就表示某个eth域名对应的地址
        /**
         * Sets the address associated with an ENS node.
         * May only be called by the owner of that node in the ENS registry.
         * @param node The node to update.
         * @param a The address to set.
         */
        function setAddr(bytes32 node, address a) external authorised(node) {
            setAddr(node, COIN_TYPE_ETH, addressToBytes(a));
        }   
    
        function setAddr(bytes32 node, uint coinType, bytes memory a) public authorised(node) {
            emit AddressChanged(node, coinType, a);
            if(coinType == COIN_TYPE_ETH) {
                emit AddrChanged(node, bytesToAddress(a));
            }
            _addresses[node][coinType] = a;
        }   
    }
    

ENS域名也是ERC721的NFT，可以以opensea等平台进行交易，但是它没有metadata，且在交易成功后，新的owner需要调用一下BaseRegistrarImplementation合约的reclaim方法，重新认领一下，才可以管理自己的ENS域名。

在域名过期后，设有90天的保护期，在保护期内owner是可以直接renew的，也就是续期，过了保护期后就要重新commit去注册了，这时候其他人也就可以注册这个域名了。

2\. 反向解析
--------

上面讲的是用ens域名解析出用户的钱包地址，下面来讲下怎么反向解析，也就是用钱包地址反向解析出ens域名。

    contract ReverseRegistrar {
        // namehash('addr.reverse')
        bytes32 public constant ADDR_REVERSE_NODE = 0x91d1777781884d03a6757a803996e38de2a42967fb37eeaca72729271025a9e2;
        
        function claimWithResolver(address owner, address resolver) public returns (bytes32) {
            bytes32 label = sha3HexAddress(msg.sender);//将钱包地址转成字符串再做keccak256
            bytes32 node = keccak256(abi.encodePacked(ADDR_REVERSE_NODE, label));
            address currentOwner = ens.owner(node);
    
            //需要更新解析器地址
            if (resolver != address(0x0) && resolver != ens.resolver(node)) {
                // Transfer the name to us first if it's not already
                if (currentOwner != address(this)) {//之前的owner不是当前合约地址
                    ens.setSubnodeOwner(ADDR_REVERSE_NODE, label, address(this));//设置node的owner为当前合约地址
                    currentOwner = address(this);
                }
                ens.setResolver(node, resolver);//设置解析器地址
            }
    
            //如果用了不同的ReverseRegistar时，就会出现之前的owner和不是当前合约地址，需要更新
            if (currentOwner != owner) {
                ens.setSubnodeOwner(ADDR_REVERSE_NODE, label, owner);
            }
    
            return node;
        }
        
        function setName(string memory name) public returns (bytes32) {
            bytes32 node = claimWithResolver(address(this), address(defaultResolver));
            defaultResolver.setName(node, name);//设置node对应的ens name
            return node;
        }    
    }
    

    contract DefaultReverseResolver {    
        mapping (bytes32 => string) public name;    
        function setName(bytes32 node, string memory _name) public onlyOwner(node) {
            name[node] = _name;
        }
    }
    

从代码出可以看出，任何地址都可以设置反向解析到指定ens域名，没有做ens域名的所有者限制。与正向解析用eth做根不同的时，反向解析用addr.reverse做根。

3\. 管理权限
--------

ENS的owner可以将ens授权给其它地址， 这些地址可以理解为管理地址，可以对这个域名设置各自解析器等操作，但不能转让这个nft。

    contract ENSRegistry is ENS {
       function setApprovalForAll(address operator, bool approved) external virtual override {
            operators[msg.sender][operator] = approved;
            emit ApprovalForAll(msg.sender, operator, approved);
        }
    }
    

可以看到BaseRegistrarImplementation继承了ERC721，最终还是用了erc721的owner体系去实现nft的归属，在ENSRegistry里维护的是ens业务上的所有者、解析器等信息。因此，如果在opensea上交易后，只完成的nft的转让，还需要再reclaim一次，才能自己设置地址解析。

    contract BaseRegistrarImplementation is ERC721, BaseRegistrar  {
        /**
         * @dev 重新认领ens
         */
        function reclaim(uint256 id, address owner) external override live {
            require(_isApprovedOrOwner(msg.sender, id));
            ens.setSubnodeOwner(baseNode, bytes32(id), owner);
        }
    }
    

4\. 小技巧
-------

ENS有提案允许保存ABI信息到链上，并规定了几种contentType，如下：

*   1 JSON
    
*   2 zlib-compressed JSON
    
*   4 CBOR
    
*   8 URI
    

要求contentType是2的指数，在代码里用了位运算来实现这个判断。

例如：contentType=8，二进制是1000，将contentType-1=7，二进制是0111，将0111与1000做与运算，得到0，也就是说-1后再&运算将永远是0，以此来判断2的指数。

    function setABI(bytes32 node, uint256 contentType, bytes calldata data) external authorised(node) {
      // Content types must be powers of 2
      require(((contentType - 1) & contentType) == 0);
    
      abis[node][contentType] = data;
      emit ABIChanged(node, contentType);
    }

---

*Originally published on [franx.eth](https://paragraph.com/@franx-2/ens)*
