[bitcoin] p2pkhトランザクションを自分で組み立てる


環境

bitcoind v0.16
ruby v2.5
bitcoin-ruby v0.0.18

source code

これやる

ライブラリに頼らずに、bitcoin transactionをつくる方法。
p2pkhのoutputを消費します。
鍵と署名に関してはライブラリを使います。
トランザクションの構造と署名のつくりかたを主題とするので、scriptのしくみなどは省きます。
inputは1つだけのシンプルなものです。
基本的なことはやはりmastering bitcoin/ transactionがわかりやすいです。ご参考ください。
opコードのパースにはbtcdebを使わせていただきます。

注意点

数値はlittle endianで表記。

トランザクションの構造

トランザクションの構造に関しては、こちらの記事がとても参考になります。
BitcoinのTransaction
Bitcoin Standard Transactions(送信)
- その他参考
Transaction/Bitcoin Wiki

トランザクションの例

0100000001dbfb811cacf55c78a49e454b017cfc10f34382c51ed98859c119aa261ba3f661000000006a47304402201ff7410c71653ab547a7ace6667f8262702d392b496c7385619f4655e5d2180d02202597db2cdd1243602f7537285c8eda21952dd2afd4c6c941f566bddc7b75d6bc01210390fc7c6a9747091ac4c94721887c7d8ccecd9ddc02aaae005138689d5c93a3affffffff0200301a1e010000001976a9144ae221ba1282464e44cd8217835fb44e92030ab288ac605af405000000001976a914d3da933dc800e1d0b2645bd3fa2616af758f571188ac00000000

トランザクション(tx)を組み立てる

p2pkhのoutputを消費するtxを作ります。
msvkJkuUpcH51VodGDXtv48z58Y1udgUKbに20BTC送ります。
tx_inにはsignature script部分があります。
これは署名を含みます。
なにに対して署名をするかは、署名するユーザが選択することができますが、今回はスタンダードなtx全体に署名する方式(sighash all)を使います。

input(tx_in)の作成

ここではinputのもととなるトランザクションをbitcoindでつくります。
生成されるアドレスやトランザクションはここでの例とまったく同じにはなりません。

tx_in作成の準備

通常のアドレスを取得する

$ bitcoin-cli getnewaddress '' legacy
mkNBR5PyMTdhWtKopfSeXigS1p7QAD8f4h

作ったアドレスに30BTC送る。戻り値はトランザクションid

$ bitcoin-cli sendtoaddress mkNBR5PyMTdhWtKopfSeXigS1p7QAD8f4h 30
d651820a8718f83ae97fe5e6554798c069868a172477909a92460ca27fd7aa2f

トランザクションの内容を詳しく見る(最後に1を付け加えるのを忘れずに 1)

$ bitcoin-cli getrawtransaction d651820a8718f83ae97fe5e6554798c069868a172477909a92460ca27fd7aa2f 1
{
  "txid": "d651820a8718f83ae97fe5e6554798c069868a172477909a92460ca27fd7aa2f",
  "hash": "d651820a8718f83ae97fe5e6554798c069868a172477909a92460ca27fd7aa2f",
  "version": 2,
  "size": 190,
  "vsize": 190,
  "weight": 760,
  "locktime": 113,
  "vin": [
    {
      "txid": "56f8d8837aceb14533b42df6f76dd5eaabffdf355d8f7fb3747b7c7568b5d6b3",
      "vout": 0,
      "scriptSig": {
        "asm": "3045022100e4410a4662ff1649c26df4c8ce76e8d9e105c082ce682558d933c9f1c263823e02206467ddb539c3f498da324eec0856071356e12f04efeb76c467330aefcc66bec0[ALL]",
        "hex": "483045022100e4410a4662ff1649c26df4c8ce76e8d9e105c082ce682558d933c9f1c263823e02206467ddb539c3f498da324eec0856071356e12f04efeb76c467330aefcc66bec001"
      },
      "sequence": 4294967294
    }
  ],
  "vout": [
    {
      "value": 30.00000000,
      "n": 0,
      "scriptPubKey": {
        "asm": "OP_DUP OP_HASH160 352fd6937e24d9de927b857e5d8ea9e3a8fe8f91 OP_EQUALVERIFY OP_CHECKSIG",
        "hex": "76a914352fd6937e24d9de927b857e5d8ea9e3a8fe8f9188ac",
        "reqSigs": 1,
        "type": "pubkeyhash",
        "addresses": [
          "mkNBR5PyMTdhWtKopfSeXigS1p7QAD8f4h"
        ]
      }
    },
    {
      "value": 19.99996200,
      "n": 1,
      "scriptPubKey": {
        "asm": "OP_HASH160 29ab892dc58556e7e0e8bd213e13e5b637d29bc3 OP_EQUAL",
        "hex": "a91429ab892dc58556e7e0e8bd213e13e5b637d29bc387",
        "reqSigs": 1,
        "type": "scripthash",
        "addresses": [
          "2Mw3ZBfmCUrBzBdoKtaBp8rzYr7Qtgy3TmS"
        ]
      }
    }
  ],
  "hex": "0200000001b3d6b568757c7b74b37f8f5d35dfffabead56df7f62db43345b1ce7a83d8f8560000000049483045022100e4410a4662ff1649c26df4c8ce76e8d9e105c082ce682558d933c9f1c263823e02206467ddb539c3f498da324eec0856071356e12f04efeb76c467330aefcc66bec001feffffff02005ed0b2000000001976a914352fd6937e24d9de927b857e5d8ea9e3a8fe8f9188ac288535770000000017a91429ab892dc58556e7e0e8bd213e13e5b637d29bc38771000000"
}

tx_inを組み立てる

voutの1つ目をinputとして消費します。

previous_output

これはoutpointという型で、32 Byteのprevious transaction id(hash) + 4 Byteのoutput indexです2
previous transaction idはもととなるoutputが含まれるtransactionのidですので
d651820a8718f83ae97fe5e6554798c069868a172477909a92460ca27fd7aa2f
ただしlittle endianにするのでトランザクション内では次のように表記されます。
2faad77fa20c46929a907724178a8669c0984755e6e57fe93af818870a8251d6
output indexはoutputのうちなかで何番目かということです。ここでは、n:0の30BTC分のoutputを利用するので
0x00000000
となります。
もし1の場合はlittle endienに注意して
0x01000000
となります。

script size

まだスクリプトのサイズはわからないので保留です。

signature script(unlocking script)

署名がここに含まれるのですが、tx全体に対して署名をしたいです(sighash all)。
署名するときにはまだ存在しない署名欄(まさにこの部分=script signature)はどうすればよいのでしょうか3
p2pkhの場合、前トランザクションのscript pubkeyを一時的に当てはめておきます。
署名をするには、txの他の部分(tx_outなど)を組み立てる必要がありますので、tx_outも組み立ててから署名をしていきます。

sequence

通常ffffffffの4Byteです。
いまのところffffffff以外について私はわかりません。

outputの作成

value

送金量です。satoshiで表現し8Byteの16進数のlittle endienです。
20BTC送るので
0094357700000000

pubkey script size

pubkey script(次項目)のバイト長は25 Byteですので、16進数にして
19

pubkey script

今回はp2pkh宛で、以下のようにします。
76a91488217c4b26f9221e162fd43a919759e30069db3788ac
opコードにすると(by btcdeb)

script                                    
-----------------------------------------
OP_DUP                                    
OP_HASH160                                
88217c4b26f9221e162fd43a919759e30069db37 
OP_EQUALVERIFY                            
OP_CHECKSIG                              

tx全体

これまでつくったtx_inとtx_outをつかって完全な形式に整えます。

version: 01000000
tx_in_count: 01
(tx_in)
(outpoint)
prev_txid: 2faad77fa20c46929a907724178a8669c0984755e6e57fe93af818870a8251d6
prev_output_index: 00000000
script_size: 19
script_sig: 76a914352fd6937e24d9de927b857e5d8ea9e3a8fe8f9188ac
sequence: ffffffff
tx_out_count: 01
(tx_out)
value: 0094357700000000
script_size: 19
script_pubkye: 76a91488217c4b26f9221e162fd43a919759e30069db3788ac
01000000012faad77fa20c46929a907724178a8669c0984755e6e57fe93af818870a8251d6000000001976a914352fd6937e24d9de927b857e5d8ea9e3a8fe8f9188acffffffff0100943577000000001976a91488217c4b26f9221e162fd43a919759e30069db3788ac

署名

以上のデータに4 Byteのsighash type(little endien)を付け加えて、sha256を二回適用したものに署名します。
今回はsighash allなので0x01000000
したがって署名の対称のもととなるデータは以下です。

01000000012faad77fa20c46929a907724178a8669c0984755e6e57fe93af818870a8251d6000000001976a914352fd6937e24d9de927b857e5d8ea9e3a8fe8f9188acffffffff0100943577000000001976a91488217c4b26f9221e162fd43a919759e30069db3788ac01000000

以上をpayloadとしてsha256を二回。

double_sha256
# double hash payload
# @param payload [string] hex string
# @return [string] hex string
def double_sha256 payload
  OpenSSL::Digest::SHA256.hexdigest([OpenSSL::Digest::SHA256.hexdigest([payload].pack("H*"))].pack("H*"))
end

この結果は、同じpayloadに対しては常に同じ値となり、バイト長は32 Byteです。
3dc985affd7075acd5655de9170d51076e7fb4f80535b059a5b0c5b4e19e0775
このdouble_sha256の戻り値(target)に対して対応する秘密鍵でECDSA署名を行います。
bitcoinにおける"署名"は、ECDSA署名にsighash typeの1Byte分を付け加えたものになります。
sighash_allであれば、0x01を付加します。

sign_sample
# make signature
# @param data [string] hex string
# @return [Signature]
def sign data
  sigobj = OpenSSL::PKey::EC.new('secp256k1')
  sigobj.private_key = OpenSSL::BN.new(@priv_key, 16)
  signature = sigobj.dsa_sign_asn1([data].pack("H*")).unpack("H*")
  @signature = signature[0] + @sighash
  self
end

署名に関してはいくつか注意点があります。
同じデータに対して署名をしても、署名値は毎回異なります。
ECDSA署名では署名時にランダムな値を利用しているためです4
署名はDER形式でなければなりません。
DER形式についてはBIP66で定義されています。

ECDSA署名のSのサイズに制限があります。
大きすぎるとエラーで教えてくれます。事前にチェックしておきましょう。
Low S valueについてはBIP62で定義されています。
また、Sのサイズを自分で変更した際には、DER形式でのSサイズおよび署名全体のサイズ部分も変えなければいけないので注意してください。

この署名をinputのsignature script部分に入れてトランザクションの完成です。

コード全体はgithubにあるのでよければ参考に(なるのか?)してください。

もっとくわしく!

mastering bitcoin
Transaction/Bitcoin Wiki
BIP62
BIP66