hiro99ma blog

何か技術的なこと

btc: libwally-core で script path (2)

2025/02/05

はじめに

前回 に引き続き libwally-core v1.3.1 で P2TR script path のアドレス作成とそこからの送金を実装していく。

大きな手順

アドレスを作り、そこに送金してもらい、その送金を処理して別のアドレスに送金する。
Bitcoin での処理はだいたいそういう流れだ。
HDウォレットやブロックチェーンの監視などもあるとだんだん面倒になってくるが、そういうのを抜きにするとこれだけだ。

だいたいこんな感じのはずだ。

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_familyBIP-173 の HRP(human readable part) の文字列を指定する。 BIP-173 には mainnet と testnet は書いてあるが、regtestsignet は書かれていない。

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 Extensiontapleaf_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 を使った場合についても調べた方がよいのかもしれないが、ちょっと違うことをしたくなったのでどうするか決めてない。

< Top page