[bitcoin基礎技術] デジタル署名

<送信者>
– データをハッシュ関数でハッシュ化
– 出力されたハッシュ値を秘密鍵で暗号化し、署名を作成
– データと署名をセットにして受信者に送信

<受信者>
– 受け取ったデータを送信者と同じハッシュ関数でハッシュ化してハッシュ値Aを得る
– 受け取った署名を送信者の公開鍵で復号してハッシュ値Bを得る
– A B比較して一致していることを確認

### デジタル署名の流れ
1. 送信者のメッセージを作成
$ echo secret > message.txt; cat message.txt
secret

2. 秘密鍵でハッシュ値に署名し、message.sigに出力
$ openssl dgst -sha256 -sign secp256k1-private.pem message.txt > message.sig; ls message.sig
message.sig

3. 生成した公開鍵で署名を検証
$ openssl dgst -sha256 -verify secp256k1-public.pem -signature message.sig message.txt
Verified OK

### メッセージの改ざん検出
$ openssl ecparam -genkey -name secp256k1 -out secp256k1-private-evil.pem; ls secp256k1-private-evil.pem
secp256k1-private-evil.pem
$ echo “tampered secret” > tampered_message.txt ; cat tampered_message.txt
tampered secret
$ openssl dgst -sha256 -sign secp256k1-private-evil.pem tampered_message.txt > tampered_message.sig ; ls tampered_message.sig
tampered_message.sig
$ openssl dgst -sha256 -verify secp256k1-public.pem -signature tampered_message.sig tampered_message.txt
Verification failure

[bitcoin基礎技術] 公開鍵暗号

パラメータファイルの生成
$ openssl ecparam -name secp256k1 -out secp256k1.pem

secp256k1.pem

-----BEGIN EC PARAMETERS-----
BgUrgQQACg==
-----END EC PARAMETERS-----

$ openssl ecparam -in secp256k1.pem -text -noout
EC-Parameters: (256 bit)
ASN1 OID: secp256k1

パラメータの確認
$ openssl ecparam -in secp256k1.pem -text -param_enc explicit -noout
EC-Parameters: (256 bit)
Field Type: prime-field
Prime:
00:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:
ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:fe:ff:
ff:fc:2f
A: 0
B: 7 (0x7)
Generator (uncompressed):
04:79:be:66:7e:f9:dc:bb:ac:55:a0:62:95:ce:87:
0b:07:02:9b:fc:db:2d:ce:28:d9:59:f2:81:5b:16:
f8:17:98:48:3a:da:77:26:a3:c4:65:5d:a4:fb:fc:
0e:11:08:a8:fd:17:b4:48:a6:85:54:19:9c:47:d0:
8f:fb:10:d4:b8
Order:
00:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:
ff:fe:ba:ae:dc:e6:af:48:a0:3b:bf:d2:5e:8c:d0:
36:41:41
Cofactor: 1 (0x1)

秘密鍵を生成
$ openssl ecparam -in secp256k1.pem -genkey -noout -out secp256k1-private.pem
$ cat secp256k1-private.pem
—–BEGIN EC PRIVATE KEY—–
MHQCAQEEIAqV2IN8gUr9qXpsJyhpYA4KQfXioS/oiDMh1dgPcncAoAcGBSuBBAAK
oUQDQgAEeiuKeXTtb0t4F7DzVMbIHOrLIQ5T4JPq3NTU7OC7TWCghk6ALrWLgTGD
zUuuEIWI4GLsYea/829orVMO4Rz/FQ==
—–END EC PRIVATE KEY—–

秘密鍵を16進数表記で出力
$ openssl ec -in secp256k1-private.pem -outform DER | tail -c +8 | head -c 32 | xxd -p -c 32
read EC key
writing EC key
0a95d8837c814afda97a6c272869600e0a41f5e2a12fe8883321d5d80f727700

秘密鍵を変数に格納
$ privKey=0a95d8837c814afda97a6c272869600e0a41f5e2a12fe8883321d5d80f727700

公開鍵を生成
$ openssl ec -in secp256k1-private.pem -pubout -out secp256k1-public.pem; cat secp256k1-public.pem
read EC key
writing EC key
—–BEGIN PUBLIC KEY—–
MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEeiuKeXTtb0t4F7DzVMbIHOrLIQ5T4JPq
3NTU7OC7TWCghk6ALrWLgTGDzUuuEIWI4GLsYea/829orVMO4Rz/FQ==

公開鍵を16進数で出力
$ openssl ec -in secp256k1-private.pem -pubout -outform DER | tail -c 65 | xxd -p -c 65
read EC key
writing EC key
047a2b8a7974ed6f4b7817b0f354c6c81ceacb210e53e093eadcd4d4ece0bb4d60a0864e802eb58b813183cd4bae108588e062ec61e6bff36f68ad530ee11cff15

秘密鍵はスカラーですが、公開鍵は座標x, y
$ pubKey=047a2b8a7974ed6f4b7817b0f354c6c81ceacb210e53e093eadcd4d4ece0bb4d60a0864e802eb58b813183cd4bae108588e062ec61e6bff36f68ad530ee11cff15
$ prefix=`echo $pubKey | cut -c1-2`; echo “prefix= $prefix”
prefix= 04
$ x=`echo $pubKey | cut -c3-66` ; echo “x = $x”
x = 7a2b8a7974ed6f4b7817b0f354c6c81ceacb210e53e093eadcd4d4ece0bb4d60
$ y=`echo $pubKey | cut -c67-130`; echo “y= $y”
y= a0864e802eb58b813183cd4bae108588e062ec61e6bff36f68ad530ee11cff15

sig_hashとverify_input

txをシリアライズする際に、redeem_script=None としてp2pkh か p2shかを分ける

    def sig_hash(self, input_index, redeem_script=None):
        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:
                if redeem_script:
                    script_sig = redeem_script
                else:
                    script_sig = tx_in.script_pubkey(self.testnet)
            else:
                script_sig = None    
            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()
        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)
        if script_pubkey.is_p2sh_script_pubkey():
            cmd = tx_in.script_sig.cmds[-1]
            raw_redeem = encode_varint(len(cmd)) + cmd
            redeem_script = Script.parse(BytesIO(raw_redeem))
        else:
            redeem_script = None
        z = self.sig_hash(input_index, redeem_script)
        combined = tx_in.script_sig + script_pubkey
        return combined.evaluate(z)
    def is_p2pkh_script_pubkey(self):
        return len(self.cmds) == 5 and self.cmds[0] == 0x76 \
            and self.cmds[1] == 0xa9 \
            and type(self.cmds[2]) == bytes and len(self.cmds[2]) == 20 \
            and self.cmds[3] == 0x88 and self.cmds[4] == 0xac

    def is_p2sh_script_pubkey(self):
        return len(self.cmds) == 3 and self.cmds[0] == 0xa9 \
            and type(self.cmds[1]) == bytes and len(self.cmds[1]) == 20 \
            adn self.cmds[2] == 0x87

p2shの署名検証

p2pkhは1つの署名と1つの公開鍵のみだが、p2shは幾つかの公開鍵(RedeemScript内のSECフォーマット)と、それ以下の数の署名(ScriptSig内、DERフォーマット)がある。署名は公開鍵と同じ順番である必要がある。
特定の署名と公開鍵を取得したら、署名が有効かを判断するために必要なのは署名ハッシュ、すなわちzのみである。

### 署名ハッシュの取得方法
1. 全てのScriptSig(output)を空にする => 00
2. 署名対象となるp2shのインプットのScriptSigをRedeemScriptに置き換える
 L RedeemScriptを空のScriptSigの場所に配置する。※ScriptPubKeyでないことがp2pkhと異なる
3. 4バイトのハッシュタイプを末尾に追加する。p2pkhと同じ。SIGHASH_ALLに対応する整数は1であり、4バイトのリトルエンディアンでエンコードする必要がある
4. SEC公開鍵、DER署名はScriptSig(output)から

from ecc import S256Point, Signature
from helper import hash256
modified_tx = bytes.fromhex('0100000001868278ed6ddfb6c1ed3ad5f8181eb0c7a38\
5aa0836f01d5e4789e6bd304d87221a000000475221022626e955ea6ea6d98850c994f9107b036\
b1334f18ca8830bfff1295d21cfdb702103b287eaf122eea69030a0e9feed096bed8045c8b98be\
c453e1ffac7fbdbd4bb7152aeffffffff04d3b11400000000001976a914904a49878c0adfc3aa0\
5de7afad2cc15f483a56a88ac7f400900000000001976a914418327e3f3dda4cf5b9089325a4b9\
5abdfa0334088ac722c0c00000000001976a914ba35042cfe9fc66fd35ac2224eebdafd1028ad2\
788acdc4ace020000000017a91474d691da1574e6b3c192ecfb52cc8984ee7b6c5687000000000\
1000000')
h256 = hash256(modified_tx)
z = int.from_bytes(h256, 'big')
sec = bytes.fromhex('022626e955ea6ea6d98850c994f9107b036b1334f18ca8830bfff\
1295d21cfdb70')
der = bytes.fromhex('3045022100dc92655fe37036f47756db8102e0d7d5e28b3beb83a\
8fef4f5dc0559bddfb94e02205a36d4e4e6c7fcd16658c50783e00c341609977aed3ad00937bf4\
ee942a89937')
point = S256Point.parse(sec)
sig = Signature.parse(der)
print(point.verify(z, sig))

$ python3 test.py
True

from io import BytesIO
from ecc import S256Point, Signature
from helper import hash256, int_to_little_endian, encode_varint
from script import Script
from tx import Tx, TxIn, SIGHASH_ALL
hex_tx = '0100000001868278ed6ddfb6c1ed3ad5f8181eb0c7a385aa0836f01d5e4789e6\
bd304d87221a000000db00483045022100dc92655fe37036f47756db8102e0d7d5e28b3beb83a8\
fef4f5dc0559bddfb94e02205a36d4e4e6c7fcd16658c50783e00c341609977aed3ad00937bf4e\
e942a8993701483045022100da6bee3c93766232079a01639d07fa869598749729ae323eab8eef\
53577d611b02207bef15429dcadce2121ea07f233115c6f09034c0be68db99980b9a6c5e754022\
01475221022626e955ea6ea6d98850c994f9107b036b1334f18ca8830bfff1295d21cfdb702103\
b287eaf122eea69030a0e9feed096bed8045c8b98bec453e1ffac7fbdbd4bb7152aeffffffff04\
d3b11400000000001976a914904a49878c0adfc3aa05de7afad2cc15f483a56a88ac7f40090000\
0000001976a914418327e3f3dda4cf5b9089325a4b95abdfa0334088ac722c0c00000000001976\
a914ba35042cfe9fc66fd35ac2224eebdafd1028ad2788acdc4ace020000000017a91474d691da\
1574e6b3c192ecfb52cc8984ee7b6c568700000000'
hex_sec = '03b287eaf122eea69030a0e9feed096bed8045c8b98bec453e1ffac7fbdbd4b\
b71'
hex_der = '3045022100da6bee3c93766232079a01639d07fa869598749729ae323eab8ee\
f53577d611b02207bef15429dcadce2121ea07f233115c6f09034c0be68db99980b9a6c5e75402\
2'
hex_redeem_script = '475221022626e955ea6ea6d98850c994f9107b036b1334f18ca88\
30bfff1295d21cfdb702103b287eaf122eea69030a0e9feed096bed8045c8b98bec453e1ffac7f\
bdbd4bb7152ae'
sec = bytes.fromhex(hex_sec)
der = bytes.fromhex(hex_der)
redeem_script = Script.parse(BytesIO(bytes.fromhex(hex_redeem_script)))
stream = BytesIO(bytes.fromhex(hex_tx))
tx_obj = Tx.parse(stream)
s = int_to_little_endian(tx_obj.version, 4)
s += encode_varint(len(tx_obj.tx_ins))
i = tx_obj.tx_ins[0]
s += TxIn(i.prev_tx, i.prev_index, redeem_script, i.sequence).serialize()
s += encode_varint(len(tx_obj.tx_outs))
for tx_out in tx_obj.tx_outs:
    s += tx_out.serialize()
s += int_to_little_endian(tx_obj.locktime, 4)
s += int_to_little_endian(SIGHASH_ALL, 4)
z = int.from_bytes(hash256(s), 'big')
point = S256Point.parse(sec)
sig = Signature.parse(der)
print(point.verify(z, sig))

$ python3 test.py
True

p2shのコーディング

OP_HASH160, hash160, OP_EQUALの処理を追加する

            else:
                stack.append(cmd)
                if len(cmds) == 3 and cmds[0] == 0xa9 \
                    and type(cmds[1]) == bytes and len(cmds[1]) == 20 \
                    and cmds[2] == 0x87:
                    cmds.pop()
                    if not op_hash160(stack):
                        return False
                    stack.append(h160)
                    if not op_equal(stack):
                        return False
                    if not op_verify(stack):
                        LOGGER.info('bad p2sh h160')
                        return False
                    redeem_script = encode_varint(len(cmd)) + cmd
                    stream = ByteIO(redeem_script)
                    cmds.extend(Script.parse(stream).cmds)

p2shの特徴は、RedeemScriptが長くともOP_PUSHDATA2の最大長520byteである点
複雑なロジックを定義するスクリプトにすることができる。

### p2shのアドレス
p2shアドレスを計算するには、p2pkhアドレスの計算方法と類似したプロセスを用いる
hash160の前にプレフィックスバイトを、末尾にチェックサムを追加する
メインネットのp2shはプレフィックスバイトに0x05、Base58のアドレスは3から始まる
テストネットのp2shのプレフィックスバイトに0xc4を使用するため、アドレスは2から始まる

from helper import encode_base58_checksum
h160 = bytes.fromhex('74d691da1574e6b3c192ecfb52cc8984ee7b6c56')
print(encode_base58_checksum(b'\x05' + h160))

$ python3 test.py
3CLoMMyuoDQTPRD3XYZtCvgvkadrAdvdXh

p2pkhとp2shのアドレス

def h160_to_p2pkh_address(h160, testnet=False):
    if testnet:
        prefix = b'\x6f'
    else:
        prefix = b'\x00'
    return encode_base58_checksum(prefix + h160)

def h160_to_p2sh_address(h160, testnet=False):
    if testnet:
        prefix = b'\xc4'
    else:
        prefix = b'\x05'
    return encode_base58_checksum(prefix + h160)

p2sh(pay to script hash)

マルチシグ、複数署名の解法
複数の秘密鍵から単一の集約署名であるシェノア署名が一般的になると思われる

### ベアマルチシグ
オプコードのOP_CHECKMULTISIG (0xae)を理解する必要がある。
OP_CHECKMULTISIGは、スタックから多数の要素をポップし、必要な数の署名がトランザクションインプットに対して有効か否かを返す
トランザクションアウトプットは長いScriptPubKeyのまま

ベアマルチシグのScriptPubKey
51 OP_1
41 Length of pubkey
04fc..3d pubkey1
21 Length of pubkey2
0202..00 pubkey2
52 OP_2
ae OP_CHECKMULTISIG

ベアマルチシグのScriptSig
00 OP_0
48 Length of signature1
3045…01 signature1

pubkeyやsignatureの数が多くなればなるほどバイト数が多くなる
pubkey, signatureのm, nは1~20の数値を指定できる

### SCRIPTの例
OP_0, signature1, signature2, …, signature m, OP_m, pubkey1, … pubkey n, OP_n, OP_CHECKMULTISIG
OP_CHECKMULTISIGは、m + n + 3個の要素をポップし、n個の公開鍵のリストのうち、m個の署名が有効である場合は1をスタックにプッシュ、有効でない場合は0
OP_CHECKMULTISIGは m + n + 2この要素をプップするはずが、m + n + 3でポップしているため、OP_0が追加されている。

def op_checkmultisig(stack, z):
    if len(stack) < 1:
        return False
    n = decode_num(stack.pop())
    if len(stack) < n + 1:
        return False
    sec_pubkeys = []
    for _ in range(n):
        sec_pubkeys.append(stack.pop())
    m = decode_num(stack.pop())
    if len(stack) < m + 1:
        return False
    der_signatures = []
    for _ in range(m):
        der_signatures.append(stack.pop()[:-1])
    stack.pop()
    try:
        points = [S256Point.parse(sec) for sec in sec_pubkeys]
        sigs = [Signature.parse(der) for der in der_signatures]
        for sig in sigs:
            if len(points) == 0:
                LOGGER.info("signatures no good or not in right order")
                return False
            success = False
            while points:
                point = points.pop(0)
                if point.verify(z, sig):
                    success = True
                    break
                if not success:
                    return False
        stack.append(encode_num(1))
    except (ValueError, SyntaxError):
        return False
    return True

### ベアマルチシグの問題
m of nの署名を要求して単一障害を回避するが、問題がある
– ScriptPubKeyが長くなる
– outputsが通常のp2pkhアウトプットの5~20倍になる
これらの問題を軽減するためにPay to script hash(p2sh)が誕生している
L Scriptコマンドのハッシュを取得し、後でハッシュ化する前のScriptコマンドを明らかにする

### 特別なルールを実行するPay-to-script-hash
RedeemScript, OH_HASH160, hash, OP_EQUAL
スタックが1で終わる場合、RedeemScriptがパースされ、Scriptコマンドセットに追加 BIP160で導入された
2-of-2マルチシグのScriptPubKeyがあるとする これをp2shに変換する作業をスクリプトのハッシュを取得してスクリプトを引き換えたいときに利用しやすいように保持する
52 OP_2
21 Length of pubkey1
02…db70 pubkey1
21 Length of pubkey2
03…bb71 pubkey2
52 OP_2
ae OP_CHECKMULTISIG

Pay to script hash(p2sh)のScriptPubKey
a9 OP_HASH160
14 Length of Hash
74d6…56 hash
87 OP_EQUAL

これまでのScriptPubKeyに相当するもののhash160 アンロック時に明らかにしなければならないRedeemScriptのハッシュに資金をロックする
RedeemScriptは公開だけでなくアンロックにも必要で、ブロックチェーン上にはないため、p2shアドレスの作成者が保管しておかなければならない

2-of-2マルチシグのScriptSig
00 OP_0
48 Length of signature1
3045…3701 signature1
48 Length of signature2
3045…2201 signature
47 Length of RedeemScript
5221…ae RedeemScript

### 連結のScript
ScriptPubKey: OP_HASH160, hash, OP_EQUAL
ScriptSig: OP_0, signature1, signature2, RedeemScript
Script: OP_0, signature1, signature2, RedeemScript, OP_HASH160, hash, OP_EQUAL

特別なルールを実行するp2shのパターン: RedeemScript, OP_HASH160, hash, OP_EQUAL
RedeemScriptはScriptコマンドにセット
h160した結果がScriptPubKeyにあるh160の値と等しくなるRedeemScriptを明らかにすると、RedeemScriptはScriptPubKeyの代わりとして機能する
資金をロックするスクリプトをハッシュして、スクリプト自体の代わりにブロックチェーンに入れる

OP_0, signature1, signature2, RedeemScript, OP_HASH160, hash, OP_EQUAL
OP_HASH160でRedeemScriptをhashにするので、
OP_0, signature1, signature2, hash, OP_EQUALとなる。
hashが等しい場合は結果は有効とみなされる

52 OP_2
21 Length of pubkey1
02…db70 pubkey1
21 Length of pubkey2
03…bb71 pubkey2
52 OP_2
ae OP_CHECKMULTISIG

テストネットでの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

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サイズ等、より多くの項目を検証している