本文深入介绍了以太坊中用于状态管理的 Merkle Patricia Trie (MPT) 数据结构。解释了 MPT 的基本概念、在以太坊中的应用方式、以及如何利用 MPT 进行状态证明。通过示例详细说明了 MPT 的构建过程和验证方法,旨在帮助读者理解以太坊状态管理的核心机制。
以太坊依赖于密码学数据结构来高效地存储和验证其状态。其中一种结构是 Merkle Patricia Trie (MPT),它为以太坊的状态管理提供支持。在更深入地探索这个工具之后,很明显,MPT 是一种复杂的数据结构——远比简单的 Merkle 树复杂。这就是我们觉得创建这篇文章很重要的原因:让 MPT 更容易理解。在这里,我们将解释什么是 MPT,以太坊如何使用它以及它的证明如何工作。在即将发布的文章中,我们将解释如何对 MPT 进行算术化,以便生成证明,表明我们验证了元素是否在树中,或者树是否已成功更新。
在下面的内容中,我们仅假设你对 Merkle 树 和密码学哈希函数有基本的了解。
Merkle 树是一种二叉树,其中:
Merkle 树用于 数据完整性证明:你可以通过提供 Merkle 路径(从叶子节点到根节点的一系列哈希值)有效地证明一条数据属于树的叶子节点。
Trie(检索树的缩写,也称为前缀树)是一种类似树的数据结构,用于高效地存储和检索键值对,尤其是在键是字符串或序列时。
trie 的每个级别表示键中的一个字符,从根到叶的路径对应于整个键。键之间的共享前缀仅存储一次,这使得 trie 对于具有公共前缀的数据集非常节省空间。
让我们看一个玩具示例,以便更容易理解。假设我们要存储以下键值对:
| 键 | 值 |
|---|---|
| cat | curious |
| cake | sweet |
| cup | fragile |
| cups | plural |
| book | heavy |
我们的玩具 trie 看起来像这样:

要查找键 "cake" 的值:
c -> a -> k -> e。e 检索值 "sweet"。以太坊使用一种专门的 trie 形式,称为 Modified Merkle Patricia Trie (MPT)。该名称结合了三个核心思想:
以太坊使用多个 MPT,但我们将仅关注其中一个,即 State Trie,并将其用作解释它们如何工作的示例。
在 State Trie 中,每个帐户的状态都存储为一个键值对,其中:
[nonce, balance, storageRoot, codeHash]。为了达成共识,当出现一个包含交易集的新区块时,每个以太坊节点都需要执行所有这些交易并验证所有节点的结果状态是否相同。但是,比较每个帐户的计算成本非常高,因此它们使用 MPT 代替。以太坊所有帐户的状态都存储在一个名为 State Trie 的 Merkle trie 中,该 trie 在每次交易执行后都会不断更新。为了达成共识,节点只需比较 StateRoot(State Trie 的根)。如果两个节点具有相同的 StateRoot,则它们的状态匹配。
以太坊需要能够轻松地恢复到以前的状态:当节点对下一个区块存在分歧时,需要进行区块链分叉。这是可能的,因为 trie 保留了旧状态,而不是直接删除或修改它。trie 是持久的和可版本化的,而不是可变的就地结构。
当状态发生变化时(例如,帐户余额更新),trie 会为已更改的路径创建新节点,而 trie 的其余部分(未更改的部分)会被重用。因此,可以通过它们的根访问以前的 trie 版本。每个区块都在其标头中存储一个状态根,并且该根唯一地标识了当时整个以太坊的状态。因此,如果以太坊需要回滚,它只需使用先前区块的状态根即可。由于旧节点从未被删除,因此 trie 可以有效地重建旧状态。这意味着以太坊只需切换回较早的根哈希即可恢复旧状态。
让我们解释一下 State Trie 是如何构建的。正如我们上面所说,这个 trie 的键由地址的哈希值组成,表示为十六进制字符串。正如我们在玩具示例中展示的那样,trie 的每个节点将存储十六进制字符串的一个字符,也就是单个 nibble (四位数据)。
MPT 中有三种类型的节点:
分支节点(Branch Node)
例子:
[0x, 0x, child_hash, 0x, 0x, other_child_hash, 0x, 0x, 0x, 0x, 0x, 0x, 0x, 0x, 0x, 0x, value]
在这里,"0x" 表示未使用的槽,即没有子节点的数字。
[shared_prefix, child_hash][key_remaining, value]这三种类型的节点都存储一个以 RLP 编码的单个数组。指向某个节点的指针始终是存储该 RLP 字符串数据的哈希值。根可以是任何类型,但通常,由于我们有很多数据,因此根是一个分支节点。
当逐个 nibble(或逐个字符)遍历键路径时,我们最终可能会得到一个需要存储奇数个 nibble 的叶子节点或扩展节点。但是由于所有数据都存储在字节中,这会产生一个问题。例如,如果我们想存储 nibble 1,我们将不得不将其保存为 01,但我们将无法判断它来自两个 nibble 01,还是来自单个 nibble 1。为了指示我们是存储偶数个还是奇数个 nibble —— 以及我们正在处理的节点类型(叶子或扩展)——部分路径以以下标志作为前缀。
| 标志 | 节点类型 | 路径长度奇偶性 |
|---|---|---|
| 00 | 扩展 | 偶数 |
| 1 | 扩展 | 奇数 |
| 20 | 叶子 | 偶数 |
| 3 | 叶子 | 奇数 |
理解 MPT 的最佳方法是查看完整的示例。让我们模拟一个真实的 State Trie。假设我们有五个帐户的数据,这些帐户转换为以下键值对:
| - | 键 | 值 |
|---|---|---|
| 1 | 0x616b6c64 | 0x01 |
| 2 | 0x616b6c65 | 0x02 |
| 3 | 0x616b6c78 | 0x03 |
| 4 | 0x616b6d31 | 0x04 |
| 5 | 0x31323334 | 0x05 |
正如我们前面提到的,键应该是帐户地址的哈希值。由于以太坊使用 Keccak-256,因此键应该是 32 字节长(或 64 个十六进制字符)。但是,对于此示例,我们将使用短得多的键,以便生成的 trie 不会太大且更易于理解。以太坊还使用优化,例如内联小节点,为了清楚起见,我们将在本示例中跳过这些优化。
现在我们准备好构建 MPT:
(0x616b6c64, 0x01)。因为它只是一个如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!