ethereumのデータを分析する場合、大量のBlockHeaderやTransaction、Receiptを取得する必要がある。これらの膨大なデータをHTTPを介してJSON-RPCで取得しようとするとかなりの時間を要することとなる。
そのため、今回は大量データ処理をするために直接go-ethereumのLevelDBから値を読みだす方法を調べたのでその内容をまとめる。
- 試した環境
- go-ethereumのLevelDBをopenする
- 最新のblock headerを読みだす
- block headerをまとめて読みだす
- block headerのパース
- ancient DBについて
試した環境
今回はpython 3.8を使ってLevelDBから値を読みだしてみた。
使ったライブラリは以下の2つ
go-ethereumのLevelDBをopenする
go-ethereumはLevelDBファイルを
db = plyvel.DB('/home/nakajo/ethereum/geth/chaindata')
db.close()
ちなみに、LevelDBの仕様として1つのプロセスしかDBをオープンできないので、ほかのプログラムからchaindataをオープンする場合は、gethのプロセスを停止しておく必要がある。
最新のblock headerを読みだす
最新のblock headerは特別なkeyで保存されている。
具体的には以下のソースを参照。
https://github.com/ethereum/go-ethereum/blob/v1.10.8/core/rawdb/schema.go#L29-L43
なので、例えば、最新のblockのhash値が欲しければ以下のようにkeyを指定すれば取得できる。
db = plyvel.DB('./goerli2/geth/chaindata') try: value = db.get(b'LastBlock') print('FastSync Latest Header Hash = {0}'.format(value)) finally: db.close()
なお、LevelDBは壊れやすいのでしっかりとdb.close()すること。
block headerをまとめて読みだす
1件ずつ読み出すのはLevelDBに直接アクセスする意味がないので、次はまとめて読みだす方法を考える。
https://github.com/ethereum/go-ethereum/blob/v1.10.8/core/rawdb/schema.go#L78-L85
を見ると、prefixが`h`から始まるkeyにblock headerが格納されていることがわかる。
なお、plyvel(というかLevelDB自体?)がprefixしか指定できない。
prefixが`h`から始まるkeyは3種類あるので、keyの長さが41のものだけに絞り込めばblock headerが取得できそうである。
ということで、block headerをまとめて読みだすコードは次のようになる。
def load_block_headers(db): count = 0 for key, value in db.iterator(prefix=b'h'): if len(key) == 41: number = int.from_bytes(list(key)[1:9], byteorder='big', signed=False) block_hash = bytes(list(key)[9:]).hex() print(f"number={number}, hash={block_hash}") count += 1 if count > 1000000: break print(f"count={count}")
prefixがbから始まるkeyを読み出すことで、同様のコードでblock bodies、つまりtransactionを取得するが可能となる。
block headerのパース
block headerをleveldbからまとめて読みだす方法が分かったので、次は取得したblock headerをパースする必要がある。
block headerは上記コードのうち、valueにその実態が格納されており、これはRLPフォーマットとなっている。そこで、rlpライブラリを用いてパースすれば、中身が取得できる。
具体的には次のようなコードとなる。
def to_uint(number_bytes): return int.from_bytes(number_bytes, byteorder='big', signed=False) def dump_header(header_bytes): headerArray = rlp.decode(header_bytes) print("---------------- Block Header --------------") print(f"parentHash={headerArray[0].hex()}") print(f"uncleHash={headerArray[1].hex()}") print(f"coinbase={headerArray[2].hex()}") print(f"Root={headerArray[3].hex()}") print(f"txHash={headerArray[4].hex()}") print(f"receiptHash={headerArray[5].hex()}") print(f"bloom={headerArray[6].hex()}") print(f"difficulty={headerArray[7].hex()}") print(f"number={to_uint(headerArray[8])}") print(f"gasLimit={to_uint(headerArray[9])}") print(f"gasUsed={to_uint(headerArray[10])}") print(f"timestamp={to_uint(headerArray[11])}") print(f"extra={headerArray[12]}") print(f"mixDigest={headerArray[13].hex()}") print(f"nonce={headerArray[14].hex()}") print("--------- End Header ----------")
ancient DBについて
最後に、ancient DBについて軽く触れておく。
go-ethereumでは直近の90,000 blockがleveldbに格納されており、それより古いblockについてはancient DBに保存されている。ancient DBの実態は、
ancient DBはどうやら独自フォーマットらしくここからデータを読み出す方法が現在のところ不明。