Rubyでブロックチェーンをできるだけ丁寧に実装して理解する(ネットワーク編 Vol.3)
Rubyでブロックチェーンを実装する
第8回目です。
第1回はウォレットを作成して秘密鍵・公開鍵・ビットコインアドレスを発行してみました。
第2回は単純なトランザクションについて実装してみました。
第3回はトランザクションに対する署名を実装しました。
第4回はトランザクションをブロックに格納し、ブロックチェーンを作りました。
第5回はプルーフオブワークを実装しました。
第6回はブロックチェーンネットワークを作り、ノードを作成しました。
第7回はジェネシシスブロックを作成し、他のノードに伝播させました。
全てのソースコードはgithubに公開しています。
github.com
前提
過去7回の実装が終わっていることを前提とします。
Rubyでブロックチェーンをできるだけ丁寧に実装して理解する(ウォレット編) - Work Records
Rubyでブロックチェーンをできるだけ丁寧に実装して理解する(トランザクション編) - Work Records
Rubyでブロックチェーンをできるだけ丁寧に実装して理解する(署名編) - Work Records
Rubyでブロックチェーンをできるだけ丁寧に実装して理解する(ブロック編) - Work Records
Rubyでブロックチェーンをできるだけ丁寧に実装して理解する(プルーフオブワーク編) - Work Records
Rubyでブロックチェーンをできるだけ丁寧に実装して理解する(ネットワーク編 Vol.1) - Work Records
Rubyでブロックチェーンをできるだけ丁寧に実装して理解する(ネットワーク編 Vol.2) - Work Records
前回までで、ジェネシスブロックを作成できているので今回は実際にトランザクションを発行してコインの支払いが完了するところまで実装して行きたいと思います。
支払いのトランザクション
まずは支払いのトランザクションが発行できるAPIを作ります。
http_server.rbに/payのエンドポイントを足して、
server.mount('/pay', PayServlet)
PayServletを作ります。
class PayServlet < WEBrick::HTTPServlet::AbstractServlet def do_POST(req, res) to = req.query['to'] amount = req.query['amount'] # ウォレットをロードしたらpayメソッドを呼びだし、トランザクションインスタンスを作る wallet = Wallet.new wallet.load new_transaction = wallet.pay(to, amount) transactions = Transactions.new # トランザクションが正しいことを確認し、mem poolに格納する if new_transaction.is_valid? transactions.add_to_mem_pool new_transaction end # 自分以外のnodeにトランザクションを送信する dumped_new_transaction = Marshal.dump(new_transaction) Faraday.post "http://" + ENV['NODE1'] + ":8000/transaction", {transaction: dumped_new_transaction} Faraday.post "http://" + ENV['NODE2'] + ":8000/transaction", {transaction: dumped_new_transaction} end end
node1から送られてくるトランザクションデータを受け取れるように/transactionエンドポイントを作ります。
http_server.rbに/transactionを足して
server.mount('/transaction', TransactionServlet)
TransactionServletを追加します。
class TransactionServlet < WEBrick::HTTPServlet::AbstractServlet def do_POST(req, res) transaction = Marshal.load(req.query['transaction']) # 受け取ったトランザクションを自分のmem_poolに追加する transactions = Transactions.new if transaction.is_valid? transactions.add_to_mem_pool transaction end end end
マイニングする
マイニングノードは、mem poolにあるトランザクションを検証してPoWし、自分以外のnodeに伝播させる必要があります。
マイニングを開始するためのエンドポイントを作ります。実際にはノードはある程度溜まったトランザクションを自動的にブロックに追加して行きますが、今回は簡単のためAPIをトリガーに利用します。
server.mount('/start_pow', StartPowServlet)
StartPowServletを書きます。
class StartPowServlet < WEBrick::HTTPServlet::AbstractServlet def do_POST(req, res) transactions = Transactions.new transactions.load_all # mem poolに入っているトランザクションでブロックを作成し、PoWをおこなう if transactions.mem_pool.count > 0 blockchain = Blockchain.new wallet = Wallet.new wallet.load blockchain.create_block(transactions.mem_pool, wallet.address) transactions.delete_mem_pool # PoWが終了したら、他のノードに対してブロックを自分から同期するように命令を出す Faraday.post "http://" + ENV['NODE1'] + ":8000/update_blockchain", {node: ENV['ME']} Faraday.post "http://" + ENV['NODE2'] + ":8000/update_blockchain", {node: ENV['ME']} end end end
最後に、UpdateBlockchainServletを少し書き換えます。
class UpdateBlockchainServlet < WEBrick::HTTPServlet::AbstractServlet def do_POST(req, res) host = req.query['node'] || ENV['NODE1'] api_res = Faraday.get "http://" + host + ":8000/blockchain", {key: "last_hash"} last_block = Marshal.load(api_res.body) last_hash = last_block.hash # ブロック追加時に、PoWが有効かどうかをチェックする db = Database.new blockchain = Blockchain.new unless db.exist_in_local?(last_hash) unless blockchain.is_genesis_block(last_block) pow = ProofOfWork.new(last_block) return unless pow.validate end db.save('last_hash', last_hash) db.save(last_hash, last_block) else return end # ブロック追加時に、PoWが有効かどうかをチェックする 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 unless blockchain.is_genesis_block(block) pow = ProofOfWork.new(block) return unless pow.validate end db.save(block.hash, block) prev_block_hash = block.prev_block_hash end end # 全てを同期し終わったら、自分のmem poolはすでに古いので削除する transactions = Transactions.new transactions.delete_mem_pool end end
以上で実装は終了です。
実際にウォレっとの作成からマイニングまで
これで一通りの実装が終わったので、実際にウォレットを作成してから支払いが完了するまでのフローを辿ってみることにします。
まずは、ウォレットを作成します。
$ curl -X POST http://127.0.0.1:8001/wallet 17FkMorsiWrJrzbhhf8cUGKBYt7arR2bzZ $ curl -X POST http://127.0.0.1:8002/wallet 1MRL2YkXSZ2the1snphsH8zgffm53LYZxN $ curl -X POST http://127.0.0.1:8003/wallet 1NA3qUgd3xxY8ea9UTNrZJLHJSfHr6mpTN
次に、ジェネシスブロックを作成します。これはnode1に対して実行します。
$ curl -X POST http://127.0.0.1:8001/genesis_block 20c79c27d2852a20daddfa7297087b4db54edb6b2d591a0dbfeb0b342adfab34
node2,3にジェネシスブロックを同期させます。
$ curl -X POST http://127.0.0.1:8002/update_blockchain $ curl -X POST http://127.0.0.1:8003/update_blockchain
node1からnode2に10コイン支払いを行ってみます。このトランザクションは直ちに他のnodeに伝播します。
$ curl -X POST http://127.0.0.1:8001/pay -d to=1MRL2YkXSZ2the1snphsH8zgffm53LYZxN -d amount=10
node3でPoWして見ましょう。問題なければブロックがマイニングされて他のnodeに同期されるようになるはずです。
$ curl -X POST http://127.0.0.1:8003/start_pow
確認のために、各nodeの残高を表示してみます。
# node1はジェネシスブロックを作って1000コインを持っていたが、node2に支払いをしているので990 $ curl -X GET http://127.0.0.1:8001/wallet 17FkMorsiWrJrzbhhf8cUGKBYt7arR2bzZ 990 # node2はnode1から支払いを受けているので10 $ curl -X GET http://127.0.0.1:8002/wallet 1MRL2YkXSZ2the1snphsH8zgffm53LYZxN 10 # node3はマイニングを行ったので報酬として25 $ curl -X GET http://127.0.0.1:8003/wallet 1NA3qUgd3xxY8ea9UTNrZJLHJSfHr6mpTN 25
ということで、3つのノードでうまく動作していることがわかりました。
まとめ
実際に支払いのトランザクションを発行しました。
そのトランザクションを別のnodeに転送して見ました。
転送されたトランザクションをブロックにしてPoWを行いました。
PoWを行ったブロックを他のnodeに伝播させました。
次回?
今回で一通りの実装は完了したと思います。
もし次回あるなら、複数のnodeでPoWをした場合にネットワークがどのようにコンセンサスをとっていくのかといったあたりを見ていけると良いかと思っています。