Work Records

日々の作業記録です。ソフトウェアエンジニアリング全般から、趣味の話まで。

Rubyでブロックチェーンをできるだけ丁寧に実装して理解する(ネットワーク編 Vol.3)

Rubyブロックチェーンを実装する

第8回目です。
第1回はウォレットを作成して秘密鍵・公開鍵・ビットコインアドレスを発行してみました。
第2回は単純なトランザクションについて実装してみました。
第3回はトランザクションに対する署名を実装しました。
第4回はトランザクションをブロックに格納し、ブロックチェーンを作りました。
第5回はプルーフオブワークを実装しました。
第6回はブロックチェーンネットワークを作り、ノードを作成しました。
第7回はジェネシシスブロックを作成し、他のノードに伝播させました。

全てのソースコードgithubに公開しています。
github.com

支払いのトランザクション

まずは支払いのトランザクションが発行できる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をした場合にネットワークがどのようにコンセンサスをとっていくのかといったあたりを見ていけると良いかと思っています。