Work Records

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

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

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

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

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

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からブロックを同期してくる処理を書きました。

次回

次回はトランザクションを発行して見たいと思います。