1. 程式人生 > >深入講解以太坊的數據存儲

深入講解以太坊的數據存儲

view asus 數據存儲方式 提交 必須 bject 數據存儲 BE user

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

一,區塊

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數據。相關信息如下圖

技術分享圖片

四,收獲

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


作者:wacxt
鏈接:https://juejin.im/post/5a4f3aa9f265da3e5468e08e
來源:掘金
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請註明出處。

深入講解以太坊的數據存儲