先日リリースされた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のクラスを使ってBitcoinのP2Pメッセージを構成できる。例えば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_message
はbitcoin-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で確認したブロックのハッシュで、フィルターの値も同じであることが分かる。
ということで(getcfcheckpt
とcfcheckpt
メッセージは割愛したけど)、Bitcoin Core 0.21.0で有効化できるようになったBIP-157フィルターが利用できるようになってることを確認できた。これでBIP-37に代わる新しい軽量クライアント向けのフィルタ機能が利用できるようになる。