【区块链学习】Merkle Patricia Tree (MPT) 以太坊中的默克尔树

本篇博文是自己学习mpt的过程,边学边记录,很多原理性内容非自己原创,好的博文将会以链接形式进行共享。

一、什么是mpt

MPT是以太坊中的merkle改进树,基于基数树,即前缀树改进而来,大大提高了查找效率。

二、前缀树

MPT中的P,就是前缀树,也叫trie或字典树。trie每个节点是一个确定长度的数组,每个节点的值指向子节点的指针,最后还有一组标志位,用来标志到此是否是一个完整的字符串,并且有几个这样的字符串。
如下图是一个常见的用来存英文单词的trie:(图转自http://blog.csdn.net/zslomo/article/details/53434883)

【区块链学习】Merkle Patricia Tree (MPT) 以太坊中的默克尔树_第1张图片
传统trie树也存在一些不足:
1.当某一字符串很长或者与其他字符串没有相同前缀,构造的树会极其不平衡;
2.传统trie是由内存指针链接,且字符串的值没有编码,明文直接显示,安全系数低。

三、以太坊的改进

针对传统trie的不足,以太坊进行了一些改进,http://www.cnblogs.com/fengzhiwu/p/5584809.html 这篇博文对于原理的讲解详细易懂,GitHub的文章https://github.com/ethereum/wiki/wiki/Patricia-Tree也很好,该篇文章的翻译http://me.tryblockchain.org/Ethereum-MerklePatriciaTree.html


下文进行简单的阐述:

1.以太坊增加了两个节点,叶子结点和扩展节点,所以MPT中共存在四类节点:空节点、叶子节点、扩展节点和分支节点。

标准的叶子结点,形式为[key,value]的列表,其中key是key的一种十六进制编码,value是value的RLP编码(RLP编码将在下文解释)。


扩展节点,也是[key,value]的形式,但不同于叶子节点,这里的value是可以链接到其他节点的hash,可以被用来查询数据库中的节点。


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


MPT还有一个重要的概念是特殊的十六进制前缀编码(GitHub文中有提到),十六进制序列的带可选结束标记的压缩编码。传统的编码十六进制字符串的方式,是将他们转为了十进制。比如0f1248表示的是三个字节的[15,18,72]。然而,这个方式有点小小的问题,如果16进制的字符长度为奇数呢。在这种情况下,就没有办法知道如何将十六进制字符对转为十进制了。额外的,MPT需要一个额外的特性,十六进制字符串,在结束节点上,可以有一个特殊的结束标记(一般用T表示)。结束标记仅在最后出现,且只出现一次。或者说,并不存在一个结束标记,而是存在一个标记位,标记当前节点是否是一个最终结点,存着我们要查找的值。如果不含结束标记,则是表明还需指向下一个节点继续查找。


为了解决上述的这些问题。我们强制使用最终字节流第一个半字节(半个字节,4位,也叫nibble),编码两个标记位。标记是否是结束标记和当前字节流的奇偶性(不算结束标记),分别存储在第一个半字节的低两位。如果数据是偶数长,我们引入一个0值的半字节,来保证最终是偶数长,由此可以使用字节来表示整个字节流。


一个例子:

> [ 1, 2, 3, 4, 5 ]
'\x11\x23\x45'  ( Here in python, '\x11#E' because of its displaying unicodes. ) 
//不含结束,所以没有结束标记,由于字节流是奇数,标记位取值1,不补位,所以前面只补一个半字节就好。
> [ 0, 1, 2, 3, 4, 5 ]
'\x00\x01\x23\x45'
//不含结束标记的偶数,且由于是偶数第一个nibble是0,由于是偶数位,需要补一个值为零的nibble,所以是00。紧跟后面的值。
> [ 0, 15, 1, 12, 11, 8, T ]
'\x20\x0f\x1c\xb8'
//由于有结束标记,除结束标记的长度为偶数,所以第一个nibblie是2,由于是偶数长补位一个值为0的nibble,所以最后加20。
> [ 15, 1, 12, 11, 8, T ]
'\x3f\x1c\xb8'
//由于有结束标记,且为奇数,第一个值为3,又由于是奇数不需要补位,值是3加后面的值。

python编码:

def compact_encode(hexarray):
    term = 1 if hexarray[-1] == 16 else 0 
    if term: hexarray = hexarray[:-1]
    oddlen = len(hexarray) % 2
    flags = 2 * term + oddlen
    if oddlen:
        hexarray = [flags] + hexarray
    else:
        hexarray = [flags] + [0] + hexarray
    // hexarray now has an even length whose first nibble is the flags.
    o = ''
    for i in range(0,len(hexarray),2):
        o += chr(16 * hexarray[i] + hexarray[i+1])
    return o


2.安全性

首先,为了保证树的加密安全,每个节点通过他的hash被引用,而非32bit或64bit的内存地址,即树的Merkle部分是一个节点的确定性加密的hash。一个非叶节点存储在leveldb关系型数据库中,数据库中的key是节点的RLP编码的sha3哈希,value是节点的RLP编码。想要获得一个非叶节点的子节点,只需要根据子节点的hash访问数据库获得节点的RLP编码,然后解码就行了。通过这种模式,根节点就成为了整个树的加密签名,如果一颗给定trie的跟hash是公开的,那么所有人都可以提供一种证明,通过提供每步向上的路径证明特定的key是否含有给定的值。


MPT例子:

假设我们有一个树有这样一些值('dog', 'puppy'), ('horse', 'stallion'), ('do', 'verb'), ('doge', 'coin')。首先,我们将它们转为十六进制格式:

<64 6f> : 'verb'
<64 6f 67> : 'puppy'
<64 6f 67 65> : 'coin'
<68 6f 72 73 65> : 'stallion'

构造树:

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' ]

树的构造逻辑是root结点,要构造一个指向下一个结点的kv节点。先对键编码,由于当前节点不是结束结点,存值的键为奇数字符数,所以前导值为1,又由于奇数不补位,最终存键为0x16。它指向的是一个全节点A。下一层级,要编码的是d和h的第二个半字节,4和8。所以在A节点的第五个位置(从零开始)和第九个位置,我们可以看到分别被指向到了B和C两个节点。对于B节点往后do,dog,doge来说。他们紧接着的都是一个编码为6f的o字符。所以这里,B节点被编码为指向D的kv结点,数据是指向D节点的。其中键值存6f,由于是指向另一个节点的kv节点,不包含结束标记,且是偶数,需要补位0,得到00,最终的编码结果是006f。后续节点也以此类推。

附上GitHub的python代码:

def get_helper(node,path):
    if path == []: return node
    if node = '': return ''
    curnode = rlp.decode(node if len(node) < 32 else db.get(node))
    if len(curnode) == 2:
        (k2, v2) = curnode
        k2 = compact_decode(k2)
        if k2 == path[:len(k2)]:
            return get(v2, path[len(k2):])
        else:
            return ''
    elif len(curnode) == 17:
        return get_helper(curnode[path[0]],path[1:])

def get(node,path):
    path2 = []
    for i in range(len(path)):
        path2.push(int(ord(path[i]) / 16))
        path2.push(ord(path[i]) % 16)
    path2.push(16)
    return get_helper(node,path2)



你可能感兴趣的:(区块链学习)