テストネットでのtransaction作成

まずアドレスの作成

from ecc import PrivateKey
from helper import hash256, little_endian_to_int
secret = little_endian_to_int(hash256(b'hpscript secret'))
private_key = PrivateKey(secret)
print(private_key.point.address(testnet=True))

mtWT4Tga6ru2HXokJJYSp3wJKGXukTRaFk

「testnet bitcoin faucet」で検索して出てきた以下のサイトからテストネットのコインを受け取ります。
https://coinfaucet.eu/en/btc-testnet/

We sent 0.01947729 bitcoins to address
mtWT4Tga6ru2HXokJJYSp3wJKGXukTRaFk

tx: 7c671e8faaab0569acee26d76ae158adcd8f4806ec08c480d596e2aa78095026

FEES 0.00017 BTC

—-

1つのUTXOの60%を指定したアドレス「mwJn1YPMq7y5F8J3LkC5Hxg9PHyZ5K4cFv」に送付する
残額から手数料を引いた額をお釣り用のアドレスに送る必要があるため、もう一つ、お釣り用の bitcoin testnetのアドレスを作ります。hpscript blog
mmEjxM1RvKquR6FvUNibQe8ah4FFkphrdZ

from ecc import PrivateKey
from helper import decode_base58, SIGHASH_ALL
from script import p2pkh_script, Script
from tx import TxIn, TxOut, Tx
prev_tx = bytes.fromhex('7c671e8faaab0569acee26d76ae158adcd8f4806ec08c480d596e2aa78095026')
prev_index = 1
target_address = 'mwJn1YPMq7y5F8J3LkC5Hxg9PHyZ5K4cFv'
target_amount = 0.01
change_address = 'mtWT4Tga6ru2HXokJJYSp3wJKGXukTRaFk'
change_amount = 0.009
secret = 21059908846034780412291472495725985072617958337763972172781872669894507067750
priv = PrivateKey(secret=secret)
tx_ins = []
tx_ins.append(TxIn(prev_tx, prev_index))
tx_outs = []
h160 = decode_base58(target_address)
script_pubkey = p2pkh_script(h160)
target_satoshis = int(target_amount*100000000)
tx_outs.append(TxOut(target_satoshis, script_pubkey))
h160 = decode_base58(change_address)
script_pubkey = p2pkh_script(h160)
change_satoshis = int(target_amount*100000000)
tx_outs.append(TxOut(change_satoshis, script_pubkey))
tx_obj = Tx(1, tx_ins, tx_outs, 0, testnet=True)
print(tx_obj.sign_input(0, priv))
print(tx_obj.serialize().hex())

TxOutは配列で持っておき、amountとscript_pubkeyのセットをoutsごとにappendしていく。
$ python3 main.py
True
010000000126500978aae296d580c408ec06488fcdad58e16ad726eeac6905abaa8f1e677c010000006a473044022010b3ba160737a3a84a06cb431acc7b72393947c686af9fab83e6e5169dc4b58d02200233d9a6b4b3c93ddfbfb730fdc1b15563f2822a09b335f976f0f0abf7cc4c1701210284d4b1384aa13140d99f36a4143adf74c8d487dd13acc6d17def7081c2034cd2ffffffff0240420f00000000001976a914ad346f8eb57dee9a37981716e498120ae80e44f788ac40420f00000000001976a9148e813b482fc151baaf0c6b254aa02d10ee49ad5888ac00000000

### 2つのinputs, 1つのoutputsの場合
同じアドレスでfaucetからbitcoinを取得しようとすると、7hour後となる。

from ecc import PrivateKey
from helper import decode_base58, SIGHASH_ALL
from script import p2pkh_script, Script
from tx import TxIn, TxOut, Tx
prev_tx_1 = bytes.fromhex('11d05ce707c1120248370d1cbf5561d22c4f83aeba04367\
92c82e0bd57fe2a2f')
prev_index_1 = 1
prev_tx_2 = bytes.fromhex('51f61f77bd061b9a0da60d4bedaaf1b1fad0c11e65fdc74\
4797ee22d20b03d15')
prev_index_2 = 1
target_address = 'mwJn1YPMq7y5F8J3LkC5Hxg9PHyZ5K4cFv'
target_amount = 0.0429
secret = 8675309
priv = PrivateKey(secret=secret)
tx_ins = []
tx_ins.append(TxIn(prev_tx_1, prev_index_1))
tx_ins.append(TxIn(prev_tx_2, prev_index_2))
tx_outs = []
h160 = decode_base58(target_address)
script_pubkey = p2pkh_script(h160)
target_satoshis = int(target_amount*100000000)
tx_outs.append(TxOut(target_satoshis, script_pubkey))
tx_obj = Tx(1, tx_ins, tx_outs, 0, testnet=True)
print(tx_obj.sign_input(0, priv))
print(tx_obj.serialize().hex())

$ python3 main.py
True
01000000022f2afe57bde0822c793604baae834f2cd26155bf1c0d37480212c107e75cd011010000006a47304402204cc5fe11b2b025f8fc9f6073b5e3942883bbba266b71751068badeb8f11f0364022070178363f5dea4149581a4b9b9dbad91ec1fd990e3fa14f9de3ccb421fa5b269012103935581e52c354cd2f484fe8ed83af7a3097005b2f9c60bff71d35bd795f54b67ffffffff153db0202de27e7944c7fd651ec1d0fab1f1aaed4b0da60d9a1b06bd771ff6510100000000ffffffff01d0754100000000001976a914ad346f8eb57dee9a37981716e498120ae80e44f788ac00000000

inputsが複数の場合は、tx_ins[]にappendしていく。

トランザクションへの署名

署名ハッシュzを取得する方法がわかっている
ScriptPubKey内の20バイトのハッシュ(hash160)の元となる公開鍵に対応する秘密鍵を知っている場合、zに署名してDER署名を生成できる

from ecc import PrivateKey
from helper import SIGHASH_ALL
z = tranzaction.sig_hash(0)
private_key = PrivateKey(secret=8675309)
der = private_key.sign(z).der()
sig = der + SIGHASH_ALL.to_bytes(1, 'big')
sec = private_key.point.sec()
script_sig = Script([sig, sec])
transaction.tx_ins[0].script_sig = script_sig
print(transaction.serialize().hex())

関数上では、input_indexは0固定ではなく、input_indexで指定する。
der署名のscript_sigはprivate_keyとzから作成する。

    def sign_input(self, input_index, private_key):
        z = self.sig_hash(input_index)
        der = private_key.sign(z).der()
        sig = der = SIGHASH_ALL.to_bytes(1, 'big')
        sec = private_key.point.sec()
        self.tx_ins[input_index].script_sig = Script([sig, sec])
        return self.verify_input(input_index)

Transactionの作成

インプットがp2pkhのScriptPubKeyによってロックされたトランザクションの作成

1. ビットコインをどこに移動させるか
2. どのようなUTXOを使うことができるか
3. このトランザクションをどれくらい速くブロックチェーンに入れたいか

ビットコインではアドレス(公開鍵)はプライバシーやセキュリティの問題から再利用するべきではない。

### 手数料の算定
手数料はバイトサイズに基づき算定
300byteより600byteの方が高くなる
ブロックに空きがあるときは1bytesあたり1satoshi

### トランザクションの作成
アドレスを取得し、そこから20バイトのハッシュを取得する
数値を得たら、ビッグエンディアンバイトに変換
最初のバイトはネットワークプレフィックス、最後の4バイトはチェックサム、中間の20バイトが20バイトハッシュ(hash160)

def decode_base58(s):
    num = 0
    for c in s:
        num *= 58
        num += BASE58_ALPHABET.index(c)
    combined = num.to_bytes(25, byteorder='big')
    checksum = combined[-4:]
    if hash256(combined[:-4])[:4] != checksum:
        raise ValueError('bad address: {}{}'.format(checksum, hash256(combined[:-4])[:4]))
    return combined[1:-4]

この20バイトのハッシュをScriptPubKeyに変換する
script.py

def p2pkh_script(h160):
    return Script([0x76, 0xa9, h160, 0x88, 0xac])

0x76: OP_DUP
0xa9: OP_HASH160
h160 は20バイトのエレメント(引数)
0x88: OP_EQUALVERIFY
0xac: OP_CHECKSIG

### トランザクションの作成

from helper import decode_base58, SIGHASH_ALL
from script import p2pkh_script, Script
from tx import TxIn, TxOut, Tx
prev_tx = bytes.fromhex('0d6fe5213c0b3291f208cba8bfb59b7476dffacc4e5cb66f6\
eb20a080843a299')
prev_index = 13
tx_in = TxIn(prev_tx, prev_index)
tx_outs = []
change_amount = int(0.33*100000000)
change_h160 = decode_base58('mzx5YhAH9kNHtcN481u6WkjeHjYtVeKVh2')
change_script = p2pkh_script(change_h160)
change_output = TxOut(amount=change_amount, script_pubkey=change_script)
target_amount = int(0.1*100000000)
target_h160 = decode_base58('mnrVtF8DWjMu839VW3rBfgYaAfKk8983Xf')
target_script = p2pkh_script(target_h160)
target_output = TxOut(amount=target_amount, script_pubkey=target_script)
tx_obj = Tx(1, [tx_in], [change_output, target_output], 0, True)
print(tx_obj)

$ python3 test.py
tx: cd30a8da777d28ef0e61efe68a9f7c559c1d3e5bcd7b265c850ccb4068598d11
version: 1
tx_ins:
0d6fe5213c0b3291f208cba8bfb59b7476dffacc4e5cb66f6eb20a080843a299:13
tx_outs:
locktime: 33000000:OP_DUP OP_HASH160 d52ad7ca9b3d096a38e752c2018e6fbc40cdf26f OP_EQUALVERIFY OP_CHECKSIG
10000000:OP_DUP OP_HASH160 507b27411ccf7f16f10297de6cef3f291623eddf OP_EQUALVERIFY OP_CHECKSIG

Pythonの[-4:]と[:-4]

BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
print(BASE58_ALPHABET[-4:])
print(BASE58_ALPHABET[:-4])

$ python3 test.py
wxyz
123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuv

テストコードを実行してみれば、そのまんまだけど、一見[-4:]と[:-4]がプログラム上で並ぶと困惑しますね。

transaction全体の検証

verify_inputのinput_indexとは、tx_inの何番目かを指している。iはlen(self.tx_ins)で求める。

class Tx:

    // 

    def verify_input(self, input_index):
        tx_in = self.tx_ins[input_index]
        script_pubkey = tx_in.script_pubkey(testnet=self.testnet)
        z = self.sig_hash(input_index)
        combined = tx_in.script_sig + script_pubkey
        return combined.evaluate(z)

    def verify(self):
        if self.fee() < 0:
            return False
        for i in range(len(self.tx_ins)):
            if not self.verify_input(i):
                return False
        return True
[//code]

lenは配列の場合は、配列の数を表示

ary = ['ドル', 'ユーロ', 'ポンド', 'リラ']
print(len(ary))

$ python3 test.py
4

フルノードでは最大sigops、ScriptSigサイズ等、より多くの項目を検証している

Python enumerate()の使い方

enumerate()関数を利用することで、for文内のループ処理にインデックス番号を付与できる

ary = ['ドル', 'ユーロ', 'ポンド', 'リラ']
for i, d in enumerate(ary):
    print('index:' + str(i) + ' 通貨:' + d)

$ python3 test.py
index:0 通貨:ドル
index:1 通貨:ユーロ
index:2 通貨:ポンド
index:3 通貨:リラ

署名の確認 SIGHASH_ALLによる署名zの算出

トランザクションは1インプットにつき1つの署名を持つ。マルチシグの場合は、署名が複数
ECDSAアルゴリズには、公開鍵P、署名ハッシュz、署名r, sが必要

from ecc import S256Point, Signature
sec = bytes.fromhex('0349fc4e631e3624a545de3f89f5d8684c7b8138bd94bdd531d2e\
213bf016b278a')
der = bytes.fromhex('3045022100ed81ff192e75a3fd2304004dcadb746fa5e24c5031c\
cfcf21320b0277457c98f02207a986d955c6e0cb35d446a89d3f56100f4d7f67801c31967743a9\
c8e10615bed')
z = 0x27e0c5994dec7824e56dec6b2fcb342eb7cdb0d0957c2fce9882f715e85d81a6
point = S256Point.parse(sec)
signature = Signature.parse(der)
print(point.verify(z, signature))

OP_CHECKSIGが実行された場合、SEC公開鍵とDER署名がスタックにあるので簡単に取得できる。
難しいのは署名ハッシュを取得する部分
=> 署名前にトランザクションを変更し、インプットごとに異なる署名ハッシュを計算する

### ScriptSig(inputs)を空にする
インプットが1つの場合や複数の場合でも署名チェック時にScriptSigを空にする => 00
インプットが指しているScriptPubKeyを取り出して、空になっているScriptSigを入れる
=> インプットのScriptSigを前のScriptPubKeyに置き換える
4バイトのハッシュタイプを末尾に追加
署名の範囲は、他の全てのインプット、アウトプットを含めるSIGHASH_ALL、特定の1つのアウトプットを含めるSIGHASH_SINGLE、アウトプットは範囲外とするSIGHASH_NONE
=> 殆どのトランザクションはSIGHASH_ALLで署名されている。滅多に使われないSIGHASH_ANYONECANPAYというハッシュタイプもある
SIGHASH_ALLに対応する整数は1であり、リトルエンディアンで4バイトでエンコードする必要がある
ハッシュタイプ(SIGHASH_ALL)を末尾01000000付加

0100000001813f79011acb80925dfe69b3def355fe914\
bd1d96a3f5f71bf8303c6a989c7d1000000001976a914a802fc56c704ce87c42d7c92eb75e7896\
bdc41ae88acfeffffff02a135ef01000000001976a914bc3b654dca7e56b04dca18f2566cdaf02\
e8d9ada88ac99c39800000000001976a9141c4bc762dd5423e332166702cb75f40df79fea1288a\
c1943060001000000

zを算出する

from helper import hash256
modified_tx = bytes.fromhex('0100000001813f79011acb80925dfe69b3def355fe914\
bd1d96a3f5f71bf8303c6a989c7d1000000001976a914a802fc56c704ce87c42d7c92eb75e7896\
bdc41ae88acfeffffff02a135ef01000000001976a914bc3b654dca7e56b04dca18f2566cdaf02\
e8d9ada88ac99c39800000000001976a9141c4bc762dd5423e332166702cb75f40df79fea1288a\
c1943060001000000')
h256 = hash256(modified_tx)
z = int.from_bytes(h256, 'big')
print(hex(z))

$ python3 test.py
0x27e0c5994dec7824e56dec6b2fcb342eb7cdb0d0957c2fce9882f715e85d81a6

Txクラスの sig_hashメソッド

    def sig_hash(self, input_index):
        s = int_to_little_endian(self.version, 4)
        s += encode_varint(len(self.tx_ins))
        for i, tx_in in enumerate(self.tx_ins):
            if i == input_index:
                s += TxIn(
                    prev_tx = tx_in.prev_tx,
                    prev_index = tx_in.prev_index,
                    script_sig = tx_in.script_pubkey(self.testnet),
                    sequence = tx_in.sequence,
                ).serialize()
            else:
                s += TxIn(
                    prev_tx = tx_in.prev_tx,
                    prev_index = tx_in.prev_index,
                    sequence = tx_in.sequence,
                ).serialize()
        s += encode_varint(len(self.tx_outs))
        for tx_out in self.tx_outs:
            s += tx_out.serialize()
        s += int_to_little_endian(self.locktime, 4)
        s += int_to_little_endian(SIGHASH_ALL, 4)
        h256 = hash256(s)
        return int.from_bytes(h256, 'big')
    def verify_input(self, input_index):
        tx_in = self.tx_ins[input_index]
        script_pubkey = tx_in.script_pubkey(testnet=self.testnet)
        z = self.sig_hash(input_index)
        combined = tx_in.script_sig + script_pubkey
        return combined.evaluate(z)

トランザクションの作成と検証

### トランザクションの検証
ノードがトランザクションを受信すると、各トランザクションがネットワークのルールにしたがっている事を確認

確認事項
– トランザクションのインプットを過去に支払っていないこと
– インプットの合計額がアウトプットの合計額以上になっていること
– ScriptSigがScriptPubKeyのアンロックに成功していること

### inputの支払い状況の確認
インプットが過去に使われていないかを確認するにはUTXOセットを検索する
=> そのトランザクションが検証テストに合格すれば、UTXOセットからトランザクションのインプットを削除する
=> 軽量クライアントはフルノードの情報を信用する

### inputとoutputの合計額の確認
inputには額を表すフィールドがないため、ブロックチェーンを検索する必要がある

    def fee(self, testnet=False):
        input_sum, output_sum = 0, 0
        for tx_in in self.tx_ins:
            input_sum += tx_in.value(testnet=testnet)
        for tx_out in self.tx_outs:
            output_sum += tx_out.amount
        return input_sum - output_sum

このメソッドを用いてビットコインを作り出そうとしているかの確認ができる

from tx import Tx
from io import BytesIO

raw_tx = ('0100000001813f79011acb80925dfe69b3def355fe914bd1d96a3f5f71bf830\
3c6a989c7d1000000006b483045022100ed81ff192e75a3fd2304004dcadb746fa5e24c5031ccf\
cf21320b0277457c98f02207a986d955c6e0cb35d446a89d3f56100f4d7f67801c31967743a9c8\
e10615bed01210349fc4e631e3624a545de3f89f5d8684c7b8138bd94bdd531d2e213bf016b278\
afeffffff02a135ef01000000001976a914bc3b654dca7e56b04dca18f2566cdaf02e8d9ada88a\
c99c39800000000001976a9141c4bc762dd5423e332166702cb75f40df79fea1288ac19430600')
stream = BytesIO(bytes.fromhex(raw_tx))
transaction = Tx.parse(stream)
print(transaction.fee() >= 0) 

ScriptPubKeyのOP_CODE

ScriptPubKey
55 OP_5
93 OP_ADD
59 OP_9
87 OP_EQUAL

ScriptSig
54 OP_4

76 OP_DUP
76 OP_DUP
95 OP_MUL
93 OP_ADD
56 OP_6
87 OP_EQUAL

52

from script import Script
script_pubkey = Script([0x76, 0x76, 0x95, 0x93, 0x56, 0x87])
script_sig = Script([0x52])
combined_script = script_sig + script_pubkey
print(combined_script.evaluate(0))

OP_CODEが異なるような気が…
OP_MULはビットコインネットワークで無効化されている

6e OP_2DUP
87 OP_EQUAL
91 OP_NOT
69 OP_VERIFY
a7 OP_SHA1
7c OP_SWAP
a7 OP_SHA1
87 OP_EQUAL

from script import Script
script_pubkey = Script([0x6e, 0x87, 0x91, 0x69, 0xa7, 0x7c, 0xa7, 0x87])
c1 = '255044462d312e330a25e2e3cfd30a0a0a312030206f626a0a3c3c2f576964746820\
32203020522f4865696768742033203020522f547970652034203020522f537562747970652035\
203020522f46696c7465722036203020522f436f6c6f7253706163652037203020522f4c656e67\
74682038203020522f42697473506572436f6d706f6e656e7420383e3e0a73747265616d0affd8\
fffe00245348412d3120697320646561642121212121852fec092339759c39b1a1c63c4c97e1ff\
fe017f46dc93a6b67e013b029aaa1db2560b45ca67d688c7f84b8c4c791fe02b3df614f86db169\
0901c56b45c1530afedfb76038e972722fe7ad728f0e4904e046c230570fe9d41398abe12ef5bc\
942be33542a4802d98b5d70f2a332ec37fac3514e74ddc0f2cc1a874cd0c78305a215664613097\
89606bd0bf3f98cda8044629a1'
c2 = '255044462d312e330a25e2e3cfd30a0a0a312030206f626a0a3c3c2f576964746820\
32203020522f4865696768742033203020522f547970652034203020522f537562747970652035\
203020522f46696c7465722036203020522f436f6c6f7253706163652037203020522f4c656e67\
74682038203020522f42697473506572436f6d706f6e656e7420383e3e0a73747265616d0affd8\
fffe00245348412d3120697320646561642121212121852fec092339759c39b1a1c63c4c97e1ff\
fe017346dc9166b67e118f029ab621b2560ff9ca67cca8c7f85ba84c79030c2b3de218f86db3a9\
0901d5df45c14f26fedfb3dc38e96ac22fe7bd728f0e45bce046d23c570feb141398bb552ef5a0\
a82be331fea48037b8b5d71f0e332edf93ac3500eb4ddc0decc1a864790c782c76215660dd3097\
91d06bd0af3f98cda4bc4629b1'
collision1 = bytes.fromhex(c1)
collision2 = bytes.fromhex(c2)
script_sig = Script([collision1, collision2])
combined_script = script_sig + script_pubkey
print(combined_script.evaluate(0))

p2pkの問題点とp2pkh

secp256k1の公開鍵は圧縮で33バイト、非圧縮で65バイト(130文字)で長い
UTXOセットが大きくなる
ScriptPubKeyフィールドに公開鍵が存在する

### p2pkhで解決
– アドレスが短くなる
– sha256とripemd160
=> hash160と呼ぶ 160ビット
p2pkhは短くて安全性が高い

### ScriptPubKey(output)
76 OP_DUB
a9 OP_HASH160
14 Length of
bc3b…da
88 OP_EQUALVERIFY
ac OC_CHECKSIG

### ScriptSig(input)
48 Length of signature
30..01 signature
21 Length of pubkey
0349…8a pubkey

### 構成
– ScriptPubKey(output)
OP_DUB, OP_HASH160, HASH, OP_EQUALVERIFY, OP_CHECKSIG
– ScriptSig(input)
,

OP_DUPはエレメントの先頭を複製するため、pubkeyが複製される

def op_dup(stack):
    if len(stack) < 1:
        return False
    stack.append(stack[-1])
    return True

OP_HASH160でpubkeyをhash160(sha256, ripemd160)実行、20バイトのハッシュを生成する

def op_hash160(stack):
    if len(stack) < 1:
        return False
    element = stack.pop()
    stack.append(hash160(element))
    return True

OP_EQUALVERIFYで、生成されたhashとhash値が正しいか確認

def op_equalverify(stack):
    return op_equal(stack) and op_verify(stack)

def op_equal(stack):
    if len(stack) < 2:
        return False
    element1 = stack.pop()
    element2 = stack.pop()
    if element1 == element2:
        stack.append(encode_num(1))
    else:
        stack.append(encode_num(0))
    return True

def op_verify(stack):
    if len(stack) < 1:
        return False
    element = stack.pop()
    if decode_num(element) == 0:
        return False
    return True

OP_CHECKSIGはP2PKと同じ

def op_checksig(stack, z):
    if len(stack) < 1:
        return False
    sec_pubkey = stack.pop()
    der_signature = stack.pop()[:-1]
    try:
        point = S256Point.parse(sec_pubkey)
        sig = Signature.parse(der_signature)
    except (ValueError, SyntaxError) as e:
        return False
    if point.verify(z, sig):
        stack.append(encode_num(1))
    else:
        stack.append(encode_num(0))
    return True

### P2PKH
– ScriptPubKeyが短くなる
– ripemd160,sha256も計算する