---
id: 30
title: "クライアント内の EVM：インタプリタ、JIT、revm/evmone の一覧"
slug: 0-26-evm-in-clients
date: 2026/10/09
summary: 実行クライアントは EVM を交換可能な一つの実行者として扱い、その間には状態アクセスのインターフェースが一層挟まります。go-ethereum の組み込みインタプリタ、evmone、revm はそれぞれ位置づけが異なり、マルチ実装の一致性は規範のテストベクタで担保され、インタプリタの最適化余地はディスパッチ、gas 計量、メモリ管理に集中しています。
keywords: EVM実装,evmone,revm,マルチクライアント一致性,インタプリタ最適化
heroImage: /images/articles/photos/0-26-evm-in-clients.jpg
---

実行クライアント（execution client）はブロック同期、トランザクションプール、状態ストレージ、JSON-RPC、コンセンサスとの接続を同時に扱わなければならず、イーサリアム仮想マシン（Ethereum Virtual Machine、EVM）はそのうちの一つの部品にすぎません。EVM はブロックがどこから来るかを知らず、一つのトランザクションの gas 価格をどの規則が与えるかも決めません。EVM が受け取るのはバイトコード、コールデータ、実行環境、gas であり、返すのはリターンデータ、残りの gas、異常状態、そして一連の状態変更です。

この境界をはっきり描いて初めて、go-ethereum、evmone、revm がなぜそれぞれ異なる位置にあるのかを理解できます。一つはクライアントに組み込まれたインタプリタ、一つは言語をまたぐ独立したライブラリ、一つは依存として取り込まれる Rust フレームワークです。以下では汎用クライアントにおける EVM の組み込み方と、複数実装の一致性をどう担保するかだけを扱い、並列スケジューリングとマルチエンジンの設計には立ち入りません。その部分は文末の関連記事を参照してください。

## EVM とクライアントの残りの部分との境界

どの実装でも、EVM とホストの間のやり取りは同じ操作の組に収束します。アカウントの残高、nonce、コードを読み、ストレージスロットを読み、過去のブロックハッシュを読む。ストレージの変更を書き戻し、ログを発行し、アカウントを作成または削除する。そして gas の収支です。違いは、この操作の組がどのような形で公開されるかにすぎません。

go-ethereum の `core/vm` は EVM を構造体一つとインタプリタループとして実装しています。インタプリタは fork ごとに 256 エントリのジャンプテーブル（jump table）を選び、各エントリは実行関数、定数 gas、動的 gas の計算関数、スタック高の制約、メモリ要件を含みます。ループの順序は、オペコードを取り出し、スタックを検証し、定数 gas を引き、動的 gas を計算し、必要ならメモリを拡張し、実行関数を呼び出すというもので、gas が不足すれば一律に `ErrOutOfGas` を返します。状態アクセスは `StateDB` を通り、ブロックとトランザクションのコンテキストは `BlockContext` と `TxContext` を通ります。内在 gas、nonce の検証、手数料の差し引き、返金の計算はインタプリタにはなく、状態遷移レイヤーで行われます。

revm の構造はより明示的です。`Evm` は `Context` とコンストラクタから成り、`Context` はさらに三つの部分、すなわち環境（`Transaction`、`Block`、`Cfg`）、`Journal`、`Database` から構成されます。`Database` は必須メソッドが四つだけのインターフェースです。`basic` はアカウントを読み、`code_by_hash` はコードを読み、`storage` はストレージスロットを読み、`block_hash` は過去のブロックハッシュを読みます。読み込まれたデータは `Journal` にキャッシュされ、`Journal` は実行中の変更も同時に記録し、実行終了後に状態の差分を返します。インタプリタはデータベースに直接触れず、`Host` トレイトを通じてアクセスします。このトレイトはブロック、トランザクション、設定、データベース、Journal ごとにグループ化され、`sload`、`sstore`、`tload`、`tstore`、アカウントのロード、ログ、自壊をカバーします。

evmone はこの境界を C 言語の ABI として作りました。evmone は EVMC（Ethereum Client-VM Connector API）を実装し、このインターフェースは EVM とクライアントの間の低レベル ABI として定義され、クライアント側が EVM の環境と状態へのアクセス方法を規定します。境界が C ABI であるため、evmone は他の言語で書かれたクライアントからロードできます。Erigon の振り返りによれば、Erigon は EVMC を通じて evmone を自身の実行レイヤーに組み込み、インターフェースのオーバーヘッドを下げるために C++ のコードによるラッパーを一段追加しました。

## 三つの実装の位置づけの違い

| 実装 | 言語とライセンス | 形態 | 状態インターフェース | 典型的な再利用方法 |
|------|------------|------|----------|--------------|
| go-ethereum `core/vm` | Go、ライブラリコードは LGPL-3.0、`cmd/` 以下は GPL-3.0 | クライアントに組み込まれたインタプリタ | `StateDB` | L2 実行クライアントとして fork される。例として OP Stack の op-geth |
| evmone | C++20、Apache-2.0 | 独立したモジュール、EVMC を通じて公開 | EVMC ホストインターフェース | Silkworm などのクライアントから実行モジュールとしてロードされる |
| revm | Rust、MIT | ライブラリとフレームワーク、crate 単位で分割 | `Database` と `Host` トレイト | Reth、Foundry、Helios などが直接依存として取り込み、L2 と zkVM でもよく選ばれる |

三つの形態は三つのエンジニアリング上のトレードオフに対応します。組み込みインタプリタは、自身の状態レイヤー、ジャンプテーブル、トレーサと一緒に最適化できますが、他のクライアントから再利用するのが難しいという代償があります。独立したライブラリは言語をまたいで再利用できますが、汎用インターフェースを一層挟まなければならず、インターフェース両側の呼び出しオーバーヘッドがコストになります。同じ振り返りはこうも述べています。EVMC は設計上、ホストが EVM を呼び出すことも、EVM がホストを逆に呼び出すことも許しますが、後者は CGo を通じて Erigon に接続する際に無視できないインターフェースオーバーヘッドをもたらし、最終的に C++ コードを補って解消する必要がありました。この経験は、最速の EVM をクライアントに接続することと、クライアントをすぐに速くすることは別物だと示しています。

ライブラリ形態の利点はエコシステムの面でより明確です。revm のドキュメントが挙げる利用者には、ブロックビルダー、Reth や Helios といったクライアント、Foundry や Hardhat といったツール、複数の L2、そしていくつかの zkVM プロジェクトが含まれます。逆に、go-ethereum を fork する路線は L2 側で縮小の兆しが現れています。Optimism のドキュメントによれば、op-geth は 2026 年 5 月 31 日をもってサポートを終了し、現在有効な Karst ハードフォークにも対応していません。主に推奨される実行クライアントは、Reth と revm をベースにした op-reth に置き換わっています。

## 一致性は自己テストでは得られない：state test とマルチクライアントテスト

コンセンサスは、すべてのノードが同じバイトコードに対して完全に同じ状態を導くことを要求します。単一クライアントの自己テストは自己整合性を示せるだけで、一致性を証明することはできません。

歴史上の不一致の代償は定量化できます。2016 年 11 月 24 日、イーサリアムはブロック 2686351 で分岐しました。空アカウントの削除を引き起こすトランザクションが out-of-gas で終了したとき、go-ethereum はこれらの削除をロールバックせず、Parity はロールバックしました。少数派のチェーンはブロック 2686516 付近で放棄され、約 165 ブロック分の産出が無効になりました。go-ethereum は v1.5.3 でログ機構を修正して Parity の挙動に合わせ、公式説明は同時に、Parity が「out-of-gas でプリコンパイルコントラクトを呼び出す」というより狭い場面でも不一致を抱えていたと指摘しています。この修正は EIP-161 に「状態をロールバックするときは空アカウントの削除もロールバックすべき」という説明を補いました。クライアントのコードには同種の痕跡がもう一箇所残っています。go-ethereum の状態ログは RIPEMD160 プリコンパイルアドレス `0x03` に明示的な touch マーカーを保持しており、ソースのコメントはこれがブロック 1714175 の空アカウント touch/revert の特例を再現するためのものだと説明しています。仕様と実装の間のこうしたハードコードされた互換は長期に残り、マルチ実装の一致性を一件ずつ照合する必要がある理由でもあります。

業界の対応は、規範とテストベクタを同じ源から生み出すことでした。実行レイヤーの規範は Python の参照実装（Execution Layer Specification、一般に EELS と呼ばれます）の形で維持され、テストフレームワークは規範からテストベクタ（fixture）を生成します。execution-spec-tests はもともとテストベクタを生成する Python フレームワークとケース集合でしたが、2025 年 11 月にまるごと ethereum/execution-specs へ移管され（旧リポジトリはその後アーカイブされました）、公式説明ではクライアント向け fixture の公開場所は変わらないとされています。これで仕様とテストが同じ源を持つことになりました。

状態テスト（state test）の判定方法はきわめて直接的です。実行前の状態 `pre`、環境 `env`、トランザクション `transaction`、fork ごとに分けられた期待結果 `post` が与えられたとき、クライアントはそのトランザクションを適用した後、`post` に記録された状態ルート `hash` とログ要約 `logs` に一致しなければなりません。失敗すべきトランザクションについては、`expectException` で期待される例外型を記述します。ブロックテストはこの上にブロックレベルの処理を重ねます。これらのベクタは、各クライアントが備えるテスト入口から直接実行できます。go-ethereum は `evm statetest`、Besu は `evmtool state-test`、Nethermind は `nethtest`、evmone は `evmone-statetest` と `evmone-blockchaintest` を使います。Hive に入れて、Engine API でクライアントへブロックペイロードを送り、応答と状態同期を検証することもできます。これが本番の挙動に最も近いテスト形態です。ethpandaops の hive-tests リポジトリは、こうしたテストを毎日実行するパイプラインにしており、Besu、Erigon、EthereumJS、Ethrex、go-ethereum、Nethermind、Nimbus-EL、Reth をカバーしています。

テストの境界も明確に述べておきます。テストが覆えるのはすでに書き下された挙動だけであり、fork が一つ導入されるたびに、まだベクタで固定されていない窓が増えます。テストが拘束するのは観測可能なセマンティクスであり、性能でも内部構造でもありません。

## 命令ディスパッチ：ジャンプテーブル、生成的な switch、インライン化

インタプリタのメインループは、命令を一つ実行するたびにまずその処理関数を特定しなければなりません。ジャンプテーブルの方法は、256 項目の配列を一度引き、関数ポインタを通じて呼び出すというものです。Go は関数値を越えてインライン化できないため、命令ごとに配列ロード一回と間接呼び出し一回のコストを払います。

go-ethereum は 2026 年に、ディスパッチを生成的な switch へ変える変更を提出しました（PR #35144 と、それを置き換える PR #35638）。オペコードのバイトに対する密な switch はジャンプテーブルへコンパイルされ、コンパイラはホットな処理関数をループへインライン化できます。変更は三層に分かれます。fork 間で挙動が安定したホットなオペコードは個別の case とし、定数 gas とスタックの上下限は直接定数として書く。動的 gas を持つが fork 間で不変な少数のオペコードは依然としてテーブルで課金しますが、処理関数を名前で呼び出す。fork によって変化するオペコード（`CALL`、`CREATE`、`SSTORE`、`SLOAD`、ログとコピー系）は引き続き現在の fork のテーブルを通じてディスパッチします。元のインタプリタループは参照実装として残され、テストは二つのループを同時に走らせて出力、gas、エラー、返金、ログ、状態ルートを突き合わせます。加えて、任意のバイトコードに対する差分ファジングテストがあります。

PR #35638 内の A/B ベンチマークが出した数値は次のとおりです。条件はメインネットの 2000 ブロック（ブロック番号 25677501 から 25679500）、`geth-benchmark-1` マシン、go1.27.0、各側 3 回の実行です。スループットは 391.2 MGas/s から 424.9 MGas/s へ上昇し、`newPayload` の平均所要時間は 71.6 ms から 65.1 ms へ、実行フェーズの p50 は 37.47 ms から 31.58 ms へ下がりました。2026 年 9 月 18 日時点で、この PR は GitHub 上で依然 open のままでマージされておらず、したがってこれらの数値は PR 内部のベンチマークであり、リリース版の測定値ではありません。引用する際は未検証として扱うべきです。

evmone は別の道を選びました。既定の baseline インタプリタは最も基本的な `JUMPDEST` 解析だけを行います。任意で有効にできる advanced インタプリタは間接呼び出しスレッディング（indirect call threading）を使い、ロード後の EVM プログラムを仮想命令の実装関数を指すポインタ表として表現し、基本ブロック単位で gas とスタック要件を事前計算し、ブロックの入口に到達したときに一度に検証します。代償は実行前のバイトコード解析が重くなることです。

## gas 計量：定数と動的の分離、コールド／ホットアクセスはスロット単位で課金

インタプリタはオペコードを実行する前に費用を計算しきらなければなりません。go-ethereum はこれを定数部分と動的部分に分けます。定数 gas はそのまま引き、動的 gas は処理関数がスタック、メモリ、状態に基づいて計算します。メモリ拡張の費用は必要な語数に対して超線形に増えるため、インタプリタはまずオペコードが要求するメモリサイズを算出してから課金します。

EIP-2929 以降、gas 計量と状態アクセスの層は結び付けられました。この提案は、各トランザクションについてアクセス済みアドレスとアクセス済みストレージスロットの二つの集合を維持することを求め、ストレージスロットの要素型は `Set[Tuple[Address, Bytes32]]` です。あるスロットへの初回アクセスには `COLD_SLOAD_COST`、すなわち 2100 gas を、再アクセスには `WARM_STORAGE_READ_COST`、すなわち 100 gas を、あるアドレスへの初回アクセスには `COLD_ACCOUNT_ACCESS_COST`、すなわち 2600 gas を課します。アクセス集合はトランザクションのコンテキストに属し、物理的にはクライアントの実行環境が保持するため、インタプリタは `SLOAD` や呼び出しのたびにこれを照会して更新しなければなりません。これも、インタプリタの変更を状態レイヤーから切り離して単独で行うのが難しい理由です。

ブロック単位で gas を事前計算することには観測可能な副作用があります。evmone のドキュメントは明確に述べています。基本ブロック全体の要件が前もって検査されるため、命令ごとの課金では実行されたはずの外部から見える操作が、事前課金では実行されないことがあり得ます。ドキュメントはこれをコンセンサスの問題とは見なしません。実行はハード例外で終了し、すべての効果がロールバックされるからです。しかし異なる実行トレースを生んだり、異なる例外型で終了したりする可能性があります。こうした差異はまさにテストベクタが拘束する範囲に収まります。

## メモリと呼び出しフレーム：割り当てをホットパスから追い出す

コントラクト呼び出しのたびにスタックとメモリが必要になります。EVM のスタックは 1024 項目が上限で、メモリは必要に応じて拡張され課金され、リターンデータにもバッファが必要です。これらのオブジェクトは寿命が短く生成頻度が高いため、実装は一般に毎回割り当てるのではなく再利用する方向に傾きます。go-ethereum はインタプリタの入口で `Memory` オブジェクトを一つ作り、オペコードがより大きなメモリを要求したときは語単位で拡張します。revm のコンテキスト層はアイテムプーリング（item-pooling）された `FrameStack` を保持し、索引によって呼び出しフレームを再利用します。

## プリコンパイル：ネイティブ実装と依存関係のトレードオフ

プリコンパイルコントラクトは、いくつかの固定アドレスに置かれたネイティブ実装です。実行時はホストが直接計算し、バイトコードは解釈しません。インタプリタはルーティングと gas の課金だけを担い、一部のプリコンパイルの費用は入力長にも依存します。

独立したライブラリのこの層でのトレードオフは、個別に見る価値があります。evmone のドキュメントは二つの妥協を説明しています。`ecrecover` は evmone 自身が実装しており、性能が低下します。`expmod` は既定でスタブ実装を使い、既知の入力に対してのみ正しく応答します。完全な実装を得るにはビルド時に `EVMONE_PRECOMPILES_GMP=1` を有効にし、GMP をビルド時と実行時の依存として導入する必要があります。これは一般的な問題を反映しています。プリコンパイルの正しさは任意の入力に対して成立することが要求されますが、大きな整数演算、楕円曲線の復元、Keccak ハッシュといったネイティブコードを他の実装とビット単位で一致させること自体、食い違いが潜みやすい箇所なのです。

## JIT がなぜ主流の選択にならなかったのか

EVM クライアントにおける JIT（just-in-time compilation、ジャストインタイムコンパイル）の正式な試みは一度だけありました。ethereum/evmjit は LLVM をベースに、実行時にコントラクトのコードをマシンコードへコンパイルし、クライアント内のインタプリタベースの EVM を置き換えるものでした。このリポジトリは現在アーカイブされており、README にはプロジェクトはもはや保守されておらず、重要な用途には使わないよう明記されています。

保守が停止された理由は README に説明がありませんが、技術的な条件からいくつかの障害が見えます。EVM のジャンプ先はスタック上の値から来て、コードは実行時に読み取ることができ、プログラム構造は実行時になって初めて確定するため、コンパイラが JVM のようにコンパイル時に安定した制御フローグラフを得るのは困難です。一度の呼び出しで使える計算量はブロックの gas 上限にも制約され、利益の総量には天井があります。より成熟した代替は解析を静的段階に移すことであり、EOF（EVM Object Format、EIP-7692）がまさにこの方向の試みです。コンテナ形式、静的な相対ジャンプ、スタック検証、コードセグメントとデータセグメントの分離といった項目を含みます。しかしこの路線は現在活発ではありません。EOF は 2025 年 4 月 28 日に Fusaka アップグレードの範囲から外され、EIPs PR #9703 が当日マージされましたが、これは Fusaka 段階での削除のみを対象とします。EIP-7692 自体のステータスは stagnant とされ、Glamsterdam の公式範囲リスト EIP-7773 にも含まれていません。第三者のトラッキングサイト eipsinsight の変更記録によれば、EIP-7692 は 2026 年 8 月 27 日の Glamsterdam 範囲整理で候補バケットから外されましたが、この点は第三者ソースしかありません。したがって短期的に、汎用クライアントの最適化の主戦場は依然としてインタプリタであり、コンパイル実行にはまだ着地の余地がありません。

## どのような条件でこれらの最適化は効かなくなるか

実行がもはや所要時間の主体でなくなると、利益は逓減します。上で引用した go-ethereum のベンチマークでは、実行フェーズの p50 は 37.47 ms から 31.58 ms へ下がり、ブロック全体の時間の p50 は 62.1 ms から 56.2 ms へ下がりました。同じ表で計算すると、状態の読み取り、状態のハッシュ化、コミットを合わせて総時間のおよそ三分の一を占め、さらにエンジン層のオーバーヘッドを加えると半分近くになります。インタプリタ最適化の天井はこの比率で決まります。負荷が状態アクセスとディスク I/O に偏るほど、インタプリタを最適化する限界利益は低くなります。

fork の違いにより、最適化を全体に一律には適用できません。ジャンプテーブルは fork ごとに生成され、生成的な switch も fork 間で挙動が安定したオペコードしかインライン化できません。メタデータが fork によって変わるオペコードはテーブルディスパッチの経路に残さなければならず、そうしなければ誤った定数を書いてしまいます。

意味論上の制約が最適化の境界を定めます。最適化はログの順序、トレースフックの順序、例外型、gas 計量を変えてはならず、これらはテストベクタによって固定されています。evmone の、基本ブロック単位の事前検査が早期終了を招きうるという例は、まさに最適化と観測可能なセマンティクスの間で明示的なトレードオフが必要になる箇所です。

一致性のコストは実装の数とともに上がります。実装が一つ増えるたびに、コンセンサスの境界に食い違いうる箇所が一つ増えます。テストベクタはこの面を圧縮できますが、消し去ることはできません。マルチ実装がもたらす冗長性の価値とこのコストは、同じ一つのトレードオフの両端です。

## 出典

- revm ドキュメント、Introduction：https://bluealloy.github.io/revm/
- revm ドキュメント、Architecture：https://bluealloy.github.io/revm/architecture.html
- revm、`Database` トレイト：https://docs.rs/revm/latest/revm/context/trait.Database.html
- revm、`Host` トレイト：https://docs.rs/revm-interpreter/latest/revm_interpreter/trait.Host.html
- revm リポジトリと利用者のリスト：https://github.com/bluealloy/revm
- evmone リポジトリの説明（baseline と advanced インタプリタ、プリコンパイルのトレードオフ）：https://github.com/ethereum/evmone
- evmone、Efficient gas calculation algorithm for EVM：https://github.com/ethereum/evmone/blob/master/docs/efficient_gas_calculation_algorithm.md
- EVMC、Ethereum Client-VM Connector API：https://github.com/ethereum/evmc
- Silkworm プロジェクト起源の振り返り（EVMC と CGo のインターフェースオーバーヘッド）：https://erigon.substack.com/p/staged-sync-and-short-history-of
- go-ethereum インタプリタループ：https://github.com/ethereum/go-ethereum/blob/master/core/vm/interpreter.go
- go-ethereum ジャンプテーブルの定義：https://github.com/ethereum/go-ethereum/blob/master/core/vm/jump_table.go
- go-ethereum PR #35638（生成的な switch と PGO インライン化、ベンチマーク条件を含む）：https://github.com/ethereum/go-ethereum/pull/35638
- go-ethereum リポジトリとライセンスの説明：https://github.com/ethereum/go-ethereum
- go-ethereum 状態遷移レイヤー（内在 gas、nonce 検証、返金）：https://github.com/ethereum/go-ethereum/blob/master/core/state_transition.go
- イーサリアム黄書（メモリ拡張費用関数）：https://ethereum.github.io/yellowpaper/paper.pdf
- EIP-2929、Gas cost increases for state access opcodes：https://eips.ethereum.org/EIPS/eip-2929
- execution-specs リポジトリ（規範とテストベクタ）：https://github.com/ethereum/execution-specs
- The Weld is Complete（execution-spec-tests 統合の告知）：https://steel.ethereum.foundation/blog/2025-11-04_weld_final/
- 状態テスト形式の説明：https://steel.ethereum.foundation/docs/execution-specs/running_tests/test_formats/state_test/
- テストベクタの直接実行と各クライアントの入口：https://steel.ethereum.foundation/docs/execution-specs/running_tests/consume/direct/
- ethpandaops/hive-tests（マルチクライアントの毎日一致性テスト）：https://github.com/ethpandaops/hive-tests
- イーサリアム財団のセキュリティ告知、2016-11-24 のコンセンサス欠陥：https://blog.ethereum.org/2016/11/25/security-alert-11242016-consensus-bug-geth-v1-4-19-v1-5-2
- go-ethereum v1.5.3 リリースノート：https://github.com/ethereum/go-ethereum/releases/tag/v1.5.3
- ethereum/evmjit リポジトリ（アーカイブ済み）：https://github.com/ethereum/evmjit
- EIP-7692、EVM Object Format (EOFv1) Meta：https://eips.ethereum.org/EIPS/eip-7692
- EIP-7607 による EOF 削除の説明（EIPs PR #9703）：https://github.com/ethereum/EIPs/pull/9703
- EIP-7773、Hardfork Meta - Glamsterdam（公式の範囲リスト、EOF を含まない）：https://eips.ethereum.org/EIPS/eip-7773
- Optimism ドキュメント、op-geth サポート終了の告知：https://docs.optimism.io/notices/op-geth-deprecation
- 第三者のトラッキングサイト eipsinsight、Glamsterdam アップグレードの範囲と変更記録：https://eipsinsight.com/upgrade/glamsterdam

## 関連記事

- 関連：[「Bitroot マルチエンジン並列実行：スケジューリング、シャーディング、コンフリクト面」](/ja/blog/bitroot-evm)
- 関連：[「Bitroot 並列 EVM アーキテクチャ概要：コンセンサス、実行、状態がどう協調するか」](/ja/blog/bitrootevm)
- 互換性の延伸：[「EVM 互換とは実際に何を意味するのか：バイトコード、プリコンパイル、JSON-RPC、ツールチェーン」](/ja/blog/evm-compatibility-explained)
