---
id: 25
title: "世界狀態與 Merkle Patricia Trie：stateRoot 如何承諾一切"
slug: 0-8-world-state-mpt
date: 2026/09/27
summary: 區塊頭裡的 stateRoot 只有 32 位元組，卻綁定了全網帳戶、餘額與合約儲存。下面拆開 Merkle Patricia Trie 的節點結構與帳戶下掛儲存字典樹的設計，說明 Merkle 證明如何支撐鏈下驗證，以及狀態膨脹的成本落到誰身上。
keywords: Merkle Patricia Trie,stateRoot,世界狀態,狀態膨脹,Merkle證明
heroImage: /images/articles/photos/0-8-world-state-mpt.jpg
---

以太坊區塊頭裡有一個 32 位元組的欄位 stateRoot。它不含任何帳戶資料，卻聲稱能把全網所有帳戶的餘額、nonce、合約程式碼與合約儲存一起固定住：只要這個雜湊對得上，你就知道這份狀態是哪一個版本。這個欄位是 Merkle Patricia Trie（MPT，字典樹）的根雜湊，也是執行層對世界狀態給出的唯一承諾。

要理解這份承諾有多重，需要拆開三件事：樹的結構、根雜湊為什麼能綁定全部狀態，以及維護這棵樹要付出什麼代價。第三件事是後面討論狀態分片與狀態樹換代的起點。

## 一個區塊頭裡裝著三棵字典樹

以太坊區塊頭帶有三個描述執行結果的字典樹根：stateRoot（世界狀態字典樹）、transactionsRoot（交易字典樹）與 receiptsRoot（收據字典樹）。黃皮書把它們記作 H_r、H_t、H_e。上海升級之後，區塊頭還多了描述提款列表的 withdrawalsRoot（H_w），它的結構與交易字典樹類似，只是每條提款的資料量小、每塊條數少。黃皮書把「H_r 等於按順序執行完區塊內全部交易、再執行完全部提款之後得到的狀態根」列為區塊頭的有效性條件之一。stateRoot 描述的是累計狀態，另外幾個根只描述本區塊。

| 字典樹 | 鍵 | 值 | 生命週期 |
|------|----|----|----------|
| 世界狀態字典樹 | keccak256(帳戶位址) | RLP 編碼的帳戶四元組 | 跨區塊持續更新 |
| 交易字典樹 | rlp(交易在區塊內的序號) | rlp(交易)；型別化交易為型別前綴拼接編碼後的交易 | 每塊一棵，之後不再改動 |
| 收據字典樹 | rlp(交易在區塊內的序號) | 型別化收據，或 rlp([status, cumulativeGasUsed, logsBloom, logs]) | 每塊一棵，之後不再改動 |

交易字典樹與收據字典樹的葉子數與本區塊的交易數成正比，因此驗證某筆交易是否被打包，成本與鏈的長度無關，只與本區塊規模有關。世界狀態字典樹全域唯一，規模隨全網帳戶數與儲存槽數增長。區塊頭裡的 logsBloom 由收據中的日誌位址與 topic 聚合而成，供查詢做機率過濾，它是索引加速器，不是承諾。

## 位址經過雜湊才進字典樹，帳戶是四元組

世界狀態字典樹的鍵是 keccak256(帳戶位址)，值是 RLP 編碼的帳戶四元組 [nonce, balance, storageRoot, codeHash]。位址本身不進字典樹，進去的是它的 256 位雜湊：32 位元組對應 64 個 nibble（半位元組），也就是從根到葉最多 64 層。

四個欄位各有明確語意。nonce 是帳戶發出的交易數，對合約帳戶還包括它建立過的合約數。balance 是以 wei 計的餘額。codeHash 是帳戶程式碼的 keccak256，程式碼本體按雜湊存放在狀態資料庫裡，因此位元組碼相同的合約共享同一份資料。storageRoot 是另一棵字典樹的根，那棵樹只屬於這個帳戶。

結構於是呈現為樹中樹：世界狀態字典樹的葉節點裡裝著某個帳戶的 storageRoot，這個根指向該帳戶自己的儲存字典樹。儲存字典樹的鍵是 keccak256(32 位元組槽號)，值是該槽內容（256 位）的 RLP 編碼。槽值為零在規範裡等同於該槽不存在，把某個槽寫回 0 在狀態層的效果是刪除。

鍵先取雜湊讓樹的形狀由雜湊分布決定，攻擊者無法透過挑選鍵來塑造它。若鍵直接用位址或槽號，攻擊者可以挑出共享長前綴的一批鍵，把某棵子樹壓成極深的鏈，放大存取與證明的成本；keccak256 把鍵均勻打散後，這類構造不再可行。EIP-8297 草案在論證新樹結構時，也把由雜湊決定位置、從而保持平衡這一點列為設計依據。代價是槽號不可逆，合約無法從儲存字典樹反推自己寫過哪些槽，外部也只能按已知槽號逐個查詢。

兩個空值常量值得記下來，後面的證明與驗證都會用到：空字典樹的根，以及空帳戶程式碼的雜湊。

```python
# pip install eth-utils rlp
import rlp
from eth_utils import keccak

# 空字典樹的根：對 RLP 編碼的空位元組串取 keccak
print(keccak(rlp.encode(b"")).hex())
# 56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421

# 空帳戶程式碼的雜湊
print(keccak(b"").hex())
# c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470
```

## 三類節點與兩位元標誌：64 層路徑如何被壓短

MPT 是 16 叉（hexary）樹，不是二元樹。黃皮書附錄 D 定義了三類節點與一個空節點。branch 節點有 17 項，前 16 項對應下一個 nibble 的 16 種取值，第 17 項留給鍵在此結束的情形。extension 節點是 [encodedPath, key] 兩項，跳過至少兩個 nibble 的共同前綴。leaf 節點是 [encodedPath, value] 兩項，承載鍵的剩餘部分與終值。空節點用空位元組串表示。規範裡還有一條容易忽略的不變量：不允許存在只有一個非零項的 branch 節點。正因如此，同一組鍵值對只有一種編碼方式，根雜湊才有確定取值。

路徑按 nibble 而不是位元組組織，於是需要一個緊湊編碼，把路徑長度的奇偶與節點類型這兩件事塞進首 nibble：

| 首 nibble | 二進位 | 節點類型 | 路徑長度 |
|-----------|--------|----------|----------|
| 0 | 0000 | extension | 偶數 |
| 1 | 0001 | extension | 奇數 |
| 2 | 0010 | leaf | 偶數 |
| 3 | 0011 | leaf | 奇數 |

偶數長度時首 nibble 之後再補一個 0 nibble，保證整個編碼的 nibble 數為偶數，可以落成位元組串。ethereum.org 給出的參考實作如下（回傳值在此整理為 bytes）：

```python
def compact_encode(hexarray):
    # 葉節點的路徑以 16 結尾，偵測到就剝掉並置 leaf 標誌
    term = 1 if hexarray[-1] == 16 else 0
    if term:
        hexarray = hexarray[:-1]
    oddlen = len(hexarray) % 2
    flags = 2 * term + oddlen                 # 首 nibble 同時編碼類型與奇偶
    if oddlen:
        hexarray = [flags] + hexarray
    else:
        hexarray = [flags] + [0] + hexarray   # 偶數長度補一個 0 nibble
    return bytes(16 * hexarray[i] + hexarray[i + 1] for i in range(0, len(hexarray), 2))
```

路徑壓縮解釋了為什麼 64 nibble 的理論深度在實際主網上遠未用滿：EIP-8297 草案在動機部分估計，帳戶字典樹當前的最大深度約為 12 層。這是草案的自述估算，不是主網實測資料集；層數越淺，證明越短。

節點之間如何引用，由 32 位元組規則決定。子節點 RLP 編碼後不足 32 位元組，內容直接內聯進父節點；達到或超過 32 位元組，父節點只寫入 keccak(RLP(子節點)) 作為引用。這條規則省掉大量一次性的小節點讀取，代價是驗證證明時必須按實際編碼形態重組節點，而不能假定每一層都是一次獨立的資料庫查詢。

## stateRoot 是對全部狀態的一個承諾

把整個結構折疊成一個雜湊的動作只有一行：對根節點的 RLP 編碼取 keccak256。黃皮書在描述世界狀態時給出的理由很直接，根節點在密碼學上依賴其內部全部資料，因此這個雜湊可以作為整個系統狀態的安全識別。

承諾的強度來自引用方式。父節點引用子節點用的是子節點的雜湊，任何一個葉子的任何一位變化，都會改變它所在的節點，再逐層改變到根。建立在 keccak256 抗碰撞性之上，找到兩份不同狀態卻給出同一個根，等同於找到雜湊碰撞。

由此得到三個性質。根雜湊唯一確定一份鍵值集合。舊狀態可以按根取回，因為節點按內容定址、結構不可變，只要節點還在資料庫裡，知道舊根就能還原當時的狀態。驗證者也能沿路徑重建承諾，路徑上的兄弟雜湊足以讓他從某個葉子重算到根，黃皮書附錄 D 把證明的空間需求記為 O(log N)。

區塊頭裡的定義還限定了時點：stateRoot 是「執行完區塊內全部交易與提款、並套用完最終處理之後」的狀態根。它承諾的是這一刻的鍵值集合，不承諾這份狀態如何變成現在這樣，不同的歷史可以收斂到同一個狀態，同一個根也可能在連續多個區塊裡重複出現。它也不承諾狀態的可用性。根雜湊本身不含任何節點資料，要驗證某個帳戶，必須另外取得路徑上的節點。

## 從根到一個帳戶：Merkle 證明怎麼用

帳戶證明是一個節點列表，從根節點開始，沿 keccak256(位址) 的 nibble 逐層向下：每一層要麼命中 branch 的對應項，要麼命中 extension 或 leaf 的路徑前綴。驗證者的動作很機械，對第一個節點重算 keccak(RLP(節點))，確認等於已知的 stateRoot；解碼後按 nibble 找到下一個引用，重複到葉子；葉子裡的值就是 RLP 編碼的帳戶。證明不存在的帳戶同樣可行，關鍵是把路徑上最後一個匹配節點交出來：若它是 branch，對應分支為空；若它是 leaf，它與目標路徑在某個 nibble 上分叉。儲存證明要走第二遍，起點從 stateRoot 換成帳戶裡的 storageRoot。

EIP-1186 把這個過程標準化成 eth_getProof，下面是一個請求範例（位址與槽號可替換）：

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "eth_getProof",
  "params": [
    "0x7f0d15c7faae65896648c8273b6d7e43f58fa842",
    ["0x0000000000000000000000000000000000000000000000000000000000000000"],
    "latest"
  ]
}
```

回傳值裡的 accountProof 是從 stateRoot 出發的節點陣列，storageProof 裡每條記錄則從該帳戶的 storageRoot 出發。兩者都只是資料，可信與否由驗證者自己重算決定。

Helios 一類輕客戶端就按這個思路工作：共識層輕客戶端先從信標鏈驗證區塊頭，取得經過認證的執行層 payload 與 stateRoot，再向任意一個不信任的 RPC 索要 eth_getProof，最後在本機完成 MPT 驗證。EIP-1186 的動機一節也提到，這類證明讓物聯網裝置與行動應用只憑一個可信區塊雜湊，就能驗證來自不可信來源的帳戶與儲存資料。

## 承諾的邊界：證明、舊根與輕客戶端

證明的能力有清晰上限。它只覆蓋被證明的那條路徑，不能推出狀態的其他部分；證明體積隨路徑深度與分支寬度增長。EIP-8297 草案用一組範例估算說明這個量級：按帳戶字典樹最大深度約 12 層計算，一條帳戶分支需要 12 層、每層 15 個兄弟雜湊，合計 15 × 32 × 12 = 5760 位元組；若把 60M Gas 全部用於觸碰大量不同合約程式碼的單個位元組，且程式碼不分塊，草案估出的證明量約為 1.8 GB。這些都是草案的自述估算，未經主網實測複核，方向性結論可以採信，具體數值不宜當作基準。這也是 MPT 被認為對有效性證明不友好的原因：RLP 編碼、Keccak 雜湊、樹中樹結構，以及程式碼不能按段證明。

舊根的證明不是永久可得。承諾留在區塊頭裡，能不能兌現取決於客戶端如何存放狀態。Geth 自 v1.16 起的路徑式歸檔保存扁平狀態的歷史差分，讀得到歷史狀態，但 v1.16.x 上無法為舊根生成 Merkle 證明，v1.17 起才可以透過顯式保留字典樹歷史來支援歷史證明；傳統的雜湊式歸檔保留歷史字典樹節點，可以為任意舊根出證明。磁碟代價見下一節。承諾的語意屬於共識，履行承諾的方式屬於實作選擇。

執行層輕客戶端也經歷過一次收縮。主網的 LES 協定在合併之後長期無法可靠工作，Geth 在 2023 年底移除了相關程式碼。今天的鏈下驗證更多以「共識層認證根、執行層提供證明」的組合出現，而共識層輕客戶端驗證的是信標鏈區塊頭，本身不會重新執行交易。

## 狀態膨脹：承諾的帳單落在磁碟上

stateRoot 的表達能力與狀態規模無關，維護它的成本與狀態規模正相關。每次狀態更新都要重寫從葉子到根的整條路徑，狀態越深越寬，單位交易需要的讀寫越多；新節點要追上鏈，也必須把這份狀態完整拉下來。

以太坊研究論壇 2025 年 11 月的一篇分析給出了一組量級。該文稱，2025 年 5 月一個只承載狀態的 Geth 節點，未壓縮資料庫約 340 GiB；Gas 上限由 30M 上調至 36M 後，每日新增狀態的中位數從約 102 MiB 翻到約 205 MiB。bloatnet 專案頁面把 650 GB 標為臨界規模，稱接近這一規模時狀態存取時間約增加 40%、記憶體佔用與同步時間明顯變差；該頁面沒有給出可重現的基準資料，這裡的表述按專案自述口徑轉述，且它用的是 GB 而非 GiB。該論壇分析隨後按保守、基準、激進三條 Gas 上限路徑外推（2027 年年中分別升到 200M、400M、700M），得出 2027 年年中總狀態規模落在 686 GiB 到 1.08 TiB。這是情境外推，不是既成事實：取值取決於客戶端實作、壓縮與剪枝策略，以及 Gas 上限的實際走勢。

落到節點營運上，ethereum.org 彙總的 Geth 全節點（snap 同步）磁碟需求為 500 GB 以上；歸檔節點按 Geth 官方口徑是路徑式約 2 TB、保留歷史字典樹資料約 6.5 TB、雜湊式超過 20 TB，而 ethereum.org 彙總的跨客戶端歸檔需求為 3 TB 到 12 TB 以上。這些數字依賴客戶端實作、壓縮方式、剪枝策略與 Gas 上限，跨實作直接比較沒有意義；狀態大小也不等於鏈上歷史大小，歷史交易與收據是另一筆帳。

以太坊主網的狀態只增不減，帳戶與儲存槽一旦寫入就永久佔用空間，沒有狀態到期或狀態租金的機制，壓力因此單向累積。緩解方向大致有兩類：更換樹結構，例如長期討論的 Verkle 樹，以及仍處於草案階段的分區二進位樹（Partitioned Binary Tree，EIP-8297 草案）；或者把狀態切開，讓單個節點只維護其中一部分。後者是後續文章的主題，這裡只交代壓力的來源。槽位在合約層如何排布，見 0.19《Storage Layout》。

## 資料來源

- [Ethereum Yellow Paper](https://ethereum.github.io/yellowpaper/paper.pdf)：第 2 節的狀態轉移函數、第 4 章的區塊頭欄位與整體有效性（stateRoot 的定義與驗證條件）、附錄 D 的 MPT 節點定義、hex-prefix 編碼、32 位元組內聯規則與 D.1 的 O(log N) 證明空間。
- [ethereum.org: Merkle Patricia Trie](https://ethereum.org/developers/docs/data-structures-and-encoding/patricia-merkle-trie/)：節點類型、緊湊編碼參考實作、三棵字典樹的鍵值定義、交易與收據的值編碼。
- [ethereum.org: Ethereum accounts](https://ethereum.org/developers/docs/accounts/)：storageRoot 與 codeHash 的官方表述。
- [EIP-1186: RPC-Method to get Merkle Proofs](https://eips.ethereum.org/EIPS/eip-1186)：eth_getProof 的欄位、不存在的證明方式與使用情境；官方範例中的空 storageHash（0x56e81f…）與空 codeHash（0xc5d246…）也用於核對正文程式碼區塊裡的兩個空值常量。
- [EIP-8297: Partitioned Binary Tree](https://eips.ethereum.org/EIPS/eip-8297)（Draft，建立於 2026-06-11，雜湊函式尚未定稿）：帳戶字典樹最大深度約 12 層、分支證明 5760 位元組、最壞情況約 1.8 GB 的證明量，以及 MPT 對有效性證明不友好的原因。正文中這幾項均標註為該草案的自述估算。
- [Ethereum Research: State growth scenarios and the impact of repricings](https://ethresear.ch/t/state-growth-scenarios-and-the-impact-of-repricings/23476)（2025-11-19）：340 GiB 狀態規模、102 MiB 到 205 MiB 的每日新增，以及按三條 Gas 上限路徑做的 2027 年情境外推。該文屬情境分析，非既成事實。
- [Bloatnet Initiative](https://cperezz.github.io/bloatnet-website/)：650 GB 臨界規模與狀態存取時間約 40% 增幅的專案自述口徑，頁面未附可重現的基準資料。
- [go-ethereum: Archive mode](https://geth.ethereum.org/docs/fundamentals/archive)：路徑式歸檔的 2 TB 與 6.5 TB 口徑、雜湊式歸檔可超 20 TB，以及 v1.16.x 與 v1.17 在歷史證明支援上的差異。
- [ethereum.org: Ethereum archive node](https://ethereum.org/developers/docs/nodes-and-clients/archive-nodes/) 與 [Spin up your own Ethereum node](https://ethereum.org/developers/docs/nodes-and-clients/run-a-node/)：跨客戶端的歸檔與全節點磁碟需求區間（歸檔 3 TB 到 12 TB 以上；Geth snap 同步 500 GB 以上）。
- [go-ethereum PR #28586](https://github.com/ethereum/go-ethereum/pull/28586)：移除 LES 及相關輕客戶端程式碼。
- [a16z crypto: Building Helios](https://a16zcrypto.com/posts/article/building-helios-ethereum-light-client/)：共識層認證 stateRoot、執行層用 eth_getProof 做本機驗證的輕客戶端路徑。

## 延伸閱讀

- 相關閱讀：[《去中心化與效能的張力：驗證者門檻、硬體與地理分布》](/zh-Hant/blog/decentralization-performance-tradeoff)
- 相關閱讀：[《Bitroot 多引擎平行執行設計：排程、分片與衝突面》](/zh-Hant/blog/bitroot-evm)
- 下一篇：[《為什麼單執行緒 EVM 會卡住 TPS：從歷史壅塞到執行模型》](/zh-Hant/blog/evm-single-thread-bottleneck)
