アルゴリズムとかオーダーとか

仕事で勉強したことなどをまとめてます

EIP-615 - サブルーチンと静的ジャンプをEVMに導入する

今回は、次期updateの提案、EIP-1679 Hardfork Meta: Istanbulで導入候補に上がっている、EIP-615について解説します。
eips.ethereum.org

概要

現状のEVMは静的解析や形式証明が困難であり、またgasCostのさらなる削減やハイパフォーマンスな実行などが困難です。EIP-615は現状の動的ジャンプを非推奨とし、代わりに静的なジャンプを導入することでEVMの構造解析をより簡易に行え、また事前にある程度のgasCostの計測などを可能とするための提案です。
EIP-615を解説するにあたって、以下の4つのカテゴリに分けて解説していきます。
- Wasmへの対応
- 静的ジャンプの導入
- サブルーチンの導入
- BEGINDATAについて

ちなみに、このEIP-615は2016年に提案が作成され、2017年11月ごろにdraftとなっている結構古いEIPです。が、変更内容が多岐に渡るため現在まで放置議論されていました。

Wasmへの対応

EIP-615では10個のOPCODEが追加されます。(正確にはRETURNはすでにあるので、現状で考えると9個かな?)。これらのOPCODEはeWasmへの移行を考慮して、Wasmバイトコードと対応するように設計されています。対応表は以下の通りです。(「BEGINSUB」はなぜか対応表に含まれていません。下記の対応表の9個と「BEGINSUB」を合わせて10個のOPCODEが提案されています。)

EIP-615 Wasm x86 ARM
JUMPTO br JMP B
JUMPIF br_if JE BNZ
JUMPV br_table JMP TBH
JUMPSUB call CALL BL
JUMPSUBV call_indirect CALL BLX
RETURN return RET RET
GETLOCAL local.get PUSH POP
PUTLOCAL local.put PUSH POP
BEGINDATA tables .DATA .DATA

(*https://eips.ethereum.org/EIPS/eip-615より)

また、それぞれのOPCODEについても出来るだけeWasmと同じ意味論を持つように設計されています。
そのため、Eth2.0でeWasmへ乗り換える前に、一旦EIP-615をEVM1.5として実装することで、eWasmへの対応がよりスムーズに行えると考えられています。

静的ジャンプの導入

静的ジャンプとしてのOPCODE 「JUMPTO jump_target」と、「JUMPIF jump_target」を追加します。また、switch構文のための「JUMPV n jump_target」も追加します。

これらのOPCODEの説明をする前に、既存のEVMに導入されている動的ジャンプである、「JUMP」と「JUMPI」の仕組みと問題点について簡単に説明します。

JUMPとJUMPIの動作について

既存の動的ジャンプのOPCODEである、「JUMP」と「JUMPI」は下図に示すようにスタックの先頭から値をpopし、その値が指し示す位置にProgram Counter(以降、PCと呼ぶ)を移動します。(図中では0x10の位置にPCが移動しています。)ジャンプ先は必ず「JUMPDEST」でなければいけません。ジャンプ先のOPCODEが「JUMPDEST」ではない場合はEVMは異常終了します。

動的ジャンプの説明

動的ジャンプの問題点

動的ジャンプの問題点は、スタックの先頭の値が実行時依存であることに起因します。そのためコントラクトのバイトコードを静的に解析しようとした場合、「JUMP」と「JUMPI」の飛び先は全ての「JUMPDEST」が候補となります。つまり、下図に示すように(JUMP+JUMPI) * JUMPDESTの組み合わせの数だけ動的ジャンプの候補が存在することになり、コードの安全性を解析することが非常に困難となります。

動的ジャンプの時の組み合わせ可能性

静的ジャンプによる解決

新たに提案された、「JUMPTO jump_target」と、「JUMPIF jump_target」では、ジャンプ先のアドレスをスタックの先頭から取得するのではなく、命令コードとして記述します。つまり「PUSH01」 OPCODEのように、多倍長命令コードであり「JUMPTO」のすぐ後の命令コードが飛び先のアドレスデータになります。
これにより、静的解析時にはコードジャンプの候補が「JUMPTO」および、「JUMPIF」の数だけに限定されます。つまり線型的な検索で全てのジャンプの可能性を網羅することが可能になります。

JUMPVについて

JUMPVはいわゆるジャンプテーブルのための命令コードになります。これは一般的にはswitch構文を実現するための命令コードとして利用されるものです。詳しくは以下のサイトを参照ください。

natsutan.hatenablog.com
www.soum.co.jp

サブルーチンの導入

EIP-615ではさらに「JUMPSUB jump_target」と、「JUMPSUBV jump_target」のOPCODEでサブルーチンの概念を導入することを提案しています。「JUMPSUB」および、「JUMPSUBV」の飛び先は「BEGINSUB」になります。また「BEGINSUB」はサブルーチンの開始コードでもあります。
さらに、サブルーチンの概念として必要なスタックフレームとローカル変数の概念を導入し、ローカル変数を操作するためのOPCODE、「GETLOCAL n」と、「PUTLOCAL n」も追加します。(提案の説明文の「GETLOCAL」と「PUTLOCAL」の説明が逆な気もする。。。)

サブルーチンの一般的な概念はwikipediaを参照ください。
ja.wikipedia.org

サブルーチンの導入による恩恵

サブルーチンの導入により、関数の呼び出しと制御構文がバイトコードレベルで区別することが可能になります。これにより静的解析において呼び出される関数パスを明確に構築できます。

JUMPSUBVについて

「JUMPSUB」はサブルーチンへのJUMP命令です。そして「JUMPSUBV」はサブルーチンへのテーブルジャンプ命令になります。

ここで1つ疑問が出てきます。EVMではpublicな関数はABI定義により、ハッシュ関数keccak256で生成した4byteのfunctionIdが付与されます。そのため、functionIdに対して連番が付与されることはありません。つまり、「JUMPSUBV」の使い所はどこなのか?と言う疑問です。

functionIdのABI仕様については過去の記事を参照ください。
y-nakajo.hatenablog.com

JUMPSUBVはprivateとinternal関数で利用するために追加されたと思います。(提案文では特に明確な目的は記載されていません。)functionIdが強制されるのは外部に公開する必要があるpublicとexternal関数のみです。つまり、privateとinternal関数についてはそもそもfunctionIdを振る必要はありません。実際、現在Solidityでコントラクトをコンパイルするとprivateとinnternal関数はただのJUMP命令で表現されます。

これらの内部関数へのジャンプ定義を「JUMPSUBV」で定義することで、内部関数への呼び出し部分を1箇所にまとめることができます。これは静的解析を行う上で、またEVMでバイトコードを実行するときに関数テーブルを構築するためにも非常に有効となります。

BEGINDATAについて

このOPCODEは静的ジャンプとは直接関係ありませんが、なぜかこのEIP-615で同時に提案されています。

「BEGINDATA」はこのOPCODEが出現したPC以降、バイトコードの末尾までのデータが全て命令コードではなくただの意味のないデータであることを示すためのOPCODEです。
現状、Solidityでコンパイルを行うとバイトコードの末尾にSwarmにアップロードした時の識別IDが付与されます。(Swarmにアップロードしているわkではありません。)これらの識別コードは基本的にSTOPの後ろに付与されているため、現状のEVMでも到達不能コードになるように配置されています。
ただし、これはあくまでバイトコードの配置を工夫しているだけであり、何らかのコンパイラーのミスで末尾のこのデータにPCが移動してしまった場合は、EVMはそれをOPCODEであるとして実行してしまいます。

「BEGINDATA」は上記のような状況に陥ったときに、EVMにそのPCの位置のデータは(EVMにとって)無意味なただのデータであると言うことを知らせます。そのため、EVMは「BEGINDATA」より後ろのPCを指した場合は異常終了することができるようになります。

所感

僕もTransactionエラーが発生したときに、実行したバイトコードを解析してSolidityコード上でのエラー個所を表示してくれるツールや、コントラクトの単体テストのカバレッジを計測するツールを作成しているので、EIP-615は早く導入されてほしいなぁと思っています。

ただし、ジャンプ命令の変更、とくに既存のOPCODEである「JUMP」と「JUMPI」を非推奨にするため導入には細心の注意が必要であり、また導入にあたってはEVMのバージョンを区別するための方法も(これは現状ではEIP-1702を採用する予定となっている)一緒に導入する必要があります。

いずれにしても、Eth2.0で最終的にeWasmへ以降することを考えるとEIP-615を導入したEVM1.5を早めにローンチし、eWasmへの移行のための段階的なテストをしていく必要があると思います。