//tips
//pythonでのブロックチェーン記述
複数参加者間でピアツーピア通信を行うことでブロックチェーンを共有する機能をpeer.pyで確認する。
peer.pyは他の参加者からトランザクションやブロックチェーンの情報を取得するためwebサーバーを使用する。
各参加者がwebサーバーを起動しておくことで他の参加者に対してトランザクションやブロックファイルを公開する。
http.serverモジュールを使用して実行していくことになる。
Cd mycoinでmycoinフォルダにいたのでさらにその中にあるフォルダに移動する。
cd user1
ここから同一のコンピュータに対して複数の接続先を設ける際に接続先を区別するポート番号を利用していく。
デフォルトが8000なので今回は8001と8002のポートで検証していく。
python3 -m http.server 8001
を入力すると、下記のような結果が出てくる。
Serving HTTP on :: port 8001 (http://[::]:8001/) ...
また、新規で新しいターミナルを開き、user2フォルダにて下記のように実行する。
python3 -m http.server 8002
Serving HTTP on :: port 8002 (http://[::]:8002/) ...
このようにして起動した状態にしておく。
各々のフォルダにてコインの生成と送金を今までのスクリプトを使用して実施していく。
まずはサーバー接続したターミナルは放置し、新たに作成したuser1のターミナルにて下記を実行すると
python3 ..¥peer.py
No such file or directoryとなり実行できない。
python3 ../peer.py
で実行できることがわかったのでこちらで試行。
mine: private key: HbREJM8pqcTVSRs4qDqRn6YGaLvd8WSK6yis5WeecHTA
mine: public key: 2NYLsXTuiUj2foheeqTqA9ZFNcNQSP5JjpwHBiR4SsHZnwPu35JmsLc2WSi4LZEq5Fjjo34Xoi9DQqesy2HLprFH
ただbase58に関するエラーが発生している。
Traceback (most recent call last):
File "/Users/akihironakamura/Desktop/mycoin/mine.py", line 1, in <module>
import base58
ImportError: No module named base58
下記でbase58の再インストールを行うもすでに見たいしているという表示がされたので再度トライ。
python3 -m pip install base58
Requirement already satisfied: base58 in /Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages (2.1.0)
pip3 install PACKAGE
python3.9 -m pip install PACKAGE
などを試すがエラー。base58のインストールエラーはかなり発生しているよう。
エラー要因としては、pip/python3での新機能バッティング、ターミナルのzsh機能とpythonのコードが合わないからなどが考えられる。または、スクリプトの osでのフォルダパスの問題が考えられる。
ここはpython3でのこの問題への対処方法が見つからないのでスクリプトだけ追う。
まだ、python3ではなく旧バージョンなどで実装を行うべきなのかもしれない。
user1で
python .. ¥peer.py
を行うと、本来はマイニング用の秘密鍵、マイニング用の公開鍵、ゴールデンノンスとブロックハッシュが得られる。
この後に
python ..¥wallet.py
を実行するとunspent keysが一つ表示される。
今度はuser2で
python ..¥key.py
を実行し、鍵を作成。
user1に戻り、送金を行う。
python ..¥sign.py user1の秘密鍵 user1の公開鍵 user2の公開鍵
これでトランザクションファイルが作成される。
User1で再度マイニングを行うと、webサーバーを経由し、先のトランザクションが含まれるファイルを取得する。
ここからは実際にコードの方を確認する。
まずはモジュールから。
import base58
import ecdsa
import filelock
import hashlib
import json
import os //パス(ファイルやディレクトリなどを表す文字列)を操作するために使用
import re
import shutil //ファイルのコピーなどを行うために使用
import subprocess //別のプログラムを呼び出すために使用
import urllib.request //webサーバと通信するために使用
DIFFICULTY = 4
shutilモジュールのcopy関数を使用してトランザクションを格納したtrans.txtをpeer_trans.txtにコピーする。
try:
shutil.copy('trans.txt', 'peer_trans.txt')
//もしtrans.txtが存在しない場合はpassで何も処理をせずに次に進ませる。
except:
pass
通信先をJSON形式のテキストファイルpeer.txtから読み込む。
peer.txtは下記のようになっているもの。127.0.0.1はそのコンピューだ自信を表す特別なコード。
[
//IPアドレス:ポート番号
"127.0.0.1:8001",
"127.0.0.1:8002"
]
osモジュール機能を使用してpeer.pyと同じフォルダにあるpeer.txtを読み込むための処理を行なっている。
dir = os.path.dirname(os.path.abspath(__file__))
try:
with open(os.path.join(dir, 'peer.txt'), 'r') as file:
peer_list = json.load(file)
except:
peer_list = []
次にブロックチェーンのファイルを読み込む。
with filelock.FileLock('block.lock', timeout=10):
try:
with open('block.txt', 'r') as file:
block_list = json.load(file)
except:
block_list = []
ブロックチェーンの長さは各通信先で異なる可能性があるので最長のものを取得するためには各通信先のものを順番に取得し、その中で最長のものを選び出す必要がある。
for peer in peer_list:
urllib.requestモジュールのurlopen関数を使って各通信先が接続しているwebサーバと接続し、ブロックチェーンのファイルblock.txtを取得する。
url = 'http://' + peer + '/block.txt'
try:
with urllib.request.urlopen(url) as file:
peer_block_list = json.load(file)
except:
peer_block_list = []
自分が保有するblock_listよりも長いpeer_block_listがあれば下記の処理を行う。
if len(block_list) < len(peer_block_list):
取得した最長のブロックチェーンからブロックを一つずつ取り出し、ブロックハッシュを計算。
for block in peer_block_list:
sha = hashlib.sha256()
sha.update(bytes(block['nonce']))
sha.update(bytes.fromhex(block['previous_hash']))
sha.update(bytes.fromhex(block['tx_hash']))
hash = sha.digest()
もし難易度指定を満たしていなかったらその時点で中断。
if not re.match(r'0{' + str(DIFFICULTY) + r'}', hash.hex()):
break
全てのハッシュが適切であった場合には、通信で取得したブロックチェーンを採用。
else:
print('download:', url, 'length:',
len(block_list), '->', len(peer_block_list))
block_list = peer_block_list
ここでは簡易版の検証だが、本来のものはさらに多くの検証が必要になる。ただ、ベースの考え方は同じ。
ブロックチェーンをblock.txtに書き込む。
with filelock.FileLock('block.lock', timeout=10):
with open('block.txt', 'w') as file:
json.dump(block_list, file, indent=2)
通信先からトランザクションpeer_trans.txtを取得し、それを自身のトランザクションリストtx_listに追加する。
tx_list = []
for peer in peer_list:
url = 'http://' + peer + '/peer_trans.txt'
try:
with urllib.request.urlopen(url) as file:
peer_tx_list = json.load(file)
tx_list += peer_tx_list
print('download:', url, 'count:', len(peer_tx_list))
except:
pass
それから、自分のtrans.txtも読み込み、tx_listに追加して、再度その更新された内容をtrans.txtに書き戻している。
with filelock.FileLock('trans.lock', timeout=10):
try:
with open('trans.txt', 'r') as file:
tx_list += json.load(file)
except:
pass
with open('trans.txt', 'w') as file:
json.dump(tx_list, file, indent=2)
本来であれば重複分は除外するがそこは省略している。
private_key = ecdsa.SigningKey.generate(curve=ecdsa.SECP256k1)
public_key = private_key.get_verifying_key()
マイニング用の鍵のペアを作成し、base58形式に変換。
private_key = base58.b58encode(private_key.to_string()).decode('ascii')
public_key = base58.b58encode(public_key.to_string()).decode('ascii')
この鍵のペアをkey.txtに追加。追加したもので再更新。
with filelock.FileLock('key.lock', timeout=10):
try:
with open('key.txt', 'r') as file:
key_list = json.load(file)
except:
key_list = []
key_list.append({
'private': private_key,
'public' : public_key
})
with open('key.txt', 'w') as file:
json.dump(key_list, file, indent=2)
マイニング用の鍵のペアを表示。
print('mine: private key:', private_key)
print('mine: public key:', public_key)
マイニングのmine.pyをsubprocessモジュールのrun関数で呼び出している。
subprocess.run(['python', os.path.join(dir, 'mine.py'), public_key])
これがpeer.pyの実体となる。もし、複数のコンピュータに分けて作業する場合には先のpeer.txtのIPアドレスを各コンピュータのものに書き換えれば良い。
また、デーモンと呼ばれるバックグラウンドで自動的にユーザー間のブロックやトランザクションを共有し、自動的にマイニングを行うプログラムを見ていく。
ここで参加者がお互いのマイニングの進捗を確認し合うことができ、早く終わった方からブロックチェーンを取得する。
基本的には先のpeer.pyの動作を永続させれば良いので下記のようにすれば良い。
import os
import subprocess
dir = os.path.dirname(os.path.abspath(__file__))
while True:
subprocess.run(['python', os.path.join(dir, 'peer.py')])
これでpythonでの大まかなブロックチェーンスクリプトの流れがわかった。
Pythonのスクリプト自体は難しくなく、どちらかというとモジュールを知っているか、モジュールを開発できるかの方が大切なように思われる内容であった。
資金の移動が生じる際にはまず秘密鍵が作成され、その秘密鍵を変数に公開鍵も作成される。
それらを文字列に変換した後にbase58変換したものを鍵として格納。
電子署名するために、それらの入力秘密鍵、入出力公開鍵をバイト列変換。
SHA-256を使って、公開鍵二つをもとにハッシュを作成した後に、入力秘密鍵からの文字列から秘密鍵オブジェクトを作成し、そのオブジェクトに対して、ハッシュを引数とするsignメソッドを実行することで電子署名とする。
この電子署名が作成された段階はトランザクションの発生時点なので、トランザクションリストtrans.txtに公開鍵と署名を追加する。
検証ではtrans.txtを読み込み、トランザクションを一つずつ取り出し、バイト列として公開鍵と署名を取り出す。
公開鍵二つに対してSHA-256を使ってハッシュを作成。
一方で署名を使ってハッシュを作成する必要があるので、署名の元となる秘密鍵に由来する入力公開鍵から公開鍵オブジェクトを作成。
key = ecdsa.VerifyingKey.from_string(tx_in, curve=ecdsa.SECP256k1)
key.verify(tx_sig, hash))でsignの際に使用したハッシュを復元できる。
これはsign.pyのところと比較すればわかりやすくVerifyとSignでメソッドが対になっていることがわかる。
key = ecdsa.SigningKey.from_string(tx_key, curve=ecdsa.SECP256k1)
sig = key.sign(hash)
Ecdsaはそのような仕組みを提供しているモジュールだと理解しておく。
マイニングでは、報酬を出力する公開鍵を定め、送金するだけでなく、ブロックのそのもののハッシュ、直前のブロックハッシュやノンスにも考慮する必要が出てくる。
まずブロックチェーンでの自分のブロック位置を確認するためにblock.txtを読み込み、直前のブロックハッシュの取得も行う。
その後に、トランザクションファイルを読み込む。
ブロックチェーン上の全てのブロックについて順に処理し、各ブロック内部の全てのトランザクションを入力と出力に分ける。
入力と出力をもとに先のハッシュを用いた検証を行いトランザクションが正しい場合はリストに残す。また、別途public_keyを二重使用していないかも確認し、今回使用する分も追加。
ここからtx_listの先頭にブロック生成のジェネレーショントランザクションを挿入。
その上でブロック内全てのトランザクションは出揃ったので、全ての入出力、署名を利用してブロックのトランザクションハッシュを作成。
ここからノンスを探索していく。
直前のトランザクションハッシュと先に出したトランザクションハッシュと変数ノンスから、繰り返しハッシュを計算する。
このハッシュがブロックチェーンのハッシュで、それを実現したノンスも抽出できる。
新たにそれらの情報を付加したリストをブロックチェーンのblock.txtに追加。
最後にtrans.txtから今回ブロックに組み込んだ分を削除。