ビットコインをJavascriptで実装しながら理解しましょう。
前回までにブロックをマイニングする処理を実装を行いました。今回はトランザクション(送金取引)を実装してみます。
開発の流れ
前回までの状態を再現
必要な場合は、前回の状態を再現しましょう。
$ 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ブランチに公開しています。
また、ソースの変更点はこちらで確認できます。