以太坊状态树知识点

Kylinxin Kylinxin

ETH 状态树:知识点总览

ETH 状态树示意图
ETH 状态树示意图

核心一句话:以太坊状态树是全局账户状态的加密承诺。它用 Modified Merkle Patricia Trie 维护 address -> account state,每个区块执行完交易后都会得到新的 stateRoot,并写入区块头。

这一篇整理以太坊状态树的核心知识点:状态树是什么、为什么不用哈希表、为什么不用普通 Merkle Tree,以及状态树和合约存储之间的关系。


1. 先背下来的核心结论

以太坊采用账户模型,需要维护一份全局状态:

1
address -> account state

每个账户状态包含四个字段:

1
2
3
4
nonce
balance
storageRoot
codeHash

nonce 记录交易次数或合约创建次数。balance 是账户的 ETH 原生余额。storageRoot 指向合约账户自己的 Storage Trie。codeHash 是合约代码哈希。

状态树的根哈希叫 stateRoot。每个区块执行完成后,新的 stateRoot 会写入区块头,用来承诺当前完整世界状态。


2. 核心考点

状态树这一题通常考五层能力。

第一层:知道以太坊是账户模型,不是 UTXO 模型。

第二层:知道账户状态四元组:nonce、balance、storageRoot、codeHash。

第三层:知道状态树不是普通数据库,而是带根哈希和证明能力的 MPT。

第四层:能解释为什么普通哈希表、普通 Merkle Tree、Sorted Merkle Tree 都不够合适。

第五层:能把状态树、合约 Storage Trie、ERC-20 余额、分叉回滚、Merkle Proof 串起来。


3. 为什么以太坊需要状态树

比特币主要维护 UTXO Set,关注“哪些输出还没被花费”。以太坊采用账户模型,关注“每个账户现在处于什么状态”。

以太坊中的账户分为两类:

1
2
EOA:外部账户,由私钥控制
Contract Account:合约账户,由代码控制

普通账户主要关心 ETH 余额和 nonce。合约账户除了余额和 nonce,还需要保存合约代码以及合约内部变量。

因此,以太坊需要维护一份巨大的动态键值映射:

1
keccak256(address) -> RLP(account state)

这份映射不是只给本地节点查数据库用。它还必须让所有节点对同一份状态得到同一个根哈希,并支持轻节点验证某个账户状态是否真实存在。


4. 为什么不能只用哈希表

如果只看查询速度,哈希表很自然:

1
map[address] = accountState

普通哈希表平均查询复杂度接近 O(1),但它缺少区块链需要的状态承诺能力。

第一个问题是没有统一根哈希。区块链需要用一个短值代表整份全局状态,所有节点通过这个值判断状态是否一致。

第二个问题是不支持 Merkle Proof。轻节点不能只相信全节点口头返回的余额,它需要一条可验证的证明路径。

第三个问题是难以证明不存在。判断某个账户不存在,不能靠“数据库没查到”这种本地结论,而要能绑定到某个区块头的 stateRoot。

所以哈希表适合本地查找,但不能直接作为区块链共识层的状态承诺结构。


5. 为什么不能直接用普通 Merkle Tree

普通 Merkle Tree 能提供根哈希,也能证明某个叶子属于这棵树。比特币交易树就是典型例子。

但以太坊状态不是固定列表,而是动态键值映射:

1
address -> account state

普通 Merkle Tree 不擅长按 key 查找。它只知道叶子位置,不知道某个地址应该走到哪个叶子。

普通 Merkle Tree 还依赖叶子顺序。不同节点如果以不同顺序组织账户,即使账户内容一样,也会得到不同 root。

Sorted Merkle Tree 可以按 key 排序,但新增账户可能插入到中间位置,导致后续叶子位置和父节点结构大范围变化。

以太坊需要的是“key 自己决定路径”的结构,所以 Trie 比普通 Merkle Tree 更适合。


6. Trie、Patricia Trie 和 MPT

Trie 也叫前缀树。它用 key 的每一位决定路径:

1
root -> a -> 7 -> f -> 3 -> ...

这个特性适合键值映射,因为查找路径由 key 决定,不需要额外排序。

普通 Trie 的问题是路径可能很长。如果只有一个 key abcdef,普通 Trie 可能展开成六层单分支节点。

Patricia Trie 会压缩没有分叉的路径:

1
root -> abcdef -> value

Merkle Patricia Trie 可以理解为:

1
Patricia Trie + Merkle Hash

Patricia Trie 负责高效查找和路径压缩。Merkle Hash 负责防篡改、根哈希承诺和 Merkle Proof。


7. 以太坊的 Modified MPT

以太坊实际使用的是 Modified Merkle Patricia Trie。Modified 主要体现为节点编码、路径编码、节点引用和存储优化。

MPT 中常见三类节点:

1
2
3
Branch Node
Extension Node
Leaf Node

Branch Node 最多有 16 个子方向,因为以太坊把 key 拆成十六进制 nibble,每一位是 0 到 f。

Extension Node 用于压缩公共前缀。多个 key 共享一段路径时,这段路径可以用一个扩展节点表示。

Leaf Node 保存最终 value。对状态树来说,value 通常是 RLP 编码后的账户状态。


8. 为什么 key 通常是地址哈希

账户地址本身是 160 bit,也就是 20 字节。以太坊状态树通常使用地址的 Keccak-256 哈希作为路径 key:

1
key = keccak256(address)

这样做可以让 key 分布更均匀,避免攻击者构造大量相似前缀地址,让 Trie 结构退化。

哈希后的路径空间接近 256 bit,极度稀疏。因此路径压缩很重要,否则普通 Trie 会浪费大量中间节点。


9. RLP 编码的作用

账户状态在进入 MPT 前需要被序列化。以太坊使用 RLP,即 Recursive Length Prefix。

账户状态会被组织成列表:

1
[nonce, balance, storageRoot, codeHash]

然后编码为确定的字节序列:

1
RLP([nonce, balance, storageRoot, codeHash])

哈希函数处理的是字节序列。如果不同节点对同一个账户状态使用不同编码,最终会得到不同哈希和不同 stateRoot。

RLP 的核心价值是确定性。所有节点必须对同一份状态得到完全相同的字节表示。


10. 状态树和合约 Storage Trie 的关系

状态树维护的是账户级别状态。合约自己的变量不会直接摊开放在全局状态树里,而是由合约账户的 storageRoot 指向一棵 Storage Trie。

可以把结构理解为:

1
2
3
4
5
6
7
Block Header
└── stateRoot
└── State Trie
├── EOA Account State
└── Contract Account State
└── storageRoot
└── Storage Trie

这个点很常见,因为它能区分 ETH 余额和 ERC-20 余额。

Alice 有 3 ETH,存储在 Alice 账户状态的 balance 字段。

Alice 有 100 USDT,通常存储在 USDT 合约 Storage Trie 中的 balances[Alice]。

所以 ERC-20 余额不是 Alice 账户的 balance。Alice 账户的 balance 只表示 ETH 原生资产。


11. 状态变化如何影响 stateRoot

假设 Alice 给 Bob 转 1 ETH。执行前:

1
2
3
Alice.balance = 5 ETH
Alice.nonce = 10
Bob.balance = 2 ETH

执行后:

1
2
3
Alice.balance = 4 ETH - gas
Alice.nonce = 11
Bob.balance = 3 ETH

Alice 和 Bob 的账户状态改变,对应叶子节点 value 改变。叶子哈希改变后,路径上的父节点哈希逐层改变,最终 stateRoot 改变。

没有变化的账户不需要重新构建。新旧状态树可以共享大量未变节点,这类似 Git 对未变化对象的复用。


12. 为什么不能原地修改状态树

区块链会出现临时分叉:

1
2
3
Block 100
├── Block 101A
└── Block 101B

节点可能先执行 101B,后来发现 101A 才是规范链。如果状态原地覆盖,旧状态很难恢复。

智能合约执行也不容易反推。一次 DeFi 交易可能修改多个 storage slot、跨合约调用、更新协议统计变量。

所以以太坊需要能引用旧状态版本。每个区块头中的 stateRoot 都代表该高度执行完成后的世界状态版本。


13. Merkle Proof 和不存在性证明

轻节点不保存完整状态树,但可以保存区块头。只要知道区块头中的 stateRoot,就能验证账户状态证明。

证明流程可以简化为:

1
2
3
4
区块头 stateRoot
目标地址 key
从根到叶子的证明路径
账户状态 value

验证者重新计算路径上节点哈希。如果最终根哈希等于区块头里的 stateRoot,就能确认该账户状态属于这个区块的世界状态。

MPT 也能证明不存在。因为路径由 key 决定,如果路径中某个分支缺失,或者最终叶子路径不匹配,就能证明该 key 不在树中。


14. 为什么状态树必须是全局的

一个常见误区是:每个区块只保存本区块涉及账户的状态,是否可以节省空间?

这个方案不可行。假设 Alice 很久没交易,查询 Alice 当前余额时,就必须从最新区块一直向前找,直到找到 Alice 最后一次出现的位置。

判断账户不存在会更麻烦。当前区块没有 Alice,可能是 Alice 不存在,也可能只是这个区块没用到 Alice。

以太坊的设计是:

1
每个区块的 stateRoot 都承诺该区块执行后的完整全局状态

这不代表每个区块完整复制一棵树。MPT 是持久化结构,新旧版本共享未变化节点,只更新变化路径。


15. 和交易树、收据树的关系

以太坊区块头里常见三类根:

1
2
3
stateRoot
transactionsRoot
receiptsRoot

stateRoot 承诺区块执行完成后的全局状态。它是跨区块持续演进的状态版本。

transactionsRoot 承诺当前区块包含哪些交易,以及交易顺序是什么。

receiptsRoot 承诺当前区块中每笔交易执行后的结果、gas 使用和事件日志。

状态树是理解以太坊数据结构的主线,交易树和收据树是它的对照组。下一篇会专门整理交易树和收据树。


16. 高频问答

Q1:以太坊状态树保存什么?

保存全局账户状态映射。逻辑上是 address -> account state,工程上通常是 keccak256(address) -> RLP(account state)。

Q2:账户状态四个字段是什么?

nonce、balance、storageRoot、codeHash。

Q3:ERC-20 余额存在用户账户里吗?

不在。用户账户的 balance 是 ETH 原生余额。ERC-20 余额存在代币合约自己的 Storage Trie 中。

Q4:为什么不用普通哈希表?

哈希表查找快,但没有全局根哈希,也不能直接提供 Merkle Proof 和不存在性证明。

Q5:为什么不用普通 Merkle Tree?

普通 Merkle Tree 更适合固定有序列表,不适合动态键值映射。它还需要额外解决 key 查找和叶子顺序一致性问题。

Q6:为什么状态树更新不是全量重建?

MPT 可以共享未变化节点。交易只影响相关账户或 storage slot,因此只需要更新变化路径上的节点。

Q7:状态树如何支持轻节点?

轻节点保存区块头和 stateRoot,向全节点请求 Merkle Proof,然后验证账户状态是否能回算到该根哈希。

Q8:为什么需要历史状态版本?

临时分叉、回滚和链重组需要旧状态。原地修改会让节点难以从错误分支切回规范链。


17. 易错点

不要把 balance 和 ERC-20 余额混为一谈。balance 是 ETH,ERC-20 余额是合约存储。

不要说每个区块完整复制一棵状态树。逻辑上每个区块有一个状态版本,工程上大量节点共享。

不要把状态树、交易树、收据树说成同一类业务含义。它们都可以用 trie,但承诺对象不同。

不要忽略 RLP。哈希前必须有确定的字节编码,否则不同节点无法得到一致根哈希。


18. 总结

以太坊状态树的本质是全局世界状态的加密承诺。它把账户模型、MPT、RLP、Merkle Proof、合约存储和区块头 stateRoot 连接在一起。

总结时,不要只说“以太坊用 MPT”。更完整的说法是:以太坊需要维护动态全局账户状态,普通哈希表缺少证明能力,普通 Merkle Tree 不适合动态键值映射,所以使用 Modified MPT,在每个区块执行后得到新的 stateRoot。

  • 标题: 以太坊状态树知识点
  • 作者: Kylinxin
  • 创建于 : 2026-06-04 09:16:00
  • 更新于 : 2026-06-22 10:00:00
  • 链接: https://kylinxin.github.io/2026/06/04/以太坊状态树知识点/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。