Develop with pleasure!

福岡でCloudとかBlockchainとか。

Bitcoin Core 0.21.0でサポートされたBIP-157フィルターを確認してみる

先日リリースされたBitcoin Core 0.21.0で軽量クライアント向けて新しいフィルターの提供がサポートされるようになった。有効になったBIP-157の仕様については↓

techmedia-think.hatenablog.com

BIP-157のサポートはBitcoin Core起動時に-blockfilterindex=1 -peerblockfilters=1オプションを付けて起動すると有効になる。

フィルターの確認

BIP-157自体はフィルターをP2Pメッセージで提供する仕様を定義したもので、フィルターの仕様はBIP-158で定義されており、以前のバージョンからフィルターの作成自体は可能だった。ブロックハッシュを指定してgetblockfilter RPCを実行すると対象ブロックのフィルターデータが取得できる。

$ ./bitcoin-cli -signet getblockfilter 000000a70fc28dac4bd9638ed2fe72545dbc0d71f9bf0e59e326350c8c53336e
{
  "filter": "0c79dd114255fdef1542cd3f2495e0f9346b4fc32a154639f375e0691ce5a36d",
  "header": "238ca2b3a4c3f2e3eb8c97af339d2cff57b570028563ccef222661ff061dcef5"
}

フィルターのデータは、<datadir>/indexes/blockfilter/basic以下にあり、その内ディレクトリ直下にあるfltrxxxx.datというバイナリファイルにエンコードされたフィルターのデータが格納されている。具体的には<block hash><フィルターのCompactSize><エンコードされたフィルター>という形式で各フィルターのデータが記録されるようになっている。

例えば、signetのブロック000000a70fc28dac4bd9638ed2fe72545dbc0d71f9bf0e59e326350c8c53336eの2つめのトランザクション47e6e0e2d0c8a6186be35b1a42807383e9e56094c704a818631681f48cc07606の最初のアウトプット

{
  "value": 48.75997730,
  "n": 0,
  "scriptPubKey": {
    "asm": "0 1ed6f4590e7088dbebdf66b7072e87ab9c29b0c0",
    "hex": "00141ed6f4590e7088dbebdf66b7072e87ab9c29b0c0",
    "addresses": [
      "tb1qrmt0gkgwwzydh67lv6mswt584wwznvxqwnjm8n"
    ]
  }
}

が含まれているかどうかは、bitcoinrbで以下のように検証できる。

require 'bitcoin'

block_hash = '6e33538c0c3526e3590ebff9710dbc5d5472fed28e63d94bac8dc20fa7000000'
encoded_filter = '0c79dd114255fdef1542cd3f2495e0f9346b4fc32a154639f375e0691ce5a36d'
 # filterをデコード
filter = Bitcoin::BlockFilter.decode(Bitcoin::BlockFilter::TYPE[:basic], block_hash, encoded_filter).filter
filter.match?('00141ed6f4590e7088dbebdf66b7072e87ab9c29b0c0'.htb)
=> true

実装の詳細については、以前書いた記事を参照。

P2Pメッセージ

↑は今までも確認できた。今回の本題はBIP-157なのでP2Pメッセージになる。P2Pメッセージの確認はRPCではできないので、irbベースでBitcoinのノードと対話的にP2Pメッセージをやりとりするツールbitcoin-p2pを作ってみた。今回は、これを使って確認してみる。

事前準備

bitcoin-p2pは、Rubyが使える環境であればgem install bitcoin-p2pでインストールできる。接続先のノードのアドレスを-hオプションで、接続するネットワーク(mainnet, testnet, signet)を-nオプションを使ってbitcoin-p2pを起動する。

$ bitcoin-p2p -h localhost -n signet
> connected! You can send version message.

となるので、最初にハンドシェイクのためversionメッセージとverackメッセージを交換する。bitcoin-p2pでは、bitcoinrbがロードされているので、bitcoinrbのクラスを使ってBitcoinP2Pメッセージを構成できる。例えばversionメッセージは、

3.0.0 :001 > ver = Bitcoin::Message::Vresion.new
 => #<Bitcoin::Message::Version:0x00005590317410f0 @version=70013, @services=8, @timestamp=1611462693, @local_addr=#<Bitcoin::Message::NetworkAddr:0x0000559031738ae0 @time=1611462693, @ip_addr=#<IPAddr: IPv4:127.0.0.1/255.255.255.255>, @port=3... 

で生成し、必要なパラメータは随時更新した上で、

3.0.0 :002 > send_message(ver)
 => send message data: 0a03cf4076657273696f6e00000000006700000093cb71427d110100080000000000000025f80c6000000000080000000000000000000000000000000000ffff7f00000195bd080000000000000000000000000000000000ffff7f00000195bd519f0f6807576b1c112f626974636f696e72623a302e362e302f0000000000

すると、接続先のノードにversionメッセージが送信される。send_messagebitcoin-p2pに定義されているメソッドで、bitcoinrbに実装されているメッセージクラスインスタンスか、P2Pメッセージのデータを渡せば、それを相手に送信する。

versionメッセージを送信すると、すぐに相手からversionメッセージが返ってくる↓

 =>  receive version. payload: {"version"=>70016, "services"=>1097, "timestamp"=>1611462696, "local_addr"=>{"time"=>nil, "ip_addr"=>#<IPAddr: IPv6:0000:0000:0000:0000:0000:0000:0000:0000/ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff>, "port"=>0, "services"=>0}, "remote_addr"=>{"time"=>nil, "ip_addr"=>#<IPAddr: IPv6:0000:0000:0000:0000:0000:0000:0000:0000/ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff>, "port"=>0, "services"=>1033}, "nonce"=>5555482496462444166, "user_agent"=>"/Satoshi:0.21.0/", "start_height"=>21843, "relay"=>true}

※ ちなみにBIP-157のメッセージを確認するので、最初に書いた起動オプションが有効なノードに接続する必要がある。具体的にはサービスフラグのNODE_COMPACT_FILTERSが有効になっているノード。このフラグが有効かどうかは受信した相手のversionメッセージのservicesの値から確認できる。今回はそういったノード探すのも面倒だったので、ローカル環境でsignetに接続したノードを使ってる。

続いて、verackメッセージを返す。

3.0.0 :003 > ack = Bitcoin::Message::VerAck.new
 => #<Bitcoin::Message::VerAck:0x00005590316342c0> 
3.0.0 :004 > send_message(ack)
 => send message data: 0a03cf4076657261636b000000000000000000005df6e0e2

相手からもverackメッセージが返ってくる。ここまでで最初のハンドシェイクは完了。

※ 他のメッセージも送れるようになるが、こちらの送信したメッセージが仕様に準拠していなかったり相手のポリシーに違反する場合は、接続が切断される。この場合、=> connection is closed. please stop.とコンソール上にメッセージが表示されるので、stopで終了する。

フィルタヘッダーの取得

IBDで最初にブロックヘッダーを同期するように、BIP-157でも軽量クライアントは最初にフィルターヘッダーの同期を推奨してる。フィルターヘッダーは、フィルタータイプ=0(Basic)、要求する最初のヘッダーのブロック高、要求する最後のブロックのブロックハッシュ(リトルエンディアン)を指定して、getcfheadersメッセージを送信する。最初にRPCでフィルターを確認したブロックのブロック高が20921で、それから6ブロック分のデータを取得するメッセージが↓

3.0.0 :005 > getcfheaders = Bitcoin::Message::GetCFHeaders.new(0, 20921, "8e2a4663fc79a21a173c2dc6f715bd4bda4f9b939cb806390b98e007ac000000")
 => #<Bitcoin::Message::GetCFHeaders:0x000055e4221f5520 @filter_type=0, @start_height=20921, @stop_hash="8e2a4663fc79a21a173c2dc6f715bd4bda4f9b939cb806390b98e007ac000000"> 
3.0.0 :006 > send_message(getcfheaders)
 => send message data: 0a03cf40676574636668656164657273250000000f40e64500b95100008e2a4663fc79a21a173c2dc6f715bd4bda4f9b939cb806390b98e007ac000000

すると、↓のcfheadersメッセージが返ってくる。

3.0.0 :007 > 
 => receive cfheaders. {"filter_type"=>0, "stop_hash"=>"8e2a4663fc79a21a173c2dc6f715bd4bda4f9b939cb806390b98e007ac000000", "prev_filter_header"=>"df27cb4b59ebe70c93de3985ba367802e65ebd119671b2a46f612223898de4a6", "filter_hashes"=>["1f257e1e028cea1cc50d41c6acc6039c2d7e6d3611dfb475ef6e1224533f1879", "fc9c229680cefc9e88907dd15952781a11502fa7670e30eedd6c7be7fe2ad8f2", "bd9b4c675c1cec25d8b0ab7bf6132fd143dee1e0918173e3f6c56c6bca72259f", "18357fb16bf1f322fb8a054efeccc05a94bf4837df51288b15da7022572a5396", "dcc177c2588dbde7357c06f0dbd13d5ad4ba25dddfeb337f7e6f25acc119020c", "39ac2e33dbf8450794209b61092185b9fd0da9ee90020c1bb473d8dd9fc37269"]}

フィルターの取得

続いて、フィルターの取得では、getcfiltersメッセージを送信する。使用するパラメータはgetcfheadersと同様。

3.0.0 :008 > cfilter = Bitcoin::Message::GetCFilters.new(0, 20921, "8e2a4663fc79a21a173c2dc6f715bd4bda4f9b939cb806390b98e007ac000000")
 => #<Bitcoin::Message::GetCFilters:0x000055e422038250 @filter_type=0, @start_height=20921, @stop_hash="8e2a4663fc79a21a173c2dc6f715bd4bda4f9b939cb806390b98e007ac000000"> 
3.0.0 :009 > send_message(cfilter)
 => send message data: 0a03cf406765746366696c7465727300250000000f40e64500b95100008e2a4663fc79a21a173c2dc6f715bd4bda4f9b939cb806390b98e007ac000000

メッセージを送ると、要求した数分のcfilterメッセージが送られてくる。

 => receive cfilter. {"filter_type"=>0, "block_hash"=>"6e33538c0c3526e3590ebff9710dbc5d5472fed28e63d94bac8dc20fa7000000", "filter"=>"0c79dd114255fdef1542cd3f2495e0f9346b4fc32a154639f375e0691ce5a36d"}

 => receive cfilter. {"filter_type"=>0, "block_hash"=>"5b9c72782f163fca7737163462ec11e561b3abe8cad18d0269027a0e4b000000", "filter"=>"010207f0"}

 => receive cfilter. {"filter_type"=>0, "block_hash"=>"df44626bcfd2b104b4b4a4af8c0ceb06b8f855312bc557e7b18468e717000000", "filter"=>"016b8f60"}

 => receive cfilter. {"filter_type"=>0, "block_hash"=>"a47f8529260810d790b4660ca2b7c96d8e9f0b2c926e18227745131ce7000000", "filter"=>"0161c130"}

 => receive cfilter. {"filter_type"=>0, "block_hash"=>"300fea5fcad1f8e7eea5fb705a481c25723b67d94687a26bbe75400c2e010000", "filter"=>"019102d0"}

 => receive cfilter. {"filter_type"=>0, "block_hash"=>"8e2a4663fc79a21a173c2dc6f715bd4bda4f9b939cb806390b98e007ac000000", "filter"=>"016e9cd0"}

最初に受信したのがRPCで確認したブロックのハッシュで、フィルターの値も同じであることが分かる。

ということで(getcfcheckptcfcheckptメッセージは割愛したけど)、Bitcoin Core 0.21.0で有効化できるようになったBIP-157フィルターが利用できるようになってることを確認できた。これでBIP-37に代わる新しい軽量クライアント向けのフィルタ機能が利用できるようになる。