Work Records

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

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"



まとめ

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