前回 ブロックチェーンの作り方#1 では、今回作成するブロックチェーンの概要と Python でブロックチェーンを作成するための開発環境の準備について説明しました。今回 #2 では、ブロックチェーンに格納されるトランザクションの処理を実装します。具体的には、利用者がトランザクションを送信するためのプログラムと、ノード側でトランザクションを受信しトランザクションプールに保存するプログラムを作成します。
関連記事:開発環境の準備とファイル構成
秘密鍵と公開鍵の作成
ブロックチェーンの利用者は個別に秘密鍵と公開鍵のペアを持つ必要があります。秘密鍵はトランザクションに署名を行うために利用し、公開鍵はブロックチェーンアドレスとして利用します。
作成の手順はまず秘密鍵を作成し、その秘密鍵から公開鍵を作成します。作成する秘密鍵は暗号化方式によって色々な種類があるのですが、今回は楕円曲線暗号(ECDSA:Elliptic Curve Digital Signature Algorithm)を使い楕円曲線は secp256k1 を採用します。(ビットコインやイーサリアムでもこの楕円曲線暗号と楕円曲線が使われています)
gen_key.py
Python で楕円曲線暗号方式の秘密鍵と公開鍵の作成を実装すると以下のようなプログラムになります。ecdsa モジュールを使いますので事前にインストールしておきましょう。
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
に設定してください。
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
クラスを定義して必要な機能を実装していきます。
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.py
と BlockChain.py
をインポートしてください。また、トランザクションの作成に使いますので time モジュールもインポートしておきましょう。
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 モジュールを使いますので事前にインストールしておきましょう。
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
ディレクトリを作成してください。ここにトランザクションプールを保存します。
(↓モジュールを追加↓) 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
に受信したトランザクションをトランザクションプールに保存するための処理を追加していきます。また、確認用にトランザクションプールを返す関数も追加しておきましょう。
(略) # ブロックチェーンインスタンス 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
を再起動(実行を停止して再度実行)して、トランザクションプールが保持されていることも確認しておきましょう。
今回のまとめ
今回は、利用者がトランザクションを送信し、それをノード側で受信してトランザクションプールに保存するところまでを実装しました。しかし、このままでは利用者が不正なトランザクションを送信したとしても、無条件でトランザクションプールに登録されてしまいますので、次回はトランザクションの検証処理を実装していきます。
コメント