btc: libwally-core で script path (2)
2025/02/05
はじめに
前回 に引き続き libwally-core v1.3.1 で P2TR script path のアドレス作成とそこからの送金を実装していく。
大きな手順
アドレスを作り、そこに送金してもらい、その送金を処理して別のアドレスに送金する。
Bitcoin での処理はだいたいそういう流れだ。
HDウォレットやブロックチェーンの監視などもあるとだんだん面倒になってくるが、そういうのを抜きにするとこれだけだ。
だいたいこんな感じのはずだ。
- アドレスを作る
- internal public key を用意する
- 鍵でもスクリプトでも解けるようにするなら private key から生成する
- スクリプトしか許容しないなら、それ用の internal public key を計算して作る
- スクリプトを用意する
- 使用できない命令(op code)がいくつかあるので、そういうのは別に形に置き換える
- MultiSig 関連の命令も使えないのでスクリプトに置き換えるか MuSig にする
OP_IF
などで分岐して通らないでも済む経路があるなら各ルートだけのスクリプトに置き換える- その過程でスクリプトも多少変わるかもしれない(
OP_EQUAL OP_IF
をOP_EQUALVERIFY
にしたりなど)
- その過程でスクリプトも多少変わるかもしれない(
- 分解したスクリプトは merkle tree の leaf にする
- 正確には tagged_hash などで計算したハッシュ値を leaf にする
- スクリプトを解くときのトランザクション(control block)に解くルートの相手側ハッシュ値を載せることになるので、merkle tree の作りは通る確率が高い leaf を浅いところに置くことが推奨されている
- leaf が 2つなら平たくするけど、3つなら 1つは浅く、残り 2つは1段深く、など
- 使用できない命令(op code)がいくつかあるので、そういうのは別に形に置き換える
- merkle tree から merkle root を求める
- スクリプトを “TapLeaf” で tagged hash する
- merkle root に至るまでの leaf は “TapBranch” で tagged hash する
- leaf のハッシュ値を連結する前にソートすること(BIP-341の仕様)
- internal public key と merkle root から tweaked public key を得る
- 連結して “TapTweak” で tagged hash する
- このときに(そうでなくてもよいが) libsecp256k1 の
secp256k1_xonly_pubkey_from_pubkey()
を使うとcontrol block に載せる parity が得られる - libsecp256k1 で public key の parity(Y座標が奇数なら1、偶数なら0) を直接得る関数はなさそうなのでうまいことやろう
- tweaked public key の頭に
0x51
,0x20
を付けると witness program になる- tweaked public key は 32バイトなので
0x20
- P2TR で使う witness version は 1 なので
OP_1
=0x51
- tweaked public key は 32バイトなので
- bech32m でエンコードしてアドレス文字列にする
- internal public key を用意する
- 解いたトランザクションを作る
- internal public key を処理するかスクリプトを処理するか決める
- internal public key の場合はまだ調べてないので保留
- tweaked private key で署名して key path として処理すると思う
- internal public key の場合はまだ調べてないので保留
- 解くのに使うスクリプトを決める(以下、leaf script)
- 署名が必要なスクリプトなら sigMsg を作って署名する
- key path のときとおおよそ似ているが
ext_flag=1
とする - 最後に Common Signature Message Extension を付与
- key path のときとおおよそ似ているが
- witness をつくる
- 解くために必要なデータを witness の先頭から詰めていく
- witenss に leaf script を詰める
- 最後に control block を詰める
- 先頭は
0xc0
に tweaked public key の parity(Y座標が奇数なら1、そうでないなら0)を足した値 - 次は internal public key
- それ以降は leaf script の hash から merkle root を計算するまでの最小となる leaf hash たち(0個~128個)
- スクリプトが 1つしかない場合は 0個でよさそうだ
- 先頭は
- トランザクションを作ってブロックチェーンに展開
- internal public key を処理するかスクリプトを処理するか決める
TapLeaf Hash
スクリプトを作って “TapLeaf” で tagged hash する。
wally_bip340_tagged_hash()
は secp256k1_tagged_sha256()
を使わずに自分で計算していた。
なんとなく使いそうなイメージだったので意外だ。
しかし、どちらかというと libsecp256k1 が tagged hash を提供している方が珍しいパターンかもしれない。
それを提供するなら SHA256、RIPEMD160、HASH160、HASH256 なども提供してくれれば他の暗号化ライブラリを探さなくて済んだのに!と思った。
libwally-core の中で “TapLeaf” 計算をしている箇所 はあるのだが、tx_to_bip341_bytes()
のように _bytes
がついた関数はシリアライズして uint8_t
配列値に種類に付いた名前だと思う。
これを遡っていってもなかなか static
関数から抜け出せず、最後の方はいろいろな関数から呼び出されているので見るのは諦めた。
自分で “TapLeaf” 計算した方が早かろう。
libwally-core でスクリプトを使うサンプルとして思いついたのは core lightning(c-lightning)。
少し眺めたがスクリプトを作るのに ccan/tal を使って直接バイナリデータとしてスクリプトを作っているような印象を受けた。
こういう感じ だ。
なので、スクリプトをバイナリデータに落とし込むのも libwally-core に頼らず自作した方が早いだろう。
OPコードはマクロ値になっているので役に立つかもしれない。
Script Functions にスタックに push していく関数はあるのでうまく使えるかもしれない。
また、TapLeaf 計算では Compact Size型を使う。
おそらく wally_varint_to_bytes()
でよいと思うが今回はスクリプト長が長くないので使わなかった。
Compact Size型という呼び名より varint の方がよく使われている
スクリプトに埋め込む場合の push長は Compact Size型ではなくスクリプトの PUSH系命令を使う。
wally_script_push_from_bytes()
はPUSH命令とセットでデータを載せてくれそうな感じがする。
ただ、OPコードを載せるのには使えないように見えた。
まあ、あれはスタックに載せるものではないからかもしれないが。
OP_1
のような変換もなさそうだったので、使うときには注意しよう(※私は未確認です)。
Merkle Root
merkle root を作るのは libwally-core になさそう。
libsecp256k1 にもないので自作になるか。
Tweaked Public Key
wally_ec_public_key_bip341_tweak()
を使う。
この関数は 32バイトの xonly ではなく 33バイトの compressed pubkey を返す。
witness に載せる control block に “parity” というビットがあるのだが、tweaked pubkey の Y座標が奇数なら 1
を立てるようになっている。
libsecp256k1 だと pk_parity
を返す関数がそれに使えるのだが、libwally-core だと自分で判定して使うことになるというだけだ。
その代わり、xonly だけ取り出す関数などは無さそうなので自分で先頭バイトをスキップすることになる。
Witness Program と Address
tweaked public key から直接アドレスは生成できない。
witness program にしてから bech32m 文字列に変換する。
wally_witness_program_from_bytes_and_version()
で witness program にすることができる。
が、面倒なら tweaked public key のときに 34byte の配列を用意して、tweaked public key は 3バイト目以降に書込み、先頭に 0x5120
と書き込んでも悪くないと思う。
幸いなことに、というのも変だが、Bitcoin で一度運用された仕様が変わることはほとんど無く、変わる場合はだいたいが追加になる。
そう思うと、ライブラリを使うと実装が楽になったり安全になったりするが仕様変更に強くなったりすることは少ないような気がする。
その中でも witness program は V0 と V1 なので多少は変更に耐えられたのかも?
どうなんだろう。。。
アドレスは wally_addr_segwit_from_bytes()
を使う。
addr_family
は BIP-173 の HRP(human readable part) の文字列を指定する。
BIP-173 には mainnet と testnet は書いてあるが、regtest や signet は書かれていない。
wally_addr_segwit_from_bytes()
は char **output
なのでメモリの確保をされて返ってくる。
なので使い終わったら wally_free_string()
で解放する。
ここまでの実装
アドレス文字列を作ってメモリ解放するところまで実装した。
テストデータはスクリプトが 1つしかないので merkle root の計算はほとんど使われておらずちゃんと実装できているか確認していない。
まあまあな実装量だと思う。
SigHash と Signature
sighash の計算は key path でも使ったwally_tx_get_btc_taproot_signature_hash()
でよい。
tapleaf_script
には tagged hash したハッシュ値ではなくスクリプトを与える。
sighash の計算で leaf script を使うのは Common Signature Message Extension の tapleaf_hash
のはず。
前の方で TapLeaf 関連のコードを見たときに何かやっていることはわかっていたが、ここで使われているようだ。
その関数を提供してくれれば多少楽だったのにと思わなくもないが、
そうすると merkle root の計算も提供しないと中途半端になるから止めたのだろうか。
署名も key path と同じく wally_ec_sig_from_bytes()
に EC_FLAG_SCHNORR
を使えばよい。
鍵は internal とか tweak とかではなくスクリプトを解くためなので対応する private key を使う。
Witness
データが揃ったので witness を作る。
key path では wally_witness_p2tr_from_sig()
だけでよかった。
スクリプトがこう。
20 <leaf pubkey>
OP_CHECKSIG
なので解くための witness にスタックするのは署名だけで良いのだが、 解くためのスタックに加え、leaf script と control block をそれぞれスタックする必要がある。
関数の中身を見ると、サイズのチェックをしたら
wally_tx_witness_stack_init_alloc()
でメモリを確保して
wally_tx_witness_stack_add()
するだけだった。
なので script path では自分で wally_tx_witness_stack_init_alloc()
を呼んで必要なスタック数を確保し、
それぞれ wally_tx_witness_stack_add()
でスタックを積んでいくのがよいだろう。
wally_tx_witness_stack_add()
で確保した数より多くなるとうまいことやってくれそうな気もするが、やらない方がよいだろう。
ここまでの実装
トランザクションを作るところまで実装した。
実際に展開されているトランザクションと同じデータなので、成功したと考えて良かろう。
key path で実装していた箇所が使い回せたのが良かった。
追加
さすがにスクリプトが 1つしかないと実装が正しいかどうか判断できない。
bitcoinjs-lib で作った js-scriptpath を regtest で走らせたときのデータを使ってサンプルを追加した。
現時点で大きく足りていない実装は、merkle tree を作る際に左側の leaf の方が右側よりも値として小さくなるよう並べないといけないという部分だ。
最初はどうしても witness program があわなくて悩んだ。
そのうち実装しよう。
同じバイト数なので memcmp()
での比較でよいのではないかな。
BIP-341 の擬似コード taproot_tree_helper()
では左右の入れ替えをしてハッシュ計算しているだけで、ツリーの構造を変更しようとはしていないように見える。
ならばそう難しくないので実装した。
おわりに
なんとか libwally-core で P2TR のスクリプトパスを解くことができるようになった。
たぶん PSBT を使った場合についても調べた方がよいのかもしれないが、ちょっと違うことをしたくなったのでどうするか決めてない。