Develop with pleasure!

福岡でCloudとかBlockchainとか。

bitcoin-rubyを使ったマルチシグアドレスの作成と署名

bitcoin-rubyを使ってマルチシグの作成とマルチシグでロックされたUTXOへの署名をする。

他の言語のライブラリもそうだけど、bitcoin-rubyBitcoin::ScriptクラスでP2SHを使ったマルチシグアドレスの作成と署名をサポートしている。

マルチシグアドレスの作成

マルチシグはBitcoin::Script#to_p2sh_multisig_scriptで作成できる。(今回はP2SHで作るマルチシグ)

# generate p2sh multisig output script for given +args+.
# returns the p2sh output script, and the redeem script needed to spend it.
# see #to_multisig_script for the redeem script, and #to_p2sh_script for the p2sh script.
def self.to_p2sh_multisig_script(*args)
  redeem_script = to_multisig_script(*args)
  p2sh_script = to_p2sh_script(Bitcoin.hash160(redeem_script.hth))
  return p2sh_script, redeem_script
end

マルチシグのP2SHスクリプトを作成しているのはこの中のto_multisig_script

# generate multisig output script for given +pubkeys+, expecting +m+ signatures.
# returns a raw binary script of the form:
#  <m> <pubkey> [<pubkey> ...] <n_pubkeys> OP_CHECKMULTISIG
def self.to_multisig_script(m, *pubkeys)
  raise "invalid m-of-n number" unless [m, pubkeys.size].all?{|i| (0..20).include?(i) }
  raise "invalid m-of-n number" if pubkeys.size < m
  pubs = pubkeys.map{|pk| pack_pushdata([pk].pack("H*")) }

  m = m > 16 ?              pack_pushdata([m].pack("C"))              : [80 + m.to_i].pack("C")
  n = pubkeys.size > 16 ?   pack_pushdata([pubkeys.size].pack("C"))   : [80 + pubs.size].pack("C")

  [ m, *pubs, n, [OP_CHECKMULTISIG].pack("C")].join
end

で、作成しているスクリプトは一般的なマルチシグのスクリプトの形式

m key1 ... keym n OP_CHECKMULTISIG

で作成されていることが分かる。

実際にこれでマルチシグアドレスを作ろうとすると

# アリスとボブの2つのアドレスでロックする
alice_key = Bitcoin::Key.from_base58('aliceの秘密鍵')
bob_key = Bitcoin::Key.from_base58('bobの秘密鍵')

p2sh_script, redeem_script =  Bitcoin::Script.to_p2sh_multisig_script(2, alice_key.pub, bob_key.pub)
multisig_addr = Bitcoin::Script.new(p2sh_script).get_p2sh_address

で、2-of-2のマルチシグアドレスが作成できる。

redeem_scriptが↑のm of nのマルチシグのスクリプトで、それをP2SHスクリプトにしたのがp2sh_script。
P2SHのスクリプトは↓の形式のスクリプトになる。

OP_HASH160 redeem_scriptのハッシュ値 OP_EQUAL

そのp2sh_scriptからP2SHのアドレスを作成し、そのアドレスに資金を送ることで、マルチシグアドレスに資金がロックされる。ロックを解除する(ロックされたBTCを使用する)にはアリスとボブの秘密鍵を使った署名が必要になる。

マルチシグでロックされたUTXOへの署名

署名もメソッドが用意されている。↓のBitcoin::Script#to_p2sh_multisig_script_sig

# generate input script sig spending a p2sh-multisig output script.
# returns a raw binary script sig of the form:
#  OP_0 <sig> [<sig> ...] <redeem_script>
def self.to_p2sh_multisig_script_sig(redeem_script, *sigs)
  to_multisig_script_sig(*sigs.flatten) + pack_pushdata(redeem_script)
end

や、Bitcoin::Script#add_sig_to_multisig_script_sig

# take a multisig script sig (or p2sh multisig script sig) and add
# another signature to it after the OP_0. Used to sign a tx by
# multiple parties. Signatures must be in the same order as the
# pubkeys in the output script being redeemed.
def self.add_sig_to_multisig_script_sig(sig, script_sig, hash_type = SIGHASH_TYPE[:all])
  signature = sig + [hash_type].pack("C*")
  offset = script_sig.empty? ? 0 : 1
  script_sig.insert(offset, pack_pushdata(signature))
end

これらを使って、マルチシグでロックされたUTXOを使うトランザクションを作成する↓

# マルチシグにロックしたUTXOの情報
locked_txid = '298a820bf6b372f192bf6e42108a8e1ac3856a3b0b2257c46e0ef24ac888453d'
locked_vout = 1

# マルチシグにロックされたUTXOを使用するトランザクションの作成
tx = Bitcoin::Protocol::Tx.new
tx.add_in(Bitcoin::Protocol::TxIn.from_hex_hash(locked_txid, locked_vout))
tx.add_out(Bitcoin::Protocol::TxOut.value_to_address(1000, 'ロックされた資金の送付先アドレス'))

# ロックされたUTXOの署名の作成
sig_hash = tx.signature_hash_for_input(0, redeem_script)

script_sig = Bitcoin::Script.to_p2sh_multisig_script_sig(redeem_script)
# ボブの鍵を使った署名を追加
script_sig = Bitcoin::Script.add_sig_to_multisig_script_sig(bob_key.sign(sig_hash), script_sig)
# アリスの鍵を使った署名を追加
script_sig = Bitcoin::Script.add_sig_to_multisig_script_sig(alice_key.sign(sig_hash), script_sig)

tx.in[0].script_sig = script_sig
tx.to_payload.bth

最後に出力したトランザクションのpayloadをBitcoin Coreのsendrawtransaction APIでブロードキャストすることで、アンロックされたUTXOを使用できる。

と簡単に書いたけど、↑の署名部分をもう少し詳しくみていく(署名の方法はマルチシグに限った話ではない)。

署名の方法

まず最初に、作成したトランザクションの0番目の入力のためにトランザクションのハッシュを生成している。第三引数にハッシュタイプを指定できるが、省略するとSIGHASH_ALL*1が採用される。

sig_hash = tx.signature_hash_for_input(0, redeem_script)

signature_hash_for_inputを実行すると、トランザクションのversion、入力の数、入力、出力の数、出力、ロックタイム、ハッシュタイプといったデータからハッシュ値を生成している。 この時トランザクションのどのデータまでを含むかを指定するのがハッシュタイプで、SIGHASH_ALLの場合、これら全データでハッシュを生成することになる。
このトランザクションのデータから作られたハッシュに対して署名をする。

続いて、以下のコードでscript sigのテンプレートを生成している。

script_sig = Bitcoin::Script.to_p2sh_multisig_script_sig(redeem_script)

ここで生成されるマルチシグのscript sigは以下のスクリプトバイナリデータになる。

OP_0  OP_2 アリスの公開鍵 ボブの公開鍵 OP_2 OP_CHECKMULTISIG

その後↓のアリスとボブの署名が続く。

script_sig = Bitcoin::Script.add_sig_to_multisig_script_sig(bob_key.sign(sig_hash), script_sig)
script_sig = Bitcoin::Script.add_sig_to_multisig_script_sig(alice_key.sign(sig_hash), script_sig)

bob_key.sign(sig_hash)の部分では、各ユーザの秘密鍵を使ってsig_hashの署名を生成している。

add_sig_to_multisig_script_sigの内容は、↑で作成したscript sigのOP_0の後にユーザの鍵秘密鍵でsig_hashに署名したデータを挿入している。

そのためアリスとボブの署名が終わるとscript sigは

OP_0 アリスの署名 ボブの署名  OP_2 アリスの公開鍵 ボブの公開鍵 OP_2 OP_CHECKMULTISIG

となっている。

※ ↓のようにto_p2sh_multisig_script_sigの引数に一度に署名を渡すことも可能。

script_sig = Bitcoin::Script.to_p2sh_multisig_script_sig(redeem_script, alice_key.sign(sig_hash), bob_key.sign(sig_hash))

最後にこのscript_sigをトランザクションの入力にセットすれば、署名は完了し、トランザクションがブロードキャストできるようになる。

tx.in[0].script_sig = script_sig

SIGHASH_ALLで署名しているので、署名後にトランザクションに(出力の宛先を変えるとか量を変えるとか)変更を加えると当然署名検証に失敗する。

ブロードキャストする前に署名が正しいかはBitcoin::Protocol::Tx#verify_input_signatureで検証できる↓

prev_tx = Bitcoin::Protocol::Tx.new('マルチシグでロックされたトランザクションのrawデータ'.htb)
tx.verify_input_signature(0, prev_tx)

の結果がtrueであれば、署名検証が成功したことになる。

署名する順序に注意

アリスとボブの鍵を使った署名の順番は、マルチシグ作成時に指定したスクリプト内に記載した公開鍵の順番と関係があり、順序が逆だと署名検証に失敗する。ただ順番通りにソートしてくれるメソッドもあるので、それを使うのが、コードを書く側にとっては順序の意識をしなくていいので便利。↓がソートしてくれるメソッド

# Sort signatures in the given +script_sig+ according to the order of pubkeys in
# the redeem script. Also needs the +sig_hash+ to match signatures to pubkeys.
def self.sort_p2sh_multisig_signatures script_sig, sig_hash
  script = new(script_sig)
  redeem_script = new(script.chunks[-1])
  pubkeys = redeem_script.get_multisig_pubkeys

  # find the pubkey for each signature by trying to verify it
  sigs = Hash[script.chunks[1...-1].map.with_index do |sig, idx|
    pubkey = pubkeys.map {|key|
      Bitcoin::Key.new(nil, key.hth).verify(sig_hash, sig) ? key : nil }.compact.first
    raise "Key for signature ##{idx} not found in redeem script!"  unless pubkey
    [pubkey, sig]
  end]

  [OP_0].pack("C*") + pubkeys.map {|k| sigs[k] ? pack_pushdata(sigs[k]) : nil }.join + pack_pushdata(redeem_script.raw)
end

*1:SIGHASHについては以前の記事参照