イエロー・ペーパーのEVM仕様を理解する
イエロー・ペーパー (新しいタブで開く)は、イーサリアムの正式な仕様書です。EIPプロセスによって修正された場合を除き、すべての仕組みの正確な説明が含まれています。数学の論文として書かれているため、プログラマーには馴染みのない用語が含まれている場合があります。この記事では、その読み方を学び、さらに他の関連する数学論文の読み方も学びます。
どのイエロー・ペーパーか?
イーサリアムの他のほぼすべてのものと同様に、イエロー・ペーパーも時間の経過とともに進化します。特定のバージョンを参照できるように、執筆時点での最新バージョン (新しいタブで開く)をアップロードしました。ここで使用するセクション、ページ、および方程式の番号は、そのバージョンを参照しています。このドキュメントを読む間、別のウィンドウで開いておくことをお勧めします。
なぜEVMなのか?
オリジナルのイエロー・ペーパーは、イーサリアムの開発が始まった直後に書かれました。そこには、ネットワークを保護するために元々使用されていた、プルーフ・オブ・ワーク (PoW) ベースのコンセンサス・メカニズムについて説明されています。しかし、イーサリアムは2022年9月にプルーフ・オブ・ワークを停止し、プルーフ・オブ・ステーク (PoS) ベースのコンセンサスを使用し始めました。このチュートリアルでは、イエロー・ペーパーの中でイーサリアム仮想マシン(EVM)を定義している部分に焦点を当てます。EVMは、プルーフ・オブ・ステークへの移行によって変更されていません(DIFFICULTYオペコードの戻り値を除く)。
9 実行モデル
このセクション(14〜16ページ)には、EVMの定義の大部分が含まれています。
_システム状態_という用語には、システムを実行するために知っておく必要があるすべての情報が含まれます。一般的なコンピューターでは、これはメモリやレジスタの内容などを意味します。
チューリングマシン (新しいタブで開く)は計算モデルです。基本的にはコンピューターの簡略版であり、通常のコンピューターと同じように計算を実行する能力があることが証明されています(コンピューターが計算できるものはすべてチューリングマシンでも計算でき、その逆も同様です)。このモデルにより、何が計算可能で何が計算不可能かに関するさまざまな定理を証明しやすくなります。
チューリング完全 (新しいタブで開く)という用語は、チューリングマシンと同じ計算を実行できるコンピューターを意味します。チューリングマシンは無限ループに陥る可能性がありますが、EVMはガス切れになるため無限ループに陥ることはなく、準チューリング完全(quasi-Turing-complete)にすぎません。
9.1 基本
このセクションでは、EVMの基本と、他の計算モデルとの比較について説明します。
スタックマシン (新しいタブで開く)は、中間データをレジスタではなくスタック (新しいタブで開く)に保存するコンピューターです。これは実装が容易であり、バグやセキュリティの脆弱性が発生する可能性がはるかに低いため、仮想マシンに推奨されるアーキテクチャです。スタック内のメモリは256ビットのワードに分割されています。これは、ケチャック・256のハッシュ化や楕円曲線計算など、イーサリアムのコアとなる暗号化操作に便利であるため選択されました。スタックの最大サイズは1024アイテム(1024 x 256ビット)です。オペコードが実行されるとき、通常はスタックからパラメーターを取得します。POP(スタックの一番上からアイテムを削除する)、DUP_N(スタック内のN番目のアイテムを複製する)など、スタック内の要素を再編成するための専用のオペコードがあります。
EVMには、実行中にデータを保存するために使用されるメモリと呼ばれる揮発性スペースもあります。このメモリは32バイトのワードで構成されています。すべてのメモリの場所はゼロに初期化されます。このYul (新しいタブで開く)コードを実行してメモリにワードを追加すると、ワード内の空きスペースをゼロで埋めることで32バイトのメモリが埋められます。つまり、0〜29の場所にゼロ、30に0x60、31に0xA7を持つ1つのワードが作成されます。
mstore(0, 0x60A7)
mstoreは、EVMがメモリと対話するために提供する3つのオペコードの1つであり、ワードをメモリにロードします。他の2つは、1バイトをメモリにロードするmstore8と、ワードをメモリからスタックに移動するmloadです。
EVMには、システム状態の一部として維持される、独立した不揮発性のストレージモデルもあります。このメモリは(スタック内のワードアドレス指定可能なバイト配列とは対照的に)ワード配列として構成されています。このストレージは、コントラクトが永続的なデータを保持する場所です。コントラクトは自身のストレージとのみ対話できます。ストレージはキーと値のマッピングで構成されています。
イエロー・ペーパーのこのセクションでは言及されていませんが、4番目のタイプのメモリがあることを知っておくことも役立ちます。コールデータは、トランザクションのdataパラメーターで渡された値を保存するために使用される、バイトアドレス指定可能な読み取り専用メモリです。EVMには、calldataを管理するための特定のオペコードがあります。calldatasizeはデータのサイズを返します。calldataloadはデータをスタックにロードします。calldatacopyはデータをメモリにコピーします。
標準的なフォン・ノイマン・アーキテクチャ (新しいタブで開く)では、コードとデータを同じメモリに保存します。EVMはセキュリティ上の理由からこの標準に従っていません。揮発性メモリを共有すると、プログラムコードを変更できる可能性があるためです。代わりに、コードはストレージに保存されます。
コードがメモリから実行されるケースは2つだけです。
- コントラクトが別のコントラクトを作成する場合(
CREATE(新しいタブで開く)またはCREATE2(新しいタブで開く)を使用)、コントラクトのコンストラクタのコードはメモリから取得されます。 - あらゆるコントラクトの作成中、コンストラクタコードが実行され、実際のコントラクトのコード(これもメモリから取得)とともに戻ります。
例外的実行(exceptional execution)という用語は、現在のコントラクトの実行を停止させる例外を意味します。
9.2 手数料の概要
このセクションでは、ガス代の計算方法について説明します。3つのコストがあります。
オペコードコスト
特定のオペコードの固有のコストです。この値を取得するには、付録H(29ページ、方程式(329)の下)でオペコードのコストグループを見つけ、方程式(326)でコストグループを見つけます。これによりコスト関数が得られます。ほとんどの場合、付録G(28ページ)のパラメーターが使用されます。
たとえば、オペコードCALLDATACOPY (新しいタブで開く)はグループ_Wcopy_のメンバーです。そのグループのオペコードコストは_Gverylow+Gcopy×⌈μs[2]÷32⌉_です。付録Gを見ると、両方の定数が3であることがわかります。これにより、_3+3×⌈μs[2]÷32⌉_が得られます。
まだ_⌈μs[2]÷32⌉という式を解読する必要があります。最も外側の部分である⌈ <value> ⌉は天井関数(ceiling function)であり、値が与えられたときに、その値より小さくない最小の整数を返す関数です。たとえば、⌈2.5⌉ = ⌈3⌉ = 3_です。内側の部分は_μs[2]÷32_です。3ページのセクション3(Conventions)を見ると、_μ_はマシン状態です。マシン状態は15ページのセクション9.4.1で定義されています。そのセクションによると、マシン状態パラメーターの1つはスタックを表す_s_です。これらを総合すると、_μs[2]はスタック内の場所#2であると思われます。オペコード (新しいタブで開く)を見ると、スタック内の場所#2はバイト単位のデータのサイズです。グループWcopyの他のオペコードであるCODECOPY (新しいタブで開く)とRETURNDATACOPY (新しいタブで開く)を見ると、これらも同じ場所にデータのサイズを持っています。したがって、⌈μs[2]÷32⌉_は、コピーされるデータを保存するために必要な32バイトのワード数です。すべてをまとめると、CALLDATACOPY (新しいタブで開く)の固有のコストは、3ガスに加えて、コピーされるデータのワードごとに3ガスとなります。
実行コスト
呼び出しているコードを実行するためのコストです。
CREATE(新しいタブで開く)およびCREATE2(新しいタブで開く)の場合、新しいコントラクトのコンストラクタ。CALL(新しいタブで開く)、CALLCODE(新しいタブで開く)、STATICCALL(新しいタブで開く)、またはDELEGATECALL(新しいタブで開く)の場合、呼び出すコントラクト。
メモリ拡張コスト
メモリを拡張するためのコストです(必要な場合)。
方程式326では、この値は_Cmem(μi')-Cmem(μi)_と記述されています。セクション9.4.1をもう一度見ると、_μi_はメモリ内のワード数であることがわかります。したがって、_μi_はオペコードの前のメモリ内のワード数であり、_μi'_はオペコードの後のメモリ内のワード数です。
関数_Cmemは方程式328で定義されています:Cmem(a) = Gmemory × a + ⌊a2 ÷ 512⌋。⌊x⌋は床関数(floor function)であり、値が与えられたときに、その値より大きくない最大の整数を返す関数です。たとえば、⌊2.5⌋ = ⌊2⌋ = 2_です。_a < √512_の場合、a2 < 512_となり、床関数の結果はゼロになります。したがって、最初の22ワード(704バイト)までは、コストは必要なメモリワード数に比例して直線的に上昇します。その点を超えると、⌊a2 ÷ 512⌋_は正になります。必要なメモリが十分に大きい場合、ガスコストはメモリ量の2乗に比例します。
注意:これらの要因は_固有の_ガスコストにのみ影響します。エンドユーザーが支払う必要のある金額を決定する手数料市場やバリデーターへのチップは考慮されていません。これは、EVMで特定の操作を実行するための純粋なコストにすぎません。
9.3 実行環境
実行環境は、ブロックチェーンの状態やEVMの一部ではない情報を含むタプル_I_です。
| パラメーター | データにアクセスするためのオペコード | データにアクセスするためのSolidityコード |
|---|---|---|
| Ia | ADDRESS (新しいタブで開く) | address(this) |
| Io | ORIGIN (新しいタブで開く) | tx.origin |
| Ip | GASPRICE (新しいタブで開く) | tx.gasprice |
| Id | CALLDATALOAD (新しいタブで開く)など | msg.data |
| Is | CALLER (新しいタブで開く) | msg.sender |
| Iv | CALLVALUE (新しいタブで開く) | msg.value |
| Ib | CODECOPY (新しいタブで開く) | address(this).code |
| IH | NUMBER (新しいタブで開く)やDIFFICULTY (新しいタブで開く)などのブロック・ヘッダーフィールド | block.number、block.difficultyなど |
| Ie | コントラクト間の呼び出し(コントラクトの作成を含む)のコールスタックの深さ | |
| Iw | EVMが状態を変更することを許可されているか、または静的に実行されているか |
セクション9の残りの部分を理解するには、他のいくつかのパラメーターが必要です。
| パラメーター | 定義されているセクション | 意味 |
|---|---|---|
| σ | 2(2ページ、方程式1) | ブロックチェーンの状態 |
| g | 9.3(14ページ) | 残りのガス |
| A | 6.1(9ページ) | 発生したサブ状態(トランザクション終了時に予定されている変更) |
| o | 9.3(14ページ) | 出力 - 内部トランザクション(あるコントラクトが別のコントラクトを呼び出す場合)およびビュー関数の呼び出し(情報を要求しているだけであり、トランザクションを待つ必要がない場合)の場合に返される結果 |
9.4 実行の概要
すべての準備が整ったので、いよいよEVMがどのように機能するかについて説明を始めることができます。
方程式146〜151は、EVMを実行するための初期条件を示しています。
| 記号 | 初期値 | 意味 |
|---|---|---|
| μg | g | 残りのガス |
| μpc | 0 | プログラムカウンター、次に実行する命令のアドレス |
| μm | (0, 0, ...) | メモリ、すべてゼロに初期化される |
| μi | 0 | 使用された最も高いメモリの場所 |
| μs | () | スタック、最初は空 |
| μo | ∅ | 出力、戻り値データあり(RETURN (新しいタブで開く)またはREVERT (新しいタブで開く))または戻り値データなし(STOP (新しいタブで開く)またはSELFDESTRUCT (新しいタブで開く))で停止するまで、および停止しない限り空集合。 |
方程式152は、実行中の各時点で4つの可能な条件があり、それらをどう処理するかを示しています。
Z(σ,μ,A,I)。Zは、操作が無効な状態遷移を作成するかどうかをテストする関数を表します(例外的停止を参照)。Trueと評価された場合、変更が実装されていないため、新しい状態は古い状態と同じになります(ガスが消費されることを除く)。- 実行されているオペコードが
REVERT(新しいタブで開く)の場合、新しい状態は古い状態と同じになり、一部のガスが失われます。 RETURN(新しいタブで開く)で示されるように、一連の操作が終了した場合、状態は新しい状態に更新されます。- 終了条件1〜3のいずれにも該当しない場合は、実行を続行します。
9.4.1 マシン状態
このセクションでは、マシン状態についてさらに詳しく説明します。ここでは、_w_が現在のオペコードであることを指定しています。_μpcがコードの長さである||Ib||_より小さい場合、そのバイト(Ib[μpc])がオペコードになります。それ以外の場合、オペコードはSTOP (新しいタブで開く)として定義されます。
これはスタックマシン (新しいタブで開く)であるため、各オペコードによってポップされた(δ)アイテムとプッシュされた(α)アイテムの数を追跡する必要があります。
9.4.2 例外的停止
このセクションでは、異常終了が発生するタイミングを指定する_Z_関数を定義します。これはブール (新しいタブで開く)関数であるため、論理和には_∨_ (新しいタブで開く)を使用し、論理積には_∧_ (新しいタブで開く)を使用します。
これらの条件のいずれかが真である場合、例外的停止が発生します。
-
μg < C(σ,μ,A,I) セクション9.2で見たように、_C_はガスコストを指定する関数です。次のオペコードをカバーするのに十分なガスが残っていません。
-
δw=∅ オペコードに対してポップされるアイテムの数が未定義の場合、オペコード自体が未定義です。
-
|| μs || < δw スタックアンダーフロー。現在のオペコードに対してスタック内のアイテムが不足しています。
-
w = JUMP ∧ μs[0]∉D(Ib) オペコードが
JUMP(新しいタブで開く)であり、アドレスがJUMPDEST(新しいタブで開く)ではありません。ジャンプは、宛先がJUMPDEST(新しいタブで開く)である場合に_のみ_有効です。 -
w = JUMPI ∧ μs[1]≠0 ∧ μs[0] ∉ D(Ib) オペコードが
JUMPI(新しいタブで開く)であり、条件が真(ゼロではない)であるためジャンプが発生するはずですが、アドレスがJUMPDEST(新しいタブで開く)ではありません。ジャンプは、宛先がJUMPDEST(新しいタブで開く)である場合に_のみ_有効です。 -
w = RETURNDATACOPY ∧ μs[1]+μs[2]>|| μo || オペコードは
RETURNDATACOPY(新しいタブで開く)です。このオペコードでは、スタック要素_μs[1]_は戻り値データバッファから読み取るオフセットであり、スタック要素_μs[2]_はデータの長さです。この条件は、戻り値データバッファの末尾を超えて読み取ろうとしたときに発生します。コールデータやコード自体には同様の条件がないことに注意してください。これらのバッファの末尾を超えて読み取ろうとすると、単にゼロが返されます。 -
|| μs || - δw + αw > 1024
スタックオーバーフロー。オペコードを実行した結果、スタックが1024アイテムを超える場合は中止します。
-
¬Iw ∧ W(w,μ) 静的に実行しているか(¬は否定 (新しいタブで開く)であり、ブロックチェーンの状態を変更することが許可されている場合、_Iw_は真になります)。もしそうであり、状態を変更する操作を試みている場合、それは実行できません。
関数_W(w,μ)_は後で方程式159で定義されます。これらの条件のいずれかが真である場合、_W(w,μ)_は真になります。
-
w ∈ {CREATE, CREATE2, SSTORE, SELFDESTRUCT} これらのオペコードは、新しいコントラクトの作成、値の保存、または現在のコントラクトの自己破壊のいずれかによって状態を変更します。
-
LOG0≤w ∧ w≤LOG4 静的に呼び出された場合、ログエントリを発行することはできません。 ログオペコードはすべて、
LOG0(A0) (新しいタブで開く)からLOG4(A4) (新しいタブで開く)の範囲にあります。 ログオペコードの後の数字は、ログエントリに含まれるトピックの数を指定します。 -
w=CALL ∧ μs[2]≠0 静的である場合でも別のコントラクトを呼び出すことはできますが、その場合、ETHを送金することはできません。
-
-
w = SSTORE ∧ μg ≤ Gcallstipend Gcallstipend(付録Gで2300と定義)を超えるガスがない限り、
SSTORE(新しいタブで開く)を実行することはできません。
9.4.3 ジャンプ先の有効性
ここでは、JUMPDEST (新しいタブで開く)オペコードとは何かを正式に定義します。バイト値0x5Bを探すだけでは不十分です。なぜなら、それがPUSHの内部にある可能性があるためです(したがって、オペコードではなくデータになります)。
方程式(162)では、関数_N(i,w)_を定義します。最初のパラメーター_i_はオペコードの場所です。2番目の_w_はオペコード自体です。_w∈[PUSH1, PUSH32]_の場合、オペコードがPUSHであることを意味します(角括弧は端点を含む範囲を定義します)。その場合、次のオペコードは_i+2+(w−PUSH1)_にあります。PUSH1 (新しいタブで開く)の場合は2バイト(PUSH自体と1バイトの値)進める必要があり、PUSH2 (新しいタブで開く)の場合は2バイトの値であるため3バイト進める必要があります。他のすべてのEVMオペコードは1バイト長であるため、他のすべてのケースでは_N(i,w)=i+1_となります。
この関数は方程式(161)で使用され、オペコードの場所_i_から始まる、コード_c_内のすべての有効なジャンプ先の集合 (新しいタブで開く)である_DJ(c,i)_を定義します。この関数は再帰的に定義されます。_i≥||c||_の場合、コードの末尾またはそれ以降にいることを意味します。これ以上ジャンプ先を見つけることはないため、単に空集合を返します。
他のすべてのケースでは、次のオペコードに進み、そこから始まる集合を取得することで、コードの残りの部分を調べます。_c[i]_は現在のオペコードであるため、_N(i,c[i])_は次のオペコードの場所です。したがって、_DJ(c,N(i,c[i]))_は、次のオペコードから始まる有効なジャンプ先の集合です。現在のオペコードがJUMPDESTでない場合は、単にその集合を返します。JUMPDESTである場合は、それを結果の集合に含めて返します。
9.4.4 正常な停止
停止関数_H_は、3種類の値を返すことができます。
- 停止オペコードにいない場合は、空集合である_∅_を返します。慣例により、この値はブール値の偽(false)として解釈されます。
- 出力を生成しない停止オペコード(
STOP(新しいタブで開く)またはSELFDESTRUCT(新しいタブで開く))がある場合は、戻り値としてサイズゼロのバイトシーケンスを返します。これは空集合とは大きく異なることに注意してください。この値は、EVMが実際に停止したことを意味しますが、読み取る戻り値データがないだけです。 - 出力を生成する停止オペコード(
RETURN(新しいタブで開く)またはREVERT(新しいタブで開く))がある場合は、そのオペコードで指定されたバイトシーケンスを返します。このシーケンスはメモリから取得され、スタックの一番上の値(μs[0])が最初のバイトであり、その後の値(μs[1])が長さになります。
H.2 命令セット
EVMの最後のサブセクションである9.5に進む前に、命令自体を見てみましょう。これらは30ページから始まる付録H.2で定義されています。特定のオペコードで変更されると指定されていないものはすべて、同じままであると想定されます。変更される変数は、<something>′として指定されます。
たとえば、ADD (新しいタブで開く)オペコードを見てみましょう。
| 値 | ニーモニック | δ | α | 説明 |
|---|---|---|---|---|
| 0x01 | ADD | 2 | 1 | 加算操作。 |
| μ′s[0] ≡ μs[0] + μs[1] |
_δ_はスタックからポップする値の数です。この場合、上位2つの値を加算するため、2になります。
_α_はプッシュバックする値の数です。この場合、合計である1になります。
したがって、新しいスタックの先頭(μ′s[0])は、古いスタックの先頭(μs[0])とその下の古い値(μs[1])の合計になります。
すべてのオペコードを「退屈なリスト」で確認する代わりに、この記事では新しい概念を導入するオペコードのみを説明します。
| 値 | ニーモニック | δ | α | 説明 |
|---|---|---|---|---|
| 0x20 | KECCAK256 | 2 | 1 | ケチャック・256ハッシュを計算します。 |
| μ′s[0] ≡ KEC(μm[μs[0] . . . (μs[0] + μs[1] − 1)]) | ||||
| μ′i ≡ M(μi,μs[0],μs[1]) |
これはメモリにアクセスする最初のオペコードです(この場合は読み取り専用)。ただし、メモリの現在の制限を超えて拡張される可能性があるため、_μi_を更新する必要があります。これには、30ページの方程式330で定義されている_M_関数を使用します。
| 値 | ニーモニック | δ | α | 説明 |
|---|---|---|---|---|
| 0x31 | BALANCE | 1 | 1 | 指定されたアカウントの残高を取得します。 |
| ... |
残高を調べる必要があるアドレスは_μs[0] mod 2160_です。スタックの先頭はアドレスですが、アドレスは160ビットしかないため、2160を法として(modulo) (新しいタブで開く)値を計算します。
_σ[μs[0] mod 2160] ≠ ∅_の場合、このアドレスに関する情報があることを意味します。その場合、_σ[μs[0] mod 2160]b_はそのアドレスの残高です。_σ[μs[0] mod 2160] = ∅_の場合、このアドレスは初期化されておらず、残高はゼロであることを意味します。アカウント情報フィールドのリストは、4ページのセクション4.1で確認できます。
2番目の方程式_A'a ≡ Aa ∪ {μs[0] mod 2160}_は、ウォームストレージ(最近アクセスされたためキャッシュされている可能性が高いストレージ)とコールドストレージ(アクセスされておらず、取得にコストがかかる低速なストレージにある可能性が高いストレージ)へのアクセスコストの違いに関連しています。_Aa_は、9ページのセクション6.1で定義されているように、トランザクションによって以前にアクセスされたアドレスのリストであり、したがってアクセスが安価になるはずです。このテーマの詳細については、EIP-2929 (新しいタブで開く)を参照してください。
| 値 | ニーモニック | δ | α | 説明 |
|---|---|---|---|---|
| 0x8F | DUP16 | 16 | 17 | 16番目のスタックアイテムを複製します。 |
| μ′s[0] ≡ μs[15] |
スタックアイテムを使用するには、それをポップする必要があることに注意してください。つまり、その上にあるすべてのスタックアイテムもポップする必要があります。DUP<n> (新しいタブで開く)およびSWAP<n> (新しいタブで開く)の場合、これは最大16個の値をポップしてからプッシュする必要があることを意味します。
9.5 実行サイクル
すべてのパーツが揃ったので、EVMの実行サイクルがどのように文書化されているかをようやく理解することができます。
方程式(164)は、以下の状態が与えられた場合:
- σ(グローバルなブロックチェーンの状態)
- μ(EVMの状態)
- A(サブ状態、トランザクション終了時に発生する変更)
- I(実行環境)
新しい状態は_(σ', μ', A', I')_になることを示しています。
方程式(165)〜(167)は、スタックとオペコードによるスタックの変更(μs)を定義しています。方程式(168)はガスの変更(μg)です。方程式(169)はプログラムカウンターの変更(μpc)です。最後に、方程式(170)〜(173)は、オペコードによって明示的に変更されない限り、他のパラメーターが同じままであることを指定しています。
これでEVMは完全に定義されました。
まとめ
数学的表記は正確であり、イエロー・ペーパーでイーサリアムのあらゆる詳細を指定することを可能にしました。しかし、いくつかの欠点もあります。
- 人間にしか理解できないため、コンプライアンステスト (新しいタブで開く)を手動で記述する必要があります。
- プログラマーはコンピューターコードを理解します。 しかし、数学的表記を理解できるとは限りません。
おそらくこれらの理由から、新しいコンセンサス・レイヤーの仕様 (新しいタブで開く)はPythonで書かれています。Pythonで書かれた実行レイヤーの仕様 (新しいタブで開く)もありますが、完全ではありません。イエロー・ペーパー全体がPythonや類似の言語に翻訳されない限り、イエロー・ペーパーは引き続き使用されるため、それを読めるようにしておくことは役立ちます。