以太坊 Merkle Patricia Tree 全解析

以太坊 Merkle Patricia Tree 全解析

本文参考:

Patricia Tree - ethereum/wiki

Data structure in Ethereum | Episode 1+: Compact (Hex-prefix) encoding.

Data structure in Ethereum | Episode 3: Patricia trie.

以太坊中的Merkle Patricia Tree

干货 | Merkle Patricia Tree 详解

以太坊MPT原理,你最值得看的一篇


1. 概述

Merkle Patricia Tree(又称为Merkle Patricia Trie)是一种经过改良的、融合了Merkle tree和前缀树两种树结构优点的数据结构,是以太坊中用来组织管理账户数据、生成交易集合哈希的重要数据结构。

MPT树有以下几个作用:

  • 存储任意长度的key-value键值对数据,符合以太坊的state模型;
  • 提供了一种快速计算所维护数据集哈希标识的机制;
  • 提供了快速状态回滚的机制;
  • 提供了一种称为默克尔证明的证明方法,进行轻节点的扩展,实现简单支付验证;

由于MPT结合了Radix trie和Merkle两种树结构的特点与优势 ,因此在介绍MPT之前,我们首先简要地介绍下这两种树结构的特点。


2. Radix trie

Trie树,又称前缀树或字典树,是一种有序树,用于保存关联数组。其中的键通常是字符串。与二叉查找树不同,键不是直接保存在节点中,而是由节点在树中的位置决定 。一个节点的所有子孙都有相同的前缀,也就是这个节点对应的字符串,而 根节点对应空字符串。

一般情况下,不是所有的节点都有对应的值,只有叶子节点和部分内部节点所对应的键才有相关的值。实际上trie每个节点是一个确定长度的数组,数组中每个节点的值是一个指向子节点的指针,最后有个标志域,标识这个位置为止是否是一个完整的字符串.

常见的用来存英文单词的trie每个节点是一个长度为27的指针数组,index0-25代表a-z字符,26为标志域。如图:



优势

相比于哈希表,使用前缀树来进行查询拥有共同前缀key的数据时十分高效,例如在字典中查找前缀为pre的单词,对于哈希表来说,需要遍历整个表,时间效率为O(n),然而对于前缀树来说,只需要在树中找到前缀为pre的节点,且遍历以这个节点为根节点的子树即可。

但是对于最差的情况(前缀为空串),时间效率为O(n),仍然需要遍历整棵树,此时效率与哈希表相同。

相比于哈希表,在前缀树不会存在哈希冲突的问题。

劣势

  • 直接查找效率低下 前缀树的查找效率是O(m),m为所查找节点的key长度,而哈希表的查找效率为O(1)。且一次查找会有m次IO开销,相比于直接查找,无论是速率、还是对磁盘的压力都比较大。
  • 可能会造成空间浪费 当存在一个节点,其key值内容很长(如一串很长的字符串),当树中没有与他相同前缀的分支时,为了存储该节点,需要创建许多非叶子节点来构建根节点到该节点间的路径,造成了存储空间的浪费。


3. Patricia trie

他是一种更节省空间的Trie。对于基数树的每个节点,如果该节点是唯一的儿子的话,就和父节点合并。



4. Merkle tree

Merkle树是由计算机科学家 Ralph Merkle 在很多年前提出的,并以他本人的名字来命名,由于在Bitcoin网络中用到了这种数据结构来进行数据正确性的验证,在这里简要地介绍一下merkle树的特点及原理。

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


特点

  • Merkle tree是一种树,大多数是二叉树,也可以多叉树,无论是几叉树,它都具有树结构的所有特点;
  • Merkle tree叶子节点的value是数据项的内容,或者是数据项的哈希值;
  • 非叶子节点的value根据其孩子节点的信息,然后按照Hash算法计算而得出的;


将相邻两个节点的哈希值合并成一个字符串,然后计算这个字符串的哈希,得到的就是这两个节点的父节点的哈希值。

如果该层的树节点个数是单数,那么对于最后剩下的树节点,这种情况就直接对它进行哈希运算,其父节点的哈希就是其哈希值的哈希值(对于单数个叶子节点,有着不同的处理方法,也可以采用复制最后一个叶子节点凑齐偶数个叶子节点的方式)。循环重复上述计算过程,最后计算得到最后一个节点的哈希值,将该节点的哈希值作为整棵树的哈希。

若两棵树的根哈希一致,则这两棵树的结构、节点的内容必然相同。

优势

  • 快速重哈希

Merkle tree的特点之一就是当树节点内容发生变化时,能够在前一次哈希计算的基础上,仅仅将被修改的树节点进行哈希重计算,便能得到一个新的根哈希用来代表整棵树的状态。

  • 轻节点扩展

采用Merkle tree,可以在公链环境下扩展一种“轻节点”。轻节点的特点是对于每个区块,仅仅需要存储约80个字节大小的区块头数据,而不存储交易列表,回执列表等数据。然而通过轻节点,可以实现在非信任的公链环境中验证某一笔交易是否被收录在区块链账本的功能。这使得像比特币,以太坊这样的区块链能够运行在个人PC,智能手机等拥有小存储容量的终端上。

对于轻节点来说,验证一条交易只需要验证包含该交易的路径即可,并不需要把所有交易的Hash全部重新算一遍。


劣势

  • 存储空间开销大


5. MPT(Merkle Patricia Trees)

概念

在深入MPT数据结构之前,我们先了解一下如下概念:

  • 世界状态:在以太坊中,所有账户(包括合约账户、普通账户)的状态数据统称为世界状态;
  • 轻节点:指只存储区块头数据的区块链节点;
  • 区块链分叉:指向同一个父块的2个区块被同时生成的情况,某些部分的矿工看到其中一个区块,其他的矿工则看到另外一个区块。这导致2种区块链同时增长;
  • 区块头:指以太坊区块结构体的一部分,用于存储该区块的头部信息,如父区块哈希、世界状态哈希、交易回执集合哈希等。区块头仅存储一些“固定”长度的哈希字段;

MPT树中的节点

  • 空节点(NULL) - represented as the empty string

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

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

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

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

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

  • 扩展节点(extension) - a 2-item node [ encodedPath, key ]

也是[key,value]的一个键值对 ,但是这里的 value是其他节点的hash值 ,这个 hash可以被用来查询数据库中的节点。也就是说通过hash链接到其他节点

因此,有两种[key,value]节点(叶节点和扩展节点):

以太坊中对Key的编码

在以太坊中,MPT树的key值共有三种不同的编码方式,以满足不同场景的不同需求。

三种编码方式分别为:

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

Raw编码

Raw编码就是原生的key值,不做任何改变。这种编码方式的key,是MPT对外提供接口的默认编码方式。

例如一条key为“cat”,value为“dog”的数据项,其key的Raw编码就是[‘c’, ‘a’, ‘t’],换成ASCII表示方式就是[63, 61, 74](Hex)

Hex编码

Hex编码就是把一个8位的字节数据用两个十六进制数展示出来,编码时,将8位二进制码重新分组成两个4位的字节,其中一个字节的低4位是原字节的高四位,另一个字节的低4位是原数据的低4位,高4位都补0,然后输出这两个字节对应十六进制数字作为编码。Hex编码后的长度是源数据的2倍。

Exp:

ASCII码:A (65) 二进制码:0100_0001 重新分组:0000_0100 0000_0001 十六进制: 4 1 Hex编码:41

若该Key对应的节点存储的是真实的数据项内容(即该节点是叶子节点),则在末位添加一个ASCII值为16的字符作为terminator;

若该key对应的节点存储的是另外一个节点的哈希索引(即该节点是扩展节点),则不加任何字符;

[‘c’,’a’,’t’] -> [6,3,6,1,7,4,16]

HP编码

目的:

  • 区分leafextension
  • 把奇数路径变成偶数路径

步骤:

  • 如果有terminator(16)那么就去掉terminator。
  • 根据表格给key加上prefix
node type    path length    |    prefix    hexchar
--------------------------------------------------
extension    even           |    0000      0x0
extension    odd            |    0001      0x1
leaf         even           |    0010      0x2
leaf         odd            |    0011      0x3

如果prefix是0x0或者0x2,加一个padding nibble 0 在prefix后面,所以最终应该是 0x00 和 0x20。原因是为了保证key(path)的长度为偶数。

例子: 末尾的字符“16”说明该节点为叶子结点,并且加上了0x20

[ 0, f, 1, c, b, 8, 16] -> '20 0f 1c b8'

编码转换关系

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

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



MPT的结构

MPT树的特点如下:

  • 叶子节点和分支节点可以保存value, 扩展节点保存key;
  • 没有公共的key就成为2个叶子节点;key1=[1,2,3] key2=[2,2,3]
  • 有公共的key需要提取为一个扩展节点;key1=[1,2,3] key2=[1,3,3] => ex-node=[1],下一级分支node的key
  • 如果公共的key也是一个完整的key,数据保存到下一级的分支节点中;key1=[1,2] key2=[1,2,3] =>ex-node=[1,2],下一级分支node的key; 下一级分支=[3],上一级key对应的value

简单的结构如下图:



我们将存入如下state数据:

key  	 |  values
----------------------
a711355  |  45.0 ETH
a77d337  |  1.00 WEI
a7f9365  |  1.1  ETH
a77d397  |  0.12 ETH

插入第一个<a711355, 45>,由于只有一个key,直接用leaf node既可表示



接着插入a77d337,由于和a711355共享前缀’a7’,因而可以创建’a7’扩展节点。



接着插入a7f9365,也是共享’a7’,只需新增一个leaf node.



最后插入a77d397,这个key和a77d337共享’a7’+’d3’,因而再需要创建一个’d3’扩展节点



将叶子节点和最后的short node合并到一个节点了,事实上源码实现需要再深一层,最后一层的叶子节点只有数据



// nodeFlag contains caching-related metadata about a node.
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
}

MPT节点有个flag字,nodeFlag,记录了一些辅助数据:

  • 节点哈希:若该字段不为空,则当需要进行哈希计算时,可以跳过计算过程而直接使用上次计算的结果(当节点变脏时,该字段被置空);
  • 诞生标志:当该节点第一次被载入内存中(或被修改时),会被赋予一个计数值作为诞生标志,该标志会被作为节点驱除的依据,清除内存中“太老”的未被修改的节点,防止占用的内存空间过多;
  • 脏标志:当一个节点被修改时,该标志位被置为1;

flag.hash会保存该节点采用merkle tree类似算法生成的hash。同时会将hash和源数据以<hash, node.rlp.rawdata>方式保存在leveldb数据库中。这样后面通过hash就可以反推出节点数据。具体结构如下(蓝色的hash部分就是flag.hash字段)



核心思想

hash可以还原出节点上的数据,这样只需要保存一个root(hash),即可还原出完整的树结构,同时还可以按需展开节点数据,比如如果只需要访问<a771355, 45>这个数据,只需展开h00, h10, h20, h30这四个hash对应的节点


希望能和对区块链感兴趣的朋友多多交流,我的blog地址:

区块链可拓展技术:侧链(Sidechain)dinghaoli.github.io图标

发布于 2018-10-14

文章被以下专栏收录