aidemyのブロックチェーン講座をrubyでやってみた

GW研究第二弾はみんな大好きaidemyのブロックチェーン講座をrubyで書いてみました。ゴールデンウィークが思いの外忙しくあまり記事が書けていないですが、ここから挽回していきたいと思います。

いやー、本当にこの講座は素晴らしいですね。僕は無料のときにほとんどやったのですが、このコースが一番わかりやすかったですね。どうしてもブロックチェーンを自分で一から書くとなると敷居が高く、だからと言って書かないといつまで経っても実装できないので素晴らしいコースでした。

このコースは現在も無料公開中なのでぜひみなさんも勉強されたらいいのではないでしょうか。

実装

わざわざ僕が説明しなくてもaidemyの講座の方がわかりやすいので、ブロックチェーンについての説明そちらに譲ります。コードだけにしようかと思ったのですがそれではさすがに味気ないので、python→rubyの段階で比較的ややこしかったところやrubyとpythonのいいところをそれぞれ書いてみようかなと思います。

以下にコードを記します。これは、ブロックチェーン講座の最初に実行するものをrubyに書き換えたものになります。元のpythonコードはaidemyを参照してもらえたらと思います。

require 'digest/sha2'
require 'json'

def calculate_hash(block)
  Digest::SHA256.hexdigest(block.to_json)
end

def proof_of_work(blockchain, previous_hash)
  nonce = 0
  while true
    block = blockchain.create_block(nonce, previous_hash)
    guess_hash = calculate_hash(block)
    break if guess_hash[0..3] == "0000";
    blockchain.chain.pop()
    nonce += 1
  end
  return block
end

def mine(blockchain)
  previous_hash = calculate_hash(blockchain.chain.last)
  proof_of_work(blockchain, previous_hash)
  blockchain.current_transactions = []
  blockchain.create_transaction('0', 'my_address', 1)
  block = blockchain.chain.last
  response = {
      message: '新しいブロックを採掘しました',
      index: block[:index],
      nonce: block[:nonce],
      previous_hash: block[:previous_hash],
  }
end

def create_transactions(blockchain, values)
  required = [:sender, :recipient, :amount]
  return 'valueの形式が正しくありません。' if required.none?{|key| values.has_key?(key)}
  transaction_index, block_index = blockchain.create_transaction(values[:sender], values[:recipient], values[:amount])
  response = {masseage: "トランザクションはブロック #{block_index}#{transaction_index}番目 に追加されました"}
  return response
end

def chain(blockchain)
  response = {
    chain: blockchain.chain,
    length: blockchain.chain.length,
  }
  return response
end

class Blockchain
  attr_accessor :chain, :current_transactions # rubyでは必要
  def initialize
    @chain = []
    @current_transactions = [] # transaction_index不要説
    create_transaction('0', 'my_address', 1)
    proof_of_work(self, previous_hash = "00000")
  end

  def create_block(nonce, previous_hash)
    block = {
      index:@chain.length,
      timestamp: Time.now,
      transactions: @current_transactions,
      nonce: nonce,
      previous_hash: previous_hash,
    }
    @chain << block
    return block
  end

  def create_transaction(sender, recipient, amount)
    transaction = {
      transaction_index: @current_transactions.length,
      sender: sender,
      recipient: recipient,
      amount: amount,
    }
    @current_transactions << transaction
    return @current_transactions.length - 1, @chain.length
  end
end

blockchain = Blockchain.new

values = {
    sender: "ishikawa",
    recipient: "kawai",
    amount: 10
}

# 1.マイニング
p mine(blockchain)

# 2.マイニング
p mine(blockchain)

# 3.トランザクション生成
p create_transactions(blockchain, values)

# 4.トランザクション生成
p create_transactions(blockchain, values)

# 5.マイニング
p mine(blockchain)

# 6. 結果の出力
p chain(blockchain)

やったことのある人なら分かると思うのですが、本家とほとんど一緒で本当に翻訳しただけです。ちなみに実行結果は、

{:chain=>[{:index=>0, :transactions=>[{:transaction_index=>0, :sender=>"0", :recipient=>"my_address", :amount=>1}], :nonce=>40849, :previous_hash=>"00000"}, {:index=>1, :transactions=>[{:transaction_index=>0, :sender=>"0", :recipient=>"my_address", :amount=>1}], :nonce=>154816, :previous_hash=>"00006b1a0fd1ee6d3596473be5d90f150c4a329ccfb974218d84ad78952ca026"}, {:index=>2, :transactions=>[{:transaction_index=>0, :sender=>"0", :recipient=>"my_address", :amount=>1}], :nonce=>80100, :previous_hash=>"000003cc6ccc58b1e077af04602fe59c57bd7a3ba754f23745678295cf039b8c"}, {:index=>3, :transactions=>[{:transaction_index=>0, :sender=>"0", :recipient=>"my_address", :amount=>1}, {:transaction_index=>1, :sender=>"ishikawa", :recipient=>"kawai", :amount=>10}, {:transaction_index=>2, :sender=>"ishikawa", :recipient=>"kawai", :amount=>10}], :nonce=>6119, :previous_hash=>"0000b00716aeb2237e883865041a4a61cd88e8d15102abfdd8a436be05b77652"}], :length=>4}

こんな感じです。

実装する際に変更した箇所としてはtransaction_indexというインスタンス変数が必要ないのではないかなと思って削除したぐらいです。current_transactionのlengthがindexになっているので、責任を分散させるのはよくないのかなと思いました。(blockのindexの実装はそのような仕様)もし、別途管理している理由が何かあるのであれば教えてください!

rubyとpythonの違い

これといって大きな違いはないのですがいくつか仕様の違いがあって一長一短だなと感じたのでそれをまとめてみます。(書きやすさの違い)

インスタンス変数

pythonではインスタンス変数に何もしなくても外部からアクセスできるのに対して、rubyではattr_accessorを定義してやらないとアクセスできません。エラー吐かれてはじめて思い出しました。これに関してはpythonの方が便利ですね。

一方で、いちいちselfを記述するのが面倒だなと思いました。冗長になるなと。これに関しては僕がrubyに慣れているからそのように感じるのかもしれません。慣れているとスッキリして読みやすいです。

json

これに関してはrubyの方が.to_jsonだけ済むので便利だなと思いました。ただ、規格を無視してしまっているのかもしれないので要勉強だなと。

rubyには後置ifがあることもあり、全体的にコードが短くまとまった印象です。

どちらが早いのか

なんとなくpythonの方が早いイメージだったのですが、確信はありませんでした。そこで測ってみることに。time_stampが入ってると、計算時間にムラが出るので、そこは平等に両者コメントアウト。そして、どちらのコードにも時間を計測できるように工夫して実行してみました。

# ruby
"実行時間は3.045031sです。"

#python
実行時間は5.823538064956665sです。

ん?rubyの方が圧倒的に早い。何かがおかしいと思って、出力をみてみることに。

#ruby 
{:chain=>[{:index=>0, :transactions=>[{:transaction_index=>0, :sender=>"0", :recipient=>"my_address", :amount=>1}], :nonce=>40849, :previous_hash=>"00000"}, {:index=>1, :transactions=>[{:transaction_index=>0, :sender=>"0", :recipient=>"my_address", :amount=>1}], :nonce=>154816, :previous_hash=>"00006b1a0fd1ee6d3596473be5d90f150c4a329ccfb974218d84ad78952ca026"}, {:index=>2, :transactions=>[{:transaction_index=>0, :sender=>"0", :recipient=>"my_address", :amount=>1}], :nonce=>80100, :previous_hash=>"000003cc6ccc58b1e077af04602fe59c57bd7a3ba754f23745678295cf039b8c"}, {:index=>3, :transactions=>[{:transaction_index=>0, :sender=>"0", :recipient=>"my_address", :amount=>1}, {:transaction_index=>1, :sender=>"ishikawa", :recipient=>"kawai", :amount=>10}, {:transaction_index=>2, :sender=>"ishikawa", :recipient=>"kawai", :amount=>10}], :nonce=>6119, :previous_hash=>"0000b00716aeb2237e883865041a4a61cd88e8d15102abfdd8a436be05b77652"}], :length=>4}

#python
{'chain': [{'index': 0, 'transactions': [{'transaction_index': 0, 'sender': '0', 'recipient': 'my_address', 'amount': 1}], 'nonce': 102770, 'previous_hash': '00000'}, {'index': 1, 'transactions': [{'transaction_index': 0, 'sender': '0', 'recipient': 'my_address', 'amount': 1}], 'nonce': 95826, 'previous_hash': '0000923e3321a2044e0d905dffd706f2beb7f005ec929e6d00c4630b3e21477f'}, {'index': 2, 'transactions': [{'transaction_index': 0, 'sender': '0', 'recipient': 'my_address', 'amount': 1}], 'nonce': 1979, 'previous_hash': '0000c9cc9d1c7c75161efdf0218f3f007c81c8fae0e52a5ea35bd91bbbd2fa28'}, {'index': 3, 'transactions': [{'transaction_index': 0, 'sender': '0', 'recipient': 'my_address', 'amount': 1}, {'transaction_index': 1, 'sender': 'ishikawa', 'recipient': 'kawai', 'amount': 10}, {'transaction_index': 2, 'sender': 'ishikawa', 'recipient': 'kawai', 'amount': 10}], 'nonce': 269787, 'previous_hash': '00003cebce5ea1b4689f8585298f4ab6c10db3a94a3d668f8679581939f0ebc9'}], 'length': 4}

同じ計算をしているはずなのに、pythonの方がnoneceが大きくなっています。そのためにpythonの計算が遅くなっていました。

では、なぜ同じ条件で計算にズレが出てしまったのでしょう。それは、出力を見ればわかります。pythonとrubyではdictionary(rubyではhash)の出力の仕方が違います。そのため、マイニングをするときのハッシュ化の計算の入力値が同一条件なのに違う値になっていたようです。

その結果たまたまpythonの方が計算量が増えてしまったのです。結局、time_stampを無効化した意味はなかったです。笑

そこで、nonceの合計を実行時間で割って単位時間当たりの計算力を求めてみました。

#ruby
action_time = Time.now - start_time
total_nonce = 0
blockchain.chain.each{|block| total_nonce += block[:nonce] }
power = total_nonce / action_time

p "rubyの計算力は#{total_nonce}回/sです。"
#python
action_time = time.time() - start_time
total_nonce = 0
for block in blockchain.chain:
    total_nonce += block['nonce']
power = total_nonce / action_time

print(f"pythonの計算力は{total_nonce}回/sです。")

でそれぞれ実行してみると、、

#ruby
"rubyの計算力は281884回/sです。"

#python
pythonの計算力は470362回/sです。

python強すぎでしょ。笑

python勉強しないと、、

以上です。





この記事が気に入ったら、サポートをしてみませんか?気軽にクリエイターを支援できます。

これでなんとか明日もあったかいご飯が食べれそう、、ありがとうございます
7

宮川 竜太朗

コメントを投稿するには、 ログイン または 会員登録 をする必要があります。