bitbank techblog

ビットコインウォレットを作ろう!

はじめに

はじめまして。エンジニアのアドレナリンと申します。

今回はbitpayが提供するbitcoreというフルノードのライブラリ、サーバサイドJavaScriptの実行環境であるNode.js、フレームワークであるExpressを使用して、簡単なディスクトップウォレットを作成してみたいと思います。作成するウォレットのソースコードはGithubで公開しています。

※ 今回このブログで紹介させて頂くウォレットはあくまで簡易的な、学習のために開発したものとなります。よってセキュリティに関しては厳格に考慮はされておりません。また、ビットバンクにて採用しているテクノロジーとも関係はありません。

ビットコインウォレットとは

ビットコインの残高確認やコインの送受信が簡単に行えるツールです。

画面に表示されるアドレスにビットコインを送金してもらったり、相手のアドレスに金額を指定して送金できます。ウォレットにはいくつか種類があり、Webウォレット・ディスクトップウォレット・ハードウェアウォレットなどがあります。これらのウォレットは利便性とセキュリティのトレードオフになっており、使う用途に合わせて選択します。

環境

本記事ではbitcore-libのバージョンは0.15.0、bitcore-explorersは1.0.1を使用します。

ウォレット作成の流れ

  • 秘密鍵とアドレスの作成
  • 秘密鍵に紐づく残高(UTXO)の確認
  • 送金処理の作成
  • MySQLを使ったデータの管理
  • Express+Nodeを使ったアプリケーションの開発

秘密鍵とアドレスの作成

bitcore-libを使ってビットコインアドレスを作成します。

ビットコインアドレスは秘密鍵から公開鍵、公開鍵からビットコインアドレスを作成します。bitcore-libを使うと秘密鍵の生成からアドレスの作成までを簡単に行うことができます。今回はテストネット(価値のない開発用のビットコインを使ったネットワーク)で開発を行いますので、ネットワークには'testnet'に指定します。

下記のコードを実行してください!

const bitcore = require('bitcore-lib');
const network = 'testnet';

const privateKey = new bitcore.PrivateKey(network);
const address = privateKey.toAddress();

console.log(privateKey.toString());
console.log(address.toString());

実行すると以下のような結果が得られます。

//秘密鍵
e40a718b418de32725e3e89dfeca7a39d49c579448b390f835155cc26a105c20

//ビットコインアドレス
msQXdd2gDGWSYaDF32TEWb3Yji7mbk3VCJ

アドレスの接頭辞はここに記載されている通りテストネットにおける公開鍵のハッシュから作成したアドレスなので”m”から始まります。

秘密鍵に紐づく残高(UTXO)の確認

ビットコインでは分散型台帳を採用しているので、銀行のように顧客IDなどに直接残高を紐づけて記録するシステムではありません。ビットコインは保有する秘密鍵に紐づいた未使用トランザクション(UTXO)をネットワークから調べ、そのUTXOの総数をカウントする事で保有する残高を知ることができます。

残高を確認する為に、先ほど作成しましたアドレスにテストネットのコインを送金してみましょう!テストネットのコインはfaucetで無料で配布されています。作成したアドレスを「Your testnet3 adress」に指定し「Get bitcoins!」を押すとアドレス宛に少量のテストネットコインが届きます。

----------2019-02-25-13.21.16

それではbitcore-explorersを使い、テストネットのコインを送付したアドレスを指定して残高を取得してみましょう。下記のコードを実行してください!

const explorers = require('bitcore-explorers');

const network = 'testnet'
const address = 'msQXdd2gDGWSYaDF32TEWb3Yji7mbk3VCJ';
const insight = new explorers.Insight(network);

insight.getUnspentUtxos(address, (err, utxos) => {
  if (err) {
    console.log('UTXO processing error')
  } else {
    let balance = utxos.map((v) => {
      return {
        txid: v.txId,
        vout: v.outputIndex,
        satoshi: v.satoshis,
        btc: v.satoshis * 1e-8,
      }
    })
    console.log(JSON.stringify(balance));
  }
});

実行すると結果の一部として以下の結果が得られます。

//トランザクションID
"txid":"cc68d4e7af47e5ca09b25a23d2d3716882cf0d2f4e98777483479a9bc86136c6"

//今回のトランザクションで前回の出力の何番目を使用するか
"vout":0

//残高をsatoshi単位で表示したもの
"satoshi":1000

//残高をBTC単位で表示したもの
"btc":0.00001

複数のTXIDがある場合、すべての残高を足した金額が保有する残高になります。

送金処理の作成

faucetで手に入れたテストネットのコインを送金してみましょう。

送金するためにはトランザクションを作成する必要があります。トランザクションには手数料, UTXO, 送金額, 送金アドレス, お釣り受け取りアドレス, 秘密鍵による署名を含める必要があります。これらの情報を含んだトランザクションを作成し、ビットコインネットワークにブロードキャストします。

下記のコードを実行してください!

const bitcore = require('bitcore-lib');
const explorers = require('bitcore-explorers');​

const network = 'testnet'
const privatekey = 'e40a718b418de32725e3e89dfeca7a39d49c579448b390f835155cc26a105c20';
const privateKey = new bitcore.PrivateKey(privatekey);
const sendAddress = 'mq8aTnusvudJBr2A4iNmCpQkWS8SQuALGD';
const changeAddress = 'msQXdd2gDGWSYaDF32TEWb3Yji7mbk3VCJ';
const sendAmout = Math.floor(parseFloat("0.0001") * 100000000);
const fee = parseFloat(1000);
const insight = new explorers.Insight(network);
const Transaction = bitcore.Transaction;​

insight.getUnspentUtxos(changeAddress, (err, utxos) => {
  if (err) {
    console.log('Bitcoin network connection error');
  } else {
    const transaction = new Transaction()
      .fee(fee)
      .from(utxos)
      .to(sendAddress, sendAmout)
      .change(changeAddress)
      .sign(privateKey);​
    insight.broadcast(transaction, (err, returnedTxId) => {
      if (err) {
        console.log(err);
      } else {
        console.log(returnedTxId);
      }
    });
  }
});

実行すると送金が成功した場合、以下のようにトランザクションIDが表示されます。

//TXID
C295c8969cb5916facdcf0d7cd82bb12ab5a64cbed8a90a194121c2b266886b3

画面に表示されたトランザクションIDをblockcypherで、実際にテストコインの送金が本当に成功しているのか確認してみてください。

----------2019-02-26-15.46.04

MySQLを使ったデータの管理

ユーザーID, ユーザー名, メールアドレス, パスワード, 作成日, 秘密鍵をデータベースに保存します。

パスワードはbcryptでハッシュ化し、ハッシュ化した値を使って秘密鍵をcryptで暗号化しデータベースに保存しています。データベースにパスワードのハッシュ値も保存しているので、セキュリティ的にまったく意味がありません。今回のように簡易的なアプリケーションではなく、本番のウォレットを作成する際にはセキュリティを強化する必要があります。

CREATE TABLE `users` (
`user_id` int(11) NOT NULL AUTO_INCREMENT,
`user_name` varchar(255) DEFAULT NULL,
`email` varchar(255) DEFAULT NULL,
`password` varchar(255) DEFAULT NULL,
`created_at` datetime DEFAULT NULL,
`private_key` varchar(255) DEFAULT NULL,
PRIMARY KEY (`user_id`)) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8;

Express+Nodeを使ったアプリケーションの開発

それでは秘密鍵の生成、アドレスの生成、秘密鍵に紐づく残高の確認、送金処理を組み合わせて、ディスクトップウォレットを作成しましょう!

画面の構成は、新規ユーザーID作成画面、ログイン画面 、メイン画面(ログイン後の画面)となっています。すべての内容について記事の中で紹介することが難しいので、抜粋して紹介したいと思います。完全なウォレットのソースコードはGithubで公開しています。

新規ユーザーID作成画面

新規ユーザーを作成する際にはユーザー名、メールアドレス、パスワードを入力してもらいデータベースに保存します。その際に作成日時と秘密鍵を作成し、合わせてデータベースに保存しています。ログイン時にメールアドレスを使用してユーザーを一意に特定する為、メールアドレスは重複登は禁止しています。

----------2019-03-04-11.03.02

下記は新規ユーザーID作成画面のコードの一部になります。

connection.query(emailExistsQuery, emails, function(err, email) {
      if (!err) {
        let emailExists = email.length;
        if (emailExists) {
          res.render('register', {
            title: 'Sign up',
            emailExists: 'Already registered email address'
          });
        } else {
          connection.query(registerQuery, [userName, emails, hash, createdAt, privatekey], function(err) {
            if (!err) {
              res.redirect('/login');
            } else {
              console.log(err);
            }
          });
        }

ログイン画面

ログイン機能の実装はセッションを利用してユーザーIDで管理を行っています。メールアドレスとパスワードの組み合わせが、データベースと一致する場合にセッションにユーザーIDを記録しメインページにリダイレクトします。

----------2019-03-04-11.04.52-1

下記はログイン画面のコードの一部になります。

router.post('/', function(req, res) {
  const email = req.body.email;
  const password = req.body.password;

  const query = 'SELECT user_id,password FROM users WHERE email = ? LIMIT 1';
  
  connection.query(query, email, async function(err, rows) {
    if (!err) {
      if (rows.length === 0) {
        res.render('login', {
          title: 'Sign in',
          noUser: 'The email address and password are incorrect'
        });
      } else {
        const hash = rows[0].password;
        const hashs = await bcrypt.compare(password, hash);
        if (hashs) {
          const userId = rows[0].user_id;
          req.session.user_id = userId;
          res.redirect('/');
        } else {
          res.render('login', {
            title: 'Sign in',
            noUser: 'The email address and password are incorrect'
          });
        }
      }
    } else {
      console.log(err);
    }
  });
});

メイン画面(ログイン後の画面)

メイン画面ではビットコインの送金と受け取りができます。送金画面が存在する訳ではなく、送金処理が実行されるとメイン画面にリダイレクトされます。

----------2019-03-04-11.17.24

終わりに

ここまでお読みいただき、ありがとうございます。ウォレットを作成することで、一歩踏み込んでビットコインのしくみについて理解し、ビットコインをより身近に感じることができる様になるかと思います。ぜひ、この記事をきっかけにオリジナルのウォレットを作成していただければ幸いです。

Author image
About adrenaline
expand_less