@mizumotokのブログ

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

javascriptでブロックチェーンをつくってみよう - 4 トランザクションの実装

ビットコインJavascriptで実装しながら理解しましょう。
前回までにブロックをマイニングする処理を実装を行いました。今回はトランザクション(送金取引)を実装してみます。

開発の流れ

  1. ブロックの実装
  2. ブロックチェーンの実装
  3. マイニングの実装
  4. トランザクションの実装 ← 今回はここ
  5. ウォレットの実装
  6. P2Pネットワーク

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

$ git clone https://github.com/mizumotok/blockchain-js.git
$ cd blockchain-js
$ git checkout 04_adjust_difficulty


トランザクションの実装

トランザクションには送信元の電子署名を入れます。ビットコインでは電子署名の暗号として、楕円曲線暗号secp256k1が使われています。
楕円曲線暗号secp256k1を提供してくれるライブラリ(elliptic)を入れておきます。
また、トランザクションに持たせるIDとしてuuidを使ってみましょう。

$ yarn add uuid elliptic

トランザクションにはinputとoutputがあります。
outputは送信先アドレスと送金額を配列でもちます。
inputはタイムスタンプと送信前の残高、送信元アドレス、送信者による電子署名を持たせます。ビットコインでは残高ではなくUXTO(未使用のトランザクション)なのですが、ここでは分かりやすく。

inputの電子署名にはoutputのハッシュ値を使用して、送信者の秘密鍵で署名します。送信者の公開鍵(この場合は送信元アドレスと同じ)を使ってoutputに改ざんがないかを検証することができます。
検証用のメソッドとして、verifyTransactionを作っておきましょう。

src/blockchain/transaction.js

// @flow

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

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;
  coinbase: ?string;

  createOutputs(keyPair: any, senderAmount: number, recipient: string, amount: number) {
    this.outputs = [{ amount, address: recipient }];
    if (senderAmount > amount) {
      this.outputs.push({
        amount: senderAmount - amount,
        address: keyPair.getPublic().encode('hex'),
      });
    }
  }

  signTransaction(keyPair: any, amount: number) {
    const hash = SHA256(JSON.stringify(this.outputs)).toString();
    this.input = {
      timestamp: Date.now(),
      amount,
      address: keyPair.getPublic().encode('hex'),
      signature: keyPair.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(
    keyPair: any,
    senderAmount: number,
    recipient: string,
    amount: number,
  ): Transaction {
    const tx = new Transaction();
    tx.id = uuidv1();
    tx.createOutputs(keyPair, senderAmount, recipient, amount);
    tx.signTransaction(keyPair, senderAmount);
    return tx;
  }
}

export default Transaction;

src/blockchain/index.js

import Blockchain from './blockchain';
import Block from './block';
import Transaction from './transaction';

export default Blockchain;
export { Block, Transaction };

テスト

src/__tests__/blockchain/transaction.test.js

// @flow

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

const ec = new EC('secp256k1');
describe('Transaction', () => {
  let keyPair;
  let tx;
  beforeEach(() => {
    keyPair = ec.genKeyPair({ entropy: uuidv1() });
    tx = Transaction.createTransaction(keyPair, 1000, 'recipient-address', 100);
  });

  it('createOutputs test', () => {
    // おつりあり
    expect(tx.outputs).toEqual([
      { amount: 100, address: 'recipient-address' },
      { amount: 900, address: keyPair.getPublic().encode('hex') },
    ]);

    // おつりなし
    const tx2 = Transaction.createTransaction(keyPair, 1000, 'recipient-address', 1000);
    expect(tx2.outputs).toEqual([
      { amount: 1000, address: 'recipient-address' },
    ]);
  });

  it('signTransaction test', () => {
    const hash = SHA256(JSON.stringify(tx.outputs)).toString();
    expect(tx.input.address).toBe(keyPair.getPublic().encode('hex'));
    expect(tx.input.signature).toEqual(keyPair.sign(hash));
  });

  it('verifyTransaction test', () => {
    expect(tx.verifyTransaction()).toBe(true);

    // 改ざん
    tx.outputs[0].address = 'kaizansareta-address';
    expect(tx.verifyTransaction()).toBe(false);
  });
});
$ yarn test

テストが問題なく通ることを確認します。

トランザクションプール

Transactionクラスをつくったので、Blockクラスのtransactionsプロパティにflowで型付をしておきます。

src/blockchain/block.js

import SHA256 from 'crypto-js/sha256';
import Transaction from './transaction';
import { DIFFICULTY_TARGET, MINING_DURATION } from '../config';

class Block {
  timestamp: number;
  prevHash: string;
  difficultyTarget: number;
  nonce: number;
  transactions: Array<Transaction>;
  miningDuration: number;

トランザクションはマイニングに使用するのですが、いくつかのトランザクションをまとめてマイニングします。トランザクションをまとめるためにマイニングプールをMinerで管理してもらいます。
pushTransactionメソッドでトランザクションプールにトランザクションを追加しますが、Transaction#verifyTransactionメソッドで事前に検証します。
また同じ送信元がすでにトランザクションプールにあった場合は2重取引にならないように無視しています。
src/miner/index.js

// @flow

import Blockchain, { Block, Transaction } from '../blockchain';

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

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

  mine() {
    const miningStartTimestamp = new Date().getTime();
    const prevHash = this.blockchain.lastHash();
    const target = this.blockchain.nextDifficultyTarget();

    let nonce = 0;
    let block;
    let timestamp;

    do {
      timestamp = new Date().getTime();
      nonce += 1;
      block = new Block(
        timestamp,
        prevHash,
        target,
        nonce,
        this.transactionPool,
        timestamp - miningStartTimestamp,
      );
    } while (!block.isValid());

    this.blockchain.addBlock(block);
  }

  pushTransaction(tx: Transaction) {
    if (!tx.verifyTransaction()) {
      console.log('署名の検証に失敗しました。');
      return;
    }
    this.transactionPool = this.transactionPool.filter(t => t.input.address !== tx.input.address);
    this.transactionPool.push(tx);
    console.log('トランザクションを追加しました。');
  }

  clearTransactions() {
    this.transactionPool = [];
    console.log('トランザクションを削除しました。');
  }
}

export default Miner;

テスト

Miner#pushTransactionメソッドのテストをしておきます。
src/__tests__/miner/index.test.js

  it('pushTransaction test', () => {
    const keyPair = ec.genKeyPair({ entropy: uuidv1() });
    const tx = Transaction.createTransaction(keyPair, 1000, 'recipient-address', 100);

    miner.pushTransaction(tx);
    expect(miner.transactionPool).toHaveLength(1);

    // 同じアドレス
    const tx2 = Transaction.createTransaction(keyPair, 1000, 'recipient-address2', 200);
    miner.pushTransaction(tx2);
    expect(miner.transactionPool).toHaveLength(1);

    // 改ざんされたトランザクション
    const tx3 = Transaction.createTransaction(keyPair, 1000, 'recipient-address', 300);
    tx3.outputs[0].address = 'kaizansareta-address';
    miner.pushTransaction(tx3);
    expect(miner.transactionPool).toHaveLength(1);

    // clearTransaction
    miner.clearTransactions();
    expect(miner.transactionPool).toHaveLength(0);
  });

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

まとめ

今回はトランザクションの実装をしました。送信先情報を送信元の秘密鍵電子署名し、送信元以外が改ざんできないようにしました。
またMinerがまだブロックに含まれていないトランザクショントランザクションプールに管理できるようにしました。
次回はウォレットを実装します。


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

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