Rubyでブロックチェーンをできるだけ丁寧に実装して理解する(ネットワーク編 Vol.2)
Rubyでブロックチェーンを実装する
第7回目です。
第1回はウォレットを作成して秘密鍵・公開鍵・ビットコインアドレスを発行してみました。
第2回は単純なトランザクションについて実装してみました。
第3回はトランザクションに対する署名を実装しました。
第4回はトランザクションをブロックに格納し、ブロックチェーンを作りました。
第5回はプルーフオブワークを実装しました。
第6回はブロックチェーンネットワークを作り、ノードを作成しました。
全てのソースコードはgithubに公開しています。
github.com
前提
過去6回の実装が終わっていることを前提とします。
Rubyでブロックチェーンをできるだけ丁寧に実装して理解する(ウォレット編) - Work Records
Rubyでブロックチェーンをできるだけ丁寧に実装して理解する(トランザクション編) - Work Records
Rubyでブロックチェーンをできるだけ丁寧に実装して理解する(署名編) - Work Records
Rubyでブロックチェーンをできるだけ丁寧に実装して理解する(ブロック編) - Work Records
Rubyでブロックチェーンをできるだけ丁寧に実装して理解する(プルーフオブワーク編) - Work Records
Rubyでブロックチェーンをできるだけ丁寧に実装して理解する(ネットワーク編 Vol.1) - Work Records
docker-composeの修正
前回の記事のdocker-composeから少し更新します。versionsを2にすることによってlinksを使用しなくてもお互いのコンテナ同士が疎通できるようになることと、自分以外のnodeをNODE1、NODE2として環境変数で指定できるようにします。
version: '2' services: node1: build: . environment: - REDIS_HOST=redis1 - ME=node1 - NODE1=node2 - NODE2=node3 ports: - "8001:8000" volumes: - .:/app command: ruby http_server.rb redis1: image: redis node2: build: . environment: - REDIS_HOST=redis2 - ME=node2 - NODE1=node1 - NODE2=node3 ports: - "8002:8000" volumes: - .:/app command: ruby http_server.rb redis2: image: redis node3: build: . environment: - REDIS_HOST=redis3 - ME=node3 - NODE1=node1 - NODE2=node2 ports: - "8003:8000" volumes: - .:/app command: ruby http_server.rb redis3: image: redis
ジェネシスブロックの作成とその電波
ジェネシスブロックを作成するAPI
まずは一番最初のブロックであるジェネシスブロックを作成します。どのノードで作ってもいいのですがわかりやすくnode1で作成することにします。
まずは、エンドポイントをhttp_server.rbに追加します。
server.mount('/genesis_block', GenesisBlockServlet)
対応するGenesisBlockServletをservlets.rbに作ります。
/genesis_blockにPOSTされるとまずは、last_hashが存在するかどうかを調べます。これで存在した場合にはすでにブロックは作成済みなのでBad Requestを返して終了します。
last_hashがない場合には、create_genesis_blockに自分のウォレットを渡して最初のブロックを作成します。
require './blockchain.rb' # ~ 省略 ~ class GenesisBlockServlet < WEBrick::HTTPServlet::AbstractServlet def do_POST(req, res) db = Database.new begin last_hash = db.restore("last_hash") res.status = 400 rescue StandardError wallet = Wallet.new wallet.load blockchain = Blockchain.new blockchain.create_genesis_block(wallet) last_hash = db.restore("last_hash") res.body = last_hash res.status = 201 end end end
実際にAPIを叩いてみます。
# ウォレットを作り $ curl -X POST http://127.0.0.1:8001/wallet 19aYwp5VtbUzXbDC6qWSpqUpaE7QEXcZ28 # 最初のブロックを作成します $ curl -X POST http://127.0.0.1:8001/genesis_block 02ebea727cd3bb2f44dbeefbf17fdbae25b3f12b16757e58c84c7ea4832cae3a
Redisを直接みてみると、正しく作成されていることがわかります。
> keys * 1) "last_hash" 2) "wallet" 3) "02ebea727cd3bb2f44dbeefbf17fdbae25b3f12b16757e58c84c7ea4832cae3a"
他のノードがブロックを取得する
ブロックのデータをGETできるAPIを作る
node1で最初のブロックができたので他のnodeからこれを取得できるようにします。
まずは、ブロックをGETできるAPIをhttp_server.rbに作ります。
server.mount('/blockchain', BlockchainServlet)
そして、BlockchainServletをservlet.rbに作ります。
/blockchainのパラメータとしてkeyを渡せるようにしました。keyで指定されたブロックをbodyで返します。ただし、key=last_hashの時だけもう一度last_hashのkeyを元にブロックデータを取得します。
class BlockchainServlet < WEBrick::HTTPServlet::AbstractServlet def do_GET(req, res) db = Database.new if db.exist_in_local?(req.query['key']) if req.query['key'] == "last_hash" hash = db.restore(req.query['key']) res.body = Marshal.dump(db.restore(hash)) else res.body = Marshal.dump(db.restore(req.query['key'])) end else res.status = 404 end end end
ここで、Redisにkeyが存在しているかどうかだけのチェック用のメソッドをdatabase.rbに追加しています。
def exist_in_local?(key) begin restore(key) rescue StandardError return false end true end
node1からブロック情報をとってくる処理
node1からブロック情報をGETできるAPIができたので、他のnodeからそのAPIを叩いてブロック情報を同期する処理を書きます。内部でAPIを叩くのでfaradayを追加します。
gem 'faraday'
まずは、http_server.rbにupdate_blockchainを追加します。これをnode2,3に対して叩くと内部でnode1からブロックの情報を取得するようにします。
server.mount('/update_blockchain', UpdateBlockchainServlet)
UpdateBlockchainServletをservlets.rbに書いていきます。
class UpdateBlockchainServlet < WEBrick::HTTPServlet::AbstractServlet def do_POST(req, res) # node1からlast_hashのブロックを取得する api_res = Faraday.get "http://" + ENV['NODE1'] + ":8000/blockchain", {key: "last_hash"} last_block = Marshal.load(api_res.body) last_hash = last_block.hash db = Database.new # もしすでにそのハッシュ値が自分のRedisに存在するなら処理は終える。 # ないなら保存する unless db.exist_in_local?(last_hash) db.save('last_hash', last_hash) db.save(last_hash, last_block) else return end # last_hashの一個前のハッシュをprev_block_hashから取得し、それも持ってなければ追加していく # prev_block_hashが空(最初のブロック)になるまで処理を続けていく prev_block_hash = last_block.prev_block_hash while prev_block_hash != "" api_res = Faraday.get "http://" + ENV['NODE1'] + ":8000/blockchain", {key: prev_block_hash} block = Marshal.load(api_res.body) if db.exist_in_local?(block.hash) break else db.save(block.hash, block) prev_block_hash = block.prev_block_hash end end end end
実際にnode2に対してapiを叩いてみると、Redisにジェネシスブロックが同期されることがわかります。hashも一致しています。
> keys * 1) "last_hash" 2) "02ebea727cd3bb2f44dbeefbf17fdbae25b3f12b16757e58c84c7ea4832cae3a"
まとめ
ジェネシスブロックを作成する処理を作りました。
他のノードが、node1からブロックを同期してくる処理を書きました。
次回
次回はトランザクションを発行して見たいと思います。