@mizumotokのブログ

テクノロジー、投資、書評、映画、筋トレなどについて

javascriptでブロックチェーンをつくってみよう - 5 ウォレットの実装

ビットコインJavascriptで実装しながら理解しましょう。
前回までにトランザクション(送金取引)の実装を行いました。今回はウォレットを実装してみます。

前回までの状態を再現

必要な場合は、前回の状態を再現しましょう。

$ git clone https://github.com/mizumotok/blockchain-js.git
$ cd blockchain-js
$ git checkout 05_transaction


ウォレットの実装

ウォレットは公開鍵/秘密鍵と残高を管理します。またトランザクションを作成し、署名を入れます。

トランザクションを作成時に残高が必要になるので、初期値(INITIAL_BALANCE)として1,000持たせることにしましょう。

ブロックチェーン内のトランザクションから残高を求めるWallet#balanceメソッド、秘密鍵で署名するWallet#signを実装します。

src/wallet/index.js

// @flow

import uuidv1 from 'uuid/v1';
import { ec as EC } from 'elliptic';
import Blockchain, { Transaction } from '../blockchain';

const ec = new EC('secp256k1');

const INITIAL_BALANCE = 1000;

class Wallet {
  blockchain: Blockchain;
  keyPair: any;
  publicKey: string;

  constructor(blockchain: Blockchain) {
    this.blockchain = blockchain;
    this.keyPair = ec.genKeyPair({ entropy: uuidv1() });
    this.publicKey = this.keyPair.getPublic().encode('hex');
  }

  createTransaction(recipient: string, amount: number): Transaction {
    if (amount > this.balance()) {
      console.log('残高不足です。');
      return null;
    }
    return Transaction.createTransaction(this, recipient, amount);
  }

  balance() :number {
    const transactions = this.blockchain.chain
      .reduce((a, block) => a.concat(block.transactions), []);
    const inputs = transactions.reduce((a, tx) => (
      tx.input && tx.input.address === this.publicKey ? a + tx.input.amount : a
    ), 0);
    const outputs = transactions.reduce((a, tx) => (
      a + (tx.outputs || []).reduce((a2, o) => (
        o.address === this.publicKey ? a2 + o.amount : a2
      ), 0)
    ), 0);
    return (outputs - inputs) + INITIAL_BALANCE;
  }

  sign(data: string) :string {
    return this.keyPair.sign(data);
  }
}

export default Wallet;
export { INITIAL_BALANCE };

前回までのトランザクションではウォレットがなかったので、直接公開鍵/秘密鍵を使用していたところがあります。その部分をウォレットに置き換えていきます。

src/blockchain/transaction.js

// @flow

import uuidv1 from 'uuid/v1';
import SHA256 from 'crypto-js/sha256';
import { ec as EC } from 'elliptic';
import Wallet from '../wallet';

const ec = new EC('secp256k1');

type Input = {
  timestamp: number,
  amount: number,
  address: string,
  signature: string,
};

type Output = {
  amount: number,
  address: string,
};

class Transaction {
  id: string;
  outputs: Array<Output>;
  input: Input;

  createOutputs(senderWallet: Wallet, recipient: string, amount: number) {
    const balance = senderWallet.balance();
    this.outputs = [{ amount, address: recipient }];
    if (balance > amount) {
      this.outputs.push({
        amount: balance - amount,
        address: senderWallet.publicKey,
      });
    }
  }

  signTransaction(senderWallet: Wallet) {
    const hash = SHA256(JSON.stringify(this.outputs)).toString();
    this.input = {
      timestamp: Date.now(),
      amount: senderWallet.balance(),
      address: senderWallet.publicKey,
      signature: senderWallet.sign(hash),
    };
  }

  verifyTransaction(): bool {
    const hash = SHA256(JSON.stringify(this.outputs)).toString();
    return ec.keyFromPublic(this.input.address, 'hex')
      .verify(hash, this.input.signature);
  }

  static createTransaction(
    senderWallet: Wallet,
    recipient: string,
    amount: number,
  ): Transaction {
    const tx = new Transaction();
    tx.id = uuidv1();
    tx.createOutputs(senderWallet, recipient, amount);
    tx.signTransaction(senderWallet);
    return tx;
  }
}

export default Transaction;

Transactionクラスで直接keyPair(公開鍵/秘密鍵)を扱うことがなくなりました。

テスト

$ yarn test

TransactionクラスとMinerクラスのテストでエラーが出ます。keyPairを使っているところをWalletクラスを使って修正しましょう。

Walletクラスはbalanceメソッドとsignメソッドのテストをつくってみます。
src/__tests__/wallet/index.test.js

// @flow

import { ec as EC } from 'elliptic';
import Blockchain, { Transaction } from '../../blockchain';
import Miner from '../../miner';
import Wallet, { INITIAL_BALANCE } from '../../wallet';

const ec = new EC('secp256k1');

describe('Miner', () => {
  let blockchain: Blockchain;
  let miner: Miner;
  let wallet: Wallet;

  beforeEach(() => {
    blockchain = new Blockchain();
    miner = new Miner(blockchain);
    wallet = new Wallet(blockchain);
  });

  it('balance test', () => {
    expect(wallet.balance()).toBe(INITIAL_BALANCE);

    const tx = Transaction.createTransaction(wallet, 'recipient-address', 100);
    miner.pushTransaction(tx);
    miner.mine();
    expect(wallet.balance()).toBe(INITIAL_BALANCE - 100);
    console.log(`残高: ${INITIAL_BALANCE - 100}`);
  });

  it('sign test', () => {
    const data = `${new Date().getTime()}`;
    const signature = wallet.sign(data);
    expect(ec.keyFromPublic(wallet.publicKey, 'hex').verify(data, signature))
      .toBe(true);
  });
});

ここまでのソースはGitHubの06_walletブランチに公開しています。
また、ソースの変更点はこちらで確認できます。

マイニング報酬

初期残高を固定でもたせましたが実際は0です。マイニングの報酬で得ることができます。
報酬額は固定で設定しておきます。実際にはビットコインでは半減期といってある一定期間ごとに報酬額が半分になる仕組みが入っています。

src/config.js

const DIFFICULTY_TARGET = 240;
const MINING_DURATION = 5000;
const MINING_REWARD = 50;

module.exports = {
  DIFFICULTY_TARGET,
  MINING_DURATION,
  MINING_REWARD,
};

Transactionクラスで報酬トランザクションをつくるメソッドを用意します。
報酬用のトランザクションはinputはなく、coinbaseと呼ばれます。coinbaseには任意のメッセージを入れることができます。

src/blockchain/transaction.js

import { MINING_REWARD } from '../config';
  createCoinbase(recipient: string) {
    this.outputs = [
      { amount: MINING_REWARD, address: recipient },
    ];
    this.coinbase = `This is coinbase created at ${new Date().toString()}`;
  }

  static rewardTransaction(rewardAddress: string) {
    const tx = new Transaction();
    tx.id = uuidv1();
    tx.createCoinbase(rewardAddress);
    return tx;
  }

Minerクラスでマイニング時に指定のアドレス宛の報酬トランザクションをブロックにいれてあげます。

src/miner/index.js

class Miner {
  transactionPool: Array<Transaction>;
  blockchain: Blockchain;
  rewardAddress: string;

  constructor(blockchain: Blockchain, rewardAddress: string) {
    this.transactionPool = [];
    this.blockchain = blockchain;
    this.rewardAddress = rewardAddress;
  }

  mine() {
    const miningStartTimestamp = new Date().getTime();
    const prevHash = this.blockchain.lastHash();
    const target = this.blockchain.nextDifficultyTarget();
    const rewardTx = Transaction.rewardTransaction(this.rewardAddress);
    this.transactionPool.push(rewardTx);

WalletクラスからはINITIAL_BALANCEに関する記載を取り除きます。

テスト

$ yarn test

初期残高がなくなったことによる失敗が多く出ます。トランザクションを作る前にマイニング処理を入れて、残高をつくるような修正をいれていきます。

今回つくったTransaction#rewardTransactionメソッドのテストもつくておきましょう。

src/__tests__/transaction.test.js

  it('rewardTransaction', () => {
    const tx = Transaction.rewardTransaction('reward-address');
    expect(tx.outputs[0].address).toBe('reward-address');
    expect(tx.outputs[0].amount).toBe(MINING_REWARD);
    expect(tx.coinbase.length).toBeGreaterThan(1);
  });

ここまでのソースはGitHubの07_mining_rewardブランチに公開しています。
また、ソースの変更点はこちらで確認できます。

まとめ

今回はウォレットの実装をしました。ウォレットの公開鍵を送信先アドレスとし、ウォレットの署名機能でトランザクションに署名をするようにしました。
またマイニング報酬が得られるようにしました。
次回P2Pネットワーク(ルーター)を実装します。

ビットコインとブロックチェーン:暗号通貨を支える技術

ビットコインとブロックチェーン:暗号通貨を支える技術