基礎(chǔ)知識(shí)
Trie樹
Trie是一種搜索樹爽冕,又稱字典樹(digital tree)和前綴樹(prefix tree)抗碰。不同與二叉搜索樹纯续,鍵值并不是由樹中的節(jié)點(diǎn)存儲(chǔ),而是取決于其在樹中的位置蝶俱,或者說是從根到達(dá)節(jié)點(diǎn)的路徑。一個(gè)節(jié)點(diǎn)的所有子孫都有相同的前綴饥漫,也就是這個(gè)節(jié)點(diǎn)對應(yīng)的字符串榨呆,而根節(jié)點(diǎn)對應(yīng)空字符串。一般情況下庸队,不是所有的節(jié)點(diǎn)都有對應(yīng)的值积蜻,只有葉子節(jié)點(diǎn)和部分內(nèi)部節(jié)點(diǎn)所對應(yīng)的鍵才有相關(guān)的值闯割。
圖中,鍵不需要被顯式地保存在節(jié)點(diǎn)中竿拆,只是為了演示宙拉。
Patricia樹
Patricia樹又稱壓縮前綴樹(compact prefix tree),是一種更節(jié)省空間的Trie如输。對于Patricia樹的每個(gè)節(jié)點(diǎn)鼓黔,如果該節(jié)點(diǎn)是唯一的兒子的話,就和父節(jié)點(diǎn)合并不见。那么一棵Patricia樹的任何內(nèi)部節(jié)點(diǎn)有2個(gè)或以上的孩子節(jié)點(diǎn)澳化。下圖存儲(chǔ)的內(nèi)容和第一幅圖一致,可以看出節(jié)點(diǎn)數(shù)目減少了很多稳吮。
哈希列表
哈希列表(Hash List)是存儲(chǔ)hash值的列表缎谷。通常除了哈希列表自身以外,會(huì)有一個(gè)額外的根哈希(Root Hash or top hash)用來存儲(chǔ)哈希列表的哈希灶似。
在點(diǎn)對點(diǎn)網(wǎng)絡(luò)中作數(shù)據(jù)傳輸?shù)臅r(shí)候列林,會(huì)把大的文件分割成小的數(shù)據(jù)塊,然后同時(shí)從多個(gè)機(jī)器上下載數(shù)據(jù)塊酪惭。這樣的好處是希痴,如果小塊數(shù)據(jù)在傳輸過程中損壞了,那么只要重新下載這一塊數(shù)據(jù)就行了春感,不用重新下載整個(gè)文件砌创。但是在網(wǎng)絡(luò)中很多機(jī)器是不穩(wěn)定或者不可信的,需要對數(shù)據(jù)庫的真實(shí)性和完整性進(jìn)行校驗(yàn)鲫懒。這個(gè)時(shí)候就需要用到哈希列表嫩实。在開始的時(shí)候需要為每個(gè)數(shù)據(jù)塊做Hash,并運(yùn)算得到數(shù)據(jù)的根哈希窥岩。下載數(shù)據(jù)的時(shí)候甲献,首先從可信的數(shù)據(jù)源得到正確的根Hash,就可以用它來校驗(yàn)Hash列表了颂翼,然后通過校驗(yàn)后的Hash列表校驗(yàn)數(shù)據(jù)塊晃洒。
Merkle樹
Merkle Tree,通常也被稱作Hash Tree朦乏,是存儲(chǔ)hash值的一棵樹锥累。Merkle Tree可以看做Hash List的泛化(Hash List可以看作一種特殊的Merkle Tree,即樹高為2的多叉Merkle Tree集歇。)
在最底層桶略,和哈希列表一樣,數(shù)據(jù)被分成小的數(shù)據(jù)塊,有相應(yīng)地哈希和它對應(yīng)际歼。上一層惶翻,并不是直接去運(yùn)算根哈希,而是把相鄰的兩個(gè)哈希合并成一個(gè)字符串鹅心,然后運(yùn)算這個(gè)字符串的哈希吕粗,這樣逐層進(jìn)行哈希合并運(yùn)算,到最后就可以生成一個(gè)根hash旭愧。
Merkle Tree和HashList的主要區(qū)別是颅筋,可以直接下載并立即驗(yàn)證Merkle Tree的一個(gè)分支。
Merkle Tree的主要作用是當(dāng)拿到Top Hash的時(shí)候输枯,這個(gè)hash值代表了整顆樹的信息摘要议泵,當(dāng)樹里面任何一個(gè)數(shù)據(jù)發(fā)生了變動(dòng),都會(huì)使得從當(dāng)前節(jié)點(diǎn)到根節(jié)點(diǎn)的Hash的值都會(huì)發(fā)生變化桃熄,而其他子樹不會(huì)有變化先口。而Top Hash的值是會(huì)存儲(chǔ)到區(qū)塊鏈的區(qū)塊頭里面去的,區(qū)塊頭是必須經(jīng)過工作量證明瞳收。
以太坊中的MPT
以太坊中的MPT(Merkle Patricia Tree)與第一部分中的數(shù)據(jù)結(jié)構(gòu)相比碉京,MPT樹從結(jié)構(gòu)上看是一棵Patricia樹,每個(gè)節(jié)點(diǎn)保存一個(gè)hash值螟深,因此也可以起到Merkle Tree的作用谐宙。
對于MPT來說,其主要作用是用來存儲(chǔ)一系列的kv對界弧,如公式188定義卧惜。此處key是任意長度的二進(jìn)制數(shù)組,value也是任意長度的二進(jìn)制數(shù)組夹纫。為了方便用數(shù)字下標(biāo)來對kv對中的每個(gè)元素進(jìn)行索引,將每一個(gè)(key设凹,value)定義為公式189的形式舰讹。
以太坊的MPT樹中需要將key值轉(zhuǎn)化為十六進(jìn)制的表示形式,這樣在存儲(chǔ)的時(shí)候key作為一棵樹闪朱,其范圍為[0..f],即分叉的節(jié)點(diǎn)最多有16種分叉的可能月匣。將key值轉(zhuǎn)化為十六進(jìn)制的方法如公式191所示,將原先的一個(gè)字節(jié)拆成兩個(gè)半字節(jié)奋姿,而每個(gè)半字節(jié)的值都不會(huì)超過16锄开,即在[0..f]范圍內(nèi)。對key進(jìn)行處理過后的key称诗,value對萍悴,可以表示成公式190的形式。
以太坊中的MPT中有4種節(jié)點(diǎn):
- NULL節(jié)點(diǎn),用一個(gè)空的字符串表示癣诱。
- 分之(branch)節(jié)點(diǎn)计维,17元組[v0...v15,vt]。其前16個(gè)項(xiàng)對應(yīng)于這些點(diǎn)在其遍歷中的鍵的十六個(gè)可能的半字節(jié)值中的每一個(gè)撕予。第17個(gè)字段是存儲(chǔ)那些在當(dāng)前結(jié)點(diǎn)結(jié)束了的節(jié)點(diǎn)(例如鲫惶, 有三個(gè)key,分別是 (abc ,abd, ab) 第17個(gè)字段儲(chǔ)存了ab節(jié)點(diǎn)的值)
- 葉子(leaf)節(jié)點(diǎn),2元組[encodedPath,value]实抡。第一個(gè)字段是剩下的Key的半字節(jié)編碼, 第二個(gè)字段是Value欠母。
- 擴(kuò)展(extension)節(jié)點(diǎn),2元組[encodePath,key]吆寨。第一個(gè)字段是剩下的Key的半字節(jié)編碼, 第二個(gè)字段指向另一個(gè)節(jié)點(diǎn)赏淌。
構(gòu)造過程大致為:
- 如果當(dāng)前只有一個(gè)kv對,則直接將其構(gòu)造成一個(gè)葉子節(jié)點(diǎn)鸟废。
- 如果當(dāng)前需要編碼的kv集合猜敢,有公共前綴,那么提取公共前綴盒延,將公共前綴構(gòu)造成一個(gè)擴(kuò)展節(jié)點(diǎn)缩擂,并將其第二個(gè)字段指向下一個(gè)節(jié)點(diǎn)(一般為分支節(jié)點(diǎn))。
- 如果不是上面的兩種情況添寺,則構(gòu)造分支節(jié)點(diǎn)按照當(dāng)前key所在索引的16進(jìn)制對kv集合進(jìn)行劃分胯盯,[0..f]分別指向下一個(gè)節(jié)點(diǎn)。如果有kv對在此節(jié)點(diǎn)終結(jié)计露,則將其存儲(chǔ)在value中博脑。
以太坊中MPT的結(jié)構(gòu)示例如下圖:(實(shí)際的實(shí)現(xiàn)上有些地方是不一樣的)
十六進(jìn)制前綴編碼
十六進(jìn)制前綴編碼是將任意數(shù)量的半字節(jié)(nibble,半個(gè)字節(jié)也就是4個(gè)bit票罐,16進(jìn)制的一位數(shù)字)編碼為字節(jié)數(shù)組的有效方法叉趣。通過添加前綴,其可以存儲(chǔ)附加標(biāo)識(shí)该押,在Trie中使用時(shí)疗杉,會(huì)在節(jié)點(diǎn)類型間消除歧義。
當(dāng)半字節(jié)長度為偶數(shù)時(shí)蚕礼,第一個(gè)高半字節(jié)為前綴烟具,低半字節(jié)置0,后面的每個(gè)字節(jié)都是將兩個(gè)半字節(jié)組合在一起奠蹬。
當(dāng)半字節(jié)長度為奇數(shù)時(shí)朝聋,第一個(gè)高半字節(jié)為前綴,低半字節(jié)為半字節(jié)的第一個(gè)半字節(jié)囤躁,后面的每個(gè)字節(jié)都是將兩個(gè)半字節(jié)組合在一起冀痕。
-
第一個(gè)字節(jié)的高半字節(jié)包含兩個(gè)標(biāo)志荔睹,最低位表示奇偶(0為偶,1為奇)金度,第二低位編碼flag应媚。實(shí)際中就是編碼以太坊中的不同類型的節(jié)點(diǎn)。
hex char bits node type partial path length 0 0000 extension even 1 0001 extension odd 2 0010 terminating (leaf) even 3 0011 terminating (leaf) odd
MPT的序列化
MPT在數(shù)據(jù)庫中的實(shí)際存儲(chǔ)是存儲(chǔ)的hash值與即節(jié)點(diǎn)的序列化形式的對猜极。
序列化主要是指把內(nèi)存表示的數(shù)據(jù)存放到數(shù)據(jù)庫里面中姜, 反序列化是指把數(shù)據(jù)庫里面的Trie數(shù)據(jù)加載成內(nèi)存表示的數(shù)據(jù)。 序列化的目的主要是方便存儲(chǔ)跟伏,減少存儲(chǔ)大小等丢胚。 反序列化的目的是把存儲(chǔ)的數(shù)據(jù)加載到內(nèi)存,方便Trie樹的插入受扳,查詢携龟,修改等需求。
Trie的序列化主要使用了前面介紹的十六進(jìn)制前綴編碼和RLP編碼格式勘高。
定義TRIE函數(shù)峡蟋,用來表示樹根的HASH值(其中c函數(shù)的第二個(gè)參數(shù),意為key值的起始index华望,所以root對應(yīng)的值為0)
對于每個(gè)節(jié)點(diǎn)蕊蝗,如公式193所示。如果該節(jié)點(diǎn)的序列化長度小于32赖舟,則直接存儲(chǔ)其序列化蓬戚,否則存儲(chǔ)器序列化后的hash值。這里宾抓,主要是在公式194中使用子漩,是一個(gè)節(jié)點(diǎn)指向另一個(gè)節(jié)點(diǎn)時(shí),是把該子節(jié)點(diǎn)直接存儲(chǔ)在父節(jié)點(diǎn)中還是將其hash值存儲(chǔ)在父節(jié)點(diǎn)中(真正的值會(huì)單獨(dú)存的)石洗。
對于每一個(gè)節(jié)點(diǎn)存儲(chǔ)時(shí)首先對節(jié)點(diǎn)進(jìn)行RLP編碼處理幢泼,如公式194所示
- 如果當(dāng)前需要編碼的KV集合只剩下一條數(shù)據(jù),那么該節(jié)點(diǎn)是一個(gè)葉子節(jié)點(diǎn)讲衫,按照第一條規(guī)則編碼缕棵,將nibble形式的key,與表示葉子節(jié)點(diǎn)的true值一起進(jìn)行十六進(jìn)制前綴編碼焦人,并將結(jié)果和value一起進(jìn)行RLP編碼。
- 如果當(dāng)前需要編碼的KV集合有公共前綴重父,那么提取最大公共前綴花椭。第二個(gè)字段遞歸存儲(chǔ)下一個(gè)節(jié)點(diǎn)。
- 如果不是上面兩種情況房午,那么使用分支節(jié)點(diǎn)進(jìn)行集合切分矿辽。可以看到u的值由n進(jìn)行遞歸定義,而如果有節(jié)點(diǎn)剛好在這里完結(jié)了袋倔,那么第17個(gè)元素v就是為這種情況準(zhǔn)備的雕蔽。
以太坊中的MPT
以太坊中的所有的merkle樹都是指Merkle Patricia Tree。
從區(qū)塊頭中可以看到有3棵MPT的根宾娜。
- stateRoot
- transcationsRoot
- receiptsRoot
State樹
State樹是一棵全局的樹批狐,它的key是sha3(ethereumAddress),即賬戶地址的hash值。其存儲(chǔ)的值value為rlp(ethereumAccount)前塔,即賬戶信息的rlp編碼嚣艇。其中賬戶信息是一個(gè)[nonce,balance,storageRoot,codeHash]的四元組,其中storageRoot指向賬戶的Storage樹华弓。
Storage樹
一般的外部賬戶Storage為空食零,而合約賬戶通常會(huì)儲(chǔ)存一定的數(shù)據(jù),這些數(shù)據(jù)就是存儲(chǔ)在合約賬戶的Storage樹中寂屏,storage樹中的key與賬戶地址和其中實(shí)際存儲(chǔ)的key有關(guān)贰谣,其value值為合約定義的value值。
Transactions樹
每一個(gè)區(qū)塊都會(huì)有一棵Transactions樹迁霎,其key為rlp(transactionIndex)(交易在區(qū)塊中的編號(hào)吱抚,0,1...),其value值為經(jīng)過rlp編碼的交易欧引。在實(shí)際情況中該樹并不會(huì)真的存儲(chǔ)到數(shù)據(jù)庫中频伤,只是在生成block以及校驗(yàn)block的時(shí)候用于得到當(dāng)前區(qū)塊的TransactionsRoot。實(shí)際中一條交易連同同一區(qū)塊的其他交易以及區(qū)塊的uncles是被存儲(chǔ)在一個(gè)條目中的芝此。
Receipts樹
每一個(gè)區(qū)塊都會(huì)有一棵Receipts樹樹憋肖,其key為rlp(transactionIndex)(交易在區(qū)塊中的編號(hào),0婚苹,1...),其value值為經(jīng)過rlp編碼的交易收據(jù)岸更。在實(shí)際情況中該樹并不會(huì)真的存儲(chǔ)到數(shù)據(jù)庫中,只是在生成block以及校驗(yàn)block的時(shí)候用于得到當(dāng)前區(qū)塊的ReceiptsRoot膊升。實(shí)際中同一個(gè)區(qū)塊的交易收據(jù)會(huì)一起編碼然后存儲(chǔ)在一個(gè)條目中的怎炊。