以太坊 Trie 源码导读(上):原理与优化全览

·

关键词:以太坊 Trie、Merkle Patricia Tree、状态存储、内存优化、修剪区块、nibble、哈希引用、节点压缩

1. Trie 是什么,为什么以太坊离不开它

Trie,更准确地说,是「Merkle Patricia Tree」(下文简称 MPT),是以太坊核心数据结构中最高频出现的成员。它将账户状态、交易、回执、存储四大类(key, value)数据全部组织在一棵既高效又可验证的树里:

本篇聚焦 原理,下一篇再拆解 go-ethereum 中 trie 包的具体代码,帮你彻底啃下这块硬骨头。

2. 传统 Trie 速览

传统 Trie(字典树)把 key 拆成字符,沿树路径逐字符查找 value。
示意结构:

根 → m → e → d → ... → value

每个节点维护 索引数组(子节点指针)和 value。优点是前缀共享,缺点是稀疏空位多。

以太坊在传统 Trie 之上做了 6 处关键升级,从而让 MPT 既能跑在 16 MB RAM 的树莓派,又能撑起千万级账户的全球网络。

3. 以太坊 MPT 的 6 大优化点

3.1 key 统一用 []byte

任何输入先经 RLP 编码成 []byte,规避字符串兼容性问题,方便网络传输与持久化。

3.2 4 bit nibble 拆分

每个 byte 拆成高 H、低 L 两个 nibble(4 bit)
好处:索引数组长度从 256 下降到 16,节省 15× 内存;同时仍保持 4 bit 足够表示十六进制字符。

3.3 哈希引用而非指针

父节点仅保存子节点的 32 byte Keccak256 哈希。
结果:

3.4 路径压缩(shortNode)

当某条路径无分支,整条路径压缩进一个 shortNode

示例压缩示意:

# 未压缩
m → e → d → i → c → i → n → e → value

# 压缩后
[med] → icine → value (shortNode)

无分支 → 1 个节点即可覆盖整条 key。

3.5 奇偶 nibble 编码与叶子标记

数据库持久化 shortNode 时,必须解决两件事:

  1. nibble 个数为奇数时如何区分有效/填充位;
  2. 如何区分 val 指向的是「子节点哈希」还是「原始 value」。

以太坊设计了 1 byte header

4 种 header 值:

hexbits含义
00000偶 nibble + 非叶子
10001奇 nibble(首 nibble 在第 0 byte 低 4 位) + 非叶子
20010偶 nibble + 叶子
30011奇 nibble + 叶子

这样一来,磁盘空间 颗粒度最小化,读取时又可通过 header 精确定位。

3.6 内存层引用计数(pruning 基石)

Trie.Database 在内存层为每个节点维护 refCount

结合「写时复制」机制,产生以下效果:

👉 想了解节点修剪如何在主网节点落地?点击阅读实践要点详解。

4. MPT 的四张面孔:State, Storage, Tx, Receipt

同一份 MPT 模板被重复使用:

名称key 示例value 示例用途
State账户地址 20 byte账户状态(nonce, balance…)全局账户树
Storage地址 + slot 索引slot 值每个合约的持久化存储
TxTxIndex (rlp index)交易 RLP区块内交易列表
ReceiptTxIndex交易回执 RLP快速查询交易结果与事件日志

四个树的根哈希统一写进 区块头,其他节点可在无信任环境下一键验证全网共识。

5. 小结:为什么 MPT 如此优秀

  1. 极致压缩

    • nibble 拆分 + shortNode → 节点数锐减。
  2. 默克尔化

    • 任何两台节点只要根哈希相同,即可认定整棵树相同。
  3. 修剪友好

    • 引用计数 + 写时复制 → 老旧状态自动 GC。
  4. 网络友好

    • 哈希引用允许 SNAPSHOT / SNAP 协议按子树逐步拉取。

👉 继续深入:实测以太坊 snap 同步如何分钟级构建可信 State


常见问题 FAQ

Q1:Trie 为什么要拆成 nibble,而不是用 8-bit byte?
A:索引数组 0x00–0x0F 只需 16 槽,若用 8-bit 将需 256 槽,其中 90% 以上为空,造成严重浪费。

Q2:shortNode 的「奇偶标记」放在 header 的第 0 byte,那真正的第一个 nibble 会不会被覆盖?
A:不会。header byte 的高 4 位做标记,低 4 位在奇数模式下充当第一个 nibble,无冲突,数据无损还原。

Q3:引用计数是在内存还是 LevelDB?
A:仅内存。落盘节点无引用计数。重启后依靠 NodeIterator 重新计算引用,保证安全性。

Q4:Trie 读写一次就要新生成一批节点,磁盘会不会膨胀得更快?
A:会。这是「写时复制」不可避免的副作用。所以才有「修剪节点」模式,通过 Dereference 及时清理 0 引用的冷节点。

Q5:同步区块时,怎样才能只下载差异片段?
A:同步协议会先在区块头拿到新的 State Root,然后利用 「请求路径前缀 → 回复 Merkle 证明」 的方式仅拉取差异子树,避免 300 GB 巨兽同步。

Q6:学习 Trie 源码,有哪些调试或可视化工具?
A:


下篇,我们将把 trie/node.gotrie/database.gotrie/iterator.go 逐函数拆到底,带上内存图与调试日志,不见不散。