最近Lightning Labsが発表したUTXOをマルチパーティで共有する新しいスケーリングソリューションLayer Treeについて↓
論文は、ショートバージョンとフルバージョンの2種類公開されている。
ペイメントチャネルは二者間でUTXOを共有するモデルだけど、こちらは以前記事にしたCoin Poolのように多数のユーザーでUTXOを共有するタイプのソリューション。

ロール
Layer Treeでは以下の3つのロールが登場する。
- オペレーター:共有UTXOをセットアップし運営する主体。新しいメンバーをツリーに追加したり、メンバーの残高の変更に対して共同で署名する。また、ライトニングの中継役としてメンバー間の送金の仲介を行う。ただし、参加者の資金には手を出せない。
- オーナー:共有UTXO内に資金を持つユーザー。いつでも自分の判断で、残高を引き出すことができる。また自分の残高の一部を切り出して他のユーザーに渡したり、後から換金できるバウチャー(後述)の発行などが可能。リーフの書き換え(鍵や期限、残高の変更)にはオペレーターとの連携が必要。
- エビクター:リース期限(後述)を過ぎたメンバーを強制的に退出させることができる。各残高には期限を設定でき、期限が切れると、エビクターがその残高を共有UTXOから外すことができる。外す場合、残高は全額オーナーの受け取り先に支払われる。通常、オペレーターが兼務する。
共有UTXOのセットアップ
参加者の各資金は、オペレーターが運営するマークルツリーのリーフにエンコードされる。
オペレーターはまず、共有UTXOを作るにあたって以下のルールを決める:
- ツリーの大きさ:最大で何人までメンバーを受け入れられるようにするか。
- サポートする操作:一度に何人まで受け入れるか、まとめて退出できるか、バウチャーを利用可能にするかなど。
- オペレーターの鍵:メンバーの加入の承認などに使用
- 使用するBitcoinのティア(後述)
これらのルールは、共有UTXOのアドレスにコミットされ、後から変更することはできない。
ルールが決まったら最初のツリーを構成する(最初はオペレーター自身の残高だけでもOK)。初期メンバーとその残高をコミットしたツリーを作成する。ツリーの各リーフは、以下のデータで構成される固定長の116 byteのレコード。
| フィールド | byte数 | エンコード | 内容 |
|---|---|---|---|
amount |
8 | le64*1 |
残高(単位はsatoshi) |
exit_key |
32 | x-only公開鍵 | 退出時の受取先の公開鍵 |
owner_key |
32 | x-only公開鍵 | オーナーの公開鍵(退出、更新、バウチャーの認可に使用) |
evictor_key |
32 | x-only公開鍵 | エビクターの公開鍵 |
evict_after |
4 | le32*2のブロック高 |
リース期限 |
nonce |
4 | le32 |
リーフ識別用のnonceで作成時に選択 |
seq |
4 | le32 |
CLAIM/SPLIT毎にインクリメントする番号で、バウチャーのリプレイ防止 |
このリーフデータのタグ付きハッシュとamountを元にMerkle-Sum Treeを構成する。そしてツリーのルートハッシュを計算し(root_hashとする)、それを使ってTaprootの内部鍵を計算する↓
P = NUMS + int(H_tag("LayerTree/state", root_hash))・G
※ NUMSはNUMSポイント。内部鍵は上記の形で導出されるのでkey-pathでの引き出しはできない。このツリーは、Tapscriptを含むTaptreeではなく、Layer Treeの参加者の資金にコミットしたツリーで、それがTaprootの内部鍵にコミットしていることになる。
続いて、以下のTapleafを含むTaptreeを構成する:
| Tapleaf | やること | 必要な署名者 |
|---|---|---|
EXIT_1 |
リーフを1つツリーから取り除き、取り除いたリーフの金額をexit_keyへ支払う |
オーナー(期限後はエビクター) |
EXIT_K |
K人分の退出を1 Txで連続実行する | 各リーフのオーナー/エビクター |
JOIN_h |
ツリー内の高さhの部分木に、新メンバーのグループをまるごと差し込む | オペレーター |
JOINK_K |
1リーフずつのJOINをK回行う | オペレーター |
UPDATE |
リーフを同じ位置で更新する | オーナー+オペレーター |
CLAIM |
バウチャーでリーフを切り出す | バウチャー署名 |
SPLIT |
署名でリーフを切り出す | オーナー |
EVICT_SUBTREE_h |
期限切れのコホートを一括で追い出し | オペレーター |
REWRITE_h |
ツリー内の部分木を一括で置き換え | 変更されるリーフのオーナー+オペレーター |
DETACH_h |
部分木を別のツリーのアウトプットに切り出す | オペレーター |
RECLAIM_1 |
期限後に回収可能なリーフをオペレーターが取り戻す*3 | RECLAIM_KEY |
このTapscript内に冒頭のツリーのルールもコミットされる。上記のTapleafのロジックの実装方法は使用するティア(後述)によって異なる。
上記のTaptreeのルートハッシュと先程の内部鍵からP2TRアドレスを導出し、Merkle-Sum Treeでコミットされた総額と同じ金額をそのアドレス宛に送金することでセットアップが完了する。
※ このセットアップ時には、ツリー内の総額とアウトプットの金額が一致することはコンセンサスで保証されないので、オフチェーンのツリーデータと突き合わせてセットアップの正しさの検証が必要になる。以降の状態遷移においては、コベナンツによってツリーの金額とアウトプットの金額の正しさはコンセンサスで保証されるようになる。
メンバーの参加
共有UTXOのセットアップ後、新しいメンバーの参加はすべてツリーの状態遷移として行われる。JOIN_hを使って、h=0なら1人、hが大きければ最大2h人をまとめて参加させることができる。
コホートのグラフト
一緒に参加するメンバーのグループをコホートと呼ぶ(通常、同じリース期限を共有する)。ツリー内の高さhのノードの下には2h個の連続したリーフがぶら下がっていて、この区画がすべて空の場合、この区画の頂点のノードは空ノードを積み上げた結果になるので既知の定数()になる。
オペレーターは、コホートのメンバーをリーフとして、高さhの部分木を構成し、その頂点ノードのハッシュ値と合計金額をの位置に差し込むことで、複数のメンバーをまとめてツリーに参加させることができる。これをグラフトと呼ぶ。
グラフト前(D=3) グラフト後
root root'
/ \ / \
N(2,0) E_2 ← 空き区画 N(2,0) C ← コホートの頂点
/ \ / \ / \ / \
. . E_1 E_1 . . . .
/ \ / \ / \ / \ / \ / \ / \ / \
A B - - - - - - A B - - W X Y Z
JOIN_hのwitnessは以下で構成される:
- 部分木の頂点からルートまでのマークルパス
- 新しい部分木の頂点のハッシュと合計金額
- オペレーターの署名
- 現在のルートハッシュと、Taptreeのルートハッシュ
これらのwitnessを用いて、スクリプトで以下の検証が行われる
- witnessのルートハッシュとTaptreeのルートハッシュから計算したscriptPubkeyが、使用するインプットのscriptPubkeyと一致すること。これでツリーの状態とルールの両方を確認する。
からマークルパスを使ってルートを計算すると現在のルートになり、その合計金額がインプットの金額と一致すること。これによりツリー内の
の区画が空いていたことを確認する。
- 新しい部分木の頂点ノードから同じマークルパスを使って、新しいルートを計算する。
- Txのアウトプットが新しいルートと同じTaptreeから導出したP2TRで、金額が、「旧総額+コホートの合計額」になっていること。
増額分の資金は、トランザクションの別のインプットで補うことになるが、それが誰の資金かはコベナンツでは関知しない。
スクリプトは差し替える部分木の位置より上の層しか計算しないので、コホートが大きいほどトランザクションは小さくなる。
参加者による検証
コベナンツが確認するのは部分木の頂点のハッシュと合計額だけで、その下に本当にリーフが存在するかは検証しない。中身のない部分木がグラフトされると、合計額は合っていても誰もその下のリーフを証明できないため、その分の資金は凍結される(既存のメンバーの残高には影響しない)。
そのため、参加者は資金を投入する前に、未署名のトランザクションについて以下を確認する必要がある:
- インプットが、期待する金額で現在のツリーのアウトプットを使用している
- 空き区画から、マークルパスを使って現在のルートを計算できる
- 自分のリーフから、部分木内のパスを使ってグラフトする頂点ノードを計算できる(合計額も再計算)
- アウトプットが、新しいルートと「旧総額+部分木の合計額」にコミットしている
- 自分のリーフのフィールド(金額、鍵、エビクター、リース期限)が合意通りである
- 自分のインプットとお釣りが合意通りである
確認後、すべてのアウトプットにコミットする署名をすれば、自分の資金は「自分のリーフを含むツリー」を作るトランザクションにしか使われない。
資金の拠出方法
- オペレーターが立て替える:オペレーターがコホートの合計額を全額出し、メンバーはグラフトの承認後に、ライトニングやハッシュロック付きバウチャーで残高を購入する。メンバーが支払わないリスクはオペレーターが負う。1人あたりのコストが最も小さい。
- 各メンバーが拠出する:各メンバーがグラフトTxに自分のインプットを追加して署名する(1人あたり約58 vB増)。全員が署名するまでブロードキャストできないので、1人でも不在だとコホート全体が止まる。
- ミックス:一部をオペレーター、一部をメンバーが拠出する。
リース期限
evict_afterは、そのリーフのリース期限を表すブロック高で、賃貸の契約期間のようなもの。この高さを過ぎると、エビクターがそのリーフをツリーから強制的に外せるようになる(nLockTimeがevict_after以上のトランザクションでのみ有効)。ただし外す場合も、リーフの金額は全額exit_key宛に支払う必要があるので、エビクターが資金を奪えるわけではない。
オペレーターから見ると、リース期限は席を貸す期間を区切る仕組みで、主に以下の目的で使われる:
- 死蔵リーフの回収:連絡がつかなくなったメンバーの席を、期限後に空けることができる。
- コホートの一括追い出し:同じリース期限を共有するコホートは、期限後に
EVICT_SUBTREE_hで1トランザクションでまとめて追い出せる。
メンバー側は、以下のような選択ができる:
- 期限の更新:オペレーターと合意して
UPDATEでevict_afterを延ばす。 - 追い出されない設定:期限を遠い将来にするか、
evictor_keyを誰も秘密鍵を知らないNUMSポイントにする。 - 期限0:いつでもエビクターが強制的に外せる必要があるリーフ(チャネルを載せたリーフなど)は
evict_after = 0にする。
退出
オーナーはいつでも、オペレーターの協力なしに、1トランザクションでツリーから退出できる。タイムロックも事前署名済みのトランザクションも不要。
EXIT_1のwitnessは以下で構成される:
- 自分のリーフの位置からルートまでのマークルパス
- 116 byteの自分のリーフデータ
- 署名と、オーナーによる退出かエビクターによる追い出しかを示すフラグ
- 現在のルートハッシュとTaptreeのルートハッシュ
スクリプトは以下を検証する:
- ルートハッシュとTaptreeのルートハッシュから計算したscriptPubKeyが、使用中のインプットのscriptPubKeyと一致する
- リーフからマークルパスを使ってルートを計算すると、現在のルートになり、その合計額がインプットの金額と一致する
- リーフを空ノード
に置き換えて同じパスで新しいルートを計算する
- 署名を検証する
- オーナーの場合:
owner_keyによるトランザクションへの署名(全アウトプットにコミットするもの) - エビクターの場合:
evictor_keyによる署名、かつnLockTimeがevict_after以上
- オーナーの場合:
- 2つめのアウトプットがP2TR(
exit_key)宛になっている。金額は、オーナーの場合はリーフの金額以下(差額は手数料に回せる)、エビクターの場合はリーフの金額と完全に一致 - 1つめのアウトプットが、新しいルートとTaptreeから導出したP2TRで、金額が「旧総額−リーフの金額」になっている
退出トランザクションのサイズは、深さ16のツリーで約1,020 vB。1インプット1アウトプットの通常のP2TRトランザクション(約111 vB)の約9倍だけど、メンバー数には依存せず、ツリーの深さだけで決まる。同規模のトランザクションツリー(Ark型)から単独で退出する場合は約2,511 vBになる。
複数のリーフをまとめて取り除く操作もあり、1人あたりのコストはまとめるほど小さくなる。EVICT_SUBTREE_hとREWRITE_hは、部分木の内部ノードを1回ずつしか計算しないので、人数が増えるほど1人あたりのコストが下がる。
バウチャー
バウチャーは、オーナーが「自分のリーフから、この内容の新しいリーフを切り出してよい」と署名したメッセージで、いわば小切手のようなもの。受け取った人は、後から好きなタイミングで、発行者の関与なしにCLAIMを使ってこれをツリー上で換金できる。まだツリーに参加していない人にも、バウチャーを渡すことでツリー内でビットコインを送れる。
バウチャーの署名対象は、以下のメッセージのタグ付きハッシュで、
H_tag("LayerTree/voucher", tree_id || body(source_leaf) || claim_leaf)
tree_id:ツリーの識別子。ツリーをセットアップしたTxの0番めのインプットのアウトポイントから導出。body(source_leaf):切り出し元のリーフデータからamountを除いた108 byteclaim_leaf:切り出して作る新しいリーフ(116 byte)
CLAIMは、以下の2ステップを1トランザクションで実行する:
- 切り出し元のリーフの金額を新しいリーフの分だけ減らし、
seqを1増やす(金額が0になればその位置は空になる)。 - ステップ1の後のツリーで空いていることが証明された位置に、新しいリーフを配置する。
ツリーの総額は変わらないので、共有UTXOの金額も変わらない。またスクリプトは、以下のルールも強制する:
- 新しいリーフの
evictor_keyは切り出し元と同じで、evict_afterも切り出し元より後にできない。これがないと、追い出されそうなメンバーが、残高を追い出されないリーフに逃がせてしまう。 - 新しいリーフの金額と、切り出し元に残る金額は、いずれも
DUST_MIN(推奨330 sat)以上。
ステップ1でseqがインクリメントされるので、切り出し元のリーフの中身が変わり、同じバウチャーの署名は二度と検証を通らない。つまり、バウチャーは1回しか使えない。
※ またバウチャーの署名時からamount以外のリーフデータが更新された場合、バウチャーは無効になるので、基本的にバウチャーは受け取ったらすぐ使う必要がある。
制約
すべての状態遷移は同じUTXOを使用して再作成するので、未承認の遷移はトランザクションのチェーンになる。Bitcoin Coreのリレーポリシー(v31.0より前は未承認の子孫25個、以降のクラスターmempoolであれば64個)によって、他人同士が1人ずつ退出する速度には上限がある。また、チェーンの手前に手数料の低い遷移が1つあると、後続すべての手数料率が抑えられるというピンニングの課題もある。
構造的な対策は、デプロイメントを複数のツリーに分割することになる模様。DETACH_hでコホートを別のアウトプットに切り出すこともできる。
ティア
Layer TreeをBitcoinで利用可能にするためにはコベナンツのサポートが必要になる。Layer Tree自体は独自のコベナンツopcodeは提案しておらず、現在提案中のopcodeを組み合わせて以下の4種類の実現方法を提案している。
| ティア | 必要なopcode | サポート操作 | EXIT_1のスクリプト/witness |
退出Tx |
|---|---|---|---|---|
| FULL | BIP-441、OP_TX、OP_CHECKSIGFROMSTACK、楕円曲線の演算opcode |
すべて | 2,101 / 902 B | 1,020 vB |
| MID | OP_CAT、OP_TX、OP_CHECKSIGFROMSTACK、OP_TWEAKADD |
すべて*4 | 3,367 / 978 B | 1,355 vB |
| MIN | OP_CAT、OP_TWEAKADD |
EXIT_1とSPLITのみ |
3,640 / 1,175 B | 1,473 vB |
| CCV | OP_CHECKCONTRACTVERIFY、OP_TX、OP_CHECKSIGFROMSTACK*5 |
すべて | 1,716 / 838 B | 908 vB |
ツリーのデータフォーマットは全ティア共通で、ティアごとに違うのはTapleafのスクリプトの実装方法になる。ツリーはどれか1つのティアで作成し、作成後に変更することはできない。スクリプトのバイト列が違うので、同じパラメータでもティアが違えばTaptreeのルートハッシュも異なる。
いずれにせよ、これらのいずれかの組み合わせを満たすopcode群がソフトフォークで導入される必要がある。
Coin Poolとの違い
どちらも多数のユーザーで1つのUTXOを共有し、各ユーザーが他の参加者の許可なくいつでも退出できる、という目標は同じ。ただし、仕組みは大きく異なる。
| Coin Pool | Layer Tree | |
|---|---|---|
| オペレーター | なし | あり |
| 状態の更新 | 全員の署名 | 関係者(オーナー+オペレーター)の署名 |
| 状態の表現 | 集約公開鍵+参加者ごとのTapleaf+事前署名Tx | Merkle-Sum Treeのルート(内部鍵)+共通の遷移ルール(Taptree) |
| 単独退出 | Update Tx → タイムロック → Withdraw Tx | 1トランザクション |
| 退出後の残りのメンバー | Snapshot Txで作り直し | 何もしなくていい |
| メンバーの追加 | 全員で作り直し | オペレーターによるグラフト |
| オペレーターへのトラスト | 不要 | 活性のみ(加入、リース後の継続、協調操作)。資金の安全性は不要 |
| 退出の集中 | 課題 | 課題 |