Rubyでブロックチェーンをできるだけ丁寧に実装して理解する(ネットワーク編 Vol.1)
Rubyでブロックチェーンを実装する
第6回目です。
第1回はウォレットを作成して秘密鍵・公開鍵・ビットコインアドレスを発行してみました。
第2回は単純なトランザクションについて実装してみました。
第3回はトランザクションに対する署名を実装しました。
第4回はトランザクションをブロックに格納し、ブロックチェーンを作りました。
第5回はプルーフオブワークを実装しました。
全てのソースコードはgithubに公開しています。
github.com
前提
過去5回の実装が終わっていることを前提とします。
Rubyでブロックチェーンをできるだけ丁寧に実装して理解する(ウォレット編) - Work Records
Rubyでブロックチェーンをできるだけ丁寧に実装して理解する(トランザクション編) - Work Records
Rubyでブロックチェーンをできるだけ丁寧に実装して理解する(署名編) - Work Records
Rubyでブロックチェーンをできるだけ丁寧に実装して理解する(ブロック編) - Work Records
Rubyでブロックチェーンをできるだけ丁寧に実装して理解する(プルーフオブワーク編) - Work Records
ブロックチェーンのネットワークについて
今までの実装では、ブロックチェーンの各機能の動きに関して必要な部分を実装してきました。
今回はからは少し毛色が異なり、ブロックチェーンのネットワーク自体を実装して見たいと思います。
とはいえ、流石に本格的な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
問題なさそうです。
次回
次回はジェネシスブロックを作成し、他のノードがそのブロックを同期できるようにしていきます。