前回の記事でTaroのアセットツリーの構造を確認したので↓
techmedia-think.hatenablog.com
今回は、Taroでのアセット発行の仕組みをRubyで実装しながらみていく。
tarocliを使ったアセットの発行
tarocliのassets mint
コマンドを利用するとアセットを新規発行することができる。実際にtestnetで発行してみたのが↓
$ tarocli assets mint --type normal --name testcoin --supply 1000 --meta "metadata for test" --skip_batch
{
"batch_key": "02fe3155363518765636316775fec96b57e454c3dd50ce19d18da5a1f9cf91b3a7"
}
$ tarocli assets list
{
"assets": [
{
"version": 0,
"asset_genesis": {
"genesis_point": "af2ed8bc93a28cba80bc3c8c55fc554bb7cb3f9071f59f87b83724d4e8b92ea9:1",
"name": "testcoin",
"meta": "6d6574616461746120666f722074657374",
"asset_id": "9333d8360be674dfae320b7ae16bd3b618726637fad107a1d2b248a3083061ca",
"output_index": 0,
"genesis_bootstrap_info": "a92eb9e8d42437b8879ff571903fcbb74b55fc558c3cbc80ba8ca293bcd82eaf000000010874657374636f696e116d6574616461746120666f7220746573740000000000",
"version": 0
},
"asset_type": "NORMAL",
"amount": "1000",
"lock_time": 0,
"relative_lock_time": 0,
"script_version": 0,
"script_key": "026e552eef52663dcf984ad1443660a66923aca4c8b77240fb83114dbb0ea91269",
"asset_family": null,
"chain_anchor": {
"anchor_tx": "02000000000101a92eb9e8d42437b8879ff571903fcbb74b55fc558c3cbc80ba8ca293bcd82eaf0100000000ffffffff02e8030000000000002251204a8b4d59fede637b63c2473c06234615a7c82406912954f744e358e74922e254e9e500000000000016001496c99700a2d53b57904468a77e0c3db3da7463660140f5af40e5a9bcc96a57aa0d7a696e05f3752e0b150dd414fb0f5796e9b4ff625b3efaf06dc90733d0b6b949126d3a0b24bc1db796e54984234654e6a9c4e72a2000000000",
"anchor_txid": "57ffcf286d5f1ec8a3168d48e7ec861bda2edaec87ddba4784d81e46a3a48a47",
"anchor_block_hash": "0000000000000000000000000000000000000000000000000000000000000000",
"anchor_outpoint": "478aa4a3461ed88447badd87ecda2eda1b86ece7488d16a3c81e5f6d28cfff57:0",
"internal_key": "02fe3155363518765636316775fec96b57e454c3dd50ce19d18da5a1f9cf91b3a7"
}
}
]
}
※ anchor_txidは、エンディアンが逆になってるので要注意
今回は、この発行処理の中身を見ていく。
アセットの発行手順
アセットを発行するということは、Taprootベースのアセット発行用のTaprootのアウトプットを作成するということで、Taroでは以下の手順でアセット発行用のTaprootアウトプットを作成する。
- ウォレットが保持している任意のUTXOをピックアップし、アセット発行に使用するOutPointを選択する。
- 1をgenesis_pointとして、asset_tagとasset_metaからアセットIDを計算する。
- 新規発行するアセットのアセットツリーを構築する(ツリー構造については↑の記事参照)。
- 新規発行する2のアセットIDのアセットツリー(2階層めのツリー)のリーフノードを作成する(
asset_leaf || leaf_sum
)。
- 生成したリーフを含むMS-SMTのルートハッシュを計算する。
- 計算したルートを使って、アセットツリー(1階層めのツリー)であるMS-SMTのリーフを作成する(
taro_version || asset_id_tree_root || asset_sum
)。
- 作成したリーフを持つMS-SMTのルートハッシュを計算する。
- Taprootの内部鍵用の鍵を生成する。
- アセットツリーのルートハッシュをリーフとしたTapscriptのツリーを作成し、内部鍵と合わせてTaprootアウトプットを作成する。
このプロセスを詳しくみていく。
アセットIDの計算
まず最初にアセットIDは、前回の記事から、
asset_id = sha256(genesis_outpoint || asset_tag || asset_meta || output_index || asset_type)
として計算されるので、genesis_point
、name
(=タグ)、meta
、output_index
、asset_type
(NORMAL = 0)から以下のように計算できる(bitcoinrbを使用)。
require 'bitcoin'
genesis_point = Bitcoin::OutPoint.from_txid('af2ed8bc93a28cba80bc3c8c55fc554bb7cb3f9071f59f87b83724d4e8b92ea9', 1)
tag = Bitcoin.sha256('testcoin')
asset_meta = Bitcoin.sha256("6d6574616461746120666f722074657374".htb)
output_index = [0].pack('I>')
type = [0].pack('C')
payload = genesis_point.to_payload + tag + asset_meta + output_index + type
asset_id = Bitcoin.sha256(payload)
=> "9333d8360be674dfae320b7ae16bd3b618726637fad107a1d2b248a3083061ca"
アセットツリーの構築
Taroのアセットツリーは、↑の記事に書いたように2階層のツリーで構成されている。
2階層めのツリー
まず最初に、TaroのアセットID毎のツリー(MS-SMT)を作成する。このツリーに挿入するKey-Valueのペアは、
- Key:Asset Script Key(アセットの所有者の管理化にある公開鍵)
- Value:
asset_leaf || leaf_sum
をリーフの値とし、Sumとして発行量leaf_sum
を持つリーフノード。
KeyのAsset Script Keyは、↑のtarocli
で発行したアセットの場合↓の公開鍵になる。
"script_key": "026e552eef52663dcf984ad1443660a66923aca4c8b77240fb83114dbb0ea91269"
Valueであるリーフノードは、asset_leaf
とleaf_sum
結合したデータで、asset_leaf
はアセットに関連する項目をLightningでおなじみのTLVフォーマットで定義した内容になる(現状、こことかFamily Keyのバイト数とか一部BIPドラフトとtarod
のα版とでどうも仕様のズレがある)。
↑の単一のアセットの発行のみであれば、空のMS-SMTを作成し、↑のKey-Valueのデータを1つツリーに挿入してアセットツリーを作成すればいい。
このツリーは、あるアセットの各所有者とその所有量を管理するツリーとなる。
1階層めのツリー
続いて↑の記事の1階層めのツリー(MS-SMT)を作成する。このツリーに挿入するKey-Valueのペアは、
- Key:アセットID(Asset Family Keyが設定されている場合は、その公開鍵のSHA-256値)
- Value:リーフノードの値として
taro_version || asset_id_tree_root || asset_sum
を持ち、アセットIDツリーのsum値を持つリーフノード。ここでasset_id_tree_root
は先程作成したツリーのルートハッシュで、asset_sum
は先程作成したツリーのルートのsum値。
つまり、1階層めのツリーは、アセットIDをキーにそのアセットの所有者及び所有量がコミットされている2階層めのツリーに対するコミットを行うツリーとなる。
Taprootアウトプットの作成
アセットツリーが構成できたら、それにコミットするTaprootのアウトプットを作成する。Taprootの作成には、内部鍵(Internal Key)とTapscriptのツリーが必要になる。
Taprootの内部鍵
Taprootの内部鍵はウォレットが作成することになる。今回tarod
が生成した内部鍵は↑の出力結果から↓
"internal_key": "02fe3155363518765636316775fec96b57e454c3dd50ce19d18da5a1f9cf91b3a7"
Tapscriptツリー
続いて、↑のアセットツリーにコミットするTapscriptのツリーを構成する。今回みたいな単一のアセットの発行では、条件となるScriptブランチが1つのTapscriptツリーを構成すればいい。
この時、ツリーに挿入するリーフのスクリプトは以下のペイロードのスクリプトになる。
leaf_version || taro_marker || taro_version || asset_tree_root || asset_tree_sum
version
は、アセットツリー内のアセットのバージョンの最大値(現状0)
taro_marker
は、マーカー用のタグで、値はsha256("taro")
asset_tree_root
は↑の1階層めのツリーのルートハッシュ
asset_tree_sum
は↑の1階層めのツリーのsum値
↑から分かるように、Bitcoin Scriptとして有効なデータではなさそう。つまり、これをそのままTaprootのTapscriptとして評価するような使い方はしないということかな。
今回の発行の仕組みでは、単純にこの単一のスクリプトをリーフとしたTapscriptのツリーを作って、そのルートハッシュと、↑の内部鍵を使ってTaprootのアウトプットを作ればいい。
サンプル実装
試しにtarocli
が作成したアセット発行トランザクションと同じTaprootアウトプットをRubyで書いてみた(個別の処理を書いてくと長くなるので、tarorbというライブラリとして実装)。
require 'taro'
Bitcoin.chain_params = :testnet
genesis_point =
Bitcoin::OutPoint.from_txid(
"af2ed8bc93a28cba80bc3c8c55fc554bb7cb3f9071f59f87b83724d4e8b92ea9",
1
)
genesis =
Taro::Genesis.new(
prev_out: genesis_point,
tag: "testcoin",
metadata: "metadata for test",
output_index: 0
)
asset_script_key =
Bitcoin::Key.new(
pubkey:
"026e552eef52663dcf984ad1443660a66923aca4c8b77240fb83114dbb0ea91269"
)
asset = Taro::Asset.new(genesis, 1000, 0, 0, asset_script_key)
asset_id = asset.genesis.id
asset_commitment = Taro::AssetCommitment.new([asset])
taro_commitment = Taro::TaroCommitment.new([asset_commitment])
internal_key =
Bitcoin::Key.new(
pubkey:
"02fe3155363518765636316775fec96b57e454c3dd50ce19d18da5a1f9cf91b3a7"
)
taproot_script_pubkey = taro_commitment.to_taproot(internal_key)
taproot_script_pubkey.to_addr
最後のアドレスはtarocli
のanchor_outpoint
のUTXO(478aa4a3461ed88447badd87ecda2eda1b86ece7488d16a3c81e5f6d28cfff57:0)と同じアドレスになる。
以上が、Taroのアセット発行の手順。新規発行の場合は、アセットの所有者の検証などないので、次はアセットを転送する際の検証処理など見ていきたい。