この記事では、Bitcoin におけるネットワークへの参加方法とピア接続する仕組みについてまとめていく。(※この記事は bitcoind version 0.9.3 に基づいているため、最新バージョンとは挙動がことなるところがある)
新しいノード(A)を立ち上げたとき、接続先として他のノード(B)を見つける必要がある。B に接続するためには TCP 接続を確立(デフォルトでは 8333 番ポート)し、VERSION メッセージを送信することでハンドシェイクを始める。B は接続を承認し確立するために VERACKメッセージを返す。ではどのようにしてピアを見つけるのか?
ピアを見つけるには主に3つの方法がある。
DNS シードを使う
DNS シードとは、Bitcoin ノードの IP アドレスリストを提供する DNS コンテンツサーバのこと。DNS シードは Bitcoin のコミュニティメンバーが管理しており、動的 DNS シードや静的 DNS シードを提供している。動的 DNS シードとは、自動的にクローラ等で集められたアクティブなノードらのリストの中からランダムに選んでクライアントに返すタイプのものである。静的 DNS ノードは手動でノードリストを更新するものである。どちらの場合でも、クライアントがデフォルトのポート番号(Mainnet:8333, Testnet:18333)を用いて DNS シードに接続した後、DNS シードはそのクライアントをノードリストに追加しておく。(しかし、DNS シードの結果は認証されたものではなく、悪意のあるシード管理者や中間者攻撃によって攻撃者らが制御しているノードの IP アドレスリストのみを返すことができる。従って、DNS シードのみを独占的に頼るべきではないとされている。)
ADDR メッセージを使う
ADDR メッセージは最大 1000 個の IP アドレスとそれらのタイムスタンプを含んでおり、ピアからネットワーク情報を得る時に使用される。このメッセージを使って参加する時は、ハードコーディングされた信頼できる IP アドレスのリストから接続先ピアをランダムに選んで接続要求を出す。
IRC を使う
2020 年時点ではすでに使われていない。
上記の3つがネットワークに参加する際に使われる(使われていた)方法である。しかし、初めてネットワークに参加する場合に限っては DNS シードを使用するケースが多いだろう。
2020 年 2 月時点では、DNS シードは以下の 8 つがあるようだ。この DNS シードは BitcoinCore のソースコードに書かれており、Github から確認できる。
VERSION メッセージ
ノードが外部との接続を確立するとき、そのノードのバージョンを直ちに広告する。これを受け取ったノードは自分のバージョンを返信する。互いのバージョン情報を交換するまでは他の通信は一切できない。
VERACK メッセージ
VERSION メッセージが受け取られたらその返信として送られるメッセージ。これはコマンド文字列の “verack” を含むメッセージヘッダーのみで構成されている。
ADDR メッセージ
ネットワーク上の自分の知っているノードに関する情報を提供するためのメッセージ。広告されてこないノードは3時間後には忘れられる必要がある。ADDR メッセージは最大 1000 個の IP アドレスとそれらのタイムスタンプを含んでいる。ノードが ADDR メッセージを送るのは2つのパターンがある。1つ目は、自分のアドレスを入れた ADDR メッセージを定期的に各ピアに送る場合。定期的に自分の情報を広告することで、ネットワーク内で知られた存在を維持することができる。2つ目は、10 アドレス以下の ADDR メッセージを受け取ったノードはが、そのメッセージをランダムに選ばれた2つの接続ピアに中継する場合である。
Bitcoin では各ノードは外部に接続する異なる8つピア(outgoing connections : 外部接続)を選択するためにランダマイズプロトコルを使用している。また、最大 117 個の外部から接続されるピア(incoming connections : 内部接続)を受け入れている。(プライベート IP アドレスを使用している場合、内部接続を持つことはできない。)
ノード間の接続は TCP 上で行われており、パブリック IP のみ保存し伝搬させる。そのピアがパブリック IP かどうかは、IP パケットと Bitcoin の VERSION メッセージを比較することで確認できる。この章では、ノードがどのようにネットワーク情報を保存し伝搬しているかについて解説する。
ネットワーク情報は DNS シードと ADDR メッセージを通して Bitcoin ネットワークを伝搬する。DNS シードにクエリを送るのは2パターンある。1つ目は、新しいノードがネットワークに初めて参加する時である。そのノードはアクティブなノードのリストを DNS シードから得る。それ以外の場合は、約 600 の IP アドレスがハードコーディングされたリストにフェイルオーバーする。2つ目は、既存のノードがオフラインの状態からネットワークに復帰し新しいピアと再接続をしたい時である。ここで、DNS シードはそのノードが接続の確立を試みてから 11 秒が経過し、2つ未満の外部接続を持っている場合のみクエリされる。
パブリック IP はノードの tried テーブルと new テーブルに保存されている。テーブルはディスクに保存され、ノードがリスタートする時にも保持される。tried テーブルは 64 個のバケットからなり、それぞれに内部接続もしくは外部接続の確立に成功したピアのアドレスを最大 64 個保存できる。保存されたピアのアドレスに加えて、ノードはこのピアの成功した最も最近の接続のタイムスタンプを保持している。各ピアのアドレスはピアの IP アドレスとその IP アドレスを含む /16 IPv4 プレフィックスとして定義された group のハッシュ値を取ることによって tried のバケットにマッピングされる。バケットは以下のように選ばれる。
# SK = random value chosen when node is born
# IP = the peer's IP address and port number
# Group = the peer's group
i = Hash(SK, IP) % 4
Bucket = Hash(SK, Group, i) % 64
return Bucket
あるノードがあるピアに接続が成功した場合、そのピアのアドレスは適切な tried バケットに挿入される。もしそのバケットが満杯の場合、bitcoin eviction が使用され、4つのアドレスをそのバケットからランダムに選び、tried テーブルから最も古いピアを新しいピアに置き換えて、new テーブルに挿入する。そのピアのアドレスがすでにバケットに入っている場合、そのピアの IP アドレスに関連したタイムスタンプを更新する。タイムスタンプはアクティブに接続されたいるピアが VERSION, ADDR, INVENTORY, GETDATA, PING メッセージを送った時や最後の更新から 20 分以上経過した場合にも更新される。
new テーブルは 256 個のバケットでできており、それぞれにノードがまだ正常な接続を開始していないピアの最大64個のアドレスを保持できます。ノードは DNS シードもしくは ADDR メッセージから情報を得たことによって new テーブルに現れる。new テーブルに挿入されたすべてのアドレス aa はそのアドレスの group と、接続されたピアの IP アドレスもしくはアドレス aa を教えてくれた DNS シードが含まれているグループ(source group)に属しているところに挿入される。バケットは以下のように選ばれる。
# SK = random value chosen when node is born
# Group = /16 containing IP to be inserted
# Src_Group = /16 containing IP of peer sending IP
i = Hash(Sk, Src_Group, Group) % 32
Bucket = Hash(SK, Src_Group, i) % 256
return Bucket
各(group, source group)ペアは各 group が new テーブルに最大 32 個のバケットを選びながら、単一の new バケットにハッシュする。各バケットは一意のアドレスを持つ。もしバケットが満杯の場合、isTerrable と呼ばれる関数がバケット内のすべての 64 個のアドレスに対して実行される。もし、アドレスが terrible な場合(30 日以上たっている、もしくは接続失敗が多すぎた)、その時にそのアドレスは取り除く。そうでなければ bitcoin eviction を実行し、そのアドレスが破棄される小さな変更で済む。
新しい外部接続は、あるノードがリスタートするかある外部接続がネットワークから離脱したら選択される。Bitcoin ノードはブラックリストに載ったものに出会った時を除いて、故意に接続をドロップすることはない。ω ∈ [0,7] 個の外部接続を持つノードは ω+1 番目接続を以下のように選ぶ。
(1)tried, new どちらのテーブルから新しいピアを選択するか決定する。以下の式が tried から選ぶ確率である。ρ は tried と new テーブルに入ったアドレスの数の比率である。
(2)より新しいタイムスタンプに偏ってテーブルからアドレスを選択する。(i)そのテーブルから空でないバケットをランダムに選ぶ。(ii)そのバケットからピックアップする位置をランダムに選ぶ。もしその位置にアドレスが存在していればそのアドレスを以下の確率で返す。
そうでなければ、そのアドレスを拒否し(i)に戻る。受理確率 p(r,τ) は r と τ の関数であり、r は拒否されたアドレスの数を表し、τ はアドレスのタイムスタンプと現在の時間との違い(10分ごとに計測する)を示している。
(3)アドレスに接続する。接続に失敗したら(1)に戻りやり直し。
今回は Bitcoin ノードがどのようにネットワークに参加するか、どのように接続相手を選択しているかについてまとめた。この記事では bitcoind version 0.9.3 に基づいて書いているが、Eclipse Attack の影響緩和のため現在のバージョンでは一部が変更されているはずである。Eclipse Attack についての記事と、その後の変更点については次回以降の記事でまとめる。