简介
鉴于
理解MPT
对于进一步了解ETH运行原理
的重要性,我们今天再重头完整的梳理一遍MPT(默克尔帕特里夏字典树
)树
预备知识
将单个数字或者字母转换成16进制的ASCII
基数树
基数树
其实和字典树
在原理和实现上都很相似,都存储Key-Value
形式的数据,路径中保存的是Key
,Value
保存在节点中- 不同的是,基数树一般会把
Key
都转成数字
举例:
- 我们要存储
dog -> 100
,dog
是Key
,100
是Value
dog
-> 转成16进制表示是64 6f 67
(16进制,每两位占用一个字节)dog
在基数树的路径就是root -> 6 -> 4 -> 6 -> 15 -> 6 -> 7
(15是f的十进制表示)
我们上面的例子中使用的是
1步查找
,其中每一步都只检查了数据结构中的一个元素
,以确定所需元素的位置或存在情况
MPT(默克尔帕特里夏树)
半字节
我们把基数树的 原子单位
称为 半字节
(单个16进制字符 或 4位二进制数)。如上文所述,以 半字节
为单位遍历路径时,节点最多可指向 16
个子节点,不过还包含一个 value
元素。 因此,我们把它们表示为具有长度 17个元素的数组
哪些数据使用MPT存储
- 以太坊账户数据(World State):path是
keccak256(ethereumAddress)
, value是rlp(ethereumAccount)
- 这里的
path
就是上面说的key
,为了区分,我们后面说按path
查找,都是基于MPT查找 - 一个以太坊账户 是包含 4 个元素的数组:[nonce,balance,storageRoot,codeHash]。关于这一点值得注意的是,
storageRoot
是另一个帕特里夏字典树的根
- 智能合约数据存储
上面以太坊账户中的
storageRoot
就是智能合约数据的根Hash。path
是如何计算的,我们之后会用几篇的文章去讲解
- 交易树(每个区块都会有)
每个区块都有一个独立的交易字典树,也用于存储 (key, value) 对。
path
=rlp(transactionIndex)
,代表了对应一个值的键,value =TxType | encode(tx)
- 收据树(每个区块都会有)
path
=rlp(transactionIndex)
transactionIndex
是它在挖矿区块中的索引。收据字典树从不更新。 与交易字典树类似,它也有当前和以前的收据。为了在收据字典树中查询特定的收据,需要提供区块中交易的索引、收据有效载荷以及交易类型- 返回的收据(就是
value
)可以是Receipt
类型,定义为TransactionType
和ReceiptPayload
的串联;也可以是LegacyReceipt
类型,定义为rlp([status, cumulativeGasUsed, logsBloom, logs])
基数树存在的问题
效率低下
通过上面的存储数据可以看到,path
的长度一般都等于或者超过 64个半字节
(keccak256
算法生成的是 256位
的哈希值)
- 如果使用基数树每个节点都需要
存储指向下一个节点的指针
,占用更多的存储空间 - 查询,删除时需要执行完整的
64层
MPT做了哪些优化
NULL
: 表示空字符串Branch Node
: 一个包含17个元素的节点(能完整表示所有半字节,并且有一个Value
元素),表示为[ v0 ... v15, vt ]
Leaf Node
: 一个双元素节点[ encodedPath, value ]
Extension Node
: 一个双元素节点[ encodedPath, key ]
- 在
64 个半字节
的path
中,遍历前缀树的前几层
后,一定会到达这样的节点:至少部分下游再无分支路径
。为了避免
在路径中创建多达15 个稀疏 NULL 节点
,我们通过设置一个形式为[ encodedPath, key ]
的extension 节点
来精简向下遍历
,其中encodedPath
包含要跳过的部分路径
,key
用于下一次数据库查询 - 对于
Leaf
节点,encodedPath
编码了path
剩余的片段,我们可以直接查询value
由于 Extension
和 Leaf
都是用 encodedPath
,所有需要制定规则进行区分:
二进制表示 | node类型 | path长度奇偶性 | 16进制表示 | 待编码path | 编码后path | |
---|---|---|---|---|---|---|
🦀 | 0000 | Extension | 偶数 | 0 | [ 0, 1, 2, 3, 4, 5, …] | ‘00 01 23 45’ |
🦞 | 0001 | Extension | 奇数 | 1 | [ 1, 2, 3, 4, 5, …] | ‘11 23 45’ |
🦐 | 0010 | Leaf | 偶数 | 2 | [ 0, f, 1, c, b, 8, 10] | ‘20 0f 1c b8’ |
🦑 | 0011 | Leaf | 奇数 | 3 | [ f, 1, c, b, 8, 10] | ‘3f 1c b8’ |
可以观察到,当
path
长度是奇数时,会将16进制表示的类型
和待编码path的第一个半字节
结合,大家可以思考下为什么这样做,温馨提示:path时基于半字节的
举例说明
path
和 value
如下:
<64 6f> : 'verb'
<64 6f 67> : 'puppy'
<64 6f 67 65> : 'coin'
<68 6f 72 73 65> : 'stallion'