『ゼロから創る暗号通貨』
第5章: すべての機能を結合し、動かしてみよう
濵津 誠/hamatz
本章では、これまでに作ってきた部品をすべて繋ぎ合わせることで、SimpleBitcoinが暗号通貨として実際に動作することを確認します。これまでのおさらいを兼ねて、サーバーとWalletの要所を確認しながらSimpleBitcoinを完成させましょう。ポイントになるのは次のような内容です。
本章はこれまでの努力が実を結ぶところです。ぜひ楽しんで読み進めてください!
「作業を始める前に、これから先のポイントを確認してみよう。これまで準備してきた部品のうち完成している部分がどこで、まだ手を加える必要がある部分はどこか、全体像から見直してみよう」
「どれくらいの作業が残ってるかを確認するってことだね」
「そんなところだ。図5.1を見てほしい」
「残るポイントは5つってこと?」
「そうだね。WalletでTransactionの生成ができるところまでは、もう完成しているので、あとは生成したTransactionをP2Pネットワークに送信する機能へ繋ぎ込む必要がある。これが1つめのポイントだ」
「その必要性は認識していた」
「次に、受け取ったTransactionをブロックチェーンに取り込んでよいかどうかをチェックする機能が必要になる。これが2つめのポイントになる」
「受け取ったTransactionを無条件に信頼するのはまずいものね。使用済みのTransactionとなっているものがUTXOとして再利用されていないことの確認とかが必要か」
「そのとおり。ユーザーの不正を排除するための機能ともいえる。それに、自分の手数料を計算する必要があるから、不正の心配がなくてもTransactionの中身のチェックは必要だ」
「そういえば、Coinbase Transactionは、本来はCoreノード側でブロックを生成するときにセットするものだったよね」
「そのとおり。まさに、その機能の作り込みが3つめのポイントになる」
「図5.1の4つめのポイントは、他のCoreノードで生成されたブロックに不正がないかを確認する話?」
「そうだね。ブロックはP2Pネットワーク上のどのCoreノードで生成されるかわからないから、飛んできたものを無条件に信頼するのはよろしくない。たとえば、Coinbase Transactionに途方もない額が上乗せされているようなブロックは却下されるべきだ」
「なるほど。トラストレスだもんね。5つめのポイントは、更新されたブロックチェーンから自分に関連したUTXOが存在しているかどうかを確認して、場合によっては残高表示を更新する部分の作り込みだよね」
「Walletはさっき実装したばかりだから、よくわかってるようだな。整理の意味で、残る5つのポイントをもう一度まとめよう」
「うん。できそうな気がしてきた」
「それでは、まずはWallet側から完成させていこう」
「1つめと5つめのポイントから取り組んでいくってことだね」
「Walletから完成させていくということなので、まずは、ここまでにできあがっているWalletの機能を整理して図5.2のような図にしてみたよ」
「いいね。では、そこにP2Pネットワークとの接続周りの機能を追加して、実際に生成したTransactionを送付できるようにするには、何が必要だろう?」
「えっと、ここまでにできあがってるEdgeノードの機能を同じように整理すると、図5.3のようになるよね」
「そうだね。ここにうまくWalletを繋ぎ込むには、どうすればいいだろう?」
「これまでの方針どおりに、ClientCore
が他の機能を抽象化してUI側から利用しやすくするというイメージで進めるとすると、図5.5のような形になるかなぁ……」
「あまり釈然としていないようだね?」
「正直なところ、今のWalletのコードをいじる必要性をあまり感じていないんだ。WalletにくっついてるKeyManager
やUTXOManager
を引き剥がして、わざわざClientCore
に移動させる必要があるのかなぁって……」
「なるほど。現実のイメージに照らし合わせて考えても、Walletという名前のアプリケーションが鍵を管理していることに不自然さはないよね」
「その鍵に結び付いているTransactionだけを管理するUTXOManager
にしても、かなり特殊な機能を提供するものだから、必要になるのはWallet側だけだと思うんだ」
「そうだね。それらの機能をClientCore
側に移動しても、それによる抽象化の恩恵は特になくて、今ある機能を間接的に呼び出すことになるだろう」
「だから、むしろ図5.6のような切り分け方がいいと思うんだよね。ダメかな?」
「これは君のプロジェクトなんだから、いいも悪いもない。君が好きなように設計して作ればいいんだよ。私見を言わせてもらえば、私でも図5.6のように作ろうとするだろうね」
「よし、じゃあその方針でやっていくよ!」
「まずはWallet_GUI
とClientCore
を接続する部分から見ていこうか」
「実現したいことは、大きく分けると次の2つだったよね」
Wallet_GUI
上の入力で生成したTransactionメッセージを、ClientCore
を経由してP2Pネットワークに送信するWallet_GUI
上で利用可能なUTXOを取り出す「そのとおり」
「1について図にすると、図5.7のような役割分担の形になると思う」
「ふむ。では、この手順に沿って、送信できるまでの処理をコードを追って見ていこうか」
「うん。まずはinitApp()
の処理の中でClientCore
を登録しておく必要があるね」
def initApp(self, my_port, c_host, c_port): """ ClientCoreとの接続含めて必要な初期化処理はここで実行する """ print('SimpleBitcoin client is now activating ...: ') self.c_core = ClientCore(my_port, c_host, c_port) self.c_core.start() <以下略>
「あとは、この登録したClientCore
を使って、Coreノードへのメッセージ送信を依頼するだけ」
# TransactionをP2P Networkに送信 tx_strings = json.dumps(new_tx) self.c_core.send_message_to_my_core_node(MSG_NEW_TRANSACTION, tx_strings) print('signed new_tx:', tx_strings)
「拍子抜けするくらい簡単だね。じゃあ、勢いに乗ってCoreノードから最新のブロックチェーンを取ってくる部分を見ていこうか」
「りょうかい」
「少しややこしいので、ブロックチェーンを取ってくるまでの段階と、取ってきたブロックチェーンから有効なUTXOを保存して残高表示を書き換えるとこまでの段階を、2つのステップに分けて図にしておいた」
「なるほど。P2Pネットワーク経由で最新のブロックチェーンを取得したことをClientCore
からWallet_GUI
に通知する手段がないと、ブロックチェーンからどのタイミングでUTXOを取り出せばいいかわからないか」
「だから、ClientCore
には、図5.9の処理をするためのCallbackを登録するような口が必要になる」
「そうかー。とりあえずClientCore
の初期化時に登録できるように口だけ用意しておくことにして、まずは図5.8のステップについてコードにしてみるよ」
def initApp(self, my_port, c_host, c_port): """ ClientCoreとの接続含めて必要な初期化処理はここで実行する """ print('SimpleBitcoin client is now activating ...: ') self.c_core = ClientCore(my_port, c_host, c_port, self.update_callback) self.c_core.start() <以下略>
「それがいいだろうね」
「せっかく抽象化層としてClientCore
があるんだから、最新のブロックチェーンの取得には、こんなふうに専用の関数を用意しちゃっていいと思うんだよね」
def send_req_full_chain_to_my_core_node(self): print('send_req_full_chain_to_my_core_node called') new_message = self.cm.get_message_text(MSG_REQUEST_FULL_CHAIN) self.cm.send_msg((self.my_central_host, self.my_central_port), new_message)
「どうしてそう考えたんだい?」
「こうしておけば、UI側のメニューで『Update Blockchain』を選んだときに呼び出される関数で、この専用の関数を呼び出すだけで済むでしょ」
def update_block_chain(self): self.c_core.send_req_full_chain_to_my_core_node()
「なるほど、考えたね」
「やった」
「ここまでで、図5.8のうち、①から④までの処理が実現できたことになる」
「⑤についても、チェーンの取り込み処理については、ブロックチェーンを勉強したときにServerCore
で一度やってるよね」
「そうだね。Coreノード用のBlockchainManager
をそのまま使えばいい。ただし、EdgeノードにはTransactionPool
がない、といった違いには注意が必要だ」
「それなら簡単にできそう」
if msg[2] == RSP_FULL_CHAIN: # ブロックチェーン送信要求に応じて返却されたブロックチェーンを検証し、 # 有効なものか検証した上で自分の持つチェーンと比較し # 優位な方を今後のブロックチェーンとして利用する new_block_chain = pickle.loads(msg[4].encode('utf8')) result, pool_4_orphan_blocks = self.bm.resolve_conflicts(new_block_chain) print('blockchain received form central', result) if result is not None: self.prev_block_hash = result print('callback called') self.callback() ・・・⑨の処理呼び出しはココ else: print('Received blockchain is useless...')
「これで、図5.8にある⑥から⑨の処理が実現できたわけだ」
「呼び出し口だけで、肝心の処理の中身ができてないけどね」
「引き続き図5.9を見ながら処理の中身を考えていこう。まず、①から④までの処理についてはどうだろう?」
「ClientCore
を通じてBlockchainManager
の機能を呼び出すだけかな。格納されている全ブロックから個別にTransactionを取り出して、リストにして返せばいいと思う」
def get_stored_transactions_from_bc(self): print('get_stored_transactions_from_bc was called!') current_index = 1 stored_transactions = [] while current_index < len(self.chain): block = self.chain[current_index] transactions = block['transactions'] for t in transactions: stored_transactions.append(json.loads(t)) current_index += 1 return stored_transactions
「返されたTransactionのリストをどうする?」
「取り出した全部のTransactionをUTXOManager
でチェックして、自分のアドレスである公開鍵に関連するものだけを抽出しないといけないよね。図5.9でいうと、⑤と⑥の部分。ただ、これは、前にWalletを作るときに書いた機能を呼び出すだけだね」
「となると、図5.8の最後で呼び出されるCallbackの中身はどうなるかな?」
「処理の手順としては、こういう流れになると思う」
「それじゃあ、その流れをコードにしてみよう」
def update_callback(self): print('update_callback was called!') s_transactions = self.c_core.get_stored_transactions_from_bc() ・・・1に対応 print(s_transactions) self.um.extract_utxos(s_transactions) ・・・ 2に対応 self.update_balance() ・・・ 3に対応
「これでいけるはず。いやー、想像してたより、全然少ないコード量で済んじゃったな」
「いつもなら、ここでCoreノードとの接続を検証してみるところなんだけど、せっかくだからCoreノードまで仕上げてから最後に結合させよう」
「ブロックチェーンを作るときに、どんなTransactionでも取り込めるようにしたんだから、ここはまぁ普通に動いて当たり前だもんね」
「というわけで、ここまでの部分を図5.10に整理しておいた。①と⑤の部分について必要な処理を確認し、実際にコードを更新したわけだ」
「残りの②、③、④が片付けば、ひとまずは完成ってことだよね。いよいよ終わりが見えてきたぞー!」
「それじゃあ、いよいよサーバーを完成させるぞ」
「確認だけど、サーバーっていうのは、Coreノードのことだよね?」
「そうだ。ここまではP2Pネットワークからの流れでCoreノードと呼んできていた。そろそろ機能も揃ってきたし、サーバーと呼ぶことにしよう。Walletと同様に、まずは今までで作ってきたサーバーの全体像から確認していこう。再確認の意味も含めて、自分で図を描いてごらん」
「りょうかい。Edgeノードから受け取ったTransactionからブロックを作って、それをサーバーで共有するところまで作ってきたから、図5.11のようになるね」
「上出来だ。今回、ここから追加したい機能はなんだっけ?」
「うーんと、大きく分けて次の2つのタイミングで必要になる機能だと思う」
「それぞれの要点を図にしてごらん」
「それぞれ、図5.12、図5.13のような感じになるんじゃないかな」
「図5.12と図5.13にあげてもらったポイントの中で、これまで作っていなかった機能が追加で必要になる部分はどこかな?参考までに、これまでの実際の機能構成から、Transaction受信時の処理とデータの流れを図5.14に示しておこう」
「まず、EdgeノードからTransactionを受信した後の処理、つまり図5.12で追加しないといけない処理は、大きくは次の3つだと思う」
「いいだろう。さらにオマケ的な話だけど、もう1つ次の機能を無駄がないように入れておくべきかな」
「なるほどー。いかにも必要になりそうだね」
「4つのポイントに合わせて、どこに機能が必要なのか、まとめてごらん」
「図5.15のような関係になるかなあ」
「では、もう少し具体的に、コードを書きながら追加されるべき処理の中身について順を追って考えていこう」
「まず、デジタル署名の検証について考えるね。RSAの鍵を使えるのはKeyManager
だけど、自分の鍵を使うわけじゃない処理でKeyManagerを使うのもおかしいと思う。新しいクラスを作った方がいいんじゃないかな。たとえばRSAUtil
みたいな」
「その場合にRSAUtil
側で持つべき処理はどんなステップを踏むことになると思う?」
「うーん……。大きく分けると2つの処理になるかな」
「そうだね。あえて追加するならば、Transactionのうち実際にInputとして使われているものを、デジタル署名の検証後の処理で使えるようについでに抜き出しておこう。そうやってInputとなるTransactionを返しておくようにすれば、後で無駄がないんじゃないかな」
「おお!なるほどたしかに!」
「じゃあ、ここまでの処理をコードに直してごらん」
「えーと……、こんな感じかな」
import Crypto from Crypto.PublicKey import RSA from Crypto.Signature import PKCS1_v1_5 from Crypto.Hash import SHA256 import copy import binascii import json class RSAUtil: def __init__(self): pass def verify_signature(self, message, signature, sender_public_key): print('verify_signature was called') hashed_message = SHA256.new(message.encode('utf8')) verifier = PKCS1_v1_5.new(sender_public_key) result = verifier.verify(hashed_message, binascii.unhexlify(signature)) print(result) return result def verify_sbc_transaction_sig(self, transaction): """ simple_bitcoin のTransactionの署名の正当性を検証する """ print('verify_sbc_transaction_sig was called') sender_pubkey_text, used_outputs = self._get_pubkey_from_sbc_transaction(transaction) signature = transaction['signature'] c_transaction = copy.deepcopy(transaction) del c_transaction['signature'] target_txt = json.dumps(c_transaction, sort_keys=True) sender_pubkey = RSA.importKey(binascii.unhexlify(sender_pubkey_text)) result = self.verify_signature(target_txt, signature, sender_pubkey) return result, used_outputs def _get_pubkey_from_sbc_transaction(self, transaction): print('_get_pubkey_from_sbc_transaction was called') input_t_list = transaction['inputs'] used_outputs = [] sender_pubkey = '' for i in input_t_list: idx = i['output_index'] tx = i['transaction']['outputs'][idx] used_outputs.append(tx) sender_pubkey = tx['recipient'] return sender_pubkey, used_outputs
「データの構造からして、必要な公開鍵を取り出す部分が、けっこうややこしいね……」
「うん。思わずブロック内のテキストデータのままでデジタル署名の検証の鍵に使おうとしてしまって、ちょっと危なかったよ」
「何も意識しないで検証のためにsignature
を除去すると、もとにしたデータ自身からもその項目が消えてしまうとか、割とPythonのハマりどころみたいな処理が満載だから気をつけないとね」
「ブロックチェーンのところで痛い目を見たばかりだったから今回は大丈夫だったけど、時間を置いたら忘れそうで怖い……」
「さて、気持ちを切り替えて、次はTransactionの正当性に関するチェックについて考えていこう」
「保存されてるブロックチェーンの中を見て確認するのだから、BlockchainManager
に確認機能を持たせるのが妥当かなって思う」
「そうだね。具体的にはどんな手順でどんな処理をすればいいだろう?」
「これも大きくは2つの観点があると思う」
「なるほど。使用済みであるかどうか、という1つめの観点はわかりやすいけど、2つめはどういう観点だろうか?」
「Walletアプリで送金Transactionを実験的に作ってるときに思い出したんだけど、CoinbaseTransaction
を誰でも無制限に作れてしまうと、暗号通貨として事実上なんの意味もなくなると思うんだよね」
「たしかにそうだね。実験では便利だけど、ブロック生成時以外にCoinbaseTransaction
を誰でも作れてしまうのは、セキュリティ的にダメすぎる。この2つの処理をそれぞれコードにできるかい?」
「それぞれこんな感じの処理に分けるのがいいと思う」
def has_this_output_in_my_chain(self, transaction_output): """ 保存されているブロックチェーン内ですでにこの TransactionOutputがInputとして使われていないか?の確認 返り値はTrueがすでに存在しているということだから、 Transactionの有効性としてNGを意味していることに注意 """ print('has_this_output_in_my_chain was called!') current_index = 1 if len(self.chain) == 1: print('only the genesis block is in my chain') return False while current_index < len(self.chain): block = self.chain[current_index] transactions = block['transactions'] for t in transactions: t = json.loads(t) if t['t_type'] == 'basic' or t['t_type'] == 'coinbase_transaction': if t['inputs'] != []: inputs_t = t['inputs'] for it in inputs_t: print(it['transaction']['outputs'][it['output_index']]) if it['transaction']['outputs'][it['output_index']] == transaction_output: print('This TransactionOutput was already used', transaction_output) return True current_index += 1 return False def is_valid_output_in_my_chain(self, transaction_output): """ チェーン内で認知されていない不正なTransactionを使ってないか確認 テスト用に気軽にCoinbaseTransaction使えなくなるので有効化させるときは注意 """ print('is_valid_output_in_my_chain was called!') current_index = 1 while current_index < len(self.chain): block = self.chain[current_index] transactions = block['transactions'] for t in transactions: t = json.loads(t) if t['t_type'] == 'basic' or t['t_type'] == 'coinbase_transaction': outputs_t = t['outputs'] for ot in outputs_t: if ot == transaction_output: return True current_index += 1 return False
「is_valid_output_in_my_chain
のほうは、テストのときは動作確認が異様に辛くなる厄介な機能になりそうだから、気をつけて有効化をコントロールしないとね」
「たしかにそうかも」
TransactionPool
の中もチェックしておこう「それじゃあ、ブロックチェーンに続いて、TransactionPool
の中の重複チェックを考えていこう」
「TransactionPool
の中にあるときは、すでに辞書型になってるから、比較の際に文字列化は不要だよね。それ以外、中の処理としては、ブロックチェーン内のTransactionとの比較とほとんど同じだと思う」
def has_this_output_in_my_tp(self, transaction_output): """ TransactionPool内ですでにこのTransactionOutputが Inputとして使われていないか?の確認 """ print('has_this_output_in_my_tp is called') transactions = self.transactions for t in transactions: inputs_t = t['inputs'] for it in inputs_t: if it == transaction_output: return True return False
「そうだね。ここにテクニカルな悩みどころはないだろう」
「ブロックチェーンを探る処理には時間がかかる。だから、そもそも暗号通貨用途以外のTransactionが混じっていたら、その前の時点で検証から除外したほうがいい」
「うん。なくてもいい機能だけど、便利なので、このタイミングでつけておくことにするんだよね」
「そうだ。Transaction群の中に暗号通貨以外のものが混じってくるような状況が将来的に出てくる可能性もあるからな」
「だとしたら、この機能はUTXOManager
に持たせておこうかな」
def is_sbc_transaction(self, tx): """ 暗号通貨用のTransactionかそれ以外かを判定する。 便利なので種別も一緒に返す """ print(tx['t_type']) tx_t = tx['t_type'] t_basic = 'basic' t_coinbase = 'coinbase_transaction' unknown = 'unknown' if tx_t != t_basic: if tx_t != t_coinbase: return False, unknown else: return True, t_coinbase else: return True, t_basic
「便利そうだから、CoinbaseTransaction
か、それとも通常のTransaction
か、その判定もつけといたよ」
ServerCore
から呼び出すために「ここまでは機能単位で作り込んできたわけだが、そろそろ実際に利用できる形で組み込んでいこう」
「Transaction自身は、Callbackを通じてConnectionManager
からServerCore
に届くようになってるから、そこからデータの有効性チェックをまとめて呼び出せるようになるのが一番効率が良さそうかな」
「ふむ。つまり、ServerCore
の__handle_message()
の中だね」
「そう。こんな感じ」
if msg[2] == MSG_NEW_TRANSACTION: new_transaction = json.loads(msg[4]) print('received new_transaction', new_transaction) is_sbc_t, _ = self.um.is_sbc_transaction(new_transaction) current_transactions = self.tp.get_stored_transactions() if new_transaction in current_transactions: print('this is already pooled transaction: ', new_transaction) return if not is_sbc_t: print('this is not SimpleBitcoin transaction: ', new_transaction) else: # テスト用に最初のブロックだけ # 未知のCoinbaseTransactionを許すための暫定処置 if self.bm.get_my_chain_length() != 1: checked = self._check_availability_of_transaction( new_transaction) if not checked: print('Transaction Verification Error') return self.tp.set_new_transaction(new_transaction) if not is_core: new_message = self.cm.get_message_text(MSG_NEW_TRANSACTION, json.dumps(new_transaction)) self.cm.send_msg_to_all_peer(new_message) else: if not is_sbc_t: print('this is not SimpleBitcoin transaction: ', new_transaction) else: # テスト用に最初のブロックだけ # 未知のCoinbaseTransactionを許すための暫定処置 if self.bm.get_my_chain_length() != 1: checked = self._check_availability_of_transaction( new_transaction) if not checked: print('Transaction Verification Error') return self.tp.set_new_transaction(new_transaction) if not is_core: new_message = self.cm.get_message_text(MSG_NEW_TRANSACTION, json.dumps(new_transaction)) self.cm.send_msg_to_all_peer(new_message)
「呼び出してる_check_availability_of_transaction
はどうする?」
「_check_availability_of_transaction
自体は、こんなふうに書けばいいかなって」
def _check_availability_of_transaction(self, transaction): """ Transactionに含まれているTransactionInputの 有効性(二重使用)を検証する """ v_result, used_outputs = self.rsa_util.verify_sbc_transaction_sig( transaction) if v_result is not True: print('signature verification error on new transaction') return False for used_o in used_outputs: print('used_o', used_o) bm_v_result = self.bm.has_this_output_in_my_chain(used_o) tp_v_result = self.tp.has_this_output_in_my_tp(used_o) bm_v_result2 = self.bm.is_valid_output_in_my_chain(used_o) if bm_v_result: print('This TransactionOutput is already used', used_o) return False if tp_v_result: print('This TransactionOutput is already stored in the TransactionPool', used_o) return False if bm_v_result2 is not True: print('This TransactionOutput is unknown', used_o) return False return True
「ふむ。良さそうだね。次はCoinbaseTransaction
の生成について見ていこうか」
「おー」
「CoinbaseTransaction
が追加されるのは、ブロックの生成タイミングだけなので、処理を入れ込むタイミングはすでに自明と言っていいだろう」
「そうだね」
「あえて気をつけるところといえば、TransactionPool
内にあるすべてのTransactionをチェックして、CoinbaseTransaction
に追加で含められる手数料を計算する処理が必要ってことくらいかな」
「その機能はどこにつけるべき?」
「TransactionPool
内のTransactionが対象なんだから、TransactionPool
が持てばいいんじゃないかな」
「それじゃあ、こんな感じで。サポート外のTransactionははじくようにしておこう」
def get_total_fee_from_tp(self): """ TransactionPool内に格納されているTransactionすべての手数料の合計値を算出する """ print('get_total_fee_from_tp is called') transactions = self.transactions result = 0 for t in transactions: checked = self.check_type_of_transaction(t) if checked: total_in = sum( i['transaction']['outputs'][i['output_index']]['value'] for i in t['inputs']) total_out = sum(o['value'] for o in t['outputs']) delta = total_in - total_out result += delta return result def check_type_of_transaction(self, transaction): if transaction['t_type'] == 'basic' or transaction['t_type'] == 'coinbase_transaction': return True else: return False
「いいだろう。あとは、これと組み合わせて、ブロック生成の処理を改良するだけだね」
「全体の流れとしては図5.16のようになると思う」
「つまり?」
「手順としては、こんな感じ。4と5の処理は図5.17にしておいたよ」
「うん、いいと思う。それで、どんなコードになるだろう?」
「コードに直すとこんな感じかな」
def __generate_block_with_tp(self): print('Thread for generate_block_with_tp started!') while not self.flag_stop_block_build: self.is_bb_running = True prev_hash = copy.copy(self.prev_block_hash) result = self.tp.get_stored_transactions() if len(result) == 0: print('Transaction Pool is empty ...') break new_tp = self.bm.remove_useless_transaction(result) self.tp.renew_my_transactions(new_tp) if len(new_tp) == 0: break # リワードとしてリストの先頭に自分宛のCoinbaseTransactionを追加する total_fee = self.tp.get_total_fee_from_tp() # TODO: インセンティブの値をここに直書きするのはイケてないので後で対処する total_fee += 30 my_coinbase_t = CoinbaseTransaction(self.km.my_address(), total_fee) transactions_4_block = copy.deepcopy(new_tp) transactions_4_block.insert(0, my_coinbase_t.to_dict()) new_block = self.bb.generate_new_block(transactions_4_block, prev_hash) # タイミングがどうしてもクロスするので念のため保存前に再度確認 if new_block.to_dict()['previous_block'] == self.prev_block_hash: self.bm.set_new_block(new_block.to_dict()) self.prev_block_hash = self.bm.get_hash(new_block.to_dict()) msg_new_block = self.cm.get_message_text(MSG_NEW_BLOCK, json.dumps(new_block.to_dict())) self.cm.send_msg_to_all_peer(msg_new_block) # ブロック生成に成功したらTransaction Poolはクリアする index = len(new_tp) self.tp.clear_my_transactions(index) break else: print('Bad block. It seems someone already win the PoW.') break print('Current Blockchain is ... ', self.bm.chain) print('Current prev_block_hash is ... ', self.prev_block_hash) self.flag_stop_block_build = False self.is_bb_running = False self.bb_timer = threading.Timer(CHECK_INTERVAL, self.__generate_block_with_tp) self.bb_timer.start()
「せっかくCoinbaseTransaction
を作り出すのだから、ここで作ったSimpleBitcoinを自分のWalletから使えるようにしておくといいんじゃないかな?」
「たしかに。Coreノードとしてサーバーを起動するときにパスフレーズを渡しておくと、Walletで読み込み可能なPEM形式のファイルを書き出す、というくらいの処理なら、割と少ない変更で書けそう」
def __init__(self, privatekey_text = None, pass_phrase= None): print('Initializing KeyManager...') if privatekey_text: self.import_key_pair(privatekey_text, pass_phrase) else: random_gen = Crypto.Random.new().read self._private_key = RSA.generate(2048, random_gen) self._public_key = self._private_key.publickey() self._signer = PKCS1_v1_5.new(self._private_key) if pass_phrase is not None: my_pem = self.export_key_pair(pass_phrase) my_pem_hex = binascii.hexlify(my_pem).decode('ascii') # とりあえずファイル名は固定 path = 'my_server_key_pair.pem' f1 = open(path,'a') f1.write(my_pem_hex) f1.close()
「いいね。ServerCore
の初期化プロセスでKeyManager
を設定するところもパラメーターが増えているから、気をつけておこう」
「大丈夫。このくらいならなんとか!起動時にファイルを作るだけじゃなく、後で自分のWalletの鍵を使ってサーバーを運用したいみたいな話までカバーしようとすると、ちょっと面倒そうだけど……」
「よし。それでは次にいこう」
「次は、Transactionじゃなくてブロックの正当性確認か」
「ということで、受信したブロックの正当性に関するチェックについて考えていこう」
「基本的には、受信したTransactionを検証するときに確認すべき項目と同じことを、ブロックから取り出した各Transactionに対して実行できればいいと思うんだよね」
「基本的には、そのとおりだ。しかし、完全に同じではない。では、追加で気をつける必要があるのは、どんな点だろう?」
「CoinbaseTransaction
に対する注意だと思う」
「そうだね。CoinbaseTransaction
は、Transactionのチェック時には存在していなかった。だから、そこが重要なポイントになる。まずはそこから見ていこうか」
「うん」
「それに、ブロックの生成時は、一番悪いことができるタイミングでもある。どのような悪いことができそうかな?」
「パッと思いつくのは、この辺かな……」
「うん、そんなところだろう。悪用1と2は、ブロックにおける検証に特有の話だ。一方、悪用3と4は、Transactionの検証にも共通する話だ」
「そうかー」
「じゃあ、過剰な手数料やインセンティブが載っていないかのチェックから見てみよう。どんな処理になると思う?」
「処理の流れはこんな感じになると思う」
「良さそうだね。2のインセンティブは、どうやって決める?」
「うーん。とりあえず、SimpleBitcoinでは、固定ということでいんじゃないかな。30くらいで」
「いいだろう。では、1からコードにしていこうか。手数料を計算する処理はどこで担当するのがいいだろう?」
「ブロックのTransaction群はCoinbaseTransaction
も含んでるから、それをキレイに除外しようとすると、BlockchainManager
を使うよりもServerCore
でやるほうがいいかな。こんな感じで」
def get_total_fee_on_block(self, block): """ ブロックに格納されているbasicなTransactionすべての手数料の合計値を算出する """ print('get_total_fee_on_block is called') transactions = block['transactions'] result = 0 for t in transactions: t = json.loads(t) is_sbc_t, t_type = self.um.is_sbc_transaction(t) if t_type == 'basic': total_in = sum( i['transaction']['outputs'][i['output_index']]['value'] for i in t['inputs']) total_out = sum(o['value'] for o in t['outputs']) delta = total_in - total_out result += delta return result
「インセンティブの値は固定ということにしたから、2つめの処理はただの足し算だね。3つめはどういうふうに作ろうか?」
「ブロックの中のTransaction群からCoinbaseTransaction
を取り出して、そこでvalue
にセットされている値を取り出して比較するのが素直だと思う」
def check_transactions_in_new_block(self, block): """ ブロック内のTranactionに不正がないか確認する """ fee_for_block = self.get_total_fee_on_block(block) fee_for_block += 30 print("fee_for_block: ", fee_for_block) transactions = block['transactions'] for t in transactions: t = json.loads(t) # basic, coinbase_transaction以外はスルーチェック is_sbc_t, t_type = self.um.is_sbc_transaction(t) if is_sbc_t: if t_type == 'basic': if self._check_availability_of_transaction_in_block(t) is not True: print('Bad Block. Having invalid Transaction') return False elif t_type == 'coinbase_transaction': insentive = t['outputs'][0]['value'] print('insentive', insentive) if insentive != fee_for_block: print('Invalid value in fee for CoinbaseTransaction', insentive) return False print('ok. this block is acceptable.') return True
「いいね。ではここに、悪用ポイントの2つめ、つまり『2つ以上のCoinbaseTransaction
がセット』されていないかのチェックも加えてみようか?」
「ループの最初以外のCoinbaseTransaction
は認めないって形にすればいいだけだから、これは簡単かな」
def check_transactions_in_new_block(self, block): <中略> counter = 0 for t in transactions: t = json.loads(t) # basic, coinbase_transaction以外はスルーチェック is_sbc_t, t_type = self.um.is_sbc_transaction(t) if is_sbc_t: if t_type == 'basic': <中略> elif t_type == 'coinbase_transaction': if counter != 0: print('Coinbase Transaction is only for BlockBuilder') return False else: insentive = t['outputs'][0]['value'] <以下略>
「うん。いいだろう。それじゃあ、通常のTransactionに対するチェック処理、つまり悪用ポイントの3と4への対処について考えていこうか」
「さっき書いたコードではTODOにしていたとこだね」
「ここはTransactionの検証処理とほぼ同じだ。ただし、唯一の違いとして、TransactionPool
の中をチェックする必要はない」
「ってことは、こんな感じでいいかな。共通な処理が多いから、本当はもう少しキレイに書けるのかもしれないけど……」
def _check_availability_of_transaction_in_block(self, transaction): """ Transactionの有効性を検証する(Block用) """ v_result, used_outputs = self.km.verify_sbc_transaction_sig(transaction) if v_result is not True: print('signature verification error on new transaction') return False for used_o in used_outputs: print('used_o: ',used_o) bm_v_result = self.bm.has_this_output_in_my_chain(used_o) bm_v_result2 = self.bm.is_valid_output_in_my_chain(used_o) if bm_v_result2 is not True: print('This TransactionOutput is unknown', used_o) return False if bm_v_result: print('This TransactionOutput is already used', used_o) return False return True
「なるほど。これをcheck_transactions_in_new_block
の中で呼び出して、結果の真偽を確認しようってわけだね」
「えーと、完成……ってことでいいかな、兄さん?」
「そうだね。ここまでのまとめをしてみよう。図5.18のとおり、②、③、④の部分について必要な処理を確認し、実際にコードを更新した」
「うん。次はいよいよ、Walletとサーバーを接続しての最終確認だ!」