P2TRからredeemする(key編)

uenohiro4
Nayuta エンジニアブログ
5 min readFeb 4, 2024

Bitcoin mainnetでtaprootが有効になってしばらく経った。どのくらい使われているかは調べていないが、mempool.space で眺めてみるとそこそこ使われている感じがする。

mempool.spaceで見たブロック内のtaproot

taproot のアドレスの種類は P2TR。
“Pay To TapRoot”の略だと思う。”To”を”2"で表すのは英語文化なのかUNIX文化なのか。とにかく、”2" という数字には意味が無いので気にしないように。

今までBitcoinトランザクションについてはハッシュや署名などの暗号関係以外は自分で実装して動作確認してきた。が、さすがにそういうことをしている時間が無くなってきたので、なるべくライブラリを使おうとしている。

JavaScriptで作る場合は bitcoinjs-lib を使うことが多い。例えばこの記事でも使っている(まだtestnetで成功していないのでコードは公開していないが gist に置いている)。

最近はGo を使っているので、github.com/btcsuite/btcd を使って P2TR アドレスに送金された UTXO を redeem するコードを書いてみた。

btcd 自体は数少ない Bitcoinフルノード実装の1つだ。Lightning Network を主に見ている私からすると、LND でよく使われているライブラリという認識である。

以前は github.com/btcsuite/btcutil というのもあったが、btcd に取り込まれて github.com/btcsuite/btcd だけになっている。

フルノードだけあって、とにかく何でもできる。が、それだけに非常に使いづらい。関数が多すぎるし、モジュールも複数あるしで、どこに何を使ったらよいのかが探せないのだ。

今回は key path へのUTXOからredeem するコードを作って regtest で動かしてみた。間違っていたりしたら指摘してもらえると助かるです 🙏

P2TR は segwit version 1 という扱いになっている。今までの P2WPKH などは version 0 である。

違い

P2TRではシュノア署名を使う。
細かい話は BIP-340BIP-341 を読むのがよい。まだ私もスクリプトではない key path 関係のところしか読んでいないが、おおよそこんな感じだ。

  • 今までの secp256k1 ECDSAではないのだが、値の範囲などは secp256k1 に合わせられている。
  • 公開鍵の作り方自体は以前と同じで良いが、表現方法として 32byte になった。以前の先頭部分にあった 02/03 を取り除く。
  • が、秘密鍵からそのまま公開鍵を作るのではなく、一手間掛けるようになっている。tweak と表現されている。調べていないがシュノア署名の脆弱性を回避するためだとか。
Pが秘密鍵、Qが公開鍵
  • 署名するデータの種類として SIGHASH_xxx というのがあるのだが、今回 SIGHASH_DEFAULT (0x00) が追加された。私は SIGHASH_ALL (0x01)でデータを作るのしかやったことがないが、その場合は署名データの末尾に 0x01 を付与していた。少なくとも key path の場合SIGHASH_DEFAULT で 64byte 分の署名だけ載せておけばよさそう。

今回コードを作っていて苦労したのは、署名が合わないところだった。トランザクションデータはできあがるものの、 sendrawtransaction するとエラーになるのだ。

これは、 btcec.schnorr.SerializePubKey() を使うのではなく txscript.ComputeTaprootKeyNoScript() を使って公開鍵を作ることで解決した。tweak する処理が入っているかどうかの違いであった。

動きはしたものの、他に見落としがないか不安になるのは仕方あるまい。先ほど「使いづらい」と書いたのはこういった部分である。
他の golang 人はどうやっているか気になるところである。

--

--