Alert Source Discuss
⚠️ Review Standards Track: ERC

ERC-1185: 在 ENS 中存储 DNS 记录

一个在 ENS 合约中存储和检索 DNS 记录的系统。

Authors Jim McDonald (@mcdee)
Created 2018-06-26
Requires EIP-137

摘要

本 EIP 定义了一个 ENS 的解析器配置文件,该文件提供了存储和查找 DNS 记录的功能。这允许 ENS 被用作权威 DNS 信息的存储。

动机

ENS 是 DNS 信息的非常理想的存储。它提供了 DNS 的分布式权限,而不会混淆所有权和信息的权威性服务。使用 ENS,域的所有者可以完全控制他们自己的 DNS 记录。此外,ENS 具有(通过智能合约)将域的子域不可撤销地分配给另一个实体的能力。

规范

在 ENS 上支持 DNS 的解析器配置文件遵循 ERC-137 中定义的解析器规范。

传统上,DNS 是一种基于区域的系统,即一个区域的所有记录都保存在同一个文件中。这样做的好处是区域更新的简单性和原子性,但是当转移到 ENS 时,可能会导致简单更改的巨大 gas 成本。因此,解析器基于记录集工作。记录集由元组 (domain, name, resource record type) 唯一地定义,例如元组 (example.com, www.example.com, A) 定义了域名 example.com 中名称 www.example.comA 记录的记录集。一个记录集可以包含 0 个或多个值,例如,如果 www.example.com 具有 A 记录 1.2.3.45.6.7.8,则上述元组将具有两个值。

选择在记录集级别而不是区域级别工作意味着此规范不能完全支持 DNS 的某些功能,例如区域传输和 DNSSEC。可以构建一个在区域级别上工作的不同解析器配置文件,但是执行更新的成本会很高,因此本 EIP 不再进一步考虑。

DNS 解析器接口包含两个用于设置 DNS 信息的函数和两个用于查询 DNS 信息的函数。

setDNSRecords(bytes32 node, bytes data)

setDNSRecords() 设置、更新或清除给定节点的 1 个或多个 DNS 记录。它的函数签名是 0x0af179d7

该函数的参数如下:

  • node:要设置记录的 ENS 中完全限定域名的 namehash。Namehashes 在 ERC-137 中定义
  • data:DNS wire 格式的 1 个或多个 DNS 记录。任何没有值的记录都将被清除。请注意,同一 RRset 中的所有记录都应在数据中是连续的;如果不是这样,则后面的 RRset 将覆盖较早的 RRset

clearDNSZone(bytes32 node)

clearDNSZone() 删除域的所有 DNS 记录。 它的函数签名是 0xad5780af

尽管可以使用如上所述的 setDNSRecords() 单独清除记录,但这需要所有者知道已设置的所有记录(因为解析器没有迭代给定域的记录的方法),并且可能需要多个交易。clearDNSZone() 在单个操作中删除所有区域信息。

该函数的参数如下:

  • node:要清除记录的 ENS 中完全限定域名的 namehash。Namehashes 在 ERC-137 中定义

dnsRecords(bytes32 node, bytes32 name, uint16 resource) view returns (bytes)

dnsRecords() 获取给定节点、名称和资源的 DNS 记录。它的函数签名是 0x2461e851

该函数的参数如下:

  • node:要设置记录的 ENS 中完全限定域名的 namehash。Namehashes 在 ERC-137 中定义
  • name:DNS wire 格式记录名称的 keccak256() 哈希值。
  • resource:资源记录 ID。资源记录 ID 在 RFC1035 和后续 RFC 中定义。

该函数以 DNS wire 格式返回所有匹配的记录。如果没有记录,该函数将不返回任何内容。

hasDNSRecords(bytes32 node, bytes32 name) view returns (bool)

hasDNSRecords() 报告域中是否具有所提供名称的任何记录。它的函数签名是 0x4cbf6ba4

当使用 RFC4592 中定义的通配符资源时,DNS 解析器需要此函数。

该函数的参数如下:

  • node:要设置记录的 ENS 中完全限定域名的 namehash。Namehashes 在 ERC-137 中定义
  • name:DNS wire 格式记录名称的 keccak256() 哈希值。

如果存在所提供的节点和名称的任何记录,该函数将返回 true,否则返回 false

合理依据

DNS 是一种联合命名系统,更高级别的实体控制其下所有内容的可用性(例如 .org 控制 ethereum.org 的可用性)。分散式 DNS 版本不会有此约束,并允许直接查找 ENS 中具有相关记录的任何域。

向后兼容性

不适用。

参考实现

DNS 解析器的参考实现如下:

pragma solidity ^0.7.4;
import "../ResolverBase.sol";
import "@ensdomains/dnssec-oracle/contracts/RRUtils.sol";

abstract contract DNSResolver is ResolverBase {
    using RRUtils for *;
    using BytesUtils for bytes;

    bytes4 constant private DNS_RECORD_INTERFACE_ID = 0xa8fa5682;
    bytes4 constant private DNS_ZONE_INTERFACE_ID = 0x5c47637c;

    // DNSRecordChanged is emitted whenever a given node/name/resource's RRSET is updated.
    // 当给定节点/名称/资源的 RRSET 更新时,会发出 DNSRecordChanged 事件。
    event DNSRecordChanged(bytes32 indexed node, bytes name, uint16 resource, bytes record);
    // DNSRecordDeleted is emitted whenever a given node/name/resource's RRSET is deleted.
    // 当给定节点/名称/资源的 RRSET 删除时,会发出 DNSRecordDeleted 事件。
    event DNSRecordDeleted(bytes32 indexed node, bytes name, uint16 resource);
    // DNSZoneCleared is emitted whenever a given node's zone information is cleared.
    // 当给定节点的区域信息被清除时,会发出 DNSZoneCleared 事件。
    event DNSZoneCleared(bytes32 indexed node);

    // DNSZonehashChanged is emitted whenever a given node's zone hash is updated.
    // 当给定节点的区域哈希更新时,会发出 DNSZonehashChanged 事件。
    event DNSZonehashChanged(bytes32 indexed node, bytes lastzonehash, bytes zonehash);

    // Zone hashes for the domains.
    // 域的区域哈希。
    // A zone hash is an ERC-1577 content hash in binary format that should point to a
    // 区域哈希是以二进制格式的 ERC-1577 内容哈希,它应该指向一个
    // resource containing a single zonefile.
    // 包含单个区域文件的资源。
    // node => contenthash
    // 节点 => contenthash
    mapping(bytes32=>bytes) private zonehashes;

    // Version the mapping for each zone.  This allows users who have lost
    // 为每个区域版本化映射。这允许丢失了
    // track of their entries to effectively delete an entire zone by bumping
    // 他们的条目的跟踪的用户可以通过增加来有效地删除整个区域
    // the version number.
    // 版本号。
    // node => version
    // 节点 => 版本
    mapping(bytes32=>uint256) private versions;

    // The records themselves.  Stored as binary RRSETs
    // 记录本身。存储为二进制 RRSET
    // node => version => name => resource => data
    mapping(bytes32=>mapping(uint256=>mapping(bytes32=>mapping(uint16=>bytes)))) private records;

    // Count of number of entries for a given name.  Required for DNS resolvers
    // 给定名称的条目数计数。DNS 解析器需要
    // when resolving wildcards.
    // 在解析通配符时。
    // node => version => name => number of records
    mapping(bytes32=>mapping(uint256=>mapping(bytes32=>uint16))) private nameEntriesCount;

    /**
     * Set one or more DNS records.  Records are supplied in wire-format.
     * 设置一个或多个 DNS 记录。记录以 wire 格式提供。
     * Records with the same node/name/resource must be supplied one after the
     * 具有相同节点/名称/资源的记录必须一个接一个地提供
     * other to ensure the data is updated correctly. For example, if the data
     * 以确保数据已正确更新。例如,如果数据
     * was supplied:
     * 是提供的:
     *     a.example.com IN A 1.2.3.4
     *     a.example.com IN A 5.6.7.8
     *     www.example.com IN CNAME a.example.com.
     * then this would store the two A records for a.example.com correctly as a
     * 那么这将正确地将 a.example.com 的两个 A 记录存储为
     * single RRSET, however if the data was supplied:
     * 单个 RRSET,但是如果数据为
     *     a.example.com IN A 1.2.3.4
     *     www.example.com IN CNAME a.example.com.
     *     a.example.com IN A 5.6.7.8
     * then this would store the first A record, the CNAME, then the second A
     * 那么这将存储第一个 A 记录,CNAME,然后是第二个 A
     * record which would overwrite the first.
     * 记录,它将覆盖第一个记录。
     *
     * @param node the namehash of the node for which to set the records
     * @param node 要设置记录的节点的 namehash
     * @param data the DNS wire format records to set
     * @param data 要设置的 DNS wire 格式记录
     */
    function setDNSRecords(bytes32 node, bytes calldata data) external authorised(node) {
        uint16 resource = 0;
        uint256 offset = 0;
        bytes memory name;
        bytes memory value;
        bytes32 nameHash;
        // Iterate over the data to add the resource records
        // 迭代数据以添加资源记录
        for (RRUtils.RRIterator memory iter = data.iterateRRs(0); !iter.done(); iter.next()) {
            if (resource == 0) {
                resource = iter.dnstype;
                name = iter.name();
                nameHash = keccak256(abi.encodePacked(name));
                value = bytes(iter.rdata());
            } else {
                bytes memory newName = iter.name();
                if (resource != iter.dnstype || !name.equals(newName)) {
                    setDNSRRSet(node, name, resource, data, offset, iter.offset - offset, value.length == 0);
                    resource = iter.dnstype;
                    offset = iter.offset;
                    name = newName;
                    nameHash = keccak256(name);
                    value = bytes(iter.rdata());
                }
            }
        }
        if (name.length > 0) {
            setDNSRRSet(node, name, resource, data, offset, data.length - offset, value.length == 0);
        }
    }

    /**
     * Obtain a DNS record.
     * 获取 DNS 记录。
     * @param node the namehash of the node for which to fetch the record
     * @param node 要获取记录的节点的 namehash
     * @param name the keccak-256 hash of the fully-qualified name for which to fetch the record
     * @param name 要获取记录的完全限定名称的 keccak-256 哈希
     * @param resource the ID of the resource as per https://en.wikipedia.org/wiki/List_of_DNS_record_types
     * @param resource 资源的 ID,如 https://en.wikipedia.org/wiki/List_of_DNS_record_types 所示
     * @return the DNS record in wire format if present, otherwise empty
     * @return 如果存在,则返回 wire 格式的 DNS 记录,否则返回空
     */
    function dnsRecord(bytes32 node, bytes32 name, uint16 resource) public view returns (bytes memory) {
        return records[node][versions[node]][name][resource];
    }

    /**
     * Check if a given node has records.
     * 检查给定节点是否具有记录。
     * @param node the namehash of the node for which to check the records
     * @param node 要检查记录的节点的 namehash
     * @param name the namehash of the node for which to check the records
     * @param name 要检查记录的节点的 namehash
     */
    function hasDNSRecords(bytes32 node, bytes32 name) public view returns (bool) {
        return (nameEntriesCount[node][versions[node]][name] != 0);
    }

    /**
     * Clear all information for a DNS zone.
     * 清除 DNS 区域的所有信息。
     * @param node the namehash of the node for which to clear the zone
     * @param node 要清除区域的节点的 namehash
     */
    function clearDNSZone(bytes32 node) public authorised(node) {
        versions[node]++;
        emit DNSZoneCleared(node);
    }

    /**
     * setZonehash sets the hash for the zone.
     * setZonehash 设置区域的哈希。
     * May only be called by the owner of that node in the ENS registry.
     * 只能由 ENS 注册表中该节点的所有者调用。
     * @param node The node to update.
     * @param node 要更新的节点。
     * @param hash The zonehash to set
     * @param hash 要设置的 区域哈希
     */
    function setZonehash(bytes32 node, bytes calldata hash) external authorised(node) {
        bytes memory oldhash = zonehashes[node];
        zonehashes[node] = hash;
        emit DNSZonehashChanged(node, oldhash, hash);
    }

    /**
     * zonehash obtains the hash for the zone.
     * zonehash 获取区域的哈希。
     * @param node The ENS node to query.
     * @param node 要查询的 ENS 节点。
     * @return The associated contenthash.
     * @return 关联的 contenthash。
     */
    function zonehash(bytes32 node) external view returns (bytes memory) {
        return zonehashes[node];
    }

    function supportsInterface(bytes4 interfaceID) virtual override public pure returns(bool) {
        return interfaceID == DNS_RECORD_INTERFACE_ID ||
               interfaceID == DNS_ZONE_INTERFACE_ID ||
               super.supportsInterface(interfaceID);
    }

    function setDNSRRSet(
        bytes32 node,
        bytes memory name,
        uint16 resource,
        bytes memory data,
        uint256 offset,
        uint256 size,
        bool deleteRecord) private
    {
        uint256 version = versions[node];
        bytes32 nameHash = keccak256(name);
        bytes memory rrData = data.substring(offset, size);
        if (deleteRecord) {
            if (records[node][version][nameHash][resource].length != 0) {
                nameEntriesCount[node][version][nameHash]--;
            }
            delete(records[node][version][nameHash][resource]);
            emit DNSRecordDeleted(node, name, resource);
        } else {
            if (records[node][version][nameHash][resource].length == 0) {
                nameEntriesCount[node][version][nameHash]++;
            }
            records[node][version][nameHash][resource] = rrData;
            emit DNSRecordChanged(node, name, resource, rrData);
        }
    }
}

安全考虑

此解决方案的安全性将取决于 ENS 域中记录的安全性。这会降低到对该域具有权限的 key 的安全性。

版权

版权及相关权利通过 CC0 放弃。

Citation

Please cite this document as:

Jim McDonald (@mcdee), "ERC-1185: 在 ENS 中存储 DNS 记录 [DRAFT]," Ethereum Improvement Proposals, no. 1185, June 2018. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-1185.