Develop with pleasure!

福岡でCloudとかBlockchainとか。

P2WSHを使うトランザクションの作成と署名

P2WPKHの署名方法について書いた↓ので

techmedia-think.hatenablog.com

今回はP2WSHの署名について。

単純なマルチシグでもつまならいのでOP_CSVのBIPに出てくるLNのトランザクションサンプルのP2WSHにしてみる↓

https://github.com/bitcoin/bips/blob/master/bip-0112.mediawiki#Lightning_Network

※ LNのホワイトペーパーでは古いコミットメントトランザクションの取消に↑のようなシークレットのプリイメージを使う方法が書かれてるけど、現在の実装ではプリイメージのハッシュにロックする方法は使われておらず、チャネル設立時のベースポイントに対してシークレットを加算して生成した鍵でロックする形に変わっている。

P2WSHにBitcoinを送付する

今回は↑の例に出てくる以下のスクリプトBitcoinを送る。

HASH160 <revokehash> EQUAL
IF
    <Bob's pubkey>
ELSE
    "24h" CHECKSEQUENCEVERIFY DROP
    <Alice's pubkey>
ENDIF
CHECKSIG

このスクリプトは、revokehashの元のプリイメージを知っていればボブの秘密鍵による署名でコインが入手でき、それ以外のケースではこのUTXOがブロックに入れられて24時間経過したらアリスの秘密鍵による署名でコインが入手できるというものになる。

このスクリプトをベースにしたP2WSHのscriptPubkeyを作る。P2WSHのwitness programは以下の形式になる。

0 <↑のスクリプトの32バイトハッシュ>

実際にbitcoin-rubyで↑のP2WSHに支払う処理は以下のように書ける。

require 'bitcoin'

Bitcoin.network = :testnet3

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

# シークレットの値
preimage = "apple"

# シークレットのハッシュ値
hash = Bitcoin.hash160(preimage)

# 24時間 = 10分ブロック計算で 24 * 6 ブロック
block_count = 24 * 6

# ロックスクリプト
redeem_script = Bitcoin::Script.from_string("OP_HASH160 #{hash} OP_EQUAL OP_IF #{bob_key.pub} OP_ELSE #{block_count} OP_NOP3 OP_DROP #{alice_key.pub} OP_ENDIF OP_CHECKSIG")

# P2WSHのscriptPubkey
script_pubkey = Bitcoin::Script.from_string("0 #{Bitcoin.sha256(redeem_script.to_payload.bth)}")

# 適当なUTXOを入力にしてP2WSH宛に送る。
tx = Bitcoin::Protocol::Tx.new
tx.add_in(Bitcoin::Protocol::TxIn.from_hex_hash('07b09f22abd2d78c41b2984f05d3dd83a3c5905771f0ee290b04f933e0c13d59', 0))
# P2WSHの出力
tx.add_out(Bitcoin::Protocol::TxOut.new(100000, script_pubkey.to_payload))
# おつり
tx.add_out(Bitcoin::Protocol::TxOut.value_to_address(760000, 'mt1hZLajqyc63NkWy7qvgiuum5nuTBdVZ6'))

# (これは非segwitなTxなので)to_payloadしたデータをsignrawtransactionなどで署名してブロードキャストする
tx.to_payload.bth

実際にtestnetにブロードキャストしたデータ
(txid = 4e70cfb5ca582aeb88924fe74dbcb49503699f0f09adb2493c7439d051668af7)↓

{
  "hex": "0100000001593dc1e033f9040b29eef0715790c5a383ddd3054f98b2418cd7d2ab229fb007000000006a47304402202ecef6c319d65d408d953d9f4dc6c8882e638d6493751c6728448fc14c8d98920220684a2c222c7b5529e6f290ae08ebe07b893de69c3f92b68fc9a9b7f804e027c9012102effb2edfcf826d43027feae226143bdac058ad2e87b7cec26f97af2d357ddefaffffffff02a086010000000000220020ee6e25ff1fcf33396396cc82bb3702533fb1ad5289711262ed75e0dea34d84e4c0980b00000000001976a9148911455a265235b2d356a1324af000d4dae0326288ac00000000",
  "txid": "4e70cfb5ca582aeb88924fe74dbcb49503699f0f09adb2493c7439d051668af7",
  "hash": "4e70cfb5ca582aeb88924fe74dbcb49503699f0f09adb2493c7439d051668af7",
  "size": 234,
  "vsize": 234,
  "version": 1,
  "locktime": 0,
  "vin": [
    {
      "txid": "07b09f22abd2d78c41b2984f05d3dd83a3c5905771f0ee290b04f933e0c13d59",
      "vout": 0,
      "scriptSig": {
        "asm": "304402202ecef6c319d65d408d953d9f4dc6c8882e638d6493751c6728448fc14c8d98920220684a2c222c7b5529e6f290ae08ebe07b893de69c3f92b68fc9a9b7f804e027c9[ALL] 02effb2edfcf826d43027feae226143bdac058ad2e87b7cec26f97af2d357ddefa",
        "hex": "47304402202ecef6c319d65d408d953d9f4dc6c8882e638d6493751c6728448fc14c8d98920220684a2c222c7b5529e6f290ae08ebe07b893de69c3f92b68fc9a9b7f804e027c9012102effb2edfcf826d43027feae226143bdac058ad2e87b7cec26f97af2d357ddefa"
      },
      "sequence": 4294967295
    }
  ],
  "vout": [
    {
      "value": 0.00100000,
      "n": 0,
      "scriptPubKey": {
        "asm": "0 ee6e25ff1fcf33396396cc82bb3702533fb1ad5289711262ed75e0dea34d84e4",
        "hex": "0020ee6e25ff1fcf33396396cc82bb3702533fb1ad5289711262ed75e0dea34d84e4",
        "type": "witness_v0_scripthash"
      }
    }, 
    {
      "value": 0.00760000,
      "n": 1,
      "scriptPubKey": {
        "asm": "OP_DUP OP_HASH160 8911455a265235b2d356a1324af000d4dae03262 OP_EQUALVERIFY OP_CHECKSIG",
        "hex": "76a9148911455a265235b2d356a1324af000d4dae0326288ac",
        "reqSigs": 1,
        "type": "pubkeyhash",
        "addresses": [
          "mt1hZLajqyc63NkWy7qvgiuum5nuTBdVZ6"
        ]
      }
    }
  ]
}

0番めの出力がP2WSHの出力で、typewitness_v0_scripthashになっており、アドレスも表示されていないことが分かる。

witnessなトランザクションの作成と署名

↑でP2WSH宛に送ったBitcoinを入力にしたトランザクションの作成と署名を行う。

↑のスクリプトはアンロック条件が2つあるが、今回はシークレットを知ったボブがボブの秘密鍵を使った署名でコインをアンロックするパターンで実装する。 この条件でアンロックする際のwitnessのスタックは

<ボブの秘密鍵を使った署名> <revokehashのプリイメージ> <witness script>

になる。

version 0のwitness programで0の後に32バイトのデータがプッシュされているスクリプトをP2WSHのwitness programと認識し、以下のようなプロセスが実行される。

  1. witnessスタックの先頭から最後から1つ前までのスタックのアイテムをピックアップしてスタックを生成する。
  2. witnessスタックの最後のアイテムをredeem scriptとしてピックアップする。
  3. 1のスタックに対して2のredeem scriptを実行してスクリプトを評価する。

※ なおwitnessスタックにセットされたwitness scriptから計算したハッシュが元のscriptPubkeyの32バイトのハッシュ値と異なる場合はエラー。

この辺の処理は↓あたりで実装されている。

https://github.com/bitcoin/bitcoin/blob/v0.13.2/src/script/interpreter.cpp#L1356-L1407

実際にbitcoin-rubyで実装してみる。
実態は前回と同様、segwit対応を独自実装したものを利用する↓

github.com

P2WSHの場合も署名は、BIP-143↓で定義された新しいSIGHASHの生成方法に従う。
P2WPKHと異なるのは、scriptCodeの生成方法くらいかな。

techmedia-think.hatenablog.com

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

# シークレットの値
preimage = "apple"

# シークレットのハッシュ値
hash = Bitcoin.hash160(preimage)

# 24時間 = 10分ブロック計算で 24 * 6 ブロック
block_count = 24 * 6
# ロックスクリプト
redeem_script = Bitcoin::Script.from_string("OP_HASH160 #{hash} OP_EQUAL OP_IF #{bob_key.pub} OP_ELSE #{block_count} OP_NOP3 OP_DROP #{alice_key.pub} OP_ENDIF OP_CHECKSIG")

prev_tx = Bitcoin::Protocol::Tx.new('↑でブロードキャストしたトランザクションデータ'.htb)

tx = Bitcoin::Protocol::Tx.new

tx.add_in(Bitcoin::Protocol::TxIn.from_hex_hash('4e70cfb5ca582aeb88924fe74dbcb49503699f0f09adb2493c7439d051668af7', 0))
tx.add_out(Bitcoin::Protocol::TxOut.value_to_address(90000, bob_key.addr))

# BIP-143の仕様でSIGHASHを生成
sig_hash = tx.signature_hash_for_witness_input(0, prev_tx.out[0].script, 100000, redeem_script.to_payload)

# ボブの秘密鍵で署名
sig = bob_key.sign(sig_hash)+ [Bitcoin::Script::SIGHASH_TYPE[:all]].pack("C")

win = Bitcoin::Protocol::TxInWitness.new

win.add_stack(sig.bth) # ボブの署名
win.add_stack(preimage) # revokehashのプリイメージ
win.add_stack(redeem_script.to_payload.bth) # redeem scrpt(witness script)

tx.witness.add_witness(win)

# トランザクションのrawデータが出力されるので、これをsendrawtransactionする。
tx.to_witness_payload.bth

実際にtestnetにブロードキャストしたデータ
(txid = fc45cf999310987860cf7d287d1f0118322ac31212aef1b279c7a3f8b55c83e0)が↓

{
  "hex": "01000000000101f78a6651d039743c49b2ad090f9f690395b4bc4de74f9288eb2a58cab5cf704e0000000000ffffffff01905f0100000000001976a914f8ae1df3f73d7d2e197b467b509dfedbf1b4cdc288ac0347304402203fd393fb54584fbcd89e24aff756696edd8d1889307030a6e97c7fcc7dcb4cee02207e484fca226d4d752ee69c0a9e972f58ff0f7debd8c1209911403837847c5efb0103a995e064a9146a25f59f6c9ebe7415c640c915bd48c4b7138a62876321038c811a50fa3d7a68426fef93d92c6897d7f0a1c16c6fb17e9b183ef868d8d16f67021440b2752103f50e3fdd76870c21581af66af261ffed186b780c024f8f2fc9daeac58bf8356368ac00000000",
  "txid": "fc45cf999310987860cf7d287d1f0118322ac31212aef1b279c7a3f8b55c83e0",
  "hash": "6dcce7541bf1e89698d7f616e09b5510fe75464947cab5fb3388cf0be96aaf76",
  "size": 265,
  "vsize": 130,
  "version": 1,
  "locktime": 0,
  "vin": [
    {
      "txid": "4e70cfb5ca582aeb88924fe74dbcb49503699f0f09adb2493c7439d051668af7",
      "vout": 0,
      "scriptSig": {
        "asm": "",
        "hex": ""
      },
      "txinwitness": [
        "304402203fd393fb54584fbcd89e24aff756696edd8d1889307030a6e97c7fcc7dcb4cee02207e484fca226d4d752ee69c0a9e972f58ff0f7debd8c1209911403837847c5efb01", 
        "a995e0", 
        "a9146a25f59f6c9ebe7415c640c915bd48c4b7138a62876321038c811a50fa3d7a68426fef93d92c6897d7f0a1c16c6fb17e9b183ef868d8d16f67021440b2752103f50e3fdd76870c21581af66af261ffed186b780c024f8f2fc9daeac58bf8356368ac"
      ],
      "sequence": 4294967295
    }
  ],
  "vout": [
    {
      "value": 0.00090000,
      "n": 0,
      "scriptPubKey": {
        "asm": "OP_DUP OP_HASH160 f8ae1df3f73d7d2e197b467b509dfedbf1b4cdc2 OP_EQUALVERIFY OP_CHECKSIG",
        "hex": "76a914f8ae1df3f73d7d2e197b467b509dfedbf1b4cdc288ac",
        "reqSigs": 1,
        "type": "pubkeyhash",
        "addresses": [
          "n4BrTVLsr1PUfqAneRpVqZt3kAYJRQY6Zf"
        ]
      }
    }
  ]
}

と正常にブロードキャストできた。

前回のP2WPKHとP2WSHのトランザクションが作れればLNのペイメントチャネルを実装をする上で必要なものは揃う。

P2SHでネストしたP2WPKHや、P2SHでネストしたP2WSHなんかもあるけど、基本的にSegwit非対応なウォレットとの互換のためだけの仕様なので、普通にペイメントチャネル実装する際に使うことはないだろう。