Develop with pleasure!

福岡でCloudとかBlockchainとか。

Bitcoin Core 0.14.1のリリース

Bitcoin Core 0.14.1のマイナーアップデートがリリースされてたので主な変更内容を見ておく。

bitcoin/release-notes-0.14.1.md at master · bitcoin/bitcoin · GitHub

RPCの変更

  • createrawtransactionの最初の引数がtransactionsからinputsにリネームされた。
  • disconnectnodeの引数がnodeからaddressにリネームされた。

0.14.0からRPCコールする際に名前付き引数をサポートしているため、↑のインターフェースの変更はその点について0.14.0との互換性は無い。もし名前付き引数を使ってRPCを呼んでいるクライアントがあれば、更新する必要がある。

マイニング

以前のバージョンでは、getblocktemplateはネットワーク上でSegwitがアクティベートされたら、そのテンプレートを使用するクライアントやマイナーもSegwitをサポートする必要があった。0.14.1では、返されたブロックテンプレートから全てのSegwitトランザクションを削除することで、ネットワーク上でSegwitがアクティベートされた後もSegwitに対応していないクライアントをサポートするようになった。これによりSegwitがアクティベートされた後でも、非Segwitのマイナーは、非Segwitのトランザクションのみをマイニングし続けることができるようになる。

以前のバージョンまでの↑の制約により、getblocktemplateは非SegwitのクライアントにはSegwitのversion-bitをシグナリングをしないよう推奨してきた。今回のアップデートで、この必要は無くなり、getblocktemplateは、全てのマイナーにsegwitのシグナリングを推奨するようになっている。安全なアクティベーションにとって唯一必要な基準は、実際にSegwit対応のブロックを生成することではなく、ルールを実行する能力であるため、これは安全な取り組みである。

UTXOメモリの計算

Bitcoin CoreはUTXOのデータにより速くアクセスできるようUTXOをメモリ上にキャッシュしている。このUTXOキャッシュのフラッシュ時にメモリ使用量がピークになっても-dbcacheで設定された制限値を順守できるよう、UTXOキャッシュのメモリ使用量がより正確に計算されるようになった。(以前のリリースバージョンでは、実際のピーク使用率の半分しか使われていないと見積もられている。)

今回のリリースででデフォルトの-dbcacheのサイズも300MiBから450MiBに変更されている。現在-dbcacheを高い値に設定しているユーザーは、以前のリリースと同じキャッシュパフォーマンスを実現するためには、この設定値の増加を検討する必要がある。逆に低メモリシステム(1GB以下)のユーザーは、このパラメータの値を小さくすることを検討する必要がある。

低メモリシステムでの実行に関する追加情報は、reducing-bitcoind-memory-usage.mdを参照。

この変更に伴うプルリクは↓あたり

github.com

github.com

署名に厳密なDERエンコーディングを適用したBIP-066

techmedia-think.hatenablog.com

でECDSAの署名データの改竄に遭遇したのもあり、関連して現在のBitcoinプロトコルに適用されている署名のエンコーディングルールについて定義したBIP-066の内容もおさえておく。

bips/bip-0066.mediawiki at master · bitcoin/bips · GitHub

動機

Bitcoinの参照実装では、署名検証をOpenSSLに依存しており(現在は独自実装したlibsecp256k1)、これがBitcoinのブロックの有効性を検証する暗黙的なルールになっている。しかし残念ながらOpenSSLはコンセンサスクリティカルな動作をするよう設計されておらず、その変更がBitcoinのソフトウェアに影響を及ぼす可能性がある。

特に重要な機能の1つが署名のエンコードである。最近までOpenSSLはDER標準から外れた署名であっても有効な署名として受け入れてきた。この仕様がOpenSSL 1.0.0p と 1.0.1kで変更されると、いくつかのノードがチェーンをリジェクトするようになった。

このBIPでは、コンセンサスルールがOpenSSLの署名解析に依存しないよう、DERが要求するものを正確に制限することを提案している。コンセンサスコードの実装から全てのOpenSSLを削除する場合、この変更が必要になる。

仕様

OP_CHECKSIGOP_CHECKSIGVERIFYOP_CHECKMULTISIGOP_CHECKMULTISIGVERIFYに渡される全ての署名は、ECDSAの検証がされる。これらの署名には後述する厳密なDERエンコーディングエンコードされなければならない。

上記opcodeは全て、公開鍵/署名のペアに対してECDSAの検証を実行する。この検証では、署名が以下のIsValidSignatureEncodingチェックをパスしないと、そのスクリプトは即falseと評価される。署名は有効なDERだけど、ECDSAの検証でエラーになった場合、opcodeはこれまでと同様opcodeの実行を停止しスタックにfalseをプッシュする(ただしこの場合スクリプトはすぐにエラーにはならない)。

DERエンコーディング仕様

以下のコードが厳密なDERチェックの内容になる。この関数は1バイトのsighashフラグを含む署名のbyte vectorをテストする(sighashフラグはDERの仕様外でこの提案の影響は受けない)。この関数は署名の長さが0の場合は呼び出されない。

bool static IsValidSignatureEncoding(const std::vector<unsigned char> &sig) {
    // フォーマット: 0x30 [total-length] 0x02 [R-length] [R] 0x02 [S-length] [S] [sighash]
    // * total-length: sighashを除くこの後に続くデータのデータ長を示す1バイトのデータ
    // * R-length: Rの長さを示す1バイトのデータ
    // * R: 任意の長さでビッグエンディアンでエンコードされたRの値。正の整数の最小エンコーディングになる。(次のバイト値が最上位bitでない限りnullバイトで始まってはならない。)
    // * S-length: Sの長さを示す1バイトのデータ
    // * S: 任意の長さでビッグエンディアンでエンコードされたRの値。(次のバイト値が最上位bitでない限りnullバイトで始まってはならない。)
    // * sighash: sihash typeを示す1バイトのデータ

    // データ長の最小・最大値チェック
    if (sig.size() < 9) return false;
    if (sig.size() > 73) return false;

    // A signature is of type 0x30 (compound).
    if (sig[0] != 0x30) return false;

    // 署名のトータルサイズを検証
    if (sig[1] != sig.size() - 3) return false;

    // Rの値の長さを取得
    unsigned int lenR = sig[3];

    // 残りの署名データにSの値が格納されている余地があるか検証
    if (5 + lenR >= sig.size()) return false;

    // Sの値の長さを取得
    unsigned int lenS = sig[5 + lenR];

    // 各要素の長さの合計が署名のデータサイズと一致するか検証
    if ((size_t)(lenR + lenS + 7) != sig.size()) return false;
 
    // Rの値がintegerか検証
    if (sig[2] != 0x02) return false;

    // Rの値の長さが0の場合はエラー
    if (lenR == 0) return false;

    // Rの値がマイナス値の場合はエラー
    if (sig[4] & 0x80) return false;

    // Rがマイナスの値として解釈されない限り、Rの先頭がnull byteは許可されない。
    if (lenR > 1 && (sig[4] == 0x00) && !(sig[5] & 0x80)) return false;

    // Sの値がintegerか検証
    if (sig[lenR + 4] != 0x02) return false;

    // Sの値の長さが0の場合はエラー
    if (lenS == 0) return false;

    //  Sの値がマイナス値の場合はエラー
    if (sig[lenR + 6] & 0x80) return false;

    // Sがマイナスの値として解釈されない限り、Sの先頭がnull byteは許可されない。
    if (lenS > 1 && (sig[lenR + 6] == 0x00) && !(sig[lenR + 7] & 0x80)) return false;

    return true;
}

表記:P1とP2はシリアライズされた公開鍵で、S1及びS2はP1とP2に対する有効な署名。S1' と S2' は非DERだがP1とP2の有効な署名。FはDER準拠の無効な署名で、F'はDERに準拠しない無効な署名。

  1. S1' P1 CHECKSIG fails (changed)
  2. S1' P1 CHECKSIG NOT fails (unchanged)
  3. F P1 CHECKSIG fails (unchanged)
  4. F P1 CHECKSIG NOT can succeed (unchanged)
  5. F' P1 CHECKSIG fails (unchanged)
  6. F' P1 CHECKSIG NOT fails (changed)
  7. 0 S1' S2 2 P1 P2 2 CHECKMULTISIG fails (changed)
  8. 0 S1' S2 2 P1 P2 2 CHECKMULTISIG NOT fails (unchanged)
  9. 0 F S2' 2 P1 P2 2 CHECKMULTISIG fails (unchanged)
  10. 0 F S2' 2 P1 P2 2 CHECKMULTISIG NOT fails (changed)
  11. 0 S1' F 2 P1 P2 2 CHECKMULTISIG fails (unchanged)
  12. 0 S1' F 2 P1 P2 2 CHECKMULTISIG NOT can succeed (unchanged)

デプロイメント

BIP-034と同じ2つの閾値(詳細はBIP-034参照)を使ったデプロイを利用する。ただし、ブロックのnVersionに3をセットする。1000ブロック中750ブロックのnVersionが3になるとこのルールは有効になる。さらに1000ブロック中950ブロックのnVersionが3になるとnVersionに2をセットしたブロックは無効になり、全てのブロックがこの新しいルールを採用する。

実際にBitcoin Core 0.10.0(2015年2月)で導入され、2015年7月時点で完全に有効になったみたい。

互換性

DERに厳密に準拠した署名を作成する本要件は、参照クライアントのv0.8.0以降でリレーポリシーとして実装され、違反するトランザクションは2015年1月時点でチェーンに追加されていない。さらに、準拠していない署名は準拠した署名に変換できるため、この要件によって機能が失われることはない。またこの提案にはトランザクションのmalleabilityを軽減するというメリットもある(詳細はBIP-062参照)。

実装

参照クライアントの実装↓

github.com

所感

署名の検証ルールがOpenSSL依存というのは確かに危ないので、明示的に検証ルールを定義するのは大事。

BIPとして登録されたHTLC(BIP-199)

HTLCの仕様がBIPとして登録されてたので見てみる↓

bips/bip-0199.mediawiki at master · bitcoin/bips · GitHub

概要

Hashed Time-Locked Contract (HTLC) は、あるユーザー間(売り手と買い手)の決済を行うコントラクトで、売り手がハッシュのプリイメージを公開することで資金を入手することを可能にし、買い手が指定期間内に売り手がプリイメージを公開せずタイムアウトになった際に、資金を取り戻すことを可能にするスクリプトである。

このスクリプトは以下のような形式になる

OP_IF
    [HASHOP] <digest> OP_EQUALVERIFY OP_DUP OP_HASH160 <seller pubkey hash>            
OP_ELSE
    <num> [TIMEOUTOP] OP_DROP OP_DUP OP_HASH160 <buyer pubkey hash>
OP_ENDIF
OP_EQUALVERIFY
OP_CHECKSIG

[HASHOP]にセットするopcodeは、OP_SHA256OP_HASH160のどちらか。

[TIMEOUTOP]にセットするopcodeは、OP_CHECKSEQUENCEVERIFYOP_CHECKLOCKTIMEVERIFYのどちらか。

HTLCの流れ

  • ビクター(買い手)とペギー(売り手)は、お互いに公開鍵を交換し、タイムアウト閾値に合意する。続いてペギーはビクターにハッシュダイジェストを提供する。この段階で、両者はHTLCのスクリプトを構築し、そのP2SHアドレスを生成することができる。
  • ビクターはそのP2SHアドレスに資金を送る。
  • ビクターが送った資金は以下のいずれかで入手できる
    • べギーはビクターが送った資金のUTXOについて、自分が生成したハッシュのプリイメージを記載したトランザクションを作ることで資金を入手する(↑のOP_IFブランチの条件)
    • ビクターはタイムアウト時間を過ぎたので、資金を回収する(↑のOP_ELSEブランチの条件)

ビクターは、べギーがプリイメージを公開しない場合、資金がロックされている時間を減らすためタイムアウトを短く設定したい。逆にペギーの方は、タイムアウト閾値によって資金の入手できないといったリスクを減らすためタイムアウトを長く設定したい。

動機

多くのオフチェーンプロトコルでは、シークレットの開示が決済メカニズムの一部として使われている。HTLCのトランザクションは、回収前であればシークレットを公開することで確実に資金を入手でき、非協力的な取引相手から資金を回収する仕組みと合わせて、ブロックチェーンを介してシークレットとお金を交換する安全で安価な方法である。

Lightning Network

Lightning Networkでは、HTLCスクリプトを使ってペイメントチャネル間のアトミックスワップを実現している。

アリスはシークレットKを生成し、そのハッシュLを生成する。アリスはそのLのプリイメージ対するHTLCの支払いをボブに送る。ボブは同じプリイメージと金額へのHTLC支払いをキャロルに送る。アリスがプリイメージKをリリースした場合のみ価値の交換が発生する。シークレットは各ホップ毎に明らかになるので、全ての当事者の取引が補償される。いずれかの当事者が取引に非協力的になった場合は、払い戻し条件によって処理を中止することができる。

この辺の仕組みの話は↓の方が分かりやすいと思う

techmedia-think.hatenablog.com

ゼロ知識の条件付き支払い

ハッシュのプリイメージが期待する情報を導出することを保証するために使用されるさまざまな実験的なゼロ知識証明システムが存在する。一例として、ゼロ知識証明が、ハッシュのプリイメージが暗号化された数独パズルの解の復号鍵として機能することを証明するために利用されていたりする。(プロトコルの具体例は、pay-to-sudokuを参照)

HTLCトランザクションは、そのような復号鍵をリスク無しにお金と交換することができ、ゼロ知識証明で必要となる大規模で高価な検証トランザクションを必要としない。

実装

Bitcoin CoreでこのHTLCをサポートするプルリク↓が出てる。

github.com

所感

  • HTLC自体は以前からある仕組みで今回のBIPで特に新しい概念や機能が加わったものではない。
  • 今回のはBIP化というより、Bitcoin CoreがHTLCをサポートすることが目的かな?
  • Lightning Networkはホワイトペーパーではハッシュのプリイメージの交換だったけど、現状はRevocation Keyという公開鍵を使う方式になってるのでこのBIPとはあまり関係なくなってる。

トランザクションのmalleabilityを利用した攻撃を受けた話

最近testnetでブロードキャストしたトランザクションが、消えて無くなる現象が続いたので調べてみた。

消えたといっても実際に消えてるわけではなく、ブロードキャストした際のTXIDとは別のTXIDでブロックに格納されている。

実際にブロードキャストした際のTXIDは

16693caa1fa4a9eb5792063138d36c4c12a02287426b8c1fd759efb35199b5ef

で、その内容は↓

{
  "hex": "010000000201e4a0f1fa83c642b91feafae36a0f8fded4158dfa6fd650e046b4364b805684000000006a473044022045c65646abc12c71352335dbec2824b2dbdef9253366b4b83439b2190ce098d20220112f48fc8e2c76d079a4bc1d448ec134d22737d9765421b7a9b050dd4dc9368f01210259f6658325c4e3ca6fb38f657ffcbf4b1c45ef4f0c1dd86d5f6c0cebb0e09520ffffffff31137db564a7fad07c9db5b6b862786589977c68d1270819030a9079941ca6c9010000006a47304402204354565632eedd30fb9ca5c22bb70ef848afd74f7bed354d267705a6e71ea885022019159daf2d623ef634a65399ce70e34c4422c8eba19bf877e6b822ffc6a4607601210259f6658325c4e3ca6fb38f657ffcbf4b1c45ef4f0c1dd86d5f6c0cebb0e09520ffffffff02801d2c04000000001976a914322653c91d6038e08b6d971e4560842c155c8a8888ac80248706000000001976a9143b9722f91a2e50d913dadc3a6a8a88a58a7b859788ac00000000",
  "txid": "16693caa1fa4a9eb5792063138d36c4c12a02287426b8c1fd759efb35199b5ef",
  "hash": "16693caa1fa4a9eb5792063138d36c4c12a02287426b8c1fd759efb35199b5ef",
  "size": 372,
  "vsize": 372,
  "version": 1,
  "locktime": 0,
  "vin": [
    {
      "txid": "8456804b36b446e050d66ffa8d15d4de8f0f6ae3faea1fb942c683faf1a0e401",
      "vout": 0,
      "scriptSig": {
        "asm": "3044022045c65646abc12c71352335dbec2824b2dbdef9253366b4b83439b2190ce098d20220112f48fc8e2c76d079a4bc1d448ec134d22737d9765421b7a9b050dd4dc9368f[ALL] 0259f6658325c4e3ca6fb38f657ffcbf4b1c45ef4f0c1dd86d5f6c0cebb0e09520",
        "hex": "473044022045c65646abc12c71352335dbec2824b2dbdef9253366b4b83439b2190ce098d20220112f48fc8e2c76d079a4bc1d448ec134d22737d9765421b7a9b050dd4dc9368f01210259f6658325c4e3ca6fb38f657ffcbf4b1c45ef4f0c1dd86d5f6c0cebb0e09520"
      },
      "sequence": 4294967295
    }, 
    {
      "txid": "c9a61c9479900a03190827d1687c9789657862b8b6b59d7cd0faa764b57d1331",
      "vout": 1,
      "scriptSig": {
        "asm": "304402204354565632eedd30fb9ca5c22bb70ef848afd74f7bed354d267705a6e71ea885022019159daf2d623ef634a65399ce70e34c4422c8eba19bf877e6b822ffc6a46076[ALL] 0259f6658325c4e3ca6fb38f657ffcbf4b1c45ef4f0c1dd86d5f6c0cebb0e09520",
        "hex": "47304402204354565632eedd30fb9ca5c22bb70ef848afd74f7bed354d267705a6e71ea885022019159daf2d623ef634a65399ce70e34c4422c8eba19bf877e6b822ffc6a4607601210259f6658325c4e3ca6fb38f657ffcbf4b1c45ef4f0c1dd86d5f6c0cebb0e09520"
      },
      "sequence": 4294967295
    }
  ],
  "vout": [
    {
      "value": 0.70000000,
      "n": 0,
      "scriptPubKey": {
        "asm": "OP_DUP OP_HASH160 322653c91d6038e08b6d971e4560842c155c8a88 OP_EQUALVERIFY OP_CHECKSIG",
        "hex": "76a914322653c91d6038e08b6d971e4560842c155c8a8888ac",
        "reqSigs": 1,
        "type": "pubkeyhash",
        "addresses": [
          "mk67zfwR4f5UZg2Lfa7nuvbdsm7mdmUpcV"
        ]
      }
    }, 
    {
      "value": 1.09520000,
      "n": 1,
      "scriptPubKey": {
        "asm": "OP_DUP OP_HASH160 3b9722f91a2e50d913dadc3a6a8a88a58a7b8597 OP_EQUALVERIFY OP_CHECKSIG",
        "hex": "76a9143b9722f91a2e50d913dadc3a6a8a88a58a7b859788ac",
        "reqSigs": 1,
        "type": "pubkeyhash",
        "addresses": [
          "mkx3DXaWZt1GGaXLFh7cGPvW3NC4ZoHnMR"
        ]
      }
    }
  ]
}

ブロードキャストして、外部のブロックチェーンエクスプローラにもそのトランザクションが伝播したことは確認した。しばらくして、このTXIDのトランザクションが承認されたか確認しようとしたら、

No such mempool or blockchain transaction. Use gettransaction for wallet transactions.

となり、ブロックチェーンエクスプローラ側でもそんなトランザクションは無いと言われる。(さっきまであったのに)

ただ実際にトランザクションが消えてなくなった訳ではなく、

59af5c2f932b23dc19e06dd92e1ead1fd2a2546df7cbb834f47e2c1355272cc6

という別のTXIDでブロックに格納されていた。このトランザクションの内容は↓

{
  "hex": "010000000201e4a0f1fa83c642b91feafae36a0f8fded4158dfa6fd650e046b4364b805684000000006b483045022045c65646abc12c71352335dbec2824b2dbdef9253366b4b83439b2190ce098d2022100eed0b70371d3892f865b43e2bb713ec9e887a50d38f47e8416220daf826d0ab201210259f6658325c4e3ca6fb38f657ffcbf4b1c45ef4f0c1dd86d5f6c0cebb0e09520ffffffff31137db564a7fad07c9db5b6b862786589977c68d1270819030a9079941ca6c9010000006b48304502204354565632eedd30fb9ca5c22bb70ef848afd74f7bed354d267705a6e71ea885022100e6ea6250d29dc109cb59ac66318f1cb2768c13fb0daca7c3d91a3b8d0991e0cb01210259f6658325c4e3ca6fb38f657ffcbf4b1c45ef4f0c1dd86d5f6c0cebb0e09520ffffffff02801d2c04000000001976a914322653c91d6038e08b6d971e4560842c155c8a8888ac80248706000000001976a9143b9722f91a2e50d913dadc3a6a8a88a58a7b859788ac00000000",
  "txid": "59af5c2f932b23dc19e06dd92e1ead1fd2a2546df7cbb834f47e2c1355272cc6",
  "hash": "59af5c2f932b23dc19e06dd92e1ead1fd2a2546df7cbb834f47e2c1355272cc6",
  "size": 374,
  "vsize": 374,
  "version": 1,
  "locktime": 0,
  "vin": [
    {
      "txid": "8456804b36b446e050d66ffa8d15d4de8f0f6ae3faea1fb942c683faf1a0e401",
      "vout": 0,
      "scriptSig": {
        "asm": "3045022045c65646abc12c71352335dbec2824b2dbdef9253366b4b83439b2190ce098d2022100eed0b70371d3892f865b43e2bb713ec9e887a50d38f47e8416220daf826d0ab2[ALL] 0259f6658325c4e3ca6fb38f657ffcbf4b1c45ef4f0c1dd86d5f6c0cebb0e09520",
        "hex": "483045022045c65646abc12c71352335dbec2824b2dbdef9253366b4b83439b2190ce098d2022100eed0b70371d3892f865b43e2bb713ec9e887a50d38f47e8416220daf826d0ab201210259f6658325c4e3ca6fb38f657ffcbf4b1c45ef4f0c1dd86d5f6c0cebb0e09520"
      },
      "sequence": 4294967295
    }, 
    {
      "txid": "c9a61c9479900a03190827d1687c9789657862b8b6b59d7cd0faa764b57d1331",
      "vout": 1,
      "scriptSig": {
        "asm": "304502204354565632eedd30fb9ca5c22bb70ef848afd74f7bed354d267705a6e71ea885022100e6ea6250d29dc109cb59ac66318f1cb2768c13fb0daca7c3d91a3b8d0991e0cb[ALL] 0259f6658325c4e3ca6fb38f657ffcbf4b1c45ef4f0c1dd86d5f6c0cebb0e09520",
        "hex": "48304502204354565632eedd30fb9ca5c22bb70ef848afd74f7bed354d267705a6e71ea885022100e6ea6250d29dc109cb59ac66318f1cb2768c13fb0daca7c3d91a3b8d0991e0cb01210259f6658325c4e3ca6fb38f657ffcbf4b1c45ef4f0c1dd86d5f6c0cebb0e09520"
      },
      "sequence": 4294967295
    }
  ],
  "vout": [
    {
      "value": 0.70000000,
      "n": 0,
      "scriptPubKey": {
        "asm": "OP_DUP OP_HASH160 322653c91d6038e08b6d971e4560842c155c8a88 OP_EQUALVERIFY OP_CHECKSIG",
        "hex": "76a914322653c91d6038e08b6d971e4560842c155c8a8888ac",
        "reqSigs": 1,
        "type": "pubkeyhash",
        "addresses": [
          "mk67zfwR4f5UZg2Lfa7nuvbdsm7mdmUpcV"
        ]
      }
    }, 
    {
      "value": 1.09520000,
      "n": 1,
      "scriptPubKey": {
        "asm": "OP_DUP OP_HASH160 3b9722f91a2e50d913dadc3a6a8a88a58a7b8597 OP_EQUALVERIFY OP_CHECKSIG",
        "hex": "76a9143b9722f91a2e50d913dadc3a6a8a88a58a7b859788ac",
        "reqSigs": 1,
        "type": "pubkeyhash",
        "addresses": [
          "mkx3DXaWZt1GGaXLFh7cGPvW3NC4ZoHnMR"
        ]
      }
    }
  ],
  "blockhash": "00000000000022e3612bb0d79705417ff2cd77212d778a87156cc29769fbc83c",
  "confirmations": 5,
  "time": 1491551887,
  "blocktime": 1491551887
}

ブロードキャストしたトランザクションの各入力はSIGHASH_ALLで署名しているので、出力等のデータは全く変わっておらず、Bitcoinが盗まれた訳ではない。入力も当然OutPoint(参照先のTXIDと出力インデックス)は変わってないが、scriptSigが変わっていることが分かる。2つある入力の全てのscriptSigがブロードキャスト時のトランザクションと変わっている。

入力の参照先のscriptPubkeyはP2PKHなので、入力のscriptSig

<署名>[SIGHASH_TYPE] 公開鍵

という構成になる。これがどう改竄されたのか最初の入力データで確認してみる。

ブロードキャスト時のデータ
473044022045c65646abc12c71352335dbec2824b2dbdef9253366b4b83439b2190ce098d20220112f48fc8e2c76d079a4bc1d448ec134d22737d9765421b7a9b050dd4dc9368f01210259f6658325c4e3ca6fb38f657ffcbf4b1c45ef4f0c1dd86d5f6c0cebb0e09520

署名データは

3044022045c65646abc12c71352335dbec2824b2dbdef9253366b4b83439b2190ce098d20220112f48fc8e2c76d079a4bc1d448ec134d22737d9765421b7a9b050dd4dc9368f01
ブロックに取り込まれたデータ
483045022045c65646abc12c71352335dbec2824b2dbdef9253366b4b83439b2190ce098d2022100eed0b70371d3892f865b43e2bb713ec9e887a50d38f47e8416220daf826d0ab201210259f6658325c4e3ca6fb38f657ffcbf4b1c45ef4f0c1dd86d5f6c0cebb0e09520

署名データは

3045022045c65646abc12c71352335dbec2824b2dbdef9253366b4b83439b2190ce098d2022100eed0b70371d3892f865b43e2bb713ec9e887a50d38f47e8416220daf826d0ab201

攻撃方法

上記のデータから改竄されているのは署名部分であることが分かる。

各署名は、DER形式のECDSA署名で、以下の形式でシリアライズされている。

0x30 [total-length] 0x02 [R-length] [R] 0x02 [S-length] [S] [sighash-type]
  • total-length
    この後に続くデータの長さ( sighash-typeを除く)
  • R-length
    Rの値の長さを指す1バイトデータ。
  • R
    任意のデータ長で、署名作成時に生成した乱数値。ビッグエンディアンエンコードされている。
  • S-length
    Sの値の長さを指す1バイトデータ。
  • S
    Rと署名対象のメッセージのハッシュ値秘密鍵から計算した値。任意のデータ長で、ビッグエンディアンエンコードされている。
  • sighash-type
    署名時に使用したハッシュタイプのフラグ(0x01, 0x02, 0x03, 0x81, 0x82, 0x83を指定可能)

↑のブロードキャスト時とブロックに入れれた際の署名を見ると、Rの値(45c65646abc12c71352335dbec2824b2dbdef9253366b4b83439b2190ce098d2)は同じであることが分かる。変わっているのはSの値。

これは署名値(r,s)のsの値をマイナスにした、(r, -s (mod N))の場合もその署名は有効と判断するECDSAの署名のmalleabilityを利用したものだ。

ブロードキャスト時のS値は

112f48fc8e2c76d079a4bc1d448ec134d22737d9765421b7a9b050dd4dc9368f

だが、この-s (mod N)の値を計算すると↓

require 'bitcoin'

origin_s = '112f48fc8e2c76d079a4bc1d448ec134d22737d9765421b7a9b050dd4dc9368f'
s = OpenSSL::BN.from_hex(origin_s).to_i

n = Bitcoin.bitcoin_elliptic_curve.group.order.to_i

modified_s = OpenSSL::BN.new((s * -1) % n).to_hex
> 'eed0b70371d3892f865b43e2bb713ec9e887a50d38f47e8416220daf826d0ab2'

となり、改竄後の↓の値に合致する。

eed0b70371d3892f865b43e2bb713ec9e887a50d38f47e8416220daf826d0ab2

改竄されたデータにはさらに先頭に00が付与されている。

影響

今回の署名の改竄は出力先や量が変更できるものではないので、Bitcoinを盗まれるという影響は無い。ただ双方向のペイメントチャネル等のまだブロードキャストされていないトランザクションのTXIDを元にトランザクションを作るようなケースでは、確定したTXIDが必要になる。これはSegwitを導入により回避できる。

そういう用途で使用する際は事前にP2WPKH宛にコインを送って送る必要がありが、その際のトランザクションは今回のようなmalleabilityを利用した攻撃によりTXIDが書き換わる可能性があることを想定しておく必要がある。

また一般に公開されているtestnetのFaucetやウォレットアプリではまだP2WPKHやP2WSHを使った送金ができる機能は見かけないので、そういったアプリ側が早くWitnessなトランザクションを作れるようになる必要がある。そのためにもP2WPKHやP2WSHのアドレスフォーマットを定義したBech32↓のBIP登録が待たれる。

github.com

あと、mainnetでのSegwitのアクティベートも。

他にもtestnetでmalleabilityを利用した改竄被害にあった人いるみたい↓

www.reddit.com

誰が改竄してる?

ブロードキャストしたトランザクションはブロックに入るまでは、外部のブロックチェーンエクスプローラでも確認できたので中間者による改竄ではなくブロックをマイニングしたマイナーの可能性が高い。

通常ブロックをマイニングしたのがどのマイニングプールかは、コインベーストランザクションのcoinbase scriptに記載されていることが多い。 ↑トランザクションはブロックheightが1116497のブロックに入っていて、そのcoinbaseは

0351091100048f46e75804a17031160cdfffe558d87db95884fb08001d2f4542f09f92a92f4144f09f98b12f434b506f6f6c5072696d6172792f

BIP-034によりこの先頭はblock heightで、その後が任意のスクリプトになっており、マイニングプールの情報が記載されていることが多い。block heightを除いたデータをデコードすると↓のようになる。

 �F�X�p1���X�}�X� /EB💩/AD😱/CKPoolPrimary/

CKPoolPrimaryがそうなのか?著名なマイニングプールではなさそう。

それにしても、malleabilityの問題自体は認識していたものの、今まで実際にやられたことは無かったので良い経験になった。

Confidential Assetsの概要

Blockstreamが新しくConfidential Assetsなる機能をリリースしていたので、どういったものなのか見てみる。

blockstream.com

Open Assets ProtocolやCounterpartyやColuなどBitcoinブロックチェーン上で、Bitcoin以外のアセットを発行・送付するプロトコルが登場しているように、同じチェーン上で複数のアセットタイプをサポートするのはブロックチェーン技術の自然な進化と考えられる。複数のアセットタイプが同一のチェーンで扱えることで、それぞれのアセットタイプで同じチェーンのセキュリティ特性を共有可能になり、マルチアセットのアトミック・スワップのような新しいユースケースが可能になる。
複数のアセットタイプをサポートするには、各トランザクションの出力にasset tagをラベル付けするだけで実現できるが、この場合ユーザがそのトランザクションでどれだけの量を取引しているのか誰もが確認することができてしまう。Confidential Assetsは、Confidential Transactionの秘匿技術を利用してマルチアセットタイプをサポートする際にasset tagをブラインディングする技術になる。

Confidential Transaction*1と同様、Confidential Assetsではそのトランザクションが正しいトランザクションであることを誰もが暗号的に検証することができ、予期せずアセットが作成、破棄、変更されることが無いことを保証する。一方、そのトランザクションで取引されているアセットタイプやその量は、取引の当事者のみしか知ることができず、アセットタイプや量を他のユーザーから秘匿できる。

マルチアセットを扱うチェーンでは、通常特定のタイプのアセットの作成と破棄が行われる。Confidential Assetsにおけるアセットの発行というのは、新しいasset tagの作成とアセットの量を指定する発行用のトランザクションを作成することを指す。その後のアセットの再発行は、アセットに関連付けられた再発行トークンに関する権限を証明することで実行できる。この発行トークンはアセット作成時に一緒に作成できるが、アセットが再発行をサポートしない場合この発行トークンは作成しない。

発行トランザクションは、入力と出力のバランスを取らなくていいという点で独特なトランザクションになる。この時、発行量が公開されていれば、そのアセットの発行量は周知になる。

では、具体的にConfidential Assetsの仕組みをみていく。

Confidential Assetsの技術

Asset TagとRangeproofs

Confidential Assetsの技術を知るためにはまずConfidential TransactionのベースになっているPedersen commitmentsについて知る必要がある。

commitment = xG + aH

ここでG楕円曲線の標準のベースポイントで、Hは2つめのベースポイントで、Commitment TransactionではH = to_point(SHA256(ENCODE(G)))といったロジックで算出される。このようなベースポイントをnothing-up-my-sleeve = NUMSと呼ぶ。

Commitment Transactionで↑はxを使ってコインの量aをブラインディングするcommitmentとして説明されている。Confidential Assetsでは、Haコインとして扱う(= Hをアセットタイプとして扱う)ことができるのではないかという点に注目している。
別のNUMSベースポイントIを追加し、以下の2つのcommitmentを考えたとき、

commitment_1 = xG + aH
commitment_2 = xG + aI

ここでHIは異なるアセットを表すものとして考えることができ、この2つのcommitmentは、量は同じaを持つ異なるアセット(HI)へのcommitmentであると言える。

以下のようなアセットタイプの異なる2つの入力と2つの出力を持つトランザクションを考えてみる。

in1 = xG + aH, H --\   /-- uG + cH, H = out1
                   |---|
in2 = yG + bI, I --/   \-- vG + dI, I = out2

Commitment Transactionの場合ベースポイントは1つだったので、トランザクションの入力と出力で量が合っているかどうかは

out1 + out2 - in1 - in2 = 0

を検証すればよかった。複数アセットの場合も同様の式が成立する↓

0 = out1 + out2 - in1 - in2  = (uG + cH) + (vG + dI) - (xG + aH) - (yG + bI)  = (u + v - x - y)G + (c - a)H + (d - b)I

HIは両方とも NUMSポイントなので、この式が成り立つのは個々の項が0の場合か、c = a かつ b = dの場合。言い換えると、この式はアセットHの総量が入力と出力で同じで、アセットIの総量が入力と出力で同じ場合に成立する。

これは2つ以上のasset tagでも成立し、実際にそれぞれ一意のNUMSポイントを割り当てることができる限り、無制限の種類のアセットをサポートすることができる。

Commitment Transactionと同様、この式だけでは値のオーバーフローやマイナス値への考慮が不十分なため、Commitment Transactionと同様、各出力にrangeproofをアタッチすることでこれを解決する。唯一違うのは、検証者が予め決まっていた固定ベースポイントHを使う代わりに、asset tagを使って検証しなければならない点だけ。

Blinded Asset TagとAsset Surjection Proofs

↑の例では、各トランザクション出力にNUMSポイント、もしくはasset tagが関連付けられており、同じアセットタイプの出力では同じtagが使用される前提だった。これだと出力毎にどんな種類のアセットが取引されているのか分かってしまうというプライバシー上の問題が残る。

これは各asset tagを以下の形式のblinded asset tagに置き換えることで解決できる。

A = H + rG

Hは↑のasset tagで、rはランダムなシークレット値。rを知る人は誰でもこのタグが表すアセットが分かるが、rを知らない人にとってはランダムな楕円曲線上のポイントにしかみえない。Aを持つ値へのコミットメントはHを持つ値へのコミットメントなので、"出力の総量 - 入力の総量 = 0"というルールはトランザクションの検証時に引き続き機能する↓

commitment_A = xG + aA = xG + a(H + rG) = (x + ra)G + aH = commitment_H

このブラインディングファクターの導入は、ユーザーがrangeproofを生成するのに影響を与えないが、入出力のバランスの計算が少し複雑になる。

というのも各blinded asset tagは検証者から見るとランダムな値に見えるので、そのトランザクションasset tagが正しいか検証する際に(以下のようなblinded asset tagの悪用を考えると)"出力の総量 - 入力の総量 = 0"という検証だけでは充分でないという問題がある。

A′ = -H + rG

この場合ブラインドされたアセットA′ の量は、アセットHの負の値をとり、攻撃者が違法に資金供給量を増やすのに使われることになってしまう。

この問題を解決するためasset surjection proofという仕組みが導入される。これはトランザクション内のどの出力がその入力に対応しているのかをブラインディングした状態で、各トランザクション出力のアセットタイプがある入力のアセットタイプと同じであることを暗号的に証明する仕組みになる。

この仕組みは簡単で、同じasset tagアセットHにコミットするblinded asset tagABがあるとして、

A - B = (H + aG) - (H + bG) = (a - b)G

秘密鍵a-bに対応する署名鍵となる。
トランザクション出力out1が与えられると、トランザクションの全ての入力に対し、out1 - in1out1 - in2..の鍵で(いくつかの秘密鍵の内のどれかで署名されたことは分かるがどの秘密鍵が使われたのかは分からない)リング署名を使う。out1がある入力と同じasset tagを持つ場合、トランザクションの署名者は署名鍵の中から対応する秘密鍵を知り、リング署名をすることができる。

asset surjection proof はこのリング署名で構成されている。

デモ

DG LabがOSSで公開してる↓

github.com

まとめ&所感

  • どうやってアセットを表現するのかと思ったけど、Commitment Transactionのcommitment作成する際の2つめのベースポイントをアセットタイプとしてみなすことで実現してる。
  • そのためアセットの量の秘匿もCommitment Transactionのロジックそのままで秘匿可能。
  • 量は秘匿できても、各出力に割り当てられたasset tagから出力でどのアセットが取引されているかわかるので、アセットタイプも秘匿したい場合はblinded asset tagを使う。
  • blinded asset tagは、ランダムなシークレット値rを使用してH + rGを計算した値で、rを知るものしか本当にアセットタイプが何か知ることができない。
  • ただ、 blinded asset tagを悪用してマイナスの量を組みこめてしまうので、単純な入出力のバランス以外にasset surjection proof という仕組みで不正がないか検証できるようにしてる。
  • ベースポイントを拡張アイテムとして使うアプローチは楕円曲線の特性を上手く活かしてて面白い。
  • 時間のある時にホワイトペーパーの方も読んでおきたい。

*1:Blockstreamのサイドチェーンで実装されているコインの取引量を秘匿する技術。詳細は以前書いた↓参照。techmedia-think.hatenablog.com