『ゼロから創る暗号通貨』
第2章: P2Pネットワーク : 基盤作りから始めよう
濵津 誠/hamatz
本章では、SimpleBitcoinの基盤となるP2Pネットワークを実装します。ここで実装するP2Pネットワークでは、前章で登場したWalletとServerの機能を、EdgeノードおよびCoreノードという2種類のノードに割り当てます。2種類のノードの接続形態として考慮が必要になるのは、CoreノードとCoreノードの接続、および、CoreノードとEdgeノードの接続です。それぞれの接続を実現するプログラムを順番に書いていくので、ぜひ主人公たちと一緒にコードを書き、その動作をお手元のPC上で確認しながら読み進めてみてください。
本章を読み進める際には次のようなポイントを意識してみてください。
「それでは、お待ちかねの開発に取り掛かろう。予告どおり、まずは暗号通貨に直接は関係しないP2Pネットワークを作ってもらうよ」
「なんでP2Pネットワークを作る必要があるんだっけ」
「暗号通貨の基盤となるのがブロックチェーンであり、そのブロックチェーンは中心が存在しない複数のサーバーが連携して、はじめてうまく機能する。そのようなサーバー間の連携は、P2Pネットワークにほかならない。全体像と照らし合わせると図2.1で示す箇所になる」
「いきなりP2Pネットワークを実装しろとかいわれても、あんまイメージわかないんだよなぁ」
「当然そうだろう。ここまでの説明では、ブロックチェーンにはWalletとServerという2つの登場人物が存在する、という話しかしていないからね。これから作りたいブロックチェーンを機能させるためのP2Pネットワークにおける両者の関係性や、連携方法については、まったく触れていない。だから、まずはそれらをはっきりさせる必要がある。それがこのカリキュラムのスタートポイントだ」
「よかった……。コードを書く前にもうちょっと説明があるんだ」
「念のために、P2Pの基本からおさらいしておこう。P2Pは、peer-to-peerの意味だ。peerには、『(能力などが)同等の人』みたいな意味がある。相互に接続されているpeer同士がそれぞれ通信したり各自でデータを保持したりする図2.2のような形態のネットワークだ」
「わざわざP2Pなんて名前がついているってことは、他の形態のネットワークもあるっていうことだよね」
「ある。というか、中心となるサーバーに他のクライアントが接続する図2.3のような形態のほうが一般によく見られる。こういう形態は、クライアント-サーバー型のネットワークと呼ばれている」
「うん。それはさすがに知ってた。暗号通貨では、中央サーバーがないP2Pが選択されている、ってことだよね」
「そういうことになる。どこかのpeerが離脱したり、あるいは新しいpeerが増えたり、そういう変化に柔軟に対応できるようにすることで、特定のプレイヤーに依存することなく成立し続けるネットワークが暗号通貨には必要不可欠だからね」
「なんか、P2Pネットワークに繋がるpeerのことをサーバーって言っているけど、暗号通貨の登場人物はWalletとServerの2つだったよね。Walletはどういう位置づけになるの?」
「いい質問だ。P2Pネットワークにおいて、いわゆるサーバーにもクライアントにもなり得るものを『peer』と呼ぶことにする。その定義からするとWalletはクライアントの役割しか果たさないわけだから、『peer』ではない。Walletは、新しいTransactionを生み出したときにだけ、Server群にそれを伝えられれば十分。そう考えると、常時P2Pネットワークに接続しておく必要はない」
「なるほどー。Walletは『P2Pで繋がっているものではない』なら、しばらくは忘れていてもいいのかな」
「単にP2Pネットワークを実装することが目標なら、その認識で正しい。しかし、最終的に作りたいものはP2Pネットワークそのものではなく、暗号通貨の基盤となるブロックチェーンだ。WalletからServerに対してTransactionが送信されるというイベントがあって、はじめてブロックチェーンに更新が発生するのだから、相互接続するServerだけ作っても暗号通貨としては機能しない」
「たしかに」
「よって『peer』とはいえないWalletについてもP2Pネットワークの一部としてまとめて考えるほうがいいだろう。とはいえ、WalletとServerは同格の存在ではなく、役割が異なる。そのことを明確にしたほうがいいので、便宜上、WalletをEdgeノード、ServerをCoreノードと呼び分けることにしよう」
「あえて名前を変えて呼ぶのはなんで?」
「P2Pネットワークは暗号通貨以外の用途でも利用が可能だよね?WalletとServerは暗号通貨用途を実現するアプリケーションとしての名前であって、基盤としてのP2Pネットワークを実現するための呼び名としては適切ではない」
「ああ、なるほど。通信に特化した部分を切り出して別に名前を与える、ってことだね」
「そのとおり。次は、このCoreノードとEdgeノードについて詳しく見ていくことにしよう」
「CoreノードとEdgeノードの関係を図にすると、図2.4のようになる」
「Coreノード同士がそれぞれ相互に接続してて、各Coreノードにぶら下がるような形で複数のEdgeノード群が接続されている、って感じだね」
「そう。EdgeノードはP2Pネットワークに対し、必要なときにだけ新規のTransactionを送信する。自身に関連する値が変更されているかを定期的に確認するだけでよいので、いずれかのCoreノードを選んで接続するだけだ」
「この図を見るまで、Transactionってのは、Edgeノードから全部のCoreノードに対して送信されるものかと思ってた」
「そういう形にするのもアリだとは思う。けれど、Coreノードの数が3桁〜4桁と増えていった将来のことを考えると、全部のCoreノードに対してEdgeノードからTransactionを一括で送るのは効率が悪そうだよね」
「そりゃそうか」
「余談だけど、その意味では、CoreノードがP2Pネットワークに参加している全部のCoreノードを記憶している、というのも効率が悪い。そこで一般的には、図2.5のように、Coreノードのネットワーク同士を繋ぎ合うような、『リレーノード』などと呼ばれるものを用意する」
「ふむふむ、なるほど。リレーノードか。また面倒そうなものが増えたな……」
「まあ、SimpleBitcoinでは話を単純化するために、今回はCoreノードとEdgeノードの2種類だけで話を進めていこう」
「とはいえ、いくら話を単純化するといっても、Edgeノードの接続先を1か所に絞るような構成にすれば、すべてのEdgeノードが1つのCoreノードに集中してしまうよね。それはさすがにあんまりだ。Edgeノードの接続先が1か所に集中することなくいい感じにバラけるようにしたいけど、それにはどうすればいいかなー」
「ちゃんとやるなら、Coreノード側というかP2Pネットワーク側で、接続されているEdgeノードの数といった何らかの基準に応じて、登録を要請してきたEdgeノードの接続先を割り振るような機能があるほうがいいんじゃないかな」
「たしかに。そっちのほうがちゃんとしてる。でも、けっこう複雑になりそうだし、ここは日和ってEdgeノード側で接続したいCoreノードを選択する程度にしておきたい……」
「それは悪くない判断だよ。では、ユーザーが自分で接続先のCoreノードを選択するとして、その候補はどうやって入手するとよいだろうか?」
「気持ち的には手動で、と言いたいところだけど、さすがにCoreノードの一覧をダウンロードしてきてユーザーに選ばせるくらいのことはしたほうがいいかなぁ」
「その機能を入れるにしても、肝心のCoreノードの一覧を取得する先は手動で入力するか、あるいは、あらかじめソースコードの中に固定値を入れておく必要はあるよね」
「あー、たしかに。だったら、もう最初からCoreノードの候補をいくつか仕込んどけばよくない?」
「SimpleBitcoinはゼロからの立ち上げだから、最初からがっつりCoreノードが揃ってる前提はないよ」
「そうだった。となると、どこか特定の場所にCoreノードを登録してもらうことにして、そこから一覧をもらって接続先のCoreノードを選ぶような方式にするか……」
「せっかく『特定のプレイヤーに依存しない』ようなブロックチェーンを作ろうとしてるのに、いきなり特定の何かに依存する前提にするのは、ちょっと悲しくないかい?」
「うわー、たしかにそのとおりだー。あー、どうすりゃいいんだー」
「あんまり考えすぎるとドツボにハマるし、導入初期はシンプルにしておいて、それなりにネットワークが立ち上がった後でCoreノードの選択方法を変える、くらいの気持ちでもいいんじゃないかな」
「つまりどういうこと?」
「接続先の入力は、とりあえず手動でいいんじゃないか、ってこと。ユーザーが接続のために必要な情報を事前に入手する感じだよ」
「えっ?そんなんでいいの?」
「まぁ、完全なビットコインではなく、SimpleBitcoinだしね。Edgeノードが利用可能な接続先情報を管理するための中央サーバーを前提にして、そのサーバーを運用し続ける負担を背負うことに比べれば、ずっとマシなんじゃないかな?」
「いわれてみれば、そりゃそうか」
「でも、Edgeノードの接続先って、本当に1つのCoreノードでいいのかな?」
「なんでそう思うんだい?」
「前に『トラストレス』って話があったよね。トラストレスという観点で考えると、接続先のCoreノードを無条件に信頼するのはダメなんじゃないかって気がするんだけど」
「いい視点だ。セキュリティの話だね。間違いなくいい視点ではあるんだが……」
「……だが、なに?」
「それを真面目に解決しようとすると、まったくシンプルではなくなってしまうんだよね。たとえば、Edgeノードが接続先のCoreノードから取得できるブロックチェーンの情報に、嘘が混じっている可能性について心配するとしよう。君が疑問に思ったとおり、接続先が1つのCoreノードしかないと、この可能性を排除できない。複数のCoreノードから情報をもらう形にして、それらを比較できるようにすれば、ひとつの解決策になる。でも、その複数のCoreノードの情報ってのは、どうやって入手するんだろう?」
「Coreノードが信頼できない前提なら、そのCoreノードからもらうP2Pネットワーク上の他のノードの情報だって、やはり信用しちゃいけないわけか。かといって、たとえばどこかの掲示板で複数のCoreノードの情報を探してきたとして、それが信用できるかといえば、そんなわけもない、と」
「そのとおり」
「だったら、そもそも、CoreノードにEdgeノードが接続するっていう方式を取らなければいいのでは。参加者が自分でCoreノードを用意する前提にしてしまうのがいいんじゃない?」
「一見すると、そう思えるよね」
「その言い方だと、それじゃダメってこと?」
「さすがに、自分で立てているCoreノードなら信頼できる。それは正しい。ただ、自分が立てるCoreノードは、どこか他のCoreノードに接続することで、はじめてP2Pネットワークに参加できる。じゃあ、そのときに接続する先のCoreノードの情報は、どうやって信用するに値すると判断できるだろう。自分が立てるCoreノードにしたって、結局はP2Pネットワークに接続するにあたって同じような問題に直面するだろう?」
「たしかに、Coreノードを自前にしたからといって、何も解決してないな」
「そう。いまはとりあえず、『Coreノードとして信頼可能なものの一覧が、コミュニティなど何らかの方法で定期的にメンテナンスされている』
といった前提を置くことにしよう。何らかの方法でメンテナンスされた情報を信頼してるという時点で、厳密にいうとこれは『トラストレス』じゃない。しかし、SimpleBitcoinではシンプルさを優先しよう。実をいうと、オリジナルのビットコインにしたって、やっていることはこれと大差ない」
「そうだね。Coreノード同士で認証し合うみたいな話になると、どんどん複雑化していって、いつまでたっても暗号通貨の話にまで進まなそうだ」
「Coreノードが別のCoreノードを何らかの方法で信頼できるという前提にしておけば、Edgeノードが信頼できるCoreノードがないから自分でCoreノードを立てる、といった議論にも意味がなくなる」
「そりゃそうだね」
「念のため強調しておくと、各参加者が自分でCoreノードを運用すること自体に意味がないわけではない。セキュリティを理由にCoreノードを立てることに意味がないといっているだけだから、そこは間違えないように」
「Coreノードを立てる意味がない、なんて話になったら、Serverを提供する人にとって価値が見出せるという、SimpleBitcoinのそもそもの前提が成立しなくなるものね。ちゃんと覚えてるよ」
この節では、本書で実装する暗号通貨SimpleBitcoinの基盤となるP2Pネットワークについて、次のような仕様を確認しました。
次節では、これらの仕様を前提として、CoreノードとEdgeノードを実装していきます。
「実装にあたり、まずは、CoreノードとEdgeノードが接続されて図2.4のようなP2Pネットワークの形になるまでに、どういうステップを踏めばいいかを考えてみよう」
「えっと、とりあえず、拠り所となるCoreノードが存在していることを前提とするね。そのCoreノードに、別のCoreノードが接続する。そういう接続が次々に起こって、Coreノード同士のネットワークができる。その中で信用可能と思われるCoreノードをEdgeノードが選択し、そこに接続する」
「そうだね。じゃあ、それを絵に描いてみよう」
「図2.6のような流れになると思う」
「いいんじゃないかな。では、この図2.6を見ながら、CoreノードとEdgeノードがそれぞれどんな機能を持っていればいいかを考えてみよう。まず、2つめのステップで必要になる機能は何かな?」
「すでに起動済みのCoreノードに対して接続を依頼し、それを受け付けてもらう必要があると思う」
「それだけ?」
「えっと、Coreノードは接続する相手がどんどん増えていくから、最初のCoreノードに接続が完了したタイミングで、そこに接続されている既存の他のCoreノード群の情報を教えてもらう必要があるかな」
「厳密にいえば、初回接続時だけじゃなく、Coreノードが追加されるタイミングで常に通知してもらう必要があるけどね」
「なるほどたしかに」
「ほかにはどうだろう?」
「あとは、接続依頼と対になる形で、ネットワークから離脱したい場合にそれをネットワークに伝える機能も必要になると思う。離脱するときにお行儀よく離脱メッセージをくれる相手ばかりとも限らないだろうから、定期的に接続状況の確認ができるような機能も必要かなぁ。Ping的なやつ」
「Pingに返事がなかったらP2Pネットワークから離脱したものとして扱うことにしよう、というわけか」
「そうそう」
「じゃあ、次は3つめのステップ、Edgeノードの追加について考えてみようか」
「これは簡単だね。CoreとEdgeの区別をつける必要はあるけど、基本的にはCoreノードとほぼ同じで、接続要求と離脱要求が処理できればいい。他のCoreノードに接続されているEdgeノードについては情報を知る必要ないから、それくらいかな。念のため、Edgeノード自身が接続しているCoreノードの生存確認と、死んでしまっているときに他のCoreノードにぶら下がり先を引っ越せるようにCoreノードの一覧は持てるようにしておいたほうがいいかも」
「Coreノードの生存確認の方法と、Coreノードの一覧をもらう方法については、CoreとEdgeで区別をつける必要はないだろうね」
「うん。以上をまとめると、CoreノードとEdgeノードに必要な機能はこんな感じになると思う」
「いいね。もちろん、接続した後でメッセージを送信しあうためにP2Pネットワークを構成するわけだから、このほかにメッセージ送受信機能が必要になる。とはいえ、接続管理に関するものとしては、ここにまとめた機能で十分だろう。次は、これらの機能を使ってノードをどう実装すればいいか、少し詳しく考えてみよう」
「Coreノードの動作から考えるね。まずは、始原となるCoreノードに自分の接続先情報を送信し、それを登録してもらうことによって、他のノードからも接続可能なCoreノードとなる。それから、EdgeノードなりCoreノードなりが接続してくるのを待って、そのときがきたら送られてきた情報に応じて処理を実行して返す、という感じかな」
「だいたいあってる。Coreノードの動作を絵としてまとめると、図2.7のようになる」
「ん?ソケット?」
「そう。各ノード間のデータのやり取りにはソケット通信を使うことにする。Server SocketとClient Socket、この2つのイメージを掴むために、まずは簡単な通信プログラムを書いてみるところから始めてごらん」
「図2.7によると、Server Socketは、いったん開いたら他のノードからのデータを受け取るためにずっと口を開けておくんだね。一方、Client Socketは、送信が必要なデータがあるときだけ暫定的に開き、送信が終わって役目を果たしたら使い捨てられる。そんなイメージであってる?」
「そのとおり。というわけで、まずはServer Socketを開いて待ち受ける側の簡易サーバーと、その待ち受けているServer SocketにClient Socketを使って接続してデータを送信する簡易クライアントを作ってみよう」
「開いているソケットにデータを送信すればいいだけだから、クライアント側は簡単だね!せいぜい気をつけることがあるとすると、文字列の場合そのまま送信はできないからUTF-8でエンコードすることくらいかな……」
「次はサーバーのほうを考えてみよう」
「うーん、クライアントから接続するのだから、クライアントにわかる接続先の名前が必要だよな。ソケットプログラムの解説にあるgethostname()
を使えばいいのかな」
「利用するユーザー側の環境制限が少ないほうがいいから、IPアドレスで接続できるようにしたほうがいいんじゃないかな。ちなみに、自分で自分のIPアドレスの確認ができる人には無用の機能だが、Googleの提供しているパブリックDNSサーバーを指定してs.connect(("8.8.8.8", 80))
のようにすると、s.getsockname()[0]
でIPアドレスが取得できる」
「なるほど。これから先、複数のクライアントが接続してくることも考慮して作っとくか」
「実験目的で会社の中といった閉じられた環境で使いたい場合など、Googleのサーバーに接続できない場合も考慮して、IPアドレスの手動入力も可能にはしておくべきだろうね」
「たしかにそれはそうかも」
「さっそく、サーバーを動かしてクライアントから接続してみよう」
python3 server.py my ip address is now ... 10.1.1.27 ← ※① Waiting for the connection ... Connected by .. ('10.1.1.27', 55017) Waiting for the connection ... Hello! This is test message from my sample client!
「いいんじゃないかな。次は、このサーバーとクライアントで実際にやり取りするメッセージのフォーマットを考えてみようか」
「メッセージのフォーマットかー。単に文字列をやり取りするのじゃだめかな」
「それだと必要な情報を取り出すのが大変になる。とはいえ、複雑な情報を扱うわけではないので、シンプルにJSONとかでいいだろう」
「最低でも、このメッセージがどんな種類のメッセージなのかを伝えられないとだめだよね。それと、プロトコルの名前とバージョンくらいが宣言されてれば、受け取ったときに困らないかな。あとは、メッセージの種類に応じて必要になる情報を格納するフィールドが必要だよね」
{ 'protocol' : 'プロトコル名。今回は simple_bitcoin_protocol', 'version' : 'バージョンを宣言する。とりあえず0.1.0', 'msg_type' : 'メッセージの種別を宣言するための識別子を格納する', 'payload' : 'Coreノードのリスト等、なんらか補助的に値を渡す必要がある場合はココに格納する' }
「悪くないね。それじゃあ、これまでの話を統合して、まずはメッセージを取り扱うためのクラスを書いてみよう。メッセージの生成と、パースくらいできればいい」
「クラスの名前はMessageManager
でいいかな。必要になりそうなメッセージをすべてMSG_***
みたいな名前で定義しておくとして、メソッドとしては、とりあえず、メッセージを組み立てるbuild
と、パースするparse
だけ書けばいいってことだよね。バージョンとかプロトコル名の不一致みたいな最低限のエラー処理だけは入れておこう」
「いいね。これをベースにして、実際にCoreノードとEdgeノードを作って繋いでみよう」