Alert Source Discuss
Standards Track: ERC

ERC-137: Ethereum 域名服务 - 规范

Authors Nick Johnson <arachnid@notdot.net>
Created 2016-04-04

摘要

本 EIP 草案描述了以太坊域名服务(Ethereum Name Service)的细节,这是一个提议的协议和 ABI 定义,它提供了将短的、人类可读的名称灵活地解析为服务和资源标识符的功能。这允许用户和开发者引用人类可读且易于记忆的名称,并允许在底层资源(合约、内容寻址数据等)发生变化时根据需要更新这些名称。

域名的目标是提供稳定的、人类可读的标识符,可以用来指定网络资源。通过这种方式,用户可以输入一个易于记忆的字符串,例如“vitalik.wallet”或“www.mysite.swarm”,并被定向到适当的资源。名称和资源之间的映射可能会随着时间的推移而改变,因此用户可能会更换钱包,网站可能会更换主机,或者 swarm 文档可能会更新到新版本,而域名不会改变。此外,域名不必指定单个资源;不同的记录类型允许同一个域名引用不同的资源。例如,浏览器可以通过获取其 A(地址)记录将“mysite.swarm”解析为其服务器的 IP 地址,而邮件客户端可以通过获取其 MX(邮件交换器)记录将同一地址解析为邮件服务器。

动机

现有的 规范实现 为以太坊中的名称解析提供了基本功能,但存在一些缺陷,这些缺陷将严重限制其长期可用性:

  • 所有名称使用单一的全局命名空间,并采用单一的“中心化”解析器。
  • 对委派和子名称/子域名支持有限或没有支持。
  • 只有一种记录类型,并且不支持将多个记录副本与域名关联。
  • 由于单一的全局实现,不支持多种不同的名称分配系统。
  • 职责混淆:名称解析、注册和 whois 信息。

这些功能所允许的用例包括:

  • 支持子名称/子域名 - 例如,live.mysite.tld 和 forum.mysite.tld。
  • 单个名称下的多种服务,例如托管在 Swarm 中的 DApp、Whisper 地址和邮件服务器。
  • 支持 DNS 记录类型,允许区块链托管“传统”名称。这将允许诸如 Mist 之类的以太坊客户端从区块链名称解析传统网站的地址或电子邮件地址的邮件服务器。
  • DNS 网关,通过域名服务公开 ENS 域名,从而为传统客户端提供更轻松的方式来解析和连接到区块链服务。

特别是前两个用例,可以在当今互联网上的 DNS 下随处可见,我们认为它们是名称服务的基本特征,并且随着以太坊平台的发展和成熟,它们将继续有用。

本文档的规范性部分不指定所提出的系统的实现;其目的是记录一个协议,不同的解析器实现可以遵守该协议,以促进一致的名称解析。附录提供了解析器合约和库的示例实现,这些示例实现应仅被视为说明性示例。

同样,本文档不尝试指定应如何注册或更新域名,或者系统如何找到负责给定域名的所有者。注册是注册商的责任,并且是一个治理问题,在顶级域名之间必然会有所不同。

域名记录的更新也可以与解析分开处理。某些系统(例如 swarm)可能需要明确定义的用于更新域名的接口,在这种情况下,我们预计会为此制定一个标准。

规范

概述

ENS 系统包括三个主要部分:

  • ENS 注册表
  • 解析器
  • 注册商

注册表是一个单独的合约,它提供从任何已注册名称到负责它的解析器的映射,并允许名称的所有者设置解析器地址,以及创建子域名,这些子域名可能具有与父域名不同的所有者。

解析器负责执行名称的资源查找 - 例如,返回合约地址、内容哈希或 IP 地址(如适用)。此处定义的解析器规范以及其他 EIP 中扩展的规范定义了解析器可以实现哪些方法来支持解析不同类型的记录。

注册商负责将域名分配给系统的用户,并且是唯一能够更新 ENS 的实体;ENS 注册表中节点的拥有者是其注册商。注册商可以是合约或外部拥有的帐户,但至少根注册商和顶级注册商有望以合约的形式实现。

在 ENS 中解析名称是一个两步过程。首先,使用下面描述的过程对名称进行哈希处理后,使用该名称调用 ENS 注册表。如果记录存在,则注册表返回其解析器的地址。然后,调用解析器,使用适用于所请求资源的方法。然后,解析器返回所需的结果。

例如,假设您希望找到与“beercoin.eth”关联的 token 合约的地址。首先,获取解析器:

var node = namehash("beercoin.eth");
var resolver = ens.resolver(node);

然后,询问解析器合约的地址:

var address = resolver.addr(node);

由于 namehash 过程仅依赖于名称本身,因此可以对其进行预先计算并插入到合约中,从而无需进行字符串操作,并允许 O(1) 查找 ENS 记录,而与原始名称中的组件数量无关。

名称语法

ENS 名称必须符合以下语法:

<domain> ::= <label> | <domain> "." <label>
<label> ::= 符合 [UTS46](https://unicode.org/reports/tr46/) 的任何有效字符串标签

简而言之,名称由一系列点分隔的标签组成。每个标签必须是一个有效的标准化标签,如 UTS46 中所述,选项为 transitional=falseuseSTD3AsciiRules=true。对于 JavaScript 实现,可以使用 library 来规范化和检查名称。

请注意,虽然名称中允许使用大写和小写字母,但 UTS46 规范化过程会在哈希标签之前对标签进行大小写折叠,因此两个大小写不同但拼写相同的名称将产生相同的 namehash。

标签和域名的长度可以是任意的,但为了与旧版 DNS 兼容,建议将标签限制为不超过 64 个字符,并将完整的 ENS 名称限制为不超过 255 个字符。出于同样的原因,建议标签不要以连字符开头或结尾,也不要以数字开头。

namehash 算法

在使用 ENS 之前,名称使用“namehash”算法进行哈希处理。该算法递归地哈希名称的组成部分,从而为任何有效输入域生成唯一的、固定长度的字符串。namehash 的输出称为“节点”。

namehash 算法的伪代码如下:

def namehash(name):
  if name == '':
    return '\0' * 32
  else:
    label, _, remainder = name.partition('.')
    return sha3(namehash(remainder) + sha3(label))

非正式地,该名称被分成标签,每个标签都被哈希处理。然后,从最后一个组件开始,将先前的输出与标签哈希连接并再次哈希。第一个组件与 32 个“0”字节连接。因此,“mysite.swarm”的处理方式如下:

node = '\0' * 32
node = sha3(node + sha3('swarm'))
node = sha3(node + sha3('mysite'))

实现应符合以下 namehash 的测试向量:

namehash('') = 0x0000000000000000000000000000000000000000000000000000000000000000
namehash('eth') = 0x93cdeb708b7545dc668eb9280176169d1c33cfd8ed6f04690a0bcc88a93fc4ae
namehash('foo.eth') = 0xde9b09fd7c5f901e23a3f19fecc54828e9c848539801e86591bd9801b019f84f

注册表规范

ENS 注册表合约公开以下函数:

function owner(bytes32 node) constant returns (address);

返回指定节点的拥有者(注册商)。

function resolver(bytes32 node) constant returns (address);

返回指定节点的解析器。

function ttl(bytes32 node) constant returns (uint64);

返回节点的生存时间 (TTL);也就是说,可以缓存节点信息的最长时间。

function setOwner(bytes32 node, address owner);

将节点的所有权转移给另一个注册商。此函数只能由 node 的当前拥有者调用。成功调用此函数会记录事件 Transfer(bytes32 indexed, address)

function setSubnodeOwner(bytes32 node, bytes32 label, address owner);

创建一个新节点 sha3(node, label) 并将其所有者设置为 owner,或者如果该节点已经存在,则使用新的所有者更新该节点。此函数只能由 node 的当前拥有者调用。成功调用此函数会记录事件 NewOwner(bytes32 indexed, bytes32 indexed, address)

function setResolver(bytes32 node, address resolver);

设置 node 的解析器地址。此函数只能由 node 的所有者调用。成功调用此函数会记录事件 NewResolver(bytes32 indexed, address)

function setTTL(bytes32 node, uint64 ttl);

设置节点的 TTL。节点的 TTL 适用于注册表中的“所有者”和“解析器”记录,以及关联解析器返回的任何信息。

解析器规范

解析器可以实现此处指定的记录类型的任何子集。如果记录类型规范要求解析器提供多个函数,则解析器必须实现全部函数或完全不实现任何函数。解析器必须指定一个 fallback 函数,该函数会抛出异常。

解析器具有一个必需的函数:

function supportsInterface(bytes4 interfaceID) constant returns (bool)

supportsInterface 函数记录在EIP-165中,如果解析器实现了由提供的 4 字节标识符指定的接口,则返回 true。接口标识符由该接口提供的函数的功能签名哈希的 XOR 组成;在单函数接口的退化情况下,它仅等于该函数的功能签名哈希。如果解析器为 supportsInterface() 返回 true,则它必须实现该接口中指定的函数。

supportsInterface 必须始终为 0x01ffc9a7 返回 true,这是 supportsInterface 本身的接口 ID。

当前标准化的解析器接口在下表中指定。

定义了以下接口:

接口名称 接口哈希 规范
addr 0x3b3b57de 合约地址
name 0x691f3431 #181
ABI 0x2203ab56 #205
pubkey 0xc8690233 #619

EIP 可以定义要添加到此注册表的新接口。

合约地址接口

希望支持合约地址资源的解析器必须提供以下函数:

function addr(bytes32 node) constant returns (address);

如果解析器支持 addr 查找,但请求的节点没有 addr 记录,则解析器必须返回零地址。

解析 addr 记录的客户端必须检查零返回值,并以与未指定解析器的名称相同的方式处理它 - 也就是说,拒绝向该地址发送资金或与之交互。未能这样做可能会导致用户意外地将资金发送到 0 地址。

对地址的更改必须触发以下事件:

event AddrChanged(bytes32 indexed node, address a);

附录 A:注册表实现

contract ENS {
    struct Record {
        address owner;
        address resolver;
        uint64 ttl;
    }

    mapping(bytes32=>Record) records;

    event NewOwner(bytes32 indexed node, bytes32 indexed label, address owner);
    event Transfer(bytes32 indexed node, address owner);
    event NewResolver(bytes32 indexed node, address resolver);

    modifier only_owner(bytes32 node) {
        if(records[node].owner != msg.sender) throw;
        _
    }

    function ENS(address owner) {
        records[0].owner = owner;
    }

    function owner(bytes32 node) constant returns (address) {
        return records[node].owner;
    }

    function resolver(bytes32 node) constant returns (address) {
        return records[node].resolver;
    }

    function ttl(bytes32 node) constant returns (uint64) {
        return records[node].ttl;
    }

    function setOwner(bytes32 node, address owner) only_owner(node) {
        Transfer(node, owner);
        records[node].owner = owner;
    }

    function setSubnodeOwner(bytes32 node, bytes32 label, address owner) only_owner(node) {
        var subnode = sha3(node, label);
        NewOwner(node, label, owner);
        records[subnode].owner = owner;
    }

    function setResolver(bytes32 node, address resolver) only_owner(node) {
        NewResolver(node, resolver);
        records[node].resolver = resolver;
    }

    function setTTL(bytes32 node, uint64 ttl) only_owner(node) {
        NewTTL(node, ttl);
        records[node].ttl = ttl;
    }
}

附录 B:示例解析器实现

内置解析器

最简单的解析器是通过实现合约地址资源 profile 来充当其自身名称解析器的合约:

contract DoSomethingUseful {
    // 其他代码

    function addr(bytes32 node) constant returns (address) {
        return this;
    }

    function supportsInterface(bytes4 interfaceID) constant returns (bool) {
        return interfaceID == 0x3b3b57de || interfaceID == 0x01ffc9a7;
    }

    function() {
        throw;
    }
}

可以将此类合约直接插入到 ENS 注册表中,从而无需在简单的用例中使用单独的解析器合约。但是,在未知函数调用时“抛出”的要求可能会干扰某些类型的合约的正常运行。

独立解析器

一个实现合约地址 profile 的基本解析器,并且只允许其所有者更新记录:

contract Resolver {
    event AddrChanged(bytes32 indexed node, address a);

    address owner;
    mapping(bytes32=>address) addresses;

    modifier only_owner() {
        if(msg.sender != owner) throw;
        _
    }

    function Resolver() {
        owner = msg.sender;
    }

    function addr(bytes32 node) constant returns(address) {
        return addresses[node];    
    }

    function setAddr(bytes32 node, address addr) only_owner {
        addresses[node] = addr;
        AddrChanged(node, addr);
    }

    function supportsInterface(bytes4 interfaceID) constant returns (bool) {
        return interfaceID == 0x3b3b57de || interfaceID == 0x01ffc9a7;
    }

    function() {
        throw;
    }
}

部署此合约后,通过更新 ENS 注册表以引用此合约的名称来使用它,然后使用相同的节点调用 setAddr() 以设置它将解析到的合约地址。

公共解析器

与上面的解析器类似,此合约仅支持合约地址 profile,但使用 ENS 注册表来确定应该允许谁更新条目:

contract PublicResolver {
    event AddrChanged(bytes32 indexed node, address a);
    event ContentChanged(bytes32 indexed node, bytes32 hash);

    ENS ens;
    mapping(bytes32=>address) addresses;

    modifier only_owner(bytes32 node) {
        if(ens.owner(node) != msg.sender) throw;
        _
    }

    function PublicResolver(address ensAddr) {
        ens = ENS(ensAddr);
    }

    function addr(bytes32 node) constant returns (address ret) {
        ret = addresses[node];
    }

    function setAddr(bytes32 node, address addr) only_owner(node) {
        addresses[node] = addr;
        AddrChanged(node, addr);
    }

    function supportsInterface(bytes4 interfaceID) constant returns (bool) {
        return interfaceID == 0x3b3b57de || interfaceID == 0x01ffc9a7;
    }

    function() {
        throw;
    }
}

附录 C:示例注册商实现

此注册商允许用户免费注册名称,如果他们是第一个请求它们的人。

```solidity contract FIFSRegistrar { ENS ens; bytes32 rootNode;

function FIFSRegistrar(address ensAddr, bytes32 node) {
    ens = ENS(ensAddr);
    rootNode = node;
}

function register(bytes32 subnode, address owner) {
    var node = sha3(rootNode, subnode);
    var currentOwner = ens.owner(node);
    if(currentOwner != 0 && currentOwner != msg.sender)
        throw;

    ens.setSubnodeOwner(rootNode, subnode, owner);
} }

Citation

Please cite this document as:

Nick Johnson <arachnid@notdot.net>, "ERC-137: Ethereum 域名服务 - 规范," Ethereum Improvement Proposals, no. 137, April 2016. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-137.