hiro99ma blog

何か技術的なこと

btc: libsecp256k1 は MuSig 2 だった

2025/02/02

はじめに

P2TR で MultiSig をしたい場合、OP_CHECKMULTISIG のような命令が使えないためスクリプトを書くか 1つの鍵と見なすような MuSig 的なことをしないといけないことがわかった。

前回は libsecp256k1 に MuSig 1 のサンプルがあったので試した。
MuSig 1 は、 MuSig 2 が現れたので区別をつきやすくするために “1” と付けられるがそれまでは単に “MuSig” とだけ呼ばれていたものだ。

試したといってもサンプルコードをコピーして regtest のトランザクションに当てはめただけで、自分で実装したわけではない。
秘密鍵が必要なのは署名するときの secp256k1_musig_nonce_gen()secp256k1_musig_partial_sign() のようだが、 これを秘密鍵を持つ人で分散できるのかがわからなかった。
まあ、鍵を誰かが集めないといけないとかだったら使う人がいないから、きっと署名だけ作ってもらえばなんとかなるんだろう。
secp256k1_musig_partial_sig_agg() が名前としても署名を集約しそうだし。

nonce が出てきたりなんか複雑なのでもう MuSig はいいかなと思ったのだが、 MuSig 2 をまったく見ないわけにはいかんだろう。

MuSig 2

数学的や暗号学的なところは詳しい人に任せよう。
Blockstream の研究開発は信用してよいと思っているので、そこを参照する。

もしかして MuSig 2 ?

前回、libsecp256k1 のサンプルコードを使って MuSig 1 を試した。
・・・と思っていたのだが、secp256k1-musig.h のコメントにこう書いてある。

This module implements BIP 327 “MuSig2 for BIP340-compatible Multi-Signatures” v1.0.0.

もしかして、libsecp256k1 に載っているのは MuSig 1 ではなく 2 ??
BIP-327 とまで書いてあれば間違いあるまい。
libsecp256k1 の v0.6.0 CHANGELOG にも MuSig2 と書いてあるし。
昨日の記事には追記した。お詫びして訂正いたします 🙇。

いやぁ、MuSig 2 が出てきて以降は「単なる MuSig = MuSig 1」だと思っていたのだが、時代としては MuSig 2 がデフォルトになったということだろうか。
油断できないところだ。
Blockstream の記事は 2020年11月、libsecp256k1 v0.6.0 は 2024年11月なので十分に時間が経過したということかもしれん。

署名者は nonce を 2つ作り、1回目にはそれらを渡し、署名の時にはその 2つを演算して 1つの nonce にしてから計算する。
nonce を 1つにする計算のパラメータとして署名者全員から集めた nonce、集約 pubkey、メッセージ(sigMsgのこと?)がいる。

nonce を作っているのは secp256k1_musig_nonce_gen() なのかな?
単に nonce1, nonce2 ではなく secure, public と鍵のような扱いになっている。
サンプルコードのコメント にあるように Round 1 では public な nonce だけ渡すようだ。
“coordinator” は MultiSig のとりまとめ役だろう。取りまとめるだけなら署名者以外でもよいということだろう。
全署名者から public nonce を受け取った coodinator はそれらを集約した nonce をそれぞれに送り返す。
記事には集約 pubkey やメッセージも送るようなことを書いていたが、BIP-327 になるときに仕様が変わったのかもしれない。
secp256k1-musig.h のヘッダコメントに “v1.0.0” と書いてあるしね。
history からすると v1.0.2 がこれを書いている時点では最新のようだ。

そしてこちらが Round 2。
部分署名を集めたら署名を集約する。
署名が集まったなら誰がトランザクションを展開してもよいだろう。

簡単に見えるが、実際にやろうとしたら通信路というかデータをやりとりする方法をどうするかを考えないといけないので面倒だ。

cache と session

libsecp256k1 で MuSig 2 を実装していくと secp256k1_musig_keyagg_cachesecp256k1_musig_session の扱いが分からなくなった。

secp256k1_musig_keyagg_cachesecp256k1_musig_pubkey_agg() で生成される。
secp256k1_musig_pubkey_agg() は公開鍵の集約に使う。 なので coodinator が集約して signer には集約した公開鍵を配れば良いと考えていた。
しかしこの cache 値は部分署名する secp256k1_musig_partial_sign() の引数に出てくる。
この値については今のところ libsecp256k1 では parse や serialize の関数を提供していない。 197バイトの配列なので signer にそのまま渡すこともできるのだが、 中身が分からないデータを渡すよりは各 signer に全員の公開鍵を配った方がよさそうだ。
誰だか分からない相手と MultiSig することはないだろうし、自分で集約した公開鍵を計算するのが Bitcoin らしいだろう。

secp256k1_musig_sessionsecp256k1_musig_nonce_process() で生成される。 これは nonce の生成で使うのだが、coodinator は nonce を作る必要がない。
必要はないが、呼び出すこと自体は問題ないのかな。

サンプル実装

libwally-core の key path サンプルを改造して libsecp256k1 の MuSig2 サンプルコードを追加した。
データは regtest で展開できたときのものを使ったので、乱数要素はなくして固定値にしている。

MuSig はプロトコル外

トランザクション的に MuSig は 1 だろうと 2 だろうと key path だ。
なのでライブラリとしては P2TR key path の手順で tweaked public key からアドレスを作ったり部分署名をしたり署名を key path の witness に詰めたりする機能があればよい。
PSBT のフォーマットでうまいこと部分署名を渡したりできるのかは未調査だが、 PSBT で集約する機能は無いだろうからライブラリのサポートは期待できないだろう。

おわりに

今回はこれに尽きる。

< Top page