Merkle Patricia Tree(MPT)

区块链一个重要的亮点就是防篡改,那么它是怎么做到防篡改的呢?其中一个重要的知识点就是Merkle Patricia Tree(MPT),本篇就来解析下何为MPT。

MPT是一种加密认证的数据结构,它融合了Merkle树和Patricia Trie树(基数树/压缩前缀树)两种数据类型的优点。

则在介绍MPT树之前先介绍下Merkle树(默克尔树)、Trie树(前缀树)和Patricia Trie(基数树/压缩前缀树),介绍Trie树是因为Patricia Trie是基于Trie树衍化来的。

Trie树

Trie树又称前缀树或字典树,是一种检索树,使用一个有序的树结构存储一个动态数据集或者关联数组,其中的键通常是字符串。与二叉查找树不同,键不是直接保存在节点中,而是由节点在树中的位置决定。一个节点的所有子孙相对于当前节点都有相同的前缀,而根节点为空字符串。一般情况下,不是所有的节点都有对应的值,只有叶子节点和部分内部节点所对应的键才有相关的值。

Trie树中,key是从树根到对应value得真实的路径。即从根节点开始,key中的每个字符会标识走那个子节点从而到达相应value。Value被存储在叶子节点,是每条路径的终止。假如key来自一个包含N个字符的字母表,那么树中的每个节点都可能会有多达N个孩子,树的最大深度是key的最大长度。看个例子画个图就了然了。
例子:关键字集合{“a”, “to”, “tea”, “ted”, “i”, “in”, “inn”},此集合转为Trie树为
tire

不理想情况下,数据集中存在一个很长的key,而这个key与其它key又没有太多的公共前缀,这就造成整个树的深度会加大,需要存储多个节点,存储比较稀疏而且极不平衡。
例子:关键字集合{“algori”, “to”, “tea”, “ted”, “i”, “in”, “inn”},此集合转为Trie树为
稀疏Trie树

Patricia Trie树

既然Trie树在某些情况下存储空间利用率不高,那就给压缩下,然后就出现了Patricia Trie树。

Patricia Trie树是一种空间使用率经过优化的Trie树。与Trie树不同的是,Patricia Trie 里如果存在一个父节点只有一个子节点,那么这个父节点将与其子节点合并。这样压缩存储可以减少Trie树中不必要的深度,大大加快搜索节点速度。
如下图所示
Patricia Trie树

Merkle树

Merkle树是由计算机科学家Ralph Merkle在很多年前提出的,并以他本人的名字来命名,是一种树形数据结构,可以是二叉树,也可以是多叉树。
它由若干叶节点、中间节点和一个根节点构成。最下面的叶节点包含基础数据,每个中间节点是它子节点的散列,根节点是它的子节点的散列,代表了Merkle树的根部。

由于Merkle树是自底向上构建的,而且除叶子结点之外的其它节点都是其子节点的散列,这样每个节点的值发生变化都会一层一层的向上反映,最终在根节点上表现出来。也就是说只要对比两个Merkle树的根节点是否相等就能得到两份数据集是否一样,而且还可以验证Merkle树的某个分支。

比特币使用Merkle树存储一个区块中的所有交易信息,一是为了防篡改,因为散列是向上的,伪造任何一个节点都会引起上层节点的改动,最终导致根节点的变化。二是为了允许区块的数据可以零散的传送,即节点可以从一个节点下载区块头,从另外的源下载与其相关的树的其他部分,而依然能够确认所有的数据都是正确的。

看下图的例子,首先将L1-L4四个单元数据散列化,然后将散列值存储至相应的叶子节点。这些节点是Hash0-0, Hash0-1, Hash1-0, Hash1-1,然后将相邻两个节点的散列值合并成一个字符串,然后计算这个字符串的散列,得到的就是这两个节点的父节点的散列值。
merkle树

在比特币网络中,merkle树被用来归纳一个区块中的所有交易,同时生成整个交易集合的数字指纹。此外,由于merkle树的存在,使得在比特币这种公链的场景下,扩展一种“轻节点”实现简单支付验证变成可能。

知道了Merkle树在比特币中的应用,那么他是怎么构成呢?现在就简单看下其构成。它由一组叶节点、一组中间节点和一个根节点构成。最下面的叶节点包含基础数据,每个中间节点是它的子节点的散列,根节点是它的子节点的散列,代表了Merkle树的根部 。

Merkle树具有下列特性:

  1. 每个数据集对应一个唯一合法的根散列值。
  2. 很容易更新、添加或者删除树节点,以及生成新的根散列值 。
  3. 不改变根散列值的话就没有办法修改树的任何部分,所以如果根散列值被包括在签名的文档或有效区块中,就可以保证这棵树的正确性。
  4. 任何人可以只提供一个到特定节点的分支,并通过密码学方法证明拥有对应内容的节点确实在树里 。

Merkle Patricia Tree

叨叨了那么多,本篇的主角终于出来了。
Merkle Patricia Tree结合了Merkle树和Patricia树的特点,并针对以太坊的使用场景进行了一些改进。

首先,为了保证树的加密安全,每个节点通过它的散列值被引用,则根节点是一层一层散列向上收敛而得,被称为整棵树的加密签名,如果一棵给定 Trie树的根散列值是公开的,那么所有人都可以提供一种证明,即通过提供每步向上的路径证明特定的key是否含有特定的值。在当前的以太坊版本中,MPT存储在LevelDB数据库中

其次,MPT树引人了很多节点类型来提高效率。包括以下4种:

  1. 空节点(NULL) – represented as the empty string

    简单的表示空,在代码中是一个空串。

  2. 叶子节点(leaf) – a 2-item node [encodedPath, value]

    表示为[key,value]的一个键值对,其中key是key的一种特殊十六进制编码(MP编码), value是value的RLP编码

  3. 分支节点(branch) – a 17-item node [v0 … v15, vt]

    因为MPT树中的key被编码成一种特殊的16进制的表示,再加上最后的value,所以分支节点是一个长度为17的list,前16个元素对应着key中的16个可能的十六进制字符,如果有一个[key,value]对在这个分支节点终止,最后一个元素代表这个值value,即分支节点既可以是搜索路径的终止也可以是路径的中间节点。

  4. 扩展节点(extension) – a 2-item node [encodedPath, key]
    也是[key,value]的一个键值对,但是这里的value是其他节点的hash值,这个hash可以被用来查询数据库中的节点。也就是说通过hash链接到其他节点。

下面看个以太坊中MPT树的官方示例
MPT实例

示例

看到这里你肯定依然是一脸懵逼,(不要问我我是怎么知道的。。。。),接下来我们就来根据一批数据构造一个MPT树,这样你会理解一些。
但是在讲例子之前,还得先介绍几种编码方式,要不看例子的时候对key的取值会比较懵。

key编码

在以太坊中,MPT树的key值共有三种不同的编码方式,分别为:

  1. Raw编码(原生的字符)
  2. Hex编码(扩展的16进制编码)
  3. Hex-Prefix编码(16进制前缀编码)

Raw编码

Raw编码就是原生的key值,不做任何改变。这种编码方式的key,是MPT对外提供接口的默认编码方式。
例如key为”dog”,value为”puppy”的数据项,其key的Raw编码就是[‘d’, ‘o’, ‘g’],换成ASCII表示方式就是[64, 6f, 67] : dog => ‘puppy’

Hex编码

Hex编码就是把一个8位的字节数据根据高4位和低4位拆解为两个4位的数字,然后两个数字的高4位都补0,最后将补完之后的两个字节编码为16进制。
这里需要注意的是key对应的值为真实的数据项,即是一个有意义的kv对,比如dog->puppy(也就是叶子结点),而不是key->节点hash(也就是分支节点),则在末尾添加一个ASCII值为16(十六进制为0x10)的字符串作为terminator。如果是分支节点则不加任何字符
例如key为”dog”,value为”puppy”的数据项,其key中d(100)的二进制编码为01100100,拆解为两个4位的数字为0110和0100,高位补0之后的二进制为00000110和00000100,16进制为6和4,依次编码o和g。又因为dog对应的value是真实的数据项,则在末尾添加16。最后最终的Hex编码为[ 6, 4, 6, 15, 6, 7, 16 ] : dog => ‘puppy’

Hex-Prefix编码

首先根据名字得知这是一个前缀编码,是在原key的基础上加上一些标识作为前缀,其次我们看下这种编码方式的目的:

  1. 区分leaf和extension
  2. 把奇数路径变成偶数路径

则这个前缀的生成肯定与leaf、extension和key的奇偶相关了,下面看下编码步骤为:

  1. 先按Hex对key进行编码
  2. key的结尾如果是0x10(也就是16),则去掉这个终止符
  3. key之前补一个四元组,这个四元组第0位区分奇偶信息,第1位区分节点类型,表格表示如下
node type path length prefix hexchar
extension even 0000 0x0
extension odd 0001 0x1
leaf even 0010 0x2
leaf odd 0011 0x3
  1. 如果输入key的长度是偶数,则在之前的四元组后再添加一个四元组0x0
  2. 将原来的key内容压缩,将分离的两个byte以高四位低四位进行合并

这里有个名词解释下,四元组是四个bit位的组合(例如二进制表达的0010就是一个四元组),其中Nibble就是一个四元组,是key的基本单元。

例如key为”dog”,value为”puppy”的数据项,则首先对dog进行Hex编码[ 6, 4, 6, 15, 6, 7, 16 ],然后按照上面的步骤进行编码,dog的Hex编码末尾是16,去掉终止符为[ 6, 4, 6, 15, 6, 7 ],准备在key前补四元组,dog为叶子节点并且编码之后的长度为6是偶数,所以四元组为0010(0x2),此时到了第4步,key编码之后的长度是偶数,则在前一个四元组(0x2)之后再添加一个四元组0x0,最终的前缀为0x20。最后将原来的key内容进行高低四位合并,最终的Hex-Prefix编码是[ 0x20, 0x64, 0x6f, 0x67 ]

以上三种编码方式的转换关系为:

Raw编码: 原生的key编码,是MPT对外提供接口中使用的编码方式,当数据项被插入到树中时,Raw编码被转换成Hex编码
Hex编码: 16进制扩展编码,用于对内存中树节点key进行编码,当树节点被持久化到数据库时,Hex编码被转换成HP编码
HP编码: 16进制前缀编码,用于对数据库中树节点key进行编码,当树节点被加载到内存时,HP编码被转换成Hex编码

构造过程

接下来我们就实际操作下:
假设我们有一个树有这样一些kv对(‘dog’, ‘puppy’), (‘horse’, ‘stallion’), (‘do’, ‘verb’), (‘doge’, ‘coin’)。
首先,我们将key转成Hex编码,如下:

1
2
3
4
5
6
7
8
# 64 6f
[ 6, 4, 6, 15, 16 ] : do => 'verb'
#64 6f 67
[ 6, 4, 6, 15, 6, 7, 16 ] : dog => 'puppy'
#64 6f 67 65
[ 6, 4, 6, 15, 6, 7, 6, 5, 16 ] : doge => 'coin'
#68 6f 72 73 65
[ 6, 8, 6, 15, 7, 2, 7, 3, 6, 5, 16 ] : horse => 'stallion'

其构造MPT树如图:
MPT

1
2
3
4
5
6
7
8
rootHash: [ <16>, hashA ]
hashA: [ <>, <>, <>, <>, hashB, <>, <>, <>, hashC, <>, <>, <>, <>, <>, <>, <>, <> ]
hashC: [ <20 6f 72 73 65>, 'stallion' ]
hashB: [ <00 6f>, hashD ]
hashD: [ <>, <>, <>, <>, <>, <>, hashE, <>, <>, <>, <>, <>, <>, <>, <>, <>, 'verb' ]
hashE: [ <17>, hashF ]
hashF: [ <>, <>, <>, <>, <>, <>, hashG, <>, <>, <>, <>, <>, <>, <>, <>, <>, 'puppy' ]
hashG: [ <35>, 'coin' ]

构造过程为

  1. 假设插入第一个kv对是do->verb,key根据Hex-Prefix编码为[‘0x20’, ‘0x64’, ‘0x6f’],MPT树为:
    叶子结点
  2. 接着插入第二个kv对dog->puppy,dog和do的公共前缀为[‘64’, ‘6f’],do是叶子结点A,将叶子结点替换成扩展节点A,并将新key与叶子节点A的公共前缀(646f)作为扩展节点的key,扩展节点的value是新分支节点B的hash值,然后将do和dog剩余的key插入到分支节点中,由于do没有剩余的key,则将对应的value写入分支节点B的value中,dog剩余的key为67,则分支节点B中的路径为6,指向叶子结点C。7为dog剩下的最后一个key值,是奇数叶子结点则前缀为3,所有最终的MPT树为:
    叶子结点插入
  3. 再插入第三个kv对doge->coin,按照上面的步骤生成的MPT树是:
  4. 最后插入第四个kv对horse->stallion,horse与其它key的公共前缀只有6,则将扩展节点A新增一个分支节点F作为其孩子节点,将新key与扩展节点A的公共前缀6作为扩展节点A的key,A是扩展节点并且key为奇数,所以前缀为1,value为新增分支节点F的hash。原扩展节点A变成新扩展节点之后剩余的key,与新key剩下的key,插入到分支节点F中,分别放入4和8中,4指向新的扩展节点G,8指向叶子节点H。最终的MPT树为:

MPT在以太坊中的应用

Merkle数据结构在区块链领域中使用比较广泛,比特币和以太坊都是用了Merkle树,其中以太坊中保留了三颗MPT,分别是状态树、交易树和收据树,这三种树可以帮助以太坊客户端做一些简易的查询,如查询某个账户的余额、某笔交易是否被包含在区块中等。

需要注意的是在以太坊中对MPT再进行了一次封装,对数据项的key进行了一次哈希计算sha3(key),value进行了RLP(Recursive length prefix encoding,递归长度前缀编码)编码,数据库中存储额外的sha3(key)与key之间的对应关系。

参考

https://ethfans.org/toya/articles/588
https://github.com/ethereum/wiki/wiki/Patricia-Tree
https://stackoverflow.com/questions/14708134/what-is-the-difference-between-trie-and-radix-trie-data-structures
https://cs.stackexchange.com/questions/63048/what-is-the-difference-between-radix-trees-and-patricia-tries

您的肯定,是我装逼的最大的动力!