Develop with pleasure!

福岡でCloudとかBlockchainとか。

LNのゴシッププロトコルを使った流動性の提供

最近、C-Lightningで実験的にLNのチャネルの流動性(インバウンドキャパシティの提供)をLNのゴシッププロトコルを使って配信する機能がマージされた↓

https://github.com/ElementsProject/lightning/pull/4639

LNの支払いを受ける場合、チャネルにインバウンドキャパシティが必要になる。手数料を支払うことでこのインバウンドキャパシティを提供してくれるLNBIGのようなサービスもある(LNBIGは何で小額でインバウンドキャパシティ提供してくれてるのか不思議だ)。

今回C-Lightningが実装したのは、まだマージされていないけどBOLTにも提案されている機能で↓

https://github.com/lightningnetwork/lightning-rfc/issues/878

LNのゴシッププロトコルを使って、流動性の提供をアナウンスし、リース料をもらいインバウンドキャパシティを一定期間提供するチャネル開設を可能にしようというアプローチ。これが普及すると、チャネルのリバランスも自動化できるようになるかも。

ZmnSCPxjの提案

LNで流動性マーケットを提供する仕組みについては、2018年にZmnSCPxjがLightning-Devメーリングリストで提案している↓

[Lightning-dev] Towards a Market for Liquidity Providers -- Enforcing Minimum Channel Lifetime

流動性を購入する際には、以下の2つが重要なパラメーターになる:

  • 提供されるインバウンドキャパシティ
  • 提供される期間

流動性の提供期間の保証

せっかく購入したキャパシティも、すぐにチャネルが閉じられてしまうと損するだけなので、購入したキャパシティは一定期間利用できる状態にする必要がある。

単純にこの期間を担保する方法として考えられるのは、コミットメントトランザクションnLocktimeにリース期間を加味したブロック高を設定するという方法。ただ、この場合、支払いをルーティングする際のHTLCのタイムロックと競合してしまう。

そこで、コミットメントトランザクション自体にnLocktimeを設定するのではなく、コミットメントトランザクション流動性提供側のアウトプットにCLTVを設定するアプローチが提案されている。

流動性提供者をLicky、流動性の購入者をMercyとした場合、

Licky側のコミットメントトランザクションのアウトプットは:

  1. Mercyの残高:Mercyの鍵 && CSV
  2. Lickyの残高: Revocation || (Lickyの鍵 && CSV && CLTV)

Mercy側のコミットメントトランザクションのアウトプットは:

  1. Lickyの残高:Lickyの鍵 && CSV && CLTV
  2. Mercyの残高: Revocation || (Mercyの鍵 && CSV)

という構成になる。ここでCLTVは、流動性が提供される期間=ライフタイム。既存のコミットメントトランザクションは対称的な構成になるが、流動性のライフタイム(CLTV)が常にLicky側に付くため、トランザクションは非対称な構成になる。

この場合、コミットメントトランザクションをブロードキャストして一方的にチャネルを閉じた場合、どちらがブロードキャストしたとしてもLicky側の残高は流動性のライフタイムが過ぎるまでは動かせなくなる。この仕組みで、購入した流動性の提供期間を保証させる。

もちろん、Lickyがコミットメントトランザクションをブロードキャストするのを防止する方法は無いけど、ブロードキャストしても資金がロックされるだけで、チャネルを維持していれば得られたかもしれないルーティング手数料も手に入らないので、提供側がそのような行為をする経済的なインセンティブはない。

支払いの循環を利用した資金移動

↑の方法で、流動性提供者のインセンティブが設計されるが、例えば、

Licky → Mercy → Randy → Licky

のような経路が存在した場合、Lickyは自分の資金をRandyを経由した自分に送ることで、Licky → Mercy間の流動性を移動することができてしまう。この場合、流動性を購入したMercyにとっては、その流動性が移動してしまうという攻撃が考えられる。

ただ、Mercy → Randy間でも資金が移動していることになるので、逆側から見た場合、流動性は減っていないことになる。そのため、Mercy → Randy間のチャネルにもチャネルのライフタイムを課すことで、この攻撃は緩和できるとしている。

デュアル・ファンディング

流動性の提供者に対して、上記のコミットメントトランザクションを作成しインバウンドキャパシティを購入するが、その際に提供と支払いを同時に行うため、デュアル・ファンディングチャネルを前提としている。

つまり、チャネルを構築する際に、流動性の提供者と購入者それぞれがUTXOを持ち合い、初期残高は、

  • 提供者の残高:拠出額+流動性の購入費用
  • 購入者の残高:拠出額 - 流動性の購入費用

になる。正確には、オンチェーン手数料がこれに加味される。

BOLTの提案内容

BOLTでは、さらに流動性の提供をLNのゴシッププロトコルで配信できるようにする仕組みが(まだドラフトだけど)提案されている(冒頭に書いたC-Lightningに実験的に実装された機能)↓

https://github.com/lightningnetwork/lightning-rfc/blob/04843175f05e7fdcea8fa1cd730857f3d42c4ca3/proposals/010-will-fund-for-food.md

デュアル・ファンディングを可能にするv2 channelプロトコル上に構築され、流動性を提供したいユーザーは、node_announcementに資金提供するレートをセットして配信できるようになる。

これらの配信情報を見て、インバウンドキャパシティが欲しいユーザーは、request_fundstlvを含むopen_channel2リクエストをノードに送信して、チャネル開設のネゴシエーションを始め、提供側はwill_fundを含むメッセージでこれに応答する。

現状の提案では、流動性の貸出は4032ブロック、つまり約28日間とされてる。資金を貸出用にロックする期間であって、それが経過したらチャネルを閉じなければならないというものではない。

CSVを使ったロック

↑のZmnSCPxjの提案ではCLTVを使ったロックだったが、BOLTの定義では、CSVの使用に置き換わってる。具体的には流動性提供者の残高のCSVに、貸出金のライフタイムの期間分が加算される。

CLTV→CSVにすると、絶対時間指定がコミットメントトランザクションがブロックに入ってからの相対時間指定になるから、リース期間の指定にならないんじゃ?と疑問に思ったけど、別途update_blockheightというメッセージが定義されている。

update_blockheightメッセージは、チャネルの開設者のみが相手に送信できるメッセージで、文字通りブロック高を通知するメッセージになる。このメッセージを受け取ると、チャネルにセットされている流動性を提供しているアウトプットのCSVのタイムロックがブロック数に応じてデクリメントされる。

つまり、チャネル開設当初は、流動性を提供しているコミットメントトランザクションのアウトプット(Licky側のアウトプットには)通常のCSV値に加えて、4032ブロック分のCSV値が加算された値が設定されているが、チャネル開設後、ブロックチェーン上で新しいブロックが作られると、update_blockheightメッセージでそのブロック高を通知し、コミットメントトランザクションCSV値をデクリメントした状態で更新する。これにより、CSVによる相対時間だけど、CLTVによる絶対時間による流動性の貸出期間の制限と同様の制限を課すようにしたっぽい。なんでCLTV→CSVに変更されたのかは謎。

P2P環境で流動性を提供する試みが始まるという点で興味深い。またこういうオンチェーンに裏付け資産がある状態で、貸出をベースにした金融商品やサービスのアイディアも広がってくるかもしれない。

Bitcoin Scriptを使ったランポート署名の検証と量子耐性

最近Bitcoin-Devメーリングリストに、OP_CATを使ってBitcoinに量子耐性をもたせる方法について投稿されてたのが興味深かったので見てみる↓

[bitcoin-dev] OP_CAT Makes Bitcoin Quantum Secure [was CheckSigFromStack for Arithmetic Values]

アイディアは量子耐性のあるランポート署名をBitcoin Scriptで検証するというもので、その仕組みはJeremy Rubinの↓のブログから来てる

https://rubin.io/blog/2021/07/02/signing-5-bytes/

ランポート署名

量子耐性のある署名アルゴリズムの1つがランポート署名で、一方向性関数(通常、暗号学的ハッシュ関数)を利用しており、アルゴリズム自体はとてもシンプル。

秘密鍵の生成

秘密鍵として、n個のランダム値のペアを生成する(nは署名対象のメッセージのビット数に依存する)。ここではn=256とし、各ランダム値のサイズも256 bitであると仮定すると、秘密鍵のサイズは、2 ✕ 256 ✕ 256 = 16 KBになる。

公開鍵の生成

公開鍵の作成は、秘密鍵ハッシュ値になる。つまり↑の2✕256 = 512個の乱数をハッシュしたもの。このハッシュ関数も256 bitとすると秘密鍵と同様サイズは16 KB。

この公開鍵のリストを公開鍵として公開する。

ハッシュ関数にもよるけど、楕円曲線暗号秘密鍵が32 B、公開鍵が(圧縮版で)33 Bであることを考えると鍵長は大きい。

署名の生成

メッセージに署名する手順は↓

  1. メッセージmをハッシュする→H(m)
  2. H(m)を2進展開する。
  3. 2の各bit値に基づいて、対応する秘密鍵をピックアップする。bit = 0の場合はペアの最初の数値を、bit = 1の場合はペアの2つめの値を選択する。
  4. 3をメッセージのハッシュ値分行うと256個の数値のリストが生成され、これが署名データになる。サイズは256✕256 bit = 8 KB。

つまり、秘密鍵はn個の乱数値のペアで、署名対象のメッセージのビット値によって、そのペアのいずれかをピックアップしたものが署名データになる。

こういうアルゴリズムなので(署名を提供する=秘密鍵の一部の提供になるので)、署名に使用した秘密鍵は2回以上再利用してはならない。

署名の検証

↑で生成した署名を検証する手順は↓

  1. メッセージmのハッシュ値を計算する→H(m)
  2. H(m)を2進展開する。
  3. 2の各bit値に基づいて、対応する公開鍵をピックアップする。bit = 0の場合はペアの最初の数値を、bit = 1の場合はペアの2つめの値を選択する。
  4. 署名値の256個の値のハッシュ値を計算する。
  5. 3と4のハッシュ値がすべて一致した場合、署名は正しい。一致しなければ間違った署名データになる。

Bitcoin Scriptでランポート署名を検証

Bitcoinで↑のランポート署名を検証するにはどうしたらいいか?当然、Bitcoin Scriptにはランポート署名を直接検証できるopcodeは存在しないので、↑のブログ記事では、Bitcoin Scriptを使ってランポート署名の検証をしている。ここで検証しているのはトランザクションインプットのnSequenceを署名対象のメッセージとして見立てて、それに対して有効なランポート署名が提供されているかチェックするスクリプト

ここでは、nSequenceの値が53593(2進展開すると1101000101011001)であることを要求している。この場合、メッセージの長さは16 bitなので、n = 16。

<pk> CHECKSIGVERIFY
 0
 SWAP sha256 DUP <H(K_0_1)> EQUAL IF DROP <1> ADD ELSE <H(K_0_0)> EQUALVERIFY ENDIF
 SWAP sha256 DUP <H(K_1_1)> EQUAL IF DROP <1<<1> ADD ELSE <H(K_1_0)> EQUALVERIFY ENDIF
 SWAP sha256 DUP <H(K_2_1)> EQUAL IF DROP <1<<2> ADD ELSE <H(K_2_0)> EQUALVERIFY ENDIF
 SWAP sha256 DUP <H(K_3_1)> EQUAL IF DROP <1<<3> ADD ELSE <H(K_3_0)> EQUALVERIFY ENDIF
 SWAP sha256 DUP <H(K_4_1)> EQUAL IF DROP <1<<4> ADD ELSE <H(K_4_0)> EQUALVERIFY ENDIF
 SWAP sha256 DUP <H(K_5_1)> EQUAL IF DROP <1<<5> ADD ELSE <H(K_5_0)> EQUALVERIFY ENDIF
 SWAP sha256 DUP <H(K_6_1)> EQUAL IF DROP <1<<6> ADD ELSE <H(K_6_0)> EQUALVERIFY ENDIF
 SWAP sha256 DUP <H(K_7_1)> EQUAL IF DROP <1<<7> ADD ELSE <H(K_7_0)> EQUALVERIFY ENDIF
 SWAP sha256 DUP <H(K_8_1)> EQUAL IF DROP <1<<8> ADD ELSE <H(K_8_0)> EQUALVERIFY ENDIF
 SWAP sha256 DUP <H(K_9_1)> EQUAL IF DROP <1<<9> ADD ELSE <H(K_9_0)> EQUALVERIFY ENDIF
 SWAP sha256 DUP <H(K_10_1)> EQUAL IF DROP <1<<10> ADD ELSE <H(K_10_0)> EQUALVERIFY ENDIF
 SWAP sha256 DUP <H(K_11_1)> EQUAL IF DROP <1<<11> ADD ELSE <H(K_11_0)> EQUALVERIFY ENDIF
 SWAP sha256 DUP <H(K_12_1)> EQUAL IF DROP <1<<12> ADD ELSE <H(K_12_0)> EQUALVERIFY ENDIF
 SWAP sha256 DUP <H(K_13_1)> EQUAL IF DROP <1<<13> ADD ELSE <H(K_13_0)> EQUALVERIFY ENDIF
 SWAP sha256 DUP <H(K_14_1)> EQUAL IF DROP <1<<14> ADD ELSE <H(K_14_0)> EQUALVERIFY ENDIF
 SWAP sha256 DUP <H(K_15_1)> EQUAL IF DROP <1<<15> ADD ELSE <H(K_15_0)> EQUALVERIFY ENDIF
 CHECKSEQUENCEVERIFY

先頭の<pk> CHECKSIGVERIFYは単純に既存のECDSA署名検証の仕組みで、これはランポート署名とは無関係。

H(K_0_0)H(K_0_1)、…H(K_15_0)H(K_15_1)の値は、ランポート署名に用いる公開鍵の値。H()はハッシュ関数で、中身のK_0_0が対応する秘密鍵の値。

このスクリプトをアンロックするために必要なscriptSigは、CHECKSIGVERIFYで評価される通常のECDSA署名と、16個のランポート署名のデータ↓

K_15_1
K_14_1
K_13_0
K_12_1
K_11_0
K_10_0
K_9_0
K_8_1
K_7_0
K_6_1
K_5_0
K_4_1
K_3_1
K_2_0
K_1_0
K_0_1
<sig>

ランポート署名の検証ロジックである↓の部分は、

SWAP sha256 DUP <H(K_0_1)> EQUAL IF DROP <1> ADD ELSE <H(K_0_0)> EQUALVERIFY ENDIF

CHECKSIGVERIFYの検証が終わったとして)次のように評価される

No 実行処理 実行後のスタック
1 スタックの先頭2つの順番を入れ替える。最初に評価する時点ではスタックの先頭2つは0 K_0_1なのでこれが入れ替わってK_0_1 0になる。 K_0_1 0...
2 続いて先頭の要素がSHA-256される。 H(K_0_1) 0 K_1_0 K_2_0...
3 DUPを実行すると先頭の要素が複製される。 H(K_0_1) H(K_0_1) 0 K_1_0 K_2_0...
4 <H(K_0_1)>がプッシュされる。 H(K_0_1) H(K_0_1) H(K_0_1) 0 K_1_0 K_2_0...
5 EQUALによりスタックの先頭2つが比較される。ここでは、scriptSigで提供された秘密鍵と、対応する公開鍵のペアの2つめを比較している。先頭2つはH(K_0_1) H(K_0_1)で等しいのでTRUEがプッシュされる TRUE H(K_0_1) 0 K_1_0 K_2_0...
6 IFが評価されスタックの先頭はTRUEであるため、IF分岐に入る。 H(K_0_1) 0 K_1_0 K_2_0...
7 DROPにより、H(K_0_1)が削除される。 0 K_1_0 K_2_0...
8 <1>がスタックにプッシュされ、ADDによりスタックの先頭2要素が加算される。つまり1 + 0 = 1。 1 K_1_0 K_2_0...

5の分岐でFalseになった場合は、scriptSigの秘密鍵のハッシュともう1つの公開鍵(ペアのうちの1つめ)を比較する。この場合、メッセージの該当bitは0であるため、8の加算は発生しない。

これを繰り返すと、最終的にスタックにはメッセージ53593が残ることになり、それをCHECKSEQUENCEVERIFYで評価する。

というスクリプトにより、やろうと思えば今でもスクリプトを使ってランポート署名の検証は可能。

量子耐性

↑の仕組みを使って量子耐性を持つビットコインを作ろうというのが、続きのブログ記事↓

https://rubin.io/blog/2021/07/06/quantum-bitcoin/

量子コンピューターにより楕円曲線暗号の離散対数仮定が破られたとしても(つまりP = xGとなる公開鍵Pから秘密鍵xを逆算できるようになっても)コインを安全に保つためには、従来の署名データの検証に加えて(ここは量子コンピューターにより侵害可能)、署名データのHash160値を↑のランポート署名で検証すればいいというアイディア。

Hash160だと署名されるメッセージダイジェストの長さは20Bになるので、↑の検証を4バイトずつ実行して、その結果をOP_CATで結合する処理を5回実行した結果が、Hash160(ECDSA署名)と等しければコインを使用できるようするということみたい。ただ、これを実現するためにはOP_CATBitcoinで再度使えるようにする必要がある。

あと前述したように、ランポート署名の鍵や署名データのデータ長は大きいので、トランザクションサイズはこれまでよりも大幅に大きくなる。

Schnorr署名とOP_CATを使ったCovenants

Covenantsは、Bitcoinのコインの用途(送り先など)を制限するための仕組み。これまでCovenantsを導入する仕組みの提案がいくつかあったけど、2021年1月にAndrew PoelstraがSchnorr署名とOP_CATを利用したCovenantsの構成を発表しているので、その内容について見てみる↓

https://www.wpsoftware.net/andrew/blog/cat-and-schnorr-tricks-i.html

もともと、Elementsで実装されているスタック上のアイテムの署名検証を行うOP_CHECKSIGFROMSTACK opcodeとOP_CHECKSIGを組み合わせてCovenantsを構成する方法は以前発表されていたけど、OP_CHECKSIGFROMSTACK を使わず、Schnorr署名とOP_CATで同様のことを行う構成みたい。

Schnorr署名とOP_CAT

OP_CATは元々Bitcoinに実装されていて、2010年に削除されたopcode(なので、現在利用はできない)。機能は、スタックから2つの要素をピックアップし、それらを連結して結果をスタックにプッシュするというもの。

基本的にBitcoinのScriptの実行環境では、スクリプト内でトランザクションデータにアクセスできない。ただ、Schnorr署名とOP_CATを利用したトリックを使うと、スクリプト内でトランザクションデータにアクセス(正確にはそのハッシュ)できる。

ある鍵ペア P = xGがあったとして、Schorr署名は以下の手順で生成される。

  1. 一時鍵R = kGを生成
  2. s = k + e * xを計算
  3. (R.x, s)が署名データ

ここでeは、公開鍵PとRおよびトランザクションデータ(署名対象のメッセージ)から生成されるハッシュ値

署名データは、スタック上にプッシュされるデータであるため、この署名データからeを計算することができれば、スクリプトトランザクションデータを間接的に参照することができる。そして↑の計算式と、トリックを使うとeの値を計算することができる。

検証するユーザーはkの値は知らない(kの値が分かると秘密鍵xもバレる)ので、単純に考えるとsからeを計算することはできないけど、例えば、kxの値が1だとするとどうだろう?sの値は

s = 1 + e

になる。この場合k = 1ということはR = Gで、x = 1であればP = Gである(Gは楕円曲線のベースポイント)。そしてsの値はeに1を加算した値になる。

つまり、RとPを↑のように固定して、そのようなPとRに対して署名検証すれば、スタック上でsを使ってトランザクションハッシュにアクセスできるようになる。

そんな検証を適用する具体的なスクリプトが↓

<s> <G> OP_2DUP OP_SWAP OP_CAT OP_SWAP OP_CHECKSIG`

このスクリプトは、次のように実行される。()内は実行後のスタックの状態

  1. スタックにsとGをプッシュする(<s> <G>
  2. OP_2DUPはスタック上の2つの要素を複製する(<s> <G> <s> <G>
  3. OP_SWAPは、上位2つの要素を入れ替える(<s> <G> <G> <s>
  4. OP_CATは、上位2つのデータを連結する(<s> <G> <G><s>
    この<G><s>は↑のSchnorr署名の(R.x s)となる。
  5. OP_SWAPは、上位2つの要素を入れ替える(<s> <G><s> <G>
  6. OP_CHECKSIGは、署名検証を行う。この場合、公開鍵Gに対して署名値<Gs>を検証する。
  7. 最終的にsがスタックに残る。

最後のOP_CHECKSIGの検証により、このスクリプトを実行しているトランザクションがsの元となったデータと等しいことが保証される。これによりスクリプトで指定したデータ(s)通りのトランザクションになることを強制する。

ただ↑のままだと余分な1があるので、これに対処する必要がある。まずeの元になるトランザクションの値を微妙に変えながら(nLocktimeを変更するとか)、eの最下位バイトが01で終わるハッシュ値を探す。eがそんなハッシュ値になると、sはそれに1を加算した値になるので、sの最下位バイトは02になる。そして↑のスクリプトでは最下位バイトを省略したs値をプッシュし、<02> OP_CATスクリプトを加えて、スクリプトで最下位バイトを補うようにするみたい。

TXID参照問題とANYPREVOUT

↑のスキームでBitcoinでCovenantsが可能になるように思えるが、実は現状ではワークしない。↑では、Schnorr署名のs値を利用してトランザクションハッシュとして強制することで、トランザクションに制約を加える仕組みになっている。

つまり、scriptPubkeyの一部としてsを提供する必要があるんだけど、そのようなsを計算する場合に、そのUTXOを参照する際のTXIDを含める必要があり、sを作るためにsが必要になる循環参照が発生する。

この問題を回避するための1つの方法は、以前から提案され最近名称が更新されたSIGHASH_ANYPREVOUTの導入。SIGHASH_ANYPREVOUTは、署名対象のメッセージ=sighashの計算からインプットが参照するOutPointを除外するというもの。つまり署名がインプットが参照するUTXOのTXIDにコミットしなくなるため、これを利用すると↑の循環参照問題は解決する。

もしくは、最初に書いたElementsでやっているようなトランザクションデータの組み立て自体をScriptで行うとかかな。

中継者の協力なしにチャネルの更新を可能にするVirtual Channel

これまで、Multi-Hop LocksAtomic Multi-Channel UpdateAnonymous Atomic Locksなど、多くのペイメントチャネル系の提案をしてきたPedro Moreno-Sanchezが新しいペーパー

Donner: UTXO-Based Virtual Channels Across Multiple Hops

を発表してたので、見てみる。

Virtual Channelとは?

LNのようなオフチェーンでマルチホップ支払いをするようなケースでは、支払いの都度、支払いの経路上にいる全参加者のチャネルの状態を更新する必要がある。これに対し、Virtual Channelは、ペイメントチャネル上に構築するチャネルで、一度チャネルを開くと、チャネルの更新はいつでも何度でも中継者が関与することなく、支払いのエンドポイント(送信者と受信者)のみで行うことができる。中継者の操作が必要になるのは、チャネルを開く時と閉じるときのみ。

ペイメントチャネル上に構築され、(協調クローズする限り)オンチェーン上にはチャネルのファンディング・トランザクションが登場しないことから、Virtual Channelと名付けられている。

このVirtual Channelのメリットは、

  • 支払い時に中継者はオンラインでなくも良い
  • 支払いのレイテンシーは、直接関係する送信者と受信者の間のみに低減される
  • 中継者への手数料は、チャネルのオープン/クローズ時のみで、各支払いでは手数料は発生しない。
  • 各支払いにおける支払金額を中継者が知ることはない。

同じ相手と何度も支払いをするようなユースケースにおいては、有用なソリューションになる。

Donner

Virtual Channel(VC)の提案は、もともとチューリング完全コントラクトを記述可能なEthereum向けに提案されたPerunが最初。その後、UTXOベースのブロックチェーンでVCを構築するLightweight Virtual Payment Channelsが提案される。また、再帰的な構造を導入することで複数人の中継者(マルチホップ)にも対応している。ただ、このプロトコルには、強制クローズ攻撃やグリーフィング攻撃の可能性が考えられ、中継者の数に比例してトランザクション数やストレージスペースも増加する。

再帰構造をやめ、これらのLVPCの課題を解消するプロトコルとして提案されたのが、今回のペーパーのDonner。

VCの主な操作は、

  • VCのオープン
  • VCの更新
  • VCのクローズ
    VCの最終残高で、各参加者のペイメントチャネルの状態を更新することで、VCをクローズする。
  • VCのオフロード
    紛争時などでVCのファンディング・トランザクションをオンチェーンに公開することでVCを決済する。

またこのVCは、LNなどのペイメントチャネルと違い、 有効期間が定められ、それがVCのライフタイムとなる。

各操作について具体的にみていこう。

文字だけだと分かりづらいので、アリスがキャロルと経路アリス→ボブ→マイク→キャロルを使ってVCを構成するフローを図示してみた↓

f:id:techmedia-think:20210709164012p:plain
Donnerのトランザクションフロー

VCのオープン

送信者は、まずVCにロックするコインの量αとチャネルのライフタイムTを決定する。続いて送信者は、VCをオープンするためのファンディング・トランザクションTx VCを作成する。

  • Tx VC
    • インプット:送信者の管理下にあるUTXO
      (オンチェーン/オフチェーンどちらのUTXOでもOK)
    • アウトプット:
      • αコインを保持し、アンロック条件は送信者と受信者のマルチシグ
      • 送信者から受信者までのパス内の中継者の数分(ホップ数)のアウトプット
        各アウトプットはεコインを保持し、ロックスクリプト {中継者_iの署名 \land 相対的ロックタイム(t + Δ)}

VCのファンディング・トランザクションは、受信者を除くペイメントチャネルのパスの数分のアウトプットを持つのが特徴。εの量については小額でよく、ただトランザクション・ポリシーからdust以上でなければならないので、最低546 satoshi以上になる。

続いて、送信者と受信者は、Tx VCの先頭のアウトプット(VCの資金)をインプットとした、VCのコミットメント・トランザクションTx VC Commitmentを作成する。これがVCの状態を表すトランザクションになる。

  • Tx VC Commitment
    • インプット:Tx VCのαコイン UTXO
    • アウトプット:
      • VC内の送信者の残高
      • VC内の受信者の残高

続いて、送信者は、VCを構築するためのパス上の中継者の内、自身の隣のユーザー {U_1}にTx VCを提示して、ペイメントチャネルの状態を更新する。つまりペイメントチャネルのコミットメント・トランザクションを更新する。このトランザクションTx Stateとする。Tx Stateには(HTLCを除くと)両者の残高を表すアウトプットが2つあるので、その内の送信者の残高をαコイン減らし、αコイン保持するアウトプットを新しく追加する。

  • Tx State
    • インプット:送信者と {U_1}のペイメントチャネルのUTXO
    • アウトプット:
      •  {α_1}コイン。ロックスクリプトは、 {(U_1の署名 \land 絶対的タイムロック(T))\lor(送信者とU_1のマルチシグ \land 相対的タイムロック(Δ))}
      • ペイメントチャネル内の送信者の残高(金額は、送信者の残高 -  {α_1}コイン)
      • ペイメントチャネル内の {U_1}の残高

ここで、 {α_1}コインは、VCのキャパシティαに加えて、中継者全員分の手数料を加算した金額(この金額は {U_1}から右のチャネルに行くにつれ、手数料分減っていく)。

続いて、VCにロックされるTx State {α_1}コインのUTXOをインプットとした、取り消し用のトランザクションTx Refundと、支払いを行うTx Payトランザクションの2つのトランザクションを作成する。

  • Tx Refund

    • インプット:
      • Tx VC {U_1}用のεコイン
      • Tx StateのVC用のUTXO、 {α_1}コイン
        この時使用するアンロック条件は、 {送信者とU_1のマルチシグ \land 相対的タイムロック(Δ)}の方。
    • アウトプット:
      • インプットの {ε + α_1}コインを送信者に返金
  • Tx Pay

    • インプット:Tx StateのVC用のUTXO、 {α_1}コイン
      この時使用するアンロック条件は、 {U_1の署名 \land 絶対的タイムロック(T)}の方。
    • アウトプット: {α_1}コインを {U_1}に送金

↑のTx Stateの更新と、Tx RefundTx Payの作成をパス上の各チャネルで行っていき、受信者まで続ける。最終的に受信者まで更新が終わり、Tx VCを送信者に送りもどし、それが最初に送信者が {U_1}に送ったものと同じであればVCはオープンする。

チャネルの担保

ここで、送信者はチャネルのキャパシティαをTx VCTx State(こちらは実際に手数料が加算されている)で提供していることが分かる。実際のチャネルキャパシティは、Tx VCの方だが、Tx Stateにはそれと同額が担保としてロックされている状態になっている。

この担保は、Tx RefundおよびTx Payの原資になっており、

  • 送信者がVCのライフタイムTより前にVCを閉じるかオフロードすれば、Tx Refundにより担保は送信者に戻る。
  • 送信者が何もしなければ、Tを過ぎると担保された資金は全額チャネルを左から右に流れ、最終的に受信者がその担保を得る。

VC上の支払い

VCはTx VCによりファンディングされ、VCの実態はTx VC Commitmentであるため、送信者と受信者は、これを両者で更新することで、パス上の中継者を関与させることなく、VCの資金を使って送金のやりとりが可能になる。

Tx VC Commitmentは、両者の残高をそれぞれ表す2つのアウトプットを持ち、そのロックスクリプトは通常のペイメントチャネルと同様のものだと思われる。

VCの更新

VCのライフタイムの更新をする場合は、パス上でTx Stateを更新していくことでVC自体の更新も可能。

VCのクローズ

VCをクローズする場合、受信者が、パス上の右隣のユーザーとTx Stateの更新を開始する。この時、VC用のUTXOのコインの量は、VC内の送信者の最終残高(実際には手数料が加味)になる。この更新を送信者まで続ける。

送信者は更新した残高がVCの残高と等しいことを確認する。VCをそのまま閉じる場合は、Tx Stateを更新し、VC用の残高を送信者側のアウトプットにマージする。つまりペイメントチャネル上でVCを決済する。この場合、VCに関連するオフチェーントランザクションはオンチェーンに現れることはなく、オンチェーン手数料という意味でも最も効率的。

VCのオフロード

もし、受信者が更新してきた残高が不正な場合など、紛争が発生した場合は、送信者はTx VCをオンチェーンで公開しVCをオフロードする。

VCがオフロードされると、VCを利用していた両者は、VCの正しい残高を確定することになり(最新のTx VC Commitmentを適用する)、かつ送信者はTx Refundで担保を回収することができる。また、パス上の中継者は、Tx VCがオンチェーン上に公開されるか監視する必要がある。Tより前にオンチェーン上に公開されている場合、各自Tx Refundを請求する必要がある。

送信者は、VCのライフタイムTまでにVCをクローズするかオフロードしないと担保を失うので、VCを閉じるインセンティブは送信者にある。

Donnerのポイントは、

ところかと思う。この担保と返金のトリガーにより、マルチホップなVCを機能させている。

ステルスアドレスの利用

VCのファンディング・トランザクションTx VCには、各中継者が使用するUTXOが含まれるが、中継者の公開鍵の情報からパスの情報が明らかにならないよう、これらの中継用のアウトプットの生成にはステルスアドレスが用いられる。

P2TRの鍵導出仕様を定義したBIP-86

Taprootのアクティベートに向けて、単一の鍵でPay to Taproot(P2TR)にコインをロックする際の、鍵の導出仕様がBIP-86として定義された↓

https://github.com/bitcoin/bips/blob/master/bip-0086.mediawiki

単一の鍵なので、対象はTaprootのKey-Path使うケースで、鍵の導出仕様はこれまでのBIP-44(P2PKH), 49(P2SH-P2WPKH), 84(P2WPKH)と基本的に同じ。

HDウォレットでTaprootのInternal Keyを導出し、(BIP-341で推奨されているように)Script-PathによるアンロックができないようにそのInternal KeyのハッシュからScript-Path側の鍵を導出して、その2つの鍵を加算して、P2TRのscriptPubkeyを導出する方法になってる。

以下、BIPの意訳。

概要

このドキュメントは、TaprootのInternal Keyとして単一鍵のP2TR(BIP341)のアウトプットに関与する鍵を持つHDウォレットの導出方式を提案する。

動機

単一鍵のP2TRトランザクションを使用する場合に、共通の導出方式があると便利で、HDシードのバックアップのみを持つHDウォレットが単一鍵のTaprootアウトプットをリカバリーできるようになる。現在では、特定のスクリプトタイプに対して固定の導出パスを必要としないソリューションがあるが、多くのソフトウェアやハードウェア署名者は、導出パスやスクリプトの情報がないシードのバックアップを使用している。そのため、実装を容易にするため、BIP49およびBIP84で使用されているのと同じアプローチを主に使っている。

仕様

このBIPは、BIP32のマスター秘密鍵を基に複数の決定論的アドレスを導出するために2つのステップを定義する。

公開鍵の導出

ルートアカウントから公開鍵を導出するために、このBIPは、BIP44, 49, 84で定義されているものと同じアカウント構造を使用するが、purposeスクリプトタイプが異なる。

m / purpose' / coin_type' / account' / change / address_index

purposeパスの階層では86'を使用する。残りの階層はBIP44, 49, 84で定義されているものを使用する。

この導出パスパターンで導出した鍵は、このドキュメントではderived_keyと呼ぶ。

アドレスの導出

BIP341には、「使用条件にScript-Pathが必要ない場合、アウトプットの鍵は、Script-Pathが無い状態ではなく、使用できないScript-Pathにコミットする必要がある。これはアウトプットの鍵の点をQ = P + int(hashTapTweak(bytes(P)))Gとして計算することで実現できる」と記されている。したがって、

internal_key:       lift_x(derived_key)
32_byte_output_key: internal_key + int(HashTapTweak(bytes(internal_key)))G

トランザクションスクリプトとwitnessは、BIP341で定義されているとおり

witness:      <署名>
scriptSig:    (空)
scriptPubKey: 1 <32_byte_output_key>
              (0x5120{32_byte_output_key})

後方互換

このBIPは、設計上、後方互換性はない。互換性のないウォレットは、これらのアカウントを検出せず、ユーザーは何かが間違っていることに気づくだろう。

しかし、このBIPはBIP 44, 49, 84で使用されているのと同じ方法を使用するため、実装は難しくない。

Test Vector

BIP参照