以太坊状态树知识点

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 模型。

第二层:知道账户状态四元组:noncebalancestorageRootcodeHash

第三层:知道状态树不是普通数据库,而是带根哈希和证明能力的 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,每一位是 0f

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:账户状态四个字段是什么?

noncebalancestorageRootcodeHash

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 进行许可。