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
トランザクション周りの理解が不安であればこの本で復習すると良さそうです。