ブロックチェーンの作り方 #2(トランザクションの送受信)

ブロックチェーン
ブロックチェーン
スポンサーリンク

前回 ブロックチェーンの作り方#1 では、今回作成するブロックチェーンの概要と Python でブロックチェーンを作成するための開発環境の準備について説明しました。今回 #2 では、ブロックチェーンに格納されるトランザクションの処理を実装します。具体的には、利用者がトランザクションを送信するためのプログラムと、ノード側でトランザクションを受信しトランザクションプールに保存するプログラムを作成します。

関連記事:開発環境の準備とファイル構成

秘密鍵と公開鍵の作成

ブロックチェーンの利用者は個別に秘密鍵と公開鍵のペアを持つ必要があります。秘密鍵はトランザクションに署名を行うために利用し、公開鍵はブロックチェーンアドレスとして利用します。

作成の手順はまず秘密鍵を作成し、その秘密鍵から公開鍵を作成します。作成する秘密鍵は暗号化方式によって色々な種類があるのですが、今回は楕円曲線暗号(ECDSA:Elliptic Curve Digital Signature Algorithm)を使い楕円曲線は secp256k1 を採用します。(ビットコインやイーサリアムでもこの楕円曲線暗号と楕円曲線が使われています)

gen_key.py

Python で楕円曲線暗号方式の秘密鍵と公開鍵の作成を実装すると以下のようなプログラムになります。ecdsa モジュールを使いますので事前にインストールしておきましょう。

blockchain/tool/gen_key.py
from ecdsa import SigningKey, SECP256k1

secret_key = SigningKey.generate(curve = SECP256k1)
print("秘密鍵:" + secret_key.to_string().hex())

public_key = secret_key.verifying_key
print("公開鍵:" + public_key.to_string().hex())

コードを実行して秘密鍵と公開鍵が表示されることを確認してください。

python blockchain/tool/gen_key.py

秘密鍵:<実際には秘密鍵が表示されます>
公開鍵:30ccdd8209ff3d9fb8a4b08d2f5d20e66af2650db26a470287372691e9a7c4c97e52bf8af7b6c1c9698eaee822bc794d48fb1526a9a3d2d1c249e365f0fc9157

検証用ユーザーの秘密鍵と公開鍵

users.py

これは説明の都合になりますが、トランザクションの送信処理などで使う検証用ユーザーの秘密鍵と公開鍵は users.py にまとめておいて各実行ファイルにインポートして利用することとします。

先ほど作成した gen_key.py で各検証用ユーザーの秘密鍵と公開鍵と作成して、秘密鍵は private_key に、公開鍵は public_key に設定してください。

blockchain/mod/users.py
users = {

    # Aさんの秘密鍵と公開鍵
    'A': {
        'private_key':  '<Aさんの秘密鍵>',
        'public_key':   '<Aさんの公開鍵>'
    },

    # Bさんの秘密鍵と公開鍵
    'B': {
        'private_key':  '<Bさんの秘密鍵>',
        'public_key':   '<Bさんの秘密鍵>',
    },

    # Cさんの秘密鍵と公開鍵
    'C': {
        'private_key':  '<Cさんの秘密鍵>',
        'public_key':   '<Cさんの秘密鍵>',
    }
}

トランザクションの送信

下準備ができたところで、本題のトランザクションを処理するためのプログラムを作成していきましょう。

BlockChain.py

まずは、利用者がトランザクションを送信するためのプログラムを作成します。この記事では、ブロックチェーンのコアとなる機能を BlockChain.py に実装し、実行ファイルにインポートする形でプログラムを作成していきます。requests モジュールを使いますので事前にインストールしておきましょう。

BlockChain.py ファイルを作成して、ブロックチェーンのコア機能を実装するための BlockChain クラスを定義して必要な機能を実装していきます。

blockchain/mod/BlockChain.py
import random
from ecdsa import SECP256k1, SigningKey, VerifyingKey, BadSignatureError
import binascii
import json
import requests

# ブロックチェーンのコア機能を実装するためのクラス
class BlockChain:
    def __init__(self):
        # ノードのIPアドレス
        self.node_ips = [
            '127.0.0.1'
        ]

    # ノードのアドレスをどれか1つ返す
    def get_node_ip(self):
        return random.choice(self.node_ips)
    
    # 署名の生成
    def gen_signature(self, target_dict, private_key):
        # 秘密鍵をオブジェクト化
        private_key_obj = SigningKey.from_string(binascii.unhexlify(private_key), curve=SECP256k1)

        # 署名対象をJSONに変換してから署名(辞書型のままでは署名できない)
        target_json = json.dumps(target_dict).encode('utf-8')
        signature   = private_key_obj.sign(target_json)

        # 署名(16進数)を返す
        return signature.hex()
    
    # トランザクションの送信
    def send_tx(self, tx_dict):
        # ノードのIPアドレス
        node_ip = self.get_node_ip()

        # トランザクションの送信先URL
        url = 'http://' + node_ip + ':8000/tx-pool'

        # トランザクションをJSONに変換(辞書型のままでは送信できない)
        tx_json = json.dumps(tx_dict).encode('utf-8')

        # 送信
        res = requests.post(url, tx_json)

        # 送信結果を返す
        return res

ecdsa モジュールの VerifyingKey と BadSignatureError は、今の段階では使いませんが後々使いますのでインポートしておいてください。

self.node_ips 変数
ノードのIPアドレスをリストで指定します。開発段階ではノード側のプログラムも同じパソコンで動作確認を行いますので、今の段階ではローカルループバックアドレスの「127.0.0.1」を1つのみ指定しておきます。

get_node_ip メソッド
self.node_ips 変数に指定されたノードのIPアドレスうち1つをランダムに返します。

gen_signature メソッド
トランザクション署名を作成するためのメソッドです。引数 target_dict にトランザクションを辞書型(dict型)で指定し、引数 private_key に送信者の秘密鍵を指定することで、target_dict に指定されたトランザクションデータの署名を作成して返します。

send_tx メソッド
トランザクションをノードに送信するためのメソッドです。引数 tx_dict に指定された署名付きのトランザクションをノードの指定されたエンドポイントに送信します。

send_tx.py

send_tx.py は、トランザクションを作成して送信するための実行ファイルです。作成した users.pyBlockChain.py をインポートしてください。また、トランザクションの作成に使いますので time モジュールもインポートしておきましょう。

blockchain/send_tx.py
from mod.BlockChain import BlockChain
from mod.users import users
import time

# ブロックチェーンインスタンス
bc = BlockChain()

# トランザクションの作成
tx = {
    'time':     time.time(),
    'sender':   users['A']['public_key'],
    'to':       users['B']['public_key'],
    'coin':     3
}

# トランザクション署名の追加
tx['signature'] = bc.gen_signature(tx, users['A']['private_key'])

# 送信
res = bc.send_tx(tx)
print(res.text)

トランザクション tx の各キーに指定する値は次の通りです。

time UNIX時間(1970年1月1日午前0時0分0秒からの経過秒数)を指定します。
sender 送信者の公開鍵を指定します。
to 受信者の公開鍵を指定します。
coin 送信するコインの枚数を指定します。コインの枚数は整数のみとしマイナスは許可しないものとします。
signature 上記4つのキーを指定したトランザクション tx と送信者の秘密鍵を BlockChain クラスの gen_signature メソッドに渡して、作成されたトランザクション署名を指定します。

完成した署名付きのトランザクションを BlockChain クラスの send_tx メソッドに渡して実行すれば、ノードの指定されたエンドポイントにトランザクションが送信されます。

今の段階ではノードが起動していない状態ですので send_tx.py を実行してもエラーになります。

python blockchain/send_tx.py 
(略)
ConnectionRefusedError: [Errno 61] Connection refused

トランザクションの受信

続いて、ノード側で実行するトランザクションを受信するためのプログラムを作成していきましょう。

node.py

とりあえずトランザクションを受信して表示するだけのプログラムを作成して、順を追って処理を追加していきます。Python での Web API 構築フレームワークの定番 fastapi モジュールとデータ検証を行う pydantic モジュール、ASGI Webサーバーとして uvicorn モジュールを使いますので事前にインストールしておきましょう。

blockchain/node.py
from mod.BlockChain import BlockChain
from fastapi import FastAPI
from pydantic import BaseModel
import uvicorn

# FastAPIインスタンス
app = FastAPI()

# ブロックチェーンインスタンス
bc = BlockChain()

# トランザクションデータの定義
class Tx(BaseModel):
    time:       float
    sender:     str
    to:         str
    coin:       int
    signature:  str

# (仮)トランザクションの受信
@app.post('/tx-pool')
def receiv_tx(tx :Tx):
    print(tx)
    return 'OK'

# Webサーバー起動
if __name__ == '__main__':
    uvicorn.run('node:app', host='0.0.0.0', port=8000)

【コードの要点】

class Tx(BaseModel):
pydantic の BaseModel を継承してトランザクションデータを定義しておきます。(fastapi は pydantic に依存しているため、受信するデータの定義が必須になります)

@app.post('/tx-pool')
トランザクションを受信するエンドポイントを指定します。エンドポイント名に決まりはありませんが BlockChain クラスの send_tx メソッドの送信先URLに対応するようにしてください。この直下の関数 receiv_tx が受信したトランザクションを処理しますので、とりあえトランザクションを表示し、レスポンスとして「OK」の文字列を返すようにしておきます。

uvicorn.run('node:app', host='0.0.0.0', port=8000)
待ち受けポート「8000」で ASGI Webサーバーを起動します。待ち受けポートに決まりはありませんが、こちらも BlockChain クラスの send_tx メソッドの送信先URLに対応するようにしてください。

動作確認

node.py を実行して以下のような表示になればトランザクションを受信できる状態です。

python blockchain/node.py

INFO:     Started server process [10899]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)

この状態で send_tx.py を実行すると、先ほどのようなエラーは発生せず「OK」の文字列が表示されるはずです。

python blockchain/send_tx.py
"OK"

node.py 側では受信したトランザクションが表示されていると思います。

python blockchain/node.py 
(略)
INFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
time=1734163524.491963 sender='f4561c9d3d2d12f62577024408f0cc40e238ef78b1b4611b80b32792e2c72a5bd1d12c4b99a47a2f2a1fefe445ec27cc69aa22b6d337655412fb3a8511079328' to='0b0150759100ba4b73fab109f76139d4ec3499c57bbf747244d603b972651ad22136720800da22a120baeb42516494bf89fcd88325be28e59309fe0857f7ea2a' coin=3 signature='ac9a19e3a69e672426a3a29c23aabe56788c1c38684ee439a3a5489e8601249b8b64f8da21ec8d8896548e910171465196a18f1625ab0
INFO:     127.0.0.1:52469 - "POST /tx-pool HTTP/1.1" 200 OK

トランザクションプール

受信したトランザクションをトランザクションプールに保存するための処理を追加します。

BlockChain.py

トランザクションプールの処理に pandas モジュールと os モジュールを使いますので BlockChain.py の冒頭でインポートし、以下の変数とメソッドを追加します。

また、blockchain ディレクトリの下に data ディレクトリを作成してください。ここにトランザクションプールを保存します。

blockchain/mod/BlockChain.py
(↓モジュールを追加↓)
import pandas as pd
import os

class BlockChain:
    def __init__(self):

        (↓変数を追加↓)
        # トランザクションプール
        self.tx_pool = {'txs': []}

        # トランザクションプールの保存先
        self.tx_pool_file = './blockchain/data/tx_pool.pkl'

    (↓メソッドを追加↓)
    # トランザクションプールをファイルに保存する
    def save_tx_pool(self):
        pd.to_pickle(self.tx_pool, self.tx_pool_file)

    # 受取ったトランザクションをトランザクションプールに追加する
    def add_tx_pool(self, tx):
        self.tx_pool['txs'].append(tx)

        # トランザクションプールをファイルに保存する
        self.save_tx_pool()

    # ファイルに保存されているトランザクションプールを読み出す
    def load_tx_pool(self):
        if os.path.isfile(self.tx_pool_file):
            return pd.read_pickle(self.tx_pool_file)
        else:
            # ファイルが無ければ初期状態のトランザクションプールを返す
            return self.tx_pool 

【コードの要点】

self.tx_pool_file 変数
トランザクションプールの保存先のファイルパスを指定します。実行ファイルを実行する時のカレントディレクトリからの相対パスで指定してください。ファイルパスを ./blockchain/data/tx_pool.pkl とした場合は python blockchain/node.py のように実行する必要があります。

node.py

node.py に受信したトランザクションをトランザクションプールに保存するための処理を追加していきます。また、確認用にトランザクションプールを返す関数も追加しておきましょう。

blockchain/node.py
(略)
# ブロックチェーンインスタンス
bc = BlockChain()

(↓起動処理を追加↓)
# ノードの起動処理
bc.tx_pool = bc.load_tx_pool()  # トランザクションプールの読出し

(↓関数を追加↓)
# トランザクションプールを返す
@app.get('/tx-pool')
def get_tx_pool():
    return bc.tx_pool
    
(↓処理を追加↓)    
# トランザクションの受信
@app.post('/tx-pool')
def receiv_tx(tx :Tx):
    # データモデルインスタンスを辞書(dict)型に変換
    tx_dict = tx.model_dump()

    # 受信したトランザクションをトランザクションプールに追加
    bc.add_tx_pool(tx_dict)

    # レスポンス
    return 'OK'

【コードの要点】

bc.tx_pool = bc.load_tx_pool()
ノードの起動処理として、BlockChain クラスの load_tx_pool メソッドを実行してファイルに保存されているトランザクションプールを読出し、インスタンス変数 tx_pool にセットします。

@app.get('/tx-pool')
このエンドポイントにGETでリクエストすると、直下の関数 get_tx_pool が実行されトランザクションプールを返します。今の段階では動作確認用に使いますが、後ほどマイニングを実装する時にこのエンドポイントを使います。

動作確認

トランザクションを送信します。

python blockchain/send_tx.py

ブラウザで「http://127.0.0.1:8000/tx-pool」にアクセスしてトランザクションプールが表示されることを確認してください。

トランザクションプールの表示

また、node.py を再起動(実行を停止して再度実行)して、トランザクションプールが保持されていることも確認しておきましょう。

今回のまとめ

今回は、利用者がトランザクションを送信し、それをノード側で受信してトランザクションプールに保存するところまでを実装しました。しかし、このままでは利用者が不正なトランザクションを送信したとしても、無条件でトランザクションプールに登録されてしまいますので、次回はトランザクションの検証処理を実装していきます。

>>次のページ 【記事作成中】ブロックチェーンの作り方 #3(トランザクションの検証)
 
<<前のページ ブロックチェーンの作り方 #1(概要と開発環境の準備)

コメント

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