見出し画像

Solana のスナップショットのあれこれ

調査のために Solana の過去の状態を取得する必要があり、スナップショットからのアカウントデータ抽出を行いました。

スナップショットからバリデータを起動できる環境をもっていればバリデータに取り込む方法があるはずです。ただ、Solana のバリデータを起動するリソースはなかなか用意するのが大変です。ローカル環境やほどほどのスペックの EC2 で作業できる方法を探しました。

スナップショットの入手方法

Solana のスナップショットにはすべてのアカウントの情報が含まれており、現在では圧縮した状態で 36GB 程度の大きなファイルです。

バリデータやRPCでは定期的 (デフォルトでは 25000 秒ごと(7時間程度)) にフルスナップショットを取得するようです。

スナップショットを入手するには、以下の方法があるようです。

  1. 定期的にスナップショットをクラウドストレージなどにアップロードして公開してくれている人から提供してもらう (あまり公開されていない)

  2. スナップショットを提供している RPC からダウンロードする (ただし、最新のものしか得られない)

過去のデータを使う必要があったので 1. のアップロードされているデータを使いました(Solana の Discord をウロウロしていたら JitoLab のファウンダーが公開しているのを発見しました)。

2. の RPC からダウンロードする方法も知らなかったのですが、今回知ることができました。

下記で説明されていますが、RPC のアドレスの末尾に snapshot.tar.bz2 をつけてアクセスすると、公開している RPC ならダウンロードできます。

Solana のパブリック RPC から取得する場合は下記です。

curl -OL https://api.mainnet-beta.solana.com/snapshot.tar.bz2

ちんたらとダウンロードしていると接続が切れてしまい成功しません。EC2 などネットワークがしっかりしている環境からダウンロードすると 15 分ほどでダウンロードが成功します。

スナップショットからのデータ抽出

solana-snapshot-etl

スナップショットデータを扱うツールを探していたところ、良さげなものとして solana-snapshot-etl を見つけました。

CSV や sqlite にアカウントを抽出してくれます。
非常にありがたいツールだったのですが、下記の問題がありました。

  • 抽出できるのが token account など一部に限定されている

  • それ以外のアカウントはメタデータのみで「データ」は抽出されない

  • 全データが抽出されるので容量が大きくなる

solana-snapshot-gpa

アカウントの抽出に使いたい条件は次の2つです。

  • pubkey 直接指定

  • getProgramAccounts で使っているフィルタ

ということで、solana-snapshot-etl をフォークした solana-snapshot-gpa を作りました。

オプションは --pubkey と --owner です。

  • --pubkey 抽出したいアカウントのpubkeyをカンマ区切りで指定

  • --owner 抽出したいアカウントのオーナー・プログラムを指定

    • size アカウントのサイズでフィルターする場合に指定

    • memcmp アカウントの特定データでフィルターする場合に指定

      • 0xで始まる16進数 + @ + オフセット

      • base58 + @ + オフセット

solana-snapshot-gpa
 --owner=xxxxxxx
 --owner=xxxxxxx,size:xxxx
 --owner=xxxxxxx,size:xxxx,memcmp:0xffffffff@offset,memcmp:base58@offset
 --pubkey=xxxxxx,xxxxxxxx,xxxxxxx,xxxxxxx,xxxxxxx,xxxxxxxx,xxxxxxx
 --pubkey=xxxxxx,xxxxxxxx,xxxxxxx,xxxxxxx,xxxxxxx,xxxxxxxx,xxxxxxx
 snapshot.tar.zst > result.csv

試しに TOKEN_PROGRAM が所有する 165 byte のアカウントを探します。これはトークンの保有量を保持しているアカウントです。絞り込み条件により、所有者が r21Gamwd9DtyjHeGywsneoQYR39C1VDwrw7tWxHAwh6 のアカウントのみを抽出します。所有者を示す owner は 32 バイトから開始されるため、オフセットは @32 としています。

$ time solana-snapshot-gpa --owner=TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA,size:165,memcmp:r21Gamwd9DtyjHeGywsneoQYR39C1VDwrw7tWxHAwh6@32 snapshot.tar.bz2 > result.csv
...
[2022-12-05T14:23:18Z INFO  solana_snapshot_gpa] AppendVec processed: 413300
[2022-12-05T14:23:18Z INFO  solana_snapshot_gpa] AppendVec processed: 413400
[2022-12-05T14:23:18Z INFO  solana_snapshot_gpa] Done!

real	5m6.308s
user	1m58.612s
sys	1m10.952s

$ cat result.csv
pubkey,owner,data_len,lamports,write_version,data
9xpPgSejp1rfqDRTujS6cMd5garWTaqBAnQbhkj4MN1,TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA,165,2039280,537945849017,ERCgG7BshiinSe1kUBSowCHs3Lmp+jdw5NXxW29/Y+AMjph4T4MwT0YUgNeGtHvaBFkU0iG0rHd0ApevtnFTNQAAAAAAAAAAAAAAADVIuXtEKf470z5CN9OY+SH3e0733OyfsHH8PdygEwwiAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
6yHB6hhYtFP4gXZymoRfBp5qnPBSPGjAtqVLi21KcjuE,TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA,165,2039280,535770232457,9I+BOYp6Tfs+8VtyPhE1wk0R+rHTW37eVyK4Mhvl230Mjph4T4MwT0YUgNeGtHvaBFkU0iG0rHd0ApevtnFTNQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
7irpAfZYwTbr4NMkyDEyDNqBMAMoA44tMaGCfToYbo9p,TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA,165,2039280,535888904692,YnHLcRlHa53OANgVyP8xX8i/fShIYz00lCrf1TXy3v4Mjph4T4MwT0YUgNeGtHvaBFkU0iG0rHd0ApevtnFTNRm7tQEAAAAAAQAAAGDmA0wtHGra5qBVAjBpXMCE6UTpg4FuDJ5FCJASq50RAQAAAAAAAAAAAAAAANUHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
8q85CrsPyy2SfMJ7RetYskxMZcrvmgDtNjLFQiVZa6p9,TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA,165,2039280,536127990126,XePT0xBjXC/Ok2KhHR9qsyxZCd3Cnq6CM8pIFrSe0IQMjph4T4MwT0YUgNeGtHvaBFkU0iG0rHd0ApevtnFTNQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
8r39tn4tMCPCaS59hLrY8cv6M6LhQyVX2NipezdjJnzx,TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA,165,2039280,536130476567,sL13hcKkCx1unCUxGenG+XCiovQXZw7khAvvd9JTwXcMjph4T4MwT0YUgNeGtHvaBFkU0iG0rHd0ApevtnFTNaFUAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
9pEjLA2ao9LdMGWooyFdr5Ri7Z4AwoEH7yUbY4m97GAv,TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA,165,2039280,536298161690,eHlQ2TjOhqrKcFPZRWtix9zFAtI/+wdnweTMMEaT3XEMjph4T4MwT0YUgNeGtHvaBFkU0iG0rHd0ApevtnFTNQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
8noFwkQxtZSbvwerLiF41oTe1pKyf5YC2xmiAvPztS2z,TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA,165,2039280,536118598155,HMroaiXJqgvGFbJlTlx2xbErruUfwd05rpRCN/tnuE4Mjph4T4MwT0YUgNeGtHvaBFkU0iG0rHd0ApevtnFTNQAAAAAAAAAAAAAAAEIvdqYHlXJKnIYAAJpw6oQcaxudiNZXD46DtnjiwWB8AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Cxn28paABybbguTTabqU9Hyc7T1TGUw5ow1pnVJZyD7n,TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA,165,2039280,536877527299,qFm/hqhxPkBbL5Dax+HhPtHex4dhEdVtdgmHukSVK1oMjph4T4MwT0YUgNeGtHvaBFkU0iG0rHd0ApevtnFTNWZIXQIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
yZFHyimNt1okqmAnCA34uA9eFtHcqKXSFJXAFJSSE5h,TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA,165,2039280,534891994542,BwcwUjLmFgwYtAmdpbZr04e8ZGmtIkTyg/c/8qjkAzgMjph4T4MwT0YUgNeGtHvaBFkU0iG0rHd0ApevtnFTNQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
5nN6ud6PgB9wG8hfNj5rmT6amRg4tt5LTuC2JLQV2WxH,TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA,165,2039280,535578759258,dZBLiBD3ofylBXoB3+jn+7YV9uM6ett50/TE/vtrH4YMjph4T4MwT0YUgNeGtHvaBFkU0iG0rHd0ApevtnFTNQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
A94QXcnanSWBYXjT39UJP3Fo2qHZgmcvjTW4zhBpdmMA,TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA,165,2039280,536349850708,6ddwYwXiw7WMU1gryEDowLM8XR/2d2c7pJn6c2Km9+QMjph4T4MwT0YUgNeGtHvaBFkU0iG0rHd0ApevtnFTNVtMAgAAAAAAAAAAAMq0yEIhWxmrtaT8k5zKAGtdMhIUK3+kDhzUEPHRkb+xAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
78YxYnJheptgT1t8fEPae66gMM2T5gqtU3ZCWpPGk5Ve,TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA,165,2039280,535795592507,N5mMy/LQRYthXLzGsaNnxHSen+9zBmIuGxtYkQEgvJoMjph4T4MwT0YUgNeGtHvaBFkU0iG0rHd0ApevtnFTNQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
GbMB98t6YfVxn3Pbu54EyUkuLqdaERLANvyghtrzSf6e,TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA,165,2039280,537573190222,bSzl7WjOignQep8J48sLWXRN+yidilz09ipUuZGo1wAMjph4T4MwT0YUgNeGtHvaBFkU0iG0rHd0ApevtnFTNQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
HpCV8TYRpguG7oJtC7cfSfhiv63RbFP8gYJLQfwLHjJX,TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA,165,2039280,537818850957,5TWfcD3YnUKJlz+BPOw72Fm03bjC49ocX3LhNERoZeoMjph4T4MwT0YUgNeGtHvaBFkU0iG0rHd0ApevtnFTNQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
3CoMZ825sEGfaUGjHdsDynNixn3aTgrtZUYGTN2hgqYR,TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA,165,2039280,535103498556,YLUUloYk2dwujVoYD5gK+EXfpdftNEHdZDz/oxQVpZYMjph4T4MwT0YUgNeGtHvaBFkU0iG0rHd0ApevtnFTNQZIEQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
GMSqxLgB5uX13pibMJLPd4zMFWzkBwFGU7jW69rRLuAP,TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA,165,2039280,537522549053,uVO1+N1UV6Kg8NQZA0CXhbnYTUBFYU+qT1Be4TLc12kMjph4T4MwT0YUgNeGtHvaBFkU0iG0rHd0ApevtnFTNYQ8mwEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
4167pJdBjAoSj94GpFQ1EZKzQBd6jq5kos4MxVKt3Ywq,TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA,165,2039280,535231237790,ZuUYihMIoduQttMfP73KjD3yZ4yBEt/dPRksWjzEV6gMjph4T4MwT0YUgNeGtHvaBFkU0iG0rHd0ApevtnFTNULoAAAAAAAAAAAAAInktd96sbUcACjp1XSU8eLH6WWM6grZ6flXqTxfNF2bAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AqQiNjyTd7cNA4iwKMHiF7EkV9D7eeiJ4ms1u1r6EyYS,TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA,165,2039280,536465663071,KmosRlsSKLZ1i8+T3UBZPSoGXrxT1gyNecNy+ug1I4kMjph4T4MwT0YUgNeGtHvaBFkU0iG0rHd0ApevtnFTNQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
...

36GB 程度の圧縮ファイルで展開すればおそらく10倍近いサイズのデータをスキャンするためそれなりの時間がかかります。ディスクがすべて SSD の EC2 (t2.large) 上で実行すると 5-6 分です。それほど我慢しなくて使えます。手元の外付け HDD に保存して実行すると 30 分ほどかかってしまい、結構待たされます。

抽出データの加工

solana-snapshot-gpa の出力は CSV で、データは Base64 エンコードのため、その後の取り回しは柔軟に可能だと思います。

ただし、出力で厄介なのが、1つのアカウントに対して複数の行・複数のバージョンが出力される点です。

これは Solana のアカウントが追記専用の AppendVec というメモリ・マップドファイルの集合として管理されており、スナップショットは取得時点で有効な AppendVec のセットだということに起因します。つまり、すべてのアカウントについて最新のデータだけを持っているわけではなく、全体としてすべてのアカウントの最新データを含むが、古いデータも含むという状態です。

古いデータを弾くには、write_version 列を使います。同じアカウントのなかで古い write_version の行は捨ててしまえば OK です。

solana-snapshot-gpa 自体がこれを行ってもよいものの、次の理由から行っていません。

  • アカウントの変化を追いかけるのにある程度有効

  • 発見したアカウントを出力するだけのストリーム処理ではなくなり、メモリを消費する

最新の write_version のみを残すワンライナー

awk を使います。

  • 先頭のヘッダ行は読み飛ばし

  • write_version 列で大きい順にソート

  • 同じ pubkey のなかで最初に登場したもの(最大の write_version) のみ出力

tail -n +2 result.csv | sort -t, -k5,5nr | awk -F, '!dup[$1]++' > result.latest.csv

solana-test-validator に読み込むための .json ファイル生成

これまた awk を使います。
CSV の 1 行ごとに pubkey.json に書き込みます。
保存先のディレクトリは awk に -v オプションで入力しています。

mkdir accounts
cat result.latest.csv | awk -F, -v out="accounts" '{ filename=out"/"$1".json"; print "{\"pubkey\":\"" $1 "\",\"account\":{\"lamports\":" $4 ",\"data\":[\"" $6 "\",\"base64\"],\"owner\":\"" $2 "\",\"executable\":false,\"rentEpoch\":0}}" > filename; close(filename) }'

保存先のディレクトリ(accounts) にある .json をすべて読み込ませた solana-test-validator を起動するには --account-dir オプションが利用できます。

solana-test-validator --account-dir accounts --reset 

抽出データの注意点

solana-snapshot-gpa はスナップショットに含まれる AppendVec に含まれているアカウントデータが条件にマッチしたらすべて抽出します。そして、write_version が最新のものを残す外部処理を行います。

多くの場合、この処理は問題ありません。ただし、状態が変化するフィールドを memcmp でフィルターする場合は注意してください。

途中段階で memcmp のフィルターを満たしたものの、スナップショットの最後の時点で memcmp のフィルターを満たさなくなっていた場合、途中段階のものを write_version の最新版として抽出します。(マッチしないものは CSV にでてこないため)

ですから、memcmp による比較に関しては次に注意してください。

  • 変化しないフィールドに対して利用する

  • --owner による抽出は pubkey を探すラフな検索とわりきり、最後は --pubkey 指定ですべてのバージョンを出力する (pubkey 指定の場合は状態変化に関係なく必ず最新バージョンを含む出力が得られるため)

参照

What is an exact definition of a snapshot on Solana

詳しい雰囲気の回答だと思ったら solana-snapshot-etl の作成者様ではありませんか…

Cloudbreak

AppendVec という追記専用のメモリ・マップドファイルを使うアイデアは Cloudbreak に原案が記載されている。

Awk

awk 内でリダイレクトできちゃうと初めて知った。

awk 内のリダイレクト後に close(filename) でちゃんと閉じないと 253 ファイルぐらいで開けるファイル数の上限になってエラーに。


この記事が気に入ったらサポートをしてみませんか?