Python、Streamlit と FastAPI で、ブロックチェーンを実装してみた♪③トランザクションを作成
今回は、いよいよトランザクションを作成し、プールに保存していきます!
前回までの記事
前々回 前回
環境
Windows10、PyCharam、Python v3.13.2、Streamlit v1.45.1、FastAPI v0.115.12
ディレクトリ構成
(node2 と node3 は、テスト用ファイルとフォルダ以外同じなので省略してます)
streamlit_fastapi
├ node1
| ├ json(JSON データ保存用)
| ├ test_data(テスト・データ保存用)
| ├ app.py
| ├ block_chain.log(ログ保存用)
| ├ log.py
| ├ main.py
| ├ mikoto.py
| ├ test_app_main.py
| └ test_mikoto.py
...
今回やること
まず、前回忘れてた log.py に型ヒントを追加します。
それから、トランザクションを作成(発行、送信)し、ブロードキャストして、検証後、プールに保存します。
log.py に型ヒントを追加
...
def get_logger(name: str) -> logging.Logger:
return logging.getLogger(name)
def basic_config(level: str, filename: str = None) -> None:
...
def log_debug(logger: logging.Logger, message: str) -> None:
return logger.debug(message)
def log_error(logger: logging.Logger, message: str) -> None:
return logger.error(message)
トランザクションを作成(発行、送信)
まず、トランザクションを作成する関数を実装してテストし、その後、フロントエンド、バックエンドを実装していきます。
mikoto.py、test_mikoto.py、app.py、main.py の順にファイルを作成します。
...
def make_thanks_transaction(
sender_secret_key_str: str, sender_public_key_str: str,
receiver_public_key_str: str, mik: int) -> dict:
thanks_transaction = {
'time': dt.datetime.now().isoformat(),
'sender': sender_public_key_str,
'receiver': receiver_public_key_str,
'MIK': mik,
}
signature = make_signature_str(
thanks_transaction, sender_secret_key_str)
thanks_transaction['signature'] = signature
return thanks_transaction
def make_mikoto_transaction(receiver_public_key_str: str, mik: int) -> dict:
mikoto_transaction = {
'time': dt.datetime.now().isoformat(),
'sender': 'mikoto_project',
'receiver': receiver_public_key_str,
'MIK': mik,
'signature': 'mikoto_project',
}
return mikoto_transaction
...
テストフィアルを作成します。
...
def test_make_thanks_transaction():
""" test: type, keys, conversion, value, unhexlify """
thanks_transaction = mik.make_thanks_transaction(
co_secret_key_str, co_public_key_str,
co2_public_key_str, 100
)
assert isinstance(thanks_transaction, dict)
keys = list(thanks_transaction.keys())
assert keys == ['time', 'sender', 'receiver', 'MIK', 'signature']
assert dt.datetime.fromisoformat(thanks_transaction['time'])
assert thanks_transaction['sender'] == co_public_key_str
assert thanks_transaction['receiver'] == co2_public_key_str
assert isinstance(thanks_transaction['MIK'], int)
assert isinstance(thanks_transaction['signature'], str)
assert binascii.unhexlify(thanks_transaction['signature'])
def test_make_mikoto_trasaction():
""" test: type, keys, value """
mikoto_transaction = mik.make_mikoto_transaction(
co_public_key_str, 100
)
assert isinstance(mikoto_transaction, dict)
assert list(mikoto_transaction.keys()) == ['time', 'sender', 'receiver', 'MIK', 'signature']
assert dt.datetime.fromisoformat(mikoto_transaction['time'])
assert mikoto_transaction['sender'] == 'mikoto_project'
assert mikoto_transaction['receiver'] == co_public_key_str
assert isinstance(mikoto_transaction['MIK'], int)
assert mikoto_transaction['signature'] == 'mikoto_project'
...
テストを実行、無事にパスできました。

フロントエンドを実装します。
ログイン後の実装になります。送る側は、本人か mikoto_project(発行)、送信時には本人には送信できないようにしています。
if 文が多いので、コードはやや見にくいですが、UI の使いやすさ優先で実装しました。
...
if ss.state is True:
st.markdown('ログイン中')
menu = ['発行または送信']
choice = st.selectbox(label='メニュー', options=menu)
key_data_list = mik.load_json('json/key_data_list.json')
my_data = mik.load_json('json/.my_data.json')
if choice == '発行または送信':
# select に変更(my_name or mikoto_project)
sender_name = st.selectbox('送る側(mikoto_projectは発行)', [my_data['name'], 'mikoto_project'])
receiver_name = st.text_input('受け取り側')
receiver_data = None
if receiver_name:
if receiver_name == my_data['name']:
st.markdown('自分には送信できません')
else:
for key_data in key_data_list:
if key_data['name'] == receiver_name:
receiver_data = key_data
st.write(receiver_data)
if not receiver_data:
st.markdown('登録されていないニックネームです')
mik_value = st.number_input('MIKの量', min_value=0, step=1)
if receiver_data and mik_value:
if st.button('実行する'):
if sender_name == 'mikoto_project':
transaction = mik.make_mikoto_transaction(
receiver_data['public_key_str'],
mik_value
)
st.json(transaction)
else:
transaction = mik.make_thanks_transaction(
my_data['secret_key_str'],
my_data['public_key_str'],
receiver_data['public_key_str'],
mik_value
)
st.json(transaction)
url_list = mik.get_url_list()
for url in url_list:
try:
res = mik.post_data(url+'/transaction', transaction)
st.json(res.json())
log.log_debug(logger, f'{res}: {res.json()}')
except:
st.json({"message": f"{url}: error"})
log.log_error(logger, f"{url}: error")
else:
st.markdown('ログアウト状態')
バックエンド側を実装します。発行の場合は、検証しない仕様にしています。
...
class Transaction(BaseModel):
time: str
sender: str
receiver: str
MIK: int
signature: str
...
@app.post('/transaction')
async def post_transaction(transaction: Transaction):
transaction = dict(transaction)
if transaction['sender'] != 'mikoto_project':
if not mik.verify_data(transaction, transaction['sender']):
log.log_error(logger, 'transaction invalid')
return {'message': 'transaction invalid'}
transaction_pool = mik.load_json('json/transaction_pool.json')
transaction_pool.append(transaction)
mik.save_json(transaction_pool, 'json/transaction_pool.json')
log.log_debug(logger, 'transaction received')
return {'message': 'transaction received'}
...
動作確認します。
ログ・ファイルを削除し、変更したファイルは、node2、node3 についても更新します。
各サーバーを立ち上げます。(計6つ)
streamlit run app.py --server.port 8501
streamlit run app.py --server.port 8502
streamlit run app.py --server.port 8503
uvicorn main:app --reload --port 8010
uvicorn main:app --reload --port 8011
uvicorn main:app --reload --port 8012
各ノードが 100MIK 自分に発行します(各サーバーから)
animal のサーバー(http://127.0.0.1:8501)

実行(発行)すると、無事に発行できました。

json/transaction_pool.json

block_chain.log

(内容はほぼ同じなので、以降の画像は省略します)
各ノードが他ノードに送信します。
animal(node1) -> dog(node2): 50MIK
dog(node2) -> cat(node3): 30MIK
cat(node3) -> animal(node1): 30MIK
すべて、無事に発行、送信できました
json/transaction_pool.json
(発行)

(送信)

block_chain.log(node1)

参考文献など
Satoshi Nakamoto (2008), ビットコイン:P2P 電子通貨システム,
https://bitcoin.org/files/bitcoin-paper/bitcoin_jp.pdf, 論文
モヤっとデータサイエンティスト (2022), 『Python によるブロックチェーン開発教本』, 電子書籍
安田恒 (2023), 『ブロックチェーンを作る!』, 秀和システム, 書籍
今回のコード
GitHub: https://github.com/Animalyzm/mikoto_project
今回のコードは、block_chain/streamlit_fastapi です。
Git のコミット・メッセージは、block_chain_streamlit_fastapi_3_transaction です。
今回は以上となります、ありがとうございましたー♪
Discussion