本文轉載來源自:http://blog.csdn.net/teaspring/article/details/75390210
感謝原作者teaspring的分享。本文已經得到原作者的轉載許可。
在Ethereum的世界裡,資料的最終儲存形式是[k,v]索引值對,目前使用的[k,v]型底層資料庫是LevelDB;所有與交易,操作相關的資料,其呈現的集合形式是Block(Header);如果以Block為單位連結起來,則構成更大粒度的BlockChain(HeaderChain);若以Block作切割,那麼Transaction和Contract就是更小的粒度;所有交易或操作的結果,將以各個個體賬戶的狀態(state)存在,賬戶的呈現形式是stateObject,所有賬戶的集合受StateDB管理。下圖描繪了上述各資料單元的層次關係:
另一方面,上述資料單元如Block,stateObject,StateDB等,均大量使用Merkle-PatriciaTrie(MPT)資料結構以組織和管理[k,v]型資料。利用MPT高效的分段雜湊驗證機制和靈活的節點(Node)插入/載入設計,調用方均可快速且高效的實現對資料的插入、刪除、更新、壓縮和加密。以下各章節會對以上內容分別展開詳細介紹。
1. Block和Header
Block(區塊)是Ethereum的核心資料結構之一。所有賬戶的相關活動,以交易(Transaction)的格式儲存,每個Block有一個交易對象的列表;每個交易的執行結果,由一個Receipt對象與其包含的一組Log對象記錄;所有交易執行完後產生的Receipt列表,儲存在Block中(經過壓縮加密)。不同Block之間,通過前向指標ParentHash一個一個串聯起來成為一個單向鏈表,BlockChain 結構體管理著這個鏈表。
Block結構體基本可分為Header和Body兩個部分,其UML關係族如下圖所示:
Header部分
Header是Block的核心,注意到它的成員變數全都是公用的,這使得它可以很方便的向調用者提供關於Block屬性的操作。Header的成員變數全都很重要,值得細細理解: ParentHash:指向父區塊(parentBlock)的指標。除了創世塊(Genesis Block)外,每個區塊有且只有一個父區塊。 Coinbase:挖掘出這個區塊的作者地址。在每次執行交易時系統會給與一定補償的Ether,這筆金額就是發給這個地址的。 UncleHash:Block結構體的成員uncles的RLP雜湊值。uncles是一個Header數組,它的存在,頗具匠心。 Root:StateDB中的“state Trie”的根節點的RLP雜湊值。Block中,每個賬戶以stateObject對象表示,賬戶以Address為唯一標示,其資訊在相關交易(Transaction)的執行中被修改。所有賬戶對象可以逐個插入一個Merkle-PatricaTrie(MPT)結構裡,形成“state Trie”。 TxHash: Block中 “tx Trie”的根節點的RLP雜湊值。Block的成員變數transactions中所有的tx對象,被逐個插入一個MPT結構,形成“tx Trie”。 ReceiptHash:Block中的 "Receipt Trie”的根節點的RLP雜湊值。Block的所有Transaction執行完後會產生一個Receipt數組,這個數組中的所有Receipt被逐個插入一個MPT結構中,形成"Receipt Trie"。 Bloom:Bloom過濾器(Filter),用來快速判斷一個參數Log對象是否存在於一組已知的Log集合中。 Difficulty:區塊的難度。Block的Difficulty由共識演算法基於parentBlock的Time和Difficulty計算得出,它會應用在區塊的‘挖掘’階段。
Number:區塊的序號。Block的Number等於其父區塊Number +1。 Time:區塊“應該”被建立的時間。由共識演算法確定,一般來說,要麼等於parentBlock.Time + 10s,要麼等於當前系統時間。
GasLimit:區塊內所有Gas消耗的理論上限。該數值在區塊建立時設定,與父區塊有關。具體來說,根據父區塊的GasUsed同GasLimit * 2/3的大小關係來計算得出。 GasUsed:區塊內所有Transaction執行時所實際消耗的Gas總和。
Nonce:一個64bit的雜湊數,它被應用在區塊的"挖掘"階段,並且在使用中會被修改。
Merkle-PatriciaTrie(MPT)是Ethereum用來儲存區塊資料的核心資料結構。最簡單理解是一個倒置的樹形結構,每個節點可能有若干個子節點,關於MPT在Ethereum中的實現細節在下文有專門介紹。
Root,TxHash和ReceiptHash,分別取自三個MPT類型對象:stateTrie, txTrie, 和receiptTrie的根節點雜湊值。用一個32byte的雜湊值,來代表一個有若干節點的樹形結構(或若干元素的數組),這是為了加密。比如在Block的同步過程中,通過比對收到的TxHash,可以確認數群組成員transactions是否同步完整。
三者當中,TxHash和ReceiptHash的產生稍微特殊一點,因為這兩的資料來源是數組,而不像stateTrie原本就存在。如何將數組轉化成MPT結構。考慮到MPT專門儲存[k,v]類型資料,代碼裡利用了點小技巧:將數組中每個元素的索引作為k,該元素的RLP編碼值作為v,組成一個[k,v]索引值對作為一個節點,這樣所有數組元素作為節點逐個插入一個初始化為空白的MPT,形成MPT結構。 在stateTrie,txTrie,receiptTrie這三個MPT結構的產生時間上,receiptTrie 必須在Block的所有交易執行完成才能產生;txTrie 理論上只需tx數組transactions即可,不過依然被限制在所有交易執行完後才產生;最有趣的是stateTrie,由於它儲存了所有賬戶的資訊,比如餘額,發起交易次數,虛擬機器指令數組等等,所以隨著每次交易的執行,stateTrie 其實一直在變化,這就使得Root值也在變化中。於是StateDB 定義了一個函數IntermediateRoot(),用來產生那一時刻的Root值:
[plain] view plain copy // core/state/statedb.go func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash
這個函數的傳回值,代表了所有賬戶資訊的一個即時狀態。
關於Header.Root的產生時間,在上篇文章提到的交易執行過程中,交易執行的入口函數StateProcessor.Process()在返回前調用了Engine.Finalize()。正是這個Finalize(),在內部調用上述IntermediateRoot()函數並賦值給header.Root。所以Root值就是在該區塊所有交易完成後,所有賬戶資訊的即時狀態。 Body結構體
Block的成員變數td 表示的是整個區塊鏈表從源頭創世塊開始,到當前區塊截止,累積的所有區塊Difficulty之和,td 取名totalDifficulty。從概念上可知,某個區塊與父區塊的td之差,就等於該區塊Header帶有的Difficulty值。
Body可以理解為Block裡的數群組成員集合,它相對於Header需要更多的記憶體空間,所以在資料轉送和驗證時,往往與Header是分開進行的。
Uncles是Body非常特別的一個成員,從業務功能上說,它並不是Block結構體必須的,它的出現當然會佔用整個Block計算雜湊值時更長的時間,目的是為了抵消整個Ethereum網路中那些計算能力特彆強大的節點會對區塊的產生有過大的影響力,防止這些節點破壞“去中心化”這個根本宗旨。官方描述可見ethereum-wiki
Block的唯一識別碼
同Ethereum世界裡的其他對象類似,Block對象的唯一識別碼,就是它的(RLP)雜湊值。需要注意的是,Block的雜湊值,等於其Header成員的(RLP)雜湊值。
[plain] view plain copy // core/types/Block.go func (b *Block) Hash() common.Hash { if hash := b.hash.Load(); hash != nil { return hash.(common.Hash) } v := b.header.Hash() b.hash.Store(v) return v } func (h *Header) Hash() common.Hash { return rlpHash(h) } 小技巧:Block的成員hash會緩衝上一次Header計算出的雜湊值,以避免不必要的計算。
Block的雜湊值等於其Header的(RLP)雜湊值,這就從根本上明確了Block(結構體)和Header表示的是同一個區塊對象。考慮到這兩種結構體所佔記憶體空間的差異,這種設計可以帶來很多便利。比如在資料轉送時,完全可以先傳輸Header對象,驗證通過後再傳輸Block對象,收到後還可以利用二者的成員雜湊值做相互驗證。
成員分散儲存在底層資料庫
Header和Block的主要成員變數,最終還是要儲存在底層資料庫中。Ethereum 選用的是LevelDB, 屬於非關係型資料庫,儲存單元是[k,v]索引值對。我們來看看具體的儲存方式(core/database_util.go)
| key |
value |
| 'h' + num + hash |
header's RLP raw data |
| 'h' + num + hash + 't' |
td |
| 'h' + num + 'n' |
hash |
| 'H' + hash |
num |
| 'b' + num + hash |
body's RLP raw data |
| 'r' + num + hash |
receipts RLP |
| 'l' + hash |
tx/receipt lookup metadata |
這裡的hash就是該Block(或Header)對象的RLP雜湊值,在代碼中也被稱為canonical hash;num是Number的uint64類型,大端(big endian)整型數。可以發現,num 和 hash是key中出現最多的成分;同時num和hash還分別作為value被單獨儲存,而每當此時則另一方必組成key。這些資訊都在強烈的暗示,num(Number)和hash是Block最為重要的兩個屬性:num用來確定Block在整個區塊鏈中所處的位置,hash用來辨識惟一的Block/Header對象。
通過以上的設計,Block結構體的所有重要成員,都被儲存進了底層資料庫。當所有Block對象的資訊都已經寫進資料庫後,我們就可以使用BlockChain結構體來處理整個塊鏈。
2. HeaderChain和BlockChain
BlockChain結構體被用來管理整個區塊單向鏈表,在一個Ethereum用戶端軟體(比如錢包)中,只會有一個BlockChain對象存在。同Block/Header的關係類似,BlockChain還有一個成員變數類型是HeaderChain, 用來管理所有Header組成的單向鏈表。當然,HeaderChain在全域範圍內也僅有一個對象,並被BlockChain持有(準確說是HeaderChain只會被BlockChain和LightChain持有,LightChain類似於BlockChain,但預設只處理Headers,不過依然可以下載bodies和receipts)。它們的UML關係圖如下所示:
在結構體的設計上,BlockChain 同HeadeChain有諸多類似之處。比如二者都有相同的ChainConfig對象,有相同的Database介面行為變數以提供[k,v]資料的讀取和寫入;BlockChain 有成員genesisBlock和currentBlock,分別對應創世塊和當前塊,而HeaderChain則有genesisHeader和currentHeader;BlockChain 有bodyCache,blockCache 等成員用以緩衝高頻調用對象,而HeaderChain則有headerCache, tdCache, numbe