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を公開鍵の先頭に付与という感じです。

  1. xのみで構成される公開鍵を作成する
  2. SHA256ハッシュ化
  3. RIPEMD-160ハッシュ化
  4. 先頭に0x00を付加
  5. SHA256ハッシュ化を2回した後に、先頭の4バイトでチェックサムを作る
  6. チェックサムを後ろにつける
  7. 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 までご連絡いただければと思います!