Develop with pleasure!

福岡でCloudとかBlockchainとか。

Utreexoベースのノード実装Floresta

UtreexoをサポートするRustで実装された軽量ノード実装Florestaを試してみる。

https://github.com/getfloresta/Floresta

以前書いたように、UtreexoはUTXOセットのアキュムレーターとして機能するので、UTXOセット自体を保持するのではなく、有効なUTXOセットに対するコミットメントのみを保持するようになる。

  • このコミットメントはマークルフォレストの各ツリーのルートハッシュのリストになるので、UTXOセット自体を保持するのと比べると劇的にデータ保持量は少なくなる。
  • またFloresta自体はブロックは受信するもののその後破棄する

ため、これまでのノード実装と比べるとノードが保持するデータ量がとてもコンパクトになる。

Florestaの実行

先日、0.9.0がリリースされたばかりだったのでそれを利用する。リリースページにはバイナリがなかったので、ビルドする。

$ cargo build --release
...
$  ./target/release/florestad --daemon 

デフォルトのデータディレクトリは$HOME/.florestaで、変更する場合は--data-dir <パス>で指定する。

IBDが始まりブロックチェーンの同期が行われるが、約1時間20分ほどで同期が終わった。これは以前行ったBitcoin CoreのAssumeUTXOを利用した同期よりも速い。要因としては、

  1. AssumeValidにより、ブロック939,969までの各トランザクションのスクリプトの署名検証はスキップされる。
  2. Utreexoの仕組みによってUTXOセット自体をディスクに保存することがないので、ブロックの同期中に発生していたディスクIOがなくなった。
  3. AssumeUtreexoにより、ブロック939,969までのマークルフォレストのルートハッシュのリストがchainparams.rsにハードコードされており、マークルフォレストの更新はそれ以降のブロックに対して行われる。
    • 一応、同期後にジェネシスブロックからハードコードされたブロックまでマークルフォレストを生成して検証が行われる(--no-backfillオプションをつけて行わなくすることも可能)。

は挙げられる。おそらく2の効果が大きい。

getblockchaininfoを実行すると、root_hashesというフィールドにUTXOのマークルフォレストのルートハッシュのリストが表示される↓

$ ./target/release/floresta-cli getblockchaininfo
{
  "best_block": "000000000000000000003c6c733368d6ff92d012ef3fe11119916cd48193124d",
  "height": 943984,
  "ibd": false,
  "validated": 943984,
  "latest_work": "00000000000000000000000000000000000000011d844a2abb6b83fb34be0ae8",
  "latest_block_time": 1775526916,
  "leaf_count": 3087305105,
  "root_count": 11,
  "root_hashes": [
    "db5957872e6ef754f4e0bfe106b1d68469052c76808ef2e943abd275daccb6b4",
    "d97528a6964d45540edae4eff2319150e1cc72d652c920cf5b01a787472e07dc",
    "f2c17db54572d31e1909518aac369b88c1bb9f5aaac4ca200a6910083ba3dd7a",
    "ace99d96476e4e7b1575bde8cb7d86edb60990afb0b9e50e69444cd210966c55",
    "d27bdab690640d5c544cbb7638674df77066be98f098ec7adf63042799408426",
    "b7226a72fde83486a42bbb1740057e818eb4c042bef9696d18360061b645addd",
    "f5989c22810edfb0ac4ebbb5914e0270aef9138a6f150cbf359e3c234e3cd8c7",
    "1bf05999bcec61f320153be3d724dd827eae50bf616113029f91ffbd9d1d7b6e",
    "63e9ae72f97c34d3ec87ec1108ad0ba9c4c9511ee6bda05c3500a9ccf0d92d59",
    "fef9fec3a23b4cc9833ef4e36ae3011629e20074a6e4ea6b7750906a48848b25",
    "2da3e45cc5b725d4948866168a2c4c4fcef046d4751f1c2dd0de4317387d5c98"
  ],
  "chain": "bitcoin",
  "progress": 1.0,
  "difficulty": 138966872071213
}

データディレクトリ

Florestaのデータディレクトリの内容は(容量はブロック943984時点のmainnetのデータ)、まずディレクトリ直下が

ファイル サイズ 内容
cfilters 12GB BIP158コンパクトブロックフィルター(ウォレットスキャン用)
cfilters-index 152B フィルターの位置インデックス
db 2.8KB 監視専用ウォレットのKey/Valueストア
peers.json 6.8MB 既知のピアアドレス一覧
conf 62B 設定ファイル

他にデバッグログや、pidファイルがある。

続いて、chainstateディレクトリ内が

ファイル サイズ 内容
headers.bin 2GB*1 ブロックヘッダー関連のデータ(124バイト×約94万ブロック)
blocks_index.bin 64MB ブロックハッシュから高さを引くためのインデックス
accumulators.bin 2.4MB*2 Utreexoアキュムレーター状態(各ブロック毎に、マークルフォレストのリーフの数とルートハッシュのリストを保持。)
fork_headers.bin 2MB フォークチェーンのヘッダー(再編成時に使用)
metadata.bin 2.2KB チェーンの状態(ベストブロックや検証済み高さなど)

コンパクトフィルター以外のデータの使用量はわずかなので、現状のmainnetでもディスク使用量は13GBまでいかない(後述するウォレットの連携をしないのであれば--no-cfiltersでこの部分は削減できる。また、--filters-start-heightを使って開始ブロック高を指定すれば過去の不要なインデックス分スペースを節約できる)。直近のフィルターのみを保持するようにすればデータサイズは数百MBで済む。

ちなみにgetblock RPCを実行するとトランザクションのリストも返って来て、あれブロック保持していないのに?と不思議に思ったけど、どうやら都度ネットワークにブロック問い合わせて返すようになってるっぽい。

ブリッジノードとの連携

Utreexoノードは、UTXOセットを保持していないため、新しいブロックを受信した際に、その中に含まれている各トランザクションがインプットで参照しているUTXOが有効なUTXOかどうか単体では確認できない。そのUTXOがマークルフォレストに含まれていることを証明するための包含証明を必要とする。この証明を提供してくれるのが、ブリッジノードと呼ばれるノードになる。ブリッジノードは(現在BIP-183のドラフトで定義中の)以下のサービスビットを通知している:

  • NODE_UTREEXO1 << 12):
    新しいブロックやトランザクションの包含証明を提供するブリッジノード(NODE_NETWORK_LIMITEDと組み合わせると直近288 ブロック分、NODE_NETWORKと組み合わせると全ブロック分)
  • NODE_UTREEXO_ARCHIVE1 << 13):
    過去すべてのブロックに対してその包含証明のみを提供するブリッジノード(ブロック自体は提供しない)

Florestaは、DNSシードからこれらのサービスビットを持つノードの情報を入手して、ピアとして接続する。AssumeUtreexoを利用して同期する場合はNODE_UTREEXOノードでよく、利用せずに(--no-assume-utreexoで実行)ジェネシスブロックから同期する場合はNODE_UTREEXO_ARCHIVEノードに接続する必要がある。ブリッジノードと接続したら、BIP-183で新しく導入される2つのP2Pメッセージgetuproofuproofを使って、ブロックの有効性を検証する。

sequenceDiagram                                                                                                                                                                                                              
      participant F as Floresta                                                                                                                                                                                                
      participant B as ブリッジノード<br/>(UTREEXO)                                                                                                                                                                            
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 
      F->>B: getdata (block_hash)                                                                                                                                                                                              
      B->>F: block (生ブロック)                                                                                                                                                                                                
                                                                                                                                                                                                                               
      Note over F: 必要な入力を特定<br/>(ローカルキャッシュにないもの)                                                                                                                                                         
                                                                                                                                                                                                                               
      F->>B: getuproof                                                                                                                                                
      B->>F: uproof                                                                                                                                                                   
                                                                                                                                                                                                                               
      Note over F: 1. 証明を検証<br/>2. ブロックを検証<br/>3. Utreexoフォレスト更新<br/>4. ブロック破棄(保存しない)

Txはリレーしない

今後Txもリレーするようにするのかもしれないけど、v0.9.0時点ではハンドシェイク時にrelay = falseを指定してるのでTx自体のリレーはしないようになっている。リレーする場合はブリッジノードとの間でutreexotxメッセージで証明付きのTxを要求するような実装が必要になる。

組み込みElectrumサーバー

Floresta自体にウォレットの機能はなく、Electrum サーバーが組み込まれている。ウォレットを利用したい場合は、別途ElectrumやSparrow などのウォレットアプリをFlorestaのElectrum サーバーに接続するという使い方になる。またその際は、予めウォレットからxpub(拡張公開鍵)をエクスポートして、floresta-cliimportdescriptorコマンドで拡張公開鍵やディスクリプターをインポートしておく必要がある。Florestaにはwatch-onlyウォレットはあるので、インポートしたディスクリプターから導出したアドレスについてインデックスを構築する。

*1:2GBもあるのはメモリマップドファイルでこの容量確保しているため。実際のディスク消費は書き込んだ分だけなので現状110MBちょっと。

*2:AssumeUtreexoのブロック以降の状態のみ保持しているので少なめになっている