---
id: 21
title: EVM 到底是什麼：從分散式帳本到分散式狀態機
slug: 0-1-what-is-evm
date: 2026/09/18
summary: 把以太坊當成一本記錄轉帳的帳本，會漏掉它最關鍵的部分：帳戶儲存裡可任意讀寫的變數。依據黃皮書的狀態轉移函數 Υ(σ, T)，以太坊是一個分散式狀態機，EVM 是這套狀態轉移的執行規範。全網逐位重放又給執行語意帶來了一組硬約束。
keywords: EVM,世界狀態,狀態轉移函數,確定性執行,Yellow Paper
heroImage: /images/community-bg.png
---

在鏈上轉 100 個 USDT（ERC-20 代幣）不會新增一條「甲付給乙 100 USDT」的條目。實際發生的是合約裡 `balanceOf` 映射的取值被改寫：呼叫方減少 100，接收方增加 100；這筆改寫最終落在哪些儲存槽由合約寫法與編譯器決定，協議只約束 `balanceOf` 回傳什麼。同時，一筆事件日誌（log）被追加進交易收據，日誌是給鏈下索引器消費的副產品，餘額的真相只在儲存的當前值裡。

帳本類比在這裡失效。帳本記錄已經發生的事，讀者累加歷史得出結論；以太坊保存的是一個會被持續覆蓋的「當前世界」，交易只把它推向下一版。要回答「EVM 到底是什麼」，先要把這個「當前世界」定義清楚，再談誰負責改寫它。

## 比特幣的帳本模型夠用，因為它的狀態結構極其受限

「帳本」對比特幣是準確的描述。一筆交易的輸入引用此前某筆交易的輸出，輸出在被後續輸入花掉之前，保持為未花費交易輸出（unspent transaction output，UTXO）。一個 UTXO 只能被花費一次，錢包顯示的餘額其實是若干 UTXO 的加總。

比特幣並非沒有狀態，UTXO 集合就是狀態。但這個狀態的結構非常受限。它是一堆只能整體建立、整體花費的「硬幣」，沒有通用的鍵值儲存，腳本也不保存跨交易存在的變數。於是「帳本加未花費輸出集合」足以完整描述整個系統：驗證一筆交易只需要檢查被引用的輸出是否存在、條件腳本是否被滿足，不必查詢任何「合約內部變數」。

以太坊要支援合約持有一個可任意讀寫的映射，這個映射就不能再當作實作細節。ERC-20 轉帳是否有效，取決於合約儲存中呼叫者的餘額槽當前是幾。狀態因此從「驗證時順帶維護的集合」升級為協議必須作出承諾的一等物件。

## 世界狀態：把「此刻的一切」壓成 32 位元組

以太坊黃皮書把世界狀態（world state）定義為一個從地址到帳戶的映射。每個帳戶是一個四元組：

- nonce：該地址已發出的交易數，或該合約帳戶已建立的合約數；
- balance：以 wei 計價的餘額；
- storageRoot：該帳戶儲存內容的 Merkle Patricia Trie 根雜湊；
- codeHash：該帳戶程式碼的 Keccak-256 雜湊，程式碼本身按這個雜湊存放在狀態資料庫裡。

這裡要區分兩樣東西。storageRoot 承諾的是帳戶自己的鍵值空間，合約作者宣告的狀態變數最終都落在這裡；codeHash 指向的是程式，部署之後不會被某筆交易改寫。把「資料」與「程式碼」用兩個欄位分別承諾，是後面討論讀寫集與衝突偵測時反覆要用的前提（同系列的 0.5《三類儲存別搞混：Memory、Storage 與 Transient Storage》會展開這三類儲存各自的歸屬與壽命）。

所有帳戶再組織成一棵全局的 Merkle Patricia Trie，其根雜湊就是 stateRoot，被寫進區塊頭。也就是說，「此刻整個以太坊長什麼樣」可以被壓縮成 32 個位元組，全網只需要對這 32 個位元組達成共識。

## 鏈上保存的是狀態根，狀態本身留在節點本地

這裡有一層常被含糊處理的事實：黃皮書明確寫道，世界狀態並不儲存在區塊鏈上，它由每個節點各自維護，鏈上只保留 stateRoot 這個承諾。交易與收據會被完整發布，狀態不會。

這個分工決定了很多使用上的邊界。想知道某個帳戶此刻的餘額，只有兩條路：自己重放全部歷史交易算出狀態，或者向某個節點索要一份可以在 stateRoot 下驗證的 Merkle 證明。輕客戶端走的是第二條路，它不需要保存全部狀態，但需要有人提供證明，而且證明只能說明「在某個 stateRoot 之下這個值是什麼」，無法說明這個 stateRoot 是最新的。後者是共識層的職責。

同樣由此可以理解歸檔節點與裁剪節點的差別。歷史狀態持續累積，保存全部歷史狀態的節點磁碟成本持續成長；只保留近期狀態的節點佔用更小、啟動更快，代價是無法回答任意歷史區塊上的狀態查詢。所謂「鏈上資料」在工程上要拆成兩件事：交易與收據的可用性，狀態根的承諾。混在一起談，後面評估資料可用性與狀態膨脹時會失去分辨力。

## 狀態轉移函數：以太坊規定的是一段函式

黃皮書用一行公式定義交易級語意，記作：

`σ_{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）是 Υ 的具體執行規範，內容包括一套指令語意、一套 gas 計量規則、一套異常與終止語意。合約程式碼是一串位元組，只有在被交易或其他合約以訊息呼叫（message call）觸發時才進入執行。

EVM 不等於以太坊協議。共識、資料可用性、結算各自是另一層系統，擴容地圖一文對此有專門拆解。EVM 只回答一件事：面對一段位元組碼和一份輸入，如何確定性地算出狀態變更與回傳值。它也決定這個計算要花多少錢，而 gas 計價規則本身就是共識的一部分，可以被硬分叉修改，EIP-2929 把首次存取帳戶與儲存槽的定價顯著抬高就是例子。

還有一處容易混淆的地方：EVM 的機器模型不只有「堆疊」。每次執行上下文都有堆疊（stack）、記憶體（memory）、程式計數器（program counter，PC）與 gas 計數器，合約的持久變數則透過 SLOAD/SSTORE 落到帳戶儲存裡。堆疊、記憶體與執行迴圈是 0.4 的主題，三類儲存的邊界是 0.5 的主題。

## 全網重放：為什麼結果必須逐位一致

以太坊沒有中心執行者。每個全節點獨立執行同一批交易，各自算出 stateRoot，再透過共識比較這個值。任何執行細節上的分歧都會直接變成共識失敗，所以規範必須細到位元組。

由此推出幾條硬約束。浮點運算沒有進入指令集，只有基於 256 位字長整數的算術。牆鐘、行程 ID、真實隨機數這類外部輸入不能出現在共識路徑上；合約能讀到的時間戳與區塊號來自區塊頭，是被共識固定的值。

計算還必須可計量且必然終止，否則無法在有限區塊裡安全執行任意程式碼。gas 是把「停機問題」改寫成「花光預算就停」的辦法，它的計量常數也因此屬於共識參數而不是效能調校項。一筆普通轉帳的固有成本是 21000 gas，calldata 裡每個零位元組計 4 gas、非零位元組計 16 gas，這些數字由黃皮書的費用表固定。改動它們與改動區塊大小屬於同一類協議變更，需要硬分叉或驗證者訊號，不能由某個客戶端自行決定。

確定性還有一個容易被忽略的後果：規範不允許存在「未定義行為」。黃皮書寫明 DIV 在除數為 0 時回傳 0，而不拋異常。這個選擇的理由，是讓所有客戶端在同一份非法輸入上得到同一個結果，與語言設計偏好無關。客戶端的單元測試與 execution-spec-tests 規範測試套件，就是用來鎖住這類邊界行為的。多個獨立客戶端（geth、revm、evmone 等）並存，本身也是確定性的校驗手段：一旦某個實作對邊緣情形理解不同，鏈就會分叉，因此分歧必須在測試階段被發現。

## 確定性與可預測是兩回事

需要區分兩個層次的不確定性。執行層是確定的：同樣的位元組碼、同樣的輸入、同樣的狀態，必然得到同樣的結果。交易能拿到什麼結果卻不只由執行層決定。同一筆交易放在區塊的不同位置，前序狀態不同，結果就可能不同。搶跑與三明治的收益來自排序權，執行層本身仍是確定的。

這個區分在評估平行執行方案時很關鍵。平行改造要守住的是「執行結果與某個串列順序等價」；至於「結果與交易提交時間無關」，這個目標從來就不成立。

## EVM 不是凍結的規格

把 EVM 當成固定標準會帶來相容性誤判。規格隨硬分叉演進：PUSH0（EIP-3855，Shanghai）為堆疊機新增一條推入常量 0 的指令；EIP-2929 重新定價了冷熱存取；交易層引入了單筆交易 16,777,216 gas 的上限（EIP-7825，隨 Fusaka 上線）；EIP-7935 把客戶端預設的區塊 gas 上限提到 60M。

因此「相容 EVM」這句話必須帶上分叉高度與具體範圍才有意義。位元組碼、預編譯合約、JSON-RPC 與工具鏈各自相容到什麼程度，屬於延伸閱讀裡那篇的範圍。

## 這套模型在什麼條件下會成為負擔

以狀態為中心的設計買到了通用計算，代價寫在三個地方。

全節點必須長期保存一份隨時可讀的當前狀態，其規模隨帳戶與儲存條目成長，持續抬高硬體門檻。

新節點要自行算出 stateRoot，就必須重放歷史交易或依賴快照同步，驗證成本與歷史長度掛鉤。

由於每筆交易都可能觸碰全局狀態的任意位置，經典實作只能逐筆串列執行，這是後面所有平行執行討論要處理的原始約束。

反過來說，帳本類比的失效也不是絕對的。審計、索引、對帳這類場景仍然按事件流處理資料，因為收據裡的日誌本來就是為鏈下消費設計的。問題只在於不要把這種視角當成協議模型。

## 資料來源

- Ethereum Yellow Paper（Upsilon、Pi、世界狀態的存放方式、帳戶四元組、DIV 語意、gas 費用表）：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 gas）：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

## 延伸閱讀

- 相關閱讀：[《EVM 相容意味著什麼：位元組碼、預編譯、JSON-RPC 與工具鏈》](/zh-Hant/blog/evm-compatibility-explained)
- 相關閱讀：[《區塊鏈擴容地圖：L1 平行、L2、分片與 DA 各自解決什麼》](/zh-Hant/blog/blockchain-scaling-map)
- 下一篇：[《為什麼單執行緒 EVM 會卡住 TPS：從歷史壅塞到執行模型》](/zh-Hant/blog/evm-single-thread-bottleneck)
