【以太坊基礎系列-004】深入淺出以太坊MPT(Merkle Patricia Tree)

1 Trie樹

Trie樹,又稱前綴樹或字典樹,是一種有序樹,用於保存關聯數組,其中的鍵通常是字符串。與二叉查找樹不同,鍵不是直接保存在節點中,而是由節點在樹中的位置決定。一個節點的所有子孫都有相同的前綴,也就是這個節點對應的字符串,而根節點對應空字符串。一般情況下,不是所有的節點都有對應的值,只有葉子節點和部分內部節點所對應的鍵纔有相關的值。

在圖示中,鍵標註在節點中,值標註在節點之下。每一個完整的英文單詞對應一個特定的整數。鍵不需要被顯式地保存在節點中。圖示中標註出完整的單詞,只是爲了演示trie的原理。trie中的鍵通常是字符串,但也可以是其它的結構。

1540261628589bb24fe1c26

 

上圖是一個簡略視圖,實際上trie每個節點是一個確定長度的數組,數組中每個節點的值是一個指向子節點的指針,最後有個標誌域,標識這個位置爲止是否是一個完整的字符串,並且有幾個這樣的字符串常見的用來存英文單詞的trie每個節點是一個長度爲27的指針數組,index0-25代表a-z字符,26爲標誌域。如圖:

154026162838828602636dd

2 Patricia樹

Patricia樹,或稱Patricia trie,或crit bit tree,壓縮前綴樹,是一種更節省空間的Trie。對於基數樹的每個節點,如果該節點是唯一的兒子的話,就和父節點合併。

154026162854833dad133b3

3 Merkle樹

Merkle Tree,通常也被稱作Hash Tree,顧名思義,就是存儲hash值的一棵樹。Merkle樹的葉子是數據塊(例如,文件或者文件的集合)的hash值。非葉節點是其對應子節點串聯字符串的hash。(不懂Hash運算的可以自行百度)

要了解Merkle Tree就要先從Hash List說起:

怎麼確定小的數據塊沒有損壞哪?只需要爲每個數據塊做Hash。BT下載的時候,在下載到真正數據之前,我們會先下載一個Hash列表。那麼問題又來了,怎麼確定這個Hash列表本事是正確的哪?答案是把每個小塊數據的Hash值拼到一起,然後對這個長字符串在作一次Hash運算,這樣就得到Hash列表的根Hash(Top Hash or Root Hash)。下載數據的時候,首先從可信的數據源得到正確的根Hash,就可以用它來校驗Hash列表了,然後通過校驗後的Hash列表校驗數據塊。

15402616284234cae4fb3e6

 

Merkle Tree可以看做Hash List的泛化(Hash List可以看作一種特殊的Merkle Tree,即樹高爲2的多叉Merkle Tree。

在最底層,和哈希列表一樣,我們把數據分成小的數據塊,有相應地哈希和它對應。但是往上走,並不是直接去運算根哈希,而是把相鄰的兩個哈希合併成一個字符串,然後運算這個字符串的哈希,這樣每兩個哈希就結婚生子,得到了一個”子哈希“。

如果最底層的哈希總數是單數,那到最後必然出現一個單身哈希,這種情況就直接對它進行哈希運算,所以也能得到它的子哈希。於是往上推,依然是一樣的方式,可以得到數目更少的新一級哈希,最終必然形成一棵倒掛的樹,到了樹根的這個位置,這一代就剩下一個根哈希了,我們把它叫做 Merkle Root。

1540261628517e6386fc4d0

 

在p2p網絡下載網絡之前,先從可信的源獲得文件的Merkle Tree樹根。一旦獲得了樹根,就可以從其他從不可信的源獲取Merkle tree。通過可信的樹根來檢查接受到的MerkleTree。如果Merkle Tree是損壞的或者虛假的,就從其他源獲得另一個Merkle Tree,直到獲得一個與可信樹根匹配的MerkleTree。

Merkle Tree和HashList的主要區別是,可以直接下載並立即驗證Merkle Tree的一個分支。因爲可以將文件切分成小的數據塊,這樣如果有一塊數據損壞,僅僅重新下載這個數據塊就行了。如果文件非常大,那麼Merkle tree和Hash list都很到,但是Merkle tree可以一次下載一個分支,然後立即驗證這個分支,如果分支驗證通過,就可以下載數據了。而Hash list只有下載整個hash list才能驗證。

4 MPT(Merkle Patricia Tree)樹

知道了Merkle Tree,知道了Patricia Tree,顧名思義,MPT(Merkle Patricia Tree)就是這兩者混合後的產物。

在以太坊(ethereum)中,使用了一種特殊的十六進制前綴(hex-prefix, HP)編碼,所以在字母表中就有16個字符。這其中的一個字符爲一個nibble。

MPT樹中的節點包括空節點、葉子節點、擴展節點和分支節點:

空節點,簡單的表示空,在代碼中是一個空串。

葉子節點(leaf),表示爲[key,value]的一個鍵值對,其中key是key的一種特殊十六進制編碼,value是value的RLP編碼。

擴展節點(extension),也是[key,value]的一個鍵值對,但是這裏的value是其他節點的hash值,這個hash可以被用來查詢數據庫中的節點。也就是說通過hash鏈接到其他節點。

分支節點(branch),因爲MPT樹中的key被編碼成一種特殊的16進制的表示,再加上最後的value,所以分支節點是一個長度爲17的list,前16個元素對應着key中的16個可能的十六進制字符,如果有一個[key,value]對在這個分支節點終止,最後一個元素代表一個值,即分支節點既可以搜索路徑的終止也可以是路徑的中間節點。

MPT樹中另外一個重要的概念是一個特殊的十六進制前綴(hex-prefix, HP)編碼,用來對key進行編碼。因爲字母表是16進制的,所以每個節點可能有16個孩子。因爲有兩種[key,value]節點(葉節點和擴展節點),引進一種特殊的終止符標識來標識key所對應的是值是真實的值,還是其他節點的hash。如果終止符標記被打開,那麼key對應的是葉節點,對應的值是真實的value。如果終止符標記被關閉,那麼值就是用於在數據塊中查詢對應的節點的hash。無論key奇數長度還是偶數長度,HP都可以對其進行編碼。最後我們注意到一個單獨的hex字符或者4bit二進制數字,即一個nibble。

HP編碼很簡單。一個nibble被加到key前(下圖中的prefix),對終止符的狀態和奇偶性進行編碼。最低位表示奇偶性,第二低位編碼終止符狀態。如果key是偶數長度,那麼加上另外一個nibble,值爲0來保持整體的偶特性。

1540261628539db1d5ca7a0

 

如圖所示:

總共有2個擴展節點,2個分支節點,4個葉子節點。

其中葉子結點的鍵值情況爲:

154026162831381f2e4f9dd

 

節點的前綴:

1540261628369ff54bca5d2

5 MPT樹的操作

下面從MPT樹的更新,刪除和查找過程來說明MPT樹的操作。

1 更新

函數_update_and_delete_storage(self, node, key, value)

i. 如果node是空節點,直接返回[pack_nibbles(with_terminator(key)), value],即對key加上終止符,然後進行HP編碼。

15402616285263e2d0af4f3

 

ii. 如果node是分支節點,如果key爲空,則說明更新的是分支節點的value,直接將node[-1]設置成value就行了。如果key不爲空,則遞歸更新以key[0]位置爲根的子樹,即沿着key往下找,即調用_update_and_delete_storage(self._decode_to_node(node[key[0]]),key[1:], value)。

1540261628781cbb9f13ea0

 

iii. 如果node是kv節點(葉子節點或者擴展節點),調用_update_kv_node(self, node, key, value),見步驟iv

iv. curr_key是node的key,找到curr_key和key的最長公共前綴,長度爲prefix_length。Key剩餘的部分爲remain_key,curr_key剩餘的部分爲remain_curr_key。

a) 如果remain_key==[]== remain_curr_key,即key和curr_key相等,那麼如果node是葉子節點,直接返回[node[0], value]。如果node是擴展節點,那麼遞歸更新node所鏈接的子節點,即調用_update_and_delete_storage(self._decode_to_node(node[1]),remain_key, value)

154026162883536072a48a2

 

b) 如果remain_curr_key == [],即curr_key是key的一部分。如果node是擴展節點,遞歸更新node所鏈接的子節點,即調用_update_and_delete_storage(self._decode_to_node(node[1]),remain_key, value);如果node是葉子節點,那麼創建一個分支節點,分支節點的value是當前node的value,分支節點的remain_key[0]位置指向一個葉子節點,這個葉子節點是[pack_nibbles(with_terminator(remain_key[1:])),value]

1540261628878845061231b

 

c) 否則,創建一個分支節點。如果curr_key只剩下了一個字符,並且node是擴展節點,那麼這個分支節點的remain_curr_key[0]的分支是node[1],即存儲node的value。否則,這個分支節點的remain_curr_key[0]的分支指向一個新的節點,這個新的節點的key是remain_curr_key[1:]的HP編碼,value是node[1]。如果remain_key爲空,那麼新的分支節點的value是要參數中的value,否則,新的分支節點的remain_key[0]的分支指向一個新的節點,這個新的節點是[pack_nibbles(with_terminator(remain_key[1:])),value]

d) 如果key和curr_key有公共部分,爲公共部分創建一個擴展節點,此擴展節點的value鏈接到上面步驟創建的新節點,返回這個擴展節點;否則直接返回上面步驟創建的新節點

1540261628839d5b8f10961

 

v. 刪除老的node,返回新的node

l 刪除

刪除的過程和更新的過程類似,而且很簡單,函數名:_delete_and_delete_storage(self, key)

i. 如果node爲空節點,直接返回空節點

ii. 如果node爲分支節點。如果key爲空,表示刪除分支節點的值,直接另node[-1]=‘’, 返回node的正規化的結果。如果key不爲空,遞歸查找node的子節點,然後刪除對應的value,即調用self._delete_and_delete_storage(self._decode_to_node(node[key[0]]),key[1:])。返回新節點

iii. 如果node爲kv節點,curr_key是當前node的key。

a) 如果key不是以curr_key開頭,說明key不在node爲根的子樹內,直接返回node。

b) 否則,如果node是葉節點,返回BLANK_NODE if key == curr_key else node。

c) 如果node是擴展節點,遞歸刪除node的子節點,即調用_delete_and_delete_storage(self._decode_to_node(node[1]),key[len(curr_key):])。如果新的子節點和node[-1]相等直接返回node。否則,如果新的子節點是kv節點,將curr_key與新子節點的可以串聯當做key,新子節點的value當做vlaue,返回。如果新子節點是branch節點,node的value指向這個新子節點,返回。

l 查找

查找操作更簡單,是一個遞歸查找的過程函數名爲:_get(self, node, key)

i. 如果node是空節點,返回空節點

ii. 如果node是分支節點,如果key爲空,返回分支節點的value;否則遞歸查找node的子節點,即調用_get(self._decode_to_node(node[key[0]]), key[1:])

iii. 如果node是葉子節點,返回node[1] if key == curr_key else ‘’

iv. 如果node是擴展節點,如果key以curr_key開頭,遞歸查找node的子節點,即調用_get(self._decode_to_node(node[1]),key[len(curr_key):]);否則,說明key不在以node爲根的子樹裏,返回空

6 以太坊的MPT

每一個以太坊的區塊頭包含三顆MPT樹,分別是

  • 交易樹
  • 收據樹(交易執行過程中的一些數據)
  • 狀態樹(賬號信息, 合約賬戶和用戶賬戶)

下圖中是兩個區塊頭,其中state roottx root receipt root分別存儲了這三棵樹的樹根,第二個區塊顯示了當賬號 175的數據變更(27 -> 45)的時候,只需要存儲跟這個賬號相關的部分數據,而且老的區塊中的數據還是可以正常訪問。(這個有點類似與函數式編程語言中的不可變的數據結構的實現

轉自:

http://www.blockchainbrother.com/article/18146

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章