執行客戶端(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