以太坊數據存儲源碼分析

上一篇主要講解了MPT的基本原理,這篇分析一下以太坊數據存儲相關的流程。

首先介紹一下MPT的存儲流程,然後依次分析StateDB、Transactions、Receipts的存儲,這3棵樹的Merkle Root最終會存儲到區塊Header中的Root、TxHash、ReceiptHash字段。

1.MPT存儲流程

這裏寫圖片描述
從圖中可以看出,MPT的存儲涉及3種編碼方式:

  • KeyBytes編碼
  • Hex編碼
  • Compact編碼

在完成Compact編碼後,會通過摺疊操作把子結點替換成子結點的hash值,然後以鍵值對的形式將所有結點存儲到數據庫中。下面詳細介紹上面3中編碼方式。

1.1 KeyBytes編碼

即原始關鍵字,比如圖中的0x811344、0x879337等。每個字節中包含2個nibble(半字節,4 bits),每個nibble的數值範圍時0x0~0xF。

1.2 Hex編碼

由於我們需要以nibble爲單位進行編碼並插入MPT,因此需要把一個字節拆分成兩個,轉換爲Hex編碼。
編碼轉換是在Trie.TryUpdate()中觸發的,具體轉換代碼參見trie/encoding.go:

func keybytesToHex(str []byte) []byte {
    l := len(str)*2 + 1
    var nibbles = make([]byte, l)
    for i, b := range str {
        nibbles[i*2] = b / 16
        nibbles[i*2+1] = b % 16
    }
    nibbles[l-1] = 16
    return nibbles
}

1.3 Compact編碼

當我們需要把內存中MPT存儲到數據庫中時,還需要再把兩個字節合併爲一個字節進行存儲,這時候會碰到2個問題:

  • 關鍵字長度爲奇數,有一個字節無法合併
  • 需要區分結點是擴展結點還是葉子結點
    爲了解決這個問題,以太坊設計了一種Compact編碼方式,具體規則如下:

  • 擴展結點,關鍵字長度爲偶數,前面加00前綴
  • 擴展結點,關鍵字長度爲奇數,前面加1前綴(前綴和第1個字節合併爲一個字節)
  • 葉子結點,關鍵字長度爲偶數,前面加20前綴(因爲是Big Endian)
  • 葉子結點,關鍵字長度爲奇數,前面加3前綴(前綴和第1個字節合併爲一個字節)

編碼轉換是在Trie.Commit()時觸發的,具體調用在hasher.hashChildren()函數中,轉換代碼參見trie/encoding.go:

func hexToCompact(hex []byte) []byte {
    terminator := byte(0)
    if hasTerm(hex) {
        terminator = 1
        hex = hex[:len(hex)-1]
    }
    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
}

2. StateDB的存儲

StateDB中存儲了很多stateObject,而每一個stateObject則代表了一個以太坊賬戶,包含了賬戶的地址、餘額、nonce、合約代碼hash等狀態信息。所有賬戶的當前狀態在以太坊中被稱爲“世界狀態”,在每次挖出或者接收到新區塊時需要更新世界狀態。

爲了能夠快速檢索和更新賬戶狀態,StateDB採用了兩級緩存機制,參見下圖:
這裏寫圖片描述

  • 第一級緩存以map的形式存儲stateObject
  • 第二級緩存以MPT的形式存儲
  • 第三級就是LevelDB上的持久化存儲

當上一級緩存中沒有所需的數據時,會從下一級緩存或者數據庫中進行加載。

我們可以看一下StateDB具體實現的UML圖:
這裏寫圖片描述

可以看到,一共封裝了3個包:state,trie,ethdb。如果按接口類型來分,主要分爲Trie和Database兩種接口。Trie接口主要用於操作內存中的MPT,而Database接口主要用於操作LevelDB,做持久化存儲。StateDB中同時包含了這兩種接口。

查看StateDB和stateObject的定義可以發現,這兩種類型內部各有一個Trie,那麼這兩個Trie裏存儲的什麼內容呢?請看下圖:
這裏寫圖片描述

StateDB裏的Trie以賬戶地址爲key,存儲經過RLP編碼後的stateObject。
stateObject裏的Trie也被稱爲storage trie,存儲的是智能合約執行後修改的變量值,細節可以參見之前的一篇文章:以太坊stateObject中Storage存儲內容的探究

這兩個Trie是怎麼關聯起來的呢?實際上stateObject內部有一個Account類型的字段,我們看一下它的類型定義:

type Account struct {
    Nonce    uint64
    Balance  *big.Int
    Root     common.Hash // merkle root of the storage trie
    CodeHash []byte
}

看到了吧,Account類型內部有一個Root字段,記錄的正是對應的storage trie的merkle root。

3. Transactions的存儲

這裏寫圖片描述

從圖中可以看出,MPT中是以交易在區塊中的索引的RLP編碼作爲key,存儲交易數據的RLP編碼。
事實上交易在LeveDB中並不是單獨存儲的,而是存儲在區塊的Body中。在往LeveDB中存儲不同類型的鍵值對時,會在關鍵字中添加不同的前綴予以區分,這些前綴的定義在core/rawdb/schema.go中:

    // Data item prefixes (use single byte to avoid mixing data types, avoid `i`, used for indexes).
    headerPrefix       = []byte("h") // headerPrefix + num (uint64 big endian) + hash -> header
    headerTDSuffix     = []byte("t") // headerPrefix + num (uint64 big endian) + hash + headerTDSuffix -> td
    headerHashSuffix   = []byte("n") // headerPrefix + num (uint64 big endian) + headerHashSuffix -> hash
    headerNumberPrefix = []byte("H") // headerNumberPrefix + hash -> num (uint64 big endian)

    blockBodyPrefix     = []byte("b") // blockBodyPrefix + num (uint64 big endian) + hash -> block body
    blockReceiptsPrefix = []byte("r") // blockReceiptsPrefix + num (uint64 big endian) + hash -> block receipts

    txLookupPrefix  = []byte("l") // txLookupPrefix + hash -> transaction/receipt lookup metadata
    bloomBitsPrefix = []byte("B") // bloomBitsPrefix + bit (uint16 big endian) + section (uint64 big endian) + hash -> bloom bits

    preimagePrefix = []byte("secure-key-")      // preimagePrefix + hash -> preimage
    configPrefix   = []byte("ethereum-config-") // config prefix for the db

因此,以b + block index + block hash作爲關鍵字就可以唯一確定某個區塊的Body所在的位置。
另外,爲了能夠快速查詢某筆交易的數據,在數據庫中還存儲了每筆交易的索引信息,稱爲TxLookupEntry。TxLookupEntry中包含了block index和block hash用於定位區塊Body,同時還包含了該筆交易在區塊Body中的索引位置。

4. Receipts的存儲

這裏寫圖片描述

交易回執的存儲和交易類似,區別是交易回執是單獨存儲到LevelDB中的,以r爲前綴。
另外,由於交易回執和交易是一一對應的,因此也可以通過TxLookupEntry快速定位交易回執所在的位置,加速交易回執的查找。

更多文章歡迎關注“鑫鑫點燈”專欄:https://blog.csdn.net/turkeycock/article/category/7669858

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章