Rubyでブロックチェーンをできるだけ丁寧に実装して理解する(ブロック編)
Rubyでブロックチェーンを実装する
第4回目です。
第1回はウォレットを作成して秘密鍵・公開鍵・ビットコインアドレスを発行してみました。
第2回は単純なトランザクションについて実装してみました。
第3回はトランザクションに対する署名を実装しました。
全てのソースコードはgithubに公開しています。
github.com
前提
過去3回の実装が終わっていることを前提とします。
Rubyでブロックチェーンをできるだけ丁寧に実装して理解する(ウォレット編) - Work Records
Rubyでブロックチェーンをできるだけ丁寧に実装して理解する(トランザクション編) - Work Records
Rubyでブロックチェーンをできるだけ丁寧に実装して理解する(署名編) - Work Records
トランザクションをひとまとめにするブロック
第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でブロックチェーンを実装する
- 前提
- トランザクションの署名とは?
- トランザクションの改ざんを防止するための署名を実装する
- トランザクションの署名が正しいかどうかを確認する
- 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"
まとめ
今回は、トランザクションの署名について詳しく見てみました。
この処理によって、トランザクションが作成されてからデータベースに保存されるまでの間に何らかの改ざんがあった事が気づけるようになります。
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でブロックチェーンをできるだけ丁寧に実装して理解する(署名編) - Work Records
トランザクション周りの理解が不安であればこの本で復習すると良さそうです。
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を公開鍵の先頭に付与という感じです。
- xのみで構成される公開鍵を作成する
- SHA256ハッシュ化
- RIPEMD-160ハッシュ化
- 先頭に0x00を付加
- SHA256ハッシュ化を2回した後に、先頭の4バイトでチェックサムを作る
- チェックサムを後ろにつける
- 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 までご連絡いただければと思います!
次回
ウォレットが出来上がったので、次回はトランザクションについて実装進めていこうと思います。
Rubyでブロックチェーンをできるだけ丁寧に実装して理解する(トランザクション編) - Work Records
エンジニア職と株式投資
エンジニアが株式投資を考えることについて
自分の観測範囲だけかもしれませんが、投資について話をするエンジニアは少ないように思います。最近の売り手市場感からいえば、一般平均以上は給与をもらっていると思うので、 単にそういった手の話を公にすることが憚れているだけかもしれませんが。
まあそれはどちらでもいいのですが、アメリカの時価総額ランキングの上位をほとんどがテック企業が占めている現在の状況で、エンジニア職であることは投資的観点においても非常に恵まれていると感じています。
具体的には、以下のような観点をエンジニアであれば持つことができるからです。
また、別観点で言えばエンジニアは常にスキルをアップデートしていく必要があり、なんの技術を習得するか?ということがある意味では自身に対しての投資になるので、そこの技術選定を正しく行うためにもビジネスとテクノロジーの関連については敏感になっておく必要があるとも思います。
NVIDIA株
テクノロジーと株価の関係が非常にわかりやすいのがNVIDIAだろうと思います。それこそ、テクノロジーを知らなければ謎の半導体メーカーなどと呼んでしまいます。
直近5年の株価を見てみると、素晴らしいくらいの右肩上がりと2018年の暴落になっています。
右肩上がりなのは良いですが、肝心なのはテクノロジーを理解していれば、このNVIDIA株を値段が上がる前に買えるかどうか?という点になります。
第一参入期 AlphaGo
NVIDIAはGPUの会社なので、もともとはゲームなどグラフィックを処理するための特殊なプロセッサを作っていました。ところが、このGPUがディープラーニングにも利用できることがわかったのが最初の参入期になります。
詳細は省きますが、AI技術の実用化の一つのブレイクスルーがディープラーニングという手法が確立したことですが、このディープラーニングの計算を可能にしたのが、多重の行列計算を現実的な時間内で完了できるGPU、という関連です。
このディープラーニングが世間の注目を集め、実用化ができてきたなという出来事といえばAlphaGoではないかと思います。
Wikipediaより。
2016年3月、韓国棋界で「魔王」と呼ばれる世界トップ棋士の一人の李世乭と戦い、4勝1敗と勝ち越した。
つまり2016年3月時点で以下の考察ができます。
- AIがついに囲碁のトップ棋士に勝ち越した
- 汎用的ではないが、ある特定の分野においては人間によりも精度の高い仕事をAIが可能になった
- この発展に寄与したのがディープラーニングという手法
- ディープラーニングはGPUを利用している
- GPUといえばNVIDIA
特に技術が理解できる、技術について調べることができる能力があれば、三つ目以降の考察も可能ではないかと思います。
この時期に参入していれば将来$400近くまで上昇する株が$40程度で買えたことになります。
とはいえ、結果から考察しているだけなのでそんなに簡単な話ではないですが、技術を知っていれば確度が高くなることは間違い無いと思います。
第二参入期 仮想通貨
二度目の参入時期は、仮想通貨ブームではないかと思います。つまり、2017年の初頭から中頃まで。bitcoinの3年チャートを見るとこんな感じ。
こんな感じで考えられていればこの時期にNVIDIA株に注目できていたと思います。
- 仮想通貨がなんとなく流行っているっぽい
- ほとんどの通貨はPoWという手法を利用している
- PoWには多くの計算が必要でそこにGPUが利用されている
ということで、ここがNVIDIA株参入の第二期目だったかなと思います。
ちなみに面白いのは、bitcoinの価格ではなくPoWに費やされている計算量(hashrate)とNVIDIAの株価の方が相関があるという点ではないでしょうか。仮想通貨の価格が暴落した2018年1月ではまだNVIDIA株は落ちずに保っています。
つまり、
といったストーリーになります。
ただし大局も見ないといけない
一応触れておくと、アメリカ株は2017年から2018年末にかけて、特にテック系株は順調だったのでそういった影響もあったとは思います。
こういった全体感はちゃんと押さえておかないと参入時期を間違えるので一応は押さえておいた方がいいです。
では次は?
NVIDIAの例を元に見てきましたが、結局大事なのは、じゃあ今何を買えばいいの?という点かと思います。
VR / Oculus Go
いくつかあると思いますが個人的に考えているのは、VRです。VRについては何年も前から実用化に向けて色々な取り組みがありましたし、色々なアイディアを耳にしてきましたが、相当実用化してきたなと感じたのが、Oculus Goでした。
gigazine.net
まず非常に価格が安いことと、実際使ってみると多少の画質の悪さはあるものの十分コンテンツとして楽しめるなという感じを受けました。
AlphaGoがプロ棋士に勝ったことや、仮想通貨が流行り始めた事と同種の出来事ではないかと思います。
ここからがエンジニア的に考えないといけないところだと思いますが、VRを実現するために大切なものは小型GPUと5Gかなと考えています。
まとめ
エンジニアならではの視点で、成長する技術について着目できるのではないかと考えこのブログを書いてみました。
ただ、一方で技術に対するアンテナを高くキープする一つのインセンティブとしての株式の売買益ってのはありな気はします。どんな目的にしろ最新技術をキャッチアップし、技術と社会・ビジネスをしっかりと関連づけられる思考能力が一番重要です。
SRE風のインフラエンジニアにならないために
この記事は、SRE Advent Calendar 2018 - Qiitaの24日目として投稿しています。
SRE風のインフラエンジニア
ここ数年で、SREという言葉が注目されていて色々な組織でSREチームが立ち上がっています。
特に、インフラチームが名前を変えてSREチームとなるケースは多いのではないかと思います。私もそうです。
この投稿では、DevOpsに対する考え方を通してインフラエンジニアではなくSREエンジニアがどう考えて働いていけば良いかということをまとめていきたいと思います。
SREとDevOps
そもそもDevOpsとは
DevOpsという言葉が生まれて随分立っていると思うので本稿を読む方にとっては釈迦に説法かもしれませんが、一応振り返って見ます。
DevOpsはDev(開発者)とOps(運用者)の境界を無くしていくことにより、提供しているサービスの価値を上げていく取り組みです。
Ops側の視点での安定性の考え方を改める
まず、ミクロなOpsの目的として挙げていた、可能な限り、システムに変更を加えたくない、は正しくありません。短期的にはそれで問題ないとしても、長期的には必ず問題を引き起こします。極端な例を挙げるとすれば、何年も新規リリースをせずに放置されていたシステムにはもう修正は入れられない、といった具合です。(しかも、そんな感じのシステムは意外に存在します)
次に考えるのは、100%安全なリリースを、ある程度の長すぎないスパンで行う、といった試みかもしれません。が、これもまた間違いの一つです。100%安全なリリースは存在せず、また、100%に近づけるためのコストは指数関数的に増加してしまうからです。
システムを高速に更新可能にしておくことで安定性を担保する
100%安全なリリースは存在しないし、未来永劫新規リリースを行わないプロダクトは存在しないので、なるべく高速に更新可能な状態をキープすることが重要になります。これが実現できていれば、コスト的に見合うだけのQAを行なった後はさっさとリリースして、問題があったら速攻でロールバックや修正のリリースを行えます。
これがシステムを安定させるための解です。
インフラエンジニアではなくSREとしてどう高速リリースを実現するか
class SRE implements DevOpsとGoogleが言っているように、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の実行は非同期で行われる
どういうことかというと、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を見てもらえるとだいたいどういった処理になっているかわかるが、内部でやっていることは
といった単純な処理をしている。
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を実行できるオプションがあるんじゃないかとちょっとヒヤヒヤしている。
ecs-cliがタスクを非同期でしか実行してくれないからキックした後のエラーを拾ってくれなくて、同期的に実行できるラッパー書いたのだけど、ecs-cliに実はそういうオプションがありそうでならない。誰かもっと簡単な方法知っている人いないだろうか。https://t.co/C9xM6dswtY#ecs #aws
— Kenji Suzuki (@kenjiszk) 2018年6月23日