Lightning Networkのコア機能は、二者間のオフチェーンでの決済時に新しい残高について合意し、(古い状態のコミットメントトランザクションをブロードキャストすると資金を失うというペナルティを与えることで)古い状態がオンチェーン上に展開できないようにする、オフチェーンのコントラクトの更新の仕組みにある。
このオフチェーンのコントラクトの更新をシンプルにする新しいeltooという仕組みを、先日Blockstreamが発表した↓
注意点として、eltooは、既存のLNのシークレットの交換とタイムロックを使ったペナルティ付きのコントラクトとは全く別のプロトコルになるので、既存のLNとの互換性はない。また、SIGHASH_NOINPUT
という新しいSIGHASHフラグの導入が前提であるため、現時点のBitcoinでは利用できない。
ホワイトペーパー↓
https://blockstream.com/eltoo.pdf
でプロトコルの詳細が紹介されているので見てみた。
eltooのオンチェーンプロトコル
eltooのホワイトペーパーにはベースとなるオンチェーンプロトコルとオフチェーンプロトコル両方が記載されており、まずベースとなるオンチェーンのプロコトルから見ていく。
eltooのプロトコルは以下の3つのフェーズからなる。
- セットアップフェーズ
資金を取引の参加者のマルチシグにロックしてチャネルを開設するフェーズ。 - ネゴシエーションフェーズ
決済の状態を更新するフェーズ。 - セトルメントフェーズ
決済を終え、チャネルをクローズするフェーズ。
基本的には既存のLNと同じフェーズがそれぞれあり、主にネゴシエーションフェーズとセットアップフェーズで作成・更新される仕組みが異なる。また、既存のLNでは、各決済の状態(残高)をチャネルに参加している両者が、新しい残高に更新したCommitment Txを作成し、自分の署名を加えて相手に渡し、古いCommitment Txで使っていたシークレットを相手に明かすことで決済の状態を更新している。
一方eltooでは、以下の2つのトランザクションのペアで各決済の状態を管理する。
- Update Tx
Funding Txのアウトプットもしくは、前のUpdate Txのアウトプットをインプットとして、新しいアウトプットを作成するトランザクション。決済の更新を表現する。 - Settlement Tx
Update Txのアウトプットをインプットとして、その決済によって新しくなったアリスとボブの残高のアウトプットを2つ持つトランザクション。Update Txによる決済の更新の結果、両者の残高を反映する。
各トランザクションがどのように構成されるのか、各フェーズ毎に見ていこう。
セットアップフェーズ
チャネルを開設するセトルメントフェーズでは以下の処理が行われる。(アリスとボブ間でオフチェーン決済する前提で記載)
最初にアリスとボブはオフチェーン決済に使用する以下の2つの鍵ペアを生成し、公開鍵を相手に伝える。
- セトルメント鍵ペア()
チャネルをクローズする際に作成するSettlement Tx(後述)で使用する鍵 - アップデート鍵ペア()
チャネルの状態を更新する際に作成するUpdate Tx(後述)で使用する鍵
続いてセットアップに必要なトランザクションを作っていく。(アリスが資金を提供するとして)アリスは自分の資金をインプットとして、アリスとボブが協力しないとコインをアンロックできないアウトプット(の署名か、の署名が必要)に資金を送る未署名のFunding Txを作成する。
この時作られるアウトプットのスクリプトが↓(オフチェーンで利用する場合、実際はELSE分岐の条件にもう1つ条件が加わるが、それについては後述)
OP_IF <タイムアウト> OP_CSV 2 As Bs 2 OP_CHECKMULTISIGVERIFY OP_ELSE 2 Au Bu 2 OP_CHECKMULTISIGVERIFY OP_ENDIF
セトルメント鍵ペア()を使ってコインをアンロックするためには、このFunding Txがブロックに入れられて、<タイムアウト>で設定された期間過ぎるのを待つ必要がある。アップデート鍵ペア()を使ってコインをアンロックする場合、時間の制約はなくすぐにアンロックできる。
続いて、Funding Txに署名&ブロードキャストする前に、払い戻し用のトランザクションを作っておく必要があり、これが最初のSettlement Txになる(最初のSettlement TxのみFunding Txのアウトプットをインプットとし、アウトプットも返却用の1つのみ)。Funding Txのアウトプットをインプットとして、全額をアリスに送るSettlement Txを作成し、ボブに署名をしてもらう。
ボブの署名が正しい署名であると検証できたら、アリスはFunding Txをブロードキャストする。
ネゴシエーションフェーズとセトルメントフェーズ
セットアップフェーズのFunding Txをブロードキャストしてチャネルを開設した後は、オフチェーン決済が可能になる。オフチェーンでの決済=新しいUpdate TxとSettlement Txを作成して、決済の状態(残高)を更新すること。
オフチェーンで決済し、状態を更新したい場合、まず新しいUpdate Txを作成する。この新しく作成するUpdate Txのインプットは前のUpdate Txのアウトプットを参照する。また新しく作成するUpdate Txのアウトプットのスクリプトはインプットが参照する前のUpdate Txのスクリプトそのまま。つまり↓
OP_IF <タイムアウト> OP_CSV 2 As Bs 2 OP_CHECKMULTISIGVERIFY OP_ELSE 2 Au Bu 2 OP_CHECKMULTISIGVERIFY OP_ENDIF
続いて、新しく作成したUpdate TxのアウトプットをインプットとするSettlement Txを作成し、そのアウトプットは決済後のアリスとボブの残高を適用した2つのアウトプットを持つ。
つまりUpdate Txは決済のチャネルの状態の更新を表すTxで、それ自体では決済後のアリスとボブの残高は管理されておらず必ずインプットと同じスクリプトを1つ持つだけのトランザクションで、そのアウトプットをインプットとするSettlement Txで決済後のアリスとボブの残高を管理するようになっている。
eltooで定義されているオンチェーンのプロトコルでは、↑のIF分岐のOP_CSVによるタイムアウトが経過する前までであれば、新しいUpdate Txを作成しブロードキャストすることで決済の更新をする。この新しいUpdate Txがブロードキャストされると、前のUpdate Txと対で作っていたSettlement Txは使えなくなる(新しく作ったUpdate Txのインプットと同じインプットを参照しているため)。そして、OP_CSVによるタイムアウトが経過したら、アリスとボブの最終の段高を反映したSettlement Txをブロードキャストして決済を完了することになる。
このオンチェーンのアプローチは、結局Update Txを決済の都度ブロードキャストする必要があるので、スケール効果はない。そのため、オフチェーン決済では、Update Txをブロードキャストせずに決済の状態を更新する仕組みが必要になる。
eltooのオフチェーンプロトコル
続いて、↑のオンチェーンプロトコルをオフチェーンプロトコルにする仕組みについて見ていく。
SIGHASH_NOINPUTを使った中間トランザクションのスキップ
オンチェーンのプロトコルでは、Update Txは前のUpdate Txのアウトプットを参照しているため、前のUpdate Txがチェーン上にブロードキャストされている必要があるが、オフチェーンにする場合、この参照を前のUpdate Txではなく、さらにその前であったり、最終的にはFunding Txのアウトプットを参照できるようにできれば中間のUpdate Txをスキップして、最終残高を表す最新のUpdate TxとSettlement Txが管理できれば効率的になる。
この仕組みを実現するのに、eltooでは新しいSIGHASHフラグSIGHASH_NOINPUT
の導入を提案している。もともとSIGHASHはトランザクションに署名する際に、署名対象となるトランザクションのデータを指定するための仕組みだ。
techmedia-think.hatenablog.com
ほとんどのトランザクションはSIGHASH_ALL
で署名されているが、それ以外に
SIGHASH_SINGLE
を使うと、署名をセットするインプットとそのインプットと同じインデックスのアウトプットは署名対象になるが、その他のインプットやアウトプットは署名対象にはならないので、それ以外のインプットやアウトプットを自由に追加できる。SIGHASH_NONE
を使うと、インプットは全て署名対象になるが、アウトプットは署名対象にならないのでアウトプットのデータを自由に変更・追加・削除できる。- さらに
SIGHASH_ANYONECANPAY
フラグをSIGHASH_ALL
、SIGHASH_SINGLE
、SIGHASH_NONE
と組み合わせることで、自由にインプットを追加できるようになる。
という機能を持つSIGHASHを利用することができる。
Floating Transaction
SIGHASH_NOINPUT
は、SIGHASH_ANYONECANPAY
のような位置付けのフラグで、インプット内の前のOutPoint(UTXOを参照するフィールド)の値を空白にし署名対象から外すようになる。このフラグが適用されると、署名データはインプットが参照する前のアウトプットに対してコミットしなくなり、異なるUTXOを参照するようトランザクションを書き換えることができるようになる。
具体的には署名済みのトランザクションがあって、そのトランザクションのバージョンやロックタイム、インプット or witnessにある署名スクリプト、アウトプットの量やscriptPubkeyが同じであれば、インプット内のOutPointの値を変更しても、インプット or witness内にある署名は有効な署名のままという訳だ。
このように任意の前のトランザクションアウトプットに接続し、中間トランザクションをスキップするトランザクションをFloating Transactionと呼び、インプットの参照先を書き換えることをバインディングと呼んでいる。
Funding Txを作成してチャネル開設後、いくつか決済をした後、最新のUpdate Txのインプットの参照先をFunding Txのアウトプットに変更すると、その中間のトランザクションをスキップして最新のUpdate Txとそれに対応するSettlement Txによりチャネルはクローズされ、その間に作ったUpdate TxとSettlement Txはチェーン上には現れない。
古い決済状態の無効化の仕組み
相手が裏切って自分の残高の方が多い、中間状態の古いUpdate Txをブロードキャストした場合、そのUpdate Txに対応するSettlement Txで資金を奪おうとしても、OP_CSV
でロックされている間、そのSettlement Txは使えないので、その間に最新のUpdate Txのインプットの参照先をブロードキャストされたUpdate Txのアウトプットに変更してブロードキャストすることで、古いSettlement Txのブロードキャストを阻止でき、最終的に最新のUpdate Txに対応したSettlement Txを使ってチャネルをクローズできる。
Locktimeを利用したUpdate Txのバージョン管理
SIGHASH_NOINPUT
により中間トランザクションをスキップでき、効率的なオフチェーン決済が可能になったかのように思えるが、1つ問題がある。オフチェーン決済によりUpdate Txの更新をTu,1
、Tu,2
、Tu,3
、Tu,4
、Tu,5
(Tu,i
は各決済のUpdate Tx)と続けた場合、Tu,5
がブロードキャスされた後に、それより前のTu,2
がブロードキャストされる可能性がある。つまり最新の状態を古い状態に置き換えることができてしまう。
これは、オンチェーンではUpdate Txは必ず前のUpdate Txのアウトプットを参照していたので順序が保証されていたのが、Floating Transactionにより参照するOutPointを自由に変更できるようにしてしまったため、作成したUpdate Txから順序情報が無くなってしまったことに起因する。
この問題に対応するためオフチェーンのUpdate Txのスクリプトは以下のような形式に変更する(Si
はステートナンバーで、決済回数を意味する)。
OP_IF <タイムアウト> OP_CSV 2 As,i Bs,i 2 OP_CHECKMULTISIGVERIFY OP_ELSE <Si + 1> OP_CHECKLOCKTIMEVERIFY 2 Au Bu 2 OP_CHECKMULTISIGVERIFY OP_ENDIF
オンチェーンのUpdate Txとの違いは、ELSE分岐に<Si + 1> OP_CHECKLOCKTIMEVERIFY
が追加されていて、次のステートナンバー<Si + 1>
にコミットしているのと、セトルメント鍵ペアが決済毎に変わる仕組みになっている(これに関しては後述)。ステートナンバーはUpdate TxのnLocktime
フィールドに格納される。
このため、Update Txのアウトプットを使用する場合、スクリプト内に定義されているステートナンバー以上のステートナンバーを持つUpdate Txのみが、そのUpdate Txのアウトプットを利用できる。nLocktime
を含めてもう少し詳細に書くと、
Tu,5
のUpdate Txのアウトプットスクリプトには6 OP_CHECKLOCKTIMEVERIFY
という条件が含まれる。つまりこのアウトプットを使用するUpdate TxのnLocktime
には6以上がセットされていなければ、スクリプトの評価時にエラーとなる。Tu,2
のトランザクションのnLocktime
には2がセットされているので、この結果エラーになり、Tu,5
がブロードキャストされた後に、Tu,2
のインプットの参照をTu,5
に変更しても、そのトランザクションは無効でブロードキャストできない。
※ もともとnLocktime
はある時間までトランザクションをブロックに入れられないよう絶対時間(UNIXタイムスタンプ or ブロック高)で制限するopcodeだが、これをステートナンバーの格納先に利用しようという仕掛け。実際nLocktime
は値が5億以上だとUNIXタイムスタンプとして利用するので、現在のUNIXタイムスタンプが約15億なのでロックされない過去分の値が10億分使えることになる。このため実際のステートナンバーは500,000,001とかから始まるんだと思う。
ステートナンバー導入を考慮したSIGHASH_NOINPUTの挙動
↑でSIGHASH_NOINPUT
は署名のダイジェストデータ作成時にOutPointを空にすることで、別のUTXOへの切り替えを可能にすると書いたけど、アウトプットにステートナンバーが含まれるようになると、これだけではまずい。署名のダイジェストデータを作成する際の要素として、インプットが参照するアウトプットのスクリプト(scriptCode)にコミットするようになっているためだ。ダイジェストに含まれる要素はBIP-143で定義されている↓
techmedia-think.hatenablog.com
何が問題かというと、ステートナンバーがアウトプットに含まれている状態で、インプットの参照を変更すると元々の署名時のステートナンバーと異なるステートナンバーがscriptCode
にセットされるので署名検証に失敗してしまう。このため、SIGHASH_NOINPUT
が変更するのはOutPointのデータだけではなくscriptCode
も含まれる。↓にSIGHASH_NOINPUT
のBIPドラフトが公開されており↓
https://github.com/cdecker/bips/blob/noinput/bip-xyz.mediawiki
BIP-143からの変更点は↓
2. hashPrevouts (32-byte hash) is 32 0x00 bytes 3. hashSequence (32-byte hash) is 32 0x00 bytes 4. outpoint (32-byte hash + 4-byte little endian) is set to 36 0x00 bytes 5. scriptCode of the input is set to an empty script 0x00
OutPointのデータ(2,3,4)に加えて、scriptCodeも空になるようになっている。これにより、署名がインプットが参照するアウトプットのスクリプトにコミットしなくなるので、参照先を変更してそのアウトプットスクリプト内に定義されている次のステートナンバーが異なる場合でも署名は有効なままになる。
Settlement TxのUpdate Txへのバインド
Update Txはステートナンバーの導入によりUpdate Txの新旧を逆行するようなブロードキャストはできなくなったが、Settlement TxもSIGHASH_NOINPUT
によりインプットが参照するUpdate Txのアウトプットへの参照を変更できてしまうと、自分に有利なSettlement TxをブロードキャストされたUpdate Txにバインドして使われてしまうリスクがあり、こちらも対応が必要になる。
このため各決済毎にそれぞれ固有の新しいセトルメント鍵ペアのセットAs,i
、Bs,i
を導出する。これが適用されるのが、↑のUpdate Txのアウトプットの2 As,i Bs,i 2 OP_CHECKMULTISIGVERIFY
の部分。鍵ペアの導出自体はHDウォレットなどと同様に決定論的に導出する。これによりSettlement Txは一致するUpdate Txにのみバインドすることができる。最新の状態に対応した鍵ペアのみ覚えておけばいいので、これにより参加者のストレージも増加しない。
ステートナンバー導入を考慮した署名の作成
アウトプットのスクリプトやnLocktime
が全て同じなら、SIGHASH_NOINPUT
でインプットの参照先を変更しても、署名は最初に作った署名だけあれば済むが、ステートナンバーがnLocktime
に格納され次のステートナンバーがアウトプットのスクリプトに記載されるようになったので、基本的にUpdate Txを作成する度に署名は作成する必要がある。
Update Txに対応するSettlement Txも、決済毎に使用するセトルメント鍵ペアが変わるので、Settlement Tx毎に署名を作成する必要がある。
ただ、どちらも最新のTxの署名だけ管理してればOK。
トリガーフェーズの導入
残る問題が1つ。最初のFunding Txを作成したとき、そのアウトプットのIF分岐のタイムロック条件がセットされており、このタイムアウトを過ぎると最初に作成したアリスに全額返金するSettlement Txがブロードキャストできるようになる。つまりFunding Txに設定されてあるタイムアウトの期間がペイメントチャネルのライフタイムになる。これを既存のLNのようにチャネルのライフタイムの制限を外すために、セットアップフェーズの後にトリガーフェーズを導入する。
まずセットアップフェーズのFunding Txのアウトプットについて、eltooのオンチェーンプロトコルではUpdate Txと同じスクリプトを使用していたが、これをアリスとボブの単純な2-of-2のマルチシグに変更する。
そして、Funding TxのマルチシグをインプットとしたTrigger Txを作成する。このTxのアウトプットは、元々のFunding Txにセットしていたスクリプトにする。そしてUpdate TxはFunding Txではなく、Trigger Txのアウトプットをインプットとする。
こうするとFunding Txがオンチェーンで承認されても、Trigger Txは手元にあるのでタイムロックの計測はスタートしない。オフチェーン決済を繰り返し、最終的にチャネルをクローズする際に、Trigger Txと最新のUpdate Tx、Settlement Txをブロードキャストすれば良い。
SIGHASH_SINGLEで手数料を追加
今まで手数料については触れなかったが、Update Txを使用する際の署名について、署名作成時のSIGHASHタイプをSIGHASH_SINGLE
にする。Update Txは1つのインプットと1つのアウトプットを持つトランザクションなので、SIGHASH_INPUT
で署名をしておけば、ブロードキャスト時にインプットとアウトプットを任意に追加できる。これによりブロードキャスト時に手数料分のインプットを追加し、インプットから手数料を引いたお釣り用のアウトプットを追加することができ、そのタイミングで適切な手数料を任意に設定することができるようになる。
複数の参加者への拡張
↑はアリスとボブの2人だけだったけど、参加者を増やす場合は、
- Update Txのマルチシグを参加者数分のマルチシグにする
- Update Txに対応したSettlement Txはそれぞれの参加者数分の残高を管理するアウトプットにする
ことで簡単に実現できる。参加者が増えると、その分ネゴシエーションフェーズでの調整コストは上がるけど、それは他の方法でも同じ。
所感
オフチェーンプロトコルで作成されるトランザクションと無効化の仕組みを簡単に表すと↓のようになる。
(各Update Txのインプットは差し替えられるのでどこを参照してても良いと思うので、とりあえずTrigger Txにしてる)
- オフチェーンプロトコルを理解するためのキーは、
SIGHASH_NOINPUT
と署名ダイジェストの仕組みの理解が必要。 - 作成するコントラクトとかはシンプルで、チャネルの参加者間で管理するトランザクションも全く同じトランザクションになるので、既存のLNに比べてコントラクトはシンプルになるかも。
- チャネルのオープン/クローズで、Funding, Trigger, Update, Settlementという4つのトランザクションがオンチェーンに乗るので、既存のLNより2つ余計にトランザクションをブロードキャストすることになるんじゃ?と思ったけど、協力してクローズする場合はFundingをインプットにして最終残高を反映したクロージング用のTxを作成すればいいだけなので変わらないな。
SIGHASH_NOINPUT
が導入されると、こういったインプットをすげ替える特性を活かしたコントラクトとかできそう。柔軟になる一方、リスクにもなる可能性はありそう。
GBECでeltooの解説動画を公開したので、そっちの方が↑の説明より分かりやすいと思う↓