深入講解以太坊的資料儲存

奇犽發表於2018-01-08

前言:本文的目的是打算深入淺出講講以太坊的整體結構以及儲存相關的內容,會聚焦在儲存上,同時會結合原始碼講解,整個過程也可以體會到作者的設計思想之精妙。

一,區塊

block是最重要的資料結構之一,主要由header和body兩部分組成

1, block原始碼(部分重要欄位)

type Block struct {
	header       *Header            //區塊頭
	uncles       []*Header          //叔節點
	transactions Transactions       //交易陣列
	hash atomic.Value
	size atomic.Value
	td *big.Int                      //所有區塊Difficulty之和
	ReceivedAt   time.Time
	ReceivedFrom interface{}
}
複製程式碼
1.1,header
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     //挖礦必須的值
}
複製程式碼
1.2,body
type Body struct {
	Transactions []*Transaction //交易的陣列
	Uncles       []*Header      
}
複製程式碼

二,MPT樹

看原始碼總是最好的方式,我們先看看trie的結構體的欄位

1,Trie

type Trie struct {
	root         node   //根節點
	db           Database   //資料庫相關,在下面再仔細介紹
	originalRoot common.Hash    //初次建立trie時候需要用到
	cachegen, cachelimit uint16 //cache次數的計數器,每次Trie的變動提交後自增
}
複製程式碼

從上面我們可以看到節點型別是node,那麼接下來看看node的各個實現類

2,node的各個實現類

type (
	fullNode struct {
		Children [17]node
		flags    nodeFlag
	}
	shortNode struct {
		Key   []byte
		Val   node
		flags nodeFlag
	}
	hashNode  []byte
	valueNode []byte
)
複製程式碼

(1) fullNode

可以擁有多個子節點,長度為17的node陣列,前16位對應16進位制,子節點根據key的第一位,插入到相應的位置。第17位,還不清除具體作用是什麼。

(2) shortNode

僅有一個子節點的節點。它的成員變數Val指向一個子節點

(3) valueNode

葉子節點,攜帶資料部分的RLP雜湊值,資料的RLP編碼值作為valueNode的匹配項儲存在資料庫裡

(4) hashNode

是fullNode或者shortNode物件的RLP雜湊值,以nodeFlag結構體的成員(nodeFlag.hash)的形式,被fullNode和shortNode間接持有

3,對key進行編碼

接下來看看在MPT樹中,是如何對key進行編碼的,在encoding.go中,我們可以看到,有三種編碼方式

(1) KEYBYTES:

就是真正的key(一個[]byte),沒什麼特殊的含義

(2) HEX:

先看一幅圖,結合圖來說明:

深入講解以太坊的資料儲存

將一個byte的高4位和低4位分別存到兩個byte中(每4位即一個nibble),然後在尾部加上一個標記來標識這是屬於HEX編碼方式。通過這種方式,每個byte都可以表示為一個16進位制,從而加入到上面提到的fullNode的children陣列中

(3) COMPACT:

同樣,看一個圖:

深入講解以太坊的資料儲存
然後來看看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

(1) header和block儲存

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等,已經寫入資料庫

(2) 交易儲存

這裡我們看一下程式碼

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
}
複製程式碼

(3) StateDB模組

在以太坊中,賬戶的呈現形式是一個stateObject,所有賬戶首StateDB管理。StateDB中有一個成員叫trie,儲存stateObject,每個stateObject有20bytes的地址,可以將其作為key;每次在一個區塊的交易開始執行前,trie由一個雜湊值(hashNode)恢復出來。另外還有一個map結構,也是存放stateObject,每個stateObject的地址作為map的key

深入講解以太坊的資料儲存
可見,這個map被用作本地的一級快取,trie是二級快取,底層資料庫是第三級

(4) 儲存賬戶(stateObject)

每個stateObject對應了一個賬戶(Account包含了餘額,合約發起次數等資料),同時它也包含了一個trie(storage trie),用來儲存State資料。相關資訊如下圖

深入講解以太坊的資料儲存

四,收穫

不僅僅對以太坊的儲存原理更加理解,同時,在系統設計方面,以太坊也有很多可以借鑑之處,例如:多級快取,資料儲存方式等等。

相關文章