Develop with pleasure!

福岡でCloudとかBlockchainとか。

未来の価格に基づいた決済を可能にするDiscreet Log Contracts

Lightning Networkのホワイトペーパーを書いたThaddeus DryjaがこないだDiscreet Log Contracts(直訳すると離散対数コントラクト?)というホワイトペーパーを公開していたので↓見てみる。

https://adiabat.github.io/dlc.pdf

アブストラクト

スマートコントラクトはよくBitcoinのような暗号通貨のシステムで目にするが、金融分野ではまだ広く使われていない。スマートコントラクトを実装、採用にするにあたって、最大のハードルは、スケーラビリティと通貨に関する外部データをスマートコントラクト内から取得する難しさにある。これまでコントラクトのプライバシーは別の問題だった。Discreet Log Contractsは、スケーラビリティとプライバシーの問題に対処し、外部データを提供するオラクル*1への信頼を最小限に抑えるシステムを提案している。またこのコントラクトは外部のオブザーバーがトランザクションログからコントラクトの存在を検出できないように設計されている。

モデル

コントラクトのプロセスに、アリスとボブとオリビアの3者が参加しているとする。このうちアリスとボブはそれぞれ契約相手で、オリビアがオラクルになる。アリスとボブの間に信頼関係はなく、互いに法的な識別情報を知る必要はないが、認証されたチャネルを介して通信でき、互いを永続的に認識できる必要がある。
またアリスとボブはオリビアが署名しブロードキャストしたメッセージを受信できる必要がある。オリビアはアリスとボブを意識する必要はなく、理想的には情報をブロードキャストする以外の接触はない。ブロードキャストする情報は、Bitcoinネットワークでブロードキャストできるほどコンパクトだが、必ずしもBitcoinネットワークにブロードキャストする必要性はない。

DLCプロトコルは多種多様なコントラクトで使用できる。以下の例では未来の通貨価格をベースにしたコントラクトを作り実行する。

コントラクトは水曜日から始まり、水曜日時点では日本円の価値は1000satoshi。コントラクトは金曜日に終了し、その時点の日本円の価値は1050satoshiだったとする。

コミットされたRポイント署名

Discreet Log Contractsでは、Schnorr署名を使うが通常の使い方とは違う。通常ユーザーは秘密のスカラーaを生成し、そのaとベースポイントGからA = aGを計算し、Aを公開鍵として公開する。あるメッセージに署名する際は、aとは別にランダムな秘密のスカラーkを生成しR = kGを計算する。続いて

s = k -h(m, R)a

を計算する。ここでh()ハッシュ関数で、mは署名対象のメッセージ。この署名データは(R, s)になる。

この署名の検証は、与えられた (A, m, R, s)で以下の計算式が成立するか検証することである。

sG = R − h(m, R)A
   = kG − h(m, R)aG

Discreet Log Contractsでは、(A, R)を公開鍵と呼び、署名はsのみとする。方程式は同じだが、Rは署名の一部ではなく公開鍵の一部として使われる。未承認のトランザクションの二重使用を防ぐ仕組みに同様の構造を使用する方法が最近発表されている*2techmedia-think.hatenablog.com

リビアは公開鍵vGであるポイントVを公開している。オリビアはこの公開鍵を複数回使用し、秘密鍵であるvを安全に保つ必要がある。vは異なる当事者が保持する異なる鍵で構成することができる。またオリビアVとは別のポイントR = kGも公開している。kはランダムなnonceでRは1回限りの署名鍵である。これは通常のSchnorr署名で使われているkRと同じだが、Rは署名が計算される前にコミットされる。つまり署名するメッセージはまだ分かっていないが、事前にnonceは選択されそのRは公開されている。

合わせてオリビアRに関するメタデータも公開する。全てのRにはアセットタイプとそれに関する終了時刻がメタデータとして存在する(例:Rを金曜日のマーケットの終値の日本円に関連付ける)。Rは33バイトの長さなので、メタデータRより大きい可能性がある。

Rは公開され既知であるため、署名者がsを計算するより前に任意のmについてsGを計算することができる。

コントラクトの作成

コントラクトはブロックチェーン上に単一の出力として存在し、その出力には契約期間中にコントラクト実行時に支払われる全ての資金がセットされている。(この出力は多くの場合最適化されたものになるが、最適化については後述)

コントラクトをセットアップする前にアリスとボブはまずお互いを見つけて、コントラクトの条件に合意する必要がある。

アリスは金曜日に日本円を買い、ボブは日本円を売る決済を行う。

契約を結ぶには、両者のマルチシグアドレスに資金を入れる(funding output)必要がある。このマルチシグに資金を投入する仕組みはLightning Networkのチャネルのセットアップと大きく変わらない。アリスとボブはマルチシグにロックされた資金のOutPoint(txidと出力のインデックス)に同意する。このトランザクションをブロードキャストする前に、そのトランザクションを入力にした後続のトランザクションを作ることもできる。

続いて、アリスとボブはfunding outputを使用する多数のトランザクションで構成されるコントラクトの作成に移る。この出力は当然ながら1回しか使えないので、作成したコントラクトを構成するトランザクションの内1つのみがブロックチェーン上に記録されることになる。アリスとボブがこの段階ではどのトランザクションがブロードキャストされるものかまだ知らないので、お互い全てのトランザクションについて署名し保存しておく必要がある。

クロージングトランザクションは、契約当初とは異なるクローズ時の価格に基づいて作られる。今回の例では、価格は日本円の価格で、satoshiで表される。そのため、アリスとボブは終値の異なる何千ものトランザクションを作る。

現在開発中のLightning Networkのソフトウェアと同様、当事者はコントラクトの状態について合意するが、双方が作成した各トランザクションを保持する。アリスはボブが署名したトランザクションを保持しており、このトランザクションは2つの出力を持つ。1つはボブへの支払いで、もう1つはアリスもしくはボブへの支払い(アリスが不正を働いた場合のみボブがアリス分も入手する)の出力になる。ボブはこの逆で、アリスが署名したトランザクションで、1つの出力は直接アリスへの支払いをする出力で、もう1つはアリスもしくはボブのどちかに支払うスクリプト(ボブが不正を働いた場合のみボブ分もアリスが入手する)の出力になる。

Lightning Networkではこれらのスクリプトを使ってペイメントチャネルの一貫性を維持し、古い状態のトランザクションをブロードキャストすると相手に全ての資金を支払うことになる。DLCでも同様の仕組みを使うが、ペイメントチャネルと異なるのは、アリスとボブがお互いに過去のコミットメントに使ったシークレットを明らかににするのではなく、オラクルとであるオリビアがシークレットを明らかにする。

コントラクト内のトランザクション

アリスとボブは何千もの署名済みのトランザクションを持ち、それは両者のコンピューター内に保存されている。これらのトランザクションは、入力にfunding outputを持ち、出力は取引相手へのP2PKHと独自のスクリプトハッシュの2つで構成されている。このスクリプトはLightning Networkのチャネルで使われているのと同じスクリプトで、アリスが持つスクリプト

PubAi ∨ (PubB ∧ TimeDelay)

で(∨はorで∧はand)、ボブが持つスクリプト

PubBi ∨ (PubA ∧ TimeDelay)

後者のスクリプトでは、PrivBiを持つユーザーはすぐに出力を使用でき、 PrivAを持つユーザーはある時間経過したら出力を使用できるようになる。Lightning Networkではこれを以前の状態のトランザクションがブロードキャストされた際にそれを取り消すのに使われるが、DLCではオラクルが間接的に正しい状態であると承認したトランザクションのみをユーザーがブロードキャストするよう強制する。

アリスが保持する↑のスクリプトのPubAiはアリスの公開鍵にsiGを加算した以下のスクリプトになる。

PubAi = PubAlice + siG

ここでsiGは↓

siG = R - h(i, R)V

ボブはこの逆で、一定時間待ってアリスの公開鍵PubAに送る出力と待ち時間の無い以下の出力宛に送るトランザクションを保持する。

PubBi = PubBob + siG

検証者はポイントsiGを計算できるが、与えられたiだけではsiの値が何かは分からない。
リビアが署名すると、アリスとボブが既に計算している点の離散対数が明らかになる。

オラクルの署名

リビアの仕事は簡単で、金曜日にマーケットが閉じるのを待って、日本円の終値を観測し予めコミットしたnonceを使ってその数値に署名する。

オラクルは観測した価格をメッセージmとしてセット(この例では1050)し、以下の計算をする。

s = k − h(1050, R)v

水曜日に1000satoshiだった日本円が金曜には1050satoshiに上昇したことになる。
(実際のmの値はハッシュ値だが、ここでは分かりやすくするため、数値をそのまま使用)

ここでアリスとボブが以前トランザクションの鍵を導出するために使ったs1050が明らかになる。

s1050G = R − h(1050, R)V

オラクルがs1050を明らかにすると、PubB1050秘密鍵b + s1050と同じなのでボブはトランザクションの署名に必要な秘密鍵を知り、アリスも同様にPubA1050秘密鍵を知ることになる。

コントラクトの実行

アリスとボブはTX1050をブロードキャストすることで、金曜日の終値に基づく正しい状態でコントラクトを一方的に閉じることができるようになる。トランザクションがブロードキャストされると、すぐその出力の資金を自身のコントロール化のアドレスに送るトランザクションを作成する。
結果、2つのオンチェーントランザクションブロックチェーン上に公開されるが、直接両者の公開鍵ハッシュにコントラクトの実行トランザクションと同額を分配する新しいトランザクションTXggを両者で合意して作成する方がより効率的になる。
またスクリプトハッシュに定義されている期間(TimeDelay)より前に資金を回収しないと相手に全て資金を持っていかれるので注意すること。

アリスとボブのいずれかが途中で実行トランザクションをブロードキャストしたり、(TX1050でなくTX950とか)間違ったトランザクションをブロードキャストすると、スクリプト内のPubAiやPubBiの条件で資金を償還することはできなくなり、もう1つの条件であるTimeDelay経過した後に相手方の公開鍵ロックの条件でしか資金を償還できなくなる。つまり契約ルールに違反すると全ての資金が相手に渡ることになる。

オラクルへの信頼リスク

オラクルであるオリビアが価格を誤って報告する可能性がある。オラクルが誤った報告をした場合、システムの全ユーザーはエラーを識別し、オラクルの使用を停止できる。もしオリビアが2つの異なる価格を公表した場合、オリビアの持つ秘密鍵と二重報告をしようとした特定のコントラクトkの値が明らかになる。
またオリビア自身が契約相手(例えばアリス)だった場合、オリビア秘密鍵を明らかにすること無く、任意のコントラクトを実行できる。オリビアが実行したいTX2をブロードキャストし、続いて以下を計算し

PrivA2 = PrivA + s2

PubA2の出力に署名する。オリビア秘密鍵vを維持しながら、s2を公開せずに署名することになる。これは検知が可能で、詐欺にあったボブは、他の全ユーザーにオリビアのコミットメントと署名の使用を中止するよう、詐欺の証拠を提出することができる。オリビアは参加しているコントラクトを1つ騙すことができるが、同時に全てのユーザーの信頼を失うことになる。

リビアRを公開した後、終値を報告する前にいなくなることも考えられる。こういうケースに対応するため、予めコントラクトが実行される予定日の数日後であれば(タイムアウトすれば)、マルチシグにロックされた資金をそれぞれに払い戻すトランザクションを作っておく必要がある。

複数のオラクルを使用することもできる。2つのオラクルの署名が必要な場合であれば、各当事者は単純にオラクルのsGポイントを各公開鍵に追加する。複数のオラクルを使用することで、オラクルの不正リスクを減らすことができるが、複数のオラクルが報告するデータの一致しないパターンのリスクについて考慮する必要がある。例えばオラクル1は終値を1050と報告し、オラクル2は1049と報告した場合、実行トランザクションを安全に使用することはできず、タイムアウト後に両者に払い戻しが行われる。

もう1つのリスクは、コントラクト内の実質的に全てのポジションを失った当事者が、正当な所有者への資金の移動を遅らせるため、わざと無効なコントラクト実行トランザクションをブロードキャストする可能性があることである。最終的には正しい所有者が資金を回収することができるが、タイムアウトの期間まで待たなくてはならない。ただ、実際に行われることはほとんどないケースだと思われる。

最適化

いくつかの最適化によりデータ量と計算量を減らすことができる。システムを実装する際にはまた多くの最適化方法が見出されると思うが、以下にいくつかの基本的なアイディアをリストアップする。

R値の基数と指数

アリスとボブはコントラクトを作る際、オリビアが署名する可能性がある全てのメッセージmiについて署名を互いに送信し保持する必要がある。価格の候補は非常に多く、何千もの署名を検証し保存する必要がでてくる。ほどんどの場合、ノックインとノックアウトの価格が、それを下回るもしくは上回るとどちらか一方がコントラクトの全ての資金を受け取ることになる。例えば日本円の価格が10 satoshi以下になった場合、アリスとボブは価格が4 satoshiであろうが5 satoshiであろうが関係なくボブが全ての資金を手にすることに合意する。同様に価格が5000を超えると、それが6000か7000かに関わらず全ての資金をアリスが手にする。

価格のレンジは数桁以上にわたるため、アリスとボブはこれらの極端な範囲での取引を少なくすることで、ノックインとノックアウトの価格を最適化することができる。これはオリビアがRmantissaとRexponentという2つのRの値にコミットすることで可能になる。オリビアは価格を表す2つのメッセージに署名することを約束する。1050に署名する代わりにRmantissa仮数部)を使用して.050に署名し、Rexponent(指数部)を使用して3に署名する。小数点の基数と仮数には暗黙的に1が指定される。1.050 ∗ 103 = 1050

アリスとボブはsmantissaGとsexponentGのポイントを加算することで、同じようにトランザクションを構築する。オリビア仮数と基数のメッセージのペアに署名すると、sの値が明らかになる。Rexponentが4,5,6のような場合であれば、仮数部は無視できるレベルの数値になる。その場合Rexponentのみでコントラクトを作ればいいし、詳細な値まで含める場合はRexponentとRmantissa両者を必要とするコントラクトになる。どちらの粒度でコントラクトを作成するかは、コントラクト作成前に合意しておく必要がある。

↑の例ではRが2つだが、Rの数を3,4と増やすことで粒度を細かくすることもできる。また10は最適な基底ではなく、基底には2を使うのが良い選択になるだろう。

チャネル内のコントラクト

コントラクトはLightning Networkのチャネル内で作ることもできる。コミットメントトランザクション内の直接の送金とHTLCの送金の2つの出力に加えて、コントラクトの出力を追加する。あとはコントラクトの結果についてオンラインで両当事者が合意すれば、コントラクトの実行結果に従いお互いの残高を更新した新しいコミットメントトランザクションを作成し交換すれば良い。取引相手が非協力的な場合は親チャネルを閉じ、オラクルが提供した値を使ってすぐにコントラクトを終了させることができる。

所感

  • 予測される未来の値をベースにした決済を執行できるという意味で、暗号通貨を使用するユースケースの拡張につながる面白いアイディアだと思う。
  • トラストポイントとしてオラクルの存在があるモデルで、オラクルが厳密に不正をできない仕組みにはなっていないが、不正の検知は可能で、不正を行ったオラクルは以降信頼されずオラクルとして使われることはない。

*1:外部データを提供するエンティティのこと

*2:http://eprint.iacr.org/2017/394.pdf

Compact Blockバージョン2

BIP-152として定義されているCompact Blockについて以前ブログを書いた↓

techmedia-think.hatenablog.com

簡単にいうと新しいブロックデータを受信する際、既存のblockメッセージを使ってデータを受け取ると、そのメッセージにはブロック内の全トランザクションのRAWデータも含まれている。ただ、ブロックに入っているトランザクションの大半は既にメモリプールにあるので、重複したデータを受け取ることになり、ネットワークの帯域幅的にも無駄なので既に保持しているトランザクションデータについては受け取らないようにしようと言うもの。詳細な仕様についてや↑参照。

このCompact Block、久しぶりにBIPを見たらバージョン2が追記されてた↓

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

内容的には、SegwitのトランザクションをサポートするCompact Blockをバージョン2としてるみたい。

バージョン2の仕様

バージョン2はほとんどバージョン1と同じだが、Segregated Witness(Segwit)のトランザクション(BIP-141、BIP-144)をサポートする。バージョン1からの変更点は以下の通り。

  1. sendcmpctメッセージの2つめの値(= バージョン)に1ではなく2をセットする。
  2. cmpctblockメッセージとblocktxnメッセージ内のトランザクションには、BIP-144で定義されているwitnessデータを含むトランザクションフォーマットが適用される。(このフォーマットはinventory typeにMSG_WITNESS_TXを指定したgetdataメッセージのレスポンスで使われている。)
  3. cmpctblockメッセージとgetblocktxnメッセージにセットされるShort transaction IDsは、バージョン1と同じプロセスで計算されるが、txidではなくBIP-141で定義されたwtxidが使われる。またノードは通常コインベーストランザクションは持ってないので(生成されたブロックのコインベーストランザクションは基本的にその時点で他のノードのメモリプールは存在しない)、当然それはShort transaction IDsに含まれるが、そのトランザクションはBIP-141で定義されているwitnessフォーマットのトランザクションエンコードしたトランザクションから計算されなければならない。
  4. cmpctblockメッセージを期待するinventory typeにMSG_CMPCT_BLOCKを指定したgetdataメッセージがレスポンスとして帰ってこず、非Compact形式のブロックを要求するblockメッセージは、cmpctblockメッセージをエンコードするのに使われたバージョンが2であればwitnessを含めてエンコードし、バージョン1であればwitness無しでエンコードされる。

バージョン2の参照実装

https://github.com/bitcoin/bitcoin/pull/8393

Bitcoinのソフトフォークとして導入するConfidentail Transaction

昨年Bitcoinの開発メーリングリストにポストされたConfidential TransactionをBitcoinのソフトフォークとして導入する話↓

[bitcoin-dev] Confidential Transactions as a soft fork (using segwit)

Confidential TransactionについてはBlockstreamが開発したサイドチェーンの実装であるElements Alphaに実装されている。
そもそもトランザクションの出力に量の代わりにコミットメントと呼ばれる楕円曲線上の点(公開鍵)をセットしたりrange proofや明示的な手数料など、既存のBitcoinの仕様とは異なるのでサイドチェーンによる拡張として実装されていた。

これをBitcoinにソフトフォークしようというのが↑のポスト。

トランザクションの拡張

このソフトフォークではSegwitの新しいwitness programを定義することで、Confidential Transactionを評価する仕組みを導入しようとしている。

既存のSegwitでは、トランザクションは以下のデータで構成される。

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

もともとSegwitでは入力のscirptSigをwitness scriptとして入力の外に分離する仕様だが、このソフトフォークではさらに、出力にも入力と同様のwitness領域を設けるようトランザクションの構造を拡張する↓

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

各出力の既存のBitcoinの量のフィールドには0をセットし、量の代わりになるConfidential Transactionのペダーセンコミットメントとそのrange proofをwitnessOutsにセットする。このソフトフォークに対応していないノードでは入力と出力のビットコインの量が0に見える。

こうやって、既存のトランザクションのデータ構造にないコミットメントとrange proofを格納する場所をトランザクションの別の領域に確保する。

コミットメントとrange proof以外にも、Confidential Transactionでは手数料を明示的に指定する必要がある。このため誰もが使用可能な(anyone-can-spend)出力(GCTXO)をその手数料用に作成する。

新しいトランザクションタイプ

Confidential Transactionは、blindingunblindingconfidentialという新しい3つのタイプのトランザクションを組み合わせることで実現する。また、これらの新しいタイプのトランザクションを含むブロックは、そのコインベーストランザクションに特殊な出力を含み、さらに追加で新しい特別なConfidential base transactionを含める必要がある。

Blinding Transaction

Blinding Transactionは通常のUTXOを入力にとり、出力が秘匿化したUTXOになるトランザクションで、以下のような構成になる。

Ins: 全て秘匿されていない通常のUTXOを参照する
Outs: 
  0..N: 新しい秘匿対象の出力
    量: 0
    scriptPubkey: `OP_2 <32バイトのハッシュ値>`
    witnessOut: `0x{petersen-commitment}> <0x{range-proof}>`
  最後の出力: 
    量: 0
    scriptPubkey: `OP_RETURN OP_2 {blinding-fee-amount}`
Fee: 全入力のコインの合計

全出力の量は全て0になるため、通常のBitcoinトランザクションとして考えると、全ての入力のコインが手数料になることになるように思えるが、このコインは秘匿化された手数料を除いて全て、このトランザクションが含まれるブロックのコインベーストランザクションの特殊な出力(GCTXO)にセットされる。

出力の最後のスクリプトはこのトランザクションがBlinding Transactionであることを示すマーカーにもなる。ソフトフォークがアクティベートされた後は、マイナーがBlinding Transactionのコインをコインベースの出力に入れずに自身のアドレスに送るようなスクリプトを書いた場合、そのブロックは無効なブロックとして扱われる。

コインベーストランザクション

Blinding Transactionを含むブロックは、Blinding Transactionの全ての手数料を新しい出力(Confidential base transaction)に送金する必要がある。GCTXOのコインは同じブロックのConfidential base transactionでのみ使用可能なためscriptPubkeyは重要でなくOP_TRUEとかで良い。

Unblinding Transaction

Blinding Transactionは秘匿化されたUTXOを入力にとり、出力が通常の秘匿化していないUTXOになるトランザクションで、以下のような構成になる。

Ins: 
  prev: CTXO[n]
  scriptSig: 空
  witnessIn: `<署名> <0x{redeem script}>`
Outs: 
  0..N: 
    量: 0
    scriptPubkey: `OP_RETURN OP_2 {アンブラインドするコインの量} {p2sh-destination}`
    witnessOut: 空
  最後の出力: 
    量: 0
    scriptPubkey: `OP_RETURN OP_2 {unblinding-fee-amount}`
Fee: 0

このトランザクションは入力のUTXOセットから秘匿性を削除する。このトランザクションの出力自体は全てOP_RETURNであるため使用可能な出力ではないが、同じブロックにGCTXOを償還するマイナーが作成したConfidential base transactionが含まれる。

Confidential transaction

Confidential transactionは秘匿化されたUTXOを入力にとり、それを別の出力に秘匿化されたまま送金するトランザクションで、以下のような構成になる。

Ins: 
  prev: CTXO[n]
  scriptSig: 空
  witnessIn: `<署名> <0x{redeem script}>`
Outs: 
  0..N: 新しい秘匿対象の出力
    量: 0
    scriptPubkey: `OP_2 <32バイトのハッシュ値>`
    witnessOut: `0x{petersen-commitment}> <0x{range-proof}>`
  最後の出力: 
    量: 0
    scriptPubkey: `OP_RETURN OP_2 {confidential-fee-amount}`
Fee: 0

入力、出力とものに全てのコインの量は0で、誰もが使用可能なスクリプトになっている。ただこのトランザクションはコミットメントとそのrange proofが有効な場合のみ有効なトランザクションと判断される。このトランザクションのマイナーへの手数料は、最後の出力で表現されている。

Confidential base transaction

上記トランザクションのいずれかを含むブロックでは、そのブロックの最後のトランザクションとして以下のトランザクションが含まれる。

Ins: 
  GCTXO[last_block],
  GCTXO[coinbase]
Outs: 
  0: GCTXO[current_block]
    量: {last_block + coinbase - unblindings}
    scriptPubkey: `OP_TRUE`
  1..N: 
    量/scriptPubkey: このブロック内でアンブラインドしたいトランザクションの出力

このトランザクションの入力にセットするのは、以下の2つのGCTXOになる。

  • GCTXO[coinbase]: このブロックのコインベーストランザクションのGCTXO。コインベースのGCTXOには、そのブロック内のBlinding Transactionで秘匿化した全てのコインの量がセットされている。
  • GCTXO[last_block]:この前のブロックのConfidential base transactionの0番目の出力(そのブロック内の秘匿化された量からアンブラインドしたコインを除外した量)のコインがセットされている。
Fee: このブロック内の全Confidential transactionの`OP_RETURN OP_2 <手数料>`で表現される手数料の合計

このトランザクションはブロックを作ったマイナーによって作成され、ブロック内の全Confidential Transactionの手数料を入手する。

まとめ&所感

ざっと見ただけだと分かりにくいけど、秘匿したコインがブロックチェーン上で無くなることなくブラインド/アンブラインドできるようになってる。

簡単に言うと、コインを秘匿化したい場合はBlinding Transactionを作成するが、このトランザクションの出力は全て量が0(秘匿化された量はちゃんと別にある)になるので一見コインが全て手数料になってしまうように思えるが、このコインは全てBlinding Transactionが含まれるブロックのコインベーストランザクションの特殊な出力(GCTXO)の量として加算される。そしてこのGCTXOのコインは、同じブロックの最後のトランザクションであるConfidential base transactionの入力になる。

秘匿化されたコインは基本的に全てGCTXOが保持することになり、ブロック内にUnblinding Transactionが存在する場合は、アンブラインドするコインをConfidential base transactionの1番目以降の出力にそれぞれ通常のUTXOとしてセットする。こうするとConfidential base transactionの0番目の出力(GCTXO)にはコインベースのGCTXOからアンブラインドした量を引いいた量がセットされる。がもう1つ前のブロックのConfidential base transactionの0番目の出力のコインの量も加算される。

こうやってブロックチェーン内のコインベースと最後のConfidential base transactionのGCTXOで秘匿化されたコインのバランスを担保している。

  • Confidential Transactionを新たなwitness programを定義して(なのでSegwit前提)Bitcoinのソフトフォークとして導入するアプローチ。
  • 既存のSegwitのwitnessは入力の署名を分離するが、出力用のwitness(witnessOuts)を用意し、そこにコミットメントとrange proofを格納する。
  • Unblinding Transactionの出力のscriptPubkeyが全てOP_RETURNなのは如何なものか。
  • 既存のBitcoinプロトコルに存在しない秘匿した量をどうやって整合性とるのか疑問だったけど、コインベーストランザクションとConfidential base transactionでその整合性をマイナーに取らせていて、秘匿された量が勝手に消えない仕組みになってる。この辺はよく破綻しないよう考えたなー。
  • 結構マイナーがやらないといけないこと多く、それに対するインセンティブも無いので、これをソフトフォークとしてアクティベートするのは難しいんじゃないかな。
  • MLにはこのポストがあるだけで、特にレスは付いて無いようで、このソフトフォークの仕組み自体は現在進んでいないのか?
  • 量の代わりにコミットメントをセットすることで出力のサイズが28バイト増えるのはまだしも、witnessOutsを導入するとは言えやはりrange proofの2.5KBというサイズがネックだと思う。

Confidential Transactionのrange proofの仕組み

techmedia-think.hatenablog.com

↑でConfidential Transactionのコミットメントの作成と検証をしたので、続いてrange proofについて見てみる。

Confidential Transactionではトランザクションの各出力にコインの量ではなくコミットメント(楕円曲線上の点)がセットされるようになり、それにより取引されるコインの量が秘匿され、検証者は楕円曲線上の点を計算することで入力と出力のコインのバランスが取れていることを検証する。

ここで1つ問題になるのが、コミットメントの計算時に非常に大きな値を設定することを値をオーバーフローさせ、負の値として動作する場合。この場合マイナスのコインによりバランスの計算が行われるため、結果的に何も無いところからコインを生み出すことができてしまう。

この問題に対応するため、コミットされた各出力の値が、正常な(0〜264)範囲の値であることを証明する仕組みが必要となり、この証明をrange proofと呼ぶ。

コミットメントとリング署名

まずコインの量へのコミットをどうするかについて説明する。例えばコインの量が1である場合、その量へのコミットは、まずコミットメントCを使って以下のC'を計算する。

C' = C - 1H

元々のCが以下だとすると(xがblinding factorで1がコインの量)

C = xG + 1H

C'は結果的に↓になる。

C' = C - 1H = xG + 1H - 1H = xG

ここでC'について、その秘密鍵はblinding factorであるxなのでHが無くなった今、xを使って普通にsecp256k1の署名を生成でき、公開鍵C'に対する有効な署名が提供されれば、Cがコインの量1に対するコミットメントであることの証明になる。

ただ単純にこの証明をしては、コインの量が明らかになってしまうので意味が無い。つまりコインの量を秘匿した状態でも実際のコインの量に対するコミットができる仕組みが必要で、そこで利用されるのがリング署名になる。

リング署名は、2つ以上の複数のキーペアで構成される公開鍵のグループの中から、ある1つの公開鍵に対応する秘密鍵を使って署名をするが、グループ内のいずれかの公開鍵に対応する秘密鍵で署名されたことは分かるが、どのキーペアで署名されたかどうかを特定することはできないといった署名スキームになる。

Confidential TransactionではC'のような量へのコミットメントと、リング署名を組み合わせることで、量を秘匿したまま量が有効な数値の範囲内であることを証明している。

量の異なるC'のような量のコミットメント(公開鍵)を沢山作って、その中のいずれかに実際コインの量が1つ含まれている。blinding factorとコインの量を知っている取引の当事者であれば、正しいコインの量のコミットメント(公開鍵)に対応する秘密鍵でリング署名を生成でき、その署名をrange proofとしてトランザクションの出力にセットすることで、その出力のコインは正しい範囲内の数値であることを保証できるようになる。

これを単純に実装しようとすると、Rubyring_sigというCryptoNoteで定義されたワンタイムリング署名を実装したgemを使うと以下のように量へのコミットメントの作成と、署名及びその検証ができる。

require 'ecdsa'
require 'securerandom'
require 'ring_sig'

# G及びHは前回のcommitment作成記事のコードと一緒

hasher = RingSig::Hasher::Secp256k1_Sha256

# blinding factor
x = 1 + SecureRandom.random_number(G.order - 1)

# 実際のトランザクションの出力にセットするコミットメント
output =  G.generator.multiply_by_scalar(x) + H.multiply_by_scalar(1000)

# 1000の量にコミットするコミットメントは output - 1000Hになるので
amount_commitment = output + H.multiply_by_scalar(1000).negate

# ↑は元がxG + 1000Hなので、amount_commitment=(x)Gになるため署名に用いる秘密鍵は↓
key = RingSig::PrivateKey.new(x, hasher)

# 偽の量へのコミットメントを32個ほど用意
foreign_keys = 32.times.map{
  random_amount = SecureRandom.random_number(10000000) # 10000000以下で乱数を生成
  commitment = output1 + H.multiply_by_scalar(random_amount).negate
  ECDSA::Format::PointOctetString.encode(commitment).unpack('H*').first
}
foreign_keys = foreign_keys.map{|s| RingSig::PublicKey.from_hex(s, hasher)}

# 秘密鍵で署名
sig, public_keys = key.sign('message', foreign_keys)

# 署名の検証
sig.verify('message', public_keys)
=> true

上記は33個の公開鍵を使ったリング署名だが、この署名値(=range proof)sigの値は以下のように2317バイトと、普通のECDSAの署名などに比べてかなりサイズが大きくなる。

308209090421023d249ef23a11e1fce8c1c539128d641b7de92edea24ceda1b17277d496ccb7e4308204720221008ebbc436681a401b4dfeadc3ec916a37545479286d1ad142a543ec6293103257022100899a7a74d109907bb70dd9588a72b125a0cb6d12e001f505910a03f1973406e802200a0aebe3c81d2a5ec8d790476c146fe567154f0f173d25479174699e2af92913022100aaafff08304a2c589f0df9b13a3b047cba57acd714184ac56173831fa8f9f73c02203a1d0afe2e50ec05ca701c88d3555700f87fa79d47bc8e7c8be1852c2e64d30e0220369812a49f7f6c649483863fcfe2c0881c66ae59c042e8f2c320815c20816efa02204935d2790b2af6a99fa292f308aee25cf60c506f2327b77eb77abde830b6e352022100d8cff770a6917aac410ff4194256efc5c59838dd78d031db537bc4f5e12a38fc022100d7ae003935b1a6d274c2601c6aaa75e0cc9e3fd6c4a1d8846968dc365a107484022075c477ad4793db07bceb6c378314df3b1039b856999f6d6f065f91f368b71347022100b4d0dac8d1b8b08c022845812c8573462fe419f3adc63b3c9fb1b42b2741e5bd022100e8e2776098157914ec325df278224fd397bb5a69085bbf4de5ae0ffdf0bf2532022100fffe317a93d788f39f4cebafa87f4d58af95a7c61e684738ec8dbdf1a2485011022100bc86a5f18a42a8faa28d0049ae3a39bccd2857b2d0477a84fd4f5f30fe01b6f502200dd450ca1ba49a7627eff2cd92c57a529d1fbee11c04c34a88b333245e2ab816021f65594deaa1fbdf1e8e964e9a8d65a82107af6e9bd81016eb09a0209c1260ab0220590e85563c7bb046cf1fe4243b275b58c0863989afd037ad7b5574dea835340c022025e0a374d4132ce5c229b39120fa3c3ff429bbf8f3765c9a0fbbd4ea7144ea5e022100d0bd1c0a8ca20d0ab80d7f9f65d4e3764325d2a4982d5d638b00aa679f1619fd022076f7fac091ca610aef6075f147249a6eca7fa9134727acedd00b8b02ddc33526022100e92f759ba305852d1700fb695c5aa602f644cf6f3f3aff02041ea38cf5e8f800022100aa1e2fb3fed1da7a803f9295701758f35f73f2ea5aa1b8bc0d6d3c05ac1078a3022100d887f4cd108e0f74c3eff611f4d2119338cda40e0aa8af997d48ce9c813189ec02207d091e00e372097c8875945164cc58083b4e49d73528d087138ce4faa500811902210080d14fa2b315cea7bbeaf6c9f0054495293de5ff1975e27b3276f10a96e6dac702205ec90a62465e1c1d46a3528a7650b084661acc5b43b0e8b0dfa5c6a452dd6f05022100a3664817eab64c4a9f576918890f70953d9fe8e4ecfbe91f26a6df221f4cfde202201c61d9b568413e18c361be555bafbed1ba923e32d01d494d8df33fc75bbfda9f022100a5dfd562402d24102bd97179ff1287b30912d70f958f03abeaf74061b6ebcb3c02201c3ca327707dd31dfc82b66e5670d43a9205096af0d60e39d5b2c2fe53777978022100d6792ee73368a6e0096b5e0ca4722aafdf0e33155285ce02b99d15851b4a3f1802202dc698c31894b00569945aa38b6862df0c6c95bb488af7c61830e28e50fbaf060220647fe6e12ab00b2182b7a7d049c9d836426497a77143b445a0c11dba45fccc973082046c022100923f807c4dca60d9f3f4b0d066a0f4921dbfb630e2e7941e7bcbde4e1ee9355102203cc56b92661b414d6180a6c7f8a28454098f95a40e01803389744dabb4d5a1ca022032ab2f64698cf78d58a50e4dec775082233bf87a82cbd29fa23445736ab45df7022042ad8bca0b6ff9271288765aa96b097746376c8b3a54cb9dbe9dee30106f69d3022100df3042a03714e8121cf006bc5328ed6241ab135e9084304856c67d5c4490b7f6022034d3245d70a993f15a55d9a718de528b3a1d4ba04b4a2ebd6899d55dfad3abce02206de7ef2041f6aa53725cc5d214725165ce602efb91cabf7ffb8a77469c90d214022020b2908567f9e8c97a8701ff91d1b18bc69f03531ac041656b14a613d63c50300221009afcf632c51788898389c2c0919828f2b86f218a763a3d6e074de61af2168963022100d3c38252bc504d8a9b141af3aa71acd0b39ee9d7b699f465120fe1dc6a15b6d50220505457bb089893986b4dd6ef561929b7703ec5fb2fe614b0983310775aa0dbc3022100cbfe697aa76c70a26b43e755afa1abb8ddff76b7c8d911a82c3d9a77116b1e1d022100e94addabbdc03e51cdc8d9369a6a2758d487497fcfa8cf8b5f2544b5d2116743022076c0368f3dcc63225d5d78bfadd21b2f516b52190ea76c7dc69de7865e1f30b402203886f5031eae7b7dfdc5bef8bc34d09f505662ac8e4970bde499e30cecd785f502207d9e3c25ab075114033744302c9f32f67f63396860a3a0b4c6614a365ff1588302205e112337977b822304abdbe00a12554a3eb7a0d8c77af569e66bb859f4702996022049181783f268be175ad6e23ff39b2c68fbf00e8adae2bf7a7405b39018a7f87402206bc5fc87deceb088c112f88e1af8392439861dff8c3bb78c1ae5426f64f4226c022079756ac5f53bc6e35a9690ddb5ece09fb6850cb544be30958207b7be284486e7022065ade8c6e7d395d9c4668a7008d4b6420e57608539730ba86672d79362cf146a02203c86f08d43447868e86e655612e8b437fd79edd3f191121214c424ec55cc03a2022100ac4dc34a34f3419ec7aaebfc47d72bb21f43b40ba7fc44a71cd4e32dfc3cc4dd022100aae8a64cac6a75c1a1eda86cbb482d86dee07433db89e65344e2b884c7feb9a70220540af0a2d9ac11e688173654b5e61dcb340d4a4850166cc07978c0d8c2251da9022100d5b9b99b25fcf1f1fbb08e08e7852b14ea5065a11f88b63b0233df6c90e4e8a5022100e0e6ca4919e767e34cecbb23acfbbcca1bc4194fdf6f6b01312ce062911a46bc02204f93b2c204ee20d3dd15e2d69b78128206e56eb038893dc070721657b3b7c95a02201986320ce60cf364f38625e1ddafe2643919c62f69809f74c72e2212b7f49df4022042d02beb2d14f47440ce131feb27038d55318e9331d633d9056c6ec9e7c3914702207aa655bb5b00cad84ad2050bbed33759c8de5b3d7d9f0e0a637566cf52315dcf021f4f4e660a9753b2960c9fe9ae0160bf7438eadd19ed99ed4cf113ef1e4acbf7022100ffa030ddef18eef1783a9661b78dd0444af40d93004b921cfba641d3d531e3c5

これは候補となる公開鍵の数が増えれば、その分署名値のサイズも増えることになる。

また↑はシンプルなrange proofの表現をしただけで、実際のConfidential Transactionでは、量を直接指定するのではなく浮動小数点を使って表現したり、Gregory Maxwellが新しいリング署名のスキームとしてBorromean Ring Signaturesを提案していたり、様々な最適化がさらに加わっている。

ただ、それでもrange proofのサイズは2.5KBほどらしく、出力1つにつき2.5KBのrange proofがあると考えると、ブロックチェーンのデータも結構なボリュームになると思われる。
アダム・バックの2KBまで削減できるという発言もあるが、それでもまだ大きなサイズだ。

Confidential Transactionのcommitmentを生成してみる

以前、Confidential Transactionのホワイトペーパーを読んだ↓

techmedia-think.hatenablog.com

Confidential Transactionでは、トランザクションの出力にコインの量をセットする代わりに、Pedersen commitmentを楕円曲線に適用したコミットメント(この場合は楕円曲線上のある点)をセットすることで、トランザクションで取引されるコインの量を秘匿している。

ホワイトペーパーを読んで概要はなんとなく分かったまま放置してたので、実際にコードで検証してみる。

commitmentの生成

このコミットメントは

commitment = xG + aH

の式で定義され、xが量の秘匿のために用いられるblinding factorでいわゆるシークレットに該当する。実際に取引するコインの量はaになる。
この式はxGからなる楕円曲線上の点と、aHからなる楕円曲線上の点を加算した新たな点を算出し、この点をコインの量の代わりにトランザクションの出力にセットしている。

Hの算出

ここでG楕円曲線のベースポイントで、HGエンコードして生成するもう1つのベースポイントである。Hは以下の式で楕円曲線上のX座標を計算し、そこからY座標を求めることで算出でき、これも楕円曲線上の点になる。

HのX座標 = SHA256(ENCODE(G))

Rubyecdsa gemを使うとこのX座標は以下のように算出できる。

require 'ecdsa'
require 'securerandom'

G = ECDSA::Group::Secp256k1

encoded_g = ECDSA::Format::PointOctetString.encode(G.generator)
coordinate_x = Digest::SHA256.hexdigest(encoded_g).to_i(16)
=> 36444060476547731421425013472121489344383018981262552973668657287772036414144

X座標が分かれば、secp256k1の楕円曲線上のY座標は以下のように算出できる。ただ楕円曲線はX軸関して対称であるため、Y座標の候補は2つある。

P = G.field.prime

coordinate_y1 = G.field.power((coordinate_x**3 + 7) % P, (P + 1)/4)
=> 93254584761608041185240733468443117438813272608612929589951789286136240436011

coordinate_y2 = coordinate_y1 * -1 % P
=> 22537504475708154238330251540244790414456712057027634449505794721772594235652

Confidential Transactionを実装しているBlockstreamのサイドチェーンElements Alphaのソースを確認すると後者のY座標の値が使われていたので、そちらを使用して、Hの楕円曲線上の点は以下の座標になる。

  • X座標:36444060476547731421425013472121489344383018981262552973668657287772036414144
  • Y座標:22537504475708154238330251540244790414456712057027634449505794721772594235652
# 2つめのベースポイントH
H = ECDSA::Point.new(G, coordinate_x, coordinate_y2)

ちなみにこのHをGから導出する固定値でなく、アセットを表す識別子としてアセット毎に任意に生成できるようにしたのがConfidential Assets

Hの算出方法を確認するため↑の計算をしたけど、Hは固定値なので以下のように定義してそのまま使うのでも良い。

# ECDSA::Grou::Secp256k1 = G とgenerP = G.field.primeatorのポイントが違うだけのGroupを定義
module ECDSA
  class Group
    Secp256k1H = new(
        name: 'secp256k2',
        p: 0xFFFFFFFF_FFFFFFFF_FFFFFFFF_FFFFFFFF_FFFFFFFF_FFFFFFFF_FFFFFFFE_FFFFFC2F,
        a: 0,
        b: 7,
        g: [0x50929b74_c1a04954_b78b4b60_35e97a5e_078a5a0f_28ec96d5_47bfee9a_ce803ac0,
            0x31d3c686_3973926e_049e637c_b1b5f40a_36dac28a_f1766968_c30c2313_f3a38904],
        n: 0xFFFFFFFF_FFFFFFFF_FFFFFFFF_FFFFFFFE_BAAEDCE6_AF48A03B_BFD25E8C_D0364141,
        h: 1,
    )
  end
end

コミットメントの作成

blinding factorをx、取引するsatoshiの量をaとした時、そのコミットメントは以下のように算出できる。

# blinding factor
x = 1 + SecureRandom.random_number(G.order - 1)

# コミットするコインの量
a = 10000

# コミットメント
commitment = G.generator.multiply_by_scalar(x) + H.multiply_by_scalar(a)

どうしてGHの2つのベースポイントが必要かというと、もしHでなく同じGを使うと、blinding factorの値によって実際のコインの量をなんとでもコントロール出来てしまうからで、そのためblinding factorとコインの量を表現する際のベースポイントは必ず別のポイントである必要がある。

秘匿された量の検証

Confidential Transactionでは、↑のように出力にセットするのが明示的な量ではなく、コミットメント=楕円曲線上の点になる。この場合実際にいくらの量がやりとりされているかは、blinding factorの値を知る取引の当事者しか分からず、blinding factorを知らない他の参加ノードは実際の取引量は分からない。

ただ、取引量がわからなくても、トランザクションの入力と出力でコインの量が正しくセットされているか=入力に使われているコインの量を超えるような出力がセットされていないかについては、blinding factorを知っている・知っていないに関らず誰もが検証できる必要がある。

この検証は、トランザクションの入力に使われている全コミットメントと出力の全コミットメントに対し以下の式が成立すれば入力と出力でコインの量は等しいことが確認できる。

(入力1のコミットメント + 入力2のコミットメント... ) - (出力1のコミットメント + 出力2のコミットメント...) = 0

ただこの時、手数料が問題になる。Bitcoinトランザクションでは入力-出力の差が手数料として使用されるが、Confidential Transactionのように量が秘匿されている状態では手数料がいくらかは分からないので、手数料は明示的にトランザクションにセットする必要がある。

実際にこの式が成立するか検証してみよう。とりあえず手数料を考えず、全入力のコミットメントを加算した点から全出力のコミットメントを加算した点を引いたものが0になれば良い。

楕円曲線上の点の減算は、減算したい点についてX軸について対称の点を計算し、その点を加算することが減算になる。(x, y)に対して(x, -y)を作れば前者の点を使った減算ができる。ちなみにこの両者を加算すると無限遠点となり、この場合、無限遠点はこの演算に関してゼロとして機能する。そのため、全入力のコミットメントから全出力のコミットメントを減算した結果、無限遠点となればイコール0と判断できる。

1500のコインをもつ1つの入力と、1000と500の2つの出力を例に計算してみる。

この場合入力のコミットメントは

# blinding factor
x = 1 + SecureRandom.random_number(G.order - 1)

input =  G.generator.multiply_by_scalar(x) + H.multiply_by_scalar(1500)

となる。同じxを使った出力のコミットメントは単純に考えると以下のようになる

output1 =  G.generator.multiply_by_scalar(x) + H.multiply_by_scalar(1000)
output2 =  G.generator.multiply_by_scalar(x) + H.multiply_by_scalar(500)

が、この出力は正しくない。このコミットメントを使って全入力のコミットメント-全出力コミットメントを計算するとG.generator.multiply_by_scalar(x)の分だけ余りが発生し0にはならない。

そのため、こういうケースでは計算が合うように出力のblinding factorの値を選択する必要がある。
今回は簡易的に計算するため、それぞれのflinding factorを以下のように調整した値のコミットメントを作成した。

output1 =  G.generator.multiply_by_scalar(x / 2) + H.multiply_by_scalar(1000)
output2 =  G.generator.multiply_by_scalar(x - x / 2) + H.multiply_by_scalar(500)

実際にblinding factorをどのように設定するかは、トランザクションを作成するユーザーが全入力のコミットメント-全出力コミットメント=0になれば後は自由に決めて良い。

実際にこのコミットメントを持つトランザクションが与えられた際、当事者以外は以下の検証をすると入力と出力が同じ量であるか検証できる。

input =  G.generator.multiply_by_scalar(x) + H.multiply_by_scalar(1500)

output1 =  G.generator.multiply_by_scalar(x / 2) + H.multiply_by_scalar(1000)
output2 =  G.generator.multiply_by_scalar(x - x / 2) + H.multiply_by_scalar(500)

outputs = output1 + output2

result = input + outputs.negate # ECDSA::Point#negateは、その点のX軸に対して対象な点を算出するメソッド

result.infinity?
=> true

実際に入力と出力の関係が1:1になることはほとんど無いと思うので、入力のblinding factorとその入力が持つ量と出力先の数を踏まえて、新たに出力で使用するbliding factorを計算する必要がある。