hiro99ma blog

Miniscript

目次 (最終更新日:2025/08/12)

概要

Miniscriptという、Bitcoinスクリプトを構造的に書くための言語がBIP-0379にある。 ここではその仕様とデモ実装を紹介する。

仕様については Miniscriptを書くユーザーが必要な情報もあるが、それ以上に Miniscriptを実装するための情報が多い。 私は書くだけのつもりなので実装仕様については読み飛ばした。

この実装を使ったsipaサイトでは最初に”policy”というものが出てくるので混乱するかもしれないが、構成はこうなっている。

Miniscriptで書くことによって自分で直接Bitcoinスクリプトを書くよりも間違いを減らしやすい。 また解析もできるため最適化したり動的にスクリプトを解くウォレットにできるかもしれない。
Bitcoin Optechの記載がわかりやすかったのでGoogle翻訳した文章を載せる。

MiniScriptを使用すると、ソフトウェアはスクリプトを自動的に分析できます。これには、そのスクリプトで保護されているビットコインを使用するために、証人データを生成する必要があるものを決定するなどがあります。 MiniScriptがウォレットに何をする必要があるかを伝えると、ウォレット開発者は、あるスクリプトテンプレートから別のテンプレートに切り替えるときに新しいコードを記述する必要はありません。

「証人データ」は”witness data”で、つまり redeem するのにどういうデータを witenss に置かないといけないのかわかるようになるということだろう。
例えばこれは公開鍵に対する署名が必要なちょっと複雑なスクリプトを Miniscript で作って解析したものである。

image

Bitcoinスクリプトに変換した結果が一番下にあるが、いきなりこのスクリプトを書くのは難しいだろう。
これが Miniscript だと、「スクリプトを解く条件が 2つあってどちらか片方だけでよいので or系」「署名チェックを条件にしたいので pk系」のように決めていって or_d(pk(key_likely),pkh(key_unlikely)) のように書ける。
それでもいきなり Miniscript にするのは難しいので “policy” で「こうしたい」を書いて Miniscript に変換することもできるようになっている。 “policy” は BIP-379 には書かれていないので、sipa氏の親切から生まれたのだろうか。

BitcoinスクリプトをMiniscriptに変換できそうに思うが、少なくともこのデモページにはなかった(ChatGPT)。

サイト

仕様

demoサイトの”Miniscript reference”を見ていく。

P2WSH or Tapscript?

P2WSHとTapScriptで使用できる命令が異なるところがある。 ここで関係するのはMultiSigのところで、P2WSHまでは OP_CHECKMULTISIG のようなMultiSig専用の命令がいくつかあったが、 TapScriptではそれらは使用できなくなって代わりに OP_CHECKSIGADD が使えるようになった。 署名と公開鍵のチェックをする OP_CHECKSIG にカウント値をインクリメントする機能が加わったような命令である。
Miniscriptでは multi() がP2WSH用、multi_a() がTapScript用となっている。

Translation table

“translation table”という表が載っている。
左列から順に、表記の意味、Miniscriptでの表記(fragment)、対応するBitcoinスクリプトとなっている。 Bitcoinスクリプトで使用できるものにすべて割り当てがあるわけではないが、よほど複雑なことをしようとしなければ事足りると思われる。

pk(key) など関数のように表記する fragment と、その前にコロンで区切って s:pk(key) のように表記する wrapper がある。
wrapper が複数ある場合は tar コマンドのオプションのようにつなげて書く。

“key” に相当するデータは、P2WSH では 33バイト(0203 で始まる)、TapScript では 32バイト(x-only表現)として扱う。

真ん中の列にイコールの表記があるものはシンタックスシュガーだそうだ。 ここから下にも表がいくつか出てくるが、シンタックスシュガーの方は省略するとのこと。 わざわざそう書いているのは、記載がないからシンタックスシュガーだと対象外になるのでは?という心配をさせないためだろう。

ハッシュのpreimage、つまり元値は 32バイトのみとする。 いろいろ理由は書いてあるが、ちょっと英語が難しい。。。不正なことをしやすくなるのを避けるためだと読み取った。
Merkleツリーの時のように SHA256(A || B) なんかはあり得そうだが、そういうのをやりたかったら自分でBitcoinスクリプトを書けば良いだけだろう。

Correctness properties

Miniscriptは、Bitcoinスクリプトに変換する全体だけでなく、例えばOR条件の一部だけでもそう呼ぶので、それらをタイプ分けしている。

それとは別に 5タイプの modifier で追加の特性を保証する。
Miniscript の使用者よりもコンパイル側で考慮する内容なので一般ユーザーは気にしなくてよさそう。

Detecting timelock mixing

nSequencenTimeLock の話。
“mixing” は、これらがブロック表現と時間表現の両方を取り得るので、それらが混ざることを言っている。

混ざらないように書こう!

Resource limitations

Bitcoinスクリプトのリソース制約について。 大きいスクリプトを書かない限りは関係しないが、チェックするときにスクリプトが正しいかどうかだけでなくサイズも気にしないといけないので知っておいて損はない。

絶対的なコンセンサスアルゴリズムによる制約と、ノード毎に異なる standardness の制約がある。
standardness は Bitcoin Core ではオプションによって変更できるそうだ。btcd などもそうなのだろう。
chatbtcにも質問した。

おそらく Miniscriptコンパイラがチェックしてくれるだろう。

列挙してみたものの、なかなか難しいものだ。

ただ、こういうのがない状態でがんばってスクリプトを作り、それを使う側と使われる側が協議して “内容は大丈夫” とお互いに認めて、 実際に展開しようとしたらこれらのチェックのどれかに引っかかって展開できなかった、というのは十分あり得ると思う。
regtest で試したとしても、スクリプトそのものの間違いは確認できてもリソース制約についてはわかりづらいだろう。

また、これらの制約は静的に判断するものだけでなく、動的に判断するものもあるそうだ。
例えば実行する or_b(X, Y) は、X の評価までだったら大丈夫なものの Y まで評価したら超してしまう、などということもあり得るということだ。
ツールがチェックしてくれそうな気はするが、わざわざそれを記載しているということは覚えておかねばなるまい。
どんなに便利なツールがあったとしても最終的にはトランザクションを展開した人しか責任を持てないのだ。

Security properties

ここまでの内容を満たすことで保証できるものとできないものが書いてあるのだと思う。 “completeness”なんてめったに聞かない単語なのだが、検証界隈でよく使われる用語なのだろうか。

また、署名チェックがないスクリプトは脆弱だとも書かれている。 確かに、ほとんど見たことはない。 Lightning Networkの anchorくらいではなかろうか。

“policy”

“Policy to Miniscript compiler” の “Supported policies:” 以下に policy というものの書き方が載っている。
いきなりMiniscriptを書くというのは難しいので、さらに 1つ層をもうけたというところか。

ビルド

C++

$ git clone https://github.com/sipa/miniscript.git
$ cd miniscript
$ make

$ echo "pk(key_1)" | ./miniscript
X    108.0000000000    35 pk(key_1) pk(key_1)

オリジナルをビルドしたコマンドで出力が少ないので、forkしてsipaサイトで出力している項目を追加した。
policy のコンパイルと Miniscript のコンパイルのコマンドも分けた。

$ git clone https://github.com/hirokuma/miniscript.git
$ cd miniscript
$ make

#
# policy to miniscript and asm
#
$ echo "or(99@pk(key_likely),pk(key_unlikely))" | ./policy
<<Spending cost>>
script_size=   63
input_size=    73.3500000000
total_cost=   136.3500000000

<<miniscript output>>
or_d(pk(key_likely),pkh(key_unlikely))

<<Resulting script structure>>
<key_likely> OP_CHECKSIG OP_IFDUP OP_NOTIF
  OP_DUP OP_HASH160 <HASH160(key_unlikely)> OP_EQUALVERIFY OP_CHECKSIG
OP_ENDIF

<<Resulting script (hex)>>
2102504b626b65795f6c696b656c7900000000000000000000000000000000000000ac736476a914504b686b65795f756e6c696b656c79000000000088ac68

#
# miniscript to asm
#
$ echo "or_d(pk(key_likely),pkh(key_unlikely))" | ./miniscript
count=0
scriptlen=63
maxops=8
type=B
safe=yes
nonmal=yes
dissat=unique
input=-
output=1
timelock_mix=no
miniscript=or_d(pk(key_likely),pkh(key_unlikely))

<<Resulting script structure>>
<key_likely> OP_CHECKSIG OP_IFDUP OP_NOTIF
  OP_DUP OP_HASH160 <HASH160(key_unlikely)> OP_EQUALVERIFY OP_CHECKSIG
OP_ENDIF

<<Resulting script (hex)>>
2102504b626b65795f6c696b656c7900000000000000000000000000000000000000ac736476a914504b686b65795f756e6c696b656c79000000000088ac68

JavaScript/WASM

JavaScriptとWASMのコードも生成できる。

$ sudo apt install emscripten
$ make miniscript.js

これらのファイルが生成された後であれば、ローカルのブラウザで index.html を開くとsipaサイトと同じことができた。

知識がないので、ここは調べていない。

Policy の例

“policy” はBIP-379にはないが、Miniscriptを一から書くのも大変なので便利そうだ。

A single key

公開鍵 key_1 による署名を要求する。

# Policy
pk(key_1)

# Miniscript
pk(key_1)

Bitcoinスクリプトで解くときはこうなる(未確認)。 <<~>> は redeem する witness スタックである。

<<signature with key_1>>
<key_1>
OP_CHECKSIG

計算

image

One of two keys

equally likely

公開鍵 key_1key_2 による署名を要求する。 1-of-2 MultiSig のように見えるが、witness のデータは2つ必要である。

面白いのは「鍵の確率は等しい(equally likely)」の記述だ。

# Policy
or(pk(key_1),pk(key_2))

# Miniscript
or_b(pk(key_1),s:pk(key_2))

Bitcoinスクリプトで解くときはこうなる(未確認)。 <<~>> は redeem する witness スタックである。

<<signature A>>
<<signature B>>
<key_1> OP_CHECKSIG OP_SWAP <key_2> OP_CHECKSIG OP_BOOLOR

まず <<signature B>><key_1>OP_CHECKSIG され、結果 true/false がスタックに載る(<<signature B>><key_1> は消える)。
OP_SWAP することでスタック上の <<signature A>> と結果を入れ替えて <<signature A>> を上にする。
<<signature A>><key_2>OP_CHECKSIG され、結果 true/false がスタックに載る(<<signature A>><key_2> は消える)。
スタックにはそれぞれの OP_CHECKSIG の結果 2つが載っている。
OP_BOOLER はスタック上の2つを取り除いて OR した結果をスタックに載せる。
これでスタック上には <<signature B>><key_1><<signature A>><key_2> かのどちらか片方でも署名チェックが成功していれば true、どちらも失敗していれば false が載っている。

書く順番が key_1key_2 なのでスタックする署名もその順番と考えそうだがそうではないことに注意しよう。

TapScript ならそれぞれの script path にしてしまえばよいと思う。 いや、P2WSH でもこういうスクリプトを使うことはないような? まあ、サンプルにあれこれいうのは野暮だろう。

one likely, one unlikely

こちらは確率が高い鍵とそうでない鍵があるパターン。
Policy の N@ は、そちらの Policy の方がデフォルトよりも N 倍高い確率で選択されるという意味である。
2倍までだとどちらでもよいからなのか等確率と同じMiniscriptになった。
それ以外にも、平均的なトランザクションサイズの計算にも使われる。 likelyが発生する確率が高いと平均的なトランザクションサイズにもそれが反映されるというわけである。

# Policy
or(99@pk(key_likely),pk(key_unlikely))

# Miniscript
or_d(pk(key_likely),pkh(key_unlikely))

Bitcoinスクリプトで解くときはこうなる(未確認)。 <<~>> は redeem する witness スタックである。

OP_FALSE
<<key_unlikely>>
<<signature B>>
<key_likely> OP_CHECKSIG OP_IFDUP OP_NOTIF
  OP_DUP OP_HASH160 <HASH160(key_unlikely)> OP_EQUALVERIFY OP_CHECKSIG
OP_ENDIF

最初の OP_CHECKSIG までは同じだが、そちらは失敗するのが分かっているので <witness> はダミーで良いはず。
違いはその次で、<key_likely> の署名である確率が高いから、もしその署名チェックに失敗したときだけ続きを行うよう OP_NOTIF で囲んでいる。
OP_IFDUP は、スタック最上部が非ゼロならそれを複製、そうでなければ何もしない命令。 なので、

となる。

しかしこのスクリプトは <key_unlikely> をスタックに載せるようになっていないので witness スタックの方で載せることになる。 <<key_unlikely>>OP_DUP で複製し、OP_HASH160 で SHA256 + riepmd160 し(複製したデータは消える)、スクリプトに埋め込んであった<HASH160(key_unlikely)> をスタックに載せ、OP_EQUALVERIFY<HASH160(key_unlikely)> とを OP_EQUALVERIFY で比較する(両方ともスタックから消える)。 OP_EQUALVERIFY は不一致なら即座にスクリプトが失敗終了する。
成功したらその結果はスタックに載せずに続けるので、<<signature A>><key_unlikely> で署名チェックする。
その結果がそのままスクリプトの結果になる。

なぜスクリプトに <key_unlikely> を直接埋め込まないかというと、おそらくそちらの方が確率が高い方の鍵だった場合のサイズが小さくなるからだ。

計算

image

miniscript: or_d(pk(key_likely),pkh(key_unlikely))

image

Analyze された結果の上にカーソルを当てると情報がホバーされる。
その情報がかなり難解である。

scriptlen はそのままスクリプトの長さだろう。 分岐命令があるので合計してもあわないが、そこは気にしなくてよい。

max stack size はスクリプトを解くときに使用するスタックの最大サイズだろう。 “key_unlikely”を通る経路の方はスタックが多くなるので、そちらが 4 ということだと思う。 “key_likely”は witness stack には署名だけだが、解く際には”key_likely”もスタックに載せるので合計で 2になる。

max ops は関数コメントが “Return the maximum number of ops needed to satisfy this script non-malleably.”。
Ops 構造体の中身からすると、OPコードのカウントをしているようだ。
“key_likely”の経路がぜろということは OP_CHECKSIG は数に入っていない?

そして type。
最初の 1文字は “Correctness properties” にある B, V, K, W だろう。 次の o, n, d までは載っている。
それ以降の m, u, s, k がわからない。u は Unit かもしれないが後ろ過ぎないか。
ただ Props() には載っている。

プロパティは主にMiniscriptを処理する側の情報なので、深追いしないことにした。

A user and a 2FA service need to sign off, but after 90 days the user alone is enough

# Policy
and(pk(key_user),or(99@pk(key_service),older(12960)))

# Miniscript
and_v(v:pk(key_user),or_d(pk(key_service),older(12960)))

# Bitcoin Script
<key_user> OP_CHECKSIGVERIFY <key_service> OP_CHECKSIG OP_IFDUP OP_NOTIF
  <a032> OP_CHECKSEQUENCEVERIFY
OP_ENDIF

A 3-of-3 that turns into a 2-of-3 after 90 days

# Policy
thresh(3,pk(key_1),pk(key_2),pk(key_3),older(12960))

# Miniscript
thresh(3,pk(key_1),s:pk(key_2),s:pk(key_3),sln:older(12960))

# Bitcoin Script
<key_1> OP_CHECKSIG OP_SWAP <key_2> OP_CHECKSIG OP_ADD OP_SWAP <key_3>
OP_CHECKSIG OP_ADD OP_SWAP OP_IF
  0
OP_ELSE
  <a032> OP_CHECKSEQUENCEVERIFY OP_0NOTEQUAL
OP_ENDIF
OP_ADD 3 OP_EQUAL

The BOLT #3 to_local policy

# Policy
or(pk(key_revocation),and(pk(key_local),older(1008)))

# Miniscript
andor(pk(key_local),older(1008),pk(key_revocation))

# Bitcoin Script
<key_local> OP_CHECKSIG OP_NOTIF
  <key_revocation> OP_CHECKSIG
OP_ELSE
  <f003> OP_CHECKSEQUENCEVERIFY
OP_ENDIF

The BOLT #3 offered HTLC policy

# Policy
or(pk(key_revocation),and(pk(key_remote),or(pk(key_local),hash160(H))))

# Miniscript
t:or_c(pk(key_revocation),and_v(v:pk(key_remote),or_c(pk(key_local),v:hash160(H))))

# Bitcoin Script
<key_revocation> OP_CHECKSIG OP_NOTIF
  <key_remote> OP_CHECKSIGVERIFY <key_local> OP_CHECKSIG OP_NOTIF
    OP_SIZE <20> OP_EQUALVERIFY OP_HASH160 <h> OP_EQUALVERIFY
  OP_ENDIF
OP_ENDIF
1

The BOLT #3 received HTLC policy

# Policy
or(pk(key_revocation),and(pk(key_remote),or(and(pk(key_local),hash160(H)),older(1008))))

# Miniscript
andor(pk(key_remote),or_i(and_v(v:pkh(key_local),hash160(H)),older(1008)),pk(key_revocation))

# Bitcoin Script
<key_remote> OP_CHECKSIG OP_NOTIF
  <key_revocation> OP_CHECKSIG
OP_ELSE
  OP_IF
    OP_DUP OP_HASH160 <HASH160(key_local)> OP_EQUALVERIFY OP_CHECKSIGVERIFY
    OP_SIZE <20> OP_EQUALVERIFY OP_HASH160 <h> OP_EQUAL
  OP_ELSE
    <f003> OP_CHECKSEQUENCEVERIFY
  OP_ENDIF
OP_ENDIF

miniscriptコンパイラの内部動作

その他

P2WSH と TaspScript で大きい違いがあるのは MultiSig の扱いで、TapScript には OP_CHECKMULTISIG のような MultiSig 関係の命令がない。 その代わりに CHECKSIGADD で公開鍵に対して署名チェックが正常だったらインクリメントする命令が追加され、チェックが成功した数を比較するスクリプトを書く。

リンク

writer: hiro99ma
tags: Bitcoin開発  

 < Top page

コメント(Google Formへ飛びます)

GitHub

X/Twitter

Homepage

About me