Develop with pleasure!

福岡でCloudとかBlockchainとか。

Elementsで実装されているOP_CHECKSIGFROMSTACKを使ったCovenants

Scaling Bitcoinのセッションで提案されていた、Bitcoin秘密鍵が例え盗まれても、Bitcoin自体が盗まれないようにするCovenantsを使った金庫についてブログを書いた↓けど、

techmedia-think.hatenablog.com

このアプローチとは別に、Blockstreamが提供するサイドチェーンElements Alphaで提供されているOP_CHECKSIGFROMSTACKというopcodeを使ったCovenantsの例も紹介されてたので見てみる↓

blockstream.com

Bitcoinスクリプトシステムでは、スクリプトが直接トランザクションデータにアクセスすることはできず、OP_CHECKSIGOP_CHECKSIGVERIFYといった操作を介して間接的にアクセスする。これらのopcodeは公開鍵と署名を入力にとる。opcodeが実行されると、トランザクションデータのダブルSHA-256ハッシュを計算し、そのハッシュに対し署名データと公開鍵を使って署名の検証を行う。

Elements AlphaもBitcoinと同様スクリプト言語が直接トランザクションデータにアクセスすることはできないが、Elements Alphaには新しく追加されたOP_CHECKSIGFROMSTACKOP_CHECKSIGFROMSTACKVERIFYというopcodeがある。このopcodeは楕円曲線の公開鍵、メッセージ、署名の3つの入力を取り、実行するとメッセージのSHA-256ハッシュを生成し、デジタル署名と公開鍵を使ってハッシュされたメッセージデータのデジタル署名の検証をする。

Elements Alphaでは、このOP_CHECKSIGFROMSTACKOP_CHECKSIGを組み合わせて使うことでCovenantsを実現している。OP_CHECKSIGの検証の際に使った公開鍵とデジタル署名をそのまま、OP_CHECKSIGFROMSTACKの検証に使用するようscriptPubkeyを組むのがこのトリックの正体になる。どういうことかというと、全く同じ公開鍵とデジタル署名を使ってOP_CHECKSIGOP_CHECKSIGFROMSTACKの検証が両方成功するということは、OP_CHECKSIGの検証時に計算したそのトランザクションデータと同じデータがOP_CHECKSIGFROMSTACKのメッセージとして渡されていることを意味する。

具体的に↑のブログに記載されている例を見ながらCovenantsを構成するスクリプトを見ていく。

Covenantsの構成

以下のスクリプトを構成した場合

  1. OP_OVER OP_SHA256 < pubKey >
  2. 2 OP_PICK 1 OP_CAT OP_OVER
  3. OP_CHECKSIGVERIFY
  4. 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したデータと< signature >について< pubkey >で署名の検証を行う。ここで使っている公開鍵と署名は、3つめのステップの署名検証で使った公開鍵と署名と同一のデータなので、OP_CHECKSIGFROMSTACKVERIFYは< sigTransactionData >が3つめのステップのOP_CHECKSIGVERIFYで検証されたメッセージと同じ場合のみ成功する。

<sigTransactionData>

この時点でスタック上の< sigTransactionData >は、署名されたトランザクションデータの正確なコピーであることが保証されている。トランザクションにフルアクセスすることで、追加のスクリプト操作でCovenantsを強制することができる。出力の数を制限したり、出力の値(量)やスクリプトを制限することができる。

このCovenantにはいくつかの制限がある。Elements Alphaのスクリプトでは、各スタックアイテムは520バイトに制限されているので、作成できるCovenantsはこのサイズの範囲内の< sigTransactionData >でなくてはならない。

再帰的なCovenants

続いて再帰的なCovenantsを構築する例を見ていく。

このCovenantsは、トランザクションに対しセットされる出力は1つだけで、その出力スクリプトは入力スクリプトと同じであるという制約を付与する。

署名されたトランザクションデータ全体をスタックに入れて解析するのではなく、このスクリプトトランザクションデータをピースから組み立てる。スクリプトはCovenantsを強制するために各ピースに条件を課すことができる。

以下は簡単な(と言っても長いけど)CovenantsのscriptPubkey

  1. <0x0100000001>
  2. OP_SWAP OP_SIZE 36 OP_NUMEQUALVERIFY OP_CAT
  3. <0x00> OP_CAT
  4. OP_SWAP OP_SIZE 32 OP_NUMEQUALVERIFY OP_CAT
  5. <0x00005f> OP_CAT
  6. 2 OP_PICK OP_SIZE 95 OP_NUMEQUALVERIFY OP_CAT
  7. <0xffffffff0100> OP_CAT
  8. OP_SWAP OP_SIZE 32 OP_NUMEQUALVERIFY OP_CAT
  9. <0x0000> OP_HASH256 OP_CAT
  10. <0x17a914> OP_CAT
  11. OP_SWAP OP_HASH160 OP_CAT
  12. <0x870000000001000000> OP_CAT
  13. OP_SHA256
  14. 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
  15. OP_DUP
  16. 2 OP_ROLL 3 OP_PICK
  17. OP_CHECKSIGFROMSTACKVERIFY
  18. 1 OP_CAT OP_SWAP
  19. 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_CHECKSIGFROMSTACKOP_CHECKSIGの検証を強制させることで、これから作るトランザクションの形式を強制させるというアプローチはおもしろい。
  • ただ、そのトランザクションデータの組み立てをOP_CATOP_SWAPなどを使って1つ1つ組み立てていくというのはスマートじゃないので別の仕組みがあった方がいい。
  • トランザクションデータの組み立ての煩雑さなどを考慮するとMöser-Eyal-SirerのOP_CHECKOUTPUTVERIFYを使ったアプローチの方がシンプルで良いと思う。
  • ただ、スタック上のデータの署名検証をするOP_CHECKSIGFROMSTACK自体はCovenants以外にもいろいろ利用できそう。