Work Records

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

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

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

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

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

ブロックチェーンのネットワークについて

今までの実装では、ブロックチェーンの各機能の動きに関して必要な部分を実装してきました。
今回はからは少し毛色が異なり、ブロックチェーンのネットワーク自体を実装して見たいと思います。
とはいえ、流石に本格的なP2Pネットワークを作るわけにもいかないので簡単な機能を持つノードを3つ立て、超簡易なブロックチェーンネットワークを構築してみようと思います。

ノードの要件定義

それぞれが独立して動けるフルノードを作成していきます。要件は次のように決めてそのように作っていきます。

システム要件
  • ノードはhttpサーバーとして作成し、全ての動作はAPI経由とする
  • データの保存はこれまで通りRedisとし、各ノードに一つづつRedisを保持する
  • 全てのノードはウォレットを持っている
ジェネシスブロック
  • マスターノードを一つ決める
  • このマスターノードがジェネシスブロックを発行する
その他のノード
  • その他のノードは、マスターノードからジェネシスブロックを受け取る
トランザクションの発行
トランザクションの検証
ブロックの検証と取り込み
  • ブロックが伝播されてきたノードは、その時点で自分のPoW作業を中止し、ブロックの検証に入る
  • ブロックが正常であればそのブロックは各ノードのブロックチェーンに保管される

こんな具合になります。詳細はもう少し複雑ではありますが、このような流れで実装して見たいと思います。


APIサーバーを立ち上げる

まず、各ノードはAPIで全てのやり取りを行うためそういった仕組みのサーバーを作ってみようと思います。
一つのPCでの作業が行えるようにするため、docker-composeにより複数環境を作れるようにします。

rubyのhttpサーバー

まずはシンプルなhttpサーバーを作り、ウォレットの作成と取得のAPIを生やしていきます。
http_server.rbという名前で作っていきます。

require 'webrick'
require './servlets.rb'

server = WEBrick::HTTPServer.new({
  :DocumentRoot => './',
  :BindAddress => '0.0.0.0',
  :Port => 8000
})

# /wallet にWalletSerlvetをマウントする
server.mount('/wallet', WalletServlet)

Signal.trap('INT'){server.shutdown}
server.start

WalletSerlvetはservlets.rbに書いていきます。
中身は単純に、GETできた場合にはwalletを取得しアドレスを返す、POSTできた場合にはwalletを新規作成します。

require './wallet.rb'

class WalletServlet < WEBrick::HTTPServlet::AbstractServlet
  def do_GET(req, res)
    wallet = Wallet.new
    wallet.load
    if wallet.public_key
      res.body = wallet.address
      res.status = 200
    else
      res.status = 404
    end
  end
  def do_POST(req, res)
    wallet = Wallet.new
    wallet.create_key
    wallet.save
    res.body = wallet.address
    res.status = 201
  end
end

wallet.rbにも少し手を加えます。
今までは、ネットワークという概念がなかったので一つのRedisに3つのウォレットを作成してきましたが、これからは1ノードに1ウォレットとしていきます。勿論複数のウォレットを持つことは可能ですが単純化のためです。
Redis用のkeyは"wallet"とし、loadとsaveメソッドも変更していきます。

class Wallet
  attr_accessor :private_key, :public_key

  def initialize
    # 簡単のために1ウォレっとにするのでkeyをwalletでRedisから取得する
    @key = 'wallet'
    @private_key = nil
    @public_key = nil
  end

  # ~ 略 ~

  # key=walletでRedisから取得し、もしなければreturnする
  def load
    db = Database.new
    begin
      saved_wallet = db.restore(@key)
    rescue StandardError
      return
    end
    @private_key = saved_wallet.private_key
    @public_key = saved_wallet.public_key
    self
  end

  # ~ 略 ~

  # key=walletで保存する
  def save
    db = Database.new
    db.save(@key, self)
  end

docker-composeによるサーバーの立ち上げ

コードはこれで編集完了ですが、複数のサーバーを立ち上げていくため、docker-composeによる立ち上げを行なっていきます。
以下のようなファイルを追加します。

docke-comose.yml

node1:
  build: .
  environment:
    - REDIS_HOST=redis1
  ports:
    - "8001:8000"
  volumes:
    - .:/app
  links:
    - redis1
  command: ruby http_server.rb
redis1:
  image: redis

DockerfileとGemfile

FROM ruby:2.5.1
WORKDIR /app
ADD Gemfile* $WORKDIR/
RUN bundle install
source 'https://rubygems.org'

gem 'base58'
gem 'ecdsa'
gem 'redis'

また、databaseの接続先を環境変数に変更するため、database.rbにも少し修正を加えます。

   def initialize
     @redis = Redis.new(host: ENV['REDIS_HOST'], port: 6379, db: 06)
   end

これで準備が整いました。

walletのAPIを叩いてみる

サーバーを起動して見ます。

$ docker-compose up -d
Creating 06-network_redis1_1 ... done
Creating 06-network_node1_1  ... done

$ docker-compose ps
       Name                      Command               State           Ports         
-------------------------------------------------------------------------------------
06-network_node1_1    ruby http_server.rb              Up      0.0.0.0:8001->8000/tcp
06-network_redis1_1   docker-entrypoint.sh redis ...   Up      6379/tcp

8001番ポートでリッスンしているのでそこにリクエストを投げて見ます。
まずはPOSTでウォレットを作成します。

$ curl -X POST http://127.0.0.1:8001/wallet
1HiA8wau2VDjQgknNXayqood53rs2o68CP

次にGETしてみます。

$ curl -X GET http://127.0.0.1:8001/wallet
1HiA8wau2VDjQgknNXayqood53rs2o68CP

作成したウォレットが取得できました。

2つめ、3つ目のノードを立ててみる

docker-composeができているのでここに追加するだけです。

node1:
  build: .
  environment:
    - REDIS_HOST=redis1
  ports:
    - "8001:8000"
  volumes:
    - .:/app
  links:
    - redis1
  command: ruby http_server.rb
redis1:
  image: redis

node2:
  build: .
  environment:
    - REDIS_HOST=redis2
  ports:
    - "8002:8000"
  volumes:
    - .:/app
  links:
    - redis2
  command: ruby http_server.rb
redis2:
  image: redis

node3:
  build: .
  environment:
    - REDIS_HOST=redis3
  ports:
    - "8003:8000"
  volumes:
    - .:/app
  links:
    - redis3
  command: ruby http_server.rb
redis3:
  image: redis

ということでこちらにもリクエストを送ってみます。

$ curl -X POST http://127.0.0.1:8002/wallet
13dbzCGBe2bmv6mp52DFZd5xYYzTy6Bn94

$ curl -X GET http://127.0.0.1:8002/wallet
13dbzCGBe2bmv6mp52DFZd5xYYzTy6Bn94

問題なさそうです。

まとめ

今回は、ブロックチェーンネットワークを作るためにdocker-composeを利用した簡単なhttpサーバーを立てて見ました。
また、walletの作成と取得をするAPIを作成しました。

次回

次回はジェネシスブロックを作成し、他のノードがそのブロックを同期できるようにしていきます。