ビットコインを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ブランチに公開しています。
また、ソースの変更点はこちらで確認できます。