techmedia-think.hatenablog.com
↑のCHECKLOCKTIMEVERIFYを使って、指定した期間まで出力を凍結するトランザクションを作ってみた。
資金凍結をするスクリプト
実際に作成したのは↓のScript
<expiry time> CHECKLOCKTIMEVERIFY DROP <pubkey> EQUALVERIFY CHECKSIG
expiry timeに凍結期限をセットし、pubkeyにはこのBitcoinの受信者のアドレスの公開鍵をセットする
この際expiry timeにセットする値はLocktimeと同じで、Locktimeは
https://bitcoin.org/en/developer-guide#locktime-and-sequence-number
に記載れているように、4バイトのunsigned integerで以下のルールでパースされる。
- 値が500万より小さい場合は、ブロック高としてパースする。
- 値が500万以上の場合は、UNIXエポックタイムのフォーマットでパースする。
bitcoin-rubyを使うと以下のようなScriptになる。(testnetを利用した場合)
include Bitcoin::Util Bitcoin.network = :testnet3 # 凍結した資金を受け取るアドレスの鍵 key = Bitcoin::Key.from_base58('秘密鍵') # 凍結期間(ブロック高で指定) locktime = 867892 # locktimeをリトルエンディンに ll = locktime.to_bn.to_s(2).reverse.unpack("H*")[0] # CHECKLOCKTIMEVERIFYを使って資金を凍結するスクリプト redeem_script = Bitcoin::Script.from_string("#{ll} OP_NOP2 OP_DROP #{key.pub} OP_CHECKSIG") # スクリプトから生成したP2SHアドレス p2sh_address = Bitcoin::Script.new(Bitcoin::Script.to_p2sh_script(hash160(redeem_script.to_payload.bth))).get_p2sh_address
ここで作成したP2SHアドレスに対してBitcoinを送付することで、そのBitcoinは↑で指定した期間までロックされる。
2MzP2YQd1dULG8DKKSuJDLZUhk7EiMjYSog
↑のアドレスにBitcoinを送付したトランザクション自体は通常通りconfirmされるが、↑で指定したブロック高(Locktime)までそのUTXOを使用することはできない。
このUTXOを使うトランザクションを作成すると、トランザクションの検証のため↑のredeem scriptが実行され、OP_NOP2(CHECKLOCKTIMEVERIFY)の命令コードによりLocktimeが評価される。この時、現在の最新ブロック高が対象のブロック高まで到達していないとスクリプトのインタプリタがエラーで終了するため、指定期間までロックされることになる。
資金を使用するスクリプト
凍結された資金を有効期間後に使用するスクリプトが↓
# 凍結されたUTXO txid = '2c4d452779b978165c007b6075a9388e1fc340bae82f2cc76a2f77d84b00a8ad' vout = 1 # 資金をロックするスクリプト def hold_script(pubkey, locktime) ll = locktime.to_bn.to_s(2).reverse.unpack("H*")[0] Bitcoin::Script.from_string("#{ll} OP_NOP2 OP_DROP #{pubkey} OP_CHECKSIG") end # 資金を使用するトランザクションの作成 def spend_unsigned_tx(locktime, txid, vout, address) tx = Bitcoin::Protocol::Tx.new tx.lock_time = locktime tx_in = Bitcoin::Protocol::TxIn.from_hex_hash(txid, vout) tx_in.sequence = [0].pack("V") tx.add_in(tx_in) tx_out = Bitcoin::Protocol::TxOut.new(送付するBitcoinの量, Bitcoin::Script.new(Bitcoin::Script.to_address_script(address)).to_payload) tx.add_out(tx_out) tx end # ロックされたUTXOの署名スクリプト def spend_script(priv_key, locktime, unsigned_tx, n) redeem_script = hold_script(priv_key.pub, locktime) tx = Marshal.load(Marshal.dump(unsigned_tx)) tx.in[0].script_sig = redeem_script.to_payload sig_hash = tx.signature_hash_for_input(n, redeem_script.to_payload, Bitcoin::Script::SIGHASH_TYPE[:all]) sig = priv_key.sign(sig_hash) + [Bitcoin::Script::SIGHASH_TYPE[:all]].pack("C") Bitcoin::Script.new(Bitcoin::Script.pack_pushdata(sig) + Bitcoin::Script.pack_pushdata(redeem_script.to_payload)) end to = '凍結された資金の送付先のアドレス' # 凍結されたUTXOを入力としたトランザクションを作成 unsigned_tx = spend_unsigned_tx(locktime, txid, vout, to) # ロックされたUTXOを使用するための署名を作成&トランザクションにセット spend_script = spend_script(key, locktime, unsigned_tx, 0) unsigned_tx.in[0].script_sig = spend_script.to_payload # 凍結された資金を利用するトランザクションのシリアライズデータ serialized_tx = unsigned_tx.to_payload.bth
指定期間後にserialized_txの内容をブロードキャストすれば、凍結されていたBitcoinが送付できる。
実際にブロードキャストしたトランザクションのTXIDが
dacf01f1e7fa1d32f51c921aa190815865a28aa1b52f34d69371af8116a3fd6f
ちなみに生成した↑のトランザクションを指定した期間より前にブロックチェーンにブロードキャストしようとするとすると(Bitcoin Coreの場合)
{"code"=>-26, "message"=>"64: non-final"}
というエラーが返ってくる。
UTXOの認識方法
P2SHアドレスが持つUTXOについては、P2PKHのように秘密鍵から作成されたアドレスではないため、Bitcoin Coreのlistunspentを実行してもそのUTXOは確認できない。(そもそもredeem script自体がウォレットに保存されるわけでもないので) 対象のP2SHアドレスをimportaddressでreadonlyなアドレスとして登録すると、listunspent等でP2SHのアドレスのUTXOを確認できるようになるが、readonlyなアドレスとしてインポートしているので、spendableはfalseと表示される。
$ bitcoin-cli -testnet importaddress 2Mwek43uEomtrz5AqK4vxfANA39RX9xhJL8
所感
- ↑のスクリプトは単純なスクリプトだけど、他の命令コードと組み合わせることで、できることの幅が広がる。
- CHECKLOCKTIMEVERIFYに限った話ではないけど、P2SHなスクリプト宛にBitcoinを送付する場合、redeem script自体の管理ってみんなどうしてるんだろう?
Bitcoin Core含めredeem scriptをストアするようなウォレットは無いように思うので、ウォレットとは別のシステムで管理してる? - CHECKLOCKTIMEVERIFYのスクリプトは最終的にはP2SHなアドレスになるので、Bitcoinに限らず、(Bitcoinのプロトコル上に定義されている)Open Assets Protocolでアセットを送る先に指定することも可能で、今回のようなスクリプトを作ることで指定期間までアセットを凍結するということもできる。
- Bitcoinだとそんなに意味が無いかもしれないが、Open Assetsだとアドレスに対して指定期間内にしか使えないアセット(期間限定キャンペーン)を発行することができそう。