前言:本文的目的是打算深刻淺出講講以太坊的總體結構以及存儲相關的內容,會聚焦在存儲上,同時會結合源碼講解,整個過程也能夠體會到做者的設計思想之精妙。node
block是最重要的數據結構之一,主要由header和body兩部分組成web
type Block struct { header *Header //區塊頭 uncles []*Header //叔節點 transactions Transactions //交易數組 hash atomic.Value size atomic.Value td *big.Int //全部區塊Difficulty之和 ReceivedAt time.Time ReceivedFrom interface{} }
type Header struct { ParentHash common.Hash //指向父區塊的指針 UncleHash common.Hash //block中叔塊數組的RLP哈希值 Coinbase common.Address //挖出該區塊的人的地址 Root common.Hash //StateDB中的stat trie的根節點的RLP哈希值 TxHash common.Hash //tx trie的根節點的哈希值 ReceiptHash common.Hash //receipt trie的根節點的哈希值 Bloom Bloom //布隆過濾器,用來判斷Log對象是否存在 Difficulty *big.Int //難度係數 Number *big.Int //區塊序號 GasLimit uint64 //區塊內全部Gas消耗的理論上限 GasUsed uint64 //區塊內消耗的總Gas Time *big.Int //區塊應該被建立的時間 Nonce BlockNonce //挖礦必須的值 }
type Body struct { Transactions []*Transaction //交易的數組 Uncles []*Header }
看源碼老是最好的方式,咱們先看看trie的結構體的字段數據庫
type Trie struct { root node //根節點 db Database //數據庫相關,在下面再仔細介紹 originalRoot common.Hash //初次建立trie時候須要用到 cachegen, cachelimit uint16 //cache次數的計數器,每次Trie的變更提交後自增 }
從上面咱們能夠看到節點類型是node,那麼接下來看看node的各個實現類數組
type ( fullNode struct { Children [17]node flags nodeFlag } shortNode struct { Key []byte Val node flags nodeFlag } hashNode []byte valueNode []byte )
能夠擁有多個子節點,長度爲17的node數組,前16位對應16進制,子節點根據key的第一位,插入到相應的位置。第17位,還不清除具體做用是什麼。緩存
僅有一個子節點的節點。它的成員變量Val指向一個子節點bash
葉子節點,攜帶數據部分的RLP哈希值,數據的RLP編碼值做爲valueNode的匹配項存儲在數據庫裏數據結構
是fullNode或者shortNode對象的RLP哈希值,以nodeFlag結構體的成員(nodeFlag.hash)的形式,被fullNode和shortNode間接持有app
接下來看看在MPT樹中,是如何對key進行編碼的,在encoding.go中,咱們能夠看到,有三種編碼方式post
就是真正的key(一個[]byte),沒什麼特殊的含義學習
先看一幅圖,結合圖來講明:
將一個byte的高4位和低4位分別存到兩個byte中(每4位即一個nibble),而後在尾部加上一個標記來標識這是屬於HEX編碼方式。經過這種方式,每一個byte均可以表示爲一個16進制,從而加入到上面提到的fullNode的children數組中
一樣,看一個圖:
而後來看看HEX是如何轉換到COMPACT的
func hexToCompact(hex []byte) []byte {
terminator := byte(0)
//判斷是不是包含真實的值
if hasTerm(hex) { terminator = 1 hex = hex[:len(hex)-1] //截取掉HEX的尾部 } buf := make([]byte, len(hex)/2+1) buf[0] = terminator << 5 // the flag byte if len(hex)&1 == 1 { //說明有效長度是奇數 buf[0] |= 1 << 4 // odd flag buf[0] |= hex[0] // first nibble is contained in the first byte hex = hex[1:] } decodeNibbles(hex, buf[1:]) return buf }
前面只是簡單的一個介紹,這裏纔是本文的一個重點,接下來將學習是各類數據如何進行存儲的。以太坊中使用的數據庫是levelDB
headerPrefix = []byte("h") // headerPrefix + num (uint64 big endian) + hash -> header tdSuffix = []byte("t") // headerPrefix + num (uint64 big endian) + hash + tdSuffix -> td numSuffix = []byte("n") // headerPrefix + num (uint64 big endian) + numSuffix -> hash blockHashPrefix = []byte("H") // blockHashPrefix + hash -> num (uint64 big endian) bodyPrefix = []byte("b") // bodyPrefix + num (uint64 big endian) + hash -> block body blockReceiptsPrefix = []byte("r") // blockReceiptsPrefix + num (uint64 big endian) + hash -> block receipts lookupPrefix = []byte("l") // lookupPrefix + hash -> transaction/receipt lookup metadata bloomBitsPrefix = []byte("B") // bloomBitsPrefix + bit (uint16 big endian) + section (uint64 big endian) + hash -> bloom bits
從上面代碼咱們能夠看出存儲的對應規則,接下來對幾個字段解釋一下。 num:區塊號(uint64大端格式); hash:區塊哈希值;
這裏有一個須要特別注意的地方:由於Header的前向指針是不能修改的,那麼當把Header寫入數據庫時候,咱們必需要先保證parent和parent的parent等,已經寫入數據庫
這裏咱們看一下代碼
func WriteTxLookupEntries(db ethdb.Putter, block *types.Block) error {
// 遍歷每一個交易而且編碼元數據
for i, tx := range block.Transactions() { entry := TxLookupEntry{ BlockHash: block.Hash(), BlockIndex: block.NumberU64(), Index: uint64(i), } data, err := rlp.EncodeToBytes(entry) if err != nil { return err } if err := db.Put(append(lookupPrefix, tx.Hash().Bytes()...), data); err != nil { return err } } return nil }
在以太坊中,帳戶的呈現形式是一個stateObject,全部帳戶首StateDB管理。StateDB中有一個成員叫trie,存儲stateObject,每一個stateObject有20bytes的地址,能夠將其做爲key;每次在一個區塊的交易開始執行前,trie由一個哈希值(hashNode)恢復出來。另外還有一個map結構,也是存放stateObject,每一個stateObject的地址做爲map的key
可見,這個map被用做本地的一級緩存,trie是二級緩存,底層數據庫是第三級
每一個stateObject對應了一個帳戶(Account包含了餘額,合約發起次數等數據),同時它也包含了一個trie(storage trie),用來存儲State數據。相關信息以下圖
不單單對以太坊的存儲原理更加理解,同時,在系統設計方面,以太坊也有不少能夠借鑑之處,例如:多級緩存,數據存儲方式等等。