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