Taproot Assets Protocol を理解する(仕様編)
Taproot Assets Protocol(旧名 Taro。以下、TAP と称す) はビットコインネットワークのオーバレイネットワークでビットコインとは異なる UTXO ベースモデルのアセットを表現するためのプロトコルである。この記事では、TAP によるアセットの生成と転送の仕組みの理解を目的としている。
仕様は以下であるが、ここではもっぱら bip-tap と bip-tap-ms-smt について説明する。
Lightning Labs による実装はこちら。
Taproot とは?
Taproot とは Bitcoin の新しい出力のタイプで、Key Path
とScript Path
という2通りの支払い条件を同時に指定することができる。
Key Path
は従来の P2PKH のように公開鍵が指定され、その署名によって支払われる。シュノア署名による鍵集約(マルチシグ)も使用できる。
Script Path
は従来の P2SH のようにスクリプト言語によって複雑な支払い条件を指定できる。また、複数のスクリプトを同時に指定できる。これらのスクリプトは Taproot 出力に直接シリアライズされて組み込まれるのではなく、マークルツリーによって構造化されそのルートハッシュに圧縮される。個々のスクリプトはその支払い条件で支払われる場合にのみオンチェーンで公開される。
TAP では、Script Path
に独自のデータ(Asset Tree)を埋め込む。このデータは Bitcoin の観点からは解釈できないものであるが、ハッシュ化されているのでそのようなデータが埋め込まれていることも感知されない。
Asset Tree
Taproot Asset は、2階層の Merkle Sum Sparse Merkle Tree(以下、MS-SMT と称す)構造の Asset Tree で表現される。下位の MS-SMT はasset_id
で識別される単一のアセットの UTXO の集合を表す。上位の MS-SMT は下位の MS-SMT を集約する。
上位の MS-SMT のルートの値(asset_tree_root
)が Taproot 出力のScript Path
に埋め込まれ、ツリーの状態がコミットされる。
アセットの転送に伴い、新たな Asset Tree が生成されたり、元のものが更新されたりする。これらには Taproot 出力を消費する新たな Bitcoin トランザクション の発行が伴う。TAP の仕様を満たしていない不正な転送(例えばインフレや二重支払い)は端的に無効なものとみなされる。*
* これはいわゆる Client side validation と呼ばれるコンセプトである。Bitcoin の創発的コンセンサスを知っている人は検証される仕様の正しさの根拠が気になるかもしれないが、今は問題にしない。
Merkle Sum Sparse Merkle Tree
Merkle Sum Sparse Merkle Tree(以下、MS-SMT と呼ぶ)は bip-tap-ms-smt で定義されるマークルツリーの変種である。キーが 256 bit なので、2 ^ 256 個のリーフを持つがそのほとんどは空である。
各リーフには数値が含まれ、各ブランチには、そこのサブツリーのリーフの合計値が記録される。サブツリーの中身がわからなくても、ブランチを見るだけでそこに含まれる合計値を知ることができるようになっている。ルートにはそのツリーの合計値が記録される。
一般的なマークルツリーのように、任意のリーフを含むサブツリー(Merkle Proof)によってそのリーフの包含証明が表現できるが、MS-SMT では非包含証明もサポートしている。これは存在しないキーのリーフを明示的に非存在を示す値(None)にしなくてはいけないという制約によって実現される(None の包含証明 = 非包含証明)。従ってデフォルトのツリーは 2 ^ 256 個 の None リーフを持つ。
Asset Leaf(Asset UTXO)
Asset Tree の下位の MS-SMT はasset_script_key
をキーとして、Asset Leaf を値として持つ。個々のAsset Leaf は アセットの UTXO (以下簡単のため UTXO と称す * )を表す。Asset Leaf にはオプショナルなものも含め、以下の項目が Type-length-value フォーマットでシリアライズされる。
* Bitcoin のそれではなく。
taproot_asset_version
asset_genesis
asset_id
asset_type
amt
lock_time
relative_lock_time
prev_asset_witnesses
-prev_asset_id
-asset_witness
-split_commitment_proof
split_commitment
asset_script_version
asset_script_key
asset_group_key
canonical_universe
asset_genesis
にはasset_id
を導出するためのデータの preimage が含まれる。
asset_script_key
はこの Asset Leaf のキーであると同時に、(Bitcoin とは別のバーチャルな tap-vm 上の)Taproot タイプの公開鍵となっており、この Asset Leaf が表現する UTXO を消費するときの支払い条件となっている。
UTXO を消費するとき、(外部の)Bitcoin の支払い条件を満たすとともに、prev_asset_id
とasset_witness
によって(内部の)TAP のasset_script_key
による支払い条件も満たさないといけない。asset_witness
は、例えば消費した UTXO のためのasset_script_key
による署名である。
転送で UTXO が分割される場合、split_commitment
とsplit_commitment_proof
必要になる。
split_commitment
は、分割後の全ての UTXO の参照を含む MS-SMT で(このケースでは Asset Tree は3層になる)、ルートの合計値は転送されたアセットの合計値になる。
split_commitment_proof
は、分割の存在を証明するsplit_commitment
の Merkle Proof である。
全ての分割のうち、ひとつだけがprev_asset_id
、 asset_witness
、 split_commitment
を持つ。それ以外の分割はsplit_commitment_proof
を持つ。全ての分割はprev_asset_id
、asset_witness
を共有する。
アセットの生成
アセットの生成は、生成される UTXO を含む新規の Asset Tree を Bitcoin トランザクション(アセットの生成トランザクション)の Taproot 出力に埋め込むことによって行われる。アセットの ID(asset_id
)の定義は次の式である。
asset_id := sha256(genesis_outpoint || sha256(asset_tag) || asset_meta_hash || output_index || asset_type)
genesis_outpoint
は当該トランザクションが消費する前のトランザクションの出力を示し、output_index
は当該 Asset Tree が含まれる当該トランザクションの出力位置を示す。これらを含むことによりasset_id
はグローバルに一意の値になることが保証される。
新規に生成された UTXO には、prev_asset_id
、asset_witness
は含まれない。
アセットの転送
アセットの転送では、Bitcoin と同じく、UTXO のマージ/分割が発生する場合がある。まずは、いずれも発生しない単純なケースからはじめる *。
* 以下の説明ではいくつか簡略化している。例えば Bitcoin のお釣り出力は省略している。
Alice が、10 単位の UTXO を持っていて、そのまま分割なしに 10 単位を Bob に転送する。
Alice は UTXO が保存されている Taproot 出力を消費する Bitcoin トランザクションを作りブロードキャストする。これは、新たに2つの Taproot 出力を持つ。一つは Bob がコントロールできる(例えば、Bob の Bitcoin 公開鍵によって)10 単位の UTXO を含んだ新規の Asset Tree を含む。もう一つは Alice がコントロールできる元の Asset Tree から当該 アセット UTXO が取り除かれた Asset Tree を含む。
Bob のための新しい UTXO は、amt
が 10 で、prev_asset_id
は Alice の前の UTXO を参照する。asset_witness
には、前のasset_script_key
による署名が配置される。当該 UTXO のasset_script_key
には、事前に Bob から貰った新しいそれが配置され、これは下位の MS-SMT のキーでもある。
Bob はアセットの受け取りを確認するために、支払い条件が満たされ、転送の前後でアセットがインフレしていないか検証する必要がある。
- 新しく作成された Bob の Asset Tree に、支払い条件を満たした新しい UTXO が存在するか?
- 更新された Alice の Asset Tree から、元の UTXO が取り除かれたか?
- トランザクションに他の Taproot 出力が存在する場合には、そこに別の Asset Tree が含まれていないか?*
- 等々。
これらが MS-SMT の包含/非包含証明および Taproot の preimage と包含/非包含証明によって証明/検証される。
*このような Asset Tree に、元の UTXO を消費する Bob に対するものとは別の UTXO が追加されていた場合、二重支払いになってしまう。
UTXO のマージ
Alice が3単位と7単位の UTXO を持っていて、合わせて 10 単位を Bob に転送する。
各入力 UTXO は異なる Asset Tree に別々に属するかもしれないし、同じツリーの異なるキー(asset_script_key
)に属するかもしれない。Bob の新しい UTXO に、二つの UTXO を消費するそれぞれのprev_asset_id
とasset_witness
を含めればいい。
UTXO の分割
Alice が 10 単位の UTXO を持っていて、そのうち 7 単位を Bob に転送する。結果として Alice がお釣りの 3 単位、Bob が 7 単位の UTXO を持つことになる。
Alice のお釣り UTXO には、prev_asset_id
とasset_witness
、あとsplit_commitment
が配置される。Bob の新たな UTXO にはsplit_commitment_proof
が含まれる。
UTXO のマージと分割が同時に行われるケースの説明はいらないだろう。
* 仕様の記述と若干異なると思うが、おそらく現在の実装ではこうなっている。
UTXO の出自の検証
ここまでの説明で言及しなかったが、アセットの転送が検証される前に、各入力 UTXO が有効なものであるのか検証されなければならない。そのためには、各入力 UTXO について、アセットの根拠となる生成トランザクションからその UTXO までのルートにおける全ての転送が正しく行われたことが検証されなければならない。
生成トランザクションから最新のトランザクションまで、そのルートは複雑なトランザクショングラフを描く。上図のTx A
ではその祖先の青色のトランザクションを全て検証する必要がある。Tx B
ではそれに加えて赤色のトランザクションも検証する必要がある。履歴は準指数関数的に増加するためスケーラビリティの大きな課題である。
また検証に必要な preimage や証明はオンチェーンに公開されないので、アセット転送時に送信者から受信者にどのようにして受け渡すかも課題である。
スケーラビリティ
履歴の増加はスケーラビリティの大きな課題である。いくつか解決策が提案されているが Lightning Network への対応は最も期待されているものの一つである。オフチェーントランザクションでは履歴が増加しない。
このプロトコルはまだ新しく、仕様は不完全な部分がある。例えば、以前私が指摘したこの問題がある。