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"

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

まとめ

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

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

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

第3回目です。

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

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

トランザクションの署名とは?

前回までの実装での問題点

トランザクションの中身を書き換えられそう

このブログで扱うのはまだ先になるが、本来のブロックチェーンのネットワークではトランザクションを作る人と、そのトランザクションを保存する人が別の人になります。そのため今の仕組みだとトランザクションを保存する人が不正し放題になることがわかると思います。
例えば、保存直前の全てのトランザクションのoutputのアドレスを自分のものにしても誰も気づけない状態です。

2重利用できそう

トランザクションの保存時に生合成のチェックを行っていないので、簡単に2重利用ができてしまいます。
例えば、create_transactions.rbで次のようにAlisがBobとCarolに1000づつ送金するような処理を書いて実行して見ます。

p 'Send 10 coin from Alis to Bob and Carol.'
transactions.all.push wallets[:Alis].pay(wallets[:Bob].address, 1000)
transactions.all.push wallets[:Alis].pay(wallets[:Carol].address, 1000)
transactions.save

すると、結果はこのように2重支払いが簡単に起きてしまいます。

"Send 10 coin from Alis to Bob and Carol."
"Alis's balance : 0"
"Bob's balance : 1000"
"Carol's balance : 1000"

今回の記事では、この辺りの問題をどう解決するかを実装していく予定です。



トランザクションの改ざんを防止するための署名を実装する

送金時に署名を追加する

これまでは、トランザクションのinput.unlocking_scriptには特に何の値も入れてきませんでしたが、ここにこのトランザクションを確かに発行したという署名を入れていきます。署名をするデータは以下のものになります。

つまり簡単にいうと、誰が誰にいくら送るか、というデータを署名してしまおうということになります。

具体的にやることは、前回作成した送金用のpayメソッドにトランザクションのサインを付加する処理を入れます。

  def pay(to, amount)

    # ~ 省略 ~

    transaction = Transaction.new(nil, inputs, outputs).set_id
  
    # 署名に使うために、トランザクションデータを複製
    sign_transaction = Marshal.load(Marshal.dump(transaction))

    # コピーしたトランザクションのinputを走査
    sign_transaction.inputs.each.with_index do |input, input_index|
      # 署名対象のトランザクションのinputに対応する、前回のトランザクションのoutputに含まれるlocking_script(アドレス)を取得する処理
      # (特定のidのトランザクションを取得するget_transaction_byメソッドは後述する)
      # このlocking_scriptをinput.unlocking_scriptに入れておく
      # この時点で、sign_transactionには上記の3つのデータが入ったことになるためこのトランザクションごと署名してしまう
      previous_transaction = transactions.get_transaction_by(input.transaction_id)
      previous_output = previous_transaction.outputs[input.related_output]
      sign_transaction.inputs[input_index].unlocking_script = previous_output.locking_script

      # そのトランザクションのhash値を自分の秘密鍵で署名する(raw_sig)
      # sign_transaction.get_hashは後述するが、トランザクション自体のハッシュ値を取得している
      group = ECDSA::Group::Secp256k1
      nonce = 1 + SecureRandom.random_number(group.order - 1)
      raw_sig = ECDSA.sign(group, self.private_key, sign_transaction.get_hash, nonce)

      # 署名データと公開鍵をバイナリにエンコードする
      encoded_sig = ECDSA::Format::SignatureDerString.encode(raw_sig)
      encoded_pub = ECDSA::Format::PointOctetString.encode(@public_key)

      # 最終的なトランザクションの署名は、エンコード済みの署名と公開鍵のセットとなる
      # 区切りに入れているのは、encoded_sigとencoded_pubの文字列をバイナリ化したもので、signatureを受け取った側が、このバイナリ文字列から復元するために入れている
      signature = [encoded_sig.length + 1].pack('C') + encoded_sig + [1].pack('C') + [encoded_pub.length].pack('C') + encoded_pub
      transaction.inputs[input_index].unlocking_script = signature
    end
    transaction
  end

transacion.rbにトランザクション全体をhash化する処理も追加します。inputsとoutputsの中身を全部足し合わせてSHA256ハッシュを取るという単純なものです。

  def get_hash
    transaction_info = self.inputs.map do |input|
      input.transaction_id.to_s + input.related_output.to_s + input.unlocking_script.to_s
    end
    transaction_info += self.outputs.map do |output|
      output.amount.to_s + output.locking_script.to_s
    end
    Digest::SHA256.hexdigest transaction_info.join
  end

transactions.rbに特定のidのトランザクションを取得するget_transaction_byメソッドを追加します

  def get_transaction_by(id)
    @all.each do |transaction|
      return transaction if transaction.id == id
    end
    return nil
  end

では、実際に署名がついているかどうか見て見ます。create_transaction.rbに次のような処理を入れ、トランザクションを表示して見ます。

p 'Send 10 coin from Alis.'
signed_transaction = wallets[:Alis].pay(wallets[:Bob].address, 1000)
p signed_transaction

しっかりと署名がunlocking_scriptに入っていることがわかります。

"Send 10 coin from Alis."
#<Transaction:0x00007f8d129ccd58 @id="c862d0dcb909d9136c2ce0a52bbd32d85eda96fa8c3844f815372f2cd93b65b2", @inputs=[#<Input:0x00007f8d129cfd00 @transaction_id="a044557e1982e8a7d94ee5430f5d0717d358627f5867580f04a38525da85e42f", @related_output=0, @unlocking_script="H0E\x02 s\xED^\xC3\xF8\xAD;W\xF2\x177?\x8F#\xA2\xEBS\xFF\xBC\xE1V\xFD\xCA>\xD9x\x18\x99\xFBk.\xE8\x02!\x00\x81\b\xAE\xB2\xB5U\xF8\xBC\xB5\xC7\xECR\xAFL@\x92\xBFOn\xCC]\xBC\t\x9Al\x15\x0FE\xBF@\xC2\x8C\x01A\x04\xEFk\x92\xD56\xE0\v\x1A\xD8\x0E\xC7\xAD\xA38u^\xA1\v\x90;MG\x1D\x1C\xC8\xCC2\x83\x892\x85(\xB9_\x9DR\x14\xAA \e\x88\x9D\x8E!&a\xB2\xC9\x90\x88\x9D\x1A\x9F4\x14\xEB\xEB\x17\x90c\xDE\xBF\xE7\xB4">], @outputs=[#<Output:0x00007f8d129cfcb0 @amount=1000, @locking_script="187GDg8Evm5AEuFMADj7JVD1Qt1NJtuEEC">, #<Output:0x00007f8d129ccd80 @amount=0, @locking_script="1LU783wbTcEc74nL2zgirXioVka6zrixyM">]>



トランザクションの署名が正しいかどうかを確認する

トランザクションを保存する前に署名の正しさを確かめる

各inputに署名データが入っているので、これを元に署名されたトランザクションが改ざんされていないかをチェックできるようになります。

まずは、unlocking_scriptから署名と公開鍵のデータを取り出す処理をtransaction.rbに書いておきます。

  def retrieve_signature_and_public_key(unlocking_script)
    # unpackすることで次のような配列データを取り出すことができる
    # [ signature.length, signature, 1byte-code, public_key.length, public_key ]
    # signatureとpublic_keyはそれぞれ複数の要素からなるので、実際のデータは以下のような感じになる(適当な値です)
    # [23, 48, 70, 2, 33, 0, 173, 93, 83, 46, 98, 50, 247, 8, 4, 217, 236, 143, 109, 210, 148, 200, 105, 97, 107, 97, 201, 75, 94, 80, 68, 233, 52, 34, 30, 93, 232, 82, 2, 33, 0, 128, 134, 19, 250, 48, 7, 139, 98, 213, 70, 246, 180, 147, 250, 165, 236, 38, 61, 255, 208, 137, 117, 92, 248, 21, 173, 9, 110, 121, 164, 164, 64]
    # 0要素目が長さを表すので、1要素目から23個のデータがsignatureのバイナリを配列変換したもの
    unpacked_unlocking_script = unlocking_script.unpack('C*')

    # 上記のルールに則り、signatureを取り出しデコードする
    signature_length = unpacked_unlocking_script[0] - 1
    unpacked_signature = unpacked_unlocking_script[1..signature_length]
    signature = unpacked_signature.pack('C*')
    dec_signature = ECDSA::Format::SignatureDerString.decode(signature)

    # 公開鍵も同様に取り出す
    public_key_length_index = signature_length + 2
    publick_key_length = unpacked_unlocking_script[public_key_length_index]
    public_key_index = public_key_length_index + 1
    unpacked_public_key = unpacked_unlocking_script[public_key_index..public_key_index+publick_key_length]
    public_key = unpacked_public_key.pack('C*')
    group = ECDSA::Group::Secp256k1
    dec_public_key = ECDSA::Format::PointOctetString.decode(public_key, group)

    # 取り出したsignatureと公開鍵を返す
    return dec_signature, dec_public_key
  end

このメソッドを利用して、トランザクションが改竄されたものでないかどうかをチェックするメソッドをtransaction.rb内に定義します。

  def is_valid?
    transactions = Transactions.new
    transactions.load_all

    # 対象のトランザクション(self)を複製
    sign_transaction = Marshal.load(Marshal.dump(self))

    # 全てのinputに含まれるunlocking_scriptを空にする
    sign_transaction.inputs do |input, input_index|
      sign_transaction.inputs[input_index].unlocking_script = nil
    end

    self.inputs.each.with_index do |input, input_index|
      # チェック対象のトランザクションのinputが参照しているoutputからlocking_script(アドレス)を取得し、空にしておいたunlocking_scriptに代入する
      previous_transaction = transactions.get_transaction_by(input.transaction_id)
      previous_output = previous_transaction.outputs[input.related_output]
      sign_transaction.inputs[input_index].unlocking_script = previous_output.locking_script

      # もともとトランザクション内に書かれていた署名と公開鍵を取り出す
      signature, public_key = retrieve_signature_and_public_key(input.unlocking_script)

      # チェック対象のトランザクションから組み立てた署名対象のデータと、このトランザクションが作られた段階で作られた署名データを付き合わせる
      # これが合致していなければ、署名されてから今までの間にトランザクションの中身が改竄されたことになる
      unless ECDSA.valid_signature?(public_key, sign_transaction.get_hash, signature)
        p 'This is invalid transaction.'
        return false
      end
    end
    true
  end

create_transaction.rbの最後にis_valid?を入れて実行してみるとトランザクションチェックがうまくいっていることがわかります。

p 'Send 10 coin from Alis.'
signed_transaction = wallets[:Alis].pay(wallets[:Bob].address, 1000)
p signed_transaction.is_valid?

実行結果。

"Send 10 coin from Alis."
true

試しに、トランザクションを改竄してみます。

p 'Send 10 coin from Alis.'
signed_transaction = wallets[:Alis].pay(wallets[:Bob].address, 1000)
# 送信先をCarolに改ざん!
signed_transaction.outputs[0].locking_script = addresses[:Carol]
p signed_transaction.is_valid?

すると、トランザクションの検証は失敗になります。

"Send 10 coin from Alis."
"This is invalid transaction."
false



2重支払いのチェックを入れる

まずは、transaction.rbのis_valid?メソッドの最後にすでに使われているinputではない事を確認する処理を入れることにします。これですでに使用済みのinputを利用した時点で処理が中断されます。

  def is_valid?

    # ~省略~

    self.inputs.each.with_index do |input, input_index|

      # ~省略~

      # 利用されているinputがすでに使われていないかをチェック
      unless transactions.unspent?(input.transaction_id, input.related_output)
        p '!!! This output is already spent !!!'
        return false
      end
    end
    true
  end

その上で、各トランザクションを保存する前にis_valid?メソッドで確認してからRedisに保存するようにcreate_transaction.rbを修正します。
以下は、冒頭で紹介した2重使用してしまう例に対して、各トランザクションごとにis_valid?を適用した例です。

p 'Send 10 coin from Alis to Bob and Carol.'
new_transactions = []
new_transactions.push wallets[:Alis].pay(wallets[:Bob].address, 1000)
new_transactions.push wallets[:Alis].pay(wallets[:Carol].address, 1000)

new_transactions.each do |transaction|
  if transaction.is_valid?
    transactions.all.push transaction
    transactions.save
  end
end

wallets.each do |name, wallet|
  p "#{name}'s balance : #{transactions.balance(wallet.address)}"
end

実行してみるとこのように、AlisからCarolに1000コイン送るときにすでに使われていることがわかり処理が止められています。
そのため、結果としてBobに1000が渡っただけの状態で2重支払いが防げています。

"Send 10 coin from Alis to Bob and Carol."
"!!! This output is already spent !!!"
"Alis's balance : 0"
"Bob's balance : 1000"
"Carol's balance : 0"



まとめ

今回は、トランザクションの署名について詳しく見てみました。
この処理によって、トランザクションが作成されてからデータベースに保存されるまでの間に何らかの改ざんがあった事が気づけるようになります。

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

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

第2回目です。
前回はウォレットを作成して秘密鍵・公開鍵・ビットコインアドレスを発行してみました。
今回はトランザクションについて実装していこうと思います。

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

前提

前回のウォレットの実装が終わっていることを前提とします。
Rubyでブロックチェーンをできるだけ丁寧に実装して理解する(ウォレット編) - Work Records

wallet.rbとdatabase.rbは実装ずみで進めます。
また、トランザクションとは?といった内容は含みません。


トランザクションを実装する

トランザクションに必要なインプットとアウトプットを実装する。

こちらがインプットクラス。
自分が所有するUTXO(Unspent Transaction Output)を指定するtransaction_idと、そのトランザクション内の何番目のアウトプットかを示すrelated_output、そのアウトプットを自分だけが使えることを証明するためのunlocking_scriptが格納されています。

class Input
  attr_accessor :transaction_id, :related_output, :unlocking_script

  def initialize(transaction_id, related_output, unlocking_script)
    @transaction_id = transaction_id
    @related_output = related_output
    @unlocking_script = unlocking_script
  end
end

こっちがアプトプットクラス。
コインの量のamountと、このアウトプットを所有者以外は利用できないようにロックするlocking_scriptが格納されています。

class Output
  attr_accessor :amount, :locking_script

  def initialize(amount, locking_script)
    @amount = amount
    @locking_script = locking_script
  end
end

トランザクションクラスを実装する。

一つのトランザクションには複数のインプットとアウトプットが格納されるので、inputs、outputsにそれぞれinputの配列とoutputの配列が格納されます。
トランザクションIDはset_idメソッドでランダムに決めることにします。

require 'securerandom'
  
class Transaction
  attr_accessor :id, :inputs, :outputs

  def initialize(id, inputs, outputs)
    @id = id
    @inputs = inputs
    @outputs = outputs
  end

  def set_id
    @id = SecureRandom.hex(32)
    self
  end
end

ウォレットを用意する

トランザクションを発生させるにはコインを所持しているウォレットが必要になります。毎回ウォレットを作成するのは面倒なので、ウォレットを3つ簡単に作成するためのscriptを用意しておきます。
create_wallet.rbという名前のruby scriptになります。

require './wallet.rb'

# Prepare 3 wallets
(1..3).each do |index|
  wallet = Wallet.new
  wallet.create_key
  puts "export WALLET#{index}=#{wallet.address}"
  wallet.save
end

内部は読むとわかるように、ウォレットを3つ作成して保存するだけのもので実行すると以下のような出力を得ます。

$ ruby create_wallet.rb 
export WALLET1=15GKumk6beswsC9CPSNt7CPbJR5FsXxDM6
export WALLET2=161XB8hb1mCifJ2pvujc7VgXBMY8uWr7aW
export WALLET3=1LgyY4USryUQbHkb66gLD5qdJy7QzyzPFE

この3行の出力は、ウォレットのアドレスを毎回覚えておく必要がないように環境変数に仕込んでおく為のものなので、コピーしてシェル内で実行してください。以下のようにしてそのステップを省略もできます。

$ `ruby create_wallet.rb` 

最初のトランザクションを作ってみる

ウォレットの準備もできたので最初のトランザクションを作ってみます。今まではirbインタラクティブに実行していましたが、今回はcreate_transaction.rbというscriptを用意して処理を追ってみます。

require './wallet.rb'
require './database.rb'
require './transaction.rb'
require './input.rb'
require './output.rb'

# WALLET1,2,3だと味気ないので、Alis, Bob, Carolのウォレットとして扱う
addresses = {
  :Alis => ENV['WALLET1'],
  :Bob => ENV['WALLET2'],
  :Carol => ENV['WALLET3']
}

# redisに保存されているウォレットの情報をloadする
wallets = {}
addresses.each do |name, address|
  wallets[name] = Wallet.new
  wallets[name].load(address)
  p "Load #{name}'s wallet : #{address}"
  wallets[name].address
end

# -----  ここまではウォレット準備のための下処理 -------

# 最初のトランザクションを作るために、Inputを用意する
input = Input.new(nil, nil, 'This is first transaction')
# 対応するoutputを作成し、Alisのウォレットのアドレスをlocking_scriptとして指定する
output = Output.new(1000, wallets[:Alis].address)
# 最初のTransactionを作成。inputとoutputを引数にとる。それぞれ配列の1つ目の要素になっている。
fist_transaction = Transaction.new(nil, [input], [output]).set_id

# 出力してみる
p fist_transaction

create_transactions.rbを実行してみると最初のトランザクションが作成されたことがわかります。

$ ruby create_transactions.rb 
"Load Alis's wallet : 19g9EFQXz92UWSpZUHxosDDGvvv5828hKz"
"Load Bob's wallet : 1FFw363PF3BCtADUTxYq2qEQ8q3zK6pUhv"
"Load Carol's wallet : 1BR3SehfHhkSn8BwZfKyhoWrU8HvjzTtuG"
#<Transaction:0x00007fce04844700 @id="2346871b607c453dfac312aa29df958dfd6d2296b8ca8939940a884aca3b04a6", @inputs=[#<Input:0x00007fce04184eb0 @transaction_id=nil, @related_output=nil, @unlocking_script="This is first transaction">], @outputs=[#<Output:0x00007fce04844278 @amount=1000, @locking_script="19g9EFQXz92UWSpZUHxosDDGvvv5828hKz">]>

トランザクションを保存する&リストアする

トランザクションを作りっぱなしでは毎回初期化されてしまうので、ウォレットの時と同じようにRedisに保存して次回は保存されているトランザクションを利用できるようにします。
そのためにトランザクション全体を管理するtransactionsクラスを作成します。まずは、transactins.rbを以下のように作ります。

require './database.rb'
  
class Transactions
  attr_accessor :all

  # 初期化処理
  # Redisにトランザクションの全体を保存するallと、Redisのkeyである"transactions"を定義する
  def initialize()
    @all = []
    @key = "transactions"
  end

  def load_all
    db = Database.new
    @all = db.restore(@key)
  end

  # 最初のトランザクションを作成する
  # 最初のトランザクションは上述のようにAlisに1000コインを渡すものとする
  def create_first_transaction(wallets)
    input = Input.new(nil, nil, 'This is first transaction')
    output = Output.new(1000, wallets[:Alis].address)
    @all.push Transaction.new(nil, [input], [output]).set_id
    db = Database.new
    db.save(@key, @all)
  end

  def save
    db = Database.new
    db.save(@key, @all)
  end
end

create_transaction.rbを以下のように修正します(ウォレットの下準備は省略)。

require './wallet.rb'
require './database.rb'
require './transaction.rb'
require './input.rb'
require './output.rb'
require './transactions.rb' # 追加

# ~ ウォレットの下準備部分は省略 ~

# トランザクションインスタンスを作成
transactions = Transactions.new
begin
  # Redisから全トランザクションの履歴を取り出す
  transactions.load_all
rescue StandardError
  # 取得が失敗した場合は、まだトランザクションが存在しないので最初のトランザクションを作成する
  p 'Load is failed. Create new transaction'
  transactions.create_first_transaction(wallets)
end

# トランザクションIDを表示
p 'List of transaction ids'
transactions.all.each do |transaction|
  p transaction.id
end

実行結果。

# 初回実行。Redisから呼び出しが失敗して新しくトランザクションを作成している
$ ruby create_transactions.rb 
"Load Alis's wallet : 19g9EFQXz92UWSpZUHxosDDGvvv5828hKz"
"Load Bob's wallet : 1FFw363PF3BCtADUTxYq2qEQ8q3zK6pUhv"
"Load Carol's wallet : 1BR3SehfHhkSn8BwZfKyhoWrU8HvjzTtuG"
"Load is failed. Create new transaction"
"List of transaction ids"
"4b5ba85f9d81e6b1e75f272986e20a27035c2dbddf93f21470fe42540396f844"

# 2回目の実行。今回はRedisからのロードに成功しているのでトランザクションは生成されていない。そのため、トランザクションのIDが同じものがプリントされている。
$ ruby create_transactions.rb 
"Load Alis's wallet : 19g9EFQXz92UWSpZUHxosDDGvvv5828hKz"
"Load Bob's wallet : 1FFw363PF3BCtADUTxYq2qEQ8q3zK6pUhv"
"Load Carol's wallet : 1BR3SehfHhkSn8BwZfKyhoWrU8HvjzTtuG"
"List of transaction ids"
"4b5ba85f9d81e6b1e75f272986e20a27035c2dbddf93f21470fe42540396f844"

自分が持っているコインの量を取得する

最初のトランザクションでWALLET1(Alis)に1000コイン付与しましたが、これを確認できるようにしたいと思います。
そのためには、まず各自のUTXOを集める必要があり、transactions.rbにcollect_enough_utxoというメソッドを追加します。
collect_enough_utxoという名前から察するように、本来は支払いに必要な分だけのutxoを取得するためのメソッドなのですが、pay_amountをnilにすることで全utxoを取得できるようにしてあります。

  def collect_enough_utxo(address, pay_amount)
    utxo = {}
    amounts = 0
    # 全てのトランザクションを走査する
    @all.each do |transaction|
      # トランザクション内に含まれるoutputとその順番を走査
      transaction.outputs.each.with_index do |output, output_index|
        # 自分がコインの所有者でそのコインが使われていない場合にutxoに追加
        if owner?(output, address)
          if unspent?(transaction.id, output_index)
            utxo[transaction.id] = [] if utxo[transaction.id].nil?
            utxo[transaction.id].push output_index
            amounts += output.amount
          end
        end
        # pay_amountが指定されている場合には、utxoの合計額がpay_amountに達した段階で処理は終わり
        # pay_amountが指定されていない場合には処理は中断しないので全ての残高が集計される
        if pay_amount != nil
          return utxo, amounts if amounts >= pay_amount
        end
      end
    end
    return utxo, amounts
  end

  # outputに含まれるlocking_scriptが自分のアドレスと一致した場合に自分の所持するoutputと判断
  def owner?(output, address)
    output.locking_script == address
  end

  # outputに含まれる情報からそれが使われたかどうかを判断する
  def unspent?(transaction_id, output_index)
    # 全てのトランザクションを走査
    # 引数に渡したトランザクションidとそのoutputに該当するinputが含まれるトランザクションが存在した場合には、それは使われていると判断する
    @all.each do |transaction|
      transaction.inputs.each do |input|
        next if input.nil?
        if input.transaction_id == transaction_id && input.related_output == output_index
          return false
        end
      end
    end
    true
  end

最後に、balanceメソッドを追加して各アドレスの残高を取得できるようにします。

  def balance(address)
    _, amounts = collect_enough_utxo(address, nil)
    amounts
  end

create_transaction.rb内に以下のコードを追加して実際に残高を表示させてみます。

# 各アドレスの残高を表示
wallets.each do |name, wallet|
  p "#{name}'s balance : #{transactions.balance(wallet.address)}"
end

実行結果。アリスの残高が1000に、他二人は0になっていることがわかります。

$ ruby create_transactions.rb 
"Load Alis's wallet : 19g9EFQXz92UWSpZUHxosDDGvvv5828hKz"
"Load Bob's wallet : 1FFw363PF3BCtADUTxYq2qEQ8q3zK6pUhv"
"Load Carol's wallet : 1BR3SehfHhkSn8BwZfKyhoWrU8HvjzTtuG"
"List of transaction ids"
"4b5ba85f9d81e6b1e75f272986e20a27035c2dbddf93f21470fe42540396f844"
"Alis's balance : 1000"
"Bob's balance : 0"
"Carol's balance : 0"

コインを送金する

トランザクションの最後は、他のウォレットにコインを送金する処理になります。つまり二つ目以降のトランザクションを追加していく処理になります。

コインを送金するとは?

例えば、Alisが持っている1000コインのうち10コインをBobに送金するとします。内部で何が起きるかというと、Alisが自分のUTXOを合計が10以上になるまでかき集めます。
今の状態だと、1000のUTXOが一つ用意されることになります。この中から10をBobに送金し、残りの990を自分に送金する処理が行われます。

送金する

AlisのUTXOを集める処理はすでにできているのでBobに送金してみます。wallet.rbにpayメソッドを追加していきます。

  def pay(to, amount)
    transactions = Transactions.new
    transactions.load_all
    # collect_enough_utxoで使用する予定のutxoを集める
    use_utxo, use_amount = transactions.collect_enough_utxo(self.address, amount)
    inputs =[]
    # 集めたutxoの中からトランザクションidと格納されている順番を取り出して次の支払いに使うためのinputを作っていく
    # 出来上がったinputから順にinputs配列にpushしていく
    use_utxo.each do |transaction_id, output_indexes|
      output_indexes.each do |output_index|
        inputs.push Input.new(transaction_id, output_index, nil)
      end
    end
    outputs = []
    # outputは2つ用意される
    # 一つはBobにamount分を送金するためのoutput
    # もう一つは、自分にお釣り分を送金するためのoutput。そのため宛先は自分のアドレスを指定している
    outputs.push Output.new(amount, to)
    outputs.push Output.new(use_amount - amount, address)
    # 最後に記録したトランザクションを返して終わり
    Transaction.new(nil, inputs, outputs).set_id
  end

create_transaction.rbに以下のサンプルコードを追加して動作確認してみましょう。

# AlisからBobに10コイン送金する
p 'Send 10 coin from Alis to Bob.'
new_transaction = wallets[:Alis].pay(wallets[:Bob].address, 10)
transactions.all.push new_transaction
transactions.save

# 各アドレスの残高を表示
wallets.each do |name, wallet|
  p "#{name}'s balance : #{transactions.balance(wallet.address)}"
end

実行結果。

$ ruby create_transactions.rb 
"Load Alis's wallet : 19g9EFQXz92UWSpZUHxosDDGvvv5828hKz"
"Load Bob's wallet : 1FFw363PF3BCtADUTxYq2qEQ8q3zK6pUhv"
"Load Carol's wallet : 1BR3SehfHhkSn8BwZfKyhoWrU8HvjzTtuG"
"List of transaction ids"
"4b5ba85f9d81e6b1e75f272986e20a27035c2dbddf93f21470fe42540396f844"

# 送金前の残高
"Alis's balance : 1000"
"Bob's balance : 0"
"Carol's balance : 0"

"Send 10 coin from Alis to Bob."

# 送金後の残高
"Alis's balance : 990"
"Bob's balance : 10"
"Carol's balance : 0"

まとめ

トランザクションクラスを定義し、一回一回のトランザクションを保存できるようになりました。
ウォレット間の送金を行えるようになりました。

何か間違いやコメントがあればお気軽に https://twitter.com/kenjiszk までご連絡いただければと思います!

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

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

これ系のブログは多いのですが、自分でも思い立ってやってみました。
ただ数あるブログは大抵の場合、簡略化した実装を行なっていることが多いと思います。
なので出来るだけ丁寧に簡略化せずにブロックチェーンというものを実装してみることにしました。
(実はこれは大分前に取り組んだもので全く別の場所で公開する予定だったのですが、諸事情によりお蔵入りしそうなので自分のブログで公開だけはしておこうというものになります)

ちなみに本当に丁寧に実装を追う予定なのでシリーズ物のブログになる予定です。
githubにも一応公開しています。多分相当長いシリーズになるはず。。。
github.com

前提

実装するのはビットコインベースのブロックチェーンにします。
実装に入る前に前提知識として、サトシナカモト論文は読んでおいてもらうと良いです。
coincheckが日本語版を公開していました。
coincheck.blog

Mastering Bitcoinも読んでおくともっと理解が早いと思います。
あと、Rubyで書いていくので当然Rubyの実行環境は必要です。このブログは2.5.1で実装します。
データストアとしてRedisを使うのでRedisもインストールが必要です。このブログではDockerでRedisを立ち上げるのでDockerが入っていれば問題はありません。
(それぞれのインストール方法は特に触れません)



ウォレットを実装する

早速、ウォレットを実装してみます。
ちなみにソースコードはこちら
https://github.com/kenjiszk/blockchain-ruby/tree/master/01

ウォレットの機能って?

まだブロックチェーンも何もない状態で、ウォレットの機能と言われると秘密鍵の発行と公開鍵の作成かと思うので、まずはそれを実装していきます。

ウォレットクラスを作る

秘密鍵(private_key)と公開鍵(public_key)を持つウォレットの容器だけ以下のように作ります。

class Wallet
  attr_accessor :private_key, :public_key

  def initialize
    @private_key = nil
    @public_key = nil
  end
end

秘密鍵を作成する

ビットコイン秘密鍵の生成は楕円曲線DSAというアルゴリズムを採用しています。幸いなことにecdsaというgemを利用することで簡単に秘密鍵と対応する公開鍵を生成することが可能です。
まずは、必要なgemをrequireします。

require 'ecdsa'
require 'securerandom'

そして、Walletクラスにcreate_keyメソッドを実装します。

  def create_key
    # 楕円曲線DSAのSecp256k1という形式をビットコインは採用している
    group = ECDSA::Group::Secp256k1
    # 秘密鍵 : 実は秘密鍵はランダムであればなんでも良いので、Secp256k1のオーダーの範囲でのランダムを取得する
    @private_key = 1 + SecureRandom.random_number(group.order - 1)
    # 秘密鍵の回数だけ楕円曲線上の演算を繰り返したものが公開鍵となる
    @public_key = group.generator.multiply_by_scalar(private_key)
  end

試しに実際に生成してみます。

irb(main):001:0> require './wallet.rb'
=> true
irb(main):002:0> w = Wallet.new
=> #<Wallet:0x00007ff8b18bc778 @private_key=nil, @public_key=nil>
irb(main):003:0> w.create_key
=> #<ECDSA::Point: secp256k1, 0x28835ad184c8fac136a52430ffadae5513c87740ebc739a4f65906b7a2f033e9, 0x78fa26ccff34b35fb2dd5a8fdf4b4ba84975bb99e7cf34a89e31d5b5f8fc9245>
irb(main):004:0> w.private_key
=> 75215084501781583769016068955682898084055049403465985929688161977210158043001
irb(main):005:0> w.public_key
=> #<ECDSA::Point: secp256k1, 0x28835ad184c8fac136a52430ffadae5513c87740ebc739a4f65906b7a2f033e9, 0x78fa26ccff34b35fb2dd5a8fdf4b4ba84975bb99e7cf34a89e31d5b5f8fc9245>

いい感じに、秘密鍵と公開鍵を生成でました。

ブロックチェーンのアドレスに変換する

公開鍵ができたら次はそれをブロックチェーンのアドレスに変換します。
このステップが長いのですが、以下のような順番で変換していきます。

ちなみに、楕円曲線上の座標は片方が決まればもう片方が決まるため、公開鍵のサイズの圧縮のため二つある鍵のx側だけを利用します。ただし、xからyを求めようとすると平方根の計算が入り、yは正負のどちらかを判別できないためその情報だけをxの先頭に付与します。具体的には、yが偶数なら02をyが奇数なら03を公開鍵の先頭に付与という感じです。

  1. xのみで構成される公開鍵を作成する
  2. SHA256ハッシュ化
  3. RIPEMD-160ハッシュ化
  4. 先頭に0x00を付加
  5. SHA256ハッシュ化を2回した後に、先頭の4バイトでチェックサムを作る
  6. チェックサムを後ろにつける
  7. Base58エンコードする

では実際に実装してみます。まずは諸々必要なものをrequire。

require 'base58'
require 'digest'

addressに変換するためのメソッドを書きます。

  def address
    # Step1. xのみで構成される公開鍵を作成する。prefixにはyの値により02か03が入る
    compressed_public_key = prefix + @public_key.x.to_s(16)
    hashed_public_key = double_hash(compressed_public_key)
    # Step4. 先頭に0x00を付加
    hashed_public_key_with_network_byte = "00" + hashed_public_key
    # Step6. 作成されたチェックサムを後ろに付加する
    row_address = hashed_public_key_with_network_byte + checksum(hashed_public_key_with_network_byte)
    # Step7. Base58エンコードする
    Base58.binary_to_base58([row_address].pack("H*"), :bitcoin)
  end

  def prefix
    if @public_key.y.to_s[-1] % 2 == 0
      "02"
    else
      "03"
    end
  end

  def double_hash(key)
    # Step2. 公開鍵のxをSHA256ハッシュ化
    sha256 = Digest::SHA256.hexdigest [key].pack("H*")
    # Step3. さらにRIPEMD-160ハッシュ化
    Digest::RMD160.hexdigest [sha256].pack("H*")
  end

  # Step5. 0x00が付けられた公開鍵を2回SHA256ハッシュ化し、先頭の8文字(4バイト)を返す
  def checksum(key)
    sha256 = Digest::SHA256.hexdigest [key].pack("H*")
    double_sha256 = Digest::SHA256.hexdigest [sha256].pack("H*")
    double_sha256[0..7]
  end

ちなみに、base58はbase64からOと0やlと1といった間違えやすい文字を抜いたもので、ビットコインアドレスの打ち間違えなどを防止する目的となっています。実際には手打ちする人はほとんどいないと思いますが。

ということで、実際にアドレスまで生成してみましょう。

irb(main):001:0> require './wallet.rb'
=> true
irb(main):002:0> w = Wallet.new
=> #<Wallet:0x00007febca16a478 @private_key=nil, @public_key=nil>
irb(main):003:0> w.create_key
=> #<ECDSA::Point: secp256k1, 0x8b1381dd6f0a3bfd9c0e71d73bb2300286008b80251d4c037e87967b506d4cf8, 0x99055c0a623fa8dc8c9927b13692d242a784417e38d6310970c9bedf96640a4a>
irb(main):004:0> w.address
=> "14TfJYFN8JPJvfjktLQSoX9pfD35WnTsDR"

無事、ビットコインのアドレスっぽいものが出来上がりました。



データストアに保存する

ウォレットが出来て一件落着、ではなく、この情報を保存しておく必要があります。
上述のようにデータストアとしてはredisを利用します。

redisをdockerで立ち上げる

dockerを利用していれば以下を打つだけでokです。

docker run -it -d -p 6379:6379 redis

データの保存と取り出し用のクラスを作る。

以下のような、Databaseクラスを作成します。これによってredisにデータを保存できるようになります。

require 'redis'
  
class Database
  def initialize
    @redis = Redis.new(host: "localhost", port: 6379, db: 01)
  end

  def save(key, data)
    @redis.set key, serialize(data)
  end

  def restore(key)
    data = @redis.get key
    deserialize(data)
  end

  # rubyのインスタンスをredisに保存するためにシリアライズを行う
  def serialize(data)
    Marshal.dump(data)
  end

  def deserialize(data)
    # redisから取得したデータをデシリアアイズしてrubyのインスタンスに戻す
    Marshal.load(data)
  end
end

Walletを実際にredisに保存する

以下のメソッドをWalletクラスに追加します。一度生成したウォレットを後から使えるようにrubyインスタンスごとredisに保存しておくことにしました。
key名は、"wallet+ビットコインアドレス"としています

  def save
    key = "wallet" + self.address
    db = Database.new
    db.save(key, self)
  end

  def load(address)
    key = "wallet" + address
    db = Database.new
    saved_wallet = db.restore(key)
    @private_key = saved_wallet.private_key
    @public_key = saved_wallet.public_key
  end

実際に動かしてみる方がわかりやすいと思うので、実際にやってみます。
その前に忘れずに、wallet.rbからdatabase.rbをrequireしておきましょう。

require './database.rb'

以下、動作サンプルです。

irb(main):001:0> require './wallet.rb'
=> true

# wというウォレットを作成し、鍵を作りaddressを表示させる
irb(main):002:0> w = Wallet.new
=> #<Wallet:0x00007fc319041fa8 @private_key=nil, @public_key=nil>
irb(main):003:0> w.create_key
=> #<ECDSA::Point: secp256k1, 0xadb3de0a53a4e16f29012f97ccf3aace3a43149fc860ea17ae5bd55d51d85d04, 0x46d47209ef23ba675c9816e2b8986de80ddc46315730f1fe27e7fd4e73f76f68>
irb(main):004:0> w.address
=> "18Grmagq3RkajF4eVUy6JbtxevUFkoEQCo"

# saveメソッドを呼び出し、redisにこのウォレットを保存する
irb(main):005:0> w.save
=> "OK"

# restored_wという別のウォレットを作成する
irb(main):006:0> restored_w = Wallet.new
=> #<Wallet:0x00007fc319131530 @private_key=nil, @public_key=nil>

# 先ほど表示させたアドレスを引数にしてredisからウォレットインスタンスをロードする
irb(main):007:0> restored_w.load('18Grmagq3RkajF4eVUy6JbtxevUFkoEQCo')
=> #<ECDSA::Point: secp256k1, 0xadb3de0a53a4e16f29012f97ccf3aace3a43149fc860ea17ae5bd55d51d85d04, 0x46d47209ef23ba675c9816e2b8986de80ddc46315730f1fe27e7fd4e73f76f68>

# アドレスを表示させると先ほど作ったアドレスと一致しているので正しくロードできていることがわかる
irb(main):008:0> restored_w.address
=> "18Grmagq3RkajF4eVUy6JbtxevUFkoEQCo"


まとめ

rubyビットコインベースのブロックチェーンを実装するにあたり、まずはウォレットを作成してみました。
ウォレットには秘密鍵を作成し、それに対応する公開鍵とビットコインアドレスの生成機能を作成しました。
また、redisにウォレットの情報を保存することを行いました。

何か間違いやコメントがあればお気軽に https://twitter.com/kenjiszk までご連絡いただければと思います!



エンジニア職と株式投資


エンジニアが株式投資を考えることについて

自分の観測範囲だけかもしれませんが、投資について話をするエンジニアは少ないように思います。最近の売り手市場感からいえば、一般平均以上は給与をもらっていると思うので、 単にそういった手の話を公にすることが憚れているだけかもしれませんが。

まあそれはどちらでもいいのですが、アメリカの時価総額ランキングの上位をほとんどがテック企業が占めている現在の状況で、エンジニア職であることは投資的観点においても非常に恵まれていると感じています。
具体的には、以下のような観点をエンジニアであれば持つことができるからです。

  • あるテクノロジーが何かの業界にブレイクスルーを起こしうるかどうか
  • 注目のサービスには実はあるコア技術が隠れている
  • 新興企業の技術力がどの程度なのか

また、別観点で言えばエンジニアは常にスキルをアップデートしていく必要があり、なんの技術を習得するか?ということがある意味では自身に対しての投資になるので、そこの技術選定を正しく行うためにもビジネスとテクノロジーの関連については敏感になっておく必要があるとも思います。


NVIDIA

テクノロジーと株価の関係が非常にわかりやすいのがNVIDIAだろうと思います。それこそ、テクノロジーを知らなければ謎の半導体メーカーなどと呼んでしまいます。

直近5年の株価を見てみると、素晴らしいくらいの右肩上がりと2018年の暴落になっています。
f:id:kenjiszk:20190114003207p:plain

右肩上がりなのは良いですが、肝心なのはテクノロジーを理解していれば、このNVIDIA株を値段が上がる前に買えるかどうか?という点になります。

第一参入期 AlphaGo

NVIDIAGPUの会社なので、もともとはゲームなどグラフィックを処理するための特殊なプロセッサを作っていました。ところが、このGPUディープラーニングにも利用できることがわかったのが最初の参入期になります。

詳細は省きますが、AI技術の実用化の一つのブレイクスルーがディープラーニングという手法が確立したことですが、このディープラーニングの計算を可能にしたのが、多重の行列計算を現実的な時間内で完了できるGPU、という関連です。

このディープラーニングが世間の注目を集め、実用化ができてきたなという出来事といえばAlphaGoではないかと思います。

Wikipediaより。

2016年3月、韓国棋界で「魔王」と呼ばれる世界トップ棋士の一人の李世乭と戦い、4勝1敗と勝ち越した。

gigazine.net

つまり2016年3月時点で以下の考察ができます。

特に技術が理解できる、技術について調べることができる能力があれば、三つ目以降の考察も可能ではないかと思います。
この時期に参入していれば将来$400近くまで上昇する株が$40程度で買えたことになります。

とはいえ、結果から考察しているだけなのでそんなに簡単な話ではないですが、技術を知っていれば確度が高くなることは間違い無いと思います。

第二参入期 仮想通貨

二度目の参入時期は、仮想通貨ブームではないかと思います。つまり、2017年の初頭から中頃まで。bitcoinの3年チャートを見るとこんな感じ。
f:id:kenjiszk:20190114185736p:plain

こんな感じで考えられていればこの時期にNVIDIA株に注目できていたと思います。

  • 仮想通貨がなんとなく流行っているっぽい
  • ほとんどの通貨はPoWという手法を利用している
  • PoWには多くの計算が必要でそこにGPUが利用されている

ということで、ここがNVIDIA株参入の第二期目だったかなと思います。

ちなみに面白いのは、bitcoinの価格ではなくPoWに費やされている計算量(hashrate)とNVIDIAの株価の方が相関があるという点ではないでしょうか。仮想通貨の価格が暴落した2018年1月ではまだNVIDIA株は落ちずに保っています。
f:id:kenjiszk:20190114192428p:plain

つまり、

  • 仮想通貨全体が冷え込む
  • マイニングによる利益が出ないラインまで価格が落ち込む
  • GPUの需要が減り、NVIDIA株が下がる

といったストーリーになります。

ただし大局も見ないといけない

一応触れておくと、アメリカ株は2017年から2018年末にかけて、特にテック系株は順調だったのでそういった影響もあったとは思います。
f:id:kenjiszk:20190114193252p:plain
こういった全体感はちゃんと押さえておかないと参入時期を間違えるので一応は押さえておいた方がいいです。


では次は?

NVIDIAの例を元に見てきましたが、結局大事なのは、じゃあ今何を買えばいいの?という点かと思います。

VR / Oculus Go

いくつかあると思いますが個人的に考えているのは、VRです。VRについては何年も前から実用化に向けて色々な取り組みがありましたし、色々なアイディアを耳にしてきましたが、相当実用化してきたなと感じたのが、Oculus Goでした。
gigazine.net

まず非常に価格が安いことと、実際使ってみると多少の画質の悪さはあるものの十分コンテンツとして楽しめるなという感じを受けました。
AlphaGoがプロ棋士に勝ったことや、仮想通貨が流行り始めた事と同種の出来事ではないかと思います。

ここからがエンジニア的に考えないといけないところだと思いますが、VRを実現するために大切なものは小型GPUと5Gかなと考えています。

小型GPU

Oculus Goを見てわかるようにVRバイス自体がかなり小型化していますし、今後普及するためにはもっと小型にしていく必要がありそうです。一方で、VRコンテンツは当然非常に多くのグラフィック処理が必要になるので、高性能で小型のGPUを作ることができる技術が重要になります。

5G

今後、VRバイスが単独でネットワーク接続できないことはあり得ないので、非常に大量のVRコンテンツを通信するための5Gも注目したいところです。5G技術自体はすでに確立していてどう広まっていくかの段階なので、必要なチップや、基地局系の企業が注目されるのではないでしょうか。

まとめ

エンジニアならではの視点で、成長する技術について着目できるのではないかと考えこのブログを書いてみました。

ただ、一方で技術に対するアンテナを高くキープする一つのインセンティブとしての株式の売買益ってのはありな気はします。どんな目的にしろ最新技術をキャッチアップし、技術と社会・ビジネスをしっかりと関連づけられる思考能力が一番重要です。

SRE風のインフラエンジニアにならないために

この記事は、SRE Advent Calendar 2018 - Qiitaの24日目として投稿しています。

SRE風のインフラエンジニア

ここ数年で、SREという言葉が注目されていて色々な組織でSREチームが立ち上がっています。
特に、インフラチームが名前を変えてSREチームとなるケースは多いのではないかと思います。私もそうです。
この投稿では、DevOpsに対する考え方を通してインフラエンジニアではなくSREエンジニアがどう考えて働いていけば良いかということをまとめていきたいと思います。


SREとDevOps

そもそもDevOpsとは

DevOpsという言葉が生まれて随分立っていると思うので本稿を読む方にとっては釈迦に説法かもしれませんが、一応振り返って見ます。
DevOpsはDev(開発者)とOps(運用者)の境界を無くしていくことにより、提供しているサービスの価値を上げていく取り組みです。

SRE本でも取り上げられている、DevとOpsの目的の差異

マクロで見るとサービスの価値を上げたいことで目的が一致しているものの、ミクロで見ればDevとOpsで思惑が異なってくることが問題になります。

ミクロなDevの目的

新しいコードをどんどんリリースしてサービスを発展させていきたい。そのため、可能な限りリリースサイクルを高速にしたい。

ミクロなOpsの目的

システムを安定化させることが第一命題。そのため、可能な限り、システムに変更を加えたくない。

この両者の矛盾を解決するのがSREとしての役割です。



Ops側の視点での安定性の考え方を改める

まず、ミクロなOpsの目的として挙げていた、可能な限り、システムに変更を加えたくない、は正しくありません。短期的にはそれで問題ないとしても、長期的には必ず問題を引き起こします。極端な例を挙げるとすれば、何年も新規リリースをせずに放置されていたシステムにはもう修正は入れられない、といった具合です。(しかも、そんな感じのシステムは意外に存在します)

次に考えるのは、100%安全なリリースを、ある程度の長すぎないスパンで行う、といった試みかもしれません。が、これもまた間違いの一つです。100%安全なリリースは存在せず、また、100%に近づけるためのコストは指数関数的に増加してしまうからです。

システムを高速に更新可能にしておくことで安定性を担保する

100%安全なリリースは存在しないし、未来永劫新規リリースを行わないプロダクトは存在しないので、なるべく高速に更新可能な状態をキープすることが重要になります。これが実現できていれば、コスト的に見合うだけのQAを行なった後はさっさとリリースして、問題があったら速攻でロールバックや修正のリリースを行えます。
これがシステムを安定させるための解です。

インフラエンジニアではなくSREとしてどう高速リリースを実現するか

class SRE implements DevOpsGoogleが言っているように、DevOpsはインターフェースとしてシステムを高速に更新可能にしておくことで安定性を担保するということを定義しているが、SREはそれを実現するもの(実装)であると考えられます。
そのため、実装方法はプロダクトや組織によって多種多様になると思います。ここで、今までのインフラエンジニアという保守や管理がメインの立場から一歩踏み出し、DevとOps、もしかするとビジネスサイドなどの境界を飛び越えてどこを改善すれば、プロダクトの高速リリースの実現と安定化を達成できるか、ということを考えていく必要が出てきます。
いくつかの例をあげていきたいと思います。

プロダクトの高速リリースに効くところを見極める

ひとえにリリースの高速化といっても具体的に何を高速化するのか?は考え所です。インフラだけ担当していると割と見えにくいのですが、CI/CDの改善や、開発環境の充実、QA環境の充実、といったあたりがかなり重要ではないかと思います。これらの要素は、本番環境に比べれば軽視されがちだったりしますが、本番環境を安定させるためにも超重要な項目であるという認識を持ちましょう。

また、SREやインフラエンジニアの視点から見るDevはサーバーサイド開発者であることが多いと思いますが、もしモバイルアプリを提供しているサービスであればクライアントのエンジニアもDevであり、アプリケーションのリリースサイクルを考える一員であることを忘れないことも大事です。よく話して見ると意外とSREチームが解決できる問題を抱えていたりすることがあると思います。

リリースするにあたっての心配事を潰す

いざ新機能をリリースして見たら全サービスが落ちてしまった、なんて経験をした開発者であえば、リリースが怖くなります。こういった心理的な障壁も高速リリースに対する壁であると思います。これに対しての施策はいくつもあると思いますが、大きな修正を小さく試せるカナリアリリースや、一部の機能が落ちても全体としてはサービスが提供できるようになるためにサーキットブレイカーを導入するといったことが効果があります。

また、リリースに際して怖いものの一つに、プロダクション環境のデータでしか再現できないようなバグ、があります。これに関してもプロダクションと同等のデータを扱える検証環境が用意されていればかなり安心してリリースが行えるようになります。

もう一点大事なのが検知システムで、システムが正常に動作しているかをいち早く検知できる仕組みが整っているかどうか、という点も重要です。

Chaos Engineeingしてみるのも一つの解ではないでしょうか。

開発チームが自律して動ける仕組みやツールを提供する

SREチーム無しでも完結できる作業を増やすための仕組みやツールを提供することも重要です。全ての構成がコード化されていたり、クラウドサービスを利用しているのであれば十分可能だと思います。
また、監視アラートの対応についても事前に十分なドキュメントや対応ルールが決まっているのであればSREチーム無しでも十分対応可能なものは多いです。

ただし、注意したいのは、十分なコミュニケーションや準備無しでこれをしようとするとアラートの押し付け合いに発展したり、お見合いが発生します。



今の組織でやれていること

色々と偉そうに書いてきたものの、私が所属しているチームで全てやれているというわけでもありません。ですが、結構いい動きができているなあと思う点もあるので紹介したいと思います。

開発チーム出身の人がSREチームにジョインしてくれている

DevとOpsが融合するための一番手っ取り早い方法だと思います。

SREチームに入る新人のエンジニアさんもRails研修などを通して最低限の開発力を持っている

これも非常にいいことで、問題が起きた時にコードまでちゃんと読めるとか、そういった素地を最初に作ってもらっています。

SREチームのケツを叩いてくれる人がいる

やはりどうしても100%開発に従事しているわけではないので、Devチームが困っていることを十分理解することは困難です。そんな時にしっかり要望を投げつけてくれるDevチームがいることが一番大事ではないかと思います。DevとOpsはお互いに忖度無しに要望を投げつけあうくらいの方がうまくいくと思います。



今の組織でこれからやってみたいこと

SREチームからの短期留学

SREチームへの短期留学はたまに聞きますが、SREチームから開発チームへの短期留学ってあまり聞かないかなと思っています。
やっぱり身を以て他のチームがどんな苦労をしているかを理解することがチーム間の境界を無くすことに繋がるのでチャンスがあればやりたいと思っています。



まとめ

インフラエンジニアからSREエンジニアになって3年くらい経ちますが、色々と感じてきたことを書いて見ました。
私が経験してきたインフラエンジニアの考える安定性が守りの安定性であれば、SREとして考えるべき安定性は攻めの安定性と言えるんじゃないかと思います。



ECSのTaskを同期的に実行するコマンドecs_task_executorを作った


ECSのTaskの実行について

ECSにはServiceとTaskという二つのコンテナの実行方法がある。
Serviceは主にwebサーバーなどに代表されるような常に一定数のコンテナを保つように管理されるもので、Taskの方は一回実行したらそれっきりで終了するバッチ的な利用を想定されている。

APIアクセスによるTaskの実行

awscliやecs-cliによりこのTaskはAPI経由で実行することができる。
awscliであればこんな感じ。

aws ecs run-task --cluster default --task-definition sleep360:1


困ること

API経由でのTaskの実行は非同期で行われる

どういうことかというと、awsコマンドが実行成功した時点で正常終了してしまうということが起きる。
つまり実際にコンテナ上で実行したタスクがしばらくした後に異常終了した場合でもAPIを叩いた側は気づく事が出来ない。

以下は、ecs-cliでの実行例だが、sleep 120を処理しているコマンドでも実行自体は13秒で終わっている事がわかる。

$ time ecs-cli compose --project-name kenjiszk-test --file docker-compose.yml run web "sleep 120"
    INFO[0000] Using ECS task definition                     TaskDefinition="kenjiszk-test:3"
    INFO[0000] Starting container...                         container=e6fece2d-6f6f-44ec-95b9-44f37c7c1205/web
    INFO[0000] Describe ECS container status                 container=e6fece2d-6f6f-44ec-95b9-44f37c7c1205/web desiredStatus=RUNNING lastStatus=PENDING taskDefinition="kenjiszk-test:3"
    INFO[0013] Started container...                          container=e6fece2d-6f6f-44ec-95b9-44f37c7c1205/web desiredStatus=RUNNING lastStatus=RUNNING taskDefinition="kenjiszk-test:3"
    
    real	0m13.105s
    user	0m0.129s
    sys	0m0.098s

で、何が困るか?

連続するタスクがあって、それぞれ直前のタスクが成功した場合のみだけ処理を進めたいような場合、デフォルトの挙動だと失敗しようがしなかろうが成功してしまうのでどんどん先に進んでしまう。
それどころか、タスクの終了も待たないので、タスク自体が重なって実行されてしまうことになる。

具体的な例をあげると、Railsのデプロイ処理の場合

  • db:migrateなどのdbの処理
  • コード(コンテナ)の入れ替え

という流れを経るので、db:migrateが失敗したとしても新しいコードをデプロイしてしまうというやばいことも起きかねない。
そういった処理をする場合には、同期的に実行する事が不可欠になる。


同期的に実行できるようなコマンドを作った

という事で、同期的にコンテナ状のタスクを実行できて、失敗した場合にはしっかり失敗するようなコマンドを作成した。
github.com

READMEを見てもらえるとだいたいどういった処理になっているかわかるが、内部でやっていることは

  • aws APIを叩いてtaskを実行
  • taskの実行結果をpollingして取得する
  • taskのstatusがSTOPPEDになったら終了状況を取得して終了する

といった単純な処理をしている。

run_and_fail.shというしばらくすると失敗するscriptを用意して実行しみた例。
以下のように終了まで待っているのと、異常終了コードをハンドリング出来ている。

$ ecs_task_executor --cluster Sample -t kenjiszk-test:1 -n web -c 'run_and_fail.sh'
Set timeout as 600 sec.
LastStatus=PENDING TimeElapsed=5.024269701s
LastStatus=PENDING TimeElapsed=10.045879005s
LastStatus=PENDING TimeElapsed=15.070078421s
LastStatus=RUNNING TimeElapsed=20.092714283s
LastStatus=RUNNING TimeElapsed=25.109575722s
LastStatus=RUNNING TimeElapsed=30.131358467s
LastStatus=RUNNING TimeElapsed=35.150669821s
LastStatus=RUNNING TimeElapsed=40.16816051s
LastStatus=RUNNING TimeElapsed=45.185257392s
LastStatus=RUNNING TimeElapsed=50.209708875s
{
  ContainerArn: "arn:aws:ecs:ap-northeast-1:000000000:container/df87b9cd-962f-4580-ad8d-f6b97446b9a2",
  ExitCode: 255,
  HealthStatus: "UNKNOWN",
  LastStatus: "STOPPED",
  Name: "web",
  NetworkBindings: [],
  NetworkInterfaces: [],
  TaskArn: "arn:aws:ecs:ap-northeast-1:00000000:task/de79a37c-4c51-4307-826c-f6a3c7d733cb"
}
$ echo $?
255


今後

自前でコマンドを作って見たが、実は同期的にTaskを実行できるオプションがあるんじゃないかとちょっとヒヤヒヤしている。