[ERC1185] DNS情報をENSに保存する仕組みを理解しよう！

# はじめに

初めまして。
**CryptoGames**というブロックチェーンゲーム企業でエンジニアをしている **cardene（かるでね）** です！
スマートコントラクトを書いたり、フロントエンド・バックエンド・インフラと幅広く触れています。

https://cryptogames.co.jp/

代表的なゲームは**クリプトスペルズ**というブロックチェーンゲームです。

https://cryptospells.jp/

今回は、ENSコントラクト内のDNSレコードを保存・取得する仕組みを提案している規格である**ERC1185**についてまとめていきます！

:::note info
以下にまとめられているものを翻訳・要約・補足しながらまとめていきます。
:::

https://eips.ethereum.org/EIPS/eip-1185

:::note warn
**1185**は現在（2023年12月30日）では「**Review**」段階です。
:::

他にも様々なERCについてまとめています。

https://cardene.notion.site/ERC-EIP-2a03fa3ea33d43baa9ed82288f98d4a9?pvs=4

# 概要

このEIPはイーサリアムの改善提案の1つで、ENSというシステムを使って、インターネット上のドメイン名に関連する情報（DNSレコード）をイーサリアムブロックチェーン上に保存し、検索できるようにするためのルールを定めています。
ENSは、ウェブサイトのアドレスなどを人が読める形で管理するためのサービスです。
この提案により、ENSがより強力になり、インターネットのドメイン名をより安全に、分散化された方法で管理できるようになることを目指しています。

:::note info
通常インターネットで使われるドメイン名システムを、ブロックチェーンの技術を使って改善しようという試みです。
これはコントラクトと呼ばれる特別なプログラムによって実行され、ENSを使うすべてのやり取りがこのコントラクトを通じて行われます。
:::

:::note info
### DNS
DNS（Domain Name System）は、インターネット上のサービスやデバイスを特定するためのシステムです。
人間が覚えやすいドメイン名（例：`www.example.com`）を、コンピューターやネットワーク機器が理解できる数値のIPアドレス（例：`192.0.2.1`）に変換します。
これは、電話帳が電話番号と名前を対応させるのに似ています。
インターネット上のどこかにあるサーバーにアクセスしたい時、人間がそのサーバーのIPアドレスを覚えておく必要はなく、覚えやすいドメイン名をブラウザに入力するだけで接続できます。

#### DNSの基本構造とプロセス
- **ルートサーバー**
    -インターネット上の最上位にあり、ドメイン名の最後の部分（例：`.com`、`.net`、`.org`などのトップレベルドメイン）を管理しています。
- **TLDサーバー**
    - トップレベルドメイン（**TLD**）ごとに存在し、その**TLD**に属するドメイン名の情報を管理します。
- **権威DNSサーバー**
    - 特定のドメインについての情報を管理しており、そのドメインに関連するすべてのサブドメイン情報を持っています。
- **再帰的DNSサーバー**
    - クライアントのリクエストに応じて、上記のサーバーを順に問い合わせ、最終的なIPアドレスをクライアントに返します。
    - 多くの場合、インターネットサービスプロバイダ（ISP）が提供します。

#### DNS解決プロセス
1. ユーザーがブラウザにドメイン名を入力します。
2. ユーザーのデバイスは、設定されている再帰的DNSサーバーにクエリを送ります。
3. 再帰的DNSサーバーは、必要に応じてルートサーバー、TLDサーバー、権威DNSサーバーに問い合わせを行い、最終的なIPアドレスを見つけます。
4. IPアドレスが見つかると、その情報はユーザーのデバイスに返され、ブラウザはそのIPアドレスにあるサーバーに接続します。
5. ユーザーのデバイスはIPアドレスを一定時間キャッシュ（一時保存）し、次回同じドメイン名にアクセスする際の解決時間を短縮します。

#### DNSの重要性
- **ユーザビリティ**
    - 人間が覚えやすい名前でインターネットリソースにアクセスできる。
- **接続性**
    - 世界中のサーバーとデバイスが安定して互いに通信できる。
- **セキュリティ**
    - DNSSECなどの技術により、ユーザーが意図した正しいサイトに接続しているかの検証が可能。

DNSはインターネットの基盤技術の一つであり、その効率的かつ正確な動作が、日々のオンライン活動の基礎を支えています。
:::

# 動機

ENSは、インターネットのドメイン名を管理するための新しい方法を提供します。
従来のDNSシステムとは異なり、ENSは所有権と情報提供を明確に区別しながら、より分散された形で機能します。
これにより、ドメインの所有者は自分のドメイン名に関連する情報（DNSレコード）を完全にコントロールできるようになります。

さらに、スマートコントラクトという技術を使って、ENSはドメイン名の下にあるサブドメインを他の人や組織に確実に、かつ変更不可能に割り当てることができます。
これにより、サブドメインの管理がより柔軟で安全になります。

:::note info
ENSはインターネット上のアドレス帳をもっと自由に、安全に管理できるようにするシステムです。
それは**ERC20**や**ERC721**などのスマートコントラクトを使って、より進んだ機能を提供し、ユーザーにとってより信頼性の高い、制御しやすいやり取りを可能にします。
:::

# 仕様

ENSでDNSをサポートするためのリゾルバプロファイルは、**ERC137**という規格に基づいています。
これは、従来のDNSシステムの問題点を改善するために設計されています。
通常のDNSはすべての情報を一箇所に保持するゾーンベースのシステムですが、これをENSに適用すると変更ごとに多くの手数料がかかってしまいます。
そこで、ENSではレコードセットという方法を使っています。
これは、特定のドメインと名前、レコードタイプの組み合わせごとに情報を管理する方式で、例えば`example.com`ドメインの`www.example.com`という名前のAレコードなら、その組み合わせが1つのレコードセットになります。
これにより、必要な部分だけを効率的に更新できます。

:::note info
**ERC137**については以下の記事を参考にしてください。
:::

https://qiita.com/cardene/items/17e6e1a452006f08a197

しかし、レコードセットを使うと、DNSの一部機能、例えばゾーン全体を移転するゾーン転送やセキュリティ機能のDNSSECは完全にはサポートできません。
ゾーンベースで動作する別のリゾルバプロファイルを作ることは技術的に可能ですが、更新コストが非常に高くなるため、この提案では採用されていません。

DNS情報を管理するために、このリゾルバプロファイルはDNS情報を設定するための関数と、それを問い合わせるための関数をそれぞれ2つ提供します。
これにより、ENSを使ってDNS情報をより柔軟かつ効率的に管理できるようになります。

ENSでDNS情報を管理するための関数について、以下のように分かりやすく説明します。

## `setDNSRecords(bytes32 node, bytes data)`
この関数は、特定のノード（ドメイン）に対して1つ以上のDNSレコードを追加、更新、または削除するために使用します。
関数シグネチャは`0x0af179d7`です。
`node`はENSでの完全修飾ドメインの`namehash`を指し、**ERC137**で定義されています。
`data`は1つ以上のDNSレコードをDNSワイヤフォーマットで含みます。
値がないレコードは削除されますが、データ内で同じ**RRset**のレコードが連続していない場合、後に来るものが前のものを上書きするので注意が必要です。

## `clearDNSZone(bytes32 node)`
`clearDNSZone`関数は、指定されたノードのすべてのDNSレコードを削除します。
関数シグネチャは`0xad5780af`です。
`node`は、クリアしたいENSの完全修飾ドメインの`namehash`です。
この関数は、ドメインに関連するすべてのレコードを一度にクリアする必要がある時に特に便利です。

## `dnsRecords(bytes32 node, bytes32 name, uint16 resource)`
`dnsRecords`関数は、指定されたノード、名前、リソースに基づいてDNSレコードを取得するために使用します。
関数シグネチャは`0x2461e851`です。
`node`はENSの完全修飾ドメインの`namehash`、`name`はレコード名の`keccak256()`ハッシュ、`resource`はリソースレコードIDです。
この関数は、一致するすべてのレコードをDNSワイヤフォーマットで返し、レコードが存在しない場合は何も返しません。

## `hasDNSRecords(bytes32 node, bytes32 name)`
`hasDNSRecords`関数は、特定のノードと名前に対して任意のレコードが存在するかどうかを確認するために使用されます。
関数シグネチャは`0x4cbf6ba4`です。`node`はENSの完全修飾ドメインのnamehash、`name`はレコード名の`keccak256()`ハッシュです。
この関数は、レコードが存在すれば`true`を、存在しなければ`false`を返します。

これらの関数を通じて、ENSを使うユーザーはDNSレコードをより効率的かつ柔軟に管理できるようになり、従来のDNSシステムよりも安全かつ快適なやり取りが可能になります。

# 補足

DNSはインターネット上での「**アドレス帳**」のようなもので、特定のドメイン名（例えば、`.com`や`.org`など）の所有者がその下の全てのアドレス（例えば、`example.com`や`example.org`など）を管理します。
これは上から下への一方的な管理構造を生み出し、最上位のドメイン所有者がすべてをコントロールするという状況を作り出しています。

一方で、分散型DNSシステムでは、このような「**上位からの管理**」が存在しません。
ENS（Ethereum Name Service）のような分散型のシステムでは、どんなドメインも、それに関連するレコードがあれば、直接誰でもルックアップ（検索）できます。
これにより、各ドメインの所有者は自分のドメインを自由に、独立して管理できるようになり、全体としてより自由で柔軟なインターネット名前解決システムが実現されます。

このシステムでは、**ERC20**や**ERC721**などのスマートコントラクトを利用して、ENSを通じた効率的かつ安全なやり取りが可能になります。
これにより、ユーザーは従来のDNSシステムに見られる中央集権的な制約から解放され、もっと自由で開かれたインターネットを享受できるようになります。

# 互換性

特になし。

# 実装

```javascript
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.
    event DNSRecordChanged(bytes32 indexed node, bytes name, uint16 resource, bytes record);
    // DNSRecordDeleted is emitted whenever a given node/name/resource's RRSET is deleted.
    event DNSRecordDeleted(bytes32 indexed node, bytes name, uint16 resource);
    // DNSZoneCleared is emitted whenever a given node's zone information is cleared.
    event DNSZoneCleared(bytes32 indexed node);

    // DNSZonehashChanged is emitted whenever a given node's zone hash is updated.
    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
    // resource containing a single zonefile.
    // node => 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
    // 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
    // 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.
     * 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
     * single RRSET, however if the data was supplied:
     *     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
     * record which would overwrite the first.
     *
     * @param node the namehash of the node for which to set the records
     * @param data the DNS wire format records to set
     */
    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.
     * @param node the namehash of the node for which to fetch the record
     * @param name the keccak-256 hash of the fully-qualified name for which to fetch the record
     * @param resource the ID of the resource as per https://en.wikipedia.org/wiki/List_of_DNS_record_types
     * @return the DNS record in wire format if present, otherwise empty
     */
    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 name the namehash of the node for which to check the records
     */
    function hasDNSRecords(bytes32 node, bytes32 name) public view returns (bool) {
        return (nameEntriesCount[node][versions[node]][name] != 0);
    }

    /**
     * Clear all information for a DNS zone.
     * @param node the namehash of the node for which to clear the zone
     */
    function clearDNSZone(bytes32 node) public authorised(node) {
        versions[node]++;
        emit DNSZoneCleared(node);
    }

    /**
     * setZonehash sets the hash for the zone.
     * May only be called by the owner of that node in the ENS registry.
     * @param node The node to update.
     * @param hash The zonehash to set
     */
    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.
     * @param node The ENS node to query.
     * @return The associated 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ドメイン内にある情報の保護に大きく依存しています。
具体的には、ドメインをコントロールするために使われる「**鍵**」の安全性が、全体のセキュリティを決定します。
ドメインに関するすべての設定や情報は、この鍵によって管理されています。
もし鍵がしっかりと保護され、安全であれば、ドメインも安全です。
しかし、もし鍵が危険にさらされたり、誰かに不正に使われたりすると、ドメイン内のすべての情報も危険にさらされることになります。
つまり、ドメインを守るためには、その鍵をしっかりと安全に保つことが非常に重要です。

# 引用

Jim McDonald (@mcdee), "ERC-1185: Storage of DNS Records in ENS [DRAFT]," Ethereum Improvement Proposals, no. 1185, June 2018. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-1185.

# 最後に

今回は「ENSコントラクト内のDNSレコードを保存・取得する仕組みを提案している規格である**ERC1185**」についてまとめてきました！
いかがだったでしょうか？

質問などがある方は以下のTwitterのDMなどからお気軽に質問してください！

[Twitter @cardene777](https://twitter.com/cardene777)

他の媒体でも情報発信しているのでぜひ他も見ていってください！

https://chaldene.net/

https://zenn.dev/heku

https://mirror.xyz/0xcE77b9fCd390847627c84359fC1Bc02fC78f0e58


https://cardene.notion.site/ERC-EIP-2a03fa3ea33d43baa9ed82288f98d4a9?pvs=4