ビットコインの仕組みをpythonで実装してみる #2 「ビットコインの仕組みとトランザクション」

スポンサーリンク
Uncategorized

はじめに

前回は、アドレスの作成をしてみました。

今回はトランザクションについて記載し、コードを書きました。

トランザクションの中身

取引(トランザクション)

前回、アドレスを作成しましたが、このアドレスは取引の際に使用されます。

具体的には自分が相手のアドレス宛に送金する、又は、相手が自分のアドレス宛に送金するという取引が行われるわけです。

この取引のことをトランザクションといいます。

自分から相手への送金をする場合は以下の手順を踏みます。(各用語は後述しますので、一旦こういう流れなんだなあと思ってもらえれば大丈夫です)

  • 相手のアドレスを受け取る
  • 自分のウォレットが公開鍵ハッシュを抽出
  • 自分のUTXOを参照し、自分側でscriptPubKeyを書く
  • 秘密鍵で署名
    • 「私はこのコインを使う正当な所有者です」と証明するために署名をする
  • 新しいトランザクションをネットワークへ
  • マイナーがブロックに入れる

一方で、相手から自分への送金をする場合はこの自分と相手が逆になります。

UTXO

UTXO(Unspent Transaction Output)とは、まだ使われていないコインを指します。

ビットコインの世界では現実世界の銀行口座のような残高が存在しません。

代わりにあるのが、まだ使われていないコインが資産になります。

例えば、誰かが自分に送金します。

トランザクションA
入力:相手のUTXO
出力:1 BTC → 自分

この「1 BTC → 自分」が UTXOです。

まだ自分が使用していないコインです。

この時の状態は以下のようになります。

自分のUTXO一覧:
- 1 BTC

さらに0.3BTCを受け取ったとします。

この時は、

自分のUTXO一覧:
- 1 BTC
- 0.3 BTC

となります。

そして、ここから、自分が誰かに0.4BTC送金するとすると、

入力:
  1 BTC(消える)

出力:
  0.4 BTC → 相手
  0.6 BTC → 自分(お釣り)

となります。(※ここで入力と出力があることは重要なポイント)

その結果UTXOは以下のような状態になります。

あなたのUTXO一覧:
- 0.3 BTC(そのまま)
- 0.6 BTC(お釣り)

scriptPubKey

scriptPubKeyはコインを使うための条件です。

上記でも入力と出力が出てきましたが、ビットコインの各トランザクションは入力(input)と出力(output)があります。

この出力は金額とscriptPubKeyが入っている形になります。

scriptPubKeyは以下のような内容のプログラムになっています。

公開鍵をコピーし、それをハッシュせよ
その値がこの「公開鍵ハッシュ」と一致するか確認せよ
一致したら署名を検証せよ

つまり、この条件である、公開鍵ハッシュに対応する秘密鍵を持っている人だけ使えるわけです。

アドレスの中身は

version + 公開鍵ハッシュ + checksum

となっており、ウォレットは送金時に

Base58Checkをデコード
バージョン除去
公開鍵ハッシュ(20バイト)を取り出し、それを scriptPubKey に埋め込む

という事をして、相手に送金します。

では実際にここまでの内容をpythonでコードに落とし込んでみます。

scriptSig

UTXOを使える正当な所有者であることを証明するデータです。

scriptPubKeyはUTXOを使う正当な所有者であることの証明である署名とscriptPubKey のハッシュと一致するか検証するための公開鍵が含まれます。

検証が通れば、そのUTXOは使用可能となります。

python実装

トランザクションのpythonコードは以下のようになりました。(trx.pyというファイル名にしてあります。コードの下に詳細の説明を載せませた。)

import hashlib
import ecdsa
import base58

from address import generate_keypair, hash256

def little_endian(value: int, length: int) -> bytes:
	return value.to_bytes(length, byteorder="little")

def encode_varint(value: int) -> bytes:
	if value < 0xFD:
		return bytes([value])
	if value <= 0xFFFF:
		return b"\xFD" + little_endian(value, 2)
	if value <= 0xFFFFFFFF:
		return b"\xFE" + little_endian(value, 4)
	return b"\xFF" + little_endian(value, 8)

def push_data(data: bytes) -> bytes:
	if len(data) >= 0x4C:
		raise ValueError("Demo only supports short pushdata")
	return bytes([len(data)]) + data

def address_to_pubkey_hash(address: str) -> bytes:
	decoded = base58.b58decode(address)
	payload, checksum = decoded[:-4], decoded[-4:]
	if hash256(payload)[:4] != checksum:
		raise ValueError("Invalid address checksum")
	return payload[1:]

def p2pkh_scriptpubkey(pubkey_hash: bytes) -> bytes:
	return b"\x76\xa9" + b"\x14" + pubkey_hash + b"\x88\xac"

def serialize_txin(prev_txid: str, prev_index: int, script_sig: bytes) -> bytes:
	prev_txid_bytes = bytes.fromhex(prev_txid)[::-1]
	return (
		prev_txid_bytes
		+ little_endian(prev_index, 4)
		+ encode_varint(len(script_sig))
		+ script_sig
		+ little_endian(0xFFFFFFFF, 4)
	)

def serialize_txout(amount_sats: int, script_pubkey: bytes) -> bytes:
	return (
		little_endian(amount_sats, 8)
		+ encode_varint(len(script_pubkey))
		+ script_pubkey
	)

def create_sighash_preimage(
	prev_txid: str,
	prev_index: int,
	script_code: bytes,
	outputs: list[tuple[int, bytes]],
	locktime: int = 0,
	sighash_type: int = 1,
) -> bytes:
	version = little_endian(1, 4)
	txin = serialize_txin(prev_txid, prev_index, script_code)
	inputs = encode_varint(1) + txin

	serialized_outputs = b"".join(
		serialize_txout(amount, script) for amount, script in outputs
	)
	outputs_blob = encode_varint(len(outputs)) + serialized_outputs

	return (
		version
		+ inputs
		+ outputs_blob
		+ little_endian(locktime, 4)
		+ little_endian(sighash_type, 4)
	)

def sign_p2pkh_input(
	private_key: bytes,
	public_key: bytes,
	prev_txid: str,
	prev_index: int,
	script_code: bytes,
	outputs: list[tuple[int, bytes]],
) -> bytes:
	preimage = create_sighash_preimage(
		prev_txid, prev_index, script_code, outputs
	)
	z = hash256(preimage)
	sk = ecdsa.SigningKey.from_string(private_key, curve=ecdsa.SECP256k1)
	signature = sk.sign_digest(z, sigencode=ecdsa.util.sigencode_der_canonize)
	signature += b"\x01"  # SIGHASH_ALL

	return push_data(signature) + push_data(public_key)

def build_raw_transaction(
	prev_txid: str,
	prev_index: int,
	script_sig: bytes,
	outputs: list[tuple[int, bytes]],
	locktime: int = 0,
) -> bytes:
	version = little_endian(1, 4)
	inputs = encode_varint(1) + serialize_txin(prev_txid, prev_index, script_sig)
	serialized_outputs = b"".join(
		serialize_txout(amount, script) for amount, script in outputs
	)
	outputs_blob = encode_varint(len(outputs)) + serialized_outputs
	return version + inputs + outputs_blob + little_endian(locktime, 4)

def build_p2pkh_demo_tx(
	sender_priv: bytes,
	sender_pub: bytes,
	sender_addr: str,
	recipient_addr: str,
	utxo_amount: int,
	send_amount: int,
	fee: int,
	prev_txid: str,
	prev_index: int,
) -> bytes:
	change_amount = utxo_amount - send_amount - fee
	if change_amount < 0:
		raise ValueError("Insufficient funds for demo")

	sender_pubkey_hash = address_to_pubkey_hash(sender_addr)
	recipient_pubkey_hash = address_to_pubkey_hash(recipient_addr)

	outputs = [
		(send_amount, p2pkh_scriptpubkey(recipient_pubkey_hash)),
		(change_amount, p2pkh_scriptpubkey(sender_pubkey_hash)),
	]

	script_code = p2pkh_scriptpubkey(sender_pubkey_hash)
	script_sig = sign_p2pkh_input(
		sender_priv,
		sender_pub,
		prev_txid,
		prev_index,
		script_code,
		outputs,
	)

	return build_raw_transaction(prev_txid, prev_index, script_sig, outputs)

def build_p2pkh_demo_tx_details(
	sender_priv: bytes,
	sender_pub: bytes,
	sender_addr: str,
	recipient_addr: str,
	utxo_amount: int,
	send_amount: int,
	fee: int,
	prev_txid: str,
	prev_index: int,
) -> tuple[bytes, int, list[tuple[int, str]]]:
	change_amount = utxo_amount - send_amount - fee
	if change_amount < 0:
		raise ValueError("Insufficient funds for demo")

	sender_pubkey_hash = address_to_pubkey_hash(sender_addr)
	recipient_pubkey_hash = address_to_pubkey_hash(recipient_addr)

	outputs = [
		(send_amount, p2pkh_scriptpubkey(recipient_pubkey_hash)),
		(change_amount, p2pkh_scriptpubkey(sender_pubkey_hash)),
	]

	script_code = p2pkh_scriptpubkey(sender_pubkey_hash)
	script_sig = sign_p2pkh_input(
		sender_priv,
		sender_pub,
		prev_txid,
		prev_index,
		script_code,
		outputs,
	)

	raw_tx = build_raw_transaction(prev_txid, prev_index, script_sig, outputs)
	output_labels = [
		(send_amount, recipient_addr),
		(change_amount, sender_addr),
	]
	return raw_tx, change_amount, output_labels

def build_demo_state(
	me_to_other_amount: int,
	other_to_me_amount: int,
	fee: int,
	me_utxo_amount: int = 100_000,
) -> dict:
	if me_to_other_amount + fee > me_utxo_amount:
		raise ValueError("Me UTXO amount is insufficient for tx1")
	if other_to_me_amount + fee > me_to_other_amount:
		raise ValueError("Other amount is insufficient for tx2")

	me_priv, me_pub, me_addr = generate_keypair()
	other_priv, other_pub, other_addr = generate_keypair()

	prev_txid_1 = "00" * 32
	prev_index_1 = 0
	raw_tx_1, change_amount_1, outputs_1 = build_p2pkh_demo_tx_details(
		me_priv,
		me_pub,
		me_addr,
		other_addr,
		me_utxo_amount,
		me_to_other_amount,
		fee,
		prev_txid_1,
		prev_index_1,
	)
	txid_1 = hash256(raw_tx_1)[::-1].hex()

	prev_txid_2 = txid_1
	prev_index_2 = 0
	raw_tx_2, change_amount_2, outputs_2 = build_p2pkh_demo_tx_details(
		other_priv,
		other_pub,
		other_addr,
		me_addr,
		me_to_other_amount,
		other_to_me_amount,
		fee,
		prev_txid_2,
		prev_index_2,
	)
	txid_2 = hash256(raw_tx_2)[::-1].hex()

	utxos_t0 = [
		{
			"owner": "me",
			"txid": prev_txid_1,
			"index": prev_index_1,
			"amount": me_utxo_amount,
			"address": me_addr,
		},
	]
	utxos_t1 = [
		{
			"owner": "other",
			"txid": txid_1,
			"index": 0,
			"amount": outputs_1[0][0],
			"address": outputs_1[0][1],
		},
		{
			"owner": "me",
			"txid": txid_1,
			"index": 1,
			"amount": change_amount_1,
			"address": me_addr,
		},
	]
	utxos_t2 = [
		{
			"owner": "me",
			"txid": txid_2,
			"index": 0,
			"amount": outputs_2[0][0],
			"address": outputs_2[0][1],
		},
		{
			"owner": "other",
			"txid": txid_2,
			"index": 1,
			"amount": change_amount_2,
			"address": other_addr,
		},
		{
			"owner": "me",
			"txid": txid_1,
			"index": 1,
			"amount": change_amount_1,
			"address": me_addr,
		},
	]

	return {
		"me_address": me_addr,
		"other_address": other_addr,
		"tx1": {
			"raw_hex": raw_tx_1.hex(),
			"txid": txid_1,
			"input": {
				"txid": prev_txid_1,
				"index": prev_index_1,
				"amount": me_utxo_amount,
				"address": me_addr,
			},
			"outputs": [
				{"amount": outputs_1[0][0], "address": outputs_1[0][1]},
				{"amount": change_amount_1, "address": me_addr},
			],
		},
		"tx2": {
			"raw_hex": raw_tx_2.hex(),
			"txid": txid_2,
			"input": {
				"txid": prev_txid_2,
				"index": prev_index_2,
				"amount": me_to_other_amount,
				"address": other_addr,
			},
			"outputs": [
				{"amount": outputs_2[0][0], "address": outputs_2[0][1]},
				{"amount": change_amount_2, "address": other_addr},
			],
		},
		"utxo_timeline": {
			"t0": utxos_t0,
			"t1": utxos_t1,
			"t2": utxos_t2,
		},
	}

各関数の概要は以下のようになっています。

  • little_endian
    • Bitcoin のトランザクションやブロックデータは多くの数値をリトルエンディアンで格納するため、数値をリトルエンディアンのバイト列に変換する関数です。
      • 例:value=4660, length=4 → b’\x34\x12\x00\x00′
  • encode_varint
    • トランザクションの入力数・出力数や「scriptSig/scriptPubKey」の長さなど、長さ情報を効率よく格納するためにBitcoinで使われる「可変長整数(VarInt)」形式で value をバイト列にエンコードします。
  • push_data
    • スクリプト用に長さ付きでデータを積みます。
  • address_to_pubkey_hash
    • Base58Check形式の人間に読みやすいアドレスから、「公開鍵ハッシュ」に変換して値を返します。
  • p2pkh_scriptpubkey
    • 公開鍵ハッシュから、P2PKH(※1)用のscriptPubKeyを生バイト列で組み立てます。
  • create_sighash_preimage
    • トランザクション署名の対象となるpreimage(※2)を組み立てます。
  • sign_p2pkh_input
    • 手順としては以下のようになります。
      • create_sighash_preimage を使ってpreimageを作成
      • z = hash256(preimage)を計算し、これをECDSA署名(※3)のメッセージとして使用する
      • ecdsa.SigningKey.from_string(private_key, curve=ecdsa.SECP256k1) で秘密鍵オブジェクトを作成
      • sk.sign_digest(z, sigencode=ecdsa.util.sigencode_der_canonize) でDER形式(※4)の署名を作成
      • 署名の末尾に 0x01 を付けてSIGHASH_ALL(※5)とする
      • push_data(signature) と push_data(public_key) で、スクリプトのPUSHDATA形式(<length><data>) に変換して連結する
      • これがscriptSig(アンロックデータ)となる
        • script 実行時には、ここから署名と公開鍵がスタックに積まれる
  • serialize_txin
    • どのUTXOを入力として使うかをバイト列にします。
  • serialize_txout
    • 1本の出力をバイト列にします。
    • ここでscript_pubkeyが上でP2PKH スクリプトになり、「誰が・どの条件でこのUTXOを使えるか」が決まります。
  • build_raw_transaction
    •  scriptSig と outputs を使って、実際のトランザクションを1本組み立てます。
    • トランザクションの中身は以下
      • version
      • inputs
      • serialized_outputs
      • outputs_blob
      • little_endian
  • build_p2pkh_demo_tx_details
    • 上記関数たちを組み合わせて、1本の P2PKH送金トランザクションを作っています。
  • build_demo_state
    • 「me → other → me」という 2 本の送金をまとめてシミュレーションするための関数です。
  • (※1)P2PKH(=Pay To Public Key Hash)
    • 公開鍵のハッシュを持っている人だけが使えるお金というビットコインで一番基本的な送金方式
  • (※2)preimage
    • ビットコインでは、いきなりトランザクションに署名しません
    • まず「署名専用の形」にデータを並べ、それをハッシュし、そのハッシュに署名します
    • この署名するために用意した作ったデータがpreimageです
  • (※3)ECDSA署名(=Elliptic Curve Digital Signature Algorithm)
    • 秘密鍵で作った署名
  • (※4)DER形式(=Distinguished Encoding Rules)
    • データを決められた形に整えて保存するルールのこと
  • (※5)SIGHASH_ALL
    • 「このトランザクション全部に同意します」という署名タイプのこと

最後に

ブロックチェーン、トランザクションの段階で情報量多すぎワロタ。

タイトルとURLをコピーしました