Develop with pleasure!

福岡でCloudとかBlockchainとか。

EthereumのDNSを利用したブートストラップノードの探索

EthereumでもEIP-1469によりノード探索プロトコルDNSがサポートされたので、内容見てみる↓

arachnid.github.io

↓で解説したように、ノードを起動するとハードコードされたブートストラップノードを使って、アウトバウンドピアの接続対象を検索する。

techmedia-think.hatenablog.com

現状ハードコードされているmainnetのブートストラップノードの数は8個で、その中からノードと距離の近い上位3つを選択してノート探索を始めることになる。EIP-1469は、このハードコードされているブートストラップノードリストに代わって、DNSを使ってブートストラップノードリストを入手するための仕組みを定義したEIP。ブートストラップノードがハードコードされていると、ブートストラップノードリストが固定されてしまうし、現在8個とその選択肢が少ないので、それをDNSを使って改善するもの。また、ネットワークの制限とかで、KadmliaベースのDHTが使用できないような環境においては代替手段となる。

DNSレコードの構造

Bitcoin CoreでもソースコードにハードコードされているのはDNSシードなので、↑の導入により似たような仕組みの選択肢が増える。Bitcoinの場合、DNSシードは有効なフルノードのIPを直接DNSのAレコードで返すが、EIP-1469が返すのはTXTレコードになる。これはノードの探索がBitcoinの場合は直接IPアドレスを使うが、Ethreumの場合はノードIDを使って距離の近いノードを探索するためであるのと、TXTレコードで返されるのはノード単体に限らず構造化されたノードリストであるからだ。ここでいうノードリストはEIP-778で定義されたEthereum Node Records(ENR)のこと。

レコードツリーを参照する際のURL

DNSを使ってレコードツリーを参照する際、URL形式で対象のドメインエンコードされる。現在公開されているmainnetのDNSドメインのURLは↓

enrtree://AKA3AM6LPBYEUDMVNU3BSVQJ5AD45Y7YPOHJLEF6W26QOE4VTUDPE@all.mainnet.ethdisco.net

all.mainnet.ethdisco.netの部分がDNSに問い合わせをするドメインで、@より前のユーザー名の部分はbase32文字列としてエンコードされた33バイトの圧縮公開鍵だ。↑をデコードすると0281b033cb78704a0d956d36195609e807cee3f87b8e9590beb6bd0713959d06f2となる。ノードツリーを参照する際、ルートレコードには署名が付与されており、この署名を作成した秘密鍵に対応するのがこの公開鍵になる。つまり、ルートツリーのデータが正しいことをこの公開鍵を使って検証することになる。

実際に↑のドメインに対してクエリを投げると以下のようにTXTレコードが取得できる。

$ dig -t TXT all.mainnet.ethdisco.net
...
all.mainnet.ethdisco.net. 1799  IN  TXT "enrtree-root:v1 e=XTLPTO7P7CR2V7W2PUJD5QKHMQ l=FDXN3SN67NA5DKA4J2GOK7BVQI seq=1217 sig=iEZZblnqsUvW98vZuhoVswKBM4RKLCVPnIFIfddhYQRjIpO7IVCzJd1v54ezhBUJHCC2sXIWnltosqG1J-LzyAA"

enrtree-rootは、これがツリーのルートであることを示し、v1はバージョンで現在有効なのはv1のみ。

  • eenr-rootの略でノードを含むサブツリーのルートハッシュでBase32エンコードされている(↑をデコードするとbcd6f9bbeff8a3aafeda7d123ec14764)。
  • llink-rootの略でリンクサブツリーを含むルートハッシュでBase32エンコードされている(↑をデコードすると28eeddc9befb41d1a81c4e8ce57c3582)。
  • seq: ツリーのシーケンス番号(↑では1217)。
  • sig: TXTレコード内容(この署名を除く)のkeccak256ハッシュに対する65バイトのsecp256k1楕円曲線の署名をURLセーフなBase64エンコードしたもの。

ルートツリーのレコードを取得すると、データの正しさを↑の公開鍵とレコード内容とレコード内の署名値のデータを使って署名検証をして確かめる。

ツリーの探索

↑のルートツリー自体にはノード情報は含まれていないので、enr-rootの値をサブドメインの値として再度DNSへ問い合わせる。

↑の場合だとXTLPTO7P7CR2V7W2PUJD5QKHMQ.all.mainnet.ethdisco.netが次のクエリになる。

$ dig -t TXT XTLPTO7P7CR2V7W2PUJD5QKHMQ.all.mainnet.ethdisco.net
...
XTLPTO7P7CR2V7W2PUJD5QKHMQ.all.mainnet.ethdisco.net. 21599 IN TXT "enrtree-branch:J2QOVIR4UJAYFY7KARSVB4TL7E,SDGK5C4FNX73SNHVQ3VHVLJZHU,FEQ5LEGY3HXJ6JFSPQFTQO6LA4,LE2HQXFZBRNWQBMAVDZ5DH2QBU"
...

と今度はenrtree-branchのTXTレコードが返ってきた。このデータは

  • J2QOVIR4UJAYFY7KARSVB4TL7E
  • SDGK5C4FNX73SNHVQ3VHVLJZHU
  • FEQ5LEGY3HXJ6JFSPQFTQO6LA4
  • LE2HQXFZBRNWQBMAVDZ5DH2QBU

の4つのブランチの情報を表している。この場合さらに、サブドメインの値を上記の値に置換して再度DNSにクエリを発行する(J2QOVIR4UJAYFY7KARSVB4TL7Eであれば、J2QOVIR4UJAYFY7KARSVB4TL7E.all.mainnet.ethdisco.net)。

これを繰り返していくと、最終的に以下のようなenrレコード、つまりノードレコードが返ってくる。

$ dig -t TXT DTLXHBXBBTRK3PIDC6MS5TC3EU.all.mainnet.ethdisco.net
...
DTLXHBXBBTRK3PIDC6MS5TC3EU.all.mainnet.ethdisco.net. 21599 IN TXT "enr:-Je4QGH3fRF3gwheutnIJjnb_gsrmZ1Ww0yHgt4d3K65M0ahUbKLz6y6fI_1REOUTuElodemxwsKgYkUt70DoCbYlasNg2V0aMfGhOAp6ZGAgmlkgnY0gmlwhMOwtZSJc2VjcDI1NmsxoQP1j8zSY7oyJBL_NyRGa713TTAYt_oAyIdQtZwn5geYhYN0Y3CCdl-DdWRwgnZf"
...

このレコードをEIP-778のルールに従ってデコードすればノードの情報が得られる。

まとめるとクライアントが行うノード探索のステップは以下のようになる。

  1. レコードツリーを表すURLのドメイン名に対してTXTレコードをクエリする。
  2. 返ってきたTXTレコードの内容がenrtree-root=v1を含むか検証し、レコードツリーURL内の公開鍵とレコード内容とレコード内の署名を使ってデータの正しさを検証する。
  3. enr-rootの値をサブドメインにしてDNSに対してTXTレコードをクエリする。
  4. クエリの応答値によって処理が別れる。
    • enrtree-branchが返ってきた場合は、そのレコード内のハッシュをサブドメインとして手順3を続ける。
    • enrが返ってきた場合、デコードしノードレコードを検証しローカルのノードストレージにインポートする。

↑を全てのブランチに対して実行すると、最終的に各リーフノードに設定されているノード情報を全て取得できるようになる。

link-root

↑ではenr-rootを探索したが、link-rootは別のドメイン名にある別のリストをリンクするために使用するものとされており、こちらのツリーはリンクのみでノード情報は含まれてなさそう。

既知のDNS

各環境毎のDNSのノードリストは以下のリポジトリで公開されている。

https://github.com/ethereum/discv4-dns-lists

ちなみに↑のいずれの環境のドメインでもlink-rootにサブツリーが設定されているものは無かった。