アルゴリズムとかオーダーとか

仕事で勉強したことなどをまとめてます

go-ethereumのLevelDBを直接読む

ethereumのデータを分析する場合、大量のBlockHeaderやTransaction、Receiptを取得する必要がある。これらの膨大なデータをHTTPを介してJSON-RPCで取得しようとするとかなりの時間を要することとなる。
そのため、今回は大量データ処理をするために直接go-ethereumのLevelDBから値を読みだす方法を調べたのでその内容をまとめる。

試した環境

今回はpython 3.8を使ってLevelDBから値を読みだしてみた。
使ったライブラリは以下の2つ

go-ethereumのLevelDBをopenする

go-ethereumはLevelDBファイルを/geth/chaindata/ 以下に保存している。なので、例えば、/home/nakajo/ethereumをdatadirとして指定していた場合は、以下のようにpathを指定することで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の実態は、/geth/chaindata/ancient/ディレクトリ以下に存在する。
ancient DBはどうやら独自フォーマットらしくここからデータを読み出す方法が現在のところ不明。