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を用意しています。
- GET /blocks (ブロックチェーンを表示)
- GET /transactions (トランザクションプールにあるトランザクションを表示)
- POST /transact (ウォレットからトランザクションを作成)
- GET /wallet (ウォレット内の残高とアドレスを表示)
- POST /mine (マイニング開始)
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/
GET系のAPIを叩くと現在の情報が表示されます。
POST /mine で最初のマイニングを行ってみましょう。walletのアドレスに報酬がもらえます。
POST /transact で任意のアドレスに送金してみましょう。再度 POST /mine でマイニングをするとウォレット内の残高が変わるのが確認できます。
ここまでのソースはGitHubの08_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版ブロックチェーンの完成です。お疲れさまでした!
- 作者: アンドレアス・M・アントノプロス,今井崇也,鳩貝淳一郎
- 出版社/メーカー: エヌティティ出版
- 発売日: 2016/07/14
- メディア: 大型本
- この商品を含むブログ (7件) を見る