Work Records

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

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

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

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

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

トランザクションをひとまとめにするブロック

第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"

ちょっと長くなってきたので今回はここまでにします。

まとめ

今まで扱ってきたトランザクションをブロックに格納しました。
また、ブロックを数珠つなぎにしたブロックチェーンを実装しました。