//tips
//pythonでのブロックチェーン記述
マイニングを実行をpythonコードで確認する。
マイニングの報酬を出力するためにターミナルで鍵をもう一組み作成する指示を出す。
Cd desktop
Cd mycoin
Python3 key.py
private key len: 32
private key hex: 54c7f7cdf91fcbf4bf76506a20362ea09da539f5bc9930174243b83d555c0fa3
public key len: 64
public key hex: dba06b5bac498caa9ed52153d5b57e3e6f41ced09d03f13757e5f00e9da781b9e21d71505b9b4dbacf0a0163d935b53b6fb673f5cf302fa818c6958176c6579a
private key base58: 6hx6m6Am7hiG7uLg2H8xEKm8yDGdcBNLccpeMVwVtCtJ
public key base58: 5PgTb3hJQBPJTmsrRRUo6UH5zb9R4s6BnH7sxzz8PpokRnvqNXuuAJmzevAPDxJTiuXBmiQNLqh2Qp14hERN39MK
これで前回の流れから計3組の鍵のペアができたことになる。
key.txtを確認すると下記のような内容となっていることがわかる。
{
"private": "4QU2fA8PE6NU95bDu2DiRP7pJ7p4Pwq3dSs1eAByeGLa",
"public": "3G3B1MkMwVEDNwE1Vx5j99peMbABmVFheGgSG1NV3h762z8gZV5NGczQaXnTpqfvTkSW1VbAMFhr8eEbZ7hmCYB3"
},
{
"private": "EXXZifNcoZ5Vxm6N8Z7pxeUzdT4DEkB46MR9PT9EcNK1",
"public": "35xdAyr3JdPqvK8UGum8HhWouEwxgyyiLgLMYeSnKdvmJhw4YeAXDj74PFDWGttgHxdATb41SPVh9eEYzhVjoKE7"
},
{
"private": "6hx6m6Am7hiG7uLg2H8xEKm8yDGdcBNLccpeMVwVtCtJ",
"public": "5PgTb3hJQBPJTmsrRRUo6UH5zb9R4s6BnH7sxzz8PpokRnvqNXuuAJmzevAPDxJTiuXBmiQNLqh2Qp14hERN39MK"
}
]
ここからマイニングを実行していく。python3 mine.py 報酬を出力する公開鍵 verboseを使用してノンスを探索する過程も表示してみる
python3 mine.py 3G3B1MkMwVEDNwE1Vx5j99peMbABmVFheGgSG1NV3h762z8gZV5NGczQaXnTpqfvTkSW1VbAMFhr8eEbZ7hmCYB3 verbose
tx-out for tx-in is not found: 710071514c58814485bca8821131cf32b4aa4de202085b2df20d86517d9d87740d4b2d4ffcaad0a66b2eb810a599a0622f0a1178c246ea0aeb40168f43709e9a
nonce:00000000 hash hex: 1c8e98d37d85ff24a2e2d91eb4af079b0fffbe3ee25fa4a0460a0750f6c40dc2
nonce:00000001 hash hex: 5c5a529df821487e1a909ce473f6184c879f3ac7a0d2b6ea812cc4f06d9de481
nonce:00000002 hash hex: 05df7d654c74db90438e4a10e9ba7f787d5aae163e777f977ce832992ee7a115
nonce:00000003 hash hex: b124b791421707c5e119c765a54a427f79ec9a2db20939fddf6e099e44ad67ee
nonce:00000004 hash hex: 692ff1fd5d3e11b516b0a9deeecee4bd9497eb4b864d1887a06af1083170df6d
・・・
nonce:00049224 hash hex: 000078d9d21d2ac751647b552c184ad127383c3462775c3f6ca67d661606cfed
49224回目の計算でゴールデンノンスを見つけることができた。ここではブロックハッシュを16進数表記した時に先頭にゼロが4個以上並ぶノンスを探索条件としている。tx-out for tx-in is not foundの警告はコインを所持していないのに送金しようとしているため。
verboseを使用しないで2回目のマイニングを行う。下記のようにかなりシンプルな形で結果が表示された。
python3 mine.py 5PgTb3hJQBPJTmsrRRUo6UH5zb9R4s6BnH7sxzz8PpokRnvqNXuuAJmzevAPDxJTiuXBmiQNLqh2Qp14hERN39MK
nonce:00228022 hash hex: 0000cf015cd5d761095d84c2ea2ffa81304f76b18c20b63a170eb0d941a7bad6
mine.pyはブロックに格納したトランザクションを使用した際にtrans.txtから除去するため現在のtrans.txtは[]空で表示されていることが確認できた。
代わりに、block.txtという新たなファイルを作成するのでそちらをみると、
[
//ブロック0
{
//ブロックハッシュ
"hash": "000078d9d21d2ac751647b552c184ad127383c3462775c3f6ca67d661606cfed",
//ノンス
"nonce": 49224,
//直前のブロックハッシュ
"previous_hash": "",
//トランザクションハッシュ
"tx_hash": "59e0be0e8b1c542f54ca9a1b5b9979eaa1ff70b6d44ce74d881de80e0e7d3b89",
"tax": [
{
"in": "",
"out": "710071514c58814485bca8821131cf32b4aa4de202085b2df20d86517d9d87740d4b2d4ffcaad0a66b2eb810a599a0622f0a1178c246ea0aeb40168f43709e9a",
"sig": ""
}
]
},
//ブロック1
{
"hash": "0000cf015cd5d761095d84c2ea2ffa81304f76b18c20b63a170eb0d941a7bad6",
"nonce": 228022,
"previous_hash": "000078d9d21d2ac751647b552c184ad127383c3462775c3f6ca67d661606cfed",
"tx_hash": "ab1b95da32c4b25d058974a06156bcdce757d106a0224b79182f1023b8a25bac",
"tx": [
{
"in": "",
"out": "dba06b5bac498caa9ed52153d5b57e3e6f41ced09d03f13757e5f00e9da781b9e21d71505b9b4dbacf0a0163d935b53b6fb673f5cf302fa818c6958176c6579a",
"sig": ""
},
{
"in": "710071514c58814485bca8821131cf32b4aa4de202085b2df20d86517d9d87740d4b2d4ffcaad0a66b2eb810a599a0622f0a1178c246ea0aeb40168f43709e9a",
"out": "684f7b460f23c5c10761c78e6855b408958cd0723eb3f387b2651c98ed137774884a05f165b261a3484c149cba7bc42c07c6d61c936c1ece7ac30d9a74f1ba50",
"sig": "e93001c3ed87aca3f7ac0c54985420df7527bdc90df3d11a02ea0cb4653803b87674ffcad46af3a5bef64d0fae6d43ef77d83d16f650f6f616fe21595d760b7b"
}
]
}
]
2回のマイニングにより2個のブロックが作成された。
各ブロックには、
ブロックハッシュ:先頭に0が4個並んでいるもの、
直前のブロックハッシュ、
トランザクションハッシュ:トランザクション配列に含まれる全トランザクションをハッシュにしている、
ジェネレートトランザクション:トランザクション配列の1個目のトランザクションはマイナーへの報酬、
通常のトランザクション:配列の2個目以降のトランザクション
が含まれている。
一回目のマイニング報酬を2回目の支払い元に使用している。
プログラムの中身を確認していく。
まずはモジュール。
import base58
import ecdsa
import filelock
import hashlib
import json
import re //regular expression正規表現:文字列のパターンマッチを行う機能
import sys
難易度の設定部分ではブロックハッシュの先頭に必要なゼロの数を表している。
このような形でマイニングの時間調整が行われる。ゴールデンノンスの見つけにくさはこの難易度1に対して16進数一桁分である16倍の時間ずつ変化する。
DIFFICULTY = 4
ノンスの探索形か表示は非常に長くログが見にくくなるのでデフォルトでは表示させないようにしている。
VERBOSE = False
コマンド引数が2個または3個以外の場合は使い方が違うのでその表示を行なっている。引数にはsys.argv[0] public-key verboseが取られる。
if len(sys.argv) != 2 and len(sys.argv) != 3:
print('usage:', sys.argv[0], 'public-key verbose')
exit()
コマンドの引数が3個でverboseが設定されている時には経過表示を行うように変えている。
if len(sys.argv) == 3 and sys.argv[2] =='verbose':
VERBOSE = True
sys.argv[1]の引数で指定された公開鍵をBase58形式から16進数表記に変更。
public_key = base58.b58decode(sys.argv[1]).hex()
ブロックチェーンファイルblock.txtを読み込むためにblock.lockを用いてロックを事前に行い処理する。
with filelock.FileLock('block.lock', timeout=10):
block.txtを読み込めたらブロックリストを変数block_listに格納。block.txtをfileという変数で呼び、fileをjson形式に変換し変数block_listに追加する。
try:
with open('block.txt', 'r') as file:
block_list = json.load(file)
この際に直前のブロックハッシュを組み込む。block_list[-1]はblock_listの最後のブロックを指す。
previous_hash = block_list[-1]['hash']
block.txtをtryで読み込めなかったら下記のように両方を空にする。
except:
block_list = []
previous_hash = ''
次はトランザクションファイルtrans.txtを読み込む。
with filelock.FileLock('trans.lock', timeout=10):
try:
with open('trans.txt', 'r') as file:
tx_list = json.load(file)
except:
tx_list = []
トランザクションの検証を行うために既存のトランザクションのリストを作成していく。
old_in = []
old_out = []
ブロックチェーン上の全てのブロックを順に処理し、全てのトランザクションについて入力をold_inに出力をold_outに追加。
for block in block_list:
for tx in block['tx']:
old_in.append(tx['in'])
old_out.append(tx['out'])
トランザクションの検証を行い、、検証に合格したものだけがリストに残るようにする。
ファイルから読み込んだトランザクションのリストfile_tx_listと空のリストtx_listを用意。
file_tx_list = tx_list
tx_list = []
まずは読み込んだトランザクションリストから処理していく。
for tx in file_tx_list:
トランザクションの入力公開鍵inと出力公開鍵outからハッシュを計算。
sha = hashlib.sha256()
sha.update(bytes.fromhex(tx['in']))
sha.update(bytes.fromhex(tx['out']))
hash = sha.digest()
入力の公開鍵から署名を検証するための鍵を作成し、変数keyに格納。
key = ecdsa.VerifyingKey.from_string(
bytes.fromhex(tx['in']), curve=ecdsa.SECP256k1)
署名sigの検証を行う。
if not key.verify(bytes.fromhex(tx['sig']), hash):
print('invalid signature:', tx['sig'])
検証に失敗したらinvalid signatureの表示。
elif tx['in'] in old_in:
print('tx-in has already been spent:', tx['in'])
トランザクションの入力が既存のトランザクション入力に含まれる場合はtx-in has already been spentの表示。
elif tx['in'] not in old_out:
print('tx-out for tx-in is not found:', tx['in'])
トランザクションの入力が既存のトランザクションの出力に含まれない場合にはtx-out for tx-in is not foundと表示。つまり、お金が入ってきていないので支払えない。
elif tx['out'] in old_in or tx['out'] in old_out:
print('tx-out is reused:', tx['out'])
トランザクションの出力が既存のトランザクションの入力または出力に含まれる場合には、tx-out is reusedの表示。
else:
tx_list.append(tx)
old_in.append(tx['in'])
old_out.append(tx['out'])
以上のどの条件にも該当しない場合、検証は成功とみなされ、tx_listに加えられる。同時に既存の入出力も更新される。pythonのin 演算子は値がリストや文字列に含まれるときにtrueを返すboolで
値 in リスト
と記述される。
続いてマイニング用の公開鍵の検証。
if public_key in old_in or public_key in old_out:
print('public-key is reused:', public_key)
exit()
鍵が既存のトランザクション入出力に含まれている時には中止。成功したら追加。
old_out.append(public_key)
マイニング報酬を支払うためのジェネレーショントランザクションの追加に移る。tx_listの先頭に追加。
tx_list.insert(0, {
'in' : '',
'out': public_key,
'sig': ''
})
トランザクションハッシュを計算し、ブロックに含まれる全ての入出力および署名を使い、ハッシュを更新。
sha = hashlib.sha256()
for tx in tx_list:
sha.update(bytes.fromhex(tx['in']))
sha.update(bytes.fromhex(tx['out']))
sha.update(bytes.fromhex(tx['sig']))
tx_hash = sha.digest()
ここからノンスの探索に移る。ここでは1億個のノンスについてブロックハッシュを計算範囲としている。
For 変数 in range(値)
で0から値-1までを繰り返し処理することができる。
for nonce in range(100000000):
ここではビットコインとは異なり、ブロックヘッダに対してブロックハッシュを計算するのではなく、ノンス、直前のブロックハッシュ、トランザクションハッシュで簡易的なブロックハッシュを計算している。ブロック作成時の時刻は抜いている。
sha = hashlib.sha256()
sha.update(bytes(nonce))
sha.update(bytes.fromhex(previous_hash))
sha.update(tx_hash)
hash = sha.digest()
もし、経過表示をさせる場合には下記のようにnonce:{0:08d}'.format(nonce)とノンスの桁数をそろえて表示させるようにしている。08dは値を8桁にそろえて表示することを表し、8桁に満たない場合は0を追加する。
if VERBOSE:
print('nonce:{0:08d}'.format(nonce), 'hash hex:', hash.hex())
次にブロックハッシュの先頭に難易度で設定された個数のゼロが並んでいるかを調べる。reモジュールのmatch関数で、ゼロが4個並んでいることを表す0{4}に一致するかを調べ、もし一致する場合はbreakで処理を抜け出す。
if re.match(r'0{' + str(DIFFICULTY) + r'}', hash.hex()):
break
Break文はforなどの繰り返し構文と一緒に用いられ、繰り返し作業の外に出ることができるもの。
そして、見つけたゴールデンノンスを表示する。
if not VERBOSE:
print('nonce:{0:08d}'.format(nonce), 'hash hex:', hash.hex())
そのあとは、block_listに新しいブロックを追加することができるのでそちらの処理を行う。
ブロックリストにブロックハッシュ、ノンス、直前のブロックハッシュ、トランザクションハッシュ、トランザクションリストを組み込むことになる。
block_list.append({
'hash' : hash.hex(),
'nonce': nonce,
'previous_hash': previous_hash,
'tx_hash': tx_hash.hex(),
'tx' : tx_list
})
新しくブロックを追加したリストをブロックチェーンのファイルであるblock.txtに書き込む。
with filelock.FileLock('block.lock', timeout=10):
with open('block.txt', 'w') as file:
json.dump(block_list, file, indent=2)
ブロックチェーンにトランザクションを登録できたら、ブロックに登録したトランザクションをトランザクションファイルから削除する。
with filelock.FileLock('trans.lock', timeout=10):
try:
with open('trans.txt', 'r') as file:
file_tx_list = json.load(file)
except:
file_tx_list = []
ここに空のリストを代入する。
tx_list = []
ファイルから読み込んだトランザクションfile_tx_listに対しては、
再度、
入力が既存の入力に含まれない
出力が既存の入力に含まれない
出力が既存の出力に含まれない
を確認、更新し、最新版にして、tx_listに追加し直す。
for tx in file_tx_list:
if (tx['in'] not in old_in and
tx['out'] not in old_in and tx['out'] not in old_out):
tx_list.append(tx)
このtx_listをトランザクションファイルtrans.txtに書き込むことでアップデートされたブロックチェーンに組み込まれる前のトランザクションプールリストができる。
with open('trans.txt', 'w') as file:
json.dump(tx_list, file, indent=2)
次にコインの残高を確認できるwallet.pyを実行していく。
python3 wallet.pyで下記のような結果を得ることができる。
2 unspent keys(coins):
//一つ目の秘密鍵
private:
EXXZifNcoZ5Vxm6N8Z7pxeUzdT4DEkB46MR9PT9EcNK1
//一つ目の公開鍵
public : 35xdAyr3JdPqvK8UGum8HhWouEwxgyyiLgLMYeSnKdvmJhw4YeAXDj74PFDWGttgHxdATb41SPVh9eEYzhVjoKE7
//二つ目の秘密鍵
private:
6hx6m6Am7hiG7uLg2H8xEKm8yDGdcBNLccpeMVwVtCtJ
//二つ目の公開鍵
public : 5PgTb3hJQBPJTmsrRRUo6UH5zb9R4s6BnH7sxzz8PpokRnvqNXuuAJmzevAPDxJTiuXBmiQNLqh2Qp14hERN39MK
0 unused keys:
未払いの鍵をunspent keysといい、受け取りに使ったが、まだ支払いには使っていない鍵(コイン残高)を表し、未使用の鍵(unused keys)は受け取りにも支払いにも使っていない鍵を表す。送金先や報酬の支払いさきとして使える。
ここで疑問なってくるのが受け取った鍵(コイン)の一部を使いたい時にはどうなるのかということで、実は受け取った鍵の塊は分割できないので、オーバーした金額の鍵を支払い、その支払いに対するお釣りをもらうことで残高を維持する形になっている。
Wallet.pyの中身を確認していく。
//モジュールインストール
import base58
import filelock
import json
鍵ペアの一覧を鍵のファイル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 = []
次にブロックの一覧をブロックチェーンのファイルblock.txtから読み込む。読み込めなかった場合は同じくリストを空にする。
with filelock.FileLock('block.lock', timeout=10):
try:
with open('block.txt', 'r') as file:
block_list = json.load(file)
except:
block_list = []
既存のトランザクションの入出力リストを作成。
old_in = []
old_out = []
for block in block_list:
for tx in block['tx']:
old_in.append(tx['in'])
old_out.append(tx['out'])
未払いの鍵リストunspentと未使用鍵のリストunusedを作成。
unspent = []
unused = []
鍵のリストkey_listから1個ずつ鍵を取り出す。
for key in key_list:
鍵をBase58形式から16進数表記に変換。
key_hex = base58.b58decode(key['public']).hex()
key_hexが既存トランザクションに含まれていないことを確認。
if key_hex not in old_in:
既存のトランザクション出力に含まれているかも確認。
if key_hex in old_out:
含まれている場合にはunspentに追加。
一連のトランザクションの流れでは、送金されたコインが誰も消費していない未使用のトランザクションアウトプットとされ、そのアウトプットが受け取られた際にインプットとして、消費される。
トランザクションがインプットとして消費されると、また新たに未使用のトランザクションアウトプットとして出力される。なので、こちらのunspentの方が残高になる。基本的にUTXOと呼ばれtれいるもの。
unspent.append(key)
含まれていない場合は未使用鍵に追加。こちらはエラーに相当するのだろうか。最初のブロック生成時にもこちらに当たりそう。
else:
unused.append(key)
Unspentの個数と一覧を表示。
print(len(unspent), 'unspent keys(coins):')
for key in unspent:
print('private:', key['private'])
print('public :', key['public'])
改行。
print()
print(len(unused), 'unused keys:')
for key in unused:
print('private:', key['private'])
print('public :', key['public'])