Rubyでブロックチェーンをできるだけ丁寧に実装して理解する(署名編)
- Rubyでブロックチェーンを実装する
- 前提
- トランザクションの署名とは?
- トランザクションの改ざんを防止するための署名を実装する
- トランザクションの署名が正しいかどうかを確認する
- 2重支払いのチェックを入れる
- まとめ
- 次回
Rubyでブロックチェーンを実装する
第3回目です。
第1回はウォレットを作成して秘密鍵・公開鍵・ビットコインアドレスを発行してみました。
第2回は単純なトランザクションについて実装してみました。
全てのソースコードはgithubに公開しています。
github.com
前提
過去2回の実装が終わっていることを前提とします。
Rubyでブロックチェーンをできるだけ丁寧に実装して理解する(ウォレット編) - Work Records
Rubyでブロックチェーンをできるだけ丁寧に実装して理解する(トランザクション編) - Work Records
トランザクションの署名とは?
前回までの実装での問題点
トランザクションの中身を書き換えられそう
このブログで扱うのはまだ先になるが、本来のブロックチェーンのネットワークではトランザクションを作る人と、そのトランザクションを保存する人が別の人になります。そのため今の仕組みだとトランザクションを保存する人が不正し放題になることがわかると思います。
例えば、保存直前の全てのトランザクションの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には特に何の値も入れてきませんでしたが、ここにこのトランザクションを確かに発行したという署名を入れていきます。署名をするデータは以下のものになります。
- 署名対象のトランザクションのinputに対応する、前回のトランザクションのoutputに含まれる公開鍵(アドレス)。つまりこれは今回のトランザクションを発行する本人が所有しているutxoに含まれている本人の公開鍵。わざわざ前回のトランザクションから引っ張ってくるのは、前回のトランザクションも署名されて正しいとされているデータであるため。
- 署名対象のトランザクションのoutputに含まれている送信先の人の公開鍵(アドレス)
- 署名対象のトランザクションのoutputに含まれている送信するコインの数
つまり簡単にいうと、誰が誰にいくら送るか、というデータを署名してしまおうということになります。
具体的にやることは、前回作成した送金用の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"
まとめ
今回は、トランザクションの署名について詳しく見てみました。
この処理によって、トランザクションが作成されてからデータベースに保存されるまでの間に何らかの改ざんがあった事が気づけるようになります。