Work Records

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

Cloudnative Days Tokyo 2019で「マイクロサービスにおける最高のDXを目指して」というタイトルで発表しました

cloudnative days tokyo 2019というイベントで登壇してきました。
speakerdeck.com

マイクロサービスにおける最高のDXを目指して、というタイトルで詳細はスライドを見ていただければと思います。


思えば、CFPを出して40分も登壇することが初めての経験だったので普段緊張しない性格ですが割と緊張しました。


普段は大抵、自社のイベントか、ありがたいことに声をかけていただく時に登壇をすることはあるのですが、
今回は社内のエンジニアたちに触発されて2月くらいに応募したのが経緯です。

DroidKaigiに登壇するandroidエンジニアの資料の壁打ちを何度かしました。


マネージャーという役職についてもう暫くたちますが、
偉そうに資料の壁打ちばかりしてないで自分もチャレンジしていかないとダメかなと思い立ったしだいです。

ということで、ここ半年で自分としても興味があって取り組んできているDXというテーマでCFPを出しました。

ちょっと予想外だったのはCFPが通った直後くらいから会社のプロジェクトが想定以上に忙しくなってしまったこと。
そのためかなりギリギリまで資料や登壇の準備とかをやっていました。
(協力してくれたみなさんありがとうございます)

twitterの反応など見るとある程度は意味のある登壇ができたのかなとホッとしています。
資料ギリギリだったのと緊張してたので、ブースを回れなかったのがちょっと残念でした。


また機会とネタがあればどんどんやっていこうと思います。
(イベント運営の方々、ありがとうございました)

30 days challenge(仮名)というアプリの構想

今年はゴールデンウィークが10日もあるので10daysハッカソンをしている。
今日は4日め。平成最後の日。

なにを作っているか?

一言で言うと、30日で何かを達成したい、習慣づけたいと言う人のためのアプリを作っている。
TEDでも同じようなアイディアを話している人がいるので興味あれば。
www.ted.com

なにができるかと言うと、毎日何かをやった証拠となるものを画像か動画の形式でアップロードしていって30日後に完成というシンプルなもの。
30日間で溜めたアセットをいい感じに変換してSNSなどでシェアできる形にしたいなと思っている。



なんでこれを作りたかったか

最近のSNSなどを見ていると、コンテンツ自体が短時間で大量に消費されていると感じている。
これ自体はいいと思うのだが、コンテンツを作る側も短時間で大量のコンテンツを作ることに傾倒しているのではないかと思う。
そのため全体的にコンテンツがキャッチーさによってしまい質が低下しているように感じた。

30days challengeはそれ自体が、何かを達成したり、習慣化するという点で完結するのだが、その活動結果を短くわかりやすくまとめれれば、短時間だが質の高いコンテンツになるのではないかと思っている。
例えば、誰かが何かに一生懸命取り組んだ30日間は数分で作った動画よりもコンテンツとしては価値が高いのではないかと思う。



チャレンジしたいことリスト

色々と構想はあるが、基本的には自分が使いたいためのアプリを作っているので完成したらやることリストを挙げておく。

腹筋ローラー

まさにこんな感じの成長ストーリー。59日かかってるけどw
www.youtube.com

ギターとかピアノとか

一番相性良さそうなのはこの辺かなと。
まあ特に弾きたい曲はないが。

30日連続で写真撮る

まあ30日と言わず毎日撮るのは変化があって面白いんじゃないかと思う。
家族で毎日写真撮るとかね。

実はこのアプリ自体も

10daysのハッカソンと言いつつも、iOS申請とかQAとか色々あるからきっとこれも30days challengeの一つになるんじゃないかと予想。



開発

ここからは開発のお話。
構成としては、RailsAPI書いて、クライアントはFlutterで作ろうかなと思っている。
画像系はS3とかに適当に送ろうかなと。画像や動画の加工はまだ全く未定。

今の所、ざっくりとしたAPIだけ完成。
github.com



今後

実は、30days challengeからさらに発展したビジネスモデルも考えていたりするけどまずは一旦MVPを作ってリリースしてから考える予定。

2019年も3ヶ月終わったので年始に立てたの目標を振り返る

2019年も3ヶ月が終わり。。。

年始に立てた目標は3ヶ月くらいの周期で振り返った方がいいですね、と。
2019 Objective - kenjiszk

その1

2019.12.31までに英語力を一人前にする - kenjiszk
英語は、mikanを1~2月と続けていたが最近停滞気味。
単語を覚え続けても全く喋れる雰囲気を感じていたところ。
4月から時間をしっかり作ってレアジョブを再開しようかな。

その2

2019年は月に最低一本は渾身のブログを書く - kenjiszk
2月に書きすぎた反動で、今月はこのブログによりなんとか体裁が整う。。。
4月からは計画的に。

その3

2019年中にAIを利用したアプリケーションを作る - kenjiszk
Kaggleを初めて見た。AIだけじゃないけど、広くデータ分析的な。

番外編

ボルダリングにはまっている。
単純に面白いのではまっているのだけど、この辺に英語の勉強とかをちゃんと継続できる秘密がありそうなのでちゃんと考えて見た。

ハマりポイント1

最初の1~3回での成長率が高い気がする。
男性であれば、一番簡単な級はおそらく大抵上れる。そのあと力だけで登れないゾーンが出てくる。
次に行くまでに、人にコツを聞いたりyoutubeを見たりすると前回登れなかったところが意外とサクッといけたりする。
3度目くらいになると筋肉も多少ついてたり筋肉の使い方がわかってきたりしてさらに結構登れるようになる。

ハマりポイント2

前回登れずに終わったところが、今回の初っぱなで登れるようになってる。
ボルダリングの始めたては筋肉を異様に使うので、最後の方はほぼ腕がパンパンになっている。
そんな状態で登れずに悔しい思いで帰るのだが、次回最初にチャレンジすると腕が全回復しているので簡単に登れちゃうことがある。
この感覚が非常にクセになるポイントな気がしている。
「あ〜、これ絶対次回きたら最初にクリアできるわ〜」って思いながら帰るので次回絶対行くのである。

ハマりポイント3

最初の頃は筋肉痛が非常に痛かった。
が、筋肉痛が達成感に変わる気がしている。
逆にあんまり筋肉痛にならなかった時には、いまいち頑張らなかったのかなーという気すらする。
(もしかしたら、さらに上達すると筋肉痛はそんなにこないのかもしれないが)

筋肉痛が引かないとジムに行けないというのも、ソシャゲの体力回復待ち、みたいな感じもする。

ハマりポイント4

一緒に行くひとと、同じくらいのレベルで一緒に成長する人の存在はかなり重要な気がしている。
まあ、これは自明ですかね。

まとめ

ということで、ボルダリングは勝手に継続的にはまってますが、この要素を英語の学習に取り入れていこうかなーというのを4~6月の目標にしようと思います。

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をした場合にネットワークがどのようにコンセンサスをとっていくのかといったあたりを見ていけると良いかと思っています。

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

次回

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

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を作成しました。

次回

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

Rubyでブロックチェーンをできるだけ丁寧に実装して理解する(プルーフオブワーク編)

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

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

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

プルーフオブワークとは?

第4回で確認したトランザクションの生成からブロックチェーンへのブロックの追加までのフローを振り返って見ます。

  • 各ノードは、送金のトランザクションを作成し、ネットワーク上に伝播させる
  • マイナーと呼ばれるネットワーク管理者達はこのトランザクションをひとまとめにしたブロックを作成する
  • マイナーはブロックに含まれているトランザクションが正しいことを証明した後に数学的な問題を解く
  • ネットワークの中で一番最初にこの問題をといたマイナーがこのブロックをマイニングしたことになり報酬を受け取る
  • マイニングされたブロックはネットワーク上に伝播され、各マイナーがこのブロックの整合性を確かめる
  • 次のブロックにはこのマイニングされたブロックのデータがハッシュ化されて保存される

第4回では、このフローの問題を解くところ(プルーフオブワーク)を扱っていませんでした。5回目はここを実装していきましょう。


プルーフオブワークの実装内容

プルーフオブワークは次のような流れで行われます。

  • ブロック内のデータ + nonceと呼ばれる数字を結合する
  • それのSHA256ハッシュを求める
  • このハッシュがあらかじめ決められているターゲットよりも小さくなるまでnonceの値を変化させていく
  • そうなるようなnonceが発見できたらそこで終了
  • nonceをブロックに書きこむ

ということでproof_of_work.rbを書いていきます。

class ProofOfWork
  def initialize(block)
    @target_block = block
    @target_bits = 20
    @target = set_target
  end

  # ハッシュの計算
  # 1をビットシフトして左に移す回数を調整することで難易度を調整する
  # target_bitsを多くすることで、シフトの回数を減らす(=数が小さくなる)ため、それ以下のハッシュ値を見つける確率が小さくなっていく
  def set_target
    (1 << (256 - @target_bits)).to_s(16)
  end

  # nonce_limitに達するか、ハッシュ値がtarget以下になるまでハッシュ値の計算を繰り返す
  # 対象のnonceが求められたらブロックに格納して終わり
  def calculate(nonce_limit)
    (1..nonce_limit).each{|nonce|
      hash = get_hash(nonce.to_s)
      if (hash.hex < @target.hex)
        @target_block.nonce = nonce
        return true
      end
    }
    false
  end

  # PoWが正しいかどうかを後から確かめるための関数
  def validate
    hash = get_hash(@target_block.nonce.to_s)
    hash.hex < @target.hex
  end

  def get_hash(nonce)
    headers = @target_block.prev_block_hash.to_s + @target_block.transactions_hash.to_s + @target_block.timestamp.to_s + nonce.to_s
    Digest::SHA256.hexdigest headers
  end
end

ブロックにnonceを保存する必要があるのでBlockクラスのプロパティにnonceを追加します。

class Block
  # nonce追加
  attr_accessor :timestamp, :transactions, :prev_block_hash, :hash, :nonce
  def initialize(timestamp, transactions, prev_block_hash)
    @timestamp = timestamp
    @transactions = transactions
    @prev_block_hash = prev_block_hash
    @hash = nil
    # nonce追加
    @nonce = nil
  end

また、ブロックに含まれている全てのトランザクションIDのハッシュ値をPoWに利用するためにblock.rbに以下のメソッドも追加します。

  def transactions_hash
    transaction_ids = @transactions.map{|transaction| transaction.id}
    Digest::SHA256.hexdigest transaction_ids.join
  end

最後に、blockchain.rbのcreate_blockでPoWの計算をさせ、add_blockでは検証を行わせます。

require './proof_of_work.rb'

# ~ 中略 ~

  def create_block(transactions)
    db = Database.new
    last_hash = db.restore("last_hash")
    block = Block.new(Time.now.to_i, transactions, last_hash)
    block.set_hash
    # ブロック作成前にPoWをし、nonceを格納する
    pow = ProofOfWork.new(block)
    if pow.calculate(10000000)
      add_block(block)
    else
      p 'Failed to get nonce.'
    end
  end

  def add_block(block)
    # ブロックをブロックチェーンに入れる前にPoWが正しいか確認する
    pow = ProofOfWork.new(block)
    if pow.validate
      db = Database.new
      db.save("last_hash", block.hash)
      db.save(block.hash, block)
    else
      p 'This block is invalid in PoW.'
    end
  end

ただ、ここで問題になるのは最初のブロックであるgenesis blockではPoWをおこなっていないのでadd_blockで失敗することになる点です。そのため今回はgenesis blockだけはPoWをしないように次のように書き換えてしまいます。

  def add_block(block)
    pow = ProofOfWork.new(block)
    # ジェネシスブロックであればPoWのチェックはせずにブロックを保存する
    if is_genesis_block(block) || pow.validate
      db = Database.new
      db.save("last_hash", block.hash)
      db.save(block.hash, block)
    else
      p 'This block is invalid in PoW.'
    end
  end

  # ジェネシスブロックのチェックはもっと厳密にやる必要があるが今はテストなので書き込まれているunlocking_scriptとしている
  def is_genesis_block(block)
    block.transactions[0].inputs[0].unlocking_script == "This is first transaction"
  end

以上でPoWの実装は終わりです。いつものようにsample_blockchain.rbを実行すると今まで通り処理が実行されトランザクションが保存されることがわかります。
ただし、PoWを行うので処理待ち時間が増えていることと思います。
また、実際にRedisの中身を見ればBlockにnonceが格納されていることがわかります。

マイニング報酬

このように、プルーフオブワークを行いますが最後に、一番最初にnonceを求めたノードに報酬を支払う必要があります。この処理を実装していきます。
まずは、blockchain.rbのcreate_blockに誰がブロックを作ったか?という情報を入れいます。

  # minerという引数を追加し、create blockをした人に対して報酬が与えられるようにします。
  def create_block(transactions, miner)
    # もともとあるトランザクションにminerあてのcoinbase(inputsを持たない報酬用のトランザクション)を作ります。
    transactions.push create_coinbase(miner)
    db = Database.new
    last_hash = db.restore("last_hash")
    block = Block.new(Time.now.to_i, transactions, last_hash)
    block.set_hash
    pow = ProofOfWork.new(block)
    if pow.calculate(10000000)
      add_block(block)
    else
      p 'Failed to get nonce.'
    end
  end

  # ~ 省略 ~

  # create_blockで呼び出されているcreate_coinbase
  # inputsにはcoinbaseであることを示す情報だけを入れ、outputsにマイニング報酬を設定する
  # 本来はブロック高によってマイニング報酬は変化するがここでは一定値とする
  def create_coinbase(address)
    input = Input.new(nil, nil, 'coinbase')
    output = Output.new(25, address)
    Transaction.new(nil, [input], [output]).set_id
  end

以上で終わりです。簡単ですね。
実行結果が以下になります、Alisがマインング報酬の25コインを受け取れるようになりました。

"Alis's balance : 995"
"Bob's balance : 30"
"Carol's balance : 0"
"create new block with valid new transactions"
"Alis's balance : 1010"
"Bob's balance : 40"
"Carol's balance : 0"

まとめ

ブロックの作成時にPoWを行うようにしました。
ブロックを作成した人に対して報酬が支払われるようにしました。

次回

今回までで、ブロックチェーンの基礎となる部分の実装は完了しました。
これまでは、単体のノードとして全機能を実装してきましたが、次回からは複数ノードを前提として実装を進めていきたいと思います。

ビットコインとブロックチェーン:暗号通貨を支える技術

ビットコインとブロックチェーン:暗号通貨を支える技術