参考
官网解释
https://github.com/ethereum/wiki/wiki/Patricia-Tree
牛逼啊,一文讲清楚了以太坊的MPT
https://blog.csdn.net/itleaks/article/details/79992072
概述
MPT,全称Merkle Patricia Trie,以太坊中用来存储用户账户的状态及其变更、交易信息、交易的收据信息。
看其全称便大概知道MPT融合了MerkleTree,Trie,Patricia Trie这三种数据结构的有点,从而最大限度地快速实现查找功能并节省空间。
- Trie:单词查找树,根据前缀匹配,比如输入法的前缀模糊匹配,都用这种树做数据结构,快速匹配
- Patricia Trie:紧凑前缀树,是一种空间使用率经过优化的 Trie
- Merkle Patricia Trie: 把每个Node都做Hash计算获取Node.Hash,然后键值对的方式存储在数据库当中
PMT在以太坊中的运用
https://github.com/ethereum/wiki/wiki/Patricia-Tree
以太坊的MPT实际上是一种key/value键值对存储的树形版本,因为如果不做特殊处理,1w个不同的key,会有1w个value,每次操作数据时,都要遍历,这样查询的效率很低。
所以按照前缀匹配的法则,在key/value的基础上,引入了树的概念(类似于 JDK1.8的 hashmap的数据结构,引入了红黑树),这样大大提升了查询的速度,就像查字典一样。
以太坊里面有4个Root,分别存储了不同的信息
- stateRoot:存储了当前区块的所有用户的最终状态:地址、余额、合约等信息,path:sha3(ethereumAddress)
- transactionsRoot:存储了当前区块的所有的交易的信息,path: rlp(transactionIndex)
- receiptsRoot:存储了当前区块的所有的交易收入的信息,path: rlp(transactionIndex)
- storageRoot: 存储了当前Account的所有相关的合约信息
以太坊比比特币做了一个更多内容的压缩存储,比特币的root仅仅是该区块的所有的交易的merkle Tree,而以太坊不仅仅有交易树,还有收款人树,还有全局的账户树和用户的智能合约树,从而开放更多的功能。
代码展示
Block.Header里面的部分代码 (状态树、交易树、收据树)
Root common.Hash `json:"stateRoot" /
TxHash common.Hash `json:"transactionsRoot"
ReceiptHash common.Hash `json:"receiptsRoot"
state_object.go Line98
type Account struct {
Nonce uint64
Balance *big.Int
Root common.Hash // merkle root of the storage trie
CodeHash []byte
}
Trie结构
树其实就是一个根节点,因为根据根节点就可以找到整个树
type Trie struct {
root node //根节点
db Database //数据库相关,在下面再仔细介绍
originalRoot common.Hash //初次创建trie时候需要用到
cachegen, cachelimit uint16 //cache次数的计数器,每次Trie的变动提交后自增
}
节点的结构
以太坊中按照业务功能分,有4种类型的结点:
- 叶子节点(leaf),表示为[key,value]的一个键值对。和前面的英文字母key不一样,这里的key都是16编码出来的字符串,每个字符只有0-f 16种,value是RLP编码的数据
- 扩展节点(extension),也是[key,value]的一个键值对,但是这里的value是其他节点的hash值,通过hash链接到其他节点
- 分支节点(branch),因为MPT树中的key被编码成一种特殊的16进制的表示,再加上最后的value,所以分支节点是一个长度为17的list,前16个元素对应着key中的16个可能的十六进制字符,如果有一个[key,value]对在这个分支节点终止,最后一个元素代表一个值,即分支节点既可以搜索路径的终止也可以是路径的中间节点。分支节点的父亲必然是extension node
- 空节点,代码中用null表示
- 分支Node
Node:[i0, i1 ... in, value]
每个slot要么为null,要么有一个指针指向下一个Node,一个fullNode有 [17]node,因为采用16进制+value值
branch的value值,表示的是上一个节点的RLP编码的数据,也就是父节点最终的值,当然value也可以为空,说明并没有存储到父节点截止的数据
-
叶子节点:叶子节点的value值是RLP编码的数据,也就是最终的值
-
扩展节点:体现了Patricia的特征,可以合并一起的前缀就不分开,可以减少查询的深度。另外扩展节点下面一定是分支节点,不然就不需要扩展了,直接叶子节点了。所以扩展节点的value值,指向的是下一个分支节点的地址
在以太坊的系统当中,其实分为2大类的节点的数据结构:
- shortNode key/value的结构(叶子节点、扩展节点)
- FullNode 数组节点(分支节点)
fullNode struct {
Children [17]node
flags nodeFlag
}
shortNode struct {
Key []byte
Val node
flags nodeFlag
}
type nodeFlag struct {
hash hashNode // cached hash of the node (may be nil)
gen uint16 // cache generation counter
dirty bool // whether the node has changes that must be written to the database
}
hashNode []byte
valueNode []byte
分析
不管是fullNode还是shortNode,在以太坊中,都是通过指针引用直接获取的,比如shortNode.Val 就直接指向下一级的Node,并不是通过hashNode查询的,所以一次加载,必须加载一个完整的树到内存中,但是存储的最终的value对象是一个RPL格式的数组valueNode []byte
不管是 fullNode还是shortNode都有一个flags属性,flags又有一个hashNode的属性,所以每个节点都有hashNode的属性值
叶子节点,携带数据部分的RLP哈希值,数据的RLP编码值作为valueNode的匹配项存储在数据库里,也就是叶子节点的shortNode.Val --> 指向valueNode []byte
分支节点之所以是17个slot,并且依次 0、1、2...f是因为把key压缩成了16进制,这样key的长度就不长了。
MPT的优点:
- 查询快,因为是Patricia Trie,前缀相同会压缩
- 更新变动小,可以快速定位位置,且一个节点的改动,不会影响太多不关联的节点,改动小
举个简单的例子
根据Address,查询该用户的余额
- 先获取最新的Block.Root,这个就是stateRoot的rootHash,根据这个rootHash去内存中查询对应的root节点(节点启动的时候,必须加载一棵完整的内存树出来)
- Sha3(Address)获取到path,然后根据path去rootNode中根据MPT规则去找到对应的valueNode,也就是rpl(Account) 的值
- 根据 valueNode然后decodeRpl(Account),得到Account,读取Account.balance
以太坊的存储
以太坊中使用的数据库是levelDB
注意事项
MTP中,key = Sha3(Account.address),所以是安全的,即使你拥有一颗完整的Tree,你不知道 address,你也是无法推测出来,哪个账户里面有多少钱,而一个address是uint160的整数,穷举的话,基数也很大,所以具有一定的私密性。
MTP就像一个公共密码箱,谁都可以去尝试打开,但是只有拥有钥匙的人才能打开自己的柜子。