如何解析Ethereum数据:读取LevelDB数据
感谢 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 比喻成氢弹,它比 LevelDB 的威力更大一些。现代开源市场上有很多数据库都在使用 RocksDB 作为底层存储引擎,比如大名鼎鼎的 TiDB。
在使用 Ldb 时,我们可以将它看成一个 Key/Value 内存数据库。它提供了基础的 Get/Set API,我们在代码里可以通过这个 API 来读写数据。你还可以将它看成一个无限大小的高级 HashMap,我们可以往里面塞入无限条 Key/Value 数据,只要磁盘可以装下。
Ldb有多种储存结构,这些结构并不是平坦的,而是分层组织的,这也是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/blob/v1.8.7/core/database_util.go#L53
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
在数据方面做了重新整理,大概有以下两个非兼容的改动:
-
历史的区块链数据(header,body, receipts等)被挪到一个flaten file存储中,因为这部分数据已经是不会更改的了
-
更改了部分数据结构的scheme,例如receipt。原先很多字段不需要存到db,是可以在read之后重新计算出来的。这部分会占据大量的存储空间,在1.9把这些字段删去了。
这里的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
1 年前用了 1 个月时间追到了 500w 高度。恩,年轻人,做人要淡定啊,只要你活的足够长,说不定官方已经解决了呢。
参考:
https://golangnote.com/topic/81.html
https://johng.cn/leveldb-intro/
https://catkang.github.io/2017/01/07/leveldb-summary.html
https://juejin.im/post/5c22e049e51d45206d12568e
https://draveness.me/bigtable-leveldb.html
https://blog.ethereum.org/2019/07/10/geth-v1-9-0/