以太坊之PMT

参考

官网解释
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,分别存储了不同的信息

  1. stateRoot:存储了当前区块的所有用户的最终状态:地址、余额、合约等信息,path:sha3(ethereumAddress)
  2. transactionsRoot:存储了当前区块的所有的交易的信息,path: rlp(transactionIndex)
  3. receiptsRoot:存储了当前区块的所有的交易收入的信息,path: rlp(transactionIndex)
  4. 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表示
  1. 分支Node
    Node:[i0, i1 ... in, value]
    每个slot要么为null,要么有一个指针指向下一个Node,一个fullNode有 [17]node,因为采用16进制+value值
    branch的value值,表示的是上一个节点的RLP编码的数据,也就是父节点最终的值,当然value也可以为空,说明并没有存储到父节点截止的数据
  1. 叶子节点:叶子节点的value值是RLP编码的数据,也就是最终的值


    image
  1. 扩展节点:体现了Patricia的特征,可以合并一起的前缀就不分开,可以减少查询的深度。另外扩展节点下面一定是分支节点,不然就不需要扩展了,直接叶子节点了。所以扩展节点的value值,指向的是下一个分支节点的地址


    image

在以太坊的系统当中,其实分为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

分析

  1. 不管是fullNode还是shortNode,在以太坊中,都是通过指针引用直接获取的,比如shortNode.Val 就直接指向下一级的Node,并不是通过hashNode查询的,所以一次加载,必须加载一个完整的树到内存中,但是存储的最终的value对象是一个RPL格式的数组valueNode []byte

  2. 不管是 fullNode还是shortNode都有一个flags属性,flags又有一个hashNode的属性,所以每个节点都有hashNode的属性值

  3. 叶子节点,携带数据部分的RLP哈希值,数据的RLP编码值作为valueNode的匹配项存储在数据库里,也就是叶子节点的shortNode.Val --> 指向valueNode []byte

  4. 分支节点之所以是17个slot,并且依次 0、1、2...f是因为把key压缩成了16进制,这样key的长度就不长了。

  5. MPT的优点:

  • 查询快,因为是Patricia Trie,前缀相同会压缩
  • 更新变动小,可以快速定位位置,且一个节点的改动,不会影响太多不关联的节点,改动小

举个简单的例子

根据Address,查询该用户的余额

  1. 先获取最新的Block.Root,这个就是stateRoot的rootHash,根据这个rootHash去内存中查询对应的root节点(节点启动的时候,必须加载一棵完整的内存树出来)
  2. Sha3(Address)获取到path,然后根据path去rootNode中根据MPT规则去找到对应的valueNode,也就是rpl(Account) 的值
  3. 根据 valueNode然后decodeRpl(Account),得到Account,读取Account.balance

以太坊的存储

以太坊中使用的数据库是levelDB

注意事项

MTP中,key = Sha3(Account.address),所以是安全的,即使你拥有一颗完整的Tree,你不知道 address,你也是无法推测出来,哪个账户里面有多少钱,而一个address是uint160的整数,穷举的话,基数也很大,所以具有一定的私密性。

MTP就像一个公共密码箱,谁都可以去尝试打开,但是只有拥有钥匙的人才能打开自己的柜子。

推荐阅读更多精彩内容