Rubyでブロックチェーンをできるだけ丁寧に実装して理解する(ブロック編)
Rubyでブロックチェーンを実装する
第4回目です。
第1回はウォレットを作成して秘密鍵・公開鍵・ビットコインアドレスを発行してみました。
第2回は単純なトランザクションについて実装してみました。
第3回はトランザクションに対する署名を実装しました。
全てのソースコードはgithubに公開しています。
github.com
前提
過去3回の実装が終わっていることを前提とします。
Rubyでブロックチェーンをできるだけ丁寧に実装して理解する(ウォレット編) - Work Records
Rubyでブロックチェーンをできるだけ丁寧に実装して理解する(トランザクション編) - Work Records
Rubyでブロックチェーンをできるだけ丁寧に実装して理解する(署名編) - Work Records
トランザクションをひとまとめにするブロック
第4回目にしてやっとブロックチェーンぽくなってきた気がします。ちょっと下準備が長かったです。
第3回までで実装してきたトランザクションを事実上改竄不能にしていくのがブロックチェーンであり、プルーフオブワークとなります。
簡単にその仕組みを説明します。
- 各ノードは、送金のトランザクションを作成し、ネットワーク上に伝播させる
- マイナーと呼ばれるネットワーク管理者達はこのトランザクションをひとまとめにしたブロックを作成する
- マイナーはブロックに含まれているトランザクションが正しいことを証明した後に数学的な問題を解く
- ネットワークの中で一番最初にこの問題をといたマイナーがこのブロックをマイニングしたことになり報酬を受け取る
- マイニングされたブロックはネットワーク上に伝播され、各マイナーがこのブロックの整合性を確かめる
- 次のブロックにはこのマイニングされたブロックのデータがハッシュ化されて保存される
このような仕組みになっているので、過去の一部のブロックを改変しようとしてもそれよりも新しいブロックに対して矛盾が生じてしまうため、改竄したブロックから現在まで続いているブロックを全て改竄していく必要が生じます。
ただし、その過程で数学的な問題を解くことを毎回求められるので事実上改竄不可能になっている、という仕組みです。この数学的問題を解いたことをプルーフオブワークと呼んでいます。
第4回では、プルーフオブワークの一歩手前まで、今までトランザクション単体で扱っていたものをブロックチェーンに乗せていくところまでを実装します。
ブロックの生成
ジェネシスブロックの生成
ということでまずは、ブロッククラスをblock.rbとして実装します。
ブロックには、タイムスタンプと一つ前のブロックのハッシュ、含まれるトランザクションが格納されています。
ブロックのハッシュを求めるためにset_hashメソッドも追加してあります。
require 'digest' class Block 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 end # ブロックのハッシュは前回のハッシュとタイムスタンプのSHA256ハッシュで求める def set_hash @hash = Digest::SHA256.hexdigest(Time.now.to_s + @prev_block_hash) end end
次に、blockchain.rbを作成して見ます。
ここではまず、最初のブロックであるgenesis_blockの作成までできるようにします。最初のブロックには最初のトランザクションだけが含まれます。
require './block.rb' require './database.rb' class Blockchain def initialize end # 最初のブロックを作成する処理 def create_genesis_block(wallet) transaction = create_first_transaction(wallet) block = Block.new(Time.now.to_i, [transaction], "") block.set_hash add_block(block) end # 最初のブロックに含まれる最初のトランザクションを作成する処理 def create_first_transaction(wallet) input = Input.new(nil, nil, 'This is first transaction') output = Output.new(1000, wallet.address) Transaction.new(nil, [input], [output]).set_id end # ブロックを保存する処理 # last_hashをキーにブロックのハッシュを保存する事で最新のブロックを取得 # ブロックのハッシュをキーにしてブロック本体も保存する。 # ハッシュがわかればブロック本体を取得できるようになるので、last_hashからgenesis_blockまでを辿れる def add_block(block) db = Database.new db.save("last_hash", block.hash) db.save(block.hash, block) end end
実際に、ブロックチェーンを作成して保存してみます。
sample_blockchain.rbというscriptを用意しました。
require './wallet.rb' require './blockchain.rb' require './database.rb' # この辺りはウォレットの下準備 addresses = { :Alis => ENV['WALLET1'], :Bob => ENV['WALLET2'], :Carol => ENV['WALLET3'] } wallets = {} addresses.each do |name, address| wallets[name] = Wallet.new wallets[name].load(address) p "Load #{name}'s wallet : #{address}" wallets[name].address end # ここまではウォレットの下準備 # Redisからlast_hashをkeyにしてロードしてみる db = Database.new begin last_hash = db.restore("last_hash") rescue StandardError # ロードできなかった場合には、まだブロックチェーンがないので最初のブロックを作成する p 'Blockchain not found. Create Genesys Block.' blockchain = Blockchain.new blockchain.create_genesis_block(wallets[:Alis]) last_hash = db.restore("last_hash") end # ハッシュを表示する p last_hash
実行結果は以下の通りとなり、初回でblockchainを作成し、2回目からはしっかりロードできています。
1回目 $ ruby sample_blockchain.rb "Load Alis's wallet : 1E9FK4nG76SXBEBGn4CuvXH5M6safhPxzy" "Load Bob's wallet : 16RcHr8G4ieX1jSrGNa9BPdve9FoDPM3EG" "Load Carol's wallet : 1BmSusXkvbbHT1ikJQaQgePPo2wYyPHra1" "Blockchain not found. Create Genesys Block." "ba4228c9429c3be9b794ae437a046336f9a54f2eff2768374ed30e2b525c5715" 2回目 $ ruby sample_blockchain.rb "Load Alis's wallet : 1E9FK4nG76SXBEBGn4CuvXH5M6safhPxzy" "Load Bob's wallet : 16RcHr8G4ieX1jSrGNa9BPdve9FoDPM3EG" "Load Carol's wallet : 1BmSusXkvbbHT1ikJQaQgePPo2wYyPHra1" "ba4228c9429c3be9b794ae437a046336f9a54f2eff2768374ed30e2b525c5715"
genesis_blockの中身を見てみるとしっかり最初のトランザクションが入っていることがわかります。sample_blockchiain.rbに以下を足して実行してみます。
genesis_block = db.restore(last_hash) p genesis_block.transactions[0].inputs[0].unlocking_script
"This is first transaction"という文字が出力されました。
"ba4228c9429c3be9b794ae437a046336f9a54f2eff2768374ed30e2b525c5715" "This is first transaction"
トランザクションの管理方法を変更する
冒頭で書いた通り、ブロックの中にトランザクションが格納されて保存される形式になるので、トランザクションの管理方法を変える必要が出てきます。
管理方法を変える結果、次のような二つの状態のトランザクションが存在することになります。
実装でもこの二つの管理を別におこなっていくことにします。
この修正により、transactions.rbのload_allの処理は、単純にtransactionsをRedisから呼び出す処理の他に、各ブロックに入っているトランザクションを読み込んでいく必要があります。
transactions.rbを修正してみます。
class Transactions # mem_poolを追加する attr_accessor :all, :mem_pool # mem_poolを追加する def initialize() @all = [] @mem_pool = [] @key = "transactions" end def load_all db = Database.new # key=transactionのものはmem poolとして扱います begin @mem_pool = db.restore(@key) rescue StandardError # 取得できない時(mem poolが空の時)は空配列を代入 @mem_pool = [] end # mem poolのものをallに入れる @all = Marshal.load(Marshal.dump(@mem_pool)) # mem poolの後にブロックチェーン上に保存されているトランザクションデータも収集します last_hash = db.restore('last_hash') # prev_block_hashをもとにジェネシスブロックまで遡る while last_hash != '' do last_block = db.restore(last_hash) # 取得したトランザクションをallに追加していく @all += last_block.transactions last_hash = last_block.prev_block_hash end end
また、blockchain.rbのなかでジェネシスブロックを作成するようになったのでcreate_first_transactionメソッドは削除します。
では、ブロックチェーンからデータが読めているかどうかをsample_blockchain.rbに以下を追加して試してみます。
# 既存のトランザクションをロードする transactions = Transactions.new transactions.load_all wallets.each do |name, wallet| p "#{name}'s balance : #{transactions.balance(wallet.address)}" end
出力結果。しっかりブロックチェーンに保存されていたジェネシスブロックからAlisの1000コインを取得できています。
"ba4228c9429c3be9b794ae437a046336f9a54f2eff2768374ed30e2b525c5715" "This is first transaction" "Alis's balance : 1000" "Bob's balance : 0" "Carol's balance : 0"
2つめ以降のブロックを作成する
ジェネシスブロックができて、トランザクションをブロックチェーンの中から取得できるようになったので2つ目以降のブロックを作成したいと思います。
まずは、格納するトランザクションを作ってみます。ここでは、AlisがBobとCarolに1000コインづつ渡す事を考えてみます。当然これはエラーにならないといけません。
例のように、まずはsample_blockchain.rbを変えていきます。
# 二つ目のブロックに格納するトランザクションを作る new_transactions = [] new_transactions.push wallets[:Alis].pay(wallets[:Bob].address, 1000) new_transactions.push wallets[:Alis].pay(wallets[:Carol].address, 1000)
これらのトランザクションのis_valid?を検証するために、このトランザクションたちをmem poolに格納する必要があります。
そのための処理をtransactions.rbに作成します。また、全てのトランザクションを保存することはなくなったので、saveメソッドは削除します。
def add_to_mem_pool(transaction) db = Database.new # 作成されたトランザクションをmem poolに追加する begin @mem_pool = db.restore(@key) rescue StandardError @mem_pool = [] end @mem_pool.push transaction db.save(@key, @mem_pool) end
また、トランザクションの検証メソッドであるis_valid?については検証毎にトランザクションの集合を更新するようにします。
def is_valid? transactions = Transactions.new transactions.load_all # この行を追加してトランザクションを最新にする。
最後に、sample_blockchain.rbでトランザクションの検証を入れてみます。
new_transactions.each do |transaction| if transaction.is_valid? # トランザクションが有効であればmem poolに追加していく transactions.add_to_mem_pool transaction end end
それでは、sample_blockchain.rbを実行してみます。
"ba4228c9429c3be9b794ae437a046336f9a54f2eff2768374ed30e2b525c5715" "This is first transaction" "Alis's balance : 1000" "Bob's balance : 0" "Carol's balance : 0" "!!! This output is already spent !!!"
すると、このようにCarolに1000コインを付与しようとするところでトランザクションが有効ではないことが検知されてしまいました。
ブロックが有効でない場合には、全てのmem poolを破棄して処理を終わらせることにします。
transactions.rbにdelete_mem_poolメソッドを生やし
def delete_mem_pool @mem_pool = [] db = Database.new db.save(@key, @mem_pool) end
sample_blockchain.rbのなかでis_valid?がfalseの時に呼び出すようにします。
new_transactions.each do |transaction| if transaction.is_valid? transactions.add_to_mem_pool transaction else # ブロックの中のトランザクションが不正だった場合には全ての検証中のトランザクションを破棄 transactions.delete_mem_pool break end end
では最後に、トランザクションが有効だった場合にブロックを作成してブロックチェーンに追加する処理を書きます。
まずば、blockchain.rbにcreate_blockメソッドを作ります。
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 # ブロックチェーンに追加 add_block(block) end
これを、sample_blockchain.rbから呼び出します。
if transactions.mem_pool.count > 0 p 'create new block with valid new transactions' blockchain = Blockchain.new # mem_poolにあるトランザクションを新しいブロックとして追加する blockchain.create_block(transactions.mem_pool) # ブロックとして追加したのでmem_poolからは削除する transactions.delete_mem_pool end # 最後に残高を出力 wallets.each do |name, wallet| p "#{name}'s balance : #{transactions.balance(wallet.address)}" end
また、トランザクション自体を有効なものに変えます。AlisがBobに100だけ渡すようにします。
new_transactions = [] new_transactions.push wallets[:Alis].pay(wallets[:Bob].address, 100)
では実行してみます。
"Alis's balance : 1000" "Bob's balance : 0" "Carol's balance : 0" "create new block with valid new transactions" "Alis's balance : 900" "Bob's balance : 100" "Carol's balance : 0"
AlisからBobに100コインが移動しました。
では次はこのようなトランザクションにして見ます
new_transactions.push wallets[:Alis].pay(wallets[:Bob].address, 100) new_transactions.push wallets[:Bob].pay(wallets[:Carol].address, 100)
2つのトランザクションが1ブロックで処理できていることがわかります。
"Alis's balance : 900" "Bob's balance : 100" "Carol's balance : 0" "create new block with valid new transactions" "Alis's balance : 800" "Bob's balance : 100" "Carol's balance : 100"
ちょっと長くなってきたので今回はここまでにします。