如何解析 Ethereum 資料:讀取 LevelDB 資料

shooter發表於2019-11-01

文章來自簡書

感謝 Gary大佬的細心解答

之前轉過一篇文章 如何解析 Bitcoin 的資料,Bitcoin 將 p2p 網路同步來的資料,儲存在了LevelDB資料庫中。我們可以通過 rpc 的方式請求資料,這些請求來的資料是從LevelDB(以下簡稱 ldb)獲取的。如果我們能直接讀取 ldb 的資料,就可以繞過 rpc請求,直接讀取。

為什麼繞過 rpc ,直接讀取 ldb 呢

一個字 快,二個字真快
我們可以通過 tcp 直接讀取 mysql 資料,也可以通過
navicat這樣的 GUI 工具讀取。有時候為了快速讀取資料,我們會捨棄GUI這樣美觀的工具,而用更直接的方式。
直接讀取 Bitcoin 的 ldb 資料庫, 就是捨棄了 美觀的 rpc 工具,顯而易見的好處是快。

由於 Bitcoin 使用 Ldb 儲存區塊鏈資料,之後的很多鏈沿用了這個技術路線,Ethereum也同樣使用 Ldb 儲存資料。

什麼是 LevelDB

Ldb是Google 工程師Jeff Dean和Sanjay Ghemawat開發的NoSQL 儲存引擎庫,是現代分散式儲存領域的一枚原子彈。在它的基礎之上,Facebook 開發出了另一個 NoSQL 儲存引擎庫 RocksDB,沿用了 Ldb 的先進技術架構的同時還解決了 LevelDB 的一些短板。現代開源市場上有很多資料庫都在使用 RocksDB 作為底層儲存引擎,比如大名鼎鼎的 TiDB

在使用 Ldb 時,我們可以將它看成一個 Key/Value 記憶體資料庫。它提供了基礎的 Get/Set API,我們在程式碼裡可以通過這個 API 來讀寫資料。你還可以將它看成一個無限大小的高階 HashMap,我們可以往裡面塞入無限條 Key/Value 資料,只要磁碟可以裝下。

Ldb有多種儲存結構,這些結構並不是平坦的,而是分層組織(Level)的,這也是LevelDB名字的來源。如下所示:

Ldb基本使用示例

Ethereum 使用最廣的開發版本是 go-eth,我們自然用 golang做些基本的程式碼事例。

// 讀或寫資料庫
db, err := leveldb.OpenFile("path/db", nil)
...
defer db.Close()
...

///////////////////////
// 讀或寫資料庫,返回資料結果不能修改
data, err := db.Get([]byte("key"), nil)
...
err = db.Put([]byte("key"), []byte("value"), nil)
...
err = db.Delete([]byte("key"), nil

///////////////////////
// 資料庫遍歷
iter := db.NewIterator(nil, nil)
for iter.Next() {
    key := iter.Key()
    value := iter.Value()
    ...
}
iter.Release()
err = iter.Error()
...

如何讀取 Eth 的 Ldb 資料呢?

我們先研究下 v1.8.7版本,這個版本有很多裸露的線索。在示例中讀取 ldb 需要有 key,Eth有key 這種東西麼?
https://github.com/ethereum/go-ethereum/bl...

headerPrefix = []byte("h") // headerPrefix + num (uint64 big endian) + hash -> header
numSuffix    = []byte("n") // headerPrefix + num (uint64 big endian) + numSuffix -> hash
bodyPrefix   = []byte("b") // bodyPrefix + num (uint64 big endian) + hash -> block body

從這句話中headerPrefix + num (uint64 big endian) + hash -> header 推斷 headerPrefix + x + y 組成了一個 key, 用程式碼試試

// 先連線ldb資料庫
db, _ := leveldb.OpenFile("/mnt/eth/geth/chaindata", nil)

num := 46147 // 任意的區塊高度
blkNum := make([]byte, 8)
binary.BigEndian.PutUint64(blkNum, uint64(num))  // 把num變為 uint64 big endian型別的資料

hashKey := append(headerPrefix, blkNum...) // headerPrefix + blkNum
hashKey = append(hashKey, numSuffix...) // blkNum + headerPrefix + numSuffix

// 查詢hashKey 對應的 value
blkHash, _ := db.Get(hashKey, nil)

從 Ldb 讀取出blkHash 是真有資料, 替換不同的 num 得到的資料不同。現實中我是看 GetCanonicalHash函式得到的提示。

得到了 blkHash key , 下一步得到 headerKey key

headerKey := append(headerPrefix, blkNum...) // headerPrefix + blkNum
headerKey = append(headerKey, blkHash...)    // headerPrefix + blkNum + blkHash

blkHeaderData, _ := db.Get(headerKey, nil) // headerKey是新的key

_byteData := bytes.NewReader(blkHeaderData)

blkHeader := new(types.Header)
rlp.Decode(_byteData, blkHeader)

fmt.Printf("Block Hash: %x \n", blkHeader.Hash())
fmt.Printf("Block Coinbase: %x \n", blkHeader.Coinbase)

rlp.Decode(_byteData, blkHeader) 這裡解釋下,如果大家讀Eth的一些文章,ldb的儲存資料是用rlp編碼後,這裡是解碼為types.Header結構的資料。

更詳細程式碼請看 ethLeveldb.go

------------------------------ 華麗麗的分割線-------------------------------

go-eth 在 2019 年 7 月推出了v1.9.x版本,在同步資料、讀取Ldb方面做了大量的封裝,使用起來更為方便了。 使用 syncmode=archive同步方式的時候, 同步時間從 62 天變成了 13 天,5 倍 。

上面的程式碼在讀取1.9.x版本資料的時候,讀取不成功。
1.9.x在資料方面做了重新整理,大概有以下兩個非相容的改動:

1) 歷史的區塊鏈資料(header,body, receipts等)被挪到一個flaten file儲存中,因為這部分資料已經是不會更改的了

2) 更改了部分資料結構的scheme,例如receipt。原先很多欄位不需要存到db,是可以在read之後重新計算出來的。這部分會佔據大量的儲存空間,在1.9把這些欄位刪去了。

有一個 inspect 命令,分別統計下 fast 、full 不同模式的資料庫詳細資訊

geth inspect --datadir fastdata

+-----------------+--------------------+------------+
|    DATABASE     |      CATEGORY      |    SIZE    |
+-----------------+--------------------+------------+
| Key-Value store | Headers            | 211.40 KiB |
| Key-Value store | Bodies             | 44.00 B    |
| Key-Value store | Receipts           | 42.00 B    |
| Key-Value store | Difficulties       | 19.07 KiB  |
| Key-Value store | Block number->hash | 17.24 KiB  |
| Key-Value store | Block hash->number | 845.67 KiB |
| Key-Value store | Transaction index  | 0.00 B     |
| Key-Value store | Bloombit index     | 0.00 B     |
| Key-Value store | Trie nodes         | 4.79 MiB   |
| Key-Value store | Trie preimages     | 547.13 KiB |
| Key-Value store | Clique snapshots   | 0.00 B     |
| Key-Value store | Singleton metadata | 149.00 B   |
| Ancient store   | Headers            | 5.97 MiB   |
| Ancient store   | Bodies             | 851.64 KiB |
| Ancient store   | Receipts           | 182.32 KiB |
| Ancient store   | Difficulties       | 279.10 KiB |
| Ancient store   | Block number->hash | 769.77 KiB |
| Light client    | CHT trie nodes     | 0.00 B     |
| Light client    | Bloom trie nodes   | 0.00 B     |
+-----------------+--------------------+------------+
|                         TOTAL        | 14.40 MIB  |
+-----------------+--------------------+------------+
geth inspect --datadir fulldata

+-----------------+--------------------+------------+
|    DATABASE     |      CATEGORY      |    SIZE    |
+-----------------+--------------------+------------+
| Key-Value store | Headers            | 42.89 MiB  |
| Key-Value store | Bodies             | 8.90 MiB   |
| Key-Value store | Receipts           | 3.76 MiB   |
| Key-Value store | Difficulties       | 3.93 MiB   |
| Key-Value store | Block number->hash | 3.33 MiB   |
| Key-Value store | Block hash->number | 3.07 MiB   |
| Key-Value store | Transaction index  | 544.56 KiB |
| Key-Value store | Bloombit index     | 1.60 MiB   |
| Key-Value store | Trie nodes         | 4.03 MiB   |
| Key-Value store | Trie preimages     | 1.01 MiB   |
| Key-Value store | Clique snapshots   | 0.00 B     |
| Key-Value store | Singleton metadata | 139.00 B   |
| Ancient store   | Headers            | 6.00 B     |
| Ancient store   | Bodies             | 6.00 B     |
| Ancient store   | Receipts           | 6.00 B     |
| Ancient store   | Difficulties       | 6.00 B     |
| Ancient store   | Block number->hash | 6.00 B     |
| Light client    | CHT trie nodes     | 0.00 B     |
| Light client    | Bloom trie nodes   | 0.00 B     |
+-----------------+--------------------+------------+
|                         TOTAL        | 73.05 MIB  |
+-----------------+--------------------+------------+

+-----------------+--------------------+------------+
|    DATABASE     |      CATEGORY      |    SIZE    |
+-----------------+--------------------+------------+
| Key-Value store | Headers            | 50.70 MiB  |
| Key-Value store | Bodies             | 15.60 MiB  |
| Key-Value store | Receipts           | 6.51 MiB   |
| Key-Value store | Difficulties       | 4.74 MiB   |
| Key-Value store | Block number->hash | 3.93 MiB   |
| Key-Value store | Block hash->number | 6.77 MiB   |
| Key-Value store | Transaction index  | 3.03 MiB   |
| Key-Value store | Bloombit index     | 3.54 MiB   |
| Key-Value store | Trie nodes         | 6.83 MiB   |
| Key-Value store | Trie preimages     | 1.50 MiB   |
| Key-Value store | Clique snapshots   | 0.00 B     |
| Key-Value store | Singleton metadata | 139.00 B   |
| Ancient store   | Headers            | 23.94 MiB  |
| Ancient store   | Bodies             | 4.97 MiB   |
| Ancient store   | Receipts           | 1.43 MiB   |
| Ancient store   | Difficulties       | 1.12 MiB   |
| Ancient store   | Block number->hash | 3.01 MiB   |
| Light client    | CHT trie nodes     | 0.00 B     |
| Light client    | Bloom trie nodes   | 0.00 B     |
+-----------------+--------------------+------------+
|                         TOTAL        | 137.62 MIB |
+-----------------+--------------------+------------+

這裡的flaten file儲存,其實是把歷史資料挪到了 ancient 資料夾,不在用 Ldb,而用普通的二進位制格式儲存資料。

.
├── 000034.ldb
├── MANIFEST-000162
└── ancient
    ├── 000006.log
    ├── bodies.0000.cdat
    ├── bodies.cidx
    ├── diffs.0000.rdat
    ├── diffs.ridx
    ├── hashes.0000.rdat
    ├── hashes.ridx
    ├── headers.0000.cdat
    ├── headers.cidx
    ├── receipts.0000.cdat
    └── receipts.cidx

那讀取是不是更麻煩了呢?非也

dbPath      = "/mnt/eth/geth/chaindata"
ancientPath = dbPath + "/ancient" // 必須是絕對路徑

ancientDb, _ := rawdb.NewLevelDBDatabaseWithFreezer(dbPath, 16, 1, ancientPath, "")

for i := 1; i <= 10; i++ {
    // ReadCanonicalHash retrieves the hash assigned to a canonical block number.
    blkHash := rawdb.ReadCanonicalHash(ancientDb, uint64(i))

    if blkHash == (common.Hash{}) {
        fmt.Printf("i: %v\n", i)
    } else {
        fmt.Printf("blkHash: %x\n", blkHash)
    }

    // ReadBody retrieves the block body corresponding to the hash.
    blkHeader := rawdb.ReadHeader(ancientDb, blkHash, uint64(i))
    fmt.Printf("blkHeader Coinbase: 0x%x\n", blkHeader.Coinbase)
}

更詳細程式碼請看newEthLeveldb.go

通過LevelDB讀取的資料

本人1 年前用了 1 個月時間追到了 500w 高度。現在看起來真香,所以做人要淡定啊, 只要你熬得夠久,你就是藝術家了啊。


參考:

https://golangnote.com/topic/81.html
https://johng.cn/leveldb-intro/
https://catkang.github.io/2017/01/07/level...
https://juejin.im/post/5c22e049e51d45206d1...
https://draveness.me/bigtable-leveldb.html
https://blog.ethereum.org/2019/07/10/geth-...

區塊鏈技術愛好者
btc address: 1FmWXNJT3jVKaHBQs2gAs6PLGVWx1zPPHf

相關文章