目次
ブロックチェーンをPythonで実装するサンプル/三回目
ブロックチェーンのお勉強がてら、pythonで実装してみる三回目です。
今回は二回目で改善したソースをベースにします。
ここに「マイニング」と呼ばれる部分を追加します。
マイニング機能を追加する
ブロックの完成とみなしてもらえる条件を満たすまで、ナンスを変えながら、ひたすら計算し続ける行為を「マイニング」と呼びます。
マイニングでやっていること、例えば、ビットコインなどだと、こんな感じです。
- ブロックのデータでハッシュ値をもとめる。
- それに自分で考えたデータ(ナンス)を足してハッシュ値を求める。
- その結果がブロックの完成とみなす条件を満たしているかどうか検査する。
- 満たしていればOK、満たしていなければナンスを変えて再度試みる。
なぜ、こんなことをするかというと。
ブロックチェーンのような暗号通貨だと、この計算の答えを見つけてブロックを追加することで報酬がもらえる仕組みだからです。
この「マイニング」の難易度設定を
- 難しすぎるとブロックの追加がいつまでもできない。
- 簡単すぎると成功者が乱立して、同時に成功するブロックが複数できたりする。
この間のちょうど良いところ・・例えば、ビットコインだと10分程度の時間で解ける程度の難易度・・に調整できるかが暗号通貨などのひとつの肝となります。
実装するマイニングの仕様
前二回で実装した「MyBlockChain」は、別に暗号通貨っぽいことをしたいわけではないので、ブロックの構築に難易度設定しても意味がありません。
でもまあ。
マイニングの雰囲気だけでもやってみて、どの程度の難易度設定で、どのくらい時間がかかるのか?・・を確かめてみようと思います。
あまり、難しくすると大変なので、以下のような仕様にします。
- ナンスは00001 からFFFFF(10進数で1,048,575)まで
- トランザクションハッシュにナンスを足して先頭4文字が「0fea」になったら成功
マイニングのサンプル実装
ハッシュの計算とトランザクションデータ作成は、前回までの「MyBlockChain」のメソッドを使って、上記の仕様をまんま実装してみます。
とりあえず、見つかったらprintでメッセージを表示するだけのソースはこちら。
# マイニング作業をやってみる def calc_proof(self, inp, outp): # トランザクションを生成する new_transaction = self.__create_new_transaction(inp, outp) root_hash = self.__calc_tran_hash(new_transaction) for i in range(1, 1048575): hex_str = format(i, 'x') result_a = self.__calc_tran_hash(root_hash + hex_str) result = result_a.lower() if result[0:4] == "0fea": print(hex_str, "でブロック完成条件を満たしました。") return True print("最後まで一致しませんでした。_| ̄|○") return False
この程度なら、自分のノートPCでも数秒で終わります。
以下のようにして、ざっくり実行してみたら。
bc = MyBlockChain()
inp = {
'xxxxxxxxxxxxxx': 123456
}
outp = {
'yyyyyyyyyyy': 7890918
}
bc.calc_proof(inp, outp)
こんな感じの結果です。
1282a でブロック完成条件を満たしました。
でも。
上記は4文字だけの条件なので、すぐ終わりますが、条件を5文字にすると104万回のループでは「最後まで一致しませんでした。_| ̄|○」が頻発します。
短くも長くもない狙った時間で 正解を見つけられるような難易度の設定というのは、実に難しいということが、やってみるとわかります。
マイニングを「MyBlockChain」クラスに組み込む
せっかく作ったので、MyBlockChainに組み込んでみます。
ファイルから読み込んだ時は行わず、テストデータを生成するときにだけ、ちょっとしたマイニング行為の負荷をかける・・と、まあ、そういう仕様で。
実装ポイント
add_new_block()の中で改良します。
ファイルから読み込まないでブロックを追加するときに、マイニングのロジックが動くようにします。
# テストデータを作るためのとりあえず処理 if check_hash == self.testkey: self.r_mode = False check_ok = self.__calc_proof(inp, outp) else: # JSONファイルから読み込んだブロックの検査 if check_hash == tran_hash: check_ok = True
その結果をブロックヘッダに記録します。
正解が出た時の16進文字列を、self.current_nonceにセットして、ブロックヘッダに以下のようにセットするわけです。
'prev_hash': prev_hash,
'tran_hash': tran_hash,
'nonce': self.current_nonce
あと、細かいところですが。
def __calc_proof(self, inp, outp) を少しだけ汎用的にします。
__init__()の中の「self.match_str = "0fea"」の長さで、ループ回数や難易度を調整できるようにします。
ループの最大回数を「 max_loop = 1048575 * magnification」で求めるようにしているのは保険です。
別に無限ループでマッチしたらブレイクでもよかったのですが、個人的に無限ループは嫌いなので、あえて一定の回数を越えたら失敗するようにしてるだけです。
マイニングを追加した三回目のソース全文
import datetime as dt import json import hashlib class MyBlockChain(object): # ブロックチェーンを初期化する def __init__(self, loadfile="chain00.json"): self.chain = [] # マイニング時の難易度 self.match_str = "0fea" # ナンス(マイニングで正解を得た時の値) self.current_nonce = "****" # テストデータ作成用キー(固定) self.testkey = "a6cfb4c140cf8cf2b281d5538961cdcfaca30a464befc02bf5af3bc7397cd4d7" # 最初のブロックだけは固定で作る self.add_new_block(0, {'747bc42088cf0b3915982af289189e8f': 0}, { '14d3325a7d594bc2d30a7014a536cb13': 0}, self.testkey) # 読み込みモード self.r_mode = True # JSONファイルからブロックデータを読み込んで、チェインに追加する try: with open(loadfile, "r") as fr: loadblocks = json.load(fr) for bck in loadblocks: isOK = self.add_new_block( bck['id'], bck['input'], bck['output'], bck['hash']) if not isOK: print("ブロック構築を異常終了します。") break except BaseException: pass # 新しいブロックを作成する def add_new_block(self, id, inp, outp, check_hash): # デフォルトに一旦戻す self.r_mode = True self.current_nonce = "****" # トランザクションを生成する new_transaction = self.__create_new_transaction(inp, outp) # 前のブロックのハッシュを取得。最初だけ固定値 if len(self.chain) > 0: prev_hash = self.chain[-1]['block_header']['tran_hash'] else: prev_hash = "747bc42088cf0b3915982af289189e8f14d3325a7d594bc2d30a7014a536cb13" check_ok = False # このブロックのハッシュ値を計算する tran_hash = self.__hash( prev_hash + self.__calc_tran_hash(new_transaction)) # テストデータを作るためのとりあえず処理 if check_hash == self.testkey: self.r_mode = False check_ok = self.__calc_proof(inp, outp) else: # JSONファイルから読み込んだブロックの検査 if check_hash == tran_hash: check_ok = True if check_ok: # トランザクションを元にブロックを生成して、チェーンに接続する new_block = { 'block_index': len(self.chain) + 1, 'block_time': dt.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), 'block_header': { 'prev_hash': prev_hash, 'tran_hash': tran_hash, 'nonce': self.current_nonce }, 'tran_counter': len(inp) + len(outp), 'tran_body': new_transaction, } self.chain.append(new_block) else: if self.r_mode: print("要注意「", id, "」のトランザクションで改ざんの疑いがあります。!") else: print("マイニングに失敗しました。") return False return True # 新しいトランザクションを生成する def __create_new_transaction(self, inp, outp): new_transaction = { 'input': inp, 'output': outp, } return new_transaction # ハッシュ値を計算する。SortをTrueにしているのはハッシュの整合性維持のため def __calc_tran_hash(self, new_transaction): tran_string = json.dumps(new_transaction, sort_keys=True).encode() return self.__hash(tran_string) # ハッシュ計算内部関数 def __hash(self, str_seed): return hashlib.sha256(str(str_seed).encode()).hexdigest() # JSONファイルにブロックチェインの主要要素のみを保存する def save(self, outfile="chain00.json"): save_arr = [] for i, x in enumerate(self.chain): if i > 0: save_dic = {} save_dic['id'] = x['block_index'] save_dic['hash'] = x['block_header']['tran_hash'] save_dic['input'] = x['tran_body']['input'] save_dic['output'] = x['tran_body']['output'] save_arr.append(save_dic) fw = open(outfile, 'w') json.dump(save_arr, fw, indent=2) # ブロックの内容を表示する def dump(self, block_index=0): if(block_index == 0): print(json.dumps(self.chain, sort_keys=False, indent=2)) else: print( json.dumps( self.chain[block_index], sort_keys=False, indent=2)) # テスト用のブロックを強制的に作るためのテストキーを返す def get_testkey(self): return str(self.testkey) # マイニング作業をやってみる def __calc_proof(self, inp, outp): # トランザクションを生成する new_transaction = self.__create_new_transaction(inp, outp) root_hash = self.__calc_tran_hash(new_transaction) m_str = self.match_str.lower() l_int = len(m_str) if l_int <= 4: magnification = 1 else: magnification = 10 ** (l_int - 4) max_loop = 1048575 * magnification for i in range(1, max_loop): hex_str = format(i, 'x') result_a = self.__calc_tran_hash(root_hash + hex_str) result = result_a.lower() if result[0:l_int] == m_str: self.current_nonce = hex_str return True return False
マイニングを組み込んだクラスの実行結果
実行するソースは以下です。
bc = MyBlockChain() inp = { 'xxxxxxxxxxxxxx': 123456 } outp = { 'yyyyyyyyyyy': 7890918 } bc.add_new_block(0, inp, outp, bc.get_testkey()) bc.dump()
前回のMyBlockChainの引き続きで、最初のブロック以降はJSONファイルから読み込んで、最後に上記の1ブロックを追加する感じです。
結果は以下。
[ { "block_index": 1, "block_time": "2020-02-13 22:55:55", "block_header": { "prev_hash": "747bc42088cf0b3915982af289189e8f14d3325a7d594bc2d30a7014a536cb13", "tran_hash": "ec3400824e188b13a5ca7821d180fe4084e120fd02a6d0e14e6848ae070b29c8", "nonce": "4f9b" }, "tran_counter": 2, "tran_body": { "input": { "747bc42088cf0b3915982af289189e8f": 0 }, "output": { "14d3325a7d594bc2d30a7014a536cb13": 0 } } }, { "block_index": 2, "block_time": "2020-02-13 22:55:55", "block_header": { "prev_hash": "ec3400824e188b13a5ca7821d180fe4084e120fd02a6d0e14e6848ae070b29c8", "tran_hash": "f31daf4fa73faa5d890cbc0d8ad285ec6dcddf64c0236fc726124e922552126b", "nonce": "****" }, "tran_counter": 3, "tran_body": { "input": { "Y3XNSHTT": 12345 }, "output": { "X2VBASDD": 10000, "Z45UHRKL": 2345 } } }, { "block_index": 3, "block_time": "2020-02-13 22:55:55", "block_header": { "prev_hash": "f31daf4fa73faa5d890cbc0d8ad285ec6dcddf64c0236fc726124e922552126b", "tran_hash": "7eca4ab3c0dd2b825d7c7568f324f3f7a7b4d883e6c9e85d5028bfcb825b470c", "nonce": "****" }, "tran_counter": 3, "tran_body": { "input": { "X2VBASDD": 22345 }, "output": { "Y3XNSHTT": 10000, "Z45UHRKL": 12345 } } }, { "block_index": 4, "block_time": "2020-02-13 22:55:55", "block_header": { "prev_hash": "7eca4ab3c0dd2b825d7c7568f324f3f7a7b4d883e6c9e85d5028bfcb825b470c", "tran_hash": "e582060c816f5930fc9874f7a8f18eeb25ccf053d40a84d2a2bb088c64cc9f1a", "nonce": "****" }, "tran_counter": 3, "tran_body": { "input": { "Z45UHRKL": 23450 }, "output": { "Y3XNSHTT": 13000, "X2VBASDD": 10450 } } }, { "block_index": 5, "block_time": "2020-02-13 22:55:55", "block_header": { "prev_hash": "e582060c816f5930fc9874f7a8f18eeb25ccf053d40a84d2a2bb088c64cc9f1a", "tran_hash": "7add9e141b25000ac8f48af5b4fd9f2917faa1cc539305ae942206d89bafe7e1", "nonce": "1282a" }, "tran_counter": 2, "tran_body": { "input": { "xxxxxxxxxxxxxx": 123456 }, "output": { "yyyyyyyyyyy": 7890918 } } } ]
ファイルから読み込んだブロックは「nonce:"****"」、計算して追加したブロックは「 "nonce": "1282a"」となってます。
OKそうですね。
とりあえず、3回にわたってやってみて、感触だけはわかった気がします。
第一回目と第二回目記事のリンク
ではでは。