1. 程式人生 > >【區塊鏈學習】Merkle Patricia Tree (MPT) 以太坊中的默克爾樹

【區塊鏈學習】Merkle Patricia Tree (MPT) 以太坊中的默克爾樹

本篇博文是自己學習mpt的過程,邊學邊記錄,很多原理性內容非自己原創,好的博文將會以連結形式進行共享。

一、什麼是mpt

MPT是以太坊中的merkle改進樹,基於基數樹,即字首樹改進而來,大大提高了查詢效率。

二、字首樹

MPT中的P,就是字首樹,也叫trie或字典樹。trie每個節點是一個確定長度的陣列,每個節點的值指向子節點的指標,最後還有一組標誌位,用來標誌到此是否是一個完整的字串,並且有幾個這樣的字串。 如下圖是一個常見的用來存英文單詞的trie:(圖轉自http://blog.csdn.net/zslomo/article/details/53434883) 傳統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)