『ゼロから創る暗号通貨』
第5章: すべての機能を結合し、動かしてみよう
濵津 誠/hamatz
本章では、これまでに作ってきた部品をすべて繋ぎ合わせることで、SimpleBitcoinが暗号通貨として実際に動作することを確認します。これまでのおさらいを兼ねて、サーバーとWalletの要所を確認しながらSimpleBitcoinを完成させましょう。ポイントになるのは次のような内容です。
本章はこれまでの努力が実を結ぶところです。ぜひ楽しんで読み進めてください!
![[2pt_line]](https://peaks-img.s3-ap-northeast-1.amazonaws.com/books/cryptocurrency/2pt_line.png)
「作業を始める前に、これから先のポイントを確認してみよう。これまで準備してきた部品のうち完成している部分がどこで、まだ手を加える必要がある部分はどこか、全体像から見直してみよう」
「どれくらいの作業が残ってるかを確認するってことだね」
「そんなところだ。図5.1を見てほしい」
図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つのポイントをもう一度まとめよう」
図: これからの残課題における5つのポイント
「うん。できそうな気がしてきた」
「それでは、まずはWallet側から完成させていこう」
「1つめと5つめのポイントから取り組んでいくってことだね」
「Walletから完成させていくということなので、まずは、ここまでにできあがっているWalletの機能を整理して図5.2のような図にしてみたよ」
図5.2: これまで完成しているWallet
「いいね。では、そこにP2Pネットワークとの接続周りの機能を追加して、実際に生成したTransactionを送付できるようにするには、何が必要だろう?」
「えっと、ここまでにできあがってるEdgeノードの機能を同じように整理すると、図5.3のようになるよね」
図5.3: これまで完成しているEdgeノードのイメージ
「そうだね。ここにうまくWalletを繋ぎ込むには、どうすればいいだろう?」
図5.4: どうやって融合させよう?
「これまでの方針どおりに、ClientCoreが他の機能を抽象化してUI側から利用しやすくするというイメージで進めるとすると、図5.5のような形になるかなぁ……」
「あまり釈然としていないようだね?」
図5.5: 最終的なWalletの姿?
「正直なところ、今のWalletのコードをいじる必要性をあまり感じていないんだ。WalletにくっついてるKeyManagerやUTXOManagerを引き剥がして、わざわざClientCoreに移動させる必要があるのかなぁって……」
「なるほど。現実のイメージに照らし合わせて考えても、Walletという名前のアプリケーションが鍵を管理していることに不自然さはないよね」
「その鍵に結び付いているTransactionだけを管理するUTXOManagerにしても、かなり特殊な機能を提供するものだから、必要になるのはWallet側だけだと思うんだ」
「そうだね。それらの機能をClientCore側に移動しても、それによる抽象化の恩恵は特になくて、今ある機能を間接的に呼び出すことになるだろう」
「だから、むしろ図5.6のような切り分け方がいいと思うんだよね。ダメかな?」
図5.6: Walletに特化した部分はClientCoreと切り離す
「これは君のプロジェクトなんだから、いいも悪いもない。君が好きなように設計して作ればいいんだよ。私見を言わせてもらえば、私でも図5.6のように作ろうとするだろうね」
「よし、じゃあその方針でやっていくよ!」
「まずはWallet_GUIとClientCoreを接続する部分から見ていこうか」
「実現したいことは、大きく分けると次の2つだったよね」
Wallet_GUI上の入力で生成したTransactionメッセージを、ClientCoreを経由してP2Pネットワークに送信するWallet_GUI上で利用可能なUTXOを取り出す「そのとおり」
「1について図にすると、図5.7のような役割分担の形になると思う」
図5.7: Walletで生成されたTransactionが送信されるまで
「ふむ。では、この手順に沿って、送信できるまでの処理をコードを追って見ていこうか」
「うん。まずはinitApp()の処理の中でClientCoreを登録しておく必要があるね」
Wallet_App.py initApp内に追加する処理
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ノードへのメッセージ送信を依頼するだけ」
Wallet_App.py sendCoins内に追加する処理
# 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つのステップに分けて図にしておいた」
図5.8: Step1: 最新ブロックチェーンの取得
図5.9: Step2: ブロックチェーンからのUTXOの取り出し
「なるほど。P2Pネットワーク経由で最新のブロックチェーンを取得したことをClientCoreからWallet_GUIに通知する手段がないと、ブロックチェーンからどのタイミングでUTXOを取り出せばいいかわからないか」
「だから、ClientCoreには、図5.9の処理をするためのCallbackを登録するような口が必要になる」
「そうかー。とりあえずClientCoreの初期化時に登録できるように口だけ用意しておくことにして、まずは図5.8のステップについてコードにしてみるよ」
Wallet_App.py initApp内にCallbackを登録できるよう口だけ開けておく
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があるんだから、最新のブロックチェーンの取得には、こんなふうに専用の関数を用意しちゃっていいと思うんだよね」
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』を選んだときに呼び出される関数で、この専用の関数を呼び出すだけで済むでしょ」
Wallet_App内に追加する処理
def update_block_chain(self):
self.c_core.send_req_full_chain_to_my_core_node()
「なるほど、考えたね」
「やった」
「ここまでで、図5.8のうち、①から④までの処理が実現できたことになる」
「⑤についても、チェーンの取り込み処理については、ブロックチェーンを勉強したときにServerCoreで一度やってるよね」
「そうだね。Coreノード用のBlockchainManagerをそのまま使えばいい。ただし、EdgeノードにはTransactionPoolがない、といった違いには注意が必要だ」
「それなら簡単にできそう」
ClientCoreの__handle_message()に追加する処理
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を取り出して、リストにして返せばいいと思う」
BlockchainManagerに追加する処理
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の中身はどうなるかな?」
「処理の手順としては、こういう流れになると思う」
図: Callbackの中で実行される処理の手順
「それじゃあ、その流れをコードにしてみよう」
Wallet_App.pyに追加する処理
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に整理しておいた。①と⑤の部分について必要な処理を確認し、実際にコードを更新したわけだ」
図5.10: 現在の進捗状況
「残りの②、③、④が片付けば、ひとまずは完成ってことだよね。いよいよ終わりが見えてきたぞー!」
「それじゃあ、いよいよサーバーを完成させるぞ」
「確認だけど、サーバーっていうのは、Coreノードのことだよね?」
「そうだ。ここまではP2Pネットワークからの流れでCoreノードと呼んできていた。そろそろ機能も揃ってきたし、サーバーと呼ぶことにしよう。Walletと同様に、まずは今までで作ってきたサーバーの全体像から確認していこう。再確認の意味も含めて、自分で図を描いてごらん」
「りょうかい。Edgeノードから受け取ったTransactionからブロックを作って、それをサーバーで共有するところまで作ってきたから、図5.11のようになるね」
図5.11: ここまでのサーバー全体像
「上出来だ。今回、ここから追加したい機能はなんだっけ?」
「うーんと、大きく分けて次の2つのタイミングで必要になる機能だと思う」
図: 追加する機能
「それぞれの要点を図にしてごらん」
「それぞれ、図5.12、図5.13のような感じになるんじゃないかな」
図5.12: Wallet側からTransactionを受信した後の処理
図5.13: 他のサーバーから新しいブロックを受信した場合の処理
「図5.12と図5.13にあげてもらったポイントの中で、これまで作っていなかった機能が追加で必要になる部分はどこかな?参考までに、これまでの実際の機能構成から、Transaction受信時の処理とデータの流れを図5.14に示しておこう」
図5.14: これまでのTransaction受信時の処理の流れ
「まず、EdgeノードからTransactionを受信した後の処理、つまり図5.12で追加しないといけない処理は、大きくは次の3つだと思う」
図: EdgeノードからのTransactionを受信時に行うべき処理
「いいだろう。さらにオマケ的な話だけど、もう1つ次の機能を無駄がないように入れておくべきかな」
図: EdgeノードからのTransaction受信時に行うべき処理への追加
「なるほどー。いかにも必要になりそうだね」
「4つのポイントに合わせて、どこに機能が必要なのか、まとめてごらん」
「図5.15のような関係になるかなあ」
図5.15: 新しく処理が必要になる箇所
「では、もう少し具体的に、コードを書きながら追加されるべき処理の中身について順を追って考えていこう」
「まず、デジタル署名の検証について考えるね。RSAの鍵を使えるのはKeyManagerだけど、自分の鍵を使うわけじゃない処理でKeyManagerを使うのもおかしいと思う。新しいクラスを作った方がいいんじゃないかな。たとえばRSAUtilみたいな」
「その場合にRSAUtil側で持つべき処理はどんなステップを踏むことになると思う?」
「うーん……。大きく分けると2つの処理になるかな」
図: RSAUtil側で持つべき処理
「そうだね。あえて追加するならば、Transactionのうち実際にInputとして使われているものを、デジタル署名の検証後の処理で使えるようについでに抜き出しておこう。そうやってInputとなるTransactionを返しておくようにすれば、後で無駄がないんじゃないかな」
「おお!なるほどたしかに!」
「じゃあ、ここまでの処理をコードに直してごらん」
「えーと……、こんな感じかな」
rsa_util.py
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つの観点があると思う」
図: Transaction正当性確認における2つの観点
「なるほど。使用済みであるかどうか、という1つめの観点はわかりやすいけど、2つめはどういう観点だろうか?」
「Walletアプリで送金Transactionを実験的に作ってるときに思い出したんだけど、CoinbaseTransactionを誰でも無制限に作れてしまうと、暗号通貨として事実上なんの意味もなくなると思うんだよね」
「たしかにそうだね。実験では便利だけど、ブロック生成時以外にCoinbaseTransactionを誰でも作れてしまうのは、セキュリティ的にダメすぎる。この2つの処理をそれぞれコードにできるかい?」
「それぞれこんな感じの処理に分けるのがいいと思う」
BlockchainManagerに追加する処理
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との比較とほとんど同じだと思う」
TransactionPoolに追加する処理
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に持たせておこうかな」
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()の中だね」
「そう。こんな感じ」
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自体は、こんなふうに書けばいいかなって」
_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ははじくようにしておこう」
TransactionPoolに追加すべき手数料計算の処理
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のようになると思う」
図5.16: CoinbaseTransactionの追加からブロック生成まで
「つまり?」
「手順としては、こんな感じ。4と5の処理は図5.17にしておいたよ」
図: ブロック生成までの処理手順
図5.17: CoinbaseTransactionの扱い
「うん、いいと思う。それで、どんなコードになるだろう?」
「コードに直すとこんな感じかな」
ServerCoreのブロック生成処理への修正
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形式のファイルを書き出す、というくらいの処理なら、割と少ない変更で書けそう」
KeyManagerの初期化プロセスへの修正
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のチェック時には存在していなかった。だから、そこが重要なポイントになる。まずはそこから見ていこうか」
「うん」
「それに、ブロックの生成時は、一番悪いことができるタイミングでもある。どのような悪いことができそうかな?」
「パッと思いつくのは、この辺かな……」
図: ブロック生成の際に考えられる4つの悪用パターン
「うん、そんなところだろう。悪用1と2は、ブロックにおける検証に特有の話だ。一方、悪用3と4は、Transactionの検証にも共通する話だ」
「そうかー」
「じゃあ、過剰な手数料やインセンティブが載っていないかのチェックから見てみよう。どんな処理になると思う?」
「処理の流れはこんな感じになると思う」
図: 手数料チェックの流れ
「良さそうだね。2のインセンティブは、どうやって決める?」
「うーん。とりあえず、SimpleBitcoinでは、固定ということでいんじゃないかな。30くらいで」
「いいだろう。では、1からコードにしていこうか。手数料を計算する処理はどこで担当するのがいいだろう?」
「ブロックのTransaction群はCoinbaseTransactionも含んでるから、それをキレイに除外しようとすると、BlockchainManagerを使うよりもServerCoreでやるほうがいいかな。こんな感じで」
ServerCoreに追加する処理その1
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にセットされている値を取り出して比較するのが素直だと思う」
ServerCoreへ追加する処理その2
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は認めないって形にすればいいだけだから、これは簡単かな」
ServerCoreへ追加する処理その2+
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の中をチェックする必要はない」
「ってことは、こんな感じでいいかな。共通な処理が多いから、本当はもう少しキレイに書けるのかもしれないけど……」
ServerCoreへ追加する処理その3
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のとおり、②、③、④の部分について必要な処理を確認し、実際にコードを更新した」
図5.18: 現在の進捗状況
「うん。次はいよいよ、Walletとサーバーを接続しての最終確認だ!」