00 · Orientation
先把“三棵树”分开
它们复用同一种 MPT 骨架,但生命周期、key、value 与根所在位置都不同。
读到 “State Trie、Storage Trie、Transaction Trie” 时,最危险的做法是把它们想成 一棵巨大目录树的三个文件夹。更准确的模型是:三类独立的键值承诺结构, 其中 State Trie 的账户值会引用某个账户自己的 Storage Trie 根。
由 State Trie 回答。每执行一个区块,客户端从旧状态导出新状态,并得到新的 stateRoot。
由该账户独立的 Storage Trie 回答。它的根被装进 State Trie 的账户记录。
由该区块的 Transaction Trie 回答。区块确定后,这棵树不再更新。
状态结果进入新 stateRoot;每笔交易的执行收据另有 Receipts Trie 与 receiptsRoot。
MPT 是确定性的、可验证的键值映射承诺。相同键值集合必得相同根; 改动任何被承诺的键、值或结构,沿途哈希都会改变,最终得到不同根。
01 · Commitment
为什么不直接保存一张清单?
因为节点不只要“读到数据”,还要能在不信任数据提供者时验证它。
假设服务器告诉你:“地址 A 的余额是 12 ETH。”普通数据库可以很快返回 12,
但这个答案本身不能证明服务器没有说谎。若你已经信任某区块头里的
stateRoot,服务器再提供从根到 A 的少量 MPT 节点,你就能在本地重算并验证。
把 key 拆成符号序列,沿路径逐步找到 value。
把没有分叉的一长段合并,避免许多只有一个孩子的空层。
父节点引用子节点内容,所有改变最终传导到唯一根。
四个能力,正好对应节点的四个需要
- 查找:给定 key,能走到对应 value。
- 更新:只重建受影响路径,其余不可变节点可复用。
- 证明:只提供路径上的节点,就能对可信根验证存在或不存在。
- 确定性:不同客户端处理相同键值后,必须算出同一个根。
32 字节根不能还原整棵树。验证者仍需要叶子与路径节点;根的作用是让这些数据 不能在不被发现的情况下被替换。
02 · Anatomy
MPT 的路径与三种节点
先把 byte 拆成 nibble,再让 Branch 分叉、Extension 跳过共同前缀、Leaf 收尾。
一步不是一个字节,而是半个字节
nibble 是 4 bit,可表示 0…f 共 16 种值。
一个 byte,例如 0xa7,会拆成两个 nibble:a 与 7。
所以 32 字节哈希会形成 64 步的候选路径。
[child₀ … child₁₅, value]。下一个 nibble 是几,就选第几个 child;第 17 项供“key 恰在此结束”时存值。
[encodedPath, childRef]。把所有后代共同拥有、且中途没有分叉的一长段路径压成一次跳跃。
[encodedPath, value]。确认剩余路径完全匹配后,返回最终 value;它必须能与 Extension 区分。
Hex-prefix:两 bit 标记两件事
两项节点的路径要被装回 bytes。首个 flag nibble 同时记录: 它是 Leaf 还是 Extension,以及剩余 nibble 数为奇数还是偶数。
| Flag | 节点类型 | 路径奇偶 | 首部含义 |
|---|---|---|---|
| 0 | Extension | 偶数 | 0 + padding 0 |
| 1 | Extension | 奇数 | 1 + first nibble |
| 2 | Leaf | 偶数 | 2 + padding 0 |
| 3 | Leaf | 奇数 | 3 + first nibble |
flag = 2 × isLeaf + isOdd。例如剩余路径 [a,b,c]:
Extension 编为 0x1abc;Leaf 编为 0x3abc。
为什么有时父节点存哈希,有时直接嵌入?
节点先做 RLP 编码。若 RLP(node) 长度小于 32 字节,
父节点直接嵌入它;若长度大于等于 32 字节,父节点存
Keccak256(RLP(node))。短节点直接放进去,比再放一个 32 字节哈希更省空间。
03 · State Trie
全局账户状态的总索引
路径来自地址哈希,叶子是四项账户记录;storageRoot 让“树中有树”成为可能。
0x71…a9
0xa7·3f·09·…
[nonce,balance,storageRoot,codeHash]
stateRoot =
MPT_ROOT({
Keccak256(address20)
↦ RLP([nonce, balance, storageRoot, codeHash])
})
0x074.2 ETH0x6b…e10xc5…70四个字段分别承诺什么?
- nonce:EOA 已发送交易数,或合约创建计数相关状态。
- balance:该账户持有的 wei 数量。
- storageRoot:该账户独立 Storage Trie 的根。
- codeHash:账户 EVM 字节码的 Keccak 哈希。
合约代码正文不在账户叶子里,也不在 Storage Trie 里。叶子只存
codeHash;客户端按该哈希在代码数据库中另存字节码。没有代码、也没有
EIP-7702 delegation designation 的传统 EOA,其空代码哈希是
Keccak256(""),不是空 Trie 根。EIP-7702 之后,EOA 地址可以保存一段
0xef0100 || delegateAddress 委托指示器,此时它的
codeHash 也会相应改变。
04 · Storage Trie
每个账户自己的持久化存储
逻辑 slot 先确定,再经一次 Keccak 变成 MPT 路径;零值默认不占叶子。
EVM 把合约持久化 storage 看成 2²⁵⁶ 个 32 字节槽位。
Storage Trie 不是一棵全网共用树:每个账户都有自己的独立 storage trie,
空账户使用统一的空 Trie 根。
pad32(1)
0xb1·0e·2d·…
900 → 0x820384
storageRoot(account) =
MPT_ROOT({
Keccak256(slot32)
↦ RLP(value)
| value ≠ 0
})
Mapping 为什么看起来“哈希了两次”?
这是两个不同层级。Solidity 先把高级语言变量映射到 EVM 的逻辑槽位; Storage Trie 再把这个 32 字节槽位哈希成树路径。
// balances 位于声明槽 p,查询 balances[user]
logicalSlot = Keccak256(pad32(user) || pad32(p))
triePath = nibbles(Keccak256(bytes32(logicalSlot)))
第一次 Keccak 来自 Solidity storage layout,用来求 mapping 元素的逻辑槽; 第二次 Keccak 来自 secured Storage Trie,用来均匀化 MPT 路径。
调用 eth_getStorageAt,或在 eth_getProof 的
storageKeys 中指定槽位时,传入的是逻辑 slot,
不是 Keccak256(slot32) 后的 MPT 路径。客户端会在内部完成 secured
Storage Trie 的第二次 Keccak。
为什么写入 0 常被称为“删除槽位”?
Storage 的默认值是 0。MPT 不必为默认值保存叶子,因此把某槽写回 0,会从 trie 中移除该 key。 读取不存在的槽仍返回 0;这也是“缺省值”和“显式存一个 0”在协议状态里不作区分的结果。
05 · Transaction Trie
一棵树,承诺一个区块的交易与顺序
它不是全局交易数据库,也不用 txHash 作 key;它为每个区块单独构建。
0x11…c80x9f…d30x84…7atransactionsRoot =
MPT_ROOT({
RLP(transactionIndex)
↦ encodedTransaction
})
三个精确点
-
key 是零基索引的 RLP。
index 0 →
0x80,index 1 →0x01, index 128 →0x8180。它不会再被 Keccak 哈希。 -
value 保留交易封装。
Legacy 交易为 RLP 列表;typed transaction 为
typeByte || payload。 -
顺序本身被承诺。
只要交换两笔不同的编码交易,它们与 index 的 key/value 绑定就会改变,
transactionsRoot也会改变;若两个 value 逐字节完全相同,互换当然不可观察。
transactionHash 用来标识一笔交易;transactionsRoot
承诺区块内按索引排列的整组编码交易。两者都用哈希,却回答不同问题。
06 · Comparison
把三棵 Trie 放进同一张表
先看生命周期,再看 key 是否哈希,最后看 root 被谁引用。
| Trie | 生命周期 | 逻辑 key | 实际路径 | value | root 所在 |
|---|---|---|---|---|---|
| State | 全局状态随区块演进 | 20-byte address | nibbles(Keccak(address)) |
RLP(account) |
区块头 stateRoot |
| Storage | 每账户一棵,随存储变化 | 32-byte slot | nibbles(Keccak(slot)) |
RLP(U256),0 省略 |
账户叶子 storageRoot |
| Transaction | 每区块一棵,确定后不变 | 区块内 index | nibbles(RLP(index)) |
编码交易 | 区块头 transactionsRoot |
PATH EXPLORER
切换三类 Trie,看同一个 “key → path → value → root” 模板如何改变。
20 字节账户地址
secured trie:先哈希
64 个 nibble
RLP([nonce,balance,storageRoot,codeHash])blockHeader.stateRoot07 · Update Ripple
一次 SSTORE,如何一路改变 stateRoot?
只重建两条受影响路径:先是账户的存储路径,再是全局账户路径。
假设合约把 slot 1 从 640 写成 900。客户端不会重写所有账户和槽位;
它创建新的相关叶子与祖先节点,未受影响的节点可以继续复用。最终得到新的
storageRoot,再把它写入该账户记录,继而得到新的 stateRoot。
UPDATE RIPPLE
观察值改变如何穿过两个嵌套 Trie,抵达区块头。
leaf 0x14…b0
path 0x8a…20
0x6b…e1
0xa7…09
0x11…c8
修改前:旧 stateRoot 对应旧世界状态。点击“执行 SSTORE”查看哈希连锁。
新旧状态可以共享绝大多数未变节点。概念上,你可以把不同区块的 stateRoot 理解为对不同“状态版本”的入口,而不是每个区块复制一份完整数据库。
08 · Merkle Proof
如何在不信任提供者时验证一条数据?
从可信 root 开始,逐个解 RLP、核对引用、消费路径,直到 Leaf 完整匹配。
存在证明不是“给我一个值”
- 你先拥有一个可信区块头,因而信任其中的
stateRoot。 - 提供者给出账户值,以及从 root 到该 Leaf 的 RLP 节点序列。
- 你计算地址的 Keccak 与 nibble 路径,从 root 逐步核对节点引用。
- Leaf 剩余路径完全匹配,且 value 等于该账户 RLP,证明成立。
PROOF WALKER
逐步检查一份简化的账户 inclusion proof;再篡改 Leaf 看根承诺如何拒绝它。
stateRoot = 0x11…c8trusted
childRef = 0x7c…91pending
shared path matchespending
balance = 4.2 ETHpending
不存在也能证明
如果路径落到空 child,或遇到一个剩余路径不匹配的 Extension / Leaf, 这组节点同样可以证明该 key 在此根下不存在。MPT proof 因此不只是“一串兄弟哈希”: 它包含路径上的 RLP 节点,短节点还可能直接嵌在父节点里。
JSON-RPC 方法 eth_getProof 可返回账户的 accountProof
与指定槽位的 storageProof。账户证明从区块 stateRoot 开始;
storage 证明从账户记录里的 storageHash 开始。
09 · Client Reality
协议里的 Trie,不等于磁盘上的文件夹
根与证明是共识语义;客户端可以自由优化底层数据库、缓存、快照和同步方式。
协议要求所有正确执行客户端对同一状态算出相同根,但不要求它们用同一种磁盘布局。 客户端可能采用 flat state、snapshot、缓存、path-based 存储或其他索引来加速读取; 只要最终 trie 承诺与执行结果满足规范即可。
账户与 storage 的协议值、交易顺序、RLP/typed 编码、MPT 根、区块头字段。
节点怎样落盘、怎样缓存、是否维护扁平索引、如何做垃圾回收与快照。
三个 32 字节常量不要混
- 空 Trie 根:
0x56e81f…63b421,即Keccak256(RLP(""))。 - 空代码哈希:
0xc5d246…85a470,即Keccak256("")。 - 零值:storage 默认 0;它通常意味着该 key 根本不进入 Storage Trie。
区块头永久承诺当时的根,不等于每个普通节点都必须无限保留所有历史 Trie 节点。 节点的历史数据保留、归档与同步策略,是协议可验证性与本地存储策略之间的另一个层次。
10 · Roadmap Context
MPT 是当前事实,不是永恒终点
路线图曾长期研究 Verkle;截至 2026 年,官方长期方向已表述为 binary trees 与 statelessness。
十六叉、Keccak、RLP、账户树嵌套 storage 树。本课全部核心细节以此为准。
曾是 The Verge 的主要研究路线,目标之一是显著缩小状态 witness。
EIP-7864 提议统一二叉状态树,但仍为 Draft,连最终哈希函数也未定。
2026 年 Ethereum Foundation 的协议优先级更新,把长期状态扩展方向写成 “a move to binary trees and statelessness”。这说明学习 MPT 时要区分两句话:
- 现在如何验证主网执行状态?答案仍是当前十六叉 MPT 语义。
- 未来状态结构可能怎样改?统一二叉树是草案方向,不是已经迁移的事实。
可以说“Ethereum 正在研究/提议用统一二叉树替代当前 MPT”; 不应说“主网已经使用 Verkle”或“EIP-7864 已确定上线”。
11 · Misconceptions
五个必须当场纠正的误区
能解释这些差异,才算真正把三棵 Trie 分开。
错。State 与 Storage 是 secured trie;Transaction Trie 使用原始 RLP(index) 路径。
错。key 是区块内索引;因此 transactionsRoot 同时承诺交易内容与顺序。
不完整。它先给出逻辑 storage slot;secured Storage Trie 再对 slot32 做一次 Keccak。
错。账户叶子存 codeHash;字节码正文由客户端按哈希单独保存。
错。MPT proof 是路径上的 RLP 节点;空分支或不匹配路径也能证明不存在。
12 · Checkpoint
六题检验你是否真的掌握
每题只有一个最佳答案。先作答,再阅读反馈。
Q1为什么 Branch Node 最多有 16 个 child?
选择一个答案。
Q2合约字节码正文直接存在哪里?
选择一个答案。
Q3区块内第 2 笔交易在 Transaction Trie 里用什么作 key?
选择一个答案。
Q4某 storage slot 被写回 0,Trie 通常怎样表示?
选择一个答案。
Q5一次 SSTORE 为什么会改变区块的新 stateRoot?
选择一个答案。
Q6MPT 能否证明某个 key 不存在?
选择一个答案。
13 · Sources & Glossary
一手资料与最小术语表
正文以当前执行规范、Ethereum.org、EIP 与 Solidity 官方存储布局为事实边界。
- Trie
- 按 key 的符号序列逐步寻址的前缀树;名称来自 retrieval,不是拼错的 tree。
- Patricia
- Practical Algorithm To Retrieve Information Coded in Alphanumeric;在这里主要体现为共同路径压缩。
- Merkle
- 父节点通过哈希引用子内容,使任意底层改变传导到根,并支持局部证明。
- Nibble
- 4 bit 半字节,取值 0–15;十六进制恰好用 0–f 表示。
- RLP
- Recursive Length Prefix,以太坊执行层用于嵌套字节序列的规范序列化编码。
- Root
- 32 字节密码学承诺;它绑定整棵 trie 的键、值与结构,但不包含可逆的全部数据。
- Proof
- 从可信根到目标 key 的路径节点;可验证 inclusion,也可验证 non-inclusion。