Rubyでブロックチェーンをできるだけ丁寧に実装して理解する(プルーフオブワーク編)
Rubyでブロックチェーンを実装する
第5回目です。
第1回はウォレットを作成して秘密鍵・公開鍵・ビットコインアドレスを発行してみました。
第2回は単純なトランザクションについて実装してみました。
第3回はトランザクションに対する署名を実装しました。
第4回はトランザクションをブロックに格納し、ブロックチェーンを作りました。
全てのソースコードはgithubに公開しています。
github.com
前提
過去4回の実装が終わっていることを前提とします。
Rubyでブロックチェーンをできるだけ丁寧に実装して理解する(ウォレット編) - Work Records
Rubyでブロックチェーンをできるだけ丁寧に実装して理解する(トランザクション編) - Work Records
Rubyでブロックチェーンをできるだけ丁寧に実装して理解する(署名編) - Work Records
Rubyでブロックチェーンをできるだけ丁寧に実装して理解する(ブロック編) - Work Records
プルーフオブワークとは?
第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を行うようにしました。
ブロックを作成した人に対して報酬が支払われるようにしました。
次回
今回までで、ブロックチェーンの基礎となる部分の実装は完了しました。
これまでは、単体のノードとして全機能を実装してきましたが、次回からは複数ノードを前提として実装を進めていきたいと思います。