イエローペーパーにおけるEVM仕様の理解
イエローペーパーは、イーサリアムの正式な仕様です。 EIPプロセスによって修正される場合を除き、すべての動作方式が正確に記されています。 数学の論文形式で書かれており、プログラマーには馴染みのない専門用語が含まれています。 この論文を通して、論文の読み方を学び、他の関連する数学論文も読めるようにしましょう。
解説するイエローペーパーについて
イーサリアムに関する他のものと同じように、イエローペーパーも時間の経過とともに進化しています。 特定のバージョンを参照できるように、執筆時のバージョンをアップロードしています。 このドキュメントで使用するセクション番号、ページ番号、数式番号は、そのバージョンを参照しています。 ドキュメントを読む際は、アップロードされたイエローペーパーを別のウィンドウで開いておくと便利です。
EVMを解説する理由
イーサリアムの開発が始まったときに書かれたオリジナルのイエローペーパーでは、 コンセンサスメカニズムのベースとなったオリジナルのプルーフ・オブ・ワークについて記述されていました。 しかし、イーサリアムでは、2022年9月にプルーフ・オブ・ワークを止め、プルーフ・オブ・ステークをベースとしたコンセンサスを使い始めました。 このチュートリアルでは、イエローペーパーにおけるイーサリアム仮想マシンの定義部について解説します。 プルーフ・オブ・ステークへの移行後も、EVMは変更されていないことが解説する理由です。ただし、DIFFICULTY オペコードの戻り値は変更されています。
9 実行モデル
このセクション(p. 12-14)には、EVMの定義のほとんどが記述されています。
_システム状態_とは、システムを実行するため必要なすべての情報を指します。 典型的なコンピュータでは、これはメモリやレジスタの内容などです。
チューリングマシンは、計算モデルです。 チューリングマシンは、コンピュータを本質的に単純化したもので、通常のコンピュータと同じ計算を実行する能力があることが証明されています。つまり、コンピュータが計算できるものはすべてチューリングマシンでも計算でき、またその逆も同様です。 このモデルは、さまざまな定理で何が計算可能で何が計算不可能であるかを証明するのに役立ちます。
チューリング完全とは、チューリングマシンと同じ計算を実行できるコンピュータのことを指します。 チューリングマシンは無限ループができますが、EVMではガスがなくなると無限ループができません。そのため、EVMは準チューリング完全であるといわれています。
9.1 基本事項
このセクションでは、EVMの基本事項と、EVMと他の計算モデルとの比較について説明しています。
スタックマシンは、中間データをレジスタではなくスタックに格納するコンピュータです。 これは、仮想マシンで好まれるアーキテクチャです。なぜなら、実装が簡単で、バグやセキュリティの脆弱性が発生する可能性を大幅に低くできるためです。 スタック内のメモリは、256ビットのワードに分割されます。 256ビットが選ばれた理由としては、Kecck-256ハッシュや楕円曲線計算など、イーサリアムの中核となる暗号操作に都合が良いためです。 スタックの最大サイズは、1024バイトです。 オペコード実行時、通常、スタックからパラメータを取得します。 POP
(スタックの先頭のアイテムの削除)、DUP_N
(スタックのN番目のアイテムの複製)など、スタック内の要素を再構成するための専用オペコードがあります。
EVMには、 実行中にデータを保存するために使用されるメモリと呼ばれる揮発性スペースもあります。 このメモリは、32バイトのワードで構成されます。 すべてのメモリのロケーションは、ゼロで初期化されます。 次のYulコードを実行してメモリにワードを追加すると、32バイトのメモリのワード内にある空スペースがパディングによってゼロで埋められます。つまり、ロケーション0~29、0x60~30、0xA7~31にゼロが含まれる1つのワードが作成されます。
1mstore(0, 0x60A7)
EVMは、メモリとやり取りをする3つのオペコードを提供しています。そのうちの1つがmstore
で、ワードをメモリにロードします。 他の2つは、1バイトをメモリにロードするmstore8
、ワードをメモリからスタックに移動するmload
です。
EVMには、システム状態の一部として保持される独自の不揮発性ストレージモデルもあります。このメモリは、(ワードアドレス可能なスタック内のバイト配列とは違い)ワードの配列で構成されます。 このストレージは、コントラクトが永続データを保存する場所です。コントラクトは、自分のストレージとしかやり取りできません。 ストレージは、キーと値のマッピングで構成されます。
イエローペーパーのこのセクションでは言及されていませんが、メモリに4番目のタイプがあることを知っておくといいでしょう。 Calldataは、トランザクションのdata
パラメータで渡された値を保存するために使用される、バイトアドレス可能な読み取り専用のメモリです。 EVMには、calldata
を管理する専用のオペコードがあります。 calldatasize
は、そのデータのサイズを返します。 calldataload
は、そのデータをスタックにロードします。 calldatacopy
は、そのデータをメモリにコピーします。
標準のフォンノイマン型アーキテクチャでは、コードとデータが同じメモリに保存されます。 しかし、EVMでは、セキュリティ上の理由からこの標準に準拠していません。なぜなら、揮発性メモリを共有すると、プログラムのコードが変更される可能性があるからです。 そのため、コードはストレージに保存されます。
コードがメモリから実行されるのは、次の2つのケースだけです。
- コントラクトが他のコントラクトを作成する場合(
CREATE
またはCREATE2
を使用)、コントラクトにあるコンストラクタのコードはメモリから取得されます。 - _あらゆる_コントラクトの作成において、コンストラクタのコードが実行され、メモリから実際のコントラクトのコードが返されます。
例外実行とは、現在のコントラクトの実行を停止させる例外を意味します。
9.2 フィーの概要
このセクションでは、ガス代の計算方法について説明しています。 次の3つのコストがあります。
オペコードコスト
特定のオペコードで発生する固有のコストです。 この値を取得するには、付録H(ページ28)の式(327)の下部でオペコードのコストグループを見つけます。そして、式(324)でコストグループを見つけます。 ここには、コスト関数があります。ほとんどのケースにおいて、付録G(ページ27)のパラメータを使用します。
例えば、オペコードCALLDATACOPY
は、_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番目のロケーションにあるということです。 こちらのオペコードでは、スタック内のロケーション#2に、データのサイズ(バイト単位)が格納されています。 グループWの他のオペコードcopyであるCODECOPY
とRETURNDATACOPY
も、同じロケーションにデータのサイズを格納しています。 したがって、⌈μs[2]÷32⌉ は、コピーされるデータを保存するために必要になる32バイトのワード数です。 以上から、CALLDATACOPY
に固有のコストは、3ガスにコピーされるデータのワードごとに3ガスを加えたものになります。
実行コスト
呼び出すコードの実行コスト。
CREATE
およびCREATE2
の場合は、新しいコントラクトのコンストラクタCALL
、CALLCODE
、STATICCALL
、DELEGATECALL
の場合は、呼び出すコントラクト
メモリーコストの拡張
(必要な場合の)メモリ拡張におけるコストについて。
この値は、式324で_Cmem(μi')-Cmem(μi)_として記述されています。 セクション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 実行環境
実行環境は、_I_で表します。これは、ブロックチェーンの状態やEVMの以外の情報を含みます。
パラメータ | データにアクセスするためのオペコード | データにアクセスするためのSolidityのコード |
---|---|---|
Ia | ADDRESS | address(this) |
Io | ORIGIN | tx.origin |
Ip | GASPRICE | tx.gasprice |
Id | CALLDATALOAD , etc. | 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(13ページ目) | ガスの残量 |
A | 6.1(8ページ目) | 発生したサブ状態(トランザクション終了時にスケジュールされた変更) |
o | 9.3(13ページ目) | 出力 - 内部トランザクションの場合に返される結果(あるコントラクトが別のコントラクトを呼び出すとき)およびビュー関数の呼び出し(情報を求めるだけなので、トランザクションを待つ必要がない場合) |
9.4 実行の概要
それでは準備が整ったので、ようやくEVMがどのように機能するかについての説明を開始します。
式137~142は、EVMを実行するための次の初期条件を示しています。
Symbol | 初期値 | 説明 |
---|---|---|
μg | g | ガスの残量 |
μpc | 0 | プログラムカウンタ、実行する次の命令が格納されたアドレス |
μm | (0, 0, ...) | メモリ、ゼロで初期化 |
μi | 0 | 使用される最上位のメモリ位置 |
μs | () | スタック、初期値は空 |
μo | ∅ | 戻りデータ(RETURN またはREVERT )もしくは戻りデータ無し(STOP またはSELFDESTRUCT )で止めない限り、戻りデータは空の出力 |
式143では、実行中の各時点で4つ状態が発生する可能性があること、そしてそれらをどのように処理するかを示しています。
Z(σ,μ,A,I)
Zは関数を表しており、操作によって無効な状態遷移が発生するかどうかをテストします(例外による停止を参照)。 Trueと評価された場合、変更が行われていないため、新しい状態は古い状態と同じです (ガスがバーンされることは除く) 。- 実行されているオペコードが
REVERT
の場合は、ガスが失われ、新しい状態と古い状態は同一になります。 - 一連の演算が終了すると、
RETURN
で示されるように、状態は新しい状態に更新されます。 - 終了条件1~3のいずれでもない場合は、実行を続けます。
9.4.1 マシンの状態
このセクションでは、マシンの状態について詳しく説明します。 ここでは、_w_が現在のオペコードであることを規定しています。 μpcがコードの長さを示す||Ib|| 未満の場合、そのバイト(Ib[μpc])は、オペコードです。 それ以外の場合、オペコードはSTOP
と定義されています。
スタックマシンであるため、ポップアウトされたアイテムの数(δ)と各オペコードによってプッシュインされたアイテムの数(α)を追跡する必要があります。
9.4.2 例外による停止
このセクションでは、異常終了がいつ発生するかを規定する_Z_関数を定義しています。 これは Boolean関数であり、論理和では_∨_を使用します。また、論理積では_∧_を使用します。
次のいずれかの条件が真になる場合、例外による停止をします。
-
μ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,μ)_は、この後に式150にて定義されています。 次の条件が真の場合、_W(w,μ)_が真になります。
-
w = SSTORE ∧ μg ≤ Gcallstipend Gcallstipend(付録Gで2300で定義)以上のガスがなければ、
SSTORE
を実行することはできません。
9.4.3 ジャンプ先の有効性
ここでは、JUMPDEST
オペコードについて形式的に定義します。 バイト値0x5Bを単純に見つけることはできません。なぜなら、バイト値0x5Bは、PUSH内にある可能性があるためです(つまり、オペコードではなくデータ)。
式(153)では、関数_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_となります。
この関数は、式(152)で_DJ(c,i)_と定義され、コード_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に進む前に、命令自体について考察してみましょう。 付録H.2で定義されており、ページ29から開始しています。 特定のオペコードでは、規定されていないものはすべて同じままであることが求められます。 変化する変数は、<something>'と規定されています。
例として、ADD
オペコードを見ていきます。
値 | Mnemonic | δ | α | 説明 |
---|---|---|---|---|
0x01 | ADD | 2 | 1 | 加算演算 |
μ′s[0] ≡ μs[0] + μs[1] |
_δ_は、スタックからポップする値の個数です。 この場合は、先頭にある2つの値を加算するので、2になります。
_α_は、プッシュバックする値の個数です。 この場合は、合計で1になります。
なぜなら、新しいスタックの先頭(μ′s[0])は、古いスタックの先頭(μs[0])とその次の古い値(μs[1])の合計となるためです。
この記事では、すべてのオペコードを網羅するのではなく、新規性のあるオペコードのみを説明します。
値 | Mnemonic | δ | α | 説明 |
---|---|---|---|---|
0x20 | KECCAK256 | 2 | 1 | Keccak-256ハッシュの計算 |
μ′s[0] ≡ KEC(μm[μs[0] . 。 。 (μs[0] + μs[1] − 1)]) | ||||
μ′i ≡ M(μi,μs[0],μs[1]) |
これはメモリにアクセスする最初のオペコードです(この場合は、読み取り専用)。 ただし、現在のメモリの制限を超えて拡張される可能性があるため、_μi_を更新する必要があります。ページ29の式328に定義されている_M_関数を使ってこの更新を行っています。
値 | Mnemonic | δ | α | 説明 |
---|---|---|---|---|
0x31 | BALANCE | 1 | 1 | 指定されたアカウントの残高を取得 |
... |
残高を知る必要のあるアドレスは、μs[0] mod 2160 です。 スタックの最上位がアドレスなのは、アドレスは160ビットしかないためです。値をmodulo 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をご覧ください。
値 | Mnemonic | δ | α | 説明 |
---|---|---|---|---|
0x8F | DUP16 | 16 | 17 | 16番目のスタックアイテムを複製 |
μ′s[0] ≡ μs[15] |
スタックアイテムを使用するには、ポップする必要があることに注意してください。つまり、そのアイテム上にあるすべてのスタックアイテムもポップする必要があります。 DUP<n>
およびSWAP<n>
の場合は、最大で16個の値をポップして、その後にプッシュしなければならなりません。
9.5 実行サイクル
すべてのパーツが揃ったので、ようやくEVMの実行サイクルがどのように文書化されているのかを理解できます。
数式(155)は、次の状態を示しています。
- σ(グローバルブロックチェーンの状態)
- μ(EVMの状態)
- A(サブ状態、トランザクション終了時に発生する変更)
- I(実行環境)
新たな状態は、_(σ', μ', A', I')_となります。
数式(156)~(158)は、スタックとオペコード(μs)によるスタックの変化を定義しています。 数式(159)は、ガスの変化(μg)です。 数式(160)は、プログラムカウンタ(μpc)の変化です。 最後に、数式(161)~(164)は、オペコードによって明示的に変更されない限り、他のパラメータが同じままであることを明記しています。
以上より、EVMが完全に定義されました。
まとめ
数学的表記法は正確であるため、イエローペーパーでは、イーサリアムのあらゆる詳細が記述されています。 ただし、次のような欠点があります。
- 人間のみが理解できるため、コンプライアンステストは、手作業によって記述する必要があります。
- プログラマーはコンピュータのコードは理解できますが、 数学的な表記法については、理解できない人もいます。
このような理由から、新たなコンセンサスレイヤーの仕様は、 Pythonで記述されています。 こちらにPythonで書かれた実行レイヤーの仕様がありますが、完全ではありません。 イエローペーパー全体がPythonもしくは同様の言語に翻訳されるまで、イエローペーパーは使われ続けます。そのため、イエローペーパーを読めるようにしておくと便利です。
最終編集者: @pettinarip, 2025年2月25日