@mizumotokのブログ

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

javascriptでブロックチェーンをつくってみよう - 6 P2Pネットワーク

ビットコインJavascriptで実装しながら理解しましょう。
前回までにウォレットの実装をしました。今回はP2Pネットワークを実装してみます。これで完成です。

前回までの状態を再現

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

$ git clone https://github.com/mizumotok/blockchain-js.git
$ cd blockchain-js
$ git checkout 07_mining_reward


シリアライズ

ブロックとトランザクションはネットワーク上を流れるので、シリアライズできるようにしておきます。

src/blockchain/block.js

  static fromJSON(json: any): Block {
    const transactions = json.transactions.map(t => Transaction.fromJSON(t));
    return new Block(
      json.timestamp,
      json.prevHash,
      json.difficultyTarget,
      json.nonce,
      transactions,
      json.miningDuration,
    );
  }

  toJSON() {
    const transactions: Array<any> = this.transactions.map(t => t.toJSON());
    return ({
      timestamp: this.timestamp,
      prevHash: this.prevHash,
      difficultyTarget: this.difficultyTarget,
      nonce: this.nonce,
      transactions,
      miningDuration: this.miningDuration,
    });
  }

src/blockchain/transaction.js

  static fromJSON(json: any): Transaction {
    const tx = new Transaction();
    tx.id = json.id;
    tx.outputs = json.outputs;
    tx.input = json.input;
    tx.coinbase = json.coinbase;
    return tx;
  }

  toJSON(): any {
    return ({
      id: this.id,
      outputs: this.outputs,
      input: this.input,
      coinbase: this.coinbase,
    });
  }

テスト

いつも通りテストもつくります。

src/__tests__/blockchain/block.test.js

  it('toJSON test', () => {
    const timestamp = new Date();
    const block = new Block(timestamp, genesis.hash(), 0, 0, [], 0);
    expect(block.toJSON()).toEqual({
      timestamp,
      prevHash: genesis.hash(),
      difficultyTarget: 0,
      nonce: 0,
      transactions: [],
      miningDuration: 0,
    });
  });

  it('fromJSON test', () => {
    const timestamp = new Date();
    const block = new Block(timestamp, genesis.hash(), 0, 0, [], 0);
    const block2 = Block.fromJSON(block.toJSON());
    expect(block).toEqual(block2);
  });

src/__tests__/blockchain/transaction.test.js

  it('toJSON test', () => {
    const tx = Transaction.createTransaction(wallet, 'recipient-address', 10);
    expect(tx.toJSON()).toEqual({
      id: tx.id,
      outputs: tx.outputs,
      input: tx.input,
      coinbase: tx.coinbase,
    });
  });

  it('fromJSON test', () => {
    const tx = Transaction.createTransaction(wallet, 'recipient-address', 10);
    const tx2 = Transaction.fromJSON(tx.toJSON());
    expect(tx).toEqual(tx2);
  });

Webユーザインターフェース

P2Pネットワークを作る前に、ユーザインタフェースを作りましょう。今まではjtestでやってきましたが、ネットワークが絡むのでGUIを作って確認を分かりやすくします。

$ yarn add express 

src/config.js

const HTTP_PORT = process.env.HTTP_PORT || 3001;

expressでHTTPサーバをたてます。5つのAPIを用意しています。

src/index.js

// @flow

import express from 'express';
import bodyParser from 'body-parser';
import Blockchain from './blockchain';
import Wallet from './wallet';
import Miner from './miner';
import { HTTP_PORT } from './config';

const app = express();
app.use(bodyParser.urlencoded({ extended: true }));
app.use(express.static(`${__dirname}/public`));

const bc = new Blockchain();
const wallet = new Wallet(bc);
const miner = new Miner(bc, wallet.publicKey);

app.get('/blocks', (req, res) => {
  const r = bc.chain.map((b, i) => {
    const j = b.toJSON();
    j.hash = b.hash();
    j.height = i;
    return j;
  });
  res.json(r.reverse());
});

app.get('/transactions', (req, res) => {
  res.json(miner.transactionPool);
});

app.post('/transact', (req, res) => {
  const { recipient, amount } = req.body;
  const tx = wallet.createTransaction(recipient, Number(amount));
  if (tx) {
    miner.pushTransaction(tx);
  }
  res.redirect('/transactions');
});

app.get('/wallet', (req, res) => {
  res.json({ address: wallet.publicKey, blance: wallet.balance() });
});

app.post('/mine', (req, res) => {
  miner.mine();
  res.redirect('/blocks');
});

app.listen(HTTP_PORT, () => console.log(`Listening on port ${HTTP_PORT}`));

フロントエンドとして public/index.html を作ります。
ソースコードGitHub上で確認してください。

package.json

  "scripts": {
    "test": "jest",
    "flow": "flow",
    "lint": "eslint src",
    "start": "babel-node ./src"
  },

サーバを起動して http://localhost:3001/ にアクセスしてみると以下のような画面になります。

$ open http://localhost:3001/

f:id:mizumotok:20180426222855j:plain

GET系のAPIを叩くと現在の情報が表示されます。
POST /mine で最初のマイニングを行ってみましょう。walletのアドレスに報酬がもらえます。
POST /transact で任意のアドレスに送金してみましょう。再度 POST /mine でマイニングをするとウォレット内の残高が変わるのが確認できます。

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

P2Pネットワーク(ルーター

さあ、いよいよ最後です。
P2PはWebSocketを使いますので、wsをインストールします。

$ yarn add ws

P2Pネットワーク上を流れるデータのプロトコルを決めておきます。
メッセージタイプは3種類で十分でしょう。

src/router/message_types.js

const MESSAGE_TYPES = {
  BLOCK_CHAIN: 'BLOCK_CHAIN',
  TRANSACTION: 'TRANSACTION',
  MINED: 'MINED',
};

type MessageData = {
  type: string,
  payload: any,
};

export default MESSAGE_TYPES;
export type { MessageData };

ブロックチェーンの交換ができるようにしておきます。

src/blockchain/blockchain.js

  replaceChain(newChain: Array<Block>) {
    if (newChain.length < this.chain.length) {
      console.log('チェーンが短いため無視します。');
      return;
    }

    if (!Blockchain.isValidChain(newChain)) {
      console.log('チェーンが有効でないため無視します。');
      return;
    }

    this.chain = newChain;
    console.log('ブロックチェーンを更新しました。');
  }

src/__tests__/blockchain/blockchain.test.js

  it('replaceChain test', () => {
    blockchain.addBlock(newBlock);
    const genesis = Block.genesis();

    // 短いチェーンは無視
    blockchain.replaceChain([genesis]);
    expect(blockchain.chain).toHaveLength(2);

    // 有効でないチェーンは無視
    blockchain.replaceChain([genesis, new Block(0, 'xxx', 256, 0, [])]);
    expect(blockchain.chain[1]).toBe(newBlock);

    // 長くて有効なチェーンは置き換える
    const newBlock2 = new Block(new Date(), newBlock.hash(), 256, 0, [], MINING_DURATION);
    blockchain.replaceChain([genesis, newBlock, newBlock2]);
    expect(blockchain.chain).toHaveLength(3);
  });

P2PサーバーでWebSocketでつなげて、3種類のメッセージを送るメソッド(sendBlockchain、broadcastTransaction、broadcastMined)を作っておきます。

src/router/p2p_server.js

// @flow

import WebSocket from 'ws';
import Blockchain, { Transaction } from '../blockchain';
import MESSAGE_TYPES from './message_types';
import type { MessageData } from './message_types';
import { P2P_PORT, PEERS } from '../config';

class P2pServer {
  blockchain: Blockchain;
  messageHandler: Function;
  sockets: Array<WebSocket>;

  constructor(blockchain: Blockchain, messageHandler: Function, p2pEnabled: bool) {
    this.sockets = [];
    this.blockchain = blockchain;
    this.messageHandler = messageHandler;
    if (p2pEnabled) {
      this.listen();
    }
  }

  listen() {
    try {
      const server = new WebSocket.Server({ port: P2P_PORT });
      server.on('connection', socket => this.connect2Socket(socket));
      this.connect2Peers();
      console.log(`P2P Listening on ${P2P_PORT}`);
    } catch (e) {
      console.log('P2Pサーバの起動に失敗しました。');
    }
  }

  connect2Peers() {
    PEERS.forEach((peer) => {
      const socket = new WebSocket(peer);
      socket.on('open', () => this.connect2Socket(socket));
    });
  }

  connect2Socket(socket: WebSocket) {
    this.sockets.push(socket);
    console.log('Socket connected');
    socket.on('message', (message) => {
      console.log(`received from peer: ${message}`);
      const data: MessageData = JSON.parse(message);
      this.messageHandler(data);
    });
    this.sendBlockchain(socket);
  }

  sendBlockchain(socket: WebSocket) {
    P2pServer.sendData(socket, {
      type: MESSAGE_TYPES.BLOCK_CHAIN,
      payload: this.blockchain.chain.map(b => b.toJSON()),
    });
  }

  broadcastTransaction(transaction: Transaction) {
    this.sockets.forEach(socket => P2pServer.sendData(socket, {
      type: MESSAGE_TYPES.TRANSACTION,
      payload: transaction.toJSON(),
    }));
  }

  broadcastMined() {
    this.sockets.forEach(socket => P2pServer.sendData(socket, {
      type: MESSAGE_TYPES.MINED,
      payload: this.blockchain.chain.map(b => b.toJSON()),
    }));
  }

  static sendData(socket: WebSocket, data: MessageData) {
    socket.send(JSON.stringify(data));
  }
}

export default P2pServer;

ルーターの仕事は以下のとおりです。

  • Wallerからトランザクションを受け取ってP2Pネットワークに流す(自分のMinerにも知らせる)
  • Minerからマイニング完了の知らせを受け取ってP2Pネットワークに流す
  • P2Pネットワークから受け取ったメッセージを処理する

これを踏まえて実装すると以下のようになります。

src/router/index.js

// @flow

import WebSocket from 'ws';
import Blockchain, { Block, Transaction } from '../blockchain';
import Miner from '../miner';
import P2pServer from './p2p_server';
import MESSAGE_TYPES from './message_types';
import type { MessageData } from './message_types';

class Router {
  blockchain: Blockchain;
  miner: Miner;
  sockets: Array<WebSocket>;
  p2pServer: P2pServer;

  constructor(blockchain: Blockchain, p2pEnabled: bool = true) {
    this.blockchain = blockchain;
    this.p2pServer = new P2pServer(this.blockchain, this.messageHandler.bind(this), p2pEnabled);
  }

  subscribe(miner: Miner) {
    this.miner = miner;
  }

  pushTransaction(tx: Transaction) {
    if (this.miner) {
      this.miner.pushTransaction(tx);
    }
    this.p2pServer.broadcastTransaction(tx);
  }

  mineDone() {
    this.p2pServer.broadcastMined();
  }

  messageHandler(data: MessageData) {
    switch (data.type) {
      case MESSAGE_TYPES.BLOCK_CHAIN:
        this.blockchain.replaceChain(data.payload.map(o => Block.fromJSON(o)));
        break;
      case MESSAGE_TYPES.TRANSACTION:
        if (this.miner) {
          this.miner.pushTransaction(Transaction.fromJSON(data.payload));
        }
        break;
      case MESSAGE_TYPES.MINED:
        this.blockchain.replaceChain(data.payload.map(o => Block.fromJSON(o)));
        if (this.miner) {
          this.miner.clearTransactions();
        }
        break;
      default:
        break;
    }
  }
}

export default Router;

MinerとWalletをRouterを使うように書き換えます。

src/miner/index.js

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

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

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

  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);

    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);
    this.clearTransactions();
    this.router.mineDone();
  }

src/wallet/index.js

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

const ec = new EC('secp256k1');

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

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

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

テストが通るようにテストも修正しましょう。

$ yarn test

テスト時にはP2Pは邪魔なので、P2Pをオフにするモードもつけておきました。
GitHub上でソースコードを確認しておいてください。

Webユーザーインターフェースを使って確認

2つのノードを別々のコンソールで立ち上げます。

$ yarn start
$ HTTP_PORT=3002 P2P_PORT=5002 PEERS=ws://localhost:5001 yarn start
$ open http://localhost:3001
$ open http://localhost:3002

GET /walletで確認するとそれぞれ異なるウォレットを持っていることが分かります。
一方のノードでマイニング(POST /mine)をすると、もう一方のノードのブロックチェーンGET /blocks)にも反映されます。
一方のノードから、もう一方のノードのウォレットのアドレスに送金(POST /transact)することもできます。送金はマイニング後にブロックチェーンに記録されます。

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

まとめ

  • ブロックとトランザクションシリアライズしてネットワーク上で交換できるようにしました。
  • Webユーザインターフェースをつくりました。
  • Routerクラスを実装して、複数のノードをP2Pネットワークでつなげ、ブロックチェーンの同期ができるようにしました。

以上で、Javascriptブロックチェーンの完成です。お疲れさまでした!

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

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