Photo by Attribution: Descryptive.com
秋山です。Pythonエンジニアです。
皆さんはブロックチェーンに興味ありますか?
このブログでも、過去にブロックチェーンに関する記事をいくつか書いてきました。
↓こちら記事では単体のソフトウエアとして採掘の流れをざっくり実装していますが、これだけだと不足があるというか、実際にネットワーク上で動いているブロックチェーンを複数人で共有する…といった実践的な話はつかみにくいかと思います。
paiza.hatenablog.com
そこで今回は、PythonのWebアプリケーションフレームワークFlaskを使って簡単なAPIを作り、複数人で一つのブロックチェーンを共有して、トランザクションを登録・採掘・確認するところまでを実装したいと思います。
ブロックチェーンや暗号通貨、またPython・Flaskを使ったWeb開発に興味がある人の参考になればと思います。
■開発環境について
今回使うライブラリは、FlaskとRequestsだけです。
Requestsについては以前こちらの記事でもちらっと紹介しましたが、APIを叩きたいときとかに便利に使えるライブラリです。
paiza.hatenablog.com
Flaskは、paizaラーニングで入門講座が今だけ全編無料になっているので、興味のある方は見てみてください。
とりあえず、今回はどちらも特に複雑な使い方はしませんので、前提知識とかは必要ない(はず)です。
■ブロックチェーンについて
ブロックチェーンは、基本的に時系列順の取引データを改ざんされにくい形式で蓄積する…というのが基本的な目的です。残高を第三者経由で参照されても、改ざんされるようなことは確率的にほぼあり得ません。
ブロックは、以下のようなデータを含みます。
- no:ブロックナンバー
- hash:そのブロックのハッシュ値
- prev_hash:一つ前のブロックのハッシュ値
- transaction:取引データ
- unixtime:時間
- nonce:採掘時に付与する値
- diff:採掘難易度
ざっくり説明すると、
noは単純に連番です。
hash,prev_hashはハッシュ値、ここではsha256を使います。
prev_hashは一つ前のブロックを指します。
transactionは取引データをそのまま配列として持ちます。
unixtime(UNIX時間 - Wikipedia)は時間をそのまま数値として持ちます。
nonceは採掘者が採掘時に見つける値で、整数値とします。
diffはunixtimeが一つ前とのブロックと比べて採掘に3分以上時間がかかったら難易度を1下げる、3分未満であれば1上げるようにします。
といった仕様です。
transactionについては、あくまで取引データというていの文字列を追加するだけです。公開鍵暗号で署名したハッシュ値を付与するなどしないと、一般的なブロックチェーンにおける「本人しか書き込むことの出来ない取引情報」という条件は満たせません。
これから作るブロックチェーンの、no=0 になる0番目のブロックの具体例を出すとこんな感じです。
{"diff":2,"hash":"c30fe3a431a753ffd728e26901422ec1a32ffcb8dca26698bf1b21ad1bafac72","no":0,"nonce":"","prev_hash":"NONE","transaction":["this is genesis block, from paiza"],"unixtime":1537247368}
順番が少し前後していますが、左から難易度、ハッシュ値〜…となっているのがわかるかと思います。
0番目のブロックは、特殊なブロックとしてgenesisブロックと呼ばれることが多いです。unixtimeは、今この記事を書いている時間ですね。わかる人は実際の時間に変換してみて下さい。
余談ですが、Bitcoinのgenesisブロックには「The Times 03/Jan/2009 Chancellor on brink of second bailout for banks」とThe Timesの見出しが引用されています。
Bitcoin Transaction 4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b
銀行への救済措置の見出しで、ジョークとも取れますが、一応それ以前にBitcoinのブロックチェーンは存在しなかった…ということを証明できるものです。
■ブロックチェーンを実装してみる
では実際にコードを書きつつやっていきます。
ライブラリを入れていない人はpipで入れときましょう。
pip install flask requests
まずは、genesisブロックを作る処理を書きます。
↓こうすると、先に示したブロックのハッシュ値と全く同じものが得られるかと思います。このブロックをもとにして、ブロックをどんどんつなげてブロックチェーンを作っていきましょう。
from flask import Flask, jsonify, request from hashlib import sha256 import requests #genesis_blockの雛形を作ります。計算が必要はハッシュ値などは後述 genesis_block = {'no' : 0, 'transaction' : ['this is genesis block, from paiza'], 'nonce' : '', \ 'prev_hash' : 'NONE', 'hash' : '', 'diff' : 2, 'unixtime' : 1537247368} #blockの情報のうちhash値を計算するために使う値を文字列結合します。 genesis_block_str = str(genesis_block['no']) \ + ",".join(genesis_block['transaction']) \ + str(genesis_block['nonce']) \ + genesis_block['prev_hash'] \ + str(genesis_block['unixtime']) #ハッシュ値化 genesis_block_byte = genesis_block_str.encode('utf-8') genesis_block_hash = sha256(genesis_block_byte).hexdigest() genesis_block['hash'] = genesis_block_hash
これでデータの持ち方が決まりました。
次に、Flaskを使って複数の人が参加するネットワークを作っていきましょう。
前述のコードに書き足す形式で続けて行きます。
app = Flask(__name__) block_chain = [] block_chain.append(genesis_block) nodes = [] pool = []
appは、Flaskを呼び出すために使います。
block_chainは、そのままブロックチェーンをどんどん蓄積していく配列ですので、すぐにgenesis_blockをappendしていきます。
nodesは、接続先情報を配列で持ちます。Flaskを使ってhttp://〜のURL形式でデータをやり取りしていきますので、IPアドレスとポートの結合した文字列とします。
poolは、ブロックに追加する前のtransactionを一時的にプールしておく場所です。要するに採掘されるのを待っている取引ですね。トランザクションプールと呼ぶ場合が多いです。
次は、ひとまず複雑ではない単純なレスポンスを返す、あるいは単にデータを書き込んだり消したりする操作を追加していきます。
@app.route('/add_transaction', methods=['GET']) def add_transaction(): transaction = request.args.get('transaction') pool.append(transaction) return jsonify(pool) @app.route('/get_pool', methods=['GET']) def get_pool(): return jsonify(pool) @app.route('/remove_pool', methods=['GET']) def remove_pool(): transaction = request.args.get('transaction') pool.remove(transaction) return jsonify(pool) @app.route('/last_block', methods=['GET']) def last_block(): return jsonify(block_chain[-1]) @app.route('/get_block', methods=['GET']) def get_block(): no = request.args.get('no') if no and len(block_chain) > int(no): block = block_chain[int(no)] else: block = {} return jsonify(block)
上から順に…
- add_transaction:採掘待ちのプールへ追加
- get_pool:採掘待ち情報を取得
- remove_pool:採掘待ち情報を削除
- last_block:持っている最後のブロックを取得
- get_block:番号指定でブロックを取得(last_blockの処理とまとめてもよかったかもしれないです)
といった感じです。
続いて、node 情報の取得、追加処理とblock_chainすべてを取得する処理です。
@app.route('/get_node', methods=['GET']) def get_node(): return jsonify(nodes) @app.route('/add_node', methods=['GET']) def add_node(): node = request.args.get('node') if node and node not in nodes: nodes.append(node) r = requests.get("http://" + node + "/add_node?node=0.0.0.0:" + str(my_port)) return jsonify(nodes) @app.route('/block_chain', methods=['GET']) def get_block_chain(): check_node() return jsonify(block_chain)
get_nodeは、そのまま持っているノードを返します。
add_nodeは、ノードを追加します。また、追加したノードに対して「自分も追加して」とお願いする処理をします。
get_block_chainは、ブロックチェーンを返してもらう処理です。
ここで未定義の check_node() という関数が現れましたね。これは、各ノードからブロックを取得していき、自分が持っているブロックよりも長いブロックが見つかったら繋げていく…という処理です。
↓具体的にはこんな感じ。
def check_node(): max_no = len(block_chain) max_node = "" for node in nodes: #各ノードに対してlast_blockをしていって最も大きなブロックナンバーを取得 r = requests.get("http://"+node+"/last_block") if r.status_code == 200: json_data = r.json() if max_no < json_data['no']: max_no = json_data['no'] max_node = node if max_no != 0: #自分より長いブロックがあったのならば r = requests.get("http://"+node+"/get_block?no="+str(len(block_chain)-1)) #自分の持ってる最新のブロックと相手の持っている同じ番号のブロックを取得 if r.status_code == 200: json_data = r.json() if json_data['hash'] == block_chain[-1]['hash']: #ハッシュ値が一致していることを確認する。 for i in range(len(block_chain), max_no+1): #以降ブロックを繋いでいく(本当はhashとprev_hashがちゃんと繋がっているか検証したほうが良い。 r = requests.get("http://" + node + "/get_block?no="+str(i)) if r.status_code == 200: json_data = r.json() block_chain.append(json_data)
まず、for node in nodesですべてのノードに対して最新のブロックを要求し、最も大きいブロック番号を持っている人を見つけたら、その人のブロックを自分のブロックに繋いで更新していきます。そうして、別のところで採掘されたブロックなどを取り込んでいくんですね。
仕組み的に一番面倒な部分はこのあたりなので、逆に言うとあと少しで終わりです。
続いて採掘の処理です。
↓ソースが少し長いですが、よく見ればやっていることは単純です。(ちなみに1000万回採掘したら一旦止まるようにしています。無限に採掘し続けるのが現実ですが、今回はとりあえずデバッグとかがしづらいので…)
import time,datetime @app.route('/mining', methods=['GET']) def mining(): global pool for i in range(10000000): nodes_pool = [] remove_pools = {} for node in nodes: #各nodeのトランザクションプールから取引情報を取得取り込みます。 r = requests.get("http://"+node+"/get_pool") if r.status_code == 200: json_data = r.json() remove_pools[node] = json_data nodes_pool.extend(json_data) next_block_no = len(block_chain) #次のブロックナンバーを決める #genesisブロックと同じ形でブロックを作成します。 mining_block = {'no' : next_block_no, 'transaction' : pool+nodes_pool, 'nonce' : i, \ 'prev_hash' : block_chain[next_block_no-1]['hash'], 'hash' : '', 'diff' : block_chain[next_block_no-1]['diff'], 'unixtime' : 0} mining_block['unixtime'] = int(datetime.datetime.now().timestamp()) mining_block_str = str(mining_block['no']) \ + ",".join(mining_block['transaction']) \ + str(mining_block['nonce']) \ + mining_block['prev_hash'] \ + str(mining_block['unixtime']) mining_block_byte = mining_block_str.encode('utf-8') mining_block_hash = sha256(mining_block_byte).hexdigest() mining_block['hash'] = mining_block_hash check_hash = bin(int(mining_block_hash, 16))[2:] #ハッシュ値を2進数に変換します。 print(check_hash[:20], check_hash.index('0')) #2進数化したハッシュ値の1が何個並んでいるかを難易度とし0が出現した場所がdiffより大きいか検証、大きければ採掘成功。 if check_hash.index('0') > block_chain[next_block_no-1]['diff']: #採掘難易度を調整します。 60 * 3 秒 より採掘時間が短ければ難易度アップ、逆であれば難易度ダウンです if mining_block['unixtime'] - 60*3 < block_chain[next_block_no-1]['unixtime']: mining_block['diff'] += 1 else: mining_block['diff'] -= 1 #完成したブロックを追加、更に取引プールを消していきます。(この処理は取引プール側でブロックチェーンで見つけたら消すが正しい処理ですが簡単化しています。 block_chain.append(mining_block) for k, v in remove_pools.items(): for trans in v: print("http://" + k + "/remove_pool?transaction=") r = requests.get("http://" + k + "/remove_pool?transaction=" + trans) pool = [] break else: mining_block = {} return jsonify(mining_block)
トランザクションプールから取引を回収し、ブロックを構成し、ハッシュ値を計算しています。ここまでは、genesisブロックの作り方を見直してもらうとわかりやすいかと思います。
続いて、採掘したハッシュ値がルールに沿っているか(難易度diffの値に沿ったものをかどうか)を検証しています。
検証が完了したら、難易度の調整、トランザクションプールの削除、ブロックの追加…と順に処理していきます。
トランザクションプールの削除に関してはかなり簡略化して、いきなりほかのノードのものをガンガン消していますが、これは本来であればよくないやり方です。本来は、ブロックチェーンに追加されたことの確認を持ってトランザクションプールから削除されるべきなので…。(今回はトランザクションに署名などがなく、一意性を確認できないため簡単化した手順で処理しています)
というわけで、これで一旦完成しました。動かしてみましょう。
import sys my_port = None if __name__ == "__main__": my_port = int(sys.argv[1]) app.run('0.0.0.0', my_port, use_reloader=True)
Python block_chain.py 1234 などとすれば動作し始めます。最初は単純に単独ノードで動作し、genesisブロックのみを持った状態です。
■PaizaCloudでやってみる
ちなみに、オンラインクラウド実行環境PaizaCloudを使うと、上記コードを順にコピペしていくだけで動かすことができますので、やってみましょう。
PaizaCloudでもRequestsとFlaskをpipインストールして…
pip install requests flask
ターミナルを開いたら、新規ファイルで block_chain.py をコピペします。
まずは単体動作させてみます。ターミナルで
python block_chain.py 1234
とすると、ポート1234で起動します。
次に左のブラウザボタンを開いて
https://localhost-[サーバーを作ったときの名前].paiza-user.cloud:1234/block_chain
を開きます。(もしくは、ブラウザをクリックして開くURLの頭に "localhost-" にポート番号 :1234 をつけてもOKです)その後、/block_chainを見てみましょう。
json形式でgenesis blockが確認できるかと思います。
これだけだと一つのブロックしかないので、マイニングさせてみましょう。
https://localhost-[サーバーを作ったときの名前].paiza-user.cloud:1234/mining
へアクセスすると、マイニングが開始されます。
マイニング状況がターミナルに出てくるので、ながめているとそれっぽい0,1 がバーっと流れてきます。これは、左から見て1がいくつ並んでいるかで、難易度を調整している様子です。
一番下の 11111110111111010111 7 は、難易度7で "1" が7個並んだハッシュ値を見つけたよ!という表示です。
再度 /block_chain へアクセスすると…だいぶブロックが追加されていますね。
6番目のブロックまで見つかっています。
transactionがないのと、一人で掘っているだけではあんまり意味がないので、今度は2つのノードで試してみましょう。
もう一つターミナルとブラウザを開き、ターミナルに python block_chain.py 5678 と入力して、ポート番号 5678 で起動させます。
この状態だと、お互いに genesis block は共通ですが、 5678 は 1234 のブロックを取り込んでいないので、とりあえずノードを追加してみましょう。
5678 のブラウザに
https://localhost-[サーバーを作ったときの名前].paiza-user.cloud:5678/add_node=0.0.0.0:1234
と入力して、1234 を追加します。追加リクエストを受け取った 1234 の方もターミナルをよく見ていると、5678 にノード追加のリクエストを送っています。
この状態で
https://localhost-[サーバーを作ったときの名前].paiza-user.cloud:5678/block_chain
と 5678 のblock_chainへアクセスすると、ノードを参照して、最も長い 1234 のブロックを取り込んできます。
続いて、それぞれのトランザクションを登録してみましょう。
https://localhost-[サーバーを作ったときの名前].paiza-user.cloud:5678/add_transaction?transaction=5678message%20add%20transaction
と 5678 のトランザクションプールに追加して、採掘を開始します。1234のほうで採掘してみましょう。
https://localhost-[サーバーを作ったときの名前].paiza-user.cloud:1234/mining
へアクセスすると、マイニングがされます。
そうして追加されたブロックは 1234 のみが持っているので
https://localhost-[サーバーを作ったときの名前].paiza-user.cloud:1234/block_chain
で確認できますが、 5678 も
https://localhost-[サーバーを作ったときの名前].paiza-user.cloud:5678/block_chain
にアクセスすると、各ノードの中で新しいブロック(1234が採掘成功したブロック)を見つけて追加できるので、以下のような感じになります。
{"diff":8,"hash":"ffe9aa55f35295724d02738f1daa3297ed69cda43e53e7bd0c460096b65d0d58","no":10,"nonce":82,"prev_hash":"ffc93c87c269033add391a3339b032db97d674615505295040e82a672b33e6d2","transaction":["1234message add transaction paiza","5678message add transaction"],"unixtime":1537337505}
こんな感じでブロックが作成されます。
適当にマイニングURLを連続で叩くとどんどん難易度が上がって、以下のような盛り上がっている感じが見えます。
Bitcoinではこういうことを世界規模でやっている感じですね。(今回作ったものは、当然ですが明らかに整合性がとれない処理が多いです。例えば掘ったブロック自体に掘った本人の署名などが登録されていないので、掘った人が判別不能とか…)
Bitcoinの場合、採掘報酬を書き込める権利が発生し、その書き込みもまたハッシュ値の計算に使うため、第三者が「自分が掘った」と嘘を言ったところで、採掘報酬の書き込みデータは変えられません。(変えたらハッシュ値の再計算が必要)採掘報酬を一億Bitcoin!とか書いたところで、ほかの人達はそのブロックを受け入れない、といった仕組みを組み込んでおくことで、整合性をとっていく必要があります。
■まとめ
ブロックチェーンのアルゴリズム自体はシンプルですが、実際はブルームフィルタ(ブルームフィルタ - Wikipedia)やハッシュ木(ハッシュ木 - Wikipedia)など、さまざまなアルゴリズムを使って利便性と安全性を高めています。
この辺のアルゴリズムも調べつつ自分で組んでいくと、理解が深まって楽しいので興味がある人はそのへんもやってみてください。
あと、途中でも紹介しましたが、これまでにもPythonや暗号通貨、機械学習などに関する記事をいろいろ書いているので、気になった人はぜひ見てみてください。
paiza.hatenablog.com
paiza.hatenablog.com
現在paizaラーニングでは、9/24(月)までの期間限定で「Webアプリ開発入門 Flask編」を全編無料公開しています。 Pythonを使ったWeb開発やFlaskに興味がある方は、見てみてください。
詳しくはこちら
「Python入門編」も全編無料なので、プログラミング初心者の方はこちらから始めてみてください。
「PaizaCloud」は、環境構築に悩まされることなく、ブラウザだけで簡単にウェブサービスやサーバアプリケーションの開発や公開ができます。
「paizaラーニング」では、未経験者でもブラウザさえあれば、今すぐプログラミングの基礎が動画で学べるレッスンを多数公開しております。
詳しくはこちら
そしてpaizaでは、Webサービス開発企業などで求められるコーディング力や、テストケースを想定する力などが問われるプログラミングスキルチェック問題も提供しています。
スキルチェックに挑戦した人は、その結果によってS・A・B・C・D・Eの6段階のランクを取得できます。必要なスキルランクを取得すれば、書類選考なしで企業の求人に応募することも可能です。「自分のプログラミングスキルを客観的に知りたい」「スキルを使って転職したい」という方は、ぜひチャレンジしてみてください。
詳しくはこちら