レッスン6 — EVMはスタックマシンだ
問い
これまで Alloy で「外側から」Ethereum と話してきた。ここから EVM の中身を覗く — Ethereum Virtual Machine は何で、どう動くか。EVM は スタックマシン、レジスタもメモリアドレスもなく、ただ「上に積む / 下から取り出す」だけで計算する。
原理(最小モデル)
- スタックマシン vs レジスタマシン. スタック = 値の置き場が 1 つ(top)/ レジスタ = 名前付き場所が複数(R0, R1, ...)。EVM は 1024 段のスタックのみ。
- 5 つの記憶領域. Stack(1024 段、現行計算)/ Memory(揮発、tx 内のみ)/ Calldata(読み専用、tx の入力)/ Storage(永続化、ブロックチェーン状態)/ Code(読み専用、コントラクトコード)。
- Opcode = 1 バイト命令.
0x01 ADD/0x60 PUSH1/0x52 MSTORE/0x55 SSTORE、ADD はスタックトップ 2 値を pop して和を push。 - ガス. 全 opcode が gas コスト持ち、tx に gas limit、消費でランタイム停止、Storage 書き込みが最も高い(永続化)。
- スタックマシン選択の理由. 命令セット小 + オペランドエンコード単純 = コンセンサスバグ少 + 検証 / ZK 回路化容易、トレードオフはネイティブレジスタコードに対するランタイム効率。
具体例 + ステップで組み立てる
EVMはスタックマシンだ
EVM(Ethereum Virtual Machine)は スタックマシン と呼ばれる仮想機械の一種である。レジスタや関数呼び出し規約を持たず、ほぼすべての計算を スタック 上で行う。
主要な「場所」
EVMは命令を実行する際、以下の3つを使います:
| 場所 | 性質 | 用途 |
|---|---|---|
| Stack | LIFOの最大1024段 | 計算の入出力 |
| Memory | 1トランザクション内で揮発 | 大きなデータの一時置き場 |
| Storage | 永続化される(高コスト) | コントラクトの状態 |
ADD命令の動き
例えば「2つの数を足す」というOpcode ADD は次のように動きます:
事前: スタック [..., 7, 5]
ADD実行
事後: スタック [..., 12]
つまり:
- スタックの一番上を2つ pop
- 加算
- 結果を push
本物のRevm Stack
これは理論ではなく、crates/interpreter/src/interpreter/stack.rs にある実構造体:
pub const STACK_LIMIT: usize = 1024;
#[derive(Debug, PartialEq, Eq, Hash)]
pub struct Stack {
/// スタックの内部データ
data: Vec<U256>,
}
これがスタック構造の全て:U256 のVec、上限1024。インタープリター内で呼ばれるメソッド群:
pub fn new() -> Self
pub fn push(&mut self, value: U256) -> bool
pub fn pop(&mut self) -> Result<U256, InstructionResult>
pub fn peek(&self, no_from_top: usize) -> Result<U256, InstructionResult>
pub fn popn<const N: usize>(&mut self) -> Option<[U256; N]>
pub fn dup(&mut self, n: usize) -> bool
pub fn swap(&mut self, n: usize) -> bool
注意深く読む:
push(...) -> bool— pushできたらtrue、1024を超えたらfalse。インタープリターのマクロがこれをチェックしてStackOverflowで抜ける。pop(...) -> Result<...>— 明示的なアンダーフロー検出、InstructionResult::StackUnderflowとして返す。popn<const N: usize>()— N個を一度にpop し、固定サイズ配列で返す。const ジェネリクスによりコンパイラがpopループを展開。これがpopn_top!を速くしている正体。
ADD の実際の動作
2つpop、加算、push。擬似コード:
事前: スタック [..., 7, 5]
ADD
事後: スタック [..., 12]
Revm の 本物の add ソース(中級ティアで一行ずつ分解する)は、両方popしてpushすらしない。1つpopし、もう1つへ可変参照経由で書き戻す。Revmのインタープリターは、EVM概念モデルをRustへ写しつつ、サイクル単位の最適化を重ねた設計である。
なぜEVMはスタックマシンか
- シンプルさ — 命令セットが小さく、コンセンサスバグを減らせる
- 再現性 — 再実行と検証がしやすい
- ZK化との相性 — スタックセマンティクスは制約系にきれいに写る(後のzkEVMで見ます)
練習
リポジトリで crates/interpreter/src/interpreter/stack.rs を開く。次を探す:
push内のSTACK_LIMITチェック — オーバーフロー時に何をする?popnの実装 — constNでコンパイラがループを完全に省略する仕組みdupとswapメソッド — 確保せずインデックスを操作するだけ
これで次のレッスンで自分のミニスタックマシンを書く準備ができました。
まとめ(3行)
- EVM = スタックマシン、1024 段スタック + 5 記憶領域(Stack / Memory / Calldata / Storage / Code)、Storage のみ永続化。
- 1 バイト Opcode、
ADDは pop 2 + push 1、wrapping arithmetic(mod 2²⁵⁶)、全 opcode が gas コスト持ち、Storage 書き込み最も高い。 - スタックマシン選択 = 命令セット小 + 検証容易 + ZK 回路化容易、トレードオフはレジスタコードに対するランタイム効率、次はミニ EVM スタッククイズ。