VyperでPlasma MVPを実装しました

Ryuya Nakamura
LayerX-jp
Published in
15 min readAug 24, 2018
Merkle Proof Membership Check in Vyper

LayerX(@LayerXcom)研究開発チームのRyuya Nakamura(@veryNR), Osuke Sudo(@zoom_zoomzo)は、Ethereumのスマートコントラクト言語VyperによりPlasma MVPを実装しました。

https://github.com/LayerXcom/plasma-mvp-vyper

VyperによるPlasmaコントラクトの実装事例は今回の実装が初めてであり、今後のVyper・Plasmaの発展にうまく活かしていければと思います。

本記事では、実際にVyperでPlasmaコントラクトを実装する過程で得た知見や、Vyper開発で体験したことについてお伝えしていきます。

目次

  • 本プロジェクトの背景・動機
  • Vyperの特徴
  • Plasma MVP in Vyper
  • Vyper開発の経験

背景・動機

Vyperとは、 EVM用に作られたPythonライクなプログラミング言語で、言語をできる限りシンプルに、コードを読みやすいものにし、その結果コントラクトの安全性を高めることが目指されています。

コントラクトの脆弱性を突きユーザーの資金が奪われる等の様々な攻撃が過去起きてきましたが、Vyperは、このような脆弱性を生みにくくし、コントラクトのセキュリティを高めることを重要視した言語仕様となっています。

ドキュメントの冒頭に書いてある、Vyperのデザインに関する三つの原則がこれをよく表しています。

Security: Vyperでは、安全なスマートコントラクトを自然に開発できなければならない。

Language and compiler simplicity: 言語とコンパイラの実装はシンプルであろうとしなければならない。

Auditablity: Vyperのコードは最大限human-readableでなければならないし、ミスリーディングなコードが書かれにくいようにしなければならない。

Vyperはまだまだ開発中の言語ではあるものの、スマートコントラクトのセキュリティの重要性を考えると、大きなポテンシャルを秘めている言語であり、さらに開発が加速されていかなければならないと感じています。

そこで、私たちはPlasma MVPのコントラクトをVyperで実装することにしました。

なぜPlasmaのコントラクトなのかというと、Plasmaコントラクトは署名検証やマークルプルーフ、優先度つきキューなどさまざまなプログラムから成り立っているため、Vyper特有の特徴を活かした実装事例を共有し、今後のVyper開発の発展に寄与できると考えたからです。

さらに、実際にVyperを使ってコントラクトを作成することにより、言語としての現状の課題や必要な周辺開発ツールなど課題を洗い出し、私たちのチームがVyperの発展のためにできることを明確にしたかったからです。

Acknowledgement

今回我々は、OmiseGoチームのPlasma MVPルートチェーンのコントラクトをベースにして実装を行いました。

また、 Vyperのコア開発者であるJacques Wagener氏, Bryant Eisenbach氏にも、Gitterにて何度も助けてもらいました。

この場を借りて、感謝を申し上げます。

※ 以下の記事は2018/8月時点の情報であり、Vyperの仕様等は今後大いに変わる可能性があります。

Vyperの特徴

コントラクトの書き方

  • 1ファイル = 1コントラクト。contract {…} という宣言はしません。
  • 文法はPythonライクです。修飾子は@private, @payableなどとデコレータで、コンストラクタは__init__()と書きます。selfはそのコントラクト自身を表し、ストレージ変数にはself.myData、関数にはself.myFunc()などとアクセスします。
  • 型は必ず明記し、コンパイラが厳しくチェックします。Pythonのtype hintingのような記法で型を表記します。
  • 定義の順番は外部コントラクトのインターフェース→イベント定義→ ストレージ変数定義→関数定義でなければなりません。
  • 後ろで定義された関数を呼ぶことはできません。

Vyperが意図的に持たない機能

先ほど述べたデザイン原則から、Vyperは下記の機能がありません。(詳しい理由などはドキュメントを参照してください!)

  • Modifierの定義
  • クラスの継承
  • インラインアセンブリ
  • 関数・演算子のオーバーロード
  • 再帰呼び出し
  • 無限ループ
  • バイナリ固定小数点

Plasma MVP in Solidityとの主な相違点

Vyperの仕様上生まれた実装の違いについて、主要なものを、

  • Vyperの根本的な特徴によるもの
  • 現状のVyperの制約によるもの

に分けて説明していこうと思います。

Vyperの根本的な特徴による違い

前述したような、Vyperの設計思想による特徴や制約から生まれた実装の違いを見ていきます。

(1) modifierの代わりにassertを書く

可読性や監査のしやすさの観点から、Vyperではmodifierを定義することはできません。子チェーンのオペレーターのみに実行権限を限定するonlyOperator修飾子の代わりに、直接assert文を書きました。

assert msg.sender == self.operator

ちなみに、require, assert, revertとエラーハンドリングが3種類あるSolidityと異なり、Vyperではassertだけが使えます(Solidityのrequireと同じ)。

(2) 無限ループがない

Solidityでは、 while (condition) {…} を利用してconditionが成り立つ条件下で処理を繰り返すことができましたが、Vyperにはwhileはありません。 for文はありますが、反復回数の上限は決定的でなければならず、range()には整数リテラルしか渡せません。Plasma MVPのケースでは、優先度つきキューのサイズを固定することで全てのループ箇所に関して最大の必要反復回数が決まりました。例えばfinalizeExit関数では、最大の必要反復数はキューの数と等しくなります。

# 1073741824 is 2^30, max size of priority queue is 2^30 - 1
for i in range(1073741824):
if not exitable_at < as_unitless_number(block.timestamp):
break

(3) 幾つかの関数が予め組み込まれている

Vyperでは通常の演算子がオーバーフローを起こさないように作られており、SafeMathライブラリは不要です。max関数、バイト列のslice関数、RLPデコードの関数はビルトインとして定義されている

(4) インラインアセンブリがない

Solidityではインラインアセンブリを書くことができます。例えば、子チェーンのtxの署名のバイト列からr, s, vの値を抜き出す処理はこのように書くことができます。

assembly {
r := mload(add(_sig, 32))
s := mload(add(_sig, 64))
v := byte(0, mload(add(_sig, 96)))
}

Vyperでは、可読性を損ないかねないインラインアセンブリはありません。上記の処理はビルトイン関数であるextract32(), slice()関数を使って実装しました。

r: uint256 = extract32(_sig, 0, type=uint256)
s: uint256 = extract32(_sig, 32, type=uint256)
v: int128 = convert(slice(_sig, start=64, len=1), "int128")

(5) その他

  • 関数の引数を受ける変数は関数内で変えられません。Solidityでも関数の引数に_をつけるプラクティスがありますが、それをさらに厳しくした感じです。
  • 配列にアクセスする時のインデックスや、冪乗の指数にも、整数リテラルしか使えません。

現状のVyperの制約によるもの

Vyperでは、提案自体はapproveされているもののまだ実装されていない機能や、仕様自体が議論中ものが多くあります。このような、今後変わる可能性が大いにあるものの、現状のVyperの仕様上対応が必要だった主な箇所を列挙します。

  • Libraryの仕組みがまだ無いため、一つのコントラクトに全ての関数を書きました。
  • importがないため、root_chainコントラクトの中にpriority_queueコントラクトのインターフェースを書いています
  • Solidityのnewの代わりとしてデプロイ済みのコントラクトを複製するcreate_with_code_of()を用いていますが、現状コンストラクタを実行してくれないため、コンストラクタの代わりとしてsetup()関数を書いて、それをデプロイ後に呼び出すことで代替しています。
  • ストレージ変数の定義で初期値を書くことができないので、コンストラクタで値を入れています。
  • 定数を定義する方法がないので、ハードコードし、コメントで補足する形にしました。
  • 可変長配列がないので、int128/uint256をkeyとしたmapping型を用います。同じく可変サイズのバイト列はないので、バイト列のサイズをあらかじめ制限するか、bytes[1024]などと大きなサイズにしておきます。
  • 構造体型はありますが、構造体の新しい型を定義することはできません。(Solidityはこれができるが構造体型はない) 提案はあります。
  • private関数内ではmsg.senderの値が自身のコントラクトのアドレスになってしまいます。これは現状Vyperでは、メモリアクセスのリスクを考慮しprivate関数をJUMPではなくCALL命令で呼び出しているためです。gasが高くなるという大きな問題もあります。現在他の方法が模索されています
  • その他、unitの扱いが厳密だったり、private関数でも構造体を返せないなどの違いもあります。

Vyperを用いた開発について

具体的にVyperを用いた開発の環境や流れについて説明します。

開発環境

VyperのコンパイラはPythonモジュールです。virtualenvなどでVyper専用の環境を作り、そこにインストールします。docker imageもあります。(参考: Vyperのインストール方法)

各種エディタでVyperのプラグインがありますが、我々はvscodeを使いました。その他のプラグインやツールについてはこちらにまとまっています。

開発の流れはSolidityと大きく変わることはなく、コードを書いてビルドするのみです。コントラクトを書くファイルの拡張子に関して、.vyと.v.pyの二種類があり、前者の方が主流ですが、後述するtruperが.v.pyにしか対応していないため後者にしました。

テスト

テストはtruffleを使いました。truperというnode.jsのモジュールが、Vyperで書かれたコントラクトからtruffle互換のartifactを作ってくれます。truperでビルドすると、MyContract.vyper.json等というファイル名でartifactが作られてしまいtruffleのマイグレーション(.jsonという拡張子でファイルが作られる)で上書きされなくなってしまうので、npm run buildでtruperでのビルドの後にそのまま.jsonにリネームするようスクリプトを書いています。

また、Vyperのコンパイラはgetter関数の自動生成を行いません。テストに必要なgetterは明示的に定義する必要があります。

なお、ちょっとしたVyperのコードの挙動を確認したい時、Remixのようなツールがないので、truffleのdevelop環境を使いました。vyper.onlineというwebアプリのコンパイラもあり、ブラウザでサクッと文法などを確認したいときはこちらも便利です。ただし、今年2月くらいから更新されていないらしく、最新のVyperの仕様に対応していないことがあります。

苦労したこと

(1) デバッグ

Vyperのデバッグは少し大変です。truffleのdebuggerはVyperでは使えず、sol-traceのようにエラー行を表示してくれるツールもありません。

仕方なく、テストコードの中で愚直にconsole.log()を使ったり、コントラクトのコードを部分的にコメントアウトするのを繰り返して問題を特定しました。

vyper-debugというツールもありますが、まだ開発中とのことです。(pip installでエラーが出る)

(2) RLPデコーダーのデプロイ

RLPのデコードを行うビルトインのRLPList()関数は、内部的にはRLPデコードのロジックを実装したオンチェーンのコントラクトをCALLするようになっています。

プライベートのテスト環境などでRLPList()関数を試したい場合は、特定の手順を踏んであらかじめRLPデコード用のコントラクトをデプロイをする必要があります。

具体的な流れとしては、

  1. アカウントA(0x39ba083c30fCe59883775Fc729bBE1f9dE4DEe11)に、手順2のトランザクションを送信するためのgas代として10 ** 17 weiを送る
  2. RLPデコードコントラクトをデプロイするために、このアカウントAが署名したトランザクションを送信する

となります。

この手順によって、アカウントAがRLPデコーダーコントラクトをデプロイし、そのコントラクトアドレスは一意に定まります。

こちらの処理はhelperとしてこちらに実装していますので、truffleでRLPList()を使う場合は使ってみてください。

詰まったら。。。

Vyperはまだ情報が少なく、公式のドキュメントが一番の情報源になります。しかし、説明がかなり少ないのと、最新の仕様が反映されていないことが度々あります。

ドキュメントを見ても解決しない場合は、Gitterで質問したり、言語実装を読むのも良いです。Vyperは中間言語としてLisp-like language(LLL)にコンパイルされ、コンパイラがシンプルに作られていますので、意外と読みやすいです。例えば、型をキャストするconvert()関数は、キャスト可能な型の組み合わせが若干複雑なのですが、ドキュメントに書かれていなかったので、実装を確認しました。

また、他のVyper実装例も参考になります。

コントリビューション

今回、開発中に気づいたドキュメントの間違いや、機能の提案についてわずかながらコントリビューションをしました。

Vyperでは、機能の提案はVIP(Vyper Improvement Proposal)として提案します。下記は今回提案したVIPの一例です。

Vyperの使用感

Vyperを書くのは初めてでしたが、シンプルで覚えることが少なく、書きやすいな、と言う印象です。

Pythonに慣れている人にとっては、今のところVyperの文法はPythonのサブセットなので、新しい文法を覚えるというより、使えない文法を覚えていく感じになります。

現状だと、ドキュメントや開発ツールが乏しい感じはあるのですが、将来的に広く利用される可能性のある言語だと思いました。(Ethereum FoundationもGrants ProgramのWishlistにVyperを挙げています。)

先に述べたデザイン目標に基づいて新しい言語を作っていく感じもとてもワクワクします。

最後に

plasma-mvp-vyperへのpull request, issueは大歓迎です!また、記事の間違いやVyper開発のアドバイスなどありましたら、ぜひコメントなどで指摘いただければ幸いです。

また、LayerXでは仲間を募集しております。このような研究開発に加え、ブロックチェーンに関する様々なプロダクト作りに興味ある方、ぜひご応募ください。

エンジニア: https://www.wantedly.com/projects/175017

BizDev: https://www.wantedly.com/projects/225562

--

--