読者です 読者をやめる 読者になる 読者になる

Develop with pleasure!

福岡でCloudとかBlockchainとか。

OP_CSVで指定期間まで資金を凍結

Bitcoin Ruby

以前、絶対時間を使用するOP_CLTV(BIP-65)で指定期間まで資金をロックするスクリプトを作成したので↓

techmedia-think.hatenablog.com

今回は、相対時間を使用するOP_CSV(BIP-112)↓を使って同様のことをやってみる。

techmedia-think.hatenablog.com

OP_CLTVとOP_CSVの違い

どちらを使っても資金のロックは可能だが、以下のような違いがある。

  • OP_CLTVはスクリプト内でCHECKLOCKTIMEVERIFY(OP_NOP2)の引数に与えられた時間とnLocktimeを比較し、nLocktimeを過ぎていない場合はスクリプトインタプリタはエラーになりブロードキャストできない(nLockTimeに対してCHECKLOCKTIMEVERIFYの引数を比較することで、指定されたブロック高もしくはブロックタイムに達しているか)。
    CLTVで指定するロックタイムは絶対時間(ブロック高 or タイムスタンプ)。
  • OP_CSVスクリプト内でCHECKSEQUENCEVERIFY(OP_NOP3)の引数に与えられた時間と入力のnSequenceを比較し、引数で指定されたロックタイムより前のnSequenceが設定された場合はスクリプトインタプリタはエラーになりブロードキャストできない。
    CSVで指定するのは出力がブロックに含まれてからの相対的な経過時間(ブロック数 or 512秒単位の時間)

資金を凍結する

CLTVの時と同様、一定期間資金をロックするスクリプトが↓

<expiry time> CHECKSEQUENCEVERIFY DROP <pubkey> EQUALVERIFY CHECKSIG

regtestモードで、bitcoin-rubyを使って実装したコードが↓

今回ロックタイムはブロックベースで5ブロックほどロックするように記載。

require 'bitcoin'
include Bitcoin::Util
Bitcoin.network = :regtest

alice_key = Bitcoin::Key.from_base58('アリスの秘密鍵')

lock_block = 5 # ロックするブロック数

def hold_script(alice_key, lock_block_count)
  Bitcoin::Script.from_string("#{lock_block_count} OP_NOP3 OP_DROP #{alice_key.pub} OP_CHECKSIG")
end

redeem_script = hold_script(alice_key, lock_block)

p2sh = Bitcoin::Script.new(Bitcoin::Script.to_p2sh_script(hash160(redeem_script.to_payload.bth))).get_p2sh_address

を実行して生成したP2SHアドレスにBitcoinを送ると、そのUTXOは5ブロック経過するまで誰も利用できず、5ブロック経過するとアリスの秘密鍵で利用可能になる。

資金を使用する

↑でロックした資金を使用するトランザクションを作成。

# 資金をロックしたUTXOのトランザクション
txid = '7fc2ef9d486582e525dcd1eb279128efba822d796a6034dbf56ebff8ccd36b53' 
vout = 1

def spend_script(key, unsigned_tx, redeem_script)
  tx = Marshal.load(Marshal.dump(unsigned_tx))
  tx.in[0].script_sig = redeem_script.to_payload
  sig_hash = tx.signature_hash_for_input(0, redeem_script.to_payload, Bitcoin::Script::SIGHASH_TYPE[:all])
  sig = 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

def spend_unsigned_tx(sequence, txid, vout, address)
  tx = Bitcoin::Protocol::Tx.new
  tx.ver = 2 # トランザクションのバージョンが2より小さいとBIP-68に対応していないとみなされる
  tx_in = Bitcoin::Protocol::TxIn.from_hex_hash(txid, vout)
  tx_in.sequence = sequence
  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

# 資金を使用するトランザクションの作成
unsigned_tx = spend_unsigned_tx([lock_block].pack("V"), txid, vout, '資金の送付先のアドレス')

# ロックされたUTXOを使用するための署名を作成&トランザクションにセット
alice_script = spend_script(alice_key, unsigned_tx, redeem_script)
unsigned_tx.in[0].script_sig = alice_script.to_payload

# 凍結された資金を利用するトランザクションのシリアライズデータ
serialized_tx = unsigned_tx.to_payload.bth

指定期間後にserialized_txの内容をブロードキャストすれば、凍結されていたBitcoinが送付できる。

ちなみにUTXOのトランザクションがブロック含まれてから、まだ5ブロック経過しないうちにブロードキャストしようとするとBitcoin Coreの場合は以下のエラーが発生する。

{"code"=>-26, "message"=>"64: non-BIP68-final"}

所感

  • CLTVと違ってロックする期間を相対時間で設定できるので、柔軟にコントラクトを作れるようになる。
  • あとはSegwitがリリースされれば双方向Payment Channelが実装できそう。
  • CSVに限った話ではないけど、P2SHのredeem scriptの管理やそのアドレスの残高管理する仕組みは欲しい。