Work Records

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

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 までご連絡いただければと思います!