Work Records

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

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を行うようにしました。
ブロックを作成した人に対して報酬が支払われるようにしました。

次回

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