Develop with pleasure!

福岡でCloudとかBlockchainとか。

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

Bitcoin Core 0.14.0のリリース

Bitcoin Core 0.14.0がリリースされたのでどんな変更があったのか見ておく↓

Bitcoin Core :: Bitcoin Core 0.14.0 Released with Performance Improvements

主な新機能は↓

IBDのパフォーマンス改善

ブロックチェーンのデータサイズは絶えず増加していて、現在mainnetのデータ量は125GBと巨大。新しくノードを立ち上げる場合、最初に大量のブロックデータをダウンロードして処理する必要がありウォレットが利用可能になるまで結構な時間がかかる。この初期ブロックのダウンロード(IBD)時間を改善するため今までいろんな改良が加えられてきた↓

リリース IBDに関する改善
0.5.0 チェックポイントより前の署名のスキップ
0.8.0 LevelDBへの切り替えと署名検証の並列化
0.10.0 ヘッダを先に同期し、その後ブロックを並列ダウンロード
0.11.0 (オプション)ディスクスペースを節約するためのブロックファイルの剪定
0.12.0 楕円曲線のライブラリを独自実装(libsecp256k1)し署名検証を高速化
0.13.1 将来古い署名のダウンロードをスキップできるようにするSegwitの導入

今回の0.14.0のリリースではこのIBDの速度を大幅に向上させる2つの改善点が含まれている↓

  1. Assumed valid blocks
  2. mempoolとUTXOのDBキャッシュ間のメモリ共有

この結果、EC2のt2.xlarge(4コア、メモリ16GB)で、Bitcoin Core 0.13.2とBitcoin Core 0.14.0で同じ設定で初期ブロックのダウンロード行うと

  • Bitcoin Core 0.13.2だと、全ブロックダウンロードが終わるのに1日と12時間40分
  • Bitcoin Core 0.14.0だと、全ブロックダウンロードが終わるのに6時間25分

と5.7倍高速化してる。

ちなみに↑はデフォルト設定の場合で、起動オプションの1つである-dbcacheオプション(デフォルト300(MB))を-dbcache=8000(8GB)を指定して実行すると約3時間で同期ができる。

で2つの改善点がどういうものかというと

Assumed valid blocks

0.3.2でチェックポイントと呼ばれる仕組みが導入された。これはフルノードに、ある時点でベストチェーンとは別のチェーンを検証させ無駄な負荷をかけるようなDoS攻撃を防ぐためのもの。

0.5.0では最新のチェックポイントより前のブロックの署名をスキップすることでIBDを高速化した。

しばらくしてBitcoinのセキュリティの改善やその他の改善(ヘッダの初期同期やminimum chainworkなど)によりチェックポイントの必要性は減り、チェックポイントは既に有効だと判断したチェーンのみを受け入れるものではあるが、どのチェーンが有効か判定しているように見えセキュリティモデルについて開発者の混乱を招くおそれがあったので、多数の開発者からチェックポイントを削除したいという要望が出た。

Assumed valid blockは、署名スキップの最適化処理をDoSを防ぐためのチェックポイントから分離し、それぞれ独立して実行できるようにした新機能になる。

どういうものかというと、フルノードを新規に起動する際、そのユーザが有効なブロックを知っている場合、その有効な最新ブロックのブロックハッシュをBitcoin Core 0.14.0に渡すことで、そのブロックの前までの署名をスキップする機能になる。署名の検証処理はIBDの処理の中でもCPU消費が大きいためAssumed valid blockを使うとIBDを大幅に高速化できる。指定したAssumed valid block以降のブロックについては、全てちゃんと署名が検証される。

チェックポイントとAssumed valid blockの大きな違いは、チェックポイントに指定されているブロックはブロックチェーンの一部でないといけないけど、Assumed valid blockは別にチェーンの一部でなくても良い。ユーザーから渡されたAssumed valid blockがチェーンの一部でない場合、Bitcoin Coreは単純に全ての履歴ブロックの署名を検証するだけ。また、フォークが発生し最新のPoWの結果有効なチェーンにAssumed valid blockで指定したブロックが無い場合、Bitcoin CoreはAssumed valid blockを破棄して新しい有効なチェーンに切り替える。

これからBitcoinを始めようというユーザーは、おそらく有効なブロックとか知らないと思うけど、全てのコンセンサスルールを知っているとも思えないので、そういったユーザーは単純にフルノードをダウンロードして使えば良い。Bitcoin Core 0.14.0には複数の著名な開発者がそれぞれ有効であると確認したデフォルトのAssumed valid blockが組み込まれている↓

  • mainnet(#453354)
    00000000000000000013176bf8d7dfeab4e1db31dc93bc311b436e82ab226b90
  • testnet(#1079274)
    00000000000128796ee387cf110ccb9d2f36cffaf7f73079c995377c65ac0dcc

Assumed valid blockを使わずに全ての署名を検証したい場合は、-assumevalid=0をオプションにつけて起動すればいい。任意のAssumed valid blockを指定する際は以下のようにassumevalidで対象ブロックのブロックハッシュを指定する。

-assumevalid=00000000000000000013176bf8d7dfeab4e1db31dc93bc311b436e82ab226b90

mempoolとUTXOのDBキャッシュ間のメモリ共有

IBDの処理中は、Bitcoin Coreはmempoolを使用しない。これは最新のブロックが無いと最新のトランザクションが有効化どうか確認する方法が無いためで、その間通常よりもメモリ消費は少ない。

0.14.0では、このIBD中に使われていないmempoolのメモリをUTXOデータベースキャッシュと共有するようになる。これによりより多くのUTXOを高速なメモリ上にキャッシュできるようになり、ディスクアクセスを減らすことができる。

新しいブロックの検証とリレーの高速化

0.14.0の↓の4つの改善点は、新しいブロックをできるだけ速く受信したいマイナーやその他のユーザーにとって特に関心があるものになる。

署名キャッシュの改善

最初の機能はcuckoo hashingを使うための署名キャッシュの更新。署名キャッシュはBitcoin Coreが未確認のトランザクションの署名の検証結果をキャッシュできるようにするもので、同じトランザクションが新しいブロックに含まれていた場合に再度署名を検証しなくてすむ。署名の検証は新しいブロックを処理する際に計算コストが最も高い処理なので、署名キャッシュを使うと新しいブロックを処理する速度が大幅に向上する。

既存の0.13.2の署名キャッシュは、CPUが8コア未満であれば上手く機能するけど、8コア以上のCPUを持つ場合、ロックが発生し使用可能なコアを有効活用できない。“cuckoo cache”を利用できるようcuckoo hashingを使用するようアップグレードすると、この問題は解決され、より多くのコアを効果的に利用できるようになる。

16コアの環境でテストすると、0.14.0を使うと0.13.2より40%速く新しいブロックの処理が可能になった。8コア未満の環境では大幅なパフォーマンスの向上は無いけど、同じメモリ量で以前より多くの署名をキャッシュすることができる。

以前のBIP152のCompact Blockリレー

2つめの改善点は、 BIP152のCompact Blockの実装。以前の実装ではBIP-152の2つのオプトインモードがサポートされていた↓

  • 帯域幅モード
    新しいブロックをリレーするのに必要最小限のデータ送信するモードで、受信ノードが特定の新しいブロックの要求を待つ
  • 帯域幅モード
    受信ノードによる特定の新しいブロックの要求を待つこと無く、新しいブロックデータを送信するモード。この場合、受信ノードが既に別ノードから受信したデータと重複するデータを送信するリスクはあるが、ブロックは迅速に転送される。

0.14.2で更新された実装では、ブロックが完全に検証される前に新しいブロックのリレーを始めることで、高帯域幅モードのノードを強化する。最良の場合、検証遅延が無くなることで新しいブロックがP2Pネットワーク上で数ホップに渡って以前より数倍速くブロックを伝播させることができる。最悪の無効なブロックの伝播で余計な帯域幅を消費する。いずれの場合も無効なブロックは受信側のノードで拒否されるのでセキュリティモデルは今までのまま。

※ BIP-152の仕様については↓

techmedia-think.hatenablog.com

並行性とスループットにフォーカスしたP2Pコードのリファクタリング

3つめの改善点は並行性とスループット向上のためのP2Pネットワークコード全体のリファクタリング。並行性の改善では、新しく受信したブロックを優先度の低いトラフィックよりも先に処理できるようになり、ブロックができるだけ速くリレーされ検証できるようになる。

またメッセージの処理中にバックグラウンドでネットワーク処理も継続して行えるようになり、特にIBDの速度の改善にも繋がる。

mempoolをディスクに保存

4つめの改善点は、各ノードが受信した未確認のトランザクションのmempoolが通常のシャットダウン時にディスクに保存され、ノードが再び起動した際にディスクからメモリ上にロードされるようになった。

Compact Blockと組み合わせることで、未確認のトランザクションが新しく生成されたブロックに含まれている際、再ダウンロードする必要がなくなる。署名キャッシュと組み合わせることで、未確認トランザクションの署名の検証結果のキャッシュで、新しいブロックをより迅速に検証することができる。

こういった機能はよくRDBがシャットダウン時にキャッシュをディスクに保存して、次回起動時にそこからキャッシュを復元しウォームアップ期間を短縮するのに使われるテクニックと似てる。

(オプション)手数料の置換

0.14.0のオプション機能(デフォルトは無効)として、ウォレットで前に作成したトランザクションを新しく生成したトランザクションで置換する機能が含まれ、これによりBIP-125のオプトインReplace By Fee が利用可能になる。

Bitcoin Core起動時に-walletrbfオプションを付与してこの機能を有効にすると、作成したけどまだ承認されていないトランザクションについて、より高い手数料を設定して再送信することができるbumpfee RPCが利用できるようになる。オプトインRBFもしくはフルRBFをサポートするマイナーは、手数料の低いトランザクションから高くなったトランザクションにキューのデータを置換する。手数料が高くなった分マイナーにはより速くブロックに入れるインセンティブが働く。

UTXOのサイズ削減のアプローチTXO Commitments

BitcoinのUTXOのデータサイズは増加する一方で、フルノードを運用する際のボトルネックにもなっているが、そのUTXOの成長の問題を解決する方法としてBitcoinのコア開発者の1人であるPerter Toddが昨年 TXO Commitmentsという仕組みについてブログに投稿してた↓ので仕様部分だけまとめてみた。

Making UTXO Set Growth Irrelevant With Low-Latency Delayed TXO Commitments

現状の課題

現状Bitcoinにはブロックサイズの制限以外に、各種データのサイズを制限するようなコンセンサスルールは存在せず、UTXOのサイズも増えている。UTXOのセットを保持するのがフルノードの最小要件なので、UTXOのサイズが大きくなるほどフルノードの実行のハードルが上がる。

またUTXOのディスク上のサイズは圧縮しシリアライズされた状態であり、UTXOをモリ上に展開するとそのサイズは大幅に増える。複数の入力をマージして1つのUTXOを作るなどすればUTXOのサイズを減らせるが、現状そういったことをするインセンティブも無いので、サイズ削減も期待できない。それ以外に、小額過ぎて実際に使うことのないUTXOや秘密鍵の紛失により使用できないUTXO、コインの送付を目的としていないタイムスタンプサービスなどの使い方など、多くの要因によってUTXOサイズは増加している。特にタイムスタンプサービスなどはデータを記録するのが目的なので、通常のBitcoin送付の手数料よりもずっと高額な手数料を支払うことも許容される。

今のところUTXOの成長を防ぐための良いツールはなく、Segregated Witnessには、UTXOのサイズの削減する効果があるが、UTXOの成長を無視できるほどの効果は無い。

UTXOセットのサイズをハードリミットするというアイディアもあるが、これはBitcoinノードを効率的に動かすための最小要件であって、UTXOのスパム問題の根本解決にならない。

TXO Commitments

このUTXOのサイズの問題を解消しようと提案されているのがTXO Commitments。

(使用済み・未使用両方の出力を含む)全てのトランザクション出力の状態にコミットするマークルツリーを構築する仕組みで、出力の現在の状態をコンパクトに証明する方法を提供できる。これによりUTXOセットのアクセスの低い部分をアーカイブし、フルノードで関連データを破棄しても、それらの出力が実際に使用されていないことをノードに証明することで、アーカイブされた出力が使用する仕組みを提供する。

具体的には、TXO Commitmentsでは決定性のあるインデックス可能な挿入型マークルツリーの一種であるMerkle Mountain Range (MMR)を提案しており、ストレージ要件を最小限に抑え、新しいアイテムを安価にツリーに追加することができる。出力がTXO MMRに追加されると、その出力は決して削除されることはない。出力が使用された場合は、その場でステータスが更新される。MMRの特定のアイテムの状態と、MMRのアイテムの変更の有効性は両方ともツリーの先端までのマークルパスからなる { \displaystyle\log{2} n}サイズのプルーフで証明できる。

TXO commitment proofsは署名者の関与無しに生成及びトランザクションへの追加ができる。TXO commitment proofsは自体は署名される必要はなく、トランザクションハッシュの一部でもないので、TXO MMRのデータにアクセスできる人であれば誰でも、不足しているproofを再生成することができ、TXO commitmentsを使用するためにウォレットのソフトウェアに最小限の変更が必要となる。

TXO Commitmentsの実装

コミットメントフルノード実装では次のデータを格納する必要がある。

  1. UTXOセット
    未使用であることが明らかなtxoutsのキー:バリューのMap。既存のUTXOの実装と同様だが、古い出力のUTXOはこのセットから除外されるのが大きく違う。
  2. STXOセット
    直近のTXO commitmentの後に使用されたトランザクション出力のセット。ただしTXO commitmentより前に作成されたもの
  3. TXOジャーナル
    TXO MMRに使用済みとマークする必要がある出力。追加は低レイテンシーで、削除は高レイテンシーで行われる必要がある。
  4. TXO MMRリスト
    pruning可能な順序付けられたTXO MMRのリストで、主にペンディングされてるコミットメント。
Fast-Path: ブロック内で使用されているTxOutの検証

トランザクション出力がブロック内のトランザクションで使われた場合、以下の2ケースが考えられる。

  1. 最近作られた出力の場合
    最新のTXO commitmentの後に作られた出力なので、出力はUTXOセット内に存在する。そのためその出力を使用するトランザクションにはTXO commitment proofは不要。出力をUTXOセットから削除し、TXOジャーナルに追加する。
  2. アーカイブされた出力の場合
    最新のTXO commitmentより前に作られた出力なので、出力がUTXOセット内に存在するとは限らない。トランザクションはその出力が未使用であることを示す最新のTXO commitmentに対するTXO commitment proofを持つ。出力がSTXOに存在していないことを確認し、STXO内に無ければSTXOに追加する。出力と TXO commitment proofをTXOジャーナルに追加する。

どちらのケースでも、出力を使用済みとして記録する際は2つのキー・バリューの更新と1つのジャーナルの追加が必要。既存のUTXOセットは使用の都度1つのキー:バリューの更新が必要で、最悪ブロック内のトランザクション出力の使用が全てアーカイブされた出力であったとしてもブロックの検証レイテンシーは現状の2倍以内になると期待される。

Slow-Path:保留中のTXO Commitmentsの計算

優先順位の低いバックグラウンドタスクでTXOジャーナルをフラッシュし、TXO MMRに各ブロックが使用した出力を記録し、MMRデータをハッシュしTXO commitmentsダイジェストを取得する。さらにこのバックグラウンドタスクはTXO commitmentsに記録された出力をSTXOから削除し、不要になったTXO commitment を剪定する。

TXO commitmentの計算のスループットは既存のUTXOのみのスキームより悪くなる。

TXO MMRの詳細な実装

各TXO MMRの状態は、ほとんどの情報が共有されている前のものの修正で、多数の TXO commitmentsの状態を効率的に保存している。マークル化されたデータ構造では循環参照は不可能なので、ガベージコレクションは単純な参照カウンタで充分。不要になったデータはデータベースから削除することで剪定できる。

実際に構成も見たほうが理解しやすいと思うkので具体例で考えてみる。

2つのtxoutsを持つ以下のTXO MMRを考える。(これを状態#0と呼ぶ)

f:id:techmedia-think:20170306151818p:plain

別のエントリーを追加すると状態#1になる↓

f:id:techmedia-think:20170306152037p:plain

commitment #1で状態#0のデータがどのように再利用されたかに注目してほしい。状態#2を得るためさらに2つのエントリーを追加する。

f:id:techmedia-think:20170306152802p:plain

このケースでは状態#1のデータは再利用されていない。

ここで状態#2が最新のブロックによってブロックチェーンにコミットされたとする。状態#2で作られた出力を使おうとする将来のトランザクションは、それが未使用であることを証明する責務がある。基本的には状態#2のMMRのデータを提供しなければならない。これによりTXO MMRに新しいtxoutを追加するために必要最低限のデータを残し、データを剪定することができる。

f:id:techmedia-think:20170306154439p:plain

実装の詳細に応じて、ノード"2"と"e"に必要なデータはそのハッシュダイジェストだけである。

さらに3つのtxoutを追加すると状態#3になる。

f:id:techmedia-think:20170306155818p:plain

ここで最近作られたtxout fが使用されたとする。MMRを更新するための全てのデータがあり、状態#4になる。ここでは2つの内部ノードと1つのリーフノードを変更する。

f:id:techmedia-think:20170306160150p:plain

もしアーカイブされたtxoutが使われると、トランザクションは最近コミットされたTXO(このケースでは状態#2)にマークルパスを提供するよう要求する。txout bが使用された場合であれば、トランザクションは状態#2から以下のデータを提供する必要がある。

f:id:techmedia-think:20170306160644p:plain

ローカルのTXO MMRをベースに上記データを復元すると↓

f:id:techmedia-think:20170306173933p:plain

この時点でまだ状態#4は変更していない。txout bが使用されたことを示すようデータを更新し状態#5にする。

f:id:techmedia-think:20170306174242p:plain

続いて、現在状態#3がチェーンにコミットされている状態で、状態#3で作られたtxoutを使用するトランザクションを作りたい場合、状態#3のデータからなるTXO proofを提供する必要がある。出力gとhのリーフノードとその内部ノードが状態#3の一部であるため、それらを剪定する↓

f:id:techmedia-think:20170307103616p:plain

最後にa、c、gの3つのtxoutを使って、新しい3つのtxout i、j、kを作る。状態#3が最新のコミットされた状態だったので、aとgを使用するトランザクションはそこまでのマークルパスを提供する必要がある。これには状態#2のデータの一部が含まれる↓

f:id:techmedia-think:20170307105530p:plain

unpruningすると、状態#5のデータは以下のようになる。

f:id:techmedia-think:20170307110846p:plain

a、c、gの3つの出力を使用し、新しい出力i、j、kを追加した結果以下のの状態#6になる。

f:id:techmedia-think:20170307111424p:plain

再度、状態#4の関連データを剪定することができる。さらにSTXOセットの実装方法によっては、この状態以降の使用済みtxtoutに関連するデータも剪定することができる(配下のノードが使用済みとなった内部ノードを含む)。

まとめ&所感

  • 様々な要因によってBitcoinのUTXOのサイズは増加する一方で、フルノードを運用する際のボトルネックにもなっている。
  • 現状の実装ではUTXOセット1つのみの構成で、そこに全UTXOのデータが保持されているけど、アクセス頻度の低いUTXOをアーカイブして、高速にアクセスする必要のあるUTXOのサイズを削減しようというアプローチ。
  • アーカイブされたUTXOは、それがまだ未使用である証拠=TXO commitment proofsを提供することでUTXOとしての正当性を評価して使えるようにする。
  • TXO commitment では、全てのトランザクション出力(未使用・使用済み両方含む)からなるマークルツリー = Merkle Mountain Range (MMR)を構成する。
  • MMRは決定性があるインデックス可能な挿入型のマークルツリー。
  • 未使用・使用済み両方含んでるからUTXOじゃなくTXO Commitmentsなんだろう。
  • トランザクションでは、そのトランザクションで使用しようとしているUTXOが未使用であることをTXO commitment proofsを添付することで証明する。
  • TXO commitment proofsの提供が必要になるのはアーカイブ済みのtxoutsを使用するトランザクションの場合のみ。
  • アーカイブ済みのtxoutを使用するトランザクションは、最新のTXO commitmentへのマークルパスを提供する。
  • UTXOの削減以外の用途でもMMR使ったコミットメントの仕組み利用できるんじゃないかなー。