如何解析Ethereum数据：读取LevelDB数据

感谢 [Gary](https://github.com/rjl493456442)大佬的细心解答

之前转过一篇文章 [如何解析 Bitcoin 的数据](https://www.jianshu.com/p/1fcc52f01512)，Bitcoin 将 p2p 网络同步来的数据，保存在了[LevelDB](https://github.com/google/leveldb)数据库中。我们可以通过 rpc 的方式请求数据，这些请求来的数据是从LevelDB(以下简称 ldb)获取的。如果我们能直接读取 ldb 的数据，就可以绕过 rpc请求，直接读取。

### 为什么绕过 rpc ，直接读取 ldb 呢

*一个字 快，二个字真快*。
我们可以通过 tcp 直接读取 mysql 数据，也可以通过
[navicat](https://www.navicat.com.cn/products/navicat-for-mysql)这样的 GUI 工具读取。有时候为了快速读取数据，我们会舍弃GUI这样美观的工具，而用更直接的方式。
直接读取 Bitcoin 的 ldb 数据库， 就是舍弃了 美观的 rpc 工具，显而易见的好处是快。

由于 Bitcoin 使用 Ldb 保存区块链数据，之后的很多链沿用了这个技术路线，Ethereum也同样使用 Ldb 保存数据。

### 什么是 LevelDB

Ldb是Google 工程师Jeff Dean和Sanjay Ghemawat开发的NoSQL 存储引擎库，是现代分布式存储领域的一枚原子弹。在它的基础之上，Facebook 开发出了另一个 NoSQL 存储引擎库 RocksDB，沿用了 Ldb 的先进技术架构的同时还解决了 LevelDB 的一些短板。现代开源市场上有很多数据库都在使用 RocksDB 作为底层存储引擎，比如大名鼎鼎的 [TiDB]([https://github.com/pingcap/tidb](https://github.com/pingcap/tidb)
)。

在使用 Ldb 时，我们可以将它看成一个 Key/Value 内存数据库。它提供了基础的 Get/Set API，我们在代码里可以通过这个 API 来读写数据。你还可以将它看成一个无限大小的高级 HashMap，我们可以往里面塞入无限条 Key/Value 数据，只要磁盘可以装下。

Ldb有多种储存结构，这些结构并不是平坦的，而是分层组织(Level)的，这也是LevelDB名字的来源。如下所示：

> ![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/169726/326e1152-ec01-e21e-3042-501c4ce91f00.jpeg)


### Ldb基本使用示例

Ethereum 使用最广的开发版本是 go-eth，我们自然用 golang做些基本的代码事例。

```go
// 读或写数据库
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， 用代码试试

``` go
// 先连接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

```golang
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](https://github.com/liushooter/learn-blockchain/blob/master/golang/ethLeveldb.go)

------------------------------ 更新分割线-------------------------------

go-eth 在 2019 年 7 月推出了`v1.9.x`版本，在同步数据、读取Ldb方面做了大量的封装，使用起来更为方便了。 使用 `syncmode=archive`同步方式的时候， 同步时间从 **62 天变成了 13 天，5 倍 。**

>![](https://upload-images.jianshu.io/upload_images/47789-05a04112e179e603.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

上面的代码在读取`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
```

那读取是不是更麻烦了呢？非也

```go
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](https://github.com/liushooter/learn-blockchain/blob/master/golang/newEthLeveldb.go)

![通过LevelDB读取的数据](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/169726/834137f2-0a81-6649-8258-40c9cc4468a1.jpeg)


本人1 年前用了 1 个月时间追到了 500w 高度。现在看起来真香！！！

---

参考：

http://qyuan.top/2019/10/12/mpt-2/
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/