[rubyでブロックチェーン] bitcoinのアドレスを作ってみる


堅牢なスマートコントラクト開発のためのブロックチェーンを読んだので、楽しい車輪の再発明 自分で手を動かして実装してみました。

まずはアドレスを作るところからですが、最終的に自分なりのプロトタイプを作ってトランザクションを理解するところまでできれば楽しそうです。

もちろん遊びのレベルなので、実際に使用する際は注意してくださいね!(お金は大事だよ)

アドレスとは

ビットコインを送金する際に宛先として利用されるもので、一般的に自分の公開鍵から作られます。

たとえばbitcoin wikiにある実例を見るとこういうものがあります。



16UwLL9Risc3QfPqBUvKofHmBQ7wMtjvM

仮想通貨を持ってない、理解もおぼつかないビットコイン弱者の僕も、こういったアドレスを持つことで誰かに送金してもらえる態勢だけは整えられるというメリットがありますね。

鍵とは

ここで言う鍵とは、いつもSSHでお世話になってるあの「鍵」と同じ、いわゆる公開鍵暗号です。

公開鍵暗号は2つの鍵がペアになっていて、ざっくり下記の性質があります。

  1. 秘密鍵でかけた暗号は、公開鍵でしか復号できない
  2. 公開鍵でかけた暗号は、秘密鍵でしか復号できない
  3. 公開鍵から秘密鍵、秘密鍵から公開鍵を推測することが非常に難しい

この性質を利用すると、たとえばヨシコさんからジロウくん宛のメッセージを送ってもらう時に、

  • ヨシコさんにジロウくんの公開鍵を送っておく(公開鍵は知られても大丈夫)
  • ヨシコさんに、その公開鍵を使ってメッセージに暗号をかけてもらう
  • ヨシコさんは暗号化されたメッセージをジロウくんに送る
  • ジロウくんだけが持つ秘密鍵を使うことで、ジロウくんだけがメッセージを読むことができる

という使い方ができます。

ただし公開鍵をバラまいてもよいというのは、3の前提が大切です。

弱い暗号を使っていると、公開鍵から秘密鍵を推測できてしまうため、本人だけにしか知られるはずのない秘密が漏れたり、AWSを乗っ取られたり、ビットコインが流出したり、大変なことになってしまいます。

ビットコインでは一定の強度を担保するために、特定の楕円関数1をベースとした鍵を使うというしばりがあります。
ボクがふだん使っているのはRSA鍵なので、そのまま流用することはできません(残念)。

鍵を作る

まずは鍵を作ってみましょう。
rubyにはopensslライブラリがあるので、これを利用します。(資料が全然充実してなくて辛かった・・)

まず、 gem install openssl で最新のopenssl gemを導入しましょう。

あとはpryでもirbでもいいので下記のコードを実行します。

# 事前に gem install openssl しておく

gem 'openssl' # ruby標準のopensslを使わないため必要
require 'openssl'

class KeyGenerator
  def initialize
    generate_private_key!
  end

  def public_key
    @public_key ||= generate_public_key!
  end

  def save_private_key!(file)
    File.open(file, 'w') { |f| f.puts @private_key.to_pem}
  end

  def save_public_key!(file)
    File.open(file, 'w') { |f| f.puts public_key.to_pem}
  end

  private

  # 楕円曲線Secp256k1によるEC秘密鍵作成
  def generate_private_key!
    @private_key = OpenSSL::PKey::EC.new("secp256k1").generate_key
  end

  # 公開鍵は秘密鍵オブジェクトで、private_key = nilとすると作成できる
  def generate_public_key!
    @private_key
    public_key = @private_key.dup
    public_key.private_key = nil
    public_key
  end

end

やっていることは簡単でOpenSSL::PKey::EC#generate_keyで鍵を生成しています。
#to_pem でテキストで読めるpem形式になり、#to_derでバイナリデータになります。

次に keys というフォルダを作り、下記のようにするとそれぞれ秘密鍵と公開鍵が keys 以下に保存されます。

my_key = KeyGenerator.new
my_key.save_private_key!('keys/private_key.pem')
my_key.save_public_key!('keys/public_key.pem')

この鍵がビットコインネットワークでのアイデンティティになります。

ざっくり表現すると、ビットコインでの「送金」とは人から人への送金をしてくれるものではなく、ある鍵から別の鍵への信頼性の高い送金を実現する技術です。

つまりビットコイン(残高)に紐づいているのは鍵であって、自分ではありません。その鍵を持っているのが自分(のウォレット)だから、自分のビットコインと言えるわけですね。

当然、秘密鍵をなくしたらそこに紐づくビットコインを使うことは未来永劫できません。秘密鍵を盗まれたら、自由に残高を使われてしまうので気をつけましょう。(お金は大事だよ)

アドレスをつくる

アドレスはここで作成した公開鍵をベースに作ります。

ざっくり言うと公開鍵と、バージョン番号(公開鍵の形式などが入る)、誤りを検出するためのチェックサムなどの情報をいれたものに対して、複数段のハッシュ化をしたものです。

ざっくりしすぎ ^^;

bitcoin wikiに詳しい図がのっていたので転載します。

さっそくやってみましょう。

ここにあるコードはmacで正常な動作を確認していますが、ビット表現のendianの違いをrubyでどう処理するのかがやや心もとなく、もしかして生成されるアドレスにシステム依存性があるかもしれません。(pack, unpackの使い方に注意)

段階を踏んで見ていきたいので、手続き的に書いていきます。



# 先ほどのコードを実行していなければ、opensslをrequireしておく

public_key  = my_key.public_key.to_der # 先ほど作っておいた鍵

digest = OpenSSL::Digest::RIPEMD160.digest(
          OpenSSL::Digest::SHA256.digest(public_key)
         )


# 0x6Fはテスト用アドレスに対応する。0x00を入れると公開鍵ハッシュになる
prefix = ['6F'].pack('H2')

最初で使われているRIPEMD160、SHA256というのはハッシュ関数で、これにより公開鍵をハッシュ化しています。

公開鍵は短いものなのになぜハッシュ化するかというのが気になりますが、どうやら匿名性やセキュリティ上の理由などがあるようです。(後述します)

さらに、バージョンプレフィクスという1バイトのコードを作成しておきます。

ここからチェックサムを作ります。



digest_with_prefix = digest.insert(0, prefix)

checksum            = OpenSSL::Digest::SHA256.new.digest(
                        OpenSSL::Digest::SHA256.new.digest(
                          digest_with_prefix
                        )
                      ).unpack('H4').pack('H4') # 先頭4バイト

このチェックサムはビットコイン技術とはあまり関係なく、誤り防止用についているものです。

これがあるおかげで「メモしておいたアドレスが1文字違って、あらぬ鍵にあてた送金が実行されてしまった」という悲劇を防けます。
(アドレスを少しだけ変更したらチェックサムがあわなくなり、有効なアドレスにならない)

さて、これでアドレスを組み立てる材料が揃いました。バイナリ形式だと、次のような形になります。

my_binary_address = digest_with_prefix + checksum

最後にbase58というエンコードをかけてバイナリから人間が読みやすい形に変換すると、いわゆるビットコインアドレスが作成できます。

base58はwebでよく使うbase64の変形版で、1やIとlの違いなどからくる悲劇を防ぐために一部のアルファベットを使用しないようにしたものです。マッピング順に細かい方言があるようですが、当然ながらbitcoinが定める規格に従います。

・・・というか、今回はとっても便利なgemを発見したのでそれを使いましたw

# gem install base58 しておく

require 'base58'

my_address = Base58.binary_to_base58(my_binary_address, :bitcoin) # 変換形式はbitcoin仕様を指定する

やたー!!
人生初のビットコインアドレスができました!パチパチパチ

検証してみる

実際にbitcoin wikiにステップ・バイ・ステップの実例があるので検証してみましょう。

public_key 
 = 0450863AD64A87AE8A2FE83C1AF1A8403CB53F53E486D8511DAD8A04887E5B23522CD470243453A299FA9E77237716103ABC11A1DF38855ED6F2EE187E9C582BA6

# ...作成手順は略

puts my_address

# => 16UwLL9Risc3QfPqBUvKofHmBQ7wMtjvM となっていれば正解!

これがどうやってアドレスの機能を果たすのかはまた次回(勉強中w)

ところで、このアドレスは自分の口座のように誤解されていますが、オフィシャルwikiを見ると取引ごとの使い捨てが強く推奨されています。(理由は後述します)
アドレスは鍵から一意に生成されるため、取引一回ごとに鍵を作れということになりますね。

セキュリティ上の注意点

公開鍵は公開して大丈夫なはずなのに、なぜわざわざハッシュ化して匿名性を高めたりするのでしょうか。
bitcoinは鍵から鍵への送金をするものなのに、公開鍵に対してダイレクトに送金させないのは不思議ですね。

この辺を参考につらつら調べていくと・・・

https://bitcointalk.org/index.php?topic=139381.0
https://bitcoin.stackexchange.com/questions/49158/why-do-you-use-bitcoin-addresses-instead-of-public-keys

どうやら公開鍵をオープンにすることでいくつかの懸念があるということのようです。

  1. セキュリティ上の懸念
    • 公開鍵から秘密鍵を破る攻撃をかけられる可能性がある。Secp256k1は量子コンピュータで破られる可能性がある一方、数学的な構造が少ないハッシュ関数は量子耐性が高い(quantum resistant)と信じられている。
  2. プライバシー上の懸念
    • 過去の送金のやりとりなどから、その鍵の持ち主を知っている人がいたら、どんな取引をしているか・残高はいくらかといった内容がすべて見られてしまう。

技術的には(古いプロトコルを使えば)直接公開鍵に送金することも可能、ということらしいです。

アドレスは再利用しない

同じ理由によって、取引ごとに自分の公開鍵やアドレスを生成する方がよいようです。

一方的に送金してもらっている間はいいのですが、自分から送金(お金を使う)時には自分の公開鍵をネットワークに公開する必要があります。
この時にアドレスをずっと使いまわしていると、アドレスの元になった公開鍵がバレてしまうのです。

つまり鍵は取引ごとに生成するのが安全、ということでした。

まとめ

というわけで温かみあふれるお手製のアドレスを作成してみました。

あとはなんちゃってブロックチェーンをrubyで実装して、取引をつくるところまで行きたい・・。余裕があればQiitaに公開しようと思います。

なおここまでの記事は上にあげた資料の他に、こちらのQiita記事を盛大に参考にさせていただきました(ぺこり)

楕円曲線の説明もあって、めちゃくちゃわかりやすいのでおすすめです。

また公開鍵暗号、ハッシュ化や署名などの暗号技術については、暗号技術入門をなんども読み返しました。


  1. ビットコインではSecp256k1という特定の楕円曲線に基づいた暗号を使うことが定められている。Ethereumでも同じものを使っているようですね。