本文深入介绍了以太坊中用于状态管理的 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:
从一个空的 MPT 开始,并添加第一个键值对:(0x616b6c64, 0x01)
。因为它只是一个键,所以它会生成一个只有一个叶子节点的 trie。要创建该节点,请按以下方式进行:
20
添加到第一个元素,指示该节点是一个叶子节点,并且该键具有偶数个 nibble。然后,该数组应如下所示:["0x20616b6c64","0x01"]
。import rlp
key = bytes.fromhex("20616b6c64")
value = bytes.fromhex("01")
encoded = rlp.encode([key, value])
print(encoded.hex())
这应该输出这个十六进制:c78520616b6c6401
。
from eth_utils import keccak
rlp_bytes = bytes.fromhex("c78520616b6c6401")
hash_pointer = keccak(rlp_bytes)
print(hash_pointer.hex())
这应该输出:
4e2d0fbe6726eac15c5ecf49a4e1f947aa50e0531f4f3e98b8e1577ba52e1783
生成的 MPT 应如下所示:
添加第二个键值对:(0x616b6c65, 0x02)
。由于此键与前一个键共享前 7 位数字,因此我们按以下方式进行操作:
20
,指示我们位于叶子节点中,并且该路径具有偶数个数字(零个数字)。之后,像我们在步骤 1 中所做的那样,在 RLP 中编码数组并哈希 RLP 编码。构建扩展节点和根节点: 创建一个包含共享前缀作为第一个元素和先前构建的分支节点的哈希值作为第二个元素的两项数组。由于共享前缀具有奇数个数字,因此将标志 1
添加到其中。在 RLP 中编码该数组并哈希该编码。
添加键值对 (0x616b6c78, 0x03)
:
3
指示它是一个具有奇数个路径数字的叶子节点。00
添加到其中。当前的 trie 应如下所示:
现在让我们了解 MPT 中的证明是什么样的。继续前面的示例,假设我们要证明键值对 (0x616b6d31, 0x04)
属于我们的 State Trie。我们如何构建证明?
该证明将包含 StateRoot 0x13ea...bed7
(根节点的哈希值)以及从根开始并向下遍历 trie 的路径,沿着目标键的每个数字,直到到达其叶子节点。让我们逐步了解如何构建此路径:
路径的第一个元素是根节点的 RLP:0xf851...8080
。
如果我们解码此 RLP,我们发现根是一个分支节点。它表示的数组除了索引 3 和 6 之外,都具有所有空槽(因为所有键都以数字 3
或 6
开头)。这意味着根节点分支为两个子节点。由于我们要查找的键的第一位数字是 6
,因此我们需要查看存储在数组中索引 6 处的哈希值,然后移动到该节点。
我们移动到下一个节点,并将它的 RLP 内容存储为路径的第二个元素:0xe583...67e9
。
此节点是一个扩展节点,因为所有以 6 开头的键都共享相同的后四位数字:16b6
。为了确定接下来要去哪里,我们解码 RLP 并得到一个两项数组。第二个项目为我们提供了我们需要访问的下一个节点的哈希值。
同样,我们移动到下一个节点,并将它的 RLP 内容存储为路径的第三个元素:0xf851...8080
。
此节点是一个分支节点。由于我们的键继续使用数字 d
,因此我们需要查看存储在此节点数组中索引 dd 处的哈希值,然后移动到此哈希指向的节点。
最后,我们到达叶子节点:0xc482203104
。我们存储此最终节点的 RLP 内容,这样,证明就完成了。
然后键值 (0x616b6d31, 0x04)
的证明应如下所示:
state_root = 0x13ea549e268b5aa80e9752c6be0770cffba34d2b1aa1f858cb90f3f13ac3bed7
proof_path =
[\
0xf851808080a0a26b2ac124718443aeed68fa0309225d0c8dd9dbee45685909e92fb594e1a4638080a02ccd118c9470c051689543b233ab109ad74d2fb4f57eb429c4d43294d6ae686780808080808080808080,\
0xe5831616b6a0917fa5cab26d915e2a89a263a578fa5f9ecf02cc0b1d3eeb433e7f32499267e9,\
0xf851808080808080808080808080a0cc97f12ea3217345e666974cd81b117ca02404f19c15d31158ac1d1e55398706a0822a55ca308aa885ad385d5e61aabaca54c2e4361eb03b6f851668c0f095ab77808080,\
0xc482203104\
]
如果验证者收到某个键的 StateRoot 和证明路径,他如何验证该证明是否有效?
与标准 Merkle 树的一个关键区别是验证方向。虽然典型的 Merkle 证明是从下往上验证的(从叶子节点到根节点),但 Merkle Patricia Tries 证明是从上往下验证的。该过程从 StateRoot
开始,并使用提供的路径逐个节点地向下遍历 trie,最终到达目标叶子节点。
假设验证者收到上述键值 (0x616b6d31, 0x04)
的证明。然后,他必须按照以下步骤操作:
keccak(bytes.fromhex(
"f851808080a0a26b2ac124718443aeed68fa0309225d0c8dd9dbee45685909e92fb594e1a4638080a02ccd118c9470c051689543b233ab109ad74d2fb4f57eb429c4d43294d6ae686780808080808080808080"
)).hex() ==
"13ea549e268b5aa80e9752c6be0770cffba34d2b1aa1f858cb90f3f13ac3bed7"
rlp.decode(bytes.fromhex(
"f851808080a0a26b2ac124718443aeed68fa0309225d0c8dd9dbee45685909e92fb594e1a4638080a02ccd118c9470c051689543b233ab109ad74d2fb4f57eb429c4d43294d6ae686780808080808080808080"
))[6].hex() ==
"2ccd118c9470c051689543b233ab109ad74d2fb4f57eb429c4d43294d6ae6867"
解码 路径的第二个 RLP 元素。你会发现一个两项数组,其第一个元素是 0x1616b6
。由于它的第一位数字是 1
,我们知道我们位于扩展节点上。检查其余数字是否与我们要查找的键相对应。验证该数组的第二个元素是否是路径的下一个元素的哈希值。
解码 第三个路径元素。你将找到一个分支节点。验证索引 dd 处是否存储了路径元素的哈希值。
解码 路径的最后一个元素。你会发现一个两项数组,其第一个元素是 0x2031
。由于它的前两位数字是 20
,我们知道我们到达了一个叶子节点。验证第一个项目是否包含剩余键的数字 31
,第二个项目是否包含键的值 0x04
。
Merkle Patricia Trie 是以太坊状态管理的支柱。它结合了 tries 的键导航效率和 Merkle 树的密码学保证。这种结构允许以太坊高效且安全地存储、验证和恢复状态。借助 MPT,以太坊节点可以独立执行交易,并通过简单地比较状态根来验证共识,从而实现可扩展且无需信任的区块链系统。在即将发布的文章中,我们将阐述如何对 MPT 更新进行算术化,并展示我们如何验证包含证明。
- 原文链接: blog.lambdaclass.com/an-...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!