Develop with pleasure!

福岡でCloudとかBlockchainとか。

Asset Definition URLをP2SHのredeem scriptに設定する

Open Assets Protocolではアセットのメタデータを定義したAsset DefinitionをURLで指定する。

techmedia-think.hatenablog.com

↑に記載されているように、Asset Definition URLとブロックチェーン上のアセットを紐付ける方法は以下の2種類がある。

  • アセット発行時のMarker Outputのmetadetaフィールドで設定する
  • P2SHを使ってアセットを発行し、そのP2SHのredeem script内に設定する

openassets-rubyは前者の方法でAsset DefinitionのURLを設定・解釈するように実装している。このMarker Outputに付加する方法は、アセットの発行をウォレットがデフォルトでサポートしているP2PKHベースで行え、設定も用意にできるといったメリットがある。しかし反面、アセットを追加発行する際に別のAsset Definition URLを設定することも可能で、Asset IDは同じでも参照しているAsset Definitionが異なるといったデメリットもある。

P2SHを使う方法では、P2SHであるためそのUTXOを使うにはredeem scriptを知っている必要があり、P2PKHなどに比べると手間がかかるというデメリットはあるが、Asset Definition URLがP2SHのredeem scriptの一部になっているため、Asset Definition URLを変えてアセットを発行するとAsset IDが変わることになり、Asset Definition URLの一意性を担保できる。それ以外にも、P2SHのredeem scriptのサイズの上限は520バイトであるため、Marker Outputの上限80バイトに比べて、より多くのデータを格納することができる。

Asset Definition URLを含むP2SHの作成

まず以下のようにしてAsset Definition URLを含むP2SHのスクリプトを作成する。

require 'openassets'

# 最初の引数がAsset Definition URLで、2番目の引数がこのP2SHを使用する際に必要な署名を持つOpen Asset Address。
p2sh = OpenAssets::Protocol::AssetDefinitionLoader.create_pointer_p2sh('https://goo.gl/bmVEuw', 'bX3yaoWQ5KTVmucut1jx7LapV6gy5V3sjjL')

この時内部で以下のredeem scriptを作ってる。

PUSHDATA <最初の引数で指定したURL> OP_DROP OP_DUP OP_HASH160 <2つ目のアドレスの公開鍵のハッシュ> OP_EQUALVERIFY OP_CHECKSIG

Open Assets Protocolで定義されているのはPUSHDATAからOP_DROPまでなので、それ以降は任意のopcodeを使って自由にスクリプトを作れる。今回は通常のP2PKHのスクリプトを付与している。

あとはP2SHの仕様に従って↓P2SHスクリプトを作っている。

OP_HASH160 <redeem scriptのハッシュ> OP_EQUAL

P2SHまで作れたら

p2sh.get_p2sh_address

でP2SHのアドレスが分かるのでそのアドレス宛にBitcoinを送っておく。

続いて、いよいよアセットの発行を行う。先ほどのP2SHアドレスに送ったBitcoinのUTXOを入力にセットしたアセット発行用のトランザクションを作成し、上記のredeem scriptを満たす署名を付与する↓

from_tx = Bitcoin::Protocol::Tx.new(api.provider.get_transaction('b7e225d9988796f26c2fdd672795b2d27c667d298cf1db169cdf11ab8b064844').htb)
tx = Bitcoin::Protocol::Tx.new
tx.add_in(Bitcoin::Protocol::TxIn.from_hex_hash(from_tx.hash, 1))
tx.add_out(Bitcoin::Protocol::TxOut.value_to_address(600, 'mvZM34fU6wdDqF3gKd2tYA67vjWvwBHbDU'))
script = OpenAssets::Protocol::MarkerOutput.new([99999]).build_script
tx.add_out(Bitcoin::Protocol::TxOut.new(0, script.to_payload))
tx.add_out(Bitcoin::Protocol::TxOut.value_to_address(39400, 'mvZM34fU6wdDqF3gKd2tYA67vjWvwBHbDU'))

from_key = Bitcoin::Key.from_base58('bX3yaoWQ5KTVmucut1jx7LapV6gy5V3sjjL=(Bitcoinアドレス:mt1hZLajqyc63NkWy7qvgiuum5nuTBdVZ6)の秘密鍵')
redeem_script = OpenAssets::Protocol::AssetDefinitionLoader.create_pointer_redeem_script('https://goo.gl/bmVEuw', 'bX3yaoWQ5KTVmucut1jx7LapV6gy5V3sjjL')
sig_hash = tx.signature_hash_for_input(0, redeem_script.to_payload)
sig = Bitcoin::Script.to_pubkey_script_sig(from_key.sign(sig_hash), from_key.pub.htb)
script_sig = Bitcoin::Script.new(sig + Bitcoin::Script.pack_pushdata(redeem_script.to_payload))
tx.in[0].script_sig = script_sig.to_payload
puts tx.to_payload.bth

作成したトランザクションをブロードキャストする。実際にtestnetにブロードキャストしたトランザクションが↓
a98b000f28c9e8251925a1225832edf1c2992e103445b6b2cec3f5c0a81469e5

{
  "hex": "01000000014448068bab11df9c16dbf18c297d667cd2b2952767dd2f6cf2968798d925e2b7010000009d47304402202254f7da7c3fe2bf2a4dd2c3e255aa3ad61415550f648b564aea335f8fcd3d92022062eab5c01a5e33eb726f976ebd3b35d3991f8a45da56d64e1cd3fd5178f8c9a6012102effb2edfcf826d43027feae226143bdac058ad2e87b7cec26f97af2d357ddefa3217753d68747470733a2f2f676f6f2e676c2f626d564575777576a9148911455a265235b2d356a1324af000d4dae0326288acffffffff0358020000000000001976a914a4fdb3ce954a3cbb49fdfc9df1712bf72749c9e788ac00000000000000000b6a094f410100019f8d0600e8990000000000001976a914a4fdb3ce954a3cbb49fdfc9df1712bf72749c9e788ac00000000",
  "txid": "a98b000f28c9e8251925a1225832edf1c2992e103445b6b2cec3f5c0a81469e5",
  "hash": "a98b000f28c9e8251925a1225832edf1c2992e103445b6b2cec3f5c0a81469e5",
  "size": 296,
  "vsize": 296,
  "version": 1,
  "locktime": 0,
  "vin": [
    {
      "txid": "b7e225d9988796f26c2fdd672795b2d27c667d298cf1db169cdf11ab8b064844",
      "vout": 1,
      "scriptSig": {
        "asm": "304402202254f7da7c3fe2bf2a4dd2c3e255aa3ad61415550f648b564aea335f8fcd3d92022062eab5c01a5e33eb726f976ebd3b35d3991f8a45da56d64e1cd3fd5178f8c9a6[ALL] 02effb2edfcf826d43027feae226143bdac058ad2e87b7cec26f97af2d357ddefa 17753d68747470733a2f2f676f6f2e676c2f626d564575777576a9148911455a265235b2d356a1324af000d4dae0326288ac",
        "hex": "47304402202254f7da7c3fe2bf2a4dd2c3e255aa3ad61415550f648b564aea335f8fcd3d92022062eab5c01a5e33eb726f976ebd3b35d3991f8a45da56d64e1cd3fd5178f8c9a6012102effb2edfcf826d43027feae226143bdac058ad2e87b7cec26f97af2d357ddefa3217753d68747470733a2f2f676f6f2e676c2f626d564575777576a9148911455a265235b2d356a1324af000d4dae0326288ac"
      },
      "sequence": 4294967295
    }
  ],
  "vout": [
    {
      "value": 0.00000600,
      "n": 0,
      "scriptPubKey": {
        "asm": "OP_DUP OP_HASH160 a4fdb3ce954a3cbb49fdfc9df1712bf72749c9e7 OP_EQUALVERIFY OP_CHECKSIG",
        "hex": "76a914a4fdb3ce954a3cbb49fdfc9df1712bf72749c9e788ac",
        "reqSigs": 1,
        "type": "pubkeyhash",
        "addresses": [
          "mvZM34fU6wdDqF3gKd2tYA67vjWvwBHbDU"
        ]
      }
    }, 
    {
      "value": 0.00000000,
      "n": 1,
      "scriptPubKey": {
        "asm": "OP_RETURN 4f410100019f8d0600",
        "hex": "6a094f410100019f8d0600",
        "type": "nulldata"
      }
    }, 
    {
      "value": 0.00039400,
      "n": 2,
      "scriptPubKey": {
        "asm": "OP_DUP OP_HASH160 a4fdb3ce954a3cbb49fdfc9df1712bf72749c9e7 OP_EQUALVERIFY OP_CHECKSIG",
        "hex": "76a914a4fdb3ce954a3cbb49fdfc9df1712bf72749c9e788ac",
        "reqSigs": 1,
        "type": "pubkeyhash",
        "addresses": [
          "mvZM34fU6wdDqF3gKd2tYA67vjWvwBHbDU"
        ]
      }
    }
  ],
  "blockhash": "0000000000000349ade641e1d0b0615cd8535d7790690e2d8b501907ec30b6c7",
  "confirmations": 307,
  "time": 1475927768,
  "blocktime": 1475927768
}

ブロックチェーン上のトランザクションからAsset Definition URLを確認する

MarkerOutputmetadataを使う方法であれば、発行トランザクションがブロックチェーンに記録されると、そのMarkerOutputをパースすればURLは分かるが、P2SHを使う場合はアセット発行トランザクションの入力に使ったP2SHのUTXOのredeem scriptにURLが定義されているので、その入力のscriptSigをパースする。(redeem scriptはそのUTXOが使用される際、そのUTXOを参照する入力のscriptSigの一部として公開される。)

↑の発行トランザクションの最初の入力がP2SHのアセット発行トランザクションなので、この中にredeem scriptが含まれている。そのscriptSigは以下のように3つのデータからなり

"asm": "304402202254f7da7c3fe2bf2a4dd2c3e255aa3ad61415550f648b564aea335f8fcd3d92022062eab5c01a5e33eb726f976ebd3b35d3991f8a45da56d64e1cd3fd5178f8c9a6[ALL] 02effb2edfcf826d43027feae226143bdac058ad2e87b7cec26f97af2d357ddefa 17753d68747470733a2f2f676f6f2e676c2f626d564575777576a9148911455a265235b2d356a1324af000d4dae0326288ac",

最初が署名、2つ目が公開鍵で、最後がredeem scriptになる。このredeem scriptをパースすると

redeem_script = Bitcoin::Script.new('17753d68747470733a2f2f676f6f2e676c2f626d564575777576a9148911455a265235b2d356a1324af000d4dae0326288ac'.htb)
puts redeem_script.to_string
> 753d68747470733a2f2f676f6f2e676c2f626d56457577 OP_DROP OP_DUP OP_HASH160 8911455a265235b2d356a1324af000d4dae03262 OP_EQUALVERIFY OP_CHECKSIG

となり先頭のデータがAsset Definition URLになるので、これをデコードすればURLが入手できる。

今回openassets-rubyでも、後者のP2SHを使ったAsset Definition URLのパースをできるようにしたので、対象アセットのUTXOを持つノードでlist_unspentを実行するとAsset Definition URLも取得できる

// api.list_unspentの実行結果↓
...
{
  "txid": "a98b000f28c9e8251925a1225832edf1c2992e103445b6b2cec3f5c0a81469e5",
  "vout": 0,
  "confirmations": 310,
  "address": "mvZM34fU6wdDqF3gKd2tYA67vjWvwBHbDU",
  "oa_address": "bX6XEHEUoaRWuhVD3NF95C1zhGLh6qqqk5d",
  "script": "76a914a4fdb3ce954a3cbb49fdfc9df1712bf72749c9e788ac",
  "amount": "0.00000600",
  "asset_id": "oMb2yzA542yQgwn8XtmGefTzBv5NJ2nDjh",
  "asset_quantity": "99999",
  "asset_amount": "9999.9",
  "account": "p2sh asset",
  "asset_definition_url": "https://goo.gl/bmVEuw",
  "proof_of_authenticity": false,
  "output_type": "issuance",
  "solvable": true,
  "spendable": true
},
...

1点気になるのが、CoinprismのエクスプローラでURLが確認できないこと。P2SHのAsset Definitionに対応していないのか?それともこちらのURLのエンコード方法が間違っているのか?

P2SHを使ってデータを記録するということ

Open Assets Protocolに限った話ではなく、ブロックチェーン上に任意のデータを記録する場合、よく用いられるのがOP_RETURNを使う方法だ。実際Open Assets ProtocolトランザクションMarker Outputを付与するのにOP_RETURNを使っている。

ただOP_RETURNで付与できるデータサイズは80バイトであり、余裕あるサイズではない。

それに比べて、redeem scriptのサイズは520バイトでプッシュできるデータ量が多い。ただ、OP_RETURNが出力で使われるのに対し、redeem scriptが明らかになるのは入力であるという違いがある。このためOP_RETURNを使う方法とはデータ公開のタイミングが異なることを注意する必要はあるが、記録データ量は格段に多いので、redeem scriptを使ったデータ記録というアプローチもありだと思う。