Develop with pleasure!

福岡でCloudとかBlockchainとか。

Taroのアセット発行の仕組み

前回の記事で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アウトプットを作成する。

  1. ウォレットが保持している任意のUTXOをピックアップし、アセット発行に使用するOutPointを選択する。
  2. 1をgenesis_pointとして、asset_tagとasset_metaからアセットIDを計算する。
  3. 新規発行するアセットのアセットツリーを構築する(ツリー構造については↑の記事参照)。
    1. 新規発行する2のアセットIDのアセットツリー(2階層めのツリー)のリーフノードを作成する(asset_leaf || leaf_sum)。
    2. 生成したリーフを含むMS-SMTのルートハッシュを計算する。
    3. 計算したルートを使って、アセットツリー(1階層めのツリー)であるMS-SMTのリーフを作成する(taro_version || asset_id_tree_root || asset_sum)。
    4. 作成したリーフを持つMS-SMTのルートハッシュを計算する。
  4. Taprootの内部鍵用の鍵を生成する。
  5. アセットツリーのルートハッシュをリーフとしたTapscriptのツリーを作成し、内部鍵と合わせてTaprootアウトプットを作成する。

このプロセスを詳しくみていく。

アセットIDの計算

まず最初にアセットIDは、前回の記事から、

asset_id = sha256(genesis_outpoint || asset_tag || asset_meta || output_index || asset_type)

として計算されるので、genesis_pointname(=タグ)、metaoutput_indexasset_type(NORMAL = 0)から以下のように計算できる(bitcoinrbを使用)。

require 'bitcoin'

genesis_point = Bitcoin::OutPoint.from_txid('af2ed8bc93a28cba80bc3c8c55fc554bb7cb3f9071f59f87b83724d4e8b92ea9', 1)
tag = Bitcoin.sha256('testcoin')
asset_meta = Bitcoin.sha256("6d6574616461746120666f722074657374".htb) # 6d6574616461746120666f722074657374 = "metadata for test"
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(アセットの所有者の管理化にある公開鍵)
  • Valueasset_leaf || leaf_sumをリーフの値とし、Sumとして発行量leaf_sumを持つリーフノード。

KeyのAsset Script Keyは、↑のtarocliで発行したアセットの場合↓の公開鍵になる。

"script_key": "026e552eef52663dcf984ad1443660a66923aca4c8b77240fb83114dbb0ea91269"

Valueであるリーフノードは、asset_leafleaf_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
# 発行に使用するUTXOの指定
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 # ↑で計算したのと同じアセットID

# 2階層めのツリーを構築
asset_commitment = Taro::AssetCommitment.new([asset])
# 1階層めのツリーを構築
taro_commitment = Taro::TaroCommitment.new([asset_commitment])

# 内部鍵
internal_key =
  Bitcoin::Key.new(
    pubkey:
      "02fe3155363518765636316775fec96b57e454c3dd50ce19d18da5a1f9cf91b3a7"
  )

#Taprootのアウトプットを作成
taproot_script_pubkey = taro_commitment.to_taproot(internal_key)
taproot_script_pubkey.to_addr # tarocliが発行したTxと同じTaprootのアドレス

最後のアドレスはtaroclianchor_outpointのUTXO(478aa4a3461ed88447badd87ecda2eda1b86ece7488d16a3c81e5f6d28cfff57:0)と同じアドレスになる。

以上が、Taroのアセット発行の手順。新規発行の場合は、アセットの所有者の検証などないので、次はアセットを転送する際の検証処理など見ていきたい。