---
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 價格由哪條規則給出，它接收位元組碼、呼叫資料、執行環境與 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。它實作 EVMC（Ethereum Client-VM Connector API），該介面被定義為 EVM 與客戶端之間的低層 ABI，客戶端側規定 EVM 存取環境與狀態的方式。因為邊界是 C ABI，evmone 可以被其他語言寫的客戶端載入；Erigon 的回顧提到，它透過 EVMC 把 evmone 接入了自己的執行層，並額外包了一層 C++ 程式碼來降低介面開銷。

## 三種實作的定位差異

| 實作 | 語言與授權 | 形態 | 狀態介面 | 典型重用方式 |
|------|------------|------|----------|--------------|
| go-ethereum `core/vm` | Go，函式庫程式碼 LGPL-3.0，`cmd/` 下為 GPL-3.0 | 客戶端內建解譯器 | `StateDB` | 被 fork 為 L2 執行客戶端，例如 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` 與按分叉劃分的期望結果 `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。

測試的邊界同樣要說清楚。它只能覆蓋已經被寫下來的行為，每引入一個分叉，就多出一段尚未被向量固定的視窗；它約束的是可觀測語意，不是效能，也不是內部結構。

## 指令分派：跳表、生成式 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 多引擎平行執行設計：排程、分片與衝突面》](/zh-Hant/blog/bitroot-evm)
- 相關閱讀：[《Bitroot 平行 EVM 架構概覽：共識、執行與狀態如何協同》](/zh-Hant/blog/bitrootevm)
- 相容性延伸：[《EVM 相容意味著什麼：位元組碼、預編譯、JSON-RPC 與工具鏈》](/zh-Hant/blog/evm-compatibility-explained)
