Merkle Patricia Tree (MPT) 樹詳解


1.    介紹

  

  Merkle Patricia Tree(簡稱MPT樹,實際上是一種trie前綴樹)是以太坊中的一種加密認證的數據結構,可以用來存儲所有的(key,value)對。以太坊區塊的頭部包括一個區塊頭,一個交易的列表和一個uncle區塊的列表。在區塊頭部包括了交易的hash樹根,用來校驗交易的列表。在p2p網絡上傳輸的交易是一個簡單的列表,它們被組裝成一個叫做trie樹的特殊數據結構,來計算根hash。值得注意的是,除了校驗區塊外,這個數據結構並不是必須的,一旦區塊被驗證正確,那么它在技術上是可以忽略的。但是,這意味着交易列表在本地以trie樹的形式存儲,發送給客戶端的時候序列化成列表。客戶端接收到交易列表后重新構建交易列表trie樹來驗證根hash。RLP(Recursive length prefix encoding,遞歸長度前綴編碼),用來對trie樹種所有的條目進行編碼(參考:http://www.cnblogs.com/fengzhiwu/p/5565559.html)。

  Trie樹也叫作Radix樹,為了提高效率,以太坊在實現上對其做了一些改進。在一般的radix樹中,key是從樹根到對應value得真實的路徑。即從根節點開始,key中的每個字符會標識走那個子節點從而到達相應value。Value被存儲在葉子節點,是每條路徑的終止。假如key來自一個包含N個字符的字母表,那么樹中的每個節點都可能會有多達N個孩子,樹的最大深度是key的最大長度。

  Radix的好處是具有相同前綴的key所對應的value在樹中是非常靠近的,並且trie中不會有像hash-table一樣的沖突。但是它也有缺陷,假如有一個很長的key,沒有其他的key和它有公共的前綴,那么在遍歷或存儲它對應的值得時候,你就會遍歷或存儲相當多的節點,因為這棵樹是非常不平衡的。

 

2.    特性

  以太坊對Radix樹的實現做了很多改進。

  首先,為了保證樹的加密安全,每個節點通過他的hash被引用,而非32bit或64bit的內存地址,即樹的Merkle部分是一個節點的確定性加密的hash。一個非葉節點存儲在leveldb關系型數據庫中,數據庫中的key是節點的RLP編碼的sha3哈希,value是節點的RLP編碼。代碼中的實現如圖:

  想要獲得一個非葉節點的子節點,只需要根據子節點的hash訪問數據庫獲得節點的RLP編碼,然后解碼就行了。如圖所示:

  

  通過這種模式,根節點就成為了整個樹的加密簽名,如果一顆給定trie的跟hash是公開的,那么所有人都可以提供一種證明,通過提供每步向上的路徑證明特定的key是否含有給定的值。

 

  第二,引入了很多節點類型來提高效率。MPT樹中的節點包括空節點、葉子節點、擴展節點和分支節點。

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

  標准的葉子節點,表示為[key,value]的一個list,其中key是key的一種特殊十六進制編碼,value是value的RLP編碼。

  擴展節點,也是[key,value]的列表,但是這里的value是其他節點的hash,這個hash可以被用來查詢數據庫中的節點。也就是說通過hash鏈接到其他節點。

  最后分支節點,因為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前,對終止符的狀態和奇偶性進行編碼。最低位表示奇偶性,第二低位編碼終止符狀態。如果key是偶數長度,那么加上另外一個nubble,值為0來保持整體的偶特性。

3.  操作

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

  • 更新

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

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

  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)。

  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)

    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]

    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鏈接到上面步驟創建的新節點,返回這個擴展節點;否則直接返回上面步驟創建的新節點

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

 

  • 刪除

 

  刪除的過程和更新的過程類似,而且很簡單,函數名:_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指向這個新子節點,返回。

 

  • 查找

  查找操作更簡單,是一個遞歸查找的過程函數名為:_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為根的子樹里,返回空

 

4.    總結

  相對於普通的前綴樹,MPT樹能有效減少Trie樹的深度,增加Trie樹的平衡性。而且通過節點的hash值進行樹的節點的鏈接,有助於提高樹的安全性和可驗證性。所以說MPT樹是Trie和Merkle樹混合加上平衡操作后的產物。

 

參考: 

https://easythereentropy.wordpress.com/2014/06/04/understanding-the-ethereum-trie/

https://github.com/ethereum/wiki/wiki/Patricia-Tree

https://github.com/ebuchman/understanding_ethereum_trie

https://github.com/ethereum/pyethereum


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM