Develop with pleasure!

福岡でCloudとかBlockchainとか。

Tapscript用の新しいopcode OP_INTERNALKEYを提案するBIP-349

先日、TapscriptにOP_INTERNALKEYという新しいopcodeの導入を提案するBIP-349がBIPとして登録された↑

https://github.com/bitcoin/bips/blob/master/bip-0349.md

OP_INTERNALKEY

OP_INTERNALKEY opcodeは、スタックにTaprootの内部鍵をプッシュするopcode。Tapscriptには将来のソフトフォークで拡張可能なOP_SUCCESS系のopcodeがいくつか確保されており、その内のOP_SUCCESS203(0xcb)をOP_INTERNALKEYに割り当てる。

Taprootについておさらい

Taprootの内部鍵について簡単に説明すると、Taprootのアウトプットは、よくある公開鍵宛の支払い(P2PKHとかP2WPKH)とスクリプトを用いた条件付きの支払い(P2SHとかP2WSH)の両方を1つの形式のアウトプットで表現できるようになっている*1

  • この公開鍵部分を内部鍵(Internal Key)と呼ぶ。ここではPとする。
  • スクリプト部分は、以下のような手順で公開鍵としてエンコードされる
    1. まず、各アンロック条件をリーフノードとしたマークルツリー(スクリプトツリー)を構成し、
    2. マークルツリーのルートハッシュを計算する(ルートハッシュの値をrとする)
    3. 内部鍵とルートハッシュの値を連結したタグ付きハッシュ {t = H_{TapTweak}(P || r)}を計算する。
    4. 楕円曲線のベースポイントGにtを乗算して公開鍵tGを計算する。

こうしてできた、2つの楕円曲線上の点(公開鍵)を加算した点Q = P + tGをsegwit programとしたsegwit version 1のアウトプットがTaprootアウトプットになる。ロックスクリプトは↓

OP_1 <Q>

詳細については、GBEC動画や、過去のブログ記事を参照。

Taprootアウトプットの使用

上記のように構成されたTaprootのアウトプットを使用する場合は、

  • 鍵のみを使用してアンロックするkey-path
  • 鍵は使用せずにスクリプトを使用してアンロックをするscript-path

のいずれかでアンロックすることでTparootのUTXOを使用できる。

key-pathでのアンロック

この場合は、公開鍵Q(Pではない)に対応する秘密鍵を使用して有効なSchnorr署名を提供することでアンロックできる。内部鍵の作成者であればPの秘密鍵は知っているので(ここでは仮にxとする)、マークルルートから生成したtの値も知っていれば、Qの秘密鍵x + tを知ることができる。

Taproot UTXOを使用するトランザクションのwitnessとして提供するデータは、この秘密鍵で生成したSchnorr署名のみ。

script-pathでのアンロック

この場合はkey-pathと比べると少し複雑で、Taproot UTXOを使用するトランザクションのwitnessとして以下のデータを提供する必要がある。

  1. (当然ながら)使用するスクリプトをアンロックするのに必要なデータ
  2. スクリプトツリー内のアンロックに使用するスクリプト自体
  3. 2のスクリプトがツリー内に存在することを証明するマークルプルーフ
  4. Qを構成する際に使用した内部鍵P

3,4のデータはControl Blockという構造のデータの一部として提供される。これらのデータが提供されると、

  • スクリプトとマークルプルーフを使ってスクリプトツリーのルートハッシュが計算され
  • それとPから {t = H_{TapTweak}(P || r)}を計算し、
  • P + tGが使用するTaproot UTXOのQと等しいか検証できる。

検証をパスしたらスクリプトに対して1のデータを使って2のスクリプトのアンロックを試みる。

OP_INTERNALKEYの用途

本題に戻って、OP_INTERNALKEYの機能は、script-pathを使ってアンロックする際に、↑のようにwitnessとして提供された内部鍵Pをスタックにプッシュすること。

内部鍵Pは、witnessでControl Blockの一部として提供されるものの、上記の検証をする際にControl BlockのデータはスタックからPOPされてるので、検証をパスした後のスクリプト実行時にはスタック上に存在しない。

スクリプト側でOP_INTERNALKEYを使って内部鍵PをスタックにPUSHすることで、key-pathを構成する際に使用した鍵と同じ鍵を使った条件をスクリプトでも利用できるようになる。見方を変えると、本来はSchnorr署名のみを提供するkey-pathに対して、条件付きのkey-pathを構成することができる。

OP_INTERNALKEYを導入しなくても、内部鍵と同じ鍵をプッシュするようなスクリプトを最初から作っておけば同じことはできる。ただBIPでも言及されていたけど、OP_INTERNALKEYを使うと、スクリプトで同じことをする場合に比べて、8 vbyte分の節約になる。これは、スクリプトで内部鍵をプッシュする場合、内部鍵のデータ32 byte + データのプッシュopcode 1 byte分合計33 byte分のスペースを使用する。OP_INTERNALKEYの場合は、このopcode 1 byte分のみで済むため32 byte(=8 vbyte)分の節約になるということ。

LNhance

またOP_INTERNALKEYは、他の3つの新しいopcodeの追加と合わせて、ライトニングネットワークの機能強化や、Arkを含む他のUTXO共有プロトコルを有効にするための提案LNhanceの一部↓

*1:そのため、アウトプットだけみても公開鍵宛なのかスクリプト宛なのか区別がつかない

OP_CATで実現するトランザクション間の状態遷移

少し前にStartWareが、Bitcoinsignet上でOP_CATを利用したSTARKベースのゼロ知識証明の実証をしてたので、どんな内容なのか見てる中で、トランザクションチェーンで状態を遷移させる手法が興味深かった↓

The path to general computation on Bitcoin | StarkWare

まず、前提としてOP_CATがあれば、SchnorrやECDSAのトリックを使ってコベナンツを実装することができる↓

これをさらに拡張することで、トランザクションが参照する前のトランザクションのデータを間接的に参照できるようになる。

State Caboose

StartWareが行っているのは、既存のBitcoinのScript機能に限定されることなく任意の計算をオフチェーンで実行し、その結果の検証をBitcoinのScriptでできるようにし、有効な状態遷移をさせるというもの。

これを実現する基本的なトランザクションは2つのアウトプット持つ:

  • 最初のアウトプットは、コントラクトのロジックとそこにロックされた資金を管理する。
  • 2つめのアウトプットは、コントラクトの現在の状態を保持する。このアウトプットは状態を保持するだけで使用されない。
block-beta
    block:first
        columns 2
        space
        A1["Logic"]
        Tx0(["Tx 0"])
        A2["State 0"]
    end
    block: second
        columns 3
        B1[" "]
        space
        B2["Logic"]
        Tx1(["Tx 1"])
        space
        B4["State 1"]
    end
    block: third
        columns 3
        C1[" "]
        space
        C2["Logic"]
        Tx2(["Tx 2"])
        space
        C4["State 2"]
    end
    space
    A1 --> B1
    B1-->A2
    B1 ---> B2
    B1 ---> B4
    B2 --> C1
    C1-->B4
    C1-->C2
    C1-->C4
    style Tx0 stroke:transparent;
    style Tx1 stroke:transparent;
    style Tx2 stroke:transparent;

そして、コベナンツにより最初のアウトプットには同じロックスクリプトが設定されていることが強制され、2つめのアウトプットには有効な状態が設定され、前の状態から新しい状態への遷移が有効であることをスクリプトで証明させる(この状態遷移が正しいか検証するのがSTARK verifier。今回の記事ではこれ自体には触れない)。

このような構成のトランザクションをState Cabooseと呼んでいる。Cabooseというのは、貨物列車の最後尾に連結される乗務員の作業ペースとなる車両のことで、状態を運搬する最後のアウトプットという意味でこの名前を提案してるみたい。

OP_CATを利用した状態の取得

状態遷移に伴い前の状態を取得(上記の例だと、Tx1がTx0のState 0を参照)する必要がある場合に、スクリプト内でどうやって前のトランザクションの情報にアクセスするかという点について。

まず、前提としてOP_CATベースのコベナンツでは、コベナンツUTXOを使用する際のトランザクションのwitnessで、コベナンツUTXOのOutPoint(TXID + Index)の情報が提供される。このTXIDとは、コベナンツUTXOを持つトランザクションのデータ*1をdouble-SHA256したハッシュ値である。

そのため、

  1. 同じくwitnessで、その前のトランザクション情報を提供させ
  2. それらの要素をOP_CATで連結し
  3. そのハッシュ値が提供されたTXIDと一致するか検証し
  4. 一致すれば前のトランザクション情報をスクリプト内で処理することができる。

つまり、前のトランザクションが持つ状態の値を入手することができるということ。さらにスクリプトの制限が許せば、前の前のトランザクションの状態と遡ることも理論的には可能だ。

OP_CATが再導入されると本当にいろんなことが可能になるなー。

*1:witnessを除く

ECDSAとOP_CATを使ったコベナンツ

OP_CATとSchnorrのトリックを使ってBitcoinコベナンツを実装するAndrew Poelstraの提案について以前書いたけど↓

techmedia-think.hatenablog.com

その後、Robin Linusが同様のことをECDSAで行う方法を公開してた↓

Emulate covenants using only OP_CAT and ECDSA signatures · GitHub

ECDSA

秘密鍵x、対応する公開鍵をP = xGとした場合、メッセージmに対するECDSA署名を生成する通常の手順は↓

  1. シークレットnonce kをランダムに選択する。
  2. R = kGを計算する。
  3. r = R.x(RのX座標)とする。
  4.  {\mathbb s = \frac{H(m) + r \times x}{k}}を計算する(Hはハッシュ関数
  5. (r, s)がECDSA署名

トリック

Schnorrのトリックと同様に、ECDSAの場合もxkを以下のような固定値にする。

  • k = 1とする(Schnorrのトリックと同様で、R = Gとなる(Gは楕円曲線のベースポイント))
  • x = 1/rとする

この2つの値が上記のように固定化されると、署名値ss = H(m) + 1となる。つまり、sの値は署名対象のメッセージのハッシュ値に1を加算した値となり、Schnorrトリックと同様の署名が得られるというもの。

OP_CATとの組み合わせ

Bitcoinの場合、mの値は(witnessを含まない)トランザクションデータなので、H(m)の計算はBitcoin Script内で行える。RとPはそれぞれ固定値(R = G、P = xG = 1/r G)なので、実際にH(m)の計算で変動部分はm、つまりコベナンツのUTXOを使用する際のトランザクションデータになる。このトランザクションデータをスタック上の要素から取得して、最終的にdouble-SHA256してH(m)を求める。

この時、組み立てるトランザクションデータの配置場所は2つに分かれる:

前者はコベナンツとして強制する内容を定義するもので主にUTXOを使用するトランザクションのアウトプットのデータ(ロックスクリプトや金額)、後者はロックスクリプトで強制する必要のない(できない)データで、特にコベナンツのUTXO自体のOutPointの情報など。

この二種類のデータを、スタック上でOP_CATを使って連結してトランザクションデータを構築し、そのdouble-SHA256値を計算してH(m)とする。なお、OP_CATはBitcoinでは現状利用できないが、再度有効化されたとしてもスタック要素については520 byteの最大サイズ制限があるので、それを超えるトランザクションを構成することはできない*1

検証スクリプト

ロックスクリプト内では、Schnorrトリックと同様に固定のPとRに対する署名検証を要求する。Schnorrトリックの場合はPもRと同じGだったけど、ECDSAトリックの場合はP = 1/r Gとなるのが異なる。↑の投稿では具体的な検証スクリプトは書かれてなかったけど、以下のようなスクリプトになると思われる。

OP_DUP <R> OP_SWAP OP_CAT <P> OP_CHECKSIGVERIFY

このスクリプトを、↑の仕組みで計算されたECDSAの署名値sがスタックにある状態で実行すると、(カッコ内はスタックの状態で、右が上位):

  1. スクリプト実行前のスタックの状態(<s>
  2. OP_DUPでスタックの最上位要素を複製(<s> <s>
  3. スタックにRをプッシュ(<s> <s> <R>
  4. OP_SWAPで、スタックの上位2つの要素を入れ替える(<s> <R> <s>
  5. OP_CATでスタックの上位2つの要素を連結(<s> <R||s>
  6. スタックにPをプッシュ(<s> <R||s> <P>
  7. OP_CHECKSIGでスタック上の署名R||sと公開鍵Pに対して署名検証をする
  8. 最後にトランザクションデータ<s>のみがスタックに残る

7の署名検証をパスするためには、トランザクション自体が↑のように組み立てられたトランザクションになっていることが求められる。

+1の対応

sの値はH(m) + 1なので、実際には、スタック上で組み立てたトランザクションハッシュ値に対して+1を加算する必要がある。これは、Schnorrのトリックでも同じ。

まず、H(m)の値の最下位バイトが0x01となるようにトランザクションデータを調整する。これはトランザクションを組み立てる際にデータ(トランザクションのロックタイムとかインプットのシーケンス番号とか)を変更しながら0x01で終わるデータを見つけることで実現できる。このようなデータは平均256回の試行で見つかる。トランザクションのハッシュが0x01で終わるのがわかっていれば、署名値sの値は0x02で終わることになる。

そこで、↑のようにスタック上でトランザクションハッシュ値を計算はするのだけど、そのハッシュ値の最終バイトを削ったハッシュ値(31バイト値)を同じくwitnessとして提供させ、その値に01をOP_CATで付与したデータとスタック上で計算したハッシュ値が同じになることをOP_EQUALVERIFYでチェックしておく。そして、この31バイトのハッシュ値sに対して、OP_CATを使って02を付与して上記の署名検証をパスさせ、最終的にスタックに残るsに対してはOP_CATで01を付与する。

sの値を31バイト値として、この処理を行うよう↑の検証スクリプトを書き直すと、

OP_DUP 02 OP_CAT <R> OP_SWAP OP_CAT <P> OP_CHECKSIGVERIFY 01 OP_CAT

となる。

*1:こういうのもあって、Elementsではストリーミングハッシュ用opcodeとかを導入してるのかな?

1P1Cパッケージリレー

Bitcoin Core v28.0で新たにサポートされた機能の1つが1P1C(1 Parent 1 Child)パッケージリレー。パッケージリレーというのは、複数の関連トランザクションをまとめてP2Pネットワークでリレーできるようにする機能のこと。

複数の関連トランザクションのセットをパッケージとしてリレーするためには新しいP2Pメッセージを導入する必要がある↓けど、

techmedia-think.hatenablog.com

今回、Bitcoin Core v28.0で導入されたのは、既存のP2Pメッセージを利用して、親トランザクション1つ子トランザクション1つの計2つのトランザクションのみをリレーできるようにする限定的なパッケージリレー。

トランザクションリレーの仕組み

Bitcoinトランザクションのリレーは、以下のようにinvメッセージで新しいトランザクションを通知し、通知を受け取ったピアはそのトランザクションを持っていなければgetdataメッセージで対象のトランザクションを要求し、txメッセージでトランザクション本体を送信する仕組みになっている。

sequenceDiagram
    Node A->>Node B: inv
    Node B->>Node A: getdata
    Node A->>Node B: tx

Node Bは受け取ったトランザクションの有効性を確認し、問題がなければ自身のmempoolに追加し、自身と接続している他のピアにもそのトランザクションを通知する。

低手数料トランザクションの問題

ノードのmempoolのサイズは有限なので、mempoolがあふれるようになると、基本的に手数料率の低いトランザクションがmempoolから排出される。さらに新しいトランザクションを受け取っても、そのトランザクションの手数料がその時のmempoolの最小手数料率を下回るような場合は、mempoolに追加されない。自身のmempoolに追加されないトランザクションは他のピアにもリレーしない。

低手数料率のトランザクションが問題になるのは、mempoolの手数料率が高騰している状態で、LNのコミットメントトランザクションのように事前署名済みの低手数料率のトランザクションをブロードキャストしなければならない場合。mempoolの最小手数料率を下回ると前述したようにリレーされない。対象の親がmempoolに入らなければCPFPもできない。

1P1Cパッケージリレー

Bitcoin Core v28.0では、この問題を解決するために、1P1Cパッケージリレーを導入した。

まず先立って、Bitcoin Core v26.0で関連トランザクションのセットをパッケージとして送信するsubmitpackage RPCが追加された↓

rpc: allow submitpackage to be called outside of regtest by glozow · Pull Request #27609 · bitcoin/bitcoin · GitHub

これにより、ローカルのmempoolに格納するかどうかのチェックの際に、まず個々のトランザクションの手数料率がチェックされ、それがmempoolの最小手数料率を満たさない場合にパッケージとして手数料率が評価されるようになる。

これでローカルのmempoolのパッケージ評価ができるようになったので、あとはそれと組み合わせてパッケージをリレーする方法↓

sequenceDiagram
    Note over Node A,Node B:...
    Node A->>Node B: tx(低手数料の親)
    Note right of Node B: mempoolの最小手数料率を満たさないためリジェクト
    Note over Node A,Node B:...
    Node A->>Node B: tx(高手数料の子)
    Note right of Node B: 子の参照先を保持していないので親を要求
    Node B->>Node A: getdata(親)
    Node A->>Node B: tx(低手数料の親)
    Note right of Node B: 親子をパッケージとして評価してmempoolに格納
  1. 最初に低手数料率の親を受け取ると、mempoolの最小手数料率を満たさない場合、リジェクトされる。
  2. その後、高手数料率の子を受け取るが、この段階では参照先の親を持っていないので(missing input)、子はオーファン扱いになり、親トランザクションを要求する。
  3. 再度親を受け取ると、オーファンにいる子トランザクションと一緒にパッケージとして手数料率を評価し、mempoolの最小手数料率を満たしていれば、親子でmempoolに格納される。

という形で既存のP2Pメッセージをそのまま使いながら1P1Cの限定的なパッケージリレーをサポートしている。

親の受け取りを2回行うことになるので、使用する帯域幅は増える。これが1P1Cではなくさらに多くの関連トランザクションに拡大すると、ネットワークおよびノードへのオーバーヘッドが増え悪用される可能性もあるため、汎用的なパッケージリレーをサポートするためには、↑のBIP-331のような新しいP2Pメッセージの導入が必要になる。

手数料0の親トランザクション

トランザクション 1vBあたり1 satのという最小値の手数料を設定しなけければならないというポリシーがあるが、TRUC(v3)トランザクション↓を使用するとこれを回避して親トランザクションの手数料を0にすることもできる。

techmedia-think.hatenablog.com

たとえば、LNのコミットメントトランザクションを手数料0で作っておいて、ブロードキャストする際にCPFPで手数料を調整するといったことができるようになる。

※ 1P1CリレーはBitcoin Core v28.0で導入されたばかりなので、ネットワークで広く利用可能になるためには、ネットワークの大半がBitcoin Core v28.0になる必要がある。

オフチェーンベースの流動性提供スキームSuperScalar

LNのインバウンド流動性問題

LNでチャネルを新規に開設する場合、開設時は通常チャネル残高=自分の残高となる。これはチャネルを介して自分が支払うアウトバウンド流動性は保持しているけど、チャネルを介して支払いを受け取るインバウンド流動性はまったくない状態*1となる。

チャネル開設時に限定した話でもなく、資金の受け取りが主なユーザーにおいては、チャネル内の自分側の残高がいっぱいになれば、支払いを受け取ることができなくなってしまう。

そのため、チャネルのインバウンド流動性の管理は特に重要な問題となる。チャネルの流動性を管理しれくれるWoSのようなカストディアルウォレットを導入することで、これらの管理をアウトソースするという方法も選択肢の1つだけど、その場合代わりにサービスプロバイダーへのトラストが発生する。

SuperScalar

今回ZmnSCPxjが提案しているSuperScalar↓は、LSP(Lightning Service Provider)がユーザーとの間で、トラストレスかつ効率的に流動性の売買をできるようにするためのチャネルファクトリーの提案。

SuperScalar: Laddered Timeout-Tree-Structured Decker-Wattenhofer Factories - Protocol Design - Delving Bitcoin

この提案は、

という3つの要素で構成される。

Decker-Wattenhoferチャネルファクトリー

構成要素の1つがチャネルファクトリー↓

techmedia-think.hatenablog.com

チャネルファクトリーは、1つのオンチェーンUTXOで多数のLNチャネルをオフチェーン展開できるようにするスケーリングソリューションであり、またファクトリー内の複数のペイメントチャネル間のチャネルキャパシティの動的な調整を可能にする。

チャネルファクトリーの基本的な構造は、

  1. 多数のユーザーの資金を1つのUTXO(ファンディング・アウトポイント)にロックし*2
  2. ファンディング・アウトポイントを全参加者のn-of-nアウトプットに送信するキックオフトランザクションを作成し、
  3. キックオフトランザクションをインプットとしたステートトランザクションを作成し*3
  4. さらにステートトランザクション以下に、各参加者間でチャネルを構成するトランザクションが続く*4

チャネルファクトリーにおけるステートの更新は、チャネルの流動性を調整するタイミングで行われる。各ステートトランザクションのインプットのnSequenceには、相対的ロックタイムの値が設定されており、ステートを更新する度に、値がデクリメントされていく*5。これは、キックオフトランザクションがブロードキャストされた際に、最新のステートを一番早く承認させるため。このタイムロックにより古いステートを無効化する。

nSequenceの値が0になるとそれ以上更新できなくなるので、次は1つ上の階層のステートトランザクションのnSequenceをデクリメントとして、その子のnSequenceはリセットする。という作業を続け、一番上の階層まで使い終わったらファクトリーを一度閉じる必要がある。

このデクリメントによる状態更新の仕組みから、リーフのLNチャネル内では実質無制限の状態変更が可能だけど、チャネルファクトリーには状態変更の回数に上限がある。ステートトランザクションの階層を増やすことで上限を増やすことはできるけど、一方的なファクトリーの閉鎖が発生した場合に、ブロードキャストしなければならないトランザクション数が増えるというトレードオフが発生する。

タイムアウトツリー

ここでいうタイムアウトツリーは、LSPが複数のユーザー向けにライトニングチャネルを提供する際のトランザクションツリー。もともとOP_CTVを用いた提案だったけど、現状OP_CTVは導入されていないので、全参加者の署名による合意で代替するバージョンでタイムアウト署名ツリーと呼ばれてるっぽい。

LSP Lが8人のユーザーA〜Hにチャネルを提供する場合、以下のようなツリーを構成する:

リーフのトランザクションがLSPと各ユーザーとのLNチャネル。非リーフトランザクションの資金は以下の条件にロックされる:

  • その階層の参加者+LSPのマルチシグ or
  • CLTVによりタイムアウト後にLSPが単独で回収可能

ファクトリーを閉鎖する場合、

  • 協力的に閉鎖する場合、ユーザーはツリー内のチャネルの資金をLN経由ですべて送信する(オンチェーン資金とスワップしたり、同じLSPの新しいタイムアウトツリーに移すのも可)
  • ユーザーが一方的に退出する場合、ルートとなるファンディングトランザクションからリーフまでのトランザクションを公開し、LNチャネルは現状のLNと同様の方法で閉鎖する。

タイムアウトツリーを利用する利点は、単一のユーザーが退出した場合に他のユーザーは引き続きツリーに残ることができる。

理想的なケースでは、タイムアウトになるまでに全ユーザーがチャネルの資金を移動し、タイムアウト後にLSPがL&CLTV条件でファンディングのアウトプットを回収する。一部のユーザーが一方的に退出したとしても、ツリーの残りの資金は同様にタイムアウト後にL&CLTV条件で回収できる。

チャネルファトリーの状態更新の数は相対時間によるタイムロック(CSV)により制御される一方、タイムアウトツリーの方は絶対時間によるタイムロック(CLTV)なので、予め有効期限が決まっている。

タイムアウトツリー構造のチャネルファクトリー

SuperScalarでは、↑のチャネルファクトリーとタイムアウトツリーを組み合わせる。A〜Hのユーザーを持つLSPは以下のようなツリー構造となる。

ステートの更新の仕組みにチャネルファトリーの特性を利用し、CLTVによる指定ブロックにおけるタイムアウトと効率的な退出にタイムアウトツリーの特性を利用する感じ。

流動性の提供

上記の構成で、LSPがユーザーAに流動性を提供する場合、ツリーの一番上のA&L、B&L、Lのみのアウトプットを持つステートトランザクション

を更新する必要がある。この場合、更新にあたってオンラインになる必要があるのは、A、B、Lの三者のみ。流動性の提供をトリガーするAとLは基本的にオンラインになっているはずなので、あとはBがオンラインになるだけ。

  • Bがオンラインになれば、オフチェーンでA&Lのチャネルに資金移動できる。
  • Bがオンラインにならない場合は、LSPはオンチェーンなどの代替方法にフォールバックする必要があり、その場合のオンチェーン手数料はおそらくAに転嫁される。

Aへの流動性提供に直接関係のないBがオンラインになって協力するインセンティブとして、Bに少額のインバウンド流動性を提供するといったことが考えられてる模様。

もし、ステートトランザクションのLの資金が枯渇してしまった場合や、ステートトランザクションをこれ以上更新できなくなった場合(nSequneceを0までデクリメントした状態)は、別のステートトランザクションのLの資金を移動する必要がある。これは、1つ上の層のステートトランザクションを更新することを意味し、その場合、オンラインになる必要のあるユーザーが増える。Aに資金提供するケースだと、A&B&C&D&Lに資金がロックされているステートトランザクションを更新する必要があるので、オンラインなる必要があるのは、そのメンバー。

ファクトリーからの退出

各ユーザーはLSPをトラストする必要はないので、どのユーザーもLSPと協力することなく一方的に退出することができる。Aが一方的にファクトリーおよびLNチャネルから退出する場合、Aはツリー上のファンディングアウトプットから、A&Lチャネルまでのトランザクションをそれぞれブロードキャストする必要がある。今回の例では、最初のキックオフトランザクションからリーフのステートトランザクションまで合計4つのトランザクションをブロードキャストする。ステートトランザクションについては、最新の状態のもの。最後にA&LのLNチャネルを、これまでのLNの仕組みと同様にクローズする必要がある。

ただ、Aが一方的に退出すると、同じトランザクション内にチャネルを持つBと、さらにCとDは、これ以上LSPからオフチェーンで安価に流動性の提供を受けること(ステートトランザクションを更新すること)ができなくなる。CとDも影響を受けるのは、↓のキックオフトランザクションがオンチェーン上で承認されるため(CSVタイマーが起動し、最新のステートトランザクションをブロードキャストする必要がでてくる)。

B、C、Dについては、流動性の提供はできなくなるが引き続き各LNチャネルはそのまま利用できる。オフチェーンで流動性の購入ができなくなる分、必要な場合はオンチェーンスプライシングなど、オンチェーン手数料分コスト高になる。

E〜Hについては、キックオフトランザクションはまだ公開されていないので、引き続きオフチェーンで流動性の調整が可能。

全ユーザーが協調的に退出する場合は、上記のタイムアウトツリーで説明したように、各チャネルの資金を別のLNチャネルなどに移動したり、新しいファクトリーに移動する(後述)。

↑の例ではリーフトランザクションのチャネルの数は2つとしているけど、この数(arity)自体は任意に構成することができる。数が増えるとツリーの高さが抑えられ、

  • 一方的な退出時のオンチェーントランザクションの数を削減でき
  • トランザクション数が削減できるほど、資金を回収するまでのnSequenceによる相対的タイムロックの遅延が短くなる
    • LNチャネルのHTLCのCLTVデルタは、この相対的タイムロックの累計最大値を加味する必要がある。
  • 一方、流動性の調整時にオンラインになる必要のあるユーザー数が増える

というトレードオフがあり、arityの数や(地理的、稼働率などによる)ユーザーの配置については工夫の余地がある。

ラダー化

ラダー化というのは、技術的な話ではなく、金融商品の取り扱い方法の一種。定期預金のように、契約により一定期間資金を預け、満期になると元本に加えて利息を受け取るような場合、長期間資金がロックされるため資金運用の柔軟性という面ではデメリットがある。そこで、投資可能な金額を、終了日がそれぞれ異なる(1ヶ月おき、1年おきなど)複数の契約に分割することで資金運用の柔軟性を向上させる手法のことをラダー化と呼ぶ。

SuperScalarでは、LSPが有効期限の異なる複数のタイムアウトツリー構造のチャネルファクトリーを作成することで、LSPの資金運用の柔軟性を持たせようとしている。

LSPは、あるファクトリーのタイムアウト期限が来ると、新しいタイムアウトを設定したファクトリーを開始し、古いファクトリーから新しいファクトリーへの資金の提供を各ユーザーに依頼することができる。すべてのユーザーがオンラインになって新しいファクトリーに移動できたら、LSPはツリーの先端のキックオフトランザクションのL&CLTVの条件を使って資金を回収するトランザクションをブロードキャストする。一部のユーザーが応答ない場合でも、配下のユーザーがすべて応答したキックオフトランザクションについてはL&CLTV条件によって資金を回収できる。理想的なケースであれば、キックオフトランザクションとそれを回収するトランザクションの2つのオンチェーントランザクションのみでファクトリーを閉鎖することができる。

新旧のファクトリー間の移動については、OP_CTVがないので、各ユーザーとLSPが特定の期間内にオンラインになり協力する必要がある。

というのがSuperScalarスキームの概要。SuperScalarという名称は、プロセッサのアーキテクチャから来てる?ラダー化して複数のチャネルファクトリー運用する点が似てるとか。

*1:チャネルの両参加者が資金を提供するデュアルファンディングが一般的になれば、開設時にもインバウンド流動性を保持することができる

*2:前の記事でいうHookトランザクション

*3:前の記事でいうAllocationトランザクション

*4:前の記事でいうCommitmentトランザクション

*5:SuperScalarでは十分な時間を確保するために144ブロック単位でデクリメントするよう提案されている