少し前に、BitcoinでVaultを実現するために2つのopcodeを導入するソフトフォークの提案がBIP-345として登録された↓
https://github.com/bitcoin/bips/blob/master/bip-0345.mediawiki
Vaultとは?
Vault(金庫)は、その名前から分かるようにビットコインを安全に保管するための仕組み。
Bitcoinのような暗号通貨の場合、秘密鍵が漏洩/盗難にあうと、それは資金の損失につながる。金額が大きくなるほど、秘密鍵漏洩のリスクが大きくなる。自分が知らない間に盗まれた秘密鍵によって資金が勝手に使われることがないように、より堅牢な資金保護の仕組みを提供するのがVaultの目的。
flowchart LR A[UTXOをVaultへデポジット] -.-> C[保管されたコインを事前に指定したリカバリーパスに移動] A ---> B[引き出しをトリガー] B -.-> C B --タイムロック期限後--> D[引き出しの完了]
具体的には、資金を以下の機能を持つVault(スクリプト)に預ける:
つまり、攻撃者に秘密鍵を奪われ、攻撃者がその資金を引き出そうとしても、そこには遅延時間が設けられ、それに気づいた所有者がその間に資金を取り戻せるようにしようというもの。
このようなVaultの提案は、
- 2016年に提案されたCovenantsのユースケースの1つとして紹介され(ブログ記事)
- 同じCovenantsの文脈でElementsの
OP_CHECKSIGFROMSTACKとOP_CATで実現する方法(ブログ記事)や*2、 - 現在のコンセンサスルールでも実現可能な方法として、使い捨ての一時鍵で事前署名する方法(GBEC解説動画)*3
などがある。
BIP-345 Vault
BIP-345の提案はこれまでの汎用的なCovenantsを使用したVaultの構成ではなく、Vault専用の2つの新しいopcodeをソフトフォークで導入し遅延時間とリカバリーを可能にする特殊なCovenantsを可能にしてVaultを構成できるようにしようというもの。2つの新しいopcodeは、いずれも、Tapscriptで予約されているOP_SUCCESS系のopcodeを再定義することで導入される(witness version 1=Taprootを使用した構成になる)。
上記のVaultの操作をサポートするのに、以下の4種類のトランザクションが登場する:
- Vaultトランザクション
資金をVaultにデポジットするトランザクション - トリガートランザクション
資金をVaultから引き出すためのトリガーとなるトランザクション - 引き出し(Withdraw)トランザクション
トリガーされた引き出しを完了するためのトランザクション - リカバリートランザクション
引き出しが完了する前に資金を事前に指定したリカバリーパスに送信するトランザクション
Vaultへのデポジット
VaultトランザクションでVaultに資金をデポジットするには、少なくとも2つのリーフ(トリガー用、リカバリー用)を持つTaptreeを使って以下のようなTaprootのscriptPubkeyを構成する。
graph BT A[トリガーリーフ] --> B[Taptree] C[リカバリーリーフ] --> B D[Internal key] --> E[Vault P2TR] B --> E
このP2TRアドレスに送られたコインを使用する方法は、各リーフを使ってアンロックする方法と、内部鍵(Internal key)を使ってSchnorr署名を作成する方法*4の3通り。2つのリーフのスクリプトのは以下のような構成になる:
トリガーリーフ
トリガーリーフは新しく導入されるOP_VAULT opcodeを使った以下のようなスクリプトになる。OP_VAULTは、OP_SUCCESS187(0xbc)を再定義する形で導入される。
[trigger-auth] <遅延時間> 2 <leaf-update-script-body> OP_VAULT
trigger-authの部分は、この条件を使ってVaultからの引き出しをトリガーするための認可条件で、ウォレットの設計者が任意の条件を設定できる。例えばある公開鍵の秘密鍵を持つものに対して認可する場合は、trigger-authは以下のスクリプトになる。
<trigger-auth-pubkey> OP_CHECKSIGVERIFY
つまり、公開鍵trigger-auth-pubkey対して有効な署名を作れる人のみがこれをトリガーできる。
残りの部分は、OP_VAULTによって評価されるものなので、OP_VAULTの挙動と合わせてみていく。
OP_VAULTはスタックに以下の項目がある前提で実行される:
<leaf-update-script-body> <push-count> [ <push-count> leaf-update script 用のデータ項目の数 ] <trigger-vout-idx> <revault-vout-idx> <revault-amount>
leaf-update-script-bodyは、OP_VAULTによって更新されるリーフスクリプトの断片。サンプルとして以下のスクリプトが掲載されている。
OP_CHECKSEQUENCEVERIFY OP_DROP OP_CHECKTEMPLATEVERIFY
OP_CHECKTEMPLATEVERIFYは未導入なので、導入されていない場合は以下のようなスクリプトになる。
OP_CHECKSEQUENCEVERIFY OP_DROP OP_CHECKSIG
いずれにせよ、相対的なタイムロック(OP_CSV)が付与され、これが引き出しのトリガーに対して遅延時間を設定する。
OP_VAULTの動作は↓
- スタックから
leaf-update-script-bodyをポップし、 - スタックから
push-countをポップする。 push-count個分の要素をスタックからポップする。- 3の要素をプレフィックスとして1のスクリプトと結合し、
leaf-update-scriptを完成させる。 - スタックから、
trigger-vout-idx、revault-vout-idx、revault-amountをポップする。 - スクリプトのsigopsバジェットを60デクリメント*5
trigger-vout-idxがwitness v1 program(Taproot)かチェック- 現在評価中のリーフスクリプトを4の
leaf-update-scriptと置き換えてTaptreeを更新し、そから導出したscriptPubkey(P2TR)と、triggerOutのscriptPubkeyが合致するかチェック revaultOutのscriptPubkeyが現在評価中のscriptPubkey(P2TR)と合致するかチェックtriggerOutの金額とrevaultOutの金額が現在評価中のインプットの金額以上であることをチェック- インプットの金額から
revaultOutの金額を差し引いた金額がtriggerOutの金額になっていることをチェック - すべての検証をパスしたらスタックにTrueをプッシュ
というもの。つまり、OP_VAULTは、
- Taptree内のトリガーリーフのスクリプトを、
leaf-update-script-bodyとpush-count個分のデータで構成されるスクリプトに更新した新しいP2TRスクリプトがトランザクションアウトプットに作られるように強制する。 - Vaultから資金を引き出す際に、全額引き出すのでなく、一部だけ引き出せるように、残しておく資金は元のVaultと同じ宛先に移動するRevault機能をサポートしている。
- 全額引き出す場合は
revault-vout-idxに-1を設定し、revault-amountは0
- 全額引き出す場合は
トリガートランザクション
先程のトリガーリーフのスクリプト↓
[trigger-auth] <遅延時間> 2 <leaf-update-script-body> OP_VAULT
について、leaf-update-script-bodyをOP_CHECKSEQUENCEVERIFY OP_DROP OP_CHECKTEMPLATEVERIFYとした場合、トリガートランザクションでこの条件をアンロックするためにwitnessとして提供される項目をスタックに入れると↓
<Taprootのcontrol block(トリガーリーフへのマークルパスを含む)> <トリガーリーフスクリプトのペイロード> <trigger-auth-pubkey-signature> <target-CTV-hash> <trigger-vout-idx> <revault-vout-idx> (Revaultしない場合は-1) <revault-amount>
となり、このスタックに対してTaprootのscript-pathの検証が行われ、トリガーリーフの認可条件(trigger-auth)が実行され、その後OP_VAULTを実行する際のスタックの状態は↓
<leaf-update-script-body> 2 <遅延時間> <target-CTV-hash> <trigger-vout-idx> <revault-vout-idx> <revault-amount>
ここでは、push-count = 2であるため、遅延時間とtarget-CTV-hashがスタックからポップされ、それをleaf-update-script-bodyと結合したスクリプトが更新用のスクリプトになり、triggerOutのP2TRの構成要素になる。
leaf-update-script-bodyがOP_CHECKSEQUENCEVERIFY OP_DROP OP_CHECKSIGの場合は、target-CTV-hashの代わりに公開鍵をwitnessで提供する形になると思われる。
トリガートランザクションでは、リカバリーリーフと、↑で更新された遅延付きスクリプト(leaf-update-script)で構成されるP2TR宛にVaultの資金が移動されるので、遅延期間が終われば、leaf-update-scriptを満たせす引き出し用のトランザクションを完成させ、資金の引き出しが完了する。
target-CTV-hashや、Revaultの情報はトランザクションのwitnessで提供されるため、Vaultから資金を引き出す際に、引き出し先や金額を決めればいいようになっている。これまでのCovenantsではこれらはVaultにデポジットする段階で決めておく必要があった。
リカバリーリーフ
リカバリー用のリカバリーリーフは新しく導入されるOP_VAULT_RECOVER opcodeを使った以下のようなスクリプトになる。OP_VAULT_RECOVERは、OP_SUCCESS188(0xbd)を再定義する形で導入される。
[recovery auth] <recovery-sPK-hash> OP_VAULT_RECOVER
recovery authはオプションで、この条件でリカバリーをするための認可条件で、trigger-authと同様ウォレットの設計者が任意の条件を設定できる。
OP_VAULT_RECOVERはスタックに以下の項目がある前提で実行される:
<recovery-sPK-hash> <recovery-vout-idx>
recovery-sPK-hashは元々リーフスクリプト内にあるデータで、32バイトのハッシュ値。
OP_VAULT_RECOVERの動作は↓
- スタックから
recovery-sPK-hashをポップし、32バイトかチェック - スタックから
recovery-vout-idxをポップする。- トランザクション内の該当するインデックスのアウトプットを
recoverOutと呼ぶ。
- トランザクション内の該当するインデックスのアウトプットを
recoverOutのscriptPubkeyについてタグ付きハッシュtagged_hash("VaultRecoverySPK", recoveryOut.scriptPubKey)を計算し、その値がrecovery-sPK-hashと一致するかチェックrecoverOutの金額がインプットの金額以上であるかをチェック- すべての検証をパスしたらスタックにTrueをプッシュ
というもの。つまり、OP_VAULT_RECOVERは、リーフスクリプトでコミットされている宛先(recovery-sPK)にVaultのコインが全額送られることを強制する。
RBFのシグナリング
また、コンセンサスルールではないけどBitcoin Coreのポリシーとして、Pinning攻撃を防ぐため、リカバリーリーフを使用するリカバリートランザクションは、RBFによるトランザクションの置換可能性をシグナリングする必要がある。
具体的には、リカバリートランザクションのnVersionが3ではない場合*6、OP_VAULT_RECOVERインプットのnSequenceは0xffffffff - 1未満でなければならない。
OP_CTVとの関係
↑のトリガーリーフ内のleaf-update-script-bodyでは、BIP-119のOP_CHECKTEMPLATEVERIFY(Covenants)を使って、引き出し先を指定する方法が推奨されている。また、このソフトフォークの展開も、BIP-119と同時であることが望ましいとされている。
BIP-345は厳密にBIP-119に依存しているわけではないものの、OP_VAULTによる指定されたTapleafの更新とOP_VAULT_RECOVER自体には、遅延時間の設定や送付先のアドレスの強制は機能として含まれない(外だしされている)ので、OP_CSVによる遅延やOP_CTVによって最終的な宛先を強制する仕組みとの組み合わせが必要になる。
ただ、トリガートランザクションで指定するtarget-CTV-hashで、引き出し先をOP_CTVでコミットしたいケースってどんなケースなんだろう?
*1:よりセキュアに管理さているオフラインの鍵を利用するなど
*2:安全な一時鍵の削除や、金額と引き出しのパターンに事前コミットする必要があるなど、いくつかの制約がある
*3:金額、宛先手数料の管理はすべて事前に決めたものになる
*4:内部鍵はリカバリーパスで使用する鍵と同等のセキュリティが必要になる。内部鍵によりこのP2TRがVaultであったことが開示されなくなるというメリットはあるもの、セキュリティ上の懸念がある場合は内部鍵を使用不可能なNUMSポイントにするのも可。
*5:8のtrrigerOutの有効性チェックの際に、TaprootのControl blockの長さに比例する楕円曲線のスカラー乗算とハッシュ計算の実行が必要になるため、そのコストをsigopsのカウントに加味する