hiro99ma blog

何か技術的なこと

btc: BIP-341が難しい (3)

2025/01/16

はじめに

P2TR の script path でアドレスを作って送金し、そこから送金するところまでやろうという試み。
BIP-341 を読み解くのが難しいのでがんばっているところ。

BIP-341 再び

script path spend

scriptPubKey には、witness program しかないのでスクリプトの全貌は分からない。
witness version とデータ長さえ合っていれば乱数でも送金先として有効だと思う。
これは P2PKH や P2WSH など他の送金先でもエンコード手順や方法が違うだけで条件は同じだ。
なので作った P2TR アドレスへの送金は失敗しない。 失敗するのはその scriptPubKey に署名して送金するときだ。

“key path”, “script path” と書いているが、データとしてはどちらも混ぜられる。
送金するときにシングル鍵の方を使うなら key path spend、スクリプトの方を使うなら script path spend、というところか。
BIP-341 でも “Spending using the key path” や “Spending using one of the scripts” などという書き方だし。

script path spend だけにしたくても key path のデータは省略できないので、 P を特定の値(H)にして H + rG を internal keyとして使う(internal public key だよな)。

スクリプトの分解

分岐が複数あるスクリプトでの P2TR サンプルが見つからないのだが、1分岐で一通りやったらイメージが沸くかもしれないので先に進むことにした。
気にしているのは、たとえば BIP-112 にあったこういう分岐の中でさらに分岐があった場合だ。

    HASH160 DUP <R-HASH> EQUAL
    IF
        "24h" CHECKSEQUENCEVERIFY
        2DROP
        <Alice's pubkey>
    ELSE
        <Commit-Revocation-Hash> EQUAL
        NOTIF
            "2015/10/20 10:33" CHECKLOCKTIMEVERIFY DROP
        ENDIF
        <Bob's pubkey>
    ENDIF
    CHECKSIG

ルートはこの 3つになる。

  1. IF ルート
  2. ELSE - NOTIFを通るルート
  3. ELSE - NOTIFを通らないルート

分岐が深くなるほどルートは増えるのだが、これを全部単独のスクリプトとして書き出さないといけないのだろうか。
まあ、実際のスクリプトでそこまで複雑に作ることは少ないだろうし、仕方ないのか。

サンプル

ではサンプルを見てみよう。
ちなみに、昨日書いた「スクリプトのデバッグをしたい」はこの btcdeb のことである。

OP_IF からいきなり始まっているが、それ以外はほぼ同じ構成だ。
ということは、送金するときは OP_FALSE(0x00) かそれ以外をスタックに載せれば良いのかな?

OP_IF
    144
    OP_CHECKSEQUENCEVERIFY
    OP_DROP
    <pubkey_alice>
OP_ELSE
    OP_SHA256
    preimage_hash
    OP_EQUALVERIFY
    <pubkey_bob>
OP_ENDIF
OP_CHECKSIG

これを 2つに分ける。
BIP-342 の方に書かれている。
Script execution には OP_CSVOP_CLTV を含むと書いているので、そのまま使ってよいだろう。
OP_CSVOP_CLTV のようにチェックして NG だったら終了するような命令は分岐と考えなくてよさそうだ。
OP_CHECKSIGOP_CHECKSIGVERIFY はシュノア署名での検証になったりするが、命令の書き換えなどは不要。

そうするとこうなる。

image

IF文側:

144 OP_CHECKSEQUENCEVERIFY OP_DROP <pubkey_alice> OP_CHECKSIG

ELSE文側

OP_SHA256 preimage_hash OP_EQUALVERIFY <pubkey_bob> OP_CHECKSIG

これはそのままでよかったが、命令を実行した結果によって OP_IF などで分岐をする場合、OP_IF をなくしても問題ないようなスクリプトに書き換えることもある。
OP_EQUAL OP_IF だったら OP_EQUALVERIFY に置き換えるなどだ。
BIP-341 に書いてあるのは general guideline なので明確にこうしろというのはないのだと思う。 ただせっかく merkle tree にして全部のスクリプトを見せなくてもよいようにできるのだからそうしましょうよ、ということだと思う。

元のスクリプトがあるから「分割」だけど、スクリプト全体を載せることがないので全然関係ないスクリプトたちでもよいはずだ。
全部のスクリプトを載せないといけなかったら、それらがうまく動くようにいろいろ複雑に考えないといけないけど script path なら気にしなくてよいはずだ。

merkle tree

分割したスクリプトが merkle tree の leaf になる。
BIP-341 の def taproot_output_script(internal_pubkey, script_tree) で witness program を出力するようなのだが、再帰呼び出しなどしていて分かりづらい。

script_tree が無しの場合は key path と同じ。
2 番目の「script_treeと同じ構成」というのは 1 番目の tuple のことを言ってるのだろう。
tuple だけだった場合、最初が leaf version、次が script になっている。
leaf version は 0xc0 を想定している。
internal public key を tweak public key にしたように、ここでは “TapLeaf” を使って merkle tree の leaf になる値の計算をする。
TapLeaf の計算は “leaf_version” + “script” を使うのだが、ここでは Compact Size型のスクリプト長を使うとのこと。

image

なぜここになって Compact Size型にしたのだろうか?
ともかく btcdebのサンプル では prefix_compact_size() という関数になっていて先頭のデータは命令でのデータ長にはなっていないのでスクリプトの本体だけでよさそうだ。
144 から始まるので 029000(0190 だとマイナス値扱いだったか)になっている。

script_tree がリストだった場合、[0] が左要素、[1] が右要素として TapLeaf の計算をしている。
ここがゴチャゴチャしてよくわからない。
とにかくここは左と右の TapLeaf を連結した値を TapTweak でも TapLeaf でもなく TapBranch で計算する。
連結する左と右も単純に連結するのではなく、左が小さい値になるようにして連結する。

こうやってできた TapBranch の merkle root を internal public key と連結して TapTweak 計算やらあれこれすると tweak public key になる。
これを key path と同じく bech32m でエンコードすると P2TR アドレスのできあがりだ。

この辺でもう実装しようという気力がなくなってきた。。。まだ署名に行き着いていないのに。
署名と書いたが、スクリプトを解くことができるなら署名である必要はない。

検証など

こうやって作った merkle tree を検証する。
するのだが、BIP-341 の図で leaf の A, B, C, D, E が同じ高さにいないのはなんでだろう?
2. Organization によると leaf が 3以上だとツリーの作り方に自由度があるそうだ。
実行される可能性が高いスクリプトを階層の浅いところに置くことで演算量を減らすことができるそうだ。
といっても、ツリーの構造全体はスクリプトに載らないのでお気持ち程度でよかろう。

image

witness stack が 2つ以上だと script path で、stack の一番最後が control block、その1つ前が使用したスクリプトになる。
control block のところに merkle tree 関係の値が載っている。
BIP-341 では c となっている。

昔、とりあえずやってみよう、ということでこちらのサイトを見て bitcoin-js でやってみたのがこちらである。
たぶん自分でやったんじゃないかと思うが記憶にない。。。

おわりに

書いていてだんだんと訳がわからなくなってきた。
実装する気力も失せてきているが、どうしたものか。

なんとなくイメージは分かったので次に進んでしまえとも思うし、 ここまでやったんだから簡易的にでも実装しておいた方が心残りが無いのではとも思う。

明日考えよう。

< Top page