オンチェーンで 100 USDT(ERC-20 トークン)を送っても、「A が B に 100 USDT を支払った」というエントリが 1 件増えるわけではありません。実際に起きるのは、コントラクト内の balanceOf マッピングの値の書き換えです。送信側が 100 減り、受信側が 100 増えます。この書き換えが最終的にどのストレージスロットに落ちるかはコントラクトの書き方とコンパイラが決め、プロトコルが制約するのは balanceOf が何を返すかだけです。同時に、イベントログ(log)がトランザクション・レシートに追記されます。ログはオフチェーンのインデクサに消費される副産物であり、残高の真実はストレージの現在値にしかありません。
ここで台帳のアナロジーは破綻します。台帳はすでに起きたことを記録し、読み手が履歴を積み上げて結論を導きます。一方イーサリアムが保存するのは、絶えず上書きされる「現在の世界」であり、トランザクションはそれを次の版へ押し進めるだけです。「EVM とは何か」に答えるには、まずこの「現在の世界」を明確に定義し、そのうえで誰がそれを書き換えるのかを論じる必要があります。
ビットコインの台帳モデルで足りるのは、状態の構造が極端に制限されているから
「台帳」はビットコインを正確に言い当てた表現です。あるトランザクションの入力は、それ以前の別のトランザクションの出力を参照し、出力は後続の入力で使われるまで未使用トランザクション出力(unspent transaction output、UTXO)として残ります。UTXO は一度しか使えず、ウォレットが表示する残高は実際には複数の UTXO の合計です。
ビットコインに状態がないわけではなく、UTXO 集合そのものが状態です。ただしその構造はきわめて制限されています。まとめて作られ、まとめて使われる「コイン」の集まりにすぎず、汎用のキー・バリュー・ストアもなければ、スクリプトがトランザクションをまたいで存在する変数を保持することもありません。そのため「台帳+未使用出力の集合」でシステム全体を過不足なく記述できます。トランザクションの検証に必要なのは、参照された出力が存在するか、条件スクリプトが満たされているかを確認することだけで、「コントラクト内部の変数」を照会する必要はありません。
イーサリアムが、コントラクトが任意に読み書きできるマッピングを保持することを認めるなら、このマッピングはもはや実装の詳細ではありません。ERC-20 の送金が有効かどうかは、コントラクト・ストレージにある送信側の残高スロットが現在いくつかで決まります。状態はこうして「検証のついでに維持される集合」から、プロトコルがコミットメントを与えなければならない第一級の対象へ格上げされます。
ワールドステート:「今この瞬間のすべて」を 32 バイトに圧縮する
イーサリアムのイエローペーパーは、ワールドステート(world state)をアドレスからアカウントへのマッピングとして定義しています。各アカウントは 4 つ組です。
- nonce:そのアドレスがすでに送信したトランザクション数、またはそのコントラクト・アカウントが作成したコントラクト数;
- balance:wei 建ての残高;
- storageRoot:そのアカウントのストレージ内容のマークル・パトリシア・トライ(MPT)ルートハッシュ;
- codeHash:そのアカウントのコードの Keccak-256 ハッシュ。コード自体はこのハッシュをキーとして状態データベースに保存されます。
ここでは二つを区別する必要があります。storageRoot がコミットするのはアカウント自身のキー・バリュー空間であり、コントラクト作成者が宣言した状態変数は最終的にすべてここに落ちます。codeHash が指すのはプログラムで、デプロイ後にトランザクションで書き換えられることはありません。「データ」と「コード」を二つのフィールドで別々にコミットするという設計は、後で読み書き集合とコンフリクト検出を論じるときに繰り返し使う前提です(同シリーズの 0.5「三種類のストレージを混同しない:Memory、Storage、Transient Storage」で、三種類のストレージそれぞれの帰属と寿命を展開します)。
すべてのアカウントはさらにグローバルな MPT として構成され、そのルートハッシュが stateRoot としてブロックヘッダに書き込まれます。つまり「今この瞬間のイーサリアム全体の姿」は 32 バイトに圧縮でき、ネットワーク全体が合意すべきなのはこの 32 バイトだけです。
チェーン上に保存されるのは状態ルートで、状態そのものはノードの手元に残る
ここには曖昧に処理されがちな事実が一段あります。イエローペーパーは、ワールドステートはブロックチェーン上に保存されるのではなく、各ノードがそれぞれ維持し、チェーン上には stateRoot というコミットメントだけが残ると明確に述べています。トランザクションとレシートは完全に公開されますが、状態は公開されません。
この役割分担が、利用上の多くの境界を決めます。あるアカウントの現時点の残高を知る方法は二つしかありません。自分ですべての履歴トランザクションをリプレイして状態を計算するか、stateRoot の下で検証できるマークル証明をノードに要求するかです。ライトクライアントが取るのは二つ目の道で、状態全体を保存する必要はありませんが、証明を提供してくれる相手が必要です。しかも証明が示せるのは「ある stateRoot の下でこの値が何か」だけで、その stateRoot が最新であることは示せません。後者はコンセンサス層の役割です。
同じ理屈で、アーカイブノードとプルーニングノードの違いも理解できます。履歴状態は積み上がり続けるため、すべての履歴状態を保存するノードはディスクコストが増え続けます。直近の状態だけを保持するノードは占有量が小さく起動も速い代わりに、任意の過去ブロック時点での状態クエリに答えられません。いわゆる「オンチェーン・データ」は、エンジニアリング上は二つに分けて考える必要があります。トランザクションとレシートの可用性、そして状態ルートのコミットメントです。混ぜて論じると、後でデータ可用性と状態の肥大化を評価するときに解像度を失います。
状態遷移関数:イーサリアムが定めているのは一つの関数
イエローペーパーは 1 行の式でトランザクション単位のセマンティクスを定義しています。
σ_{t+1} ≡ Υ(σ_t, T)
ここで σ はワールドステート、T は一つのトランザクション、Υ は状態遷移関数です。もっと読みやすい書き方にすれば Y(S, T) → S' になります。イエローペーパーは続けて、有効な状態変更はトランザクションを通じてしか起こりえず、無効な状態変更は有効なものよりはるかに多いと強調します。たとえば、あるアカウントの残高を、どこかで同量増やすことなく減らす場合です。
この意味を踏まえると、別の言い方もできます。イーサリアムのプロトコルが本当に規定しているのは「現在の状態と一つのトランザクションが与えられたとき、次の版の状態は何か」であり、「チェーン上に何が保存されているか」はこの関数の入力と出力にすぎません。ブロックのレベルも同じで、イエローペーパーは Π によって同じ関数をトランザクション列に沿って畳み込みます。
Π(σ, B) ≡ Υ(Υ(σ, T₀), T₁)…
ここに来てようやくステートマシンという言葉が地に足をつけます。状態が σ、遷移規則が Υ、ブロックは遷移のバッチです。
台帳モデルとステートマシンモデルでは検証コストが異なる
二つのモデルを並べて比べると、違いはデータの組織方法だけでなく、検証の仕方に現れます。
UTXO トランザクションの入力は明示的に宣言されます。各入力が、どのトランザクションのどの出力を参照するかを書き記しています。検証者は UTXO 集合の中でこれらの具体的なエントリを調べるだけでよく、グローバルな構造を理解する必要はありません。これによりビットコインのトランザクション検証は本質的に局所化でき、互いに無関係なトランザクションを独立に処理できます。宣言的なアカウントモデル(たとえば Solana のトランザクション・メッセージのように、アカウント・アドレスをあらかじめ列挙し、命令がインデックスで参照する方式)も同じ発想です。
アカウントモデルでは、トランザクションの読み書き集合はトランザクションに書かれていません。コントラクトは任意のアドレスの任意のストレージスロットを読め、何を読んだかは実行が終わって初めて分かります。検証者は実行時と同じ状態ビューを保持しなければならず、そうでなければ結果を再現できません。この違いが、その後の技術的な道筋全体の出発点です。読み書き集合の推論、楽観的並行制御、コンフリクト検出は、いずれも「あるトランザクションが何に触れるか分からない」という問題を扱います。
イーサリアムも宣言的な方向へ近づこうとしたことがあります。EIP-2930 が導入したアクセスリスト(access list)は新しいトランザクション型 0x01 の任意フィールドで、トランザクションはアクセスする予定のアドレスとストレージスロットを事前に宣言し、それらを accessed_addresses と accessed_storage_keys の集合に加えて割引を受けられます。EIP 本文は同時に、リスト外のアクセスも依然として許されるがより高くつくと明記しています。読み書き集合がトランザクションに強制されたわけではなく、上の結論はそれによって変わりません。
プロトコルにおける EVM の位置づけ:「どう状態を変えるか」を定める仕様
イーサリアム仮想マシン(Ethereum Virtual Machine、EVM)は Υ の具体的な実行仕様であり、命令セマンティクスの一式、ガス計量規則の一式、例外と終了のセマンティクスの一式を含みます。コントラクトのコードは 1 バイト列にすぎず、トランザクションまたは他のコントラクトからメッセージ呼び出し(message call)で起動されたときに初めて実行に入ります。
EVM はイーサリアム・プロトコルそのものではありません。コンセンサス、データ可用性、決済はそれぞれ別の層のシステムで、スケーリングの地図の記事がこれを専門に分解しています。EVM が答えるのはただ一つ、あるバイトコードとある入力に対して、状態の変更と戻り値をどう決定論的に計算するかです。あわせてその計算にいくらかかるかも決めますが、ガス計量の規則自体がコンセンサスの一部であり、ハードフォークで変更できます。EIP-2929 がアカウントとストレージスロットへの初回アクセスの価格を大幅に引き上げたのがその例です。
もう一つ混同しやすい点があります。EVM のマシンモデルには「スタック」だけがあるのではありません。実行コンテキストごとにスタック(stack)、メモリ(memory)、プログラムカウンタ(program counter、PC)、ガスカウンタがあり、コントラクトの永続変数は SLOAD/SSTORE を通じてアカウント・ストレージに落ちます。スタック、メモリ、実行ループは 0.4 の主題で、三種類のストレージの境界は 0.5 の主題です。
ネットワーク全体でのリプレイ:なぜ結果はビット単位で一致しなければならないのか
イーサリアムには中心的な実行主体がありません。各フルノードが同じトランザクション群を独立に実行してそれぞれ stateRoot を計算し、その値をコンセンサスで突き合わせます。実行の細部に食い違いがあれば、そのままコンセンサスの失敗になります。だからこそ仕様はバイト単位まで細かく定める必要があります。
ここからいくつかの厳しい制約が導かれます。浮動小数点演算は命令セットに入っておらず、あるのは 256 ビット語長の整数に基づく算術だけです。ウォールクロック、プロセス ID、真の乱数のような外部入力はコンセンサス経路に現れてはいけません。コントラクトが読めるタイムスタンプとブロック番号はブロックヘッダ由来で、コンセンサスによって固定された値です。
計算は計量可能で必ず停止しなければならず、そうでなければ有限のブロックの中で任意のコードを安全に走らせられません。ガスは「停止問題」を「予算を使い切ったら止まる」に置き換える仕組みであり、その計量定数も性能チューニングの項目ではなくコンセンサス・パラメータに属します。通常の送金の固有コストは 21000 ガス、calldata のゼロバイトは 4 ガス、非ゼロバイトは 16 ガスで、これらの数値はイエローペーパーの費用表に固定されています。これらを変えることはブロックサイズを変えることと同じ種類のプロトコル変更であり、ハードフォークかバリデータのシグナルが必要で、特定のクライアントが独断で決められるものではありません。
決定論にはもう一つ見落とされやすい帰結があります。仕様は「未定義動作」の存在を許しません。イエローペーパーは、DIV が除数 0 のとき例外を投げずに 0 を返すと明記しています。この選択の理由は、すべてのクライアントが同じ不正な入力に対して同じ結果を得ることであり、言語設計の好みとは関係ありません。クライアントの単体テストと execution-spec-tests の仕様テストスイートは、こうした境界の挙動を固定するためのものです。複数の独立したクライアント(geth、revm、evmone など)が併存すること自体も決定論の検証手段です。ある実装がエッジケースを違って解釈すればチェーンは分岐するため、食い違いはテスト段階で見つけなければなりません。
決定論と予測可能性は別物である
二つの層の不確実性を区別する必要があります。実行層は決定論的です。同じバイトコード、同じ入力、同じ状態なら、必ず同じ結果になります。しかしトランザクションが得る結果は実行層だけで決まるわけではありません。同じトランザクションでもブロック内の位置が違えば先行する状態が変わり、結果も変わりえます。フロントランニングとサンドイッチの利益は順序づけの権利から生じるもので、実行層そのものは依然として決定論的です。
この区別は並列実行の方式を評価するうえで重要です。並列化が守るべきなのは「実行結果が何らかの直列順序と等価であること」であり、「結果がトランザクションの送信時刻に依存しないこと」という目標はそもそも成立しません。
EVM は凍結された仕様ではない
EVM を固定された標準と見なすと、互換性の判断を誤ります。仕様はハードフォークとともに進化します。PUSH0(EIP-3855、Shanghai)はスタックマシンに定数 0 を積む命令を追加しました。EIP-2929 はコールド/ホットアクセスを再価格設定しました。トランザクション層では 1 トランザクションあたり 16,777,216 ガスの上限が導入され(EIP-7825、Fusaka で稼働)、EIP-7935 はクライアントの既定のブロックガス上限を 60M に引き上げました。
したがって「EVM 互換」という言葉は、フォーク高さと具体的な範囲を伴って初めて意味を持ちます。バイトコード、プリコンパイル・コントラクト、JSON-RPC、ツールチェーンがそれぞれどこまで互換なのかは、末尾の関連記事の範囲です。
このモデルはどんな条件で負担になるのか
状態中心の設計が手に入れたのは汎用計算であり、その代価は三か所に現れます。
フルノードは、いつでも読める現在の状態を長期にわたって保持しなければならず、その規模はアカウントとストレージのエントリとともに増え、ハードウェアの敷居を押し上げ続けます。
新しいノードが自力で stateRoot を計算するには、履歴トランザクションをリプレイするかスナップショット同期に頼る必要があり、検証コストは履歴の長さに連動します。
すべてのトランザクションがグローバル状態の任意の位置に触れうるため、古典的な実装は 1 件ずつ直列に実行するしかありません。これが、以降の並列実行の議論すべてが扱うべき原初的な制約です。
逆に言えば、台帳のアナロジーが使えないのも絶対ではありません。監査、インデックス、照合といった場面では依然としてイベントストリームとしてデータを扱います。レシート内のログはもともとオフチェーンでの消費のために設計されているからです。問題は、この視点をプロトコルのモデルと取り違えないことに尽きます。
出典
- Ethereum Yellow Paper(Upsilon、Pi、ワールドステートの保存方法、アカウントの 4 つ組、DIV のセマンティクス、ガス費用表):https://ethereum.github.io/yellowpaper/paper.pdf
- ethereum.org、Ethereum Virtual Machine (EVM):https://ethereum.org/developers/docs/evm/
- ethereum.org、Understanding the Yellow Paper's EVM Specifications:https://ethereum.org/developers/tutorials/yellow-paper-evm/
- Bitcoin Developer Guide、Transactions(UTXO は一度しか使えず、入力は具体的な出力を参照する):https://developer.bitcoin.org/devguide/transactions.html
- Solana Docs、Transactions(トランザクション・メッセージにあらかじめアカウント・アドレスを列挙し、命令がインデックスで参照する):https://solana.com/docs/core/transactions
- ethereum.org、Nodes and clients(フルノード、アーカイブノード、状態プルーニング。すべての履歴状態を保持するのはアーカイブノードだけ):https://ethereum.org/developers/docs/nodes-and-clients/
- EIP-2929、Gas cost increases for state access opcodes:https://eips.ethereum.org/EIPS/eip-2929
- EIP-2930、Optional access lists(型 0x01 トランザクションの任意フィールド。リスト外のアクセスも依然として許されるが、より高くつく):https://eips.ethereum.org/EIPS/eip-2930
- EIP-3855、PUSH0 instruction:https://eips.ethereum.org/EIPS/eip-3855
- EIP-7825、Transaction Gas Limit Cap(16,777,216 ガス):https://eips.ethereum.org/EIPS/eip-7825
- EIP-7935、Set default gas limit to 60M:https://eips.ethereum.org/EIPS/eip-7935
- Ethereum Foundation、Fusaka Mainnet Announcement:https://blog.ethereum.org/2025/11/06/fusaka-mainnet-announcement
- go-ethereum、core/vm/instructions.go(opDiv は除数が 0 のとき 0 を書き込む):https://github.com/ethereum/go-ethereum/blob/master/core/vm/instructions.go
- ethereum/execution-spec-tests(仕様整合性テストスイート):https://github.com/ethereum/execution-spec-tests
