メインコンテンツへスキップ

イエローペーパーにおけるEVM仕様の理解

evm
中級
qbzzt
2022年5月15日
30 分の読書

イエローペーパー (opens in a new tab)は、イーサリアムの正式な仕様です。 EIPプロセスによって修正される場合を除き、すべての動作方式が正確に記されています。 数学の論文形式で書かれており、プログラマーには馴染みのない専門用語が含まれています。 この論文を通して、論文の読み方を学び、他の関連する数学論文も読めるようにしましょう。

解説するイエローペーパーについて

イーサリアムに関する他のものと同じように、イエローペーパーも時間の経過とともに進化しています。 特定のバージョンを参照できるように、執筆時の現行バージョンをアップロードしました。 このドキュメントで使用するセクション番号、ページ番号、数式番号は、そのバージョンを参照しています。 このドキュメントを読む際は、別のウィンドウで開いておくことをお勧めします。

なぜEVMなのか?

オリジナルのイエローペーパーは、イーサリアムの開発が始まった直後に書かれました。 そこには、ネットワークのセキュリティを確保するために当初使用されていた、プルーフ・オブ・ワークベースの独自のコンセンサスメカニズムが記述されています。 しかし、イーサリアムは2022年9月にプルーフ・オブ・ワークを停止し、プルーフ・オブ・ステークベースのコンセンサスを使用し始めました。 このチュートリアルでは、イーサリアム仮想マシンを定義するイエローペーパーの部分に焦点を当てます。 EVMは、プルーフ・オブ・ステークへの移行による影響を受けませんでした(DIFFICULTYオペコードの戻り値を除く)。

9 実行モデル

このセクション(12~14ページ)には、EVMの定義のほとんどが含まれています。

_システム状態_という用語には、システムの実行に必要なすべての情報が含まれます。 典型的なコンピュータでは、これはメモリやレジスタの内容などを意味します。

チューリングマシン (opens in a new tab)は計算モデルです。 本質的に、これはコンピュータの簡易版であり、通常のコンピュータと同じ計算を実行する能力があることが証明されています(コンピュータが計算できるものはすべてチューリングマシンでも計算でき、その逆も同様です)。 このモデルは、計算可能なものと計算不可能なものに関するさまざまな定理の証明を容易にします。

チューリング完全 (opens in a new tab)という用語は、チューリングマシンと同じ計算を実行できるコンピュータを意味します。 チューリングマシンは無限ループに陥ることがありますが、EVMはガスがなくなるため無限ループに陥ることができず、準チューリング完全であるにすぎません。

9.1 基本

このセクションでは、EVMの基本と、他の計算モデルとの比較について説明します。

スタックマシン (opens in a new tab)とは、中間データをレジスタではなくスタック (opens in a new tab)に格納するコンピュータです。 これは実装が容易で、バグやセキュリティの脆弱性がはるかに少なくなるため、仮想マシンで好まれるアーキテクチャです。 スタック内のメモリは、256ビットのワードに分割されます。 これは、Keccak-256のハッシュ化や楕円曲線計算など、イーサリアムの中核となる暗号操作に便利であるため、選択されました。 スタックの最大サイズは1024アイテム(1024 x 256ビット)です。 オペコードが実行されるとき、通常はスタックからパラメータを取得します。 POP(スタックの先頭からアイテムを削除)、DUP_N(スタックのN番目のアイテムを複製)など、スタック内の要素を再編成するための専用オペコードがあります。

EVMには、実行中にデータを保存するために使用されるメモリと呼ばれる揮発性スペースもあります。 このメモリは、32バイトのワードで構成されます。 すべてのメモリロケーションはゼロで初期化されます。 このYul (opens in a new tab)コードを実行してメモリにワードを追加すると、ワード内の空きスペースがゼロでパディングされて32バイトのメモリが埋められます。つまり、ロケーション0-29はゼロ、30は0x60、31は0xA7である1ワードが作成されます。

1mstore(0, 0x60A7)

mstoreは、EVMがメモリと対話するために提供する3つのオペコードの1つで、ワードをメモリにロードします。 他の2つは、1バイトをメモリにロードするmstore8と、メモリからスタックにワードを移動するmloadです。

EVMには、システム状態の一部として維持される、独立した不揮発性のストレージモデルもあります。このメモリは、(スタック内のワードアドレス指定可能なバイト配列とは対照的に)ワード配列として構成されます。 このストレージは、コントラクトが永続データを保持する場所であり、コントラクトは自身のストレージとのみ対話できます。 ストレージは、キーと値のマッピングで構成されます。

イエローペーパーのこのセクションでは言及されていませんが、4番目のタイプのメモリがあることを知っておくと便利です。 Calldataは、トランザクションのdataパラメータで渡された値を格納するために使用される、バイトアドレス指定可能な読み取り専用メモリです。 EVMには、calldataを管理するための特定のオペコードがあります。 calldatasizeは、データのサイズを返します。 calldataloadは、データをスタックにロードします。 calldatacopyは、データをメモリにコピーします。

標準のフォン・ノイマン・アーキテクチャ (opens in a new tab)では、コードとデータが同じメモリに格納されます。 EVMはセキュリティ上の理由からこの標準に従いません。揮発性メモリを共有すると、プログラムコードが変更される可能性があるためです。 代わりに、コードはストレージに保存されます。

コードがメモリから実行されるのは、次の2つのケースだけです。

  • コントラクトが別のコントラクトを作成する場合(CREATE (opens in a new tab)またはCREATE2 (opens in a new tab)を使用)、コントラクトコンストラクタのコードはメモリから取得されます。
  • コントラクトの作成中、コンストラクタコードが実行され、実際のコントラクトのコードとともにメモリから返されます。

例外実行という用語は、現在のコントラクトの実行を停止させる例外を意味します。

9.2 手数料の概要

このセクションでは、ガス代の計算方法について説明します。 次の3つのコストがあります。

オペコードのコスト

特定のオペコードで発生する固有のコストです。 この値を取得するには、付録H(28ページ、式(327)の下)でオペコードのコストグループを見つけ、式(324)でコストグループを見つけます。 これによりコスト関数が得られますが、ほとんどの場合、付録G(27ページ)のパラメータを使用します。

例えば、オペコードCALLDATACOPY (opens in a new tab)は、グループ_Wcopy_のメンバーです。 このグループにおけるオペコードのコストは、_Gverylow+Gcopy×⌈μs[2]÷32⌉_となります。 付録Gを見ると、両方の定数が3であることがわかり、この式は_3+3×⌈μs[2]÷32⌉_です。

次に、式_⌈μs[2]÷32⌉を解読する必要があります。 外側の部分⌈ <value> ⌉は天井関数であり、与えられた値に対して、その値以上である最小の整数を返す関数です。 例えば、⌈2.5⌉ = ⌈3⌉ = 3_となります。 内側の部分は、_μs[2]÷32_です。 3ページ目のセクション3(表記法)を見ると、_μ_はマシンの状態です。 マシンの状態は、13ページのセクション9.4.1で定義されています。 そのセクションによると、マシンの状態パラメータの1つはスタックの_s_です。 すべてをまとめると、_μs[2]はスタック内のロケーション#2であるようです。 このオペコード (opens in a new tab)を見ると、スタック内のロケーション#2はデータのサイズ(バイト単位)です。 グループWcopyの他のオペコード、CODECOPY (opens in a new tab)およびRETURNDATACOPY (opens in a new tab)を見ると、それらも同じロケーションにデータサイズを持っています。 したがって、⌈μs[2]÷32⌉_は、コピーされるデータを保存するために必要な32バイトのワード数です。 すべてをまとめると、CALLDATACOPY (opens in a new tab)の固有コストは3ガスに加えて、コピーされるデータのワードごとに3ガスです。

実行コスト

呼び出すコードの実行コスト。

メモリ拡張コスト

(必要な場合の)メモリ拡張のコスト。

式324では、この値は_Cmemi')-Cmemi)_として記述されています。 セクション9.4.1をもう一度見ると、_μi_はメモリ内のワード数であることがわかります。 したがって、_μi_はオペコード実行前のメモリ内のワード数であり、_μi'_はオペコード実行後のメモリ内のワード数です。

関数_Cmem_は、式326で次のように定義されます:Cmem(a) = Gmemory × a + ⌊a2 ÷ 512⌋⌊x⌋は床関数で、与えられた値に対して、その値以下である最大の整数を返す関数です。 例えば、⌊2.5⌋ = ⌊2⌋ = 2。_a < √512_の場合、a2 < 512_となり、床関数の結果はゼロになります。 そのため、最初の22ワード(704バイト)については、必要なメモリワード数に応じてコストが線形的に増加します。 その点を超えると、⌊a2 ÷ 512⌋_は正になります。 必要なメモリが十分に大きい場合、ガスコストはメモリ量の2乗に比例します。

:これらの要素は_固有の_ガス代にのみ影響し、エンドユーザーが支払う必要のある金額を決定する手数料マーケットやバリデータへのチップは考慮されていません。これは、EVMで特定の操作を実行するための生のコストにすぎません。

ガスについての詳細

9.3 実行環境

実行環境は、ブロックチェーンの状態やEVMの一部ではない情報を含むタプル_I_です。

パラメータデータにアクセスするためのオペコードデータにアクセスするためのSolidityのコード
IaADDRESS (opens in a new tab)address(this)
IoORIGIN (opens in a new tab)tx.origin
IpGASPRICE (opens in a new tab)tx.gasprice
IdCALLDATALOAD (opens in a new tab)などmsg.data
IsCALLER (opens in a new tab)msg.sender
IvCALLVALUE (opens in a new tab)msg.value
IbCODECOPY (opens in a new tab)address(this).code
IHブロックヘッダーフィールド(NUMBER (opens in a new tab)DIFFICULTY (opens in a new tab)など)block.numberblock.difficultyなど
Ieコントラクト間のコールのコールスタックの深さ(コントラクトの作成を含む)
IwEVMが状態を変更できるか、静的に実行されているか

セクション9の残りを理解するには、他にいくつかのパラメータが必要です。

パラメータ定義されているセクション意味
σ2 (p. 2, equation 1)ブロックチェーンの状態
g9.3 (p. 13)残りのガス
A6.1 (p. 8)発生したサブ状態(トランザクション終了時に予定されている変更)
o9.3 (p. 13)出力 - 内部トランザクション(あるコントラクトが別のコントラクトを呼び出す場合)やビュー関数の呼び出し(情報を要求するだけでトランザクションを待つ必要がない場合)で返される結果

9.4 実行の概要

すべての準備が整ったので、いよいよEVMの仕組みについて取り組んでいきます。

式137~142は、EVMを実行するための初期条件を示しています。

記号初期値意味
μgg残りのガス
μpc0プログラムカウンタ、実行する次の命令のアドレス
μm(0, 0, ...)メモリ、すべてゼロに初期化
μi0使用される最上位のメモリ位置
μs()スタック、最初は空
μo出力、戻りデータで停止する(RETURN (opens in a new tab)またはREVERT (opens in a new tab))か、戻りデータなしで停止する(STOP (opens in a new tab)またはSELFDESTRUCT (opens in a new tab))まで空集合

式143は、実行中の各時点で4つの可能な条件があり、それらをどう処理するかを示しています。

  1. Z(σ,μ,A,I) Zは、操作が無効な状態遷移を引き起こすかどうかをテストする関数を表します(例外的な停止を参照)。 Trueと評価された場合、変更が実装されていないため、新しい状態は古い状態と同一です(ガスがバーンされる場合を除く)。
  2. 実行中のオペコードがREVERT (opens in a new tab)の場合、新しい状態は古い状態と同じで、一部のガスが失われます。
  3. RETURN (opens in a new tab)で示されるように、操作のシーケンスが終了した場合、状態は新しい状態に更新されます。
  4. 終了条件1~3のいずれでもない場合は、実行を続けます。

9.4.1 マシンの状態

このセクションでは、マシンの状態について詳しく説明します。 これは、_w_が現在のオペコードであることを指定します。 _μpcがコードの長さである||Ib||_より小さい場合、そのバイト(Ibpc])がオペコードです。 それ以外の場合、オペコードはSTOP (opens in a new tab)として定義されます。

これはスタックマシン (opens in a new tab)であるため、各オペコードによってポップアウトされたアイテムの数(δ)とプッシュインされたアイテムの数(α)を追跡する必要があります。

9.4.2 例外的な停止

このセクションでは、異常終了が発生するタイミングを指定する_Z_関数を定義します。 これはブール (opens in a new tab)関数なので、論理和には_∨_ (opens in a new tab)を、論理積には_∧_ (opens in a new tab)を使用します。

次のいずれかの条件が真になる場合、例外による停止が発生します。

  • μg < C(σ,μ,A,I) セクション9.2で見たように、_C_はガス代を指定する関数です。 次のオペコードをカバーするのに十分なガスが残っていません。

  • δw=∅ オペコードに対してポップされるアイテムの数が未定義の場合、オペコード自体も未定義です。

  • || μs || < δw スタックアンダーフロー。現在のオペコードに対してスタック内に十分なアイテムがありません。

  • w = JUMP ∧ μs[0]∉D(Ib) オペコードがJUMP (opens in a new tab)で、アドレスがJUMPDEST (opens in a new tab)ではありません。 ジャンプは、ジャンプ先がJUMPDEST (opens in a new tab)のときのみ有効です。

  • w = JUMPI ∧ μs[1]≠0 ∧ μs[0] ∉ D(Ib) オペコードがJUMPI (opens in a new tab)で、条件が真(非ゼロ)であるためジャンプが発生しますが、アドレスがJUMPDEST (opens in a new tab)ではありません。 ジャンプは、ジャンプ先がJUMPDEST (opens in a new tab)のときのみ有効です。

  • w = RETURNDATACOPY ∧ μs[1]+μs[2]>|| μo || オペコードはRETURNDATACOPY (opens in a new tab)です。 このオペコードでは、スタック要素_μs[1]_は戻りデータバッファから読み取るオフセットで、スタック要素_μs[2]_はデータの長さです。 この条件は、戻りデータバッファの末尾を超えて読み取ろうとしたときに発生します。 コールデータやコード自体には同様の条件がないことに注意してください。 これらのバッファの末尾を超えて読み取ろうとすると、ゼロが返されるだけです。

  • || μs || - δw + αw > 1024

    スタックオーバーフロー。 オペコードの実行で1024を超えるアイテムのスタックが生成された場合は、中断されます。

  • ¬Iw ∧ W(w,μ) 静的に実行していますか(¬は否定 (opens in a new tab)で、ブロックチェーンの状態を変更できる場合に_Iw_は真になります)? その場合、状態変更操作を試行しようとしてもできません。

    関数_W(w,μ)_は、後の式150で定義されています。 次のいずれかの条件が真の場合、_W(w,μ)_は真になります。

    • w ∈ {CREATE, CREATE2, SSTORE, SELFDESTRUCT} これらのオペコードは、新しいコントラクトの作成、値の保存、または現在のコントラクトの破棄によって状態を変更します。

    • LOG0≤w ∧ w≤LOG4 静的に呼び出された場合、ログエントリを出力できません。 ログオペコードはすべて、LOG0 (A0) (opens in a new tab)からLOG4 (A4) (opens in a new tab)の範囲にあります。 ログオペコードの後の数字は、ログエントリに含まれるトピックの数を指定します。

    • w=CALL ∧ μs[2]≠0 静的な場合、別のコントラクトを呼び出すことはできますが、ETHをそれに送金することはできません。

  • w = SSTORE ∧ μg ≤ Gcallstipend Gcallstipend(付録Gで2300と定義)を超えるガスがない限り、SSTORE (opens in a new tab)は実行できません。

9.4.3 ジャンプ先の有効性

ここでは、JUMPDEST (opens in a new tab)オペコードについて正式に定義します。 バイト値0x5Bを単純に探すことはできません。PUSHの内部にある可能性があり(したがってオペコードではなくデータ)、そのためです。

式(153)では、関数_N(i,w)_を定義します。 最初のパラメータ_i_は、オペコードのロケーションです。 2番目のパラメータ_w_は、オペコード自体です。 _w∈[PUSH1, PUSH32]_の場合、オペコードがPUSHであることを意味します(角括弧は端点を含む範囲を定義しています)。 その場合、次のオペコードは_i+2+(w−PUSH1)_にあります。 PUSH1 (opens in a new tab)では、2バイト(PUSH自体と1バイトの値)進む必要があります。PUSH2 (opens in a new tab)では、2バイトの値であるため、3バイト進める必要があります。 他のすべてのEVMオペコードは1バイト長なので、他のすべてのケースでは_N(i,w)=i+1_です。

この関数は、式(152)で_DJ(c,i)_を定義するために使用されます。これは、コード_c_内のすべての有効なジャンプ先の集合 (opens in a new tab)であり、オペコードのロケーション_i_から始まります。 この関数は、再帰的に定義されています。 _i≥||c||_の場合、コードの末尾またはそれ以降にいることを意味します。 これ以上ジャンプ先は見つからないので、空集合を返すだけです。

それ以外のすべてのケースでは、次のオペコードに移動し、そこから始まる集合を取得することで、コードの残りの部分を調べます。 _c[i]_は現在のオペコードなので、_N(i,c[i])_は次のオペコードのロケーションです。 したがって、_DJ(c,N(i,c[i]))_は、次のオペコードから始まる有効なジャンプ先の集合です。 現在のオペコードがJUMPDESTでなければ、その集合を返すだけです。 JUMPDESTであれば、結果の集合にそれを含めて返します。

9.4.4 通常の停止

停止関数_H_は、3つの型の値を返すことができます。

  • 停止オペコードでない場合は、空集合である_∅_を返します。 慣例により、この値はブール型の偽(false)として解釈されます。
  • 出力を生成しない停止オペコード(STOP (opens in a new tab)またはSELFDESTRUCT (opens in a new tab))がある場合、戻り値としてサイズがゼロバイトのシーケンスを返します。 これは空集合とは大きく異なることに注意してください。 この値は、EVMが実際に停止したことを意味しますが、読み取る戻りデータはありません。
  • 出力を生成する停止オペコード(RETURN (opens in a new tab)またはREVERT (opens in a new tab))がある場合、そのオペコードで指定されたバイトのシーケンスを返します。 このシーケンスはメモリから取得され、スタックの先頭の値(μs[0])が最初のバイトで、その後の値(μs[1])が長さです。

H.2 命令セット

EVMの最後のサブセクション9.5に進む前に、命令自体を見てみましょう。 これらは付録H.2(29ページから)で定義されています。 特定のオペコードで変更されると指定されていないものはすべて、同じままであることが期待されます。 変更される変数は、<something>′として指定されます。

例えば、ADD (opens in a new tab)オペコードを見てみましょう。

ニーモニックδα説明
0x01ADD21加算演算
μ′s[0] ≡ μs[0] + μs[1]

_δ_は、スタックからポップする値の個数です。 この場合は、先頭にある2つの値を加算するので、2になります。

_α_は、プッシュバックする値の個数です。 この場合は合計で、1になります。

したがって、新しいスタックの先頭(_μ′s[0])は、古いスタックの先頭(_μs[0])とその下の古い値(μs[1])の合計です。

退屈なリストですべてのオペコードを確認する代わりに、この記事では何か新しいものを導入するオペコードのみを説明します。

ニーモニックδα説明
0x20KECCAK25621Keccak-256ハッシュの計算
μ′s[0] ≡ KEC(μms[0] .. 。 。 (μs[0] + μs[1] − 1)])
μ′i ≡ M(μis[0],μs[1])

これはメモリにアクセスする最初のオペコードです(この場合は読み取り専用)。 ただし、現在のメモリの制限を超えて拡張される可能性があるため、_μi_を更新する必要があります。これには、29ページの式328で定義されている_M_関数を使用します。

ニーモニックδα説明
0x31BALANCE11指定されたアカウントの残高を取得します。
...

残高を調べる必要のあるアドレスは_μs[0] mod 2160_です。 スタックの最上位がアドレスですが、アドレスは160ビットしかないため、値をモジュロ (opens in a new tab)2160で計算します。

_σ[μ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_は、トランザクションによって以前にアクセスされたアドレスのリストであり、8ページのセクション6.1で定義されているように、アクセスするコストが安くなるはずです。 この主題について詳しくは、EIP-2929 (opens in a new tab)で読むことができます。

ニーモニックδα説明
0x8FDUP16161716番目のスタックアイテムを複製します。
μ′s[0] ≡ μs[15]

スタックアイテムを使用するには、それをポップする必要があることに注意してください。これは、その上にあるすべてのスタックアイテムもポップする必要があることを意味します。 DUP<n> (opens in a new tab)およびSWAP<n> (opens in a new tab)の場合、これは最大16個の値をポップしてからプッシュする必要があることを意味します。

9.5 実行サイクル

すべてのパーツが揃ったので、いよいよEVMの実行サイクルがどのように文書化されているのかを理解できます。

式(155)は、次の状態が与えられたことを示しています。

  • σ(グローバルブロックチェーン状態)
  • μ(EVM状態)
  • A(サブ状態、トランザクション終了時に発生する変更)
  • I(実行環境)

新しい状態は_(σ', μ', A', I')_です。

式(156)~(158)は、スタックとオペコード(μs)によるスタックの変化を定義しています。 式(159)は、ガスの変化(μg)です。 式(160)は、プログラムカウンタ(μpc)の変化です。 最後に、式(161)~(164)は、オペコードによって明示的に変更されない限り、他のパラメータが同じままであることを指定しています。

これにより、EVMが完全に定義されました。

結論

数学的表記法は正確であり、イエローペーパーでイーサリアムのあらゆる詳細を指定することが可能になっています。 ただし、次のような欠点があります。

  • 人間だけが理解できるため、コンプライアンステスト (opens in a new tab)は手動で書く必要があります。
  • プログラマーはコンピュータのコードを理解します。 しかし、数学的な表記法については理解できないかもしれません。

おそらくこれらの理由から、新しいコンセンサスレイヤー仕様 (opens in a new tab)はPythonで書かれています。 Pythonで書かれた実行レイヤー仕様 (opens in a new tab)がありますが、完全ではありません。 イエローペーパー全体がPythonまたは同様の言語に翻訳されるまで、イエローペーパーは使用され続けるため、読めるようにしておくと便利です。

最終更新: 2026年2月1日

このチュートリアルは役に立ちましたか?