Scaling Bitcoinのセッションで提案されていた、Bitcoinの秘密鍵が例え盗まれても、Bitcoin自体が盗まれないようにするCovenantsを使った金庫についてブログを書いた↓けど、
techmedia-think.hatenablog.com
このアプローチとは別に、Blockstreamが提供するサイドチェーンElements Alphaで提供されているOP_CHECKSIGFROMSTACK
というopcodeを使ったCovenantsの例も紹介されてたので見てみる↓
https://blockstream.com/2016/11/02/covenants-in-elements-alpha.htmlblockstream.com
Bitcoinのスクリプトシステムでは、スクリプトが直接トランザクションデータにアクセスすることはできず、OP_CHECKSIG
やOP_CHECKSIGVERIFY
といった操作を介して間接的にアクセスする。これらのopcodeは公開鍵と署名を入力にとる。opcodeが実行されると、トランザクションデータのダブルSHA-256ハッシュを計算し、そのハッシュに対し署名データと公開鍵を使って署名の検証を行う。
Elements AlphaもBitcoinと同様スクリプト言語が直接トランザクションデータにアクセスすることはできないが、Elements Alphaには新しく追加されたOP_CHECKSIGFROMSTACK
とOP_CHECKSIGFROMSTACKVERIFY
というopcodeがある。このopcodeは楕円曲線の公開鍵、メッセージ、署名の3つの入力を取り、実行するとメッセージのSHA-256ハッシュを生成し、デジタル署名と公開鍵を使ってハッシュされたメッセージデータのデジタル署名の検証をする。
Elements Alphaでは、このOP_CHECKSIGFROMSTACK
とOP_CHECKSIG
を組み合わせて使うことでCovenantsを実現している。OP_CHECKSIG
の検証の際に使った公開鍵とデジタル署名をそのまま、OP_CHECKSIGFROMSTACK
の検証に使用するようscriptPubkeyを組むのがこのトリックの正体になる。どういうことかというと、全く同じ公開鍵とデジタル署名を使ってOP_CHECKSIG
とOP_CHECKSIGFROMSTACK
の検証が両方成功するということは、OP_CHECKSIG
の検証時に計算したそのトランザクションデータと同じデータがOP_CHECKSIGFROMSTACK
のメッセージとして渡されていることを意味する。
具体的に↑のブログに記載されている例を見ながらCovenantsを構成するスクリプトを見ていく。
Covenantsの構成
以下のスクリプトを構成した場合
- OP_OVER OP_SHA256 < pubKey >
- 2 OP_PICK 1 OP_CAT OP_OVER
- OP_CHECKSIGVERIFY
- OP_CHECKSIGFROMSTACKVERIFY
スタックに以下の2つのアイテムがあると仮定して、
<signature> <sigTransactionData>
OP_OVER
はスタックの上から2つめのアイテムをコピーしてスタックの一番上にプッシュするopcode。
最初のステップが実行されると、< sigTransactionData >のSHA-256ハッシュと公開鍵がスタックにプッシュされて、スタックは以下のようになる。
<pubKey> <sha256(sigTransactionData)> <signature> <sigTransactionData>
OP_PICK
ははスタック内の上からn番目のアイテムをコピーしてスタックの一番上にプッシュするopcode。OP_CAT
は2つの文字列を結合するopcode(ただしBitcoinのプロトコルでは無効になっている)。
2番目のステップが実行されると、OP_PICK
で< signature >がコピーされスタックにプッシュされ、それにOP_CATで0x01
が付与される。その後スタックの2番目となった公開鍵がコピーされスタックの一番上にプッシュされ、スタックは以下のようになる。
署名に付与された0x01
はSIGHASH_ALLで、署名されたトランザクションデータには全ての入力と出力が含まれていることを意味する。
<pubKey> <signature;SIGHASH_ALL> <pubKey> <sha256(sigTransactionData)> <signature> <sigTransactionData>
3番目のステップのOP_CHECKSIGVERIFY
により、スタックの上から2つの公開鍵と署名のデータが正しい秘密鍵で署名された署名であるか検証する。検証が成功するとスタックは以下のようになる。
<pubKey> <sha256(sigTransactionData)> <signature> <sigTransactionData>
4案目のステップのOP_CHECKSIGFROMSTACKVERIFY
は、スタックから< pubkey >と< sha256(sigTransactionData) >、< signature >を取り出し、< sha256(sigTransactionData) >に対してSHA-256ハッシュを生成する。そうやって出来たダブルSHA-256したOP_CHECKSIGFROMSTACKVERIFY
は< sigTransactionData >が3つめのステップのOP_CHECKSIGVERIFY
で検証されたメッセージと同じ場合のみ成功する。
<sigTransactionData>
この時点でスタック上の< sigTransactionData >は、署名されたトランザクションデータの正確なコピーであることが保証されている。トランザクションにフルアクセスすることで、追加のスクリプト操作でCovenantsを強制することができる。出力の数を制限したり、出力の値(量)やスクリプトを制限することができる。
このCovenantにはいくつかの制限がある。Elements Alphaのスクリプトでは、各スタックアイテムは520バイトに制限されているので、作成できるCovenantsはこのサイズの範囲内の< sigTransactionData >でなくてはならない。
再帰的なCovenants
続いて再帰的なCovenantsを構築する例を見ていく。
このCovenantsは、トランザクションに対しセットされる出力は1つだけで、その出力スクリプトは入力スクリプトと同じであるという制約を付与する。
署名されたトランザクションデータ全体をスタックに入れて解析するのではなく、このスクリプトはトランザクションデータをピースから組み立てる。スクリプトはCovenantsを強制するために各ピースに条件を課すことができる。
以下は簡単な(と言っても長いけど)CovenantsのscriptPubkey
- <0x0100000001>
- OP_SWAP OP_SIZE 36 OP_NUMEQUALVERIFY OP_CAT
- <0x00> OP_CAT
- OP_SWAP OP_SIZE 32 OP_NUMEQUALVERIFY OP_CAT
- <0x00005f> OP_CAT
- 2 OP_PICK OP_SIZE 95 OP_NUMEQUALVERIFY OP_CAT
- <0xffffffff0100> OP_CAT
- OP_SWAP OP_SIZE 32 OP_NUMEQUALVERIFY OP_CAT
- <0x0000> OP_HASH256 OP_CAT
- <0x17a914> OP_CAT
- OP_SWAP OP_HASH160 OP_CAT
- <0x870000000001000000> OP_CAT
- OP_SHA256
- 1 OP_DUP OP_CAT OP_DUP OP_CAT OP_DUP OP_CAT OP_DUP OP_CAT 1. OP_DUP OP_CAT OP_DUP OP_CAT
- OP_DUP
- 2 OP_ROLL 3 OP_PICK
- OP_CHECKSIGFROMSTACKVERIFY
- 1 OP_CAT OP_SWAP
- OP_CHECKSIG
このscriptPubkeyのロックを解除するscriptSigは↓
<recoveredPubKey> <script> <valueOut> <valueIn> <outPoint>
で、スタックをこの逆順で初期化すると
<outPoint> <valueIn> <valueOut> <script> <recoveredPubKey>
scriptPubkeyの最初の数ステップでは、scriptSigで提供される署名されたトランザクションデータの入力を再構築する。スクリプトはトランザクションデータの各ピースが正しいサイズか検証する。
ステップ12までスクリプトを評価したら、スタックは↓のようになる。
<0x0100000001;outPoint;00;valueIn;00005f;script;ffffffff0100;valueOut;hash(0x0000);17a914;hash160(script);870000000001000000> <recoveredPubKey>
スタックの一番上の値は、1つの入力と1つの出力を持つElements Alphaの署名済みトランザクションデータである。Bitcoinの署名済みトランザクションデータとフォーマットは少し異なる。署名されたトランザクションデータに、入力と出力の両方が与えられていることに注意する。
< hash(0x0000) >のデータは、Elements Alphaの秘匿トランザクションシステムの一部である。
<0x17a914> はP2SHトランザクションのプレフィックスであることが分かる。そのため指定する出力はP2SHアドレスでなければならない。
< script >が入力と出力両方にあるのが分かる。入力のscriptPubKeyは署名されたトランザクションデータのscriptSigを置き換える。 同じ< script > が入力と出力にあるので、これは入力のscriptPubkeyと同じscriptPubkeyに資金を移動することを意味する。結果、このCovenantsの制約が次のUTXOにも付与されることになり、再帰的なCovenantsになる。
ステップ13のOP_SHA256
は、このトランザクションデータをSHA-256で1回ハッシュする。
<sha256(0x0100000001;outpoint;00;valueIn;00005f;script;ffffffff0100;valueOut;hash(0x0000);17a914;hash160(script);870000000001000000)> <recoveredPubKey>
ステップ14では、<0x01...01>をスタックにプッシュする。この値は0x01を64個くっつけた値になる。Elements Alphaでは64バイトのシュノア署名が使われるため、<0x01...01>は固定したデジタル署名として使われる。
署名がスクリプトの入力として提供される代わりに、楕円曲線の公開鍵が入力として提供される。ステップ15のOP_DUP
は、この署名を複製しスタックは以下のようになる。
<fixedSignature> <fixedSignature> <sha256(0x0100000001;outpoint;00;valueIn;00005f;script;ffffffff0100;valueOut;hash(0x0000);17a914;hash160(script);870000000001000000)> <recoveredPubKey>
ここで、OP_CHECKSIGFROMSTACK
を fixed signatureとトランザクションデータと公開鍵を使ってcovenantsを実行していく。まずステップ16で、スタック上に以下のように適切なアイテムを配置する。
<recoveredPubKey> <sha256(0x0100000001;outpoint;00;valueIn;00005f;script;ffffffff0100;valueOut;hash(0x0000);17a914;hash160(script);870000000001000000)> <fixedSignature> <fixedSignature> <recoveredPubKey>
ステップ17でOP_CHECKSIGFROMSTACKVERIFY
がコールされると、OP_CHECKSIGFROMSTACKVERIFY
が成功する< recoveredPubKey >の値が、fixed signatureとメッセージから、楕円曲線の公開鍵を使って計算できる。
<fixedSignature> <recoveredPubKey>
ステップ18でSIGHASH_ALL
のコードである0x01
を署名に付与し、正しい順でスタックにプッシュする。
<recoveredPubKey> <fixedSignature;SIGHASH_ALL>
最後にステップ19のOP_CHECKSIG
で、スタック上に作成したトランザクションが実際のトランザクションデータと一致することを検証する。これでCovenantsは完璧になる。
↑のブログにはその他にMöser-Eyal-SirerのVaultをElements Alphaで実装するとどうなるかという説明がある。
実装
OP_CHECKSIGFROMSTACK
の実装は↓で確認できる。
https://github.com/ElementsProject/elements/blob/alpha/src/script/interpreter.cpp#L1217-L1250
所感
- 同じ公開鍵と署名を使って
OP_CHECKSIGFROMSTACK
とOP_CHECKSIG
の検証を強制させることで、これから作るトランザクションの形式を強制させるというアプローチはおもしろい。 - ただ、そのトランザクションデータの組み立てを
OP_CAT
やOP_SWAP
などを使って1つ1つ組み立てていくというのはスマートじゃないので別の仕組みがあった方がいい。 - トランザクションデータの組み立ての煩雑さなどを考慮するとMöser-Eyal-Sirerの
OP_CHECKOUTPUTVERIFY
を使ったアプローチの方がシンプルで良いと思う。 - ただ、スタック上のデータの署名検証をする
OP_CHECKSIGFROMSTACK
自体はCovenants以外にもいろいろ利用できそう。