見出し画像

ビットコイン ウォレットをJavascriptで作ってみよう

ビットコイン ウォレットの作成に、チャレンジしてみましょう。今回はそのコアとなる、アドレス生成にフォーカスします。

公開鍵暗号のライブラリを使って、Javascriptで実装します。

アドレスはビットコインのコア・プロトコルです。ビットコインを理解したかったら、今回登場する公開鍵暗号方式やハッシュ関数は避けて通れません。

ビットコインが革命だったのは、誰からも検閲されず、誰でも自由に参加でき、それでいて秩序が保たれている、分散コンセンサスが成立していることです。

分散した環境下で、秩序を保つのには根拠がいります。その土台を支えているのが、公開鍵暗号とハッシュ関数です。

ちなみに、アドレスはブロックチェーンでは管理しません。各自ウォレットで管理します。

ビットコイン・ウォレットは以下の手順でアドレスを生成します。

1.秘密鍵
2.公開鍵
3.公開鍵ハッシュ
4.アドレス

ビットコイン・アドレスを生成するのに、オンラインである必要はありません。使用する関数は数学的な処理するものだけなのです。ゆえにオフラインのローカル環境下で生成できます。

適当にプロジェクトを切って、以下のnode_modulesをinstallしておいてください。

mkdir  bitcoin_address
cd bitcoin_address
npm init
npm i secure-random elliptic secp256k1 js-sha256 ripemd160


早速、秘密鍵を生成してみましょう。

秘密鍵

const secureRandom = require("secure-random");

let privateKey = secureRandom.randomBuffer(32);

console.log(privateKey);
console.log("Private key created", privateKey.toString("hex"));

<Buffer fd 3b 4f ee 1d 64 80 f6 bd 22 c6 40 71 43 8e ba 32 31 11 e5 54 58 c5 27 a6 ce 74 20 06 c1 5b d2>

Private key created fd3b4fee1d6480f6bd22c64071438eba323111e55458c527a6ce742006c15bd2

上のようになったら、おめでとうございます。秘密鍵を生成できましたね!

<Buffer fd 3b 4f ee 1d 64 80 f6 bd 22 c6 40 71 43 8e ba 32 31 11 e5 54 58 c5 27 a6 ce 74 20 06 c1 5b d2>

まず、これは何でしょう?これは、32byteのバイナリデータです。secureRandom.randomBuffer(32)で、32バイトの乱数を生成します。これは言い換えると、256bitの数字です。それを privateKey.toString("hex")で16進数の文字列に直しています。

fd3b4fee1d6480f6bd22c64071438eba323111e55458c527a6ce742006c15bd2

これが秘密鍵です。

よく同じものができたらどうするんだ?って思うかもしれませんが。これを衝突と言います。しかし、秘密鍵の大きさを見てみると 2^256です。これは、天文学的な数字^2です。

これを10進数に直すと10^77です。0が77桁並んだと思ってください。全宇宙の原子の数です。

全宇宙の原子の数が被る確率は幾つでしょうか?? 想像できないですね。


ところが、ビットコインでは必ずしも2^256じゃないです。

ビットコインの場合、2^256ビットの数字、全てが有効なわけでなく、

16進数でFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364140  という特別な数を、下回ったもののみ有効です。なぜなら、ビットコインでは、楕円曲線暗号を使用し、秘密鍵はこの数字を下回ったものでなければ、楕円曲線暗号では有効になりません。

秘密鍵 リバイス

const max = Buffer.from(
  "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364140",
  "hex"
);

let isInvalid = true;
let privateKey;

while (isInvalid) {
 privateKey = secureRandom.randomBuffer(32);
 if (Buffer.compare(max, privateKey) === 1) {
   isInvalid = false;
 }
}

console.log("> Private key: ", privateKey.toString("hex"));
Private key:  d287aa0ee06f0f67432743c0da3e594338e72c1d982110d9aafde61af18d5bf0

秘密鍵がFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364140を下回るまでループします。

公開鍵を生成する

有効な秘密鍵を生成できたところで、楕円曲線ライブラリ(elliptic、secp256k1)を使って公開鍵を作ってみましょう。公開鍵暗号を簡単に説明すると、私が秘密鍵で署名したとしたら、秘密鍵を知らない任意の人でも、公開鍵を使って、私であることの真正を確認できます。

※楕円曲線の詳細はここでは省きます。

公開鍵 コード

const ec = require("elliptic").ec;
const ecdsa = new ec("secp256k1");

const keys = ecdsa.keyFromPrivate(privateKey);
const publicKey = keys.getPublic("hex");

console.log("Public key created", publicKey);
Public key created 04926eeb8cf398c962c80aefb984d1a20cb990eee3b058bab321781b6f7b2a83b4921431e0548db4687da557e3a7a27048f025d4797d292c09f4fd2607dbb77eef

これが公開鍵になります。

04926eeb8cf398c962c80aefb984d1a20cb990eee3b058bab321781b6f7b2a83b4921431e0548db4687da557e3a7a27048f025d4797d292c09f4fd2607dbb77eef 


ハッシュ関数

ここからがビッコインの真髄です。

ハッシュって何かというと、任意のデータを入力したら、ランダムな文字列に変換して出力するものです。

例えば"bitcoin" という文字列を、SHA-256というハッシュ関数に通すと。

bitcon→sha256→6b88c087247aa2f07ee1c5956b8e1a9f4c7f892a70e324f1bb3d161e05ca107b

暗号みたいになりましたね。

その最大の特徴は、元に戻せないことです。不可逆といって、一方通行です。チキンナゲットを鶏に戻せないのと一緒です。野菜ジュースを野菜に戻せないのと一緒です。

元に戻せないで、何に使えるんだよ?と思うかもしれませんが。ハッシュ関数では、データが改竄されてないかを確認します。1文字でも違えば、ハッシュ値は全く別物になります。

例:bitcoin(素のデータ) とハッシュ値を一緒に送る。受け取った人は、素のデータをハッシュにかける。送られてきた、ハッシュ値と照合する。同じであれば、データは改竄されていないことになる。

ここまでやってきた公開鍵暗号方式で、十分セキュアだと思うかもしれませんが、ビットコイン アドレスはさらにセキュアな実装を目指します。公開鍵を、そのまま公開するより、公開鍵をハッシュ関数にかけます。
しかも2回もです。

公開鍵→SHA-256 → RIPEMD-160 →publicKeyHash

SHA-256とRIPEMD-160というハッシュ関数にかけます。これをダブルハッシュと言います。

ダブルハッシュ コード

const sha256 = require("js-sha256");
const ripemd160 = require("ripemd160");

let hash = sha256(Buffer.from(publicKey, "hex"));
let publicKeyHash = new ripemd160().update(Buffer.from(hash, "hex")).digest();

ここで終わりじゃないです。

publicKeyHashの先頭に00を付加し、さらにSHA-256にかけます。先頭8文字(最初の1バイト)をchecksumとして切り取り、それをpublicKeyHashの最後尾に連結する。最後にBase 58でエンコードします。これでアドレスのできあがりです。

分かりにくいので、手順をまとめると。

1.  publicKeyHashの先頭に00を付加
2. 1をSHA-256にかける
3. さらにSHA-256で16進数にして
4.  3の先頭8文字をchecksumとして切り取る
5. checksumを1の最後尾に連結
6 .5をBase 58でエンコード

後処理 コード

const sha256 = require("js-sha256");
const ripemd160 = require("ripemd160");

let hash = sha256(Buffer.from(publicKey, "hex"));
let publicKeyHash = new ripemd160().update(Buffer.from(hash, "hex")).digest();

const step1 = Buffer.from("00" + publicKeyHash.toString("hex"), "hex");
const step2 = sha256(step1);
const step3 = sha256(Buffer.from(step2, "hex"));
const checksum = step3.substring(0, 8);
const step4 = step1.toString("hex") + checksum;
const base58 = require("bs58");
const address = base58.encode(Buffer.from(step4, "hex"));

console.log(address);
1tbRDEyZa1HTJADBBfhKCzV5x8w3zPdYW

おめでとうございます!ビットコインアドレスが生成できましたね。

生成したアドレスは↓のサービスで、真正か確認してみましょう。秘密鍵を入れて、同じアドレスが出ればOKです。

最終形

const secureRandom = require("secure-random");
const ec = require("elliptic").ec;
const ecdsa = new ec("secp256k1");
const sha256 = require("js-sha256");
const ripemd160 = require("ripemd160");

const max = Buffer.from(
 "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364140",
 "hex"
);

let isInvalid = true;
let privateKey;

while (isInvalid) {
 privateKey = secureRandom.randomBuffer(32);
 if (Buffer.compare(max, privateKey) === 1) {
   isInvalid = false;
 }
}

console.log("> Private key: ", privateKey.toString("hex"));

const keys = ecdsa.keyFromPrivate(privateKey);
const publicKey = keys.getPublic("hex");
console.log("Public key created", publicKey);

let hash = sha256(Buffer.from(publicKey, "hex"));
let publicKeyHash = new ripemd160().update(Buffer.from(hash, "hex")).digest();

const step1 = Buffer.from("00" + publicKeyHash.toString("hex"), "hex");
const step2 = sha256(step1);
const step3 = sha256(Buffer.from(step2, "hex"));
const checksum = step3.substring(0, 8);
const step4 = step1.toString("hex") + checksum;
const base58 = require("bs58");
const address = base58.encode(Buffer.from(step4, "hex"));

console.log("address:" + address);

一点注意点は、sha256にかける時、必ずBuffer.fromで16進数に直してください。でないと、まったく違った値になってしまいます。

参考までに、WIF(wallet import format)と言って秘密鍵のウォレット・インポート用変換コードも載せて起きます。

Private Key WIF

const step1 = Buffer.from("80" + privateKey.toString("hex"), "hex");
const step2 = sha256(step1);
const step3 = sha256(Buffer.from(step2, "hex"));
const checksum = step3.substring(0, 8);
const step4 = step11.toString("hex") + checksum;
const privateKeyWIF = base58.encode(Buffer.from(step4, "hex"));

console.log(privateKeyWIF);

いかがでしたか?本当に理解したければ、実際に自分で作ればいいのです。ビットコインはオープンソースです。簡単に真似できます。人類の共有知ですね。

もし、これを読んで学びがあったら、是非誰かとシェアしてください。そうやってこのエコシステムは、盤石なものになっていきます。

※なお、本記事で生成したアドレスによる事故が起きても、一切の責任は負いかねます。自己責任で使ってください。


ビットコイン本では名著中の名著。なぜ、ビットコインを使うのか?技術レベルで理解できます。


この記事が気に入ったらサポートをしてみませんか?