以太坊(Ethereum)是目前最被接受的区块链,而默克尔基数树(Merkle Patricia Tree,MPT)是以太坊中非常重要的数据结构,用于存储账户的状态信息、区块链中的交易信息以及交易收据信息。
当生成树形结构之后,将树的根(root)记录于区块链中,既便于查找又节约链上空间。
在以太坊的区块头(block head)中记录了三个 MPT 树的根,分别是:
- 状态树(state trie),记录此区块生成时所有账户的状态信息(如:余额)
- 交易树(tx trie),记录此区块中打包记录的所有交易信息
- 收据树(receipt trie),记录此区块中每笔交易带来的状态变化的中间状态以及日志信息
如下图所示的两个区块头中,state root、tx root 和 receipt root 分别存储了这三棵树的根,第二个区块显示了当账户 175 的数据变更时(27 -> 45),只需要关注跟这个账户相关的树的分支,在生成新的树形结构之后将树的 root 存储到区块头,同时老的区块中的数据仍然可以正常访问。
以太坊使用的 Merkle Patricia Trie(MPT),源自于 Trie 结构,又分别继承了 Patricia Trie 和 Merkle Tree 的优点。
Trie 树
Trie,又称为字典树或者前缀树 (prefix tree),是一种有序树,用于保存关联数组,其中的 key 通常是字符串。与二叉查找树不同:
- key 不是直接保存在节点中,而是由节点在 Trie 中的位置决定(下图中标注出完整的单词,只是为了演示 Trie 的原理);
- 一个节点的所有子孙都有相同的前缀(key),而根节点 key 为空;
- 待存储的数据只存于叶节点和部分内部节点中,非叶节点帮助形成叶子节点 key 的前缀。
下图展示了一个简单的 Trie 结构:
Patricia Trie 基数树
Patricia trie 基数树,或称 crit bit tree 压缩前缀树,是一种更节省空间的 Trie。Patricia Trie 里如果父节点只有一个子节点,那么这个父节点将与其子节点合并。
这样可以缩短 Trie 中不必要的深度,大大加快搜索节点速度。
Merkle Tree 默克尔树
Merkle Tree 默克尔树,通常也被称作 Hash Tree,顾名思义,就是存储 hash 值的一棵树。Merkle 树的叶子是数据块(例如,文件或者文件的集合)的 hash 值。非叶节点是其对应子节点串联字符串的 hash。
hash 哈希,简单的说就是一种将任意长度的数据压缩到某一固定长度数据的方式。
要了解 Merkle Tree 就要先从 Hash List说起:
在 P2P 网络中作数据传输的时候,会同时从多个机器上下载数据。为了校验数据的完整性,把大的文件分割成小的数据块。这样的好处是,如果小块数据在传输过程中损坏了,那么只要重新下载这一快数据就行了,不用重新下载整个文件。
怎么确定小的数据块没有损坏?
只需要为每个数据块做 hash。在下载到真正数据之前,会先下载一个 hash 列表。那么问题又来了,怎么确定这个 hash 列表本事是正确的呢?
答案是把每个小块数据的 hash 值拼到一起,然后对这个长字符串在作一次 hash 运算,这样就得到 hash 列表的根 hash(Top Hash or Root Hash)。下载数据的时候,首先从可信的数据源得到正确的根 hash,就可以用它来校验 hash 列表了,然后通过校验后的 hash 列表校验数据块。
Hash List 可以看作一种特殊的 Merkle Tree,即树高为 2 的多叉 Merkle Tree。
在最底层,和 Hash List 一样,我们把数据分成小的数据块,有相应地 hash 和它对应。但是往上走,并不是直接去运算根 hash,而是把相邻的两个 hash 合并成一个字符串,然后运算这个字符串的 hash,这样每两个 hash 就得到了一个父 hash。于是往上推,最终必然形成一棵树,得到根 hash,我们把它叫做 Merkle Root。
在 p2p 网络下载网络之前,先从可信的源获得文件的 Merkle Tree 的 root。一旦获得了 root ,就可以从其他从不可信的源获取 Merkle tree。通过可信的树根来检查接受到的 Merkle Tree。如果 Merkle Tree 是损坏的或者虚假的,就从其他源获得另一个 Merkle Tree,直到获得一个与可信树根匹配的 MerkleTree。
Merkle Tree 和 Hash List 的主要区别是:可以直接下载并立即验证 Merkle Tree 的一个分支。
因为可以将文件切分成小的数据块,这样如果有一块数据损坏,仅仅重新下载这个数据块就行了。如果文件非常大,那么 Merkle tree 和 Hash List 都很大,但是 Merkle Tree 可以一次下载一个分支,然后立即验证这个分支,如果分支验证通过,就可以下载数据了。而 Hash list 只有下载整个 hash list 才能验证。
Merkle Patricia Trie(MPT) 默克尔基数树
顾名思义,MPT(Merkle Patricia Trie)就是 Merkle Tree 和 Patricia Trie 这两者混合后的产物。
但 MPT 到底是什么结构,凭空想象还是很难以理解,那就以下图的简化示例加以说明:
图中右上角显示的是四个账户地址及其余额,这也是一个简化版的世界状态:
地址 | 余额 |
---|---|
a711355 | 45.0ETH |
a77d337 | 1.00WEI |
a7f9365 | 1.1ETH |
a77d397 | 0.12ETH |
以地址作为 key,以余额作为要存储的 value,生成了一个 Patricia Trie(具体生成的细节后面会描述);然后自底向上的遍历过程中,不断向上生成 hash,最后得到根节点的 root hash(体现了 Merkle Tree 的特点),即获得了 state root。
这就是 MPT 的大概视图,有了这个概念后才能具象化的理解 MPT 是如何结合 Merkle Tree 和 Patricia Trie 的;并更好的理解 Patricia Trie 的生成细节。
MPT 节点类型
在生成的 Patricia Trie 中可以看到存在三种类型的节点:
- Leaf(叶节点):没有子节点,表示为 [key, value] 键值对,从 root 到此节点的 key 的累加值表示完整 key 值(完整地址),value 用于存储上面账户地址中实际的余额
- Extertion(扩展节点):拥有一个分支节点作为子节点,表示为 [key, value] 键值对,key 值表示至少有两个 key 的分支共享从 root 到此节点的 key 的累加值(地址的前一部分),只用于指向分支节点,不存储实际值
- Branch(分支节点):有 17 个子项的数据结构,其中前 16 项对应 16 进制的 0-F,表示从 root 到此节点的 key 的累加值产生了分叉,分叉值恰好分别对应 0-F 的匹配项。若恰好有一个 key 值结束于此,则第 17 项存储对应的 value
通过这些节点类型,可以将上面的四个账户地址及其余额表示为 Patricia Trie。
基于以上内容,大概的 MPT 内容都已经出来了,但是与图中还有一些细小的不一致,这就引出了下面的内容。
Hex-Prefix Encoding 16进制前缀编码
Hex-Prefix Encoding (16进制前缀编码),是一种将任意长度的 nibble(4 位为一个 nibble,也可称为半字节)编码成 byte 数组的方法,也就是将最小粒度为 4 位的数组编码成最小粒度为 8 位的数组。
为什么要进行编码?
在以太坊协议中,不管是地址还是 hash,都是一个 16 进制串,如0x5b3edbcf7d0a97e95e57a4554a29ea66601b71ad
,数据最小的表示单位为一个 16 进制数,如 0-F
。
但在编程实现中,数据的最小表示单位往往是 byte(8 位,2 个 16 进制数),这样在用 byte 来表示一串奇数长度的 16 进制串时会出现问题,如5b3
和5b30
,直接转成 byte 都是5b30
。
还有一种简单直观的转换方式,5b3 -> 050b03
,这种方式虽然简单,但是数据量会翻倍,不利于大量 hash 的计算,同时也会增加 trie 的大小,降低同步性能。
而这个问题的解决方式就是 16 进制前缀编码。具体的编码参考下表:
原始数据 | 条件 | 节点类型 | HP前缀 | HP编码结果 |
---|---|---|---|---|
0x12345 | 奇数个 nibble | extension | 0x1 (0001) | 0x112345 |
0xf1cb8 | 奇数个 nibble | leaf | 0x3 (0011) | 0x3f1cb8 |
0x012345 | 偶数个 nibble | extension | 0x00 (0000 0000) | 0x00012345 |
0x0f1cb8 | 偶数个 nibble | leaf | 0x20 (0010 0000) | 0x200f1cb8 |
在原始数据前增加一个 nibble,根据奇偶性(原始数据的 nibble 数量)和终止符的状态(叶节点还是扩展节点)进行编码:
- 最低位表示奇偶性:奇数为 1,偶数为 0
- 第二低位编码终止符状态:叶节点为 1,扩展节点为 0
- 如果原始数据 nibble 数量为偶数,则增加一个值为 0 的 nibble 来保持整体的偶特性
总结
将以上这些综合起来,就完成了以太坊上 Merkle Patricia Trie(默克尔基数树)的构造。