はじめに
ETHの相場が高騰しており、相場を確認するたびに楽しい気分になる昨今ですが、Ethereumファンの皆様や暗号通貨評論家の各位やブロックチェーンマニアの諸兄姉には、いかがお過ごしでしょうか。
さてこの記事では、そんな楽しい流れとはさほど関係なく、Ethereum Yellow Paper、Ethereum RLPx、Ethereum Node Discovery Protocol、DEV P2P Wire Protocol、Ethereum Wire Protocol PV61、等々のEthereumの主要な仕様をScalaで実装して、どうにかある程度まともに動くEthereumのノードを実装することができました、という地味な話を書いてみたいと思います。
その実装したソースコードは、こんなものです。
こういう話はチラシの裏に鉛筆で書くのが最善手だとむかし習ったのですが、新聞等を取っていないものでチラシを入手できなかったため、Qiitaに書かせていただくことにしました。この記事が、筆者以外のどなたかにとっても、多少なりとも役に立つ情報を含んでいることを願っています。
いちおうの想定読者としては、量産型altcoinではない新たな暗号通貨やブロックチェーン系のシステムを実装する必要があるか実装してみたいと思っているが、全体の構造や開発の作業がどんなものになるのかイマイチ見通せないので事前に情報を得たい、といった方を念頭に置いています。(でも、そんな人いるのかなあ……)
どんなものか
本家Ethereumについて
EthereumやEthereum Projectについて細かく説明しているときりがないので、知らない方は適当にぐぐるなどして、以下の説明をご自分で補完してください。
Ethereumは、中国をはじめとする世界中で腐るほど量産されたビットコイン類似の暗号通貨システムの域を超えて、チューリング完全なプログラム実行基盤としての仮想マシンのアーキテクチャを定義している点に大きな特徴があります。
この機能を使うことによって、公証やエスクローやその他 任意の取引の柔軟かつ公正な自動実行が可能になる、というスマートコントラクトのコンセプトが前面に押し出されているわけです。
このようなコンセプトに共感するにせよしないにせよ、ともかくEthereumは口先だけのpaperwareプロジェクトではまったくなく、むしろその真逆であるということは誰しも認めざるを得ないところでしょう。
Ethereumプロジェクトは、去年(2015年)の7月末に「Frontier」リリースという開発者向けの版を公開しました。それ以来約半年間、世界中の何千というノードが、非集権的かつ相互牽制的なアーキテクチャで動作しながら単一のブロックチェーンを良好に維持しており、そのブロックチェーン上で、暗号通貨ETHの採掘や授受や、コントラクト(契約)に定義されたプログラムコードの実行やデータの入出力などが盛んに行われています。
そして来月(2016年2月)?だかには、もう「Frontier」の後続版として、開発者ならぬ一般人でも使える進化した安定バージョンである「Homestead」がリリースされるという話になっているようです。このスピード感は素晴らしいですね。
最近、マイク・ハーンの「ビットコインは失敗に終わった」発言のおかげもあって、このEthereumの基盤である暗号通貨ETHの相場が高騰気味で笑いが止まらないなのは、この記事の冒頭でちょっと触れたとおりです。
この記事で紹介する互換実装について
Ethereum Projectは、スイスに財団(非営利法人)を設立しているほどの本格派プロジェクトだけのことはあって、仕様をかなり真面目に文書化していてくれているので、それを真剣に読んで努力すれば、Ethereumみたいなものの具体的な構造を学習しながら実装できるだろう、と期待させられるものがあります。
例えば、Ethereum Yellow Paperをざっと眺めてみると、ものすごく読みたくない、という人間として非常に自然な感慨が湧いてくる反面、さすがにこれを読めば、Ethereumのようなシステムの設計や実装については、たいていのことが分かるんじゃないか、という淡い期待を抱かされる面もまたあるわけです。
この記事で紹介する互換実装は、後者の期待の正しさを信じて暗い森をさまよったあげく、どうにか光のある世界に生還することができました、という話の成果物として捉えていただければと思います。
実際、このEthereum Yellow Paperの結論(Conclusion)には、
本論では、Ethereumのプロトコルを提示し、それについて議論し、それに形式的な定義を与えた。このプロトコルに従うことによって、読者はEthereumネットワークにおけるノードを実装することができ、また非集権的でセキュアでソーシャルなオペレーティング・システム上において他のノードと結びつくことが可能になる。
そして、その相互作用におけるルールをアルゴリズム的に定義し、自律的に執行するためにコントラクトを作成することができる。
とか書いてあるわけですし。
現状でできていること
現在の実装ででていることは、おおむね以下の通りです。
- UDP上でのpeer discovery protocolの実装
設定ファイルによって与えられた少数のシードノードを出発点として、ピアノードを多数発見し、他のノードのピアディスカバリーにも協力すること。
- TCP上でのRLPxの実装
楕円曲線上のDiffie-Hellman鍵合意によって、暗号化された通信経路を確立し、厳密なMDC(modification detection code)を含む相互メッセージングを可能にすること。
- RLPx上でのP2Pプロトコルおよびethプロトコルの実装
ピアとの間に確立されたRLPxの伝送路上で、ブロックやトランザクションの情報を授受すること。
- トランザクション(送金、コントラクト作成、コントラクト呼び出し)実行の実装
取得したブロックに含まれるトランザクション(コントラクトを含む)を、自ノードの仮想マシン上で実行し、その実行に伴って、送金やコードの内容による処理やトランザクション手数料の徴収を、自ノードが管理する「世界の状態」に反映させること。
- ブロックチェーンのコンサスおよび同期処理の実装
ブロックおよびトランザクションおよびその実行後の状態について、その形式および内容の正しさを検証し、問題がなければ採掘者への報酬を自ノードが管理する「世界の状態」に反映させ、またそのブロックを自ノードが管理するブロックチェーンに連結すること。
- 新規トランザクションの発行
自ノード自身が新たなトランザクションを発行して、他ノードに広告すること。
以上をまとめると、要するに、プロセスを起動すると、勝手に複数のEthereumノードを探してきてそれらからブロックをパラレルにダウンロードして、ブロックに含まれるトランザクションを順番に自ノードの仮想マシン上で実行して、その結果のブロックチェーンおよび状態を永続化して記録する、ということです。
この記事を書いている時点において、EthereumのFrontierリリース以来の全ブロック数が100万ブロック弱で、それに含まれるトランザクション数は140万件弱ぐらいですが、この全部のトランザクションを実行し、実行後の世界の状態の正しさを検証し、自ノードのブロックチェーンに連結することができています。
その所要時間は、MacBook Pro上で10時間程度です。(細かいログを出したりしなければ。)
現状できていないことや課題
- 新たなブロックの生成および報酬の獲得
他人が生成したブロックのproof of workの検証は当然実装していますが、自ノードでの採掘(mining)は、実装していません。要するに、トランザクションの取りまとめとEthashの実装ができていないわけで、そのうちに実装しようとは思います。
ただし、ASICによる採掘支配をことさらに問題視し妨害しようとするEthereum Projectの姿勢について、私は疑問を抱いており、それゆえに、Ethashを調べて実装する意欲があまり湧いてこないという面もあります。
- 仮想マシンのLOG op codeの仕様
あるコントラクトのコードから、CALL op code等によって別のコントラクトのコードが呼びだされた場合のLOG op codeの動作仕様について不明な点があり、140万弱のトランザクションのうち、2トランザクションのみについて、LOG op codeを実行した後のトピックのBloom filterがEthereumの標準実装の出力と合致していません。
この情報は、世界の状態にもブロックチェーンにも影響を及ぼさないのですが、しかし仕様齟齬なのでちゃんと解決したいところです。
- 仮想マシンの性能
現状の実装だと、Yellow Paperで与えられている操作的意味論(operational semantics)の通りのスタックマシンをバイトコードインタプリタとして定義して、それにコントラクト等のコードを食べさせているだけで、実行時コンパイル等の最適化は何もしていません。
また、このスタックマシンのワード長は32バイト(=256ビット)と巨大なので、単純に実装すると多倍長整数演算が頻発するのですが、その点についても何の工夫もしていないので、たぶん性能改善の余地は大きいと思います。
- Windows上でのデータストレージの性能問題
世界の状態やブロックチェーンを永続化するために使うデータストアとして、数種類試した中では、性能的にはLevelDBが良かったのですが、JNI越しにWindows版LevelDBの実装を呼び出すところに固有のバグがあるらしく、この問題を解決できていないため、現状では実行時の動作環境がWindowsだった場合にのみ、LevelDBではなくBerkeley DB JEを使うようになっています。
結果として、Windows上では性能が悪くなっています。設定によるチューニングの余地はあるかもしれませんが。
- WhisperとSwarm
どちらも、非集権的アプリケーションを支える基盤として、Ethereum Projectで開発されているプロトコルです。
Whisperは掲示板とかメッセンジャーみたいなものを実現するプロトコルで、SwarmはBittorrentのようなバルクデータ共有のためのプロトコルです。(これら2つを合わせたのがWinny2の構想だったわけですが……)
本家の方でも、現在のFrontierリリースでは特に活用されているわけではないですし、ブロックチェーンや世界の状態と関係もないので、未実装としています。
- NAT越え
手が回ってません。まあ、Winny用語でいう「ポートゼロ」状態でも、友達の数が減るだけで特に問題なく動くので。
最後に全体的な問題として、すでに存在するEthereumネットワーク内のインテリジェントな本家実装のノードの挙動に依存して動作している面が多々あります。なので、現時点のこの実装のノードばかりが存在するネットワークが、全体としてうまく動作することは期待できません。これを解決するためには、挙動のモデルを切り出してシミュレーションするとか、実験しながら試行錯誤するとかが必要になるでしょう。
また、ちゃんとした互換性テストなども通していませんし、悪意ある攻撃への対策なども実装していません。**くれぐれも、この実装に自分のアカウントの秘密鍵を本番ネットワーク上で扱わせるようなことは、しないでください。**せっかく高騰しているあなたのETHが全額失われてしまっても、責任は取れません。
得られた教訓など
そもそもなぜEthereumの互換実装などということに手を染めたかと言うと、Ethereumみたいなものの中身を、正確に詳しく具体的に理解したかったからです。
例えば、ブロックチェーンやPoWや対外部通信プロトコルなどがまったく異なるシステムに、EthereumのVMだけをはめ込もうとしたら、何か課題はあるのか、とか、逆にEthereumからVM関連の仕様を取り除いたらどんな感じになるのか、とか、Ethereumにsegregated witness方式を導入するとなったらどれくらいの規模の改変になるのか、とかですね。
「バナナを持ってこようとしたら、それをつかんでいるゴリラがいて、なんだかんだでジャングル全体がセットでついてくる」みたいな良くあるパターンについて、明確な見通しを持ちたかった、ということです。
全体の構造とサブシステム間の依存関係
「ブロックをダウンロードしてトランザクションを実行し、自ノードのブロックチェーンにつなげる」という機能を実現するための領域において、サブシステム間の実質的な依存関係は、下の図のようになっています。
我ながらびっくりするほど新たな驚きがない図で、こんなことぐらい、わざわざ互換実装を作ってみなくとも簡単にわかったんじゃないか、と言いたくなりますが、それ以上いけない。
いずれにせよここから言えるのは、あるシステムに、Ethereum風のスマートコントラクト実行基盤としてのVMをはめ込もうとするならば、Ethereumが定義するアカウントやコントラクトのモデルに準拠するか否かとか、VMからブロックチェーンの情報を参照できるべきだ、というEthereumの主張を受け入れるか否か等々の意思決定が必要になる、ということです。
それらについて、Ethereumと異なった意思決定をするならば、その仮想計算機は重要な部分においてEthereumとは類似性がないものになるでしょうから、Ethereum Projectが作っているコンパイラ(VMのバイトコードを生成するやつ)とか、分散アプリケーション実行のためのいろんなツールとかを、どうにかして自前で調達しなければならないことになるでしょう。
また、Ethereumのような(チューリング完全な)スマートコントラクト実行基盤を導入するならば、コアプロトコルに「世界の状態」の合意を入れないわけにはいかないでしょう。その点で、ブロックのモデルが制約を受けることになりますし、Merkle Patricia Treeのような実装方法の面での裏付けも必要になるでしょう。
他方、Ethereumを元に、そこから特定の部品だけを取り外したり組み替えたりして、単純な暗号通貨システムを再構築するような方向性の改変、例えば、EthereumからVM関連の仕様を除去してMerkle Patricia Treeを残高の答え合わせにだけ使うことにし、それからブロックのモデルをseg wit対応にして……といった改変は、特に課題もなく、淡々とやればできると思います。
開発上の教訓
仕様書よりソースコード
当たり前ですが、システムに関して実質的に重要な情報の大部分が仕様書に書かれるべきだ、という考えには、それが複雑なシステムであればあるほど、根本的な無理があります。Ethereum Projectが提供しているドキュメントは非常に役に立ちますが、やはりソースコードから得られる情報量とは比べ物になりません。
開発の順序とか
この手のシステムをスクラッチから開発する場合、上に貼った依存関係の図の矢印を逆順にたどって実装する以外に手はないと思います。依存先をとりあえずのスタブ実装みたいなものでまかなおうとしても、そのスタブに期待される挙動が複雑すぎてうまく行かないでしょう。つまり、下から上へ、受動的なモジュールから能動的なモジュールへと一歩一歩じわじわと進む必要があるのです。
なので、Ethereumに即して具体的に言うならば、まず「シリアライゼーション→Key Value永続化モジュール→Merkle Patricia Tree」と進むラインと、「VM&コアのモデル」を形にするラインの2系統があって、その両者がそろった時点でようやく、「トランザクションの実行&validation&ブロックチェーンの連結」のテストができるということになります。そしてそれができたら、通信プロトコルを実装して、外部のノードと通信しながらブロックチェーンを操作する、という流れです。
やはり重要なのは、依存関係の矢印を遡る段階ごとにテストを頑張って、極力次の段階に負債(バグの可能性)を持ち越さないようにすることだと思います。他のノードを相手に通信しながら100万件のトランザクションを実行する段階になって初めて判明した問題をデバッグするのは、すごく大変ですから。
実際に今回の実装でも、80万ブロックを超えた段階で初めて呼び出されたコントラクトで顕在化した、VMのメモリの扱いに関する仕様齟齬の不具合があって、デバッグのために血を吐くことになりました。
避けたくても起こってしまうそういう問題を苦しまずに解決できるよう、デバッグ用の機能やロギングには、くれぐれも設計の初期から気を配っておきましょう。(棒読み
もっともEthereumコアプロトコルの仕様自体が、Merkle Patricia Treeを使って「世界の状態」のダイジェスト値を頻繁に答え合わせさせるような安心設計になっているため、何の工夫もしなくとも、今回の実装では自動的にその恩恵を受けた、とも言えるわけですが。
難しいポイントなど
データストレージ
この手のシステムでいちばん問題になりやすいのは、やはりデータストレージではないかと思います。保管されるエントリーの件数が膨大になるので、それによるI/O性能(特に読み取り性能)の劣化をどう防ぐか。
EthereumのFrontierフェイズの全データぐらいなら、LevelDBでは特に問題なく扱えるのですが、Berkeley DB JEを使おうとすると、すでにチューニングを必要とする状況です。
この問題は、プログラミング的に解決するというよりも、ハードウェア等のインフラを含めて対処すべき課題かもしれません。いずれにせよ、ミドルウェアやハードウェア面での選択肢を束縛しないように、key value永続化の実装は、容易に差し替えられるようにしておくことが絶対に必要だと思います。
また現在のEthereumにはない仕様ですが、ブロックチェーンおよび「世界の状態」のデータ量を削減するためには、一部のトランザクションをサイドチェーン空間に逃がすとか、古い情報をアーカイブして退避するとか、そういうことも重要になるかもしれません。
複雑性
そういう課題っぽい話ではなく、プログラミング作業としてどこが特に難しいのか、と訊かれたら…… Ethereumのコア全体がまんべんなく面倒くさくて難しい、と言わざるを得ないと思います。個人的に特に閉口させられたのは、セキュアな通信チャネルを確立して維持するRLPxの仕様の面倒くささでしょうか。MDC(modification detection code)の仕様はパラノイア的だと思います。
もっとも、プログラマーというものは、複雑なことを厳密にやらなければならない、という課題を楽しめるものだとするならば、Ethereum全体がまんべんなく面白い、ということにもなるでしょう。
必要となる前提知識
暗号通貨システムみたいなものを責任を持って自分で設計するには、何よりも暗号技術に対する深く正確な知識が必要に決まっています。
例えば私にはそのような知識がないので、楕円曲線の仕様としてsecp256k1を採用することの当否とか、一方向ダイジェスト関数のアルゴリズムとしてKeccakを採用することの当否は、本当のところ判断できません。
しかしそのような点を割り切って、誰かの判断力を信じてその人が決めた仕様に従うとするならば、「まず難しい数学を勉強しないと手が出せない」というような意味での専門知識は、全体にわたってまったく必要ないように思います。
Bitcoinの様子を見ていると、難しい数学とかよりは、普通のDDoS attackとかへの対策の方が現実的に重要だし、大変であるように感じられます。