Merkle Patricia Tree 梅克爾帕特里夏樹(MPT)規范

Merkle Patricia Tree[1],梅克爾帕特里夏樹,提供了一個基于加密學的,自校驗防篡改的數據結構,用來存儲鍵值對關系。后文中將簡稱為MPT。盡管在本規范范圍內,我們限定鍵值的類型只能是字符串(但仍對所有的類型適用,因為只需提供一個簡單的序列化和反序化機制,將要存儲的類型與字符串進行轉換即可)。

MPT是確定的。確定性是指同樣內容的鍵值,將被保證找到同樣的結果,有同樣的根哈希。關于效率方面,對樹的插入,查找,刪除的時間復雜度控制在O(log(n))。相較于紅黑樹來說,MPT更好理解和編碼實現。

1. 前言:基數樹(Radix Tree)

在一個標準的基數樹里,要存儲的數據,按下述所述:

[i0, i1, ... iN, value]

其中的i0iN的表示一般是二進制或十六進制的格式的字母符號。value表示的是樹節點中存儲的最終值。每一個i0iN槽位的值,要么是NULL,要么是指向另一個節點的指針(在當前這個場景中,存儲的是其它節點的哈希值)。這樣我們就實現了一個簡單的鍵值對存儲。舉個例子來說,如果你想在這個基數樹中,找到鍵dog所對應的值。首先需要將dog轉換為比如ascii碼值(十六進制表示是646f67)。然后按字母序形成一個逐層向下的樹。沿著字母組成的路徑,在樹的底部葉節點上,即找到dog對應的值。具體來說,首先找到存儲這個鍵值對數據的根節點,找到下一層的第6個節點,然后再往下一層,找到節點4,然后一層一層往下找,直到完成了路徑 root -> 6 -> 4 -> 6 -> f -> 6 -> 7。這樣你將最終找到值的對應節點。

基數樹的更新和刪除操作比較簡單,可以按下面的定義:

def update(node,key,value):
    if key == '':
        curnode = db.get(node) if node else [ NULL ] * 17
        newnode = curnode.copy()
        newnode[-1] = value
    else:
        curnode = db.get(node) if node else [ NULL ] * 17
        newnode = curnode.copy()
        newindex = update(curnode[key[0]],key[1:],value)
        newnode[key[0]] = newindex
    db.put(hash(newnode),newnode)
    return hash(newnode)

def delete(node,key):
    if key == '' or node is NULL:
        return NULL
    else:
        curnode = db.get(node)
        newnode = curnode.copy()
        newindex = delete(curnode[key[0]],key[1:])
        newnode[key[0]] = newindex
        if len(filter(x -> x is not NULL, newnode)) == 0:
            return NULL
        else:
            db.put(hash(newnode),newnode)
            return hash(newnode)

1.1 數據校驗問題 - Merkle Tree

基數樹的節點關系,一般是使用比如C語言的32位或64位的內存地址指針來串聯起來的。但在以太坊中為了實現數據的防篡改及校驗,我們引入了Merkle Tree,使用節點的哈希值來建立節點關系。這樣,如果一個給定的前綴的根哈希值是已知的,那么任何人都可以根據這個前綴來檢查。對于一個攻擊者,不可能能證明一個不存在鍵值對存在,因為根哈希最終依賴所有的下面的哈希值,所以任何的修改都會導致根哈希值的改變。

1.2 效率問題 - Patricia樹

基數樹另一個主要的缺陷是低效。即使你只想存一個鍵值對,但其中的鍵長度有幾百字符長,那么每個字符的那個層級你都需要大量的額外空間。每次查找和刪除都會有上百個步驟。在這里我們引入Patricia樹來解決這個問題。

2. 核心規范

2.1 鍵數據的編碼算法

在介紹完整規范前,我們先介紹一個對鍵的編碼算法,十六進制序列的帶可選結束標記的壓縮編碼。傳統的編碼十六進制字符串的方式,是將他們轉為了十進制。比如0f1248表示的是三個字節的[15,18,72]。然而,這個方式有點小小的問題,如果16進制的字符長度為奇數呢。在這種情況下,就沒有辦法知道如何將十六進制字符對轉為十進制了。額外的,MPT需要一個額外的特性,十六進制字符串,在結束節點上,可以有一個特殊的結束標記(一般用T表示)。結束標記僅在最后出現,且只出現一次。或者說,并不存在一個結束標記,而是存在一個標記位,標記當前節點是否是一個最終結點,存著我們要查找的值。如果不含結束標記,則是表明還需指向下一個節點繼續查找。

為了解決上述的這些問題。我們強制使用最終字節流第一個半字節(半個字節,4位,也叫nibble),編碼兩個標記位。標記是否是結束標記和當前字節流的奇偶性(不算結束標記),分別存儲在第一個半字節的低兩位。如果數據是偶數長,我們引入一個0值的半字節,來保證最終是偶數長,由此可以使用字節來表示整個字節流。編碼方式可以參考:

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

上面的代碼可以看出來,如果要表示的是T結尾的字符串,term值取1,否則取0。如果為奇數長,取值1,否則取值0。由于term標記是兩個標記中的較高位,所以將term乘2來左移一位。如果不算后面的結束標記的字節流是奇數的,就不補位。如果是偶數數位,就補一個值為零的半字節。

一些實際的轉換例子:

> [ 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加后面的值。

2.2 Merkle Patricia Tree

MPT在解決低效的問題時,對當前的數據結構進行了一些改進。MPT的節點類型定義如下。

  • NULL(空字符串)
  • 兩個元素的數組[k, v](又名鍵值對節點)
  • 一個17個元素的數組。[v0 ... v15, vt]。(又名分支結點)

思路是,是當出現一些一個元素的節點,但卻有很長路徑的情況時,將這種層級關系縮減為一個鍵值對節點[k, v]。其中鍵值為層級樹的路徑元素,使用為上述編碼的十六進制串,值為節點的哈希值,就像標準的基數樹一樣。另外,我們增加了一個概念的優化,關于內部節點不能存值,只有那些沒有孩子節點的可以存值。但為了讓這個鍵值對存儲方案變得更通用,可以同時存儲dogdoge。我們增加了一個結束標記16到字母表,所以不會存在一個值被錯誤指向到另一個值的情況。

對于一個鍵值對節點,兩個元素的數組[k, v]。v只能是一個值或者節點。

  • 當v是一個值時,k必須是含結束標記的半字節按上述的緊湊的編碼串。
  • 當v是指向另一個節點時,k必須是不含結束標記的半字節按上述的緊湊編碼串。

對于一個分支結點,一個17個元素的數組[ v0 ... v15, vt]。在v0到v15的每個元素,要么是一個節點,要么是空,而vt則總是一個值,或空。所以如果只是在v0到v15中的其中一個元素中存儲值,我們應該使用鍵值對節點,其中的k是對包含結尾標記的一個空的半字節列表編碼結果。

下面是在MPT中獲取一個節點的代碼:

def get_helper(node,key):
    if key == []: 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 == key[:len(k2)]:
            return get(v2, key[len(k2):])
        else:
            return ''
    elif len(curnode) == 17:
        return get_helper(curnode[key[0]],key[1:])

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

例子,假設我們有一個樹有這樣一些值('dog', 'puppy'), ('horse', 'stallion'), ('do', 'verb'), ('doge', 'coin')。首先,我們將它們轉為十六進制格式:

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

創建的樹,如下圖所示:

ROOT: [ '\x16', A ]
A: [ '', '', '', '', B, '', '', '', C, '', '', '', '', '', '', '', '' ]
B: [ '\x00\x6f', D ]
D: [ '', '', '', '', '', '', E, '', '', '', '', '', '', '', '', '', 'verb' ]
E: [ '\x17', F ]
F: [ '', '', '', '', '', '', G, '', '', '', '', '', '', '', '', '', 'puppy' ]
G: [ '\x35', 'coin' ]
C: [ '\x20\x6f\x72\x73\x65', 'stallion' ]

樹的構造邏輯是root結點,要構造一個指向下一個結點的kv節點。先對鍵編碼,由于當前節點不是結束結點,存值的鍵為奇數字符數,所以前導值為1,又由于奇數不補位,最終存鍵為0x16。它指向的是一個全節點A。下一層級,要編碼的是d和h的第二個半字節,4和6。所以在A節點的第五個位置(從零開始)和第七個位置,我們可以看到分別被指向到了B和C兩個節點。對于B節點往后do,dog,doge來說。他們緊接著的都是一個編碼為6f的o字符。所以這里,B節點被編碼為指向D的kv結點,數據是指向D節點的。其中鍵值存6f,由于是指向另一個節點的kv節點,不包含結束標記,且是偶數,需要補位0,得到00,最終的編碼結果是006f。后續節點也以此類推。

當在一個節點中引用另一個節點時,其中包含的是H(rlp.encode(x))。其中哈希算法是H(x) = sha3(x) if len(x) >= 32 else x,這里的rlp.encode,是使用RLP編碼函數的方法。需要注意的是當更新一個超過32字節前綴時,你需要保存鍵值對(sha3(x), x)在一個持久化的只查表中。當小于32字節時,則不需要轉存任何東西。因為f(x)始終等于它本身值x。

關于作者

專注基于以太坊(Ethereum)的相關區塊鏈(Blockchain)技術,了解以太坊,Solidity,Truffle,web3.js。

個人博客: http://me.tryblockchain.org
版權所有,轉載注明出處

參考資料


  1. 文章翻譯自: https://github.com/ethereum/wiki/wiki/Patricia-Tree ?

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,533評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,055評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,365評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,561評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,346評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,889評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,978評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,118評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,637評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,558評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,739評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,246評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,980評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,619評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,347評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,702評論 2 370

推薦閱讀更多精彩內容

  • 1 序 2016年6月25日夜,帝都,天下著大雨,拖著行李箱和同學在校門口照了最后一張合照,搬離寢室打車去了提前租...
    RichardJieChen閱讀 5,114評論 0 12
  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,776評論 18 139
  • 樹的概述 樹是一種非常常用的數據結構,樹與前面介紹的線性表,棧,隊列等線性結構不同,樹是一種非線性結構 1.樹的定...
    Jack921閱讀 4,467評論 1 31
  • 看完這個新聞之后,深表悲痛,心里有一種淡淡的憂傷,可能對這方面了解少的人并不知道,得了腫瘤(癌癥)的患者,會是怎樣...
    三劫散仙閱讀 431評論 0 0
  • 你坐在我前面的時候 心是滿滿的 你不在的時候 我開始環顧四周了
    自可留閱讀 135評論 0 0