├── .gitignore ├── images ├── logo.jpg ├── blockchain.png └── Blockchain-work.jpg ├── client └── src │ ├── .DS_Store │ ├── assets │ └── logo.png │ ├── history.js │ ├── index.html │ ├── components │ ├── Transaction.js │ ├── App.js │ ├── Blocks.js │ ├── Block.js │ ├── TransactionPool.js │ └── ConductTransaction.js │ ├── index.css │ └── index.js ├── .babelrc ├── util ├── crypto-hash.js ├── index.js └── crypto-hash.test.js ├── config.js ├── app ├── transaction-miner.js ├── pubsub.js └── pubsub.pubnub.js ├── scripts └── average-work.js ├── wallet ├── transaction-pool.js ├── index.js ├── transaction.js ├── transaction-pool.test.js ├── transaction.test.js └── index.test.js ├── blockchain ├── block.js ├── block.test.js ├── index.js └── index.test.js ├── package.json ├── index.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *node_modules* 2 | *dist* 3 | *.cache* 4 | *.cache* 5 | *dist* 6 | *secrets* -------------------------------------------------------------------------------- /images/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suzuki-kenji0627/Full-Stack-Blockchain/HEAD/images/logo.jpg -------------------------------------------------------------------------------- /client/src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suzuki-kenji0627/Full-Stack-Blockchain/HEAD/client/src/.DS_Store -------------------------------------------------------------------------------- /images/blockchain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suzuki-kenji0627/Full-Stack-Blockchain/HEAD/images/blockchain.png -------------------------------------------------------------------------------- /client/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suzuki-kenji0627/Full-Stack-Blockchain/HEAD/client/src/assets/logo.png -------------------------------------------------------------------------------- /images/Blockchain-work.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suzuki-kenji0627/Full-Stack-Blockchain/HEAD/images/Blockchain-work.jpg -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env", "react"], 3 | "plugins": ["transform-class-properties", "transform-object-rest-spread"] 4 | } -------------------------------------------------------------------------------- /client/src/history.js: -------------------------------------------------------------------------------- 1 | import createBrowserHistory from 'history/createBrowserHistory'; 2 | 3 | export default createBrowserHistory(); -------------------------------------------------------------------------------- /util/crypto-hash.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto'); 2 | 3 | const cryptoHash = (...inputs) => { 4 | const hash = crypto.createHash('sha256'); 5 | 6 | hash.update(inputs.map(input => JSON.stringify(input)).sort().join(' ')); 7 | 8 | return hash.digest('hex'); 9 | }; 10 | 11 | module.exports = cryptoHash; -------------------------------------------------------------------------------- /util/index.js: -------------------------------------------------------------------------------- 1 | const EC = require('elliptic').ec; 2 | const cryptoHash = require('./crypto-hash'); 3 | 4 | const ec = new EC('secp256k1'); 5 | 6 | const verifySignature = ({ publicKey, data, signature }) => { 7 | const keyFromPublic = ec.keyFromPublic(publicKey, 'hex'); 8 | 9 | return keyFromPublic.verify(cryptoHash(data), signature); 10 | }; 11 | 12 | module.exports = { ec, verifySignature, cryptoHash }; -------------------------------------------------------------------------------- /client/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | const MINE_RATE = 1000; 2 | const INITIAL_DIFFICULTY = 3; 3 | 4 | const GENESIS_DATA = { 5 | timestamp: 1, 6 | lastHash: '-----', 7 | hash: 'hash-one', 8 | difficulty: INITIAL_DIFFICULTY, 9 | nonce: 0, 10 | data: [] 11 | }; 12 | 13 | const STARTING_BALANCE = 1000; 14 | 15 | const REWARD_INPUT = { address: '*authorized-reward*' }; 16 | 17 | const MINING_REWARD = 50; 18 | 19 | module.exports = { 20 | GENESIS_DATA, 21 | MINE_RATE, 22 | STARTING_BALANCE, 23 | REWARD_INPUT, 24 | MINING_REWARD 25 | }; 26 | -------------------------------------------------------------------------------- /client/src/components/Transaction.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Transaction = ({ transaction }) => { 4 | const { input, outputMap } = transaction; 5 | const recipients = Object.keys(outputMap); 6 | 7 | return ( 8 |
9 |
From: {`${input.address.substring(0, 20)}...`} | Balance: {input.amount}
10 | { 11 | recipients.map(recipient => ( 12 |
13 | To: {`${recipient.substring(0, 20)}...`} | Sent: {outputMap[recipient]} 14 |
15 | )) 16 | } 17 |
18 | ); 19 | } 20 | 21 | export default Transaction; -------------------------------------------------------------------------------- /client/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #444; 3 | color: #fff; 4 | text-align: center; 5 | font-size: 20px; 6 | font-family: 'Quicksand'; 7 | padding-top: 5%; 8 | word-wrap: break-word; 9 | } 10 | 11 | .logo { 12 | width: 250px; 13 | height: 250px; 14 | } 15 | 16 | .App { 17 | display: flex; 18 | flex-direction: column; 19 | align-items: center; 20 | } 21 | 22 | .WalletInfo { 23 | width: 500px; 24 | } 25 | 26 | .Block { 27 | border: 1px solid #fff; 28 | padding: 5%; 29 | margin: 2%; 30 | } 31 | 32 | .Transaction { 33 | padding: 5%; 34 | } 35 | 36 | .ConductTransaction, .TransactionPool { 37 | margin: 10%; 38 | } 39 | 40 | a, a:hover { 41 | color: #e66; 42 | text-decoration: underline; 43 | } -------------------------------------------------------------------------------- /util/crypto-hash.test.js: -------------------------------------------------------------------------------- 1 | const cryptoHash = require('./crypto-hash'); 2 | 3 | describe('cryptoHash()', () => { 4 | it('generates a SHA-256 hashed output', () => { 5 | expect(cryptoHash('foo')) 6 | .toEqual('b2213295d564916f89a6a42455567c87c3f480fcd7a1c15e220f17d7169a790b'); 7 | }); 8 | 9 | it('produces the same hash with the same input arguments in any order', () => { 10 | expect(cryptoHash('one', 'two', 'three')) 11 | .toEqual(cryptoHash('three', 'one', 'two')); 12 | }); 13 | 14 | it('produces a unique hash when the properties have changed on an input', () => { 15 | const foo = {}; 16 | const originalHash = cryptoHash(foo); 17 | foo['a'] = 'a'; 18 | 19 | expect(cryptoHash(foo)).not.toEqual(originalHash); 20 | }); 21 | }); -------------------------------------------------------------------------------- /app/transaction-miner.js: -------------------------------------------------------------------------------- 1 | const Transaction = require('../wallet/transaction'); 2 | 3 | class TransactionMiner { 4 | constructor({ blockchain, transactionPool, wallet, pubsub }) { 5 | this.blockchain = blockchain; 6 | this.transactionPool = transactionPool; 7 | this.wallet = wallet; 8 | this.pubsub = pubsub; 9 | } 10 | 11 | mineTransactions() { 12 | const validTransactions = this.transactionPool.validTransactions(); 13 | 14 | validTransactions.push( 15 | Transaction.rewardTransaction({ minerWallet: this.wallet }) 16 | ); 17 | 18 | this.blockchain.addBlock({ data: validTransactions }); 19 | 20 | this.pubsub.broadcastChain(); 21 | 22 | this.transactionPool.clear(); 23 | } 24 | } 25 | 26 | module.exports = TransactionMiner; 27 | -------------------------------------------------------------------------------- /client/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import { Router, Switch, Route } from 'react-router-dom'; 4 | import history from './history'; 5 | import App from './components/App'; 6 | import Blocks from './components/Blocks'; 7 | import ConductTransaction from './components/ConductTransaction'; 8 | import TransactionPool from './components/TransactionPool'; 9 | import './index.css'; 10 | 11 | render( 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | , 20 | document.getElementById('root') 21 | ); -------------------------------------------------------------------------------- /scripts/average-work.js: -------------------------------------------------------------------------------- 1 | const Blockchain = require('../blockchain'); 2 | 3 | const blockchain = new Blockchain(); 4 | 5 | blockchain.addBlock({ data: 'initial' }); 6 | 7 | console.log('first block', blockchain.chain[blockchain.chain.length-1]); 8 | 9 | let prevTimestamp, nextTimestamp, nextBlock, timeDiff, average; 10 | 11 | const times = []; 12 | 13 | for (let i=0; i<10000; i++) { 14 | prevTimestamp = blockchain.chain[blockchain.chain.length-1].timestamp; 15 | 16 | blockchain.addBlock({ data: `block ${i}`}); 17 | nextBlock = blockchain.chain[blockchain.chain.length-1]; 18 | 19 | nextTimestamp = nextBlock.timestamp; 20 | timeDiff = nextTimestamp - prevTimestamp; 21 | times.push(timeDiff); 22 | 23 | average = times.reduce((total, num) => (total + num))/times.length; 24 | 25 | console.log(`Time to mine block: ${timeDiff}ms. Difficulty: ${nextBlock.difficulty}. Average time: ${average}ms`); 26 | } -------------------------------------------------------------------------------- /client/src/components/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import logo from '../assets/logo.png'; 4 | 5 | class App extends Component { 6 | state = { walletInfo: {} }; 7 | 8 | componentDidMount() { 9 | fetch(`${document.location.origin}/api/wallet-info`) 10 | .then(response => response.json()) 11 | .then(json => this.setState({ walletInfo: json })); 12 | } 13 | 14 | render() { 15 | const { address, balance } = this.state.walletInfo; 16 | 17 | return ( 18 |
19 | 20 |
21 |
22 | Welcome to the blockchain... 23 |
24 |
25 |
Blocks
26 |
Conduct a Transaction
27 |
Transaction Pool
28 |
29 |
30 |
Address: {address}
31 |
Balance: {balance}
32 |
33 |
34 | ); 35 | } 36 | } 37 | 38 | export default App; -------------------------------------------------------------------------------- /wallet/transaction-pool.js: -------------------------------------------------------------------------------- 1 | const Transaction = require('./transaction'); 2 | 3 | class TransactionPool { 4 | constructor() { 5 | this.transactionMap = {}; 6 | } 7 | 8 | clear() { 9 | this.transactionMap = {}; 10 | } 11 | 12 | setTransaction(transaction) { 13 | this.transactionMap[transaction.id] = transaction; 14 | } 15 | 16 | setMap(transactionMap) { 17 | this.transactionMap = transactionMap; 18 | } 19 | 20 | existingTransaction({ inputAddress }) { 21 | const transactions = Object.values(this.transactionMap); 22 | 23 | return transactions.find(transaction => transaction.input.address === inputAddress); 24 | } 25 | 26 | validTransactions() { 27 | return Object.values(this.transactionMap).filter( 28 | transaction => Transaction.validTransaction(transaction) 29 | ); 30 | } 31 | 32 | clearBlockchainTransactions({ chain }) { 33 | for (let i=1; i MINE_RATE ) return difficulty - 1; 41 | 42 | return difficulty + 1; 43 | } 44 | } 45 | 46 | module.exports = Block; 47 | -------------------------------------------------------------------------------- /wallet/index.js: -------------------------------------------------------------------------------- 1 | const Transaction = require('./transaction'); 2 | const { STARTING_BALANCE } = require('../config'); 3 | const { ec, cryptoHash } = require('../util'); 4 | 5 | class Wallet { 6 | constructor() { 7 | this.balance = STARTING_BALANCE; 8 | 9 | this.keyPair = ec.genKeyPair(); 10 | 11 | this.publicKey = this.keyPair.getPublic().encode('hex'); 12 | } 13 | 14 | sign(data) { 15 | return this.keyPair.sign(cryptoHash(data)) 16 | } 17 | 18 | createTransaction({ recipient, amount, chain }) { 19 | if (chain) { 20 | this.balance = Wallet.calculateBalance({ 21 | chain, 22 | address: this.publicKey 23 | }); 24 | } 25 | 26 | if (amount > this.balance) { 27 | throw new Error('Amount exceeds balance'); 28 | } 29 | 30 | return new Transaction({ senderWallet: this, recipient, amount }); 31 | } 32 | 33 | static calculateBalance({ chain, address }) { 34 | let hasConductedTransaction = false; 35 | let outputsTotal = 0; 36 | 37 | for (let i=chain.length-1; i>0; i--) { 38 | const block = chain[i]; 39 | 40 | for (let transaction of block.data) { 41 | if (transaction.input.address === address) { 42 | hasConductedTransaction = true; 43 | } 44 | 45 | const addressOutput = transaction.outputMap[address]; 46 | 47 | if (addressOutput) { 48 | outputsTotal = outputsTotal + addressOutput; 49 | } 50 | } 51 | 52 | if (hasConductedTransaction) { 53 | break; 54 | } 55 | } 56 | 57 | return hasConductedTransaction ? outputsTotal : STARTING_BALANCE + outputsTotal; 58 | } 59 | }; 60 | 61 | module.exports = Wallet; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cryptochain", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "jest --watchAll", 8 | "start": "npm run build-client & node index.js", 9 | "dev": "npm run dev-client & npm run start-redis && cross-env ENV='development' nodemon index.js", 10 | "dev-peer": "cross-env GENERATE_PEER_PORT='true' ENV='development' nodemon index.js", 11 | "start-redis": "redis-server --daemonize yes", 12 | "build-client": "npm run clean && parcel build client/src/index.html --out-dir client/dist", 13 | "dev-client": "npm run clean && parcel client/src/index.html --out-dir client/dist", 14 | "clean": "rm -rf .cache client/dist" 15 | }, 16 | "jest": { 17 | "testEnvironment": "node" 18 | }, 19 | "keywords": [], 20 | "author": "", 21 | "license": "ISC", 22 | "devDependencies": { 23 | "cross-env": "^5.2.0", 24 | "jest": "^23.6.0", 25 | "nodemon": "^1.18.4" 26 | }, 27 | "dependencies": { 28 | "babel-core": "^6.26.3", 29 | "babel-plugin-transform-class-properties": "^6.24.1", 30 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 31 | "babel-preset-env": "^1.7.0", 32 | "babel-preset-react": "^6.24.1", 33 | "body-parser": "^1.18.3", 34 | "elliptic": "^6.4.1", 35 | "express": "^4.16.3", 36 | "hex-to-binary": "^1.0.1", 37 | "history": "^4.7.2", 38 | "parcel-bundler": "^1.10.3", 39 | "pubnub": "^4.21.6", 40 | "react": "^16.6.0", 41 | "react-bootstrap": "^0.32.4", 42 | "react-dom": "^16.6.0", 43 | "react-router-dom": "^4.3.1", 44 | "redis": "^2.8.0", 45 | "request": "^2.88.0", 46 | "uuid": "^3.3.2" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /client/src/components/Blocks.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Button } from 'react-bootstrap'; 3 | import { Link } from 'react-router-dom'; 4 | import Block from './Block'; 5 | 6 | class Blocks extends Component { 7 | state = { blocks: [], paginatedId: 1, blocksLength: 0 }; 8 | 9 | componentDidMount() { 10 | fetch(`${document.location.origin}/api/blocks/length`) 11 | .then(response => response.json()) 12 | .then(json => this.setState({ blocksLength: json })); 13 | 14 | this.fetchPaginatedBlocks(this.state.paginatedId)(); 15 | } 16 | 17 | fetchPaginatedBlocks = paginatedId => () => { 18 | fetch(`${document.location.origin}/api/blocks/${paginatedId}`) 19 | .then(response => response.json()) 20 | .then(json => this.setState({ blocks: json })); 21 | } 22 | 23 | render() { 24 | console.log('this.state', this.state); 25 | 26 | return ( 27 |
28 |
Home
29 |

Blocks

30 |
31 | { 32 | [...Array(Math.ceil(this.state.blocksLength/5)).keys()].map(key => { 33 | const paginatedId = key+1; 34 | 35 | return ( 36 | 37 | {' '} 40 | 41 | ) 42 | }) 43 | } 44 |
45 | { 46 | this.state.blocks.map(block => { 47 | return ( 48 | 49 | ); 50 | }) 51 | } 52 |
53 | ); 54 | } 55 | } 56 | 57 | export default Blocks; -------------------------------------------------------------------------------- /client/src/components/Block.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Button } from 'react-bootstrap'; 3 | import Transaction from './Transaction'; 4 | 5 | class Block extends Component { 6 | state = { displayTransaction: false }; 7 | 8 | toggleTransaction = () => { 9 | this.setState({ displayTransaction: !this.state.displayTransaction }); 10 | } 11 | 12 | get displayTransaction() { 13 | const { data } = this.props.block; 14 | 15 | const stringifiedData = JSON.stringify(data); 16 | 17 | const dataDisplay = stringifiedData.length > 35 ? 18 | `${stringifiedData.substring(0, 35)}...` : 19 | stringifiedData; 20 | 21 | if (this.state.displayTransaction) { 22 | return ( 23 |
24 | { 25 | data.map(transaction => ( 26 |
27 |
28 | 29 |
30 | )) 31 | } 32 |
33 | 40 |
41 | ) 42 | } 43 | 44 | return ( 45 |
46 |
Data: {dataDisplay}
47 | 54 |
55 | ); 56 | } 57 | 58 | render() { 59 | const { timestamp, hash } = this.props.block; 60 | 61 | const hashDisplay = `${hash.substring(0, 15)}...`; 62 | 63 | return ( 64 |
65 |
Hash: {hashDisplay}
66 |
Timestamp: {new Date(timestamp).toLocaleString()}
67 | {this.displayTransaction} 68 |
69 | ); 70 | } 71 | }; 72 | 73 | export default Block; -------------------------------------------------------------------------------- /client/src/components/TransactionPool.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Button } from 'react-bootstrap'; 3 | import Transaction from './Transaction'; 4 | import { Link } from 'react-router-dom'; 5 | import history from '../history'; 6 | 7 | const POLL_INERVAL_MS = 10000; 8 | 9 | class TransactionPool extends Component { 10 | state = { transactionPoolMap: {} }; 11 | 12 | fetchTransactionPoolMap = () => { 13 | fetch(`${document.location.origin}/api/transaction-pool-map`) 14 | .then(response => response.json()) 15 | .then(json => this.setState({ transactionPoolMap: json })); 16 | } 17 | 18 | fetchMineTransactions = () => { 19 | fetch(`${document.location.origin}/api/mine-transactions`) 20 | .then(response => { 21 | if (response.status === 200) { 22 | alert('success'); 23 | history.push('/blocks'); 24 | } else { 25 | alert('The mine-transactions block request did not complete.'); 26 | } 27 | }); 28 | } 29 | 30 | componentDidMount() { 31 | this.fetchTransactionPoolMap(); 32 | 33 | this.fetchPoolMapInterval = setInterval( 34 | () => this.fetchTransactionPoolMap(), 35 | POLL_INERVAL_MS 36 | ); 37 | } 38 | 39 | componentWillUnmount() { 40 | clearInterval(this.fetchPoolMapInterval); 41 | } 42 | 43 | render() { 44 | return ( 45 |
46 |
Home
47 |

Transaction Pool

48 | { 49 | Object.values(this.state.transactionPoolMap).map(transaction => { 50 | return ( 51 |
52 |
53 | 54 |
55 | ) 56 | }) 57 | } 58 |
59 | 65 |
66 | ) 67 | } 68 | } 69 | 70 | export default TransactionPool; -------------------------------------------------------------------------------- /app/pubsub.js: -------------------------------------------------------------------------------- 1 | const redis = require('redis'); 2 | 3 | const CHANNELS = { 4 | TEST: 'TEST', 5 | BLOCKCHAIN: 'BLOCKCHAIN', 6 | TRANSACTION: 'TRANSACTION' //---------11--------// 7 | }; 8 | 9 | class PubSub { 10 | constructor({ blockchain, transactionPool, redisUrl }) { 11 | this.blockchain = blockchain; 12 | this.transactionPool = transactionPool; 13 | 14 | this.publisher = redis.createClient(redisUrl); 15 | this.subscriber = redis.createClient(redisUrl); 16 | 17 | this.subscribeToChannels(); 18 | 19 | this.subscriber.on( 20 | 'message', 21 | (channel, message) => this.handleMessage(channel, message) 22 | ); 23 | } 24 | 25 | handleMessage(channel, message) { 26 | console.log(`Message received. Channel: ${channel}. Message: ${message}.`); 27 | 28 | const parsedMessage = JSON.parse(message); 29 | 30 | switch(channel) { 31 | case CHANNELS.BLOCKCHAIN: 32 | this.blockchain.replaceChain(parsedMessage, true, () => { 33 | this.transactionPool.clearBlockchainTransactions({ 34 | chain: parsedMessage 35 | }); 36 | }); 37 | break; 38 | case CHANNELS.TRANSACTION: 39 | this.transactionPool.setTransaction(parsedMessage); 40 | break; 41 | default: 42 | return; 43 | } 44 | } 45 | 46 | subscribeToChannels() { 47 | Object.values(CHANNELS).forEach(channel => { 48 | this.subscriber.subscribe(channel); 49 | }); 50 | } 51 | 52 | publish({ channel, message }) { 53 | this.subscriber.unsubscribe(channel, () => { 54 | this.publisher.publish(channel, message, () => { 55 | this.subscriber.subscribe(channel); 56 | }); 57 | }); 58 | } 59 | 60 | broadcastChain() { 61 | this.publish({ 62 | channel: CHANNELS.BLOCKCHAIN, 63 | message: JSON.stringify(this.blockchain.chain) 64 | }); 65 | } 66 | 67 | broadcastTransaction(transaction) { 68 | this.publish({ 69 | channel: CHANNELS.TRANSACTION, 70 | message: JSON.stringify(transaction) 71 | }); 72 | } 73 | } 74 | 75 | module.exports = PubSub; 76 | -------------------------------------------------------------------------------- /wallet/transaction.js: -------------------------------------------------------------------------------- 1 | const uuid = require('uuid/v1'); 2 | const { verifySignature } = require('../util'); 3 | const { REWARD_INPUT, MINING_REWARD } = require('../config'); 4 | 5 | class Transaction { 6 | constructor({ senderWallet, recipient, amount, outputMap, input }) { 7 | this.id = uuid(); 8 | this.outputMap = outputMap || this.createOutputMap({ senderWallet, recipient, amount }); 9 | this.input = input || this.createInput({ senderWallet, outputMap: this.outputMap }); 10 | } 11 | 12 | createOutputMap({ senderWallet, recipient, amount }) { 13 | const outputMap = {}; 14 | 15 | outputMap[recipient] = amount; 16 | outputMap[senderWallet.publicKey] = senderWallet.balance - amount; 17 | 18 | return outputMap; 19 | } 20 | 21 | createInput({ senderWallet, outputMap }) { 22 | return { 23 | timestamp: Date.now(), 24 | amount: senderWallet.balance, 25 | address: senderWallet.publicKey, 26 | signature: senderWallet.sign(outputMap) 27 | }; 28 | } 29 | 30 | update({ senderWallet, recipient, amount }) { 31 | if (amount > this.outputMap[senderWallet.publicKey]) { 32 | throw new Error('Amount exceeds balance'); 33 | } 34 | 35 | if (!this.outputMap[recipient]) { 36 | this.outputMap[recipient] = amount; 37 | } else { 38 | this.outputMap[recipient] = this.outputMap[recipient] + amount; 39 | } 40 | 41 | this.outputMap[senderWallet.publicKey] = 42 | this.outputMap[senderWallet.publicKey] - amount; 43 | 44 | this.input = this.createInput({ senderWallet, outputMap: this.outputMap }); 45 | } 46 | 47 | static validTransaction(transaction) { 48 | const { input: { address, amount, signature }, outputMap } = transaction; 49 | 50 | const outputTotal = Object.values(outputMap) 51 | .reduce((total, outputAmount) => total + outputAmount); 52 | 53 | if (amount !== outputTotal) { 54 | console.error(`Invalid transaction from ${address}`); 55 | return false; 56 | } 57 | 58 | if (!verifySignature({ publicKey: address, data: outputMap, signature })) { 59 | console.error(`Invalid signature from ${address}`); 60 | return false; 61 | } 62 | 63 | return true; 64 | } 65 | 66 | static rewardTransaction({ minerWallet }) { 67 | return new this({ 68 | input: REWARD_INPUT, 69 | outputMap: { [minerWallet.publicKey]: MINING_REWARD } 70 | }); 71 | } 72 | } 73 | 74 | module.exports = Transaction; -------------------------------------------------------------------------------- /client/src/components/ConductTransaction.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { FormGroup, FormControl, Button } from 'react-bootstrap'; 3 | import { Link } from 'react-router-dom'; 4 | import history from '../history'; 5 | 6 | class ConductTransaction extends Component { 7 | state = { recipient: '', amount: 0, knownAddresses: [] }; 8 | 9 | componentDidMount() { 10 | fetch(`${document.location.origin}/api/known-addresses`) 11 | .then(response => response.json()) 12 | .then(json => this.setState({ knownAddresses: json })); 13 | } 14 | 15 | updateRecipient = event => { 16 | this.setState({ recipient: event.target.value }); 17 | } 18 | 19 | updateAmount = event => { 20 | this.setState({ amount: Number(event.target.value) }); 21 | } 22 | 23 | conductTransaction = () => { 24 | const { recipient, amount } = this.state; 25 | 26 | fetch(`${document.location.origin}/api/transact`, { 27 | method: 'POST', 28 | headers: { 'Content-Type': 'application/json' }, 29 | body: JSON.stringify({ recipient, amount }) 30 | }).then(response => response.json()) 31 | .then(json => { 32 | alert(json.message || json.type); 33 | history.push('/transaction-pool'); 34 | }); 35 | } 36 | 37 | render() { 38 | return ( 39 |
40 | Home 41 |

Conduct a Transaction

42 |
43 |

Known Addresses

44 | { 45 | this.state.knownAddresses.map(knownAddress => { 46 | return ( 47 |
48 |
{knownAddress}
49 |
50 |
51 | ); 52 | }) 53 | } 54 |
55 | 56 | 62 | 63 | 64 | 70 | 71 |
72 | 78 |
79 |
80 | ) 81 | } 82 | }; 83 | 84 | export default ConductTransaction; -------------------------------------------------------------------------------- /app/pubsub.pubnub.js: -------------------------------------------------------------------------------- 1 | const PubNub = require('pubnub'); 2 | 3 | const credentials = { 4 | publishKey: 'pub-c-ec30f7ec-578f-4aa2-81c8-59077fb942c4', 5 | subscribeKey: 'sub-c-eda4e664-027b-11e9-a39c-e60c31199fb2', 6 | secretKey: 'sec-c-OWQwMTg1MGMtY2U2YS00ZmVlLWE1YmEtOTVmMWZmN2ZiOWVm' 7 | }; 8 | 9 | const CHANNELS = { 10 | TEST: 'TEST', 11 | BLOCKCHAIN: 'BLOCKCHAIN', 12 | TRANSACTION: 'TRANSACTION' 13 | }; 14 | 15 | class PubSub { 16 | constructor({ blockchain, transactionPool, wallet }) { 17 | this.blockchain = blockchain; 18 | this.transactionPool = transactionPool; 19 | this.wallet = wallet; 20 | 21 | this.pubnub = new PubNub(credentials); 22 | 23 | this.pubnub.subscribe({ channels: Object.values(CHANNELS) }); 24 | 25 | this.pubnub.addListener(this.listener()); 26 | } 27 | 28 | broadcastChain() { 29 | this.publish({ 30 | channel: CHANNELS.BLOCKCHAIN, 31 | message: JSON.stringify(this.blockchain.chain) 32 | }); 33 | } 34 | 35 | broadcastTransaction(transaction) { 36 | this.publish({ 37 | channel: CHANNELS.TRANSACTION, 38 | message: JSON.stringify(transaction) 39 | }); 40 | } 41 | 42 | subscribeToChannels() { 43 | this.pubnub.subscribe({ 44 | channels: [Object.values(CHANNELS)] 45 | }); 46 | } 47 | 48 | listener() { 49 | return { 50 | message: messageObject => { 51 | const { channel, message } = messageObject; 52 | 53 | console.log(`Message received. Channel: ${channel}. Message: ${message}`); 54 | const parsedMessage = JSON.parse(message); 55 | 56 | switch(channel) { 57 | case CHANNELS.BLOCKCHAIN: 58 | this.blockchain.replaceChain(parsedMessage, true, () => { 59 | this.transactionPool.clearBlockchainTransactions( 60 | { chain: parsedMessage.chain } 61 | ); 62 | }); 63 | break; 64 | case CHANNELS.TRANSACTION: 65 | if (!this.transactionPool.existingTransaction({ 66 | inputAddress: this.wallet.publicKey 67 | })) { 68 | this.transactionPool.setTransaction(parsedMessage); 69 | } 70 | break; 71 | default: 72 | return; 73 | } 74 | } 75 | } 76 | } 77 | 78 | publish({ channel, message }) { 79 | // there is an unsubscribe function in pubnub 80 | // but it doesn't have a callback that fires after success 81 | // therefore, redundant publishes to the same local subscriber will be accepted as noisy no-ops 82 | this.pubnub.publish({ message, channel }); 83 | } 84 | 85 | broadcastChain() { 86 | this.publish({ 87 | channel: CHANNELS.BLOCKCHAIN, 88 | message: JSON.stringify(this.blockchain.chain) 89 | }); 90 | } 91 | 92 | broadcastTransaction(transaction) { 93 | this.publish({ 94 | channel: CHANNELS.TRANSACTION, 95 | message: JSON.stringify(transaction) 96 | }); 97 | } 98 | } 99 | 100 | module.exports = PubSub; 101 | -------------------------------------------------------------------------------- /wallet/transaction-pool.test.js: -------------------------------------------------------------------------------- 1 | const TransactionPool = require('./transaction-pool'); 2 | const Transaction = require('./transaction'); 3 | const Wallet = require('./index'); 4 | const Blockchain = require('../blockchain'); 5 | 6 | describe('TransactionPool', () => { 7 | let transactionPool, transaction, senderWallet; 8 | 9 | beforeEach(() => { 10 | transactionPool = new TransactionPool(); 11 | senderWallet = new Wallet(); 12 | transaction = new Transaction({ 13 | senderWallet, 14 | recipient: 'fake-recipient', 15 | amount: 50 16 | }); 17 | }); 18 | 19 | describe('setTransaction()', () => { 20 | it('adds a transaction', () => { 21 | transactionPool.setTransaction(transaction); 22 | 23 | expect(transactionPool.transactionMap[transaction.id]) 24 | .toBe(transaction); 25 | }); 26 | }); 27 | 28 | describe('existingTransaction()', () => { 29 | it('returns an existing transaction given an input address', () => { 30 | transactionPool.setTransaction(transaction); 31 | 32 | expect( 33 | transactionPool.existingTransaction({ inputAddress: senderWallet.publicKey }) 34 | ).toBe(transaction); 35 | }); 36 | }); 37 | 38 | describe('validTransactions()', () => { 39 | let validTransactions, errorMock; 40 | 41 | beforeEach(() => { 42 | validTransactions = []; 43 | errorMock = jest.fn(); 44 | global.console.error = errorMock; 45 | 46 | for (let i=0; i<10; i++) { 47 | transaction = new Transaction({ 48 | senderWallet, 49 | recipient: 'any-recipient', 50 | amount: 30 51 | }); 52 | 53 | if (i%3===0) { 54 | transaction.input.amount = 999999; 55 | } else if (i%3===1) { 56 | transaction.input.signature = new Wallet().sign('foo'); 57 | } else { 58 | validTransactions.push(transaction); 59 | } 60 | 61 | transactionPool.setTransaction(transaction); 62 | } 63 | }); 64 | 65 | it('returns valid transaction', () => { 66 | expect(transactionPool.validTransactions()).toEqual(validTransactions); 67 | }); 68 | 69 | it('logs errors for the invalid transactions', () => { 70 | transactionPool.validTransactions(); 71 | expect(errorMock).toHaveBeenCalled(); 72 | }); 73 | }); 74 | 75 | describe('clear()', () => { 76 | it('clears the transactions', () => { 77 | transactionPool.clear(); 78 | 79 | expect(transactionPool.transactionMap).toEqual({}); 80 | }); 81 | }); 82 | 83 | describe('clearBlockchainTransactions()', () => { 84 | it('clears the pool of any existing blockchain transactions', () => { 85 | const blockchain = new Blockchain(); 86 | const expectedTransactionMap = {}; 87 | 88 | for (let i=0; i<6; i++) { 89 | const transaction = new Wallet().createTransaction({ 90 | recipient: 'foo', amount: 20 91 | }); 92 | 93 | transactionPool.setTransaction(transaction); 94 | 95 | if (i%2===0) { 96 | blockchain.addBlock({ data: [transaction] }) 97 | } else { 98 | expectedTransactionMap[transaction.id] = transaction; 99 | } 100 | } 101 | 102 | transactionPool.clearBlockchainTransactions({ chain: blockchain.chain }); 103 | 104 | expect(transactionPool.transactionMap).toEqual(expectedTransactionMap); 105 | }); 106 | }); 107 | }); -------------------------------------------------------------------------------- /blockchain/block.test.js: -------------------------------------------------------------------------------- 1 | const hexToBinary = require('hex-to-binary'); 2 | const Block = require('./block'); 3 | const { GENESIS_DATA, MINE_RATE } = require('../config'); 4 | const { cryptoHash } = require('../util'); 5 | 6 | describe('Block', () => { 7 | const timestamp = 2000; 8 | const lastHash = 'foo-hash'; 9 | const hash = 'bar-hash'; 10 | const data = ['blockchain', 'data']; 11 | const nonce = 1; 12 | const difficulty = 1; 13 | const block = new Block({ timestamp, lastHash, hash, data, nonce, difficulty }); 14 | 15 | it('has a timestamp, lastHash, hash, and data property', () => { 16 | expect(block.timestamp).toEqual(timestamp); 17 | expect(block.lastHash).toEqual(lastHash); 18 | expect(block.hash).toEqual(hash); 19 | expect(block.data).toEqual(data); 20 | expect(block.nonce).toEqual(nonce); 21 | expect(block.difficulty).toEqual(difficulty); 22 | }); 23 | 24 | describe('genesis()', () => { 25 | const genesisBlock = Block.genesis(); 26 | 27 | it('returns a Block instance', () => { 28 | expect(genesisBlock instanceof Block).toBe(true); 29 | }); 30 | 31 | it('returns the genesis data', () => { 32 | expect(genesisBlock).toEqual(GENESIS_DATA); 33 | }); 34 | }); 35 | 36 | describe('mineBlock()', () => { 37 | const lastBlock = Block.genesis(); 38 | const data = 'mined data'; 39 | const minedBlock = Block.mineBlock({ lastBlock, data }); 40 | 41 | it('returns a Block instance', () => { 42 | expect(minedBlock instanceof Block).toBe(true); 43 | }); 44 | 45 | it('sets the `lastHash` to be the `hash` of the lastBlock', () => { 46 | expect(minedBlock.lastHash).toEqual(lastBlock.hash); 47 | }); 48 | 49 | it('sets the `data`', () => { 50 | expect(minedBlock.data).toEqual(data); 51 | }); 52 | 53 | it('sets a `timestamp`', () => { 54 | expect(minedBlock.timestamp).not.toEqual(undefined); 55 | }); 56 | 57 | it('creates a SHA-256 `hash` based on the proper inputs', () => { 58 | expect(minedBlock.hash) 59 | .toEqual( 60 | cryptoHash( 61 | minedBlock.timestamp, 62 | minedBlock.nonce, 63 | minedBlock.difficulty, 64 | lastBlock.hash, 65 | data 66 | ) 67 | ); 68 | }); 69 | 70 | it('sets a `hash` that matches the difficulty criteria', () => { 71 | expect(hexToBinary(minedBlock.hash).substring(0, minedBlock.difficulty)) 72 | .toEqual('0'.repeat(minedBlock.difficulty)); 73 | }); 74 | 75 | it('adjusts the difficulty', () => { 76 | const possibleResults = [lastBlock.difficulty+1, lastBlock.difficulty-1]; 77 | 78 | expect(possibleResults.includes(minedBlock.difficulty)).toBe(true); 79 | }); 80 | }); 81 | 82 | describe('adjustDifficulty()', () => { 83 | it('raises the difficulty for a quickly mined block', () => { 84 | expect(Block.adjustDifficulty({ 85 | originalBlock: block, timestamp: block.timestamp + MINE_RATE - 100 86 | })).toEqual(block.difficulty+1); 87 | }); 88 | 89 | it('lowers the difficulty for a slowly mined block', () => { 90 | expect(Block.adjustDifficulty({ 91 | originalBlock: block, timestamp: block.timestamp + MINE_RATE + 100 92 | })).toEqual(block.difficulty-1); 93 | }); 94 | 95 | it('has a lower limit of 1', () => { 96 | block.difficulty = -1; 97 | 98 | expect(Block.adjustDifficulty({ originalBlock: block })).toEqual(1); 99 | }); 100 | }); 101 | }); -------------------------------------------------------------------------------- /blockchain/index.js: -------------------------------------------------------------------------------- 1 | const Block = require('./block'); 2 | const Transaction = require('../wallet/transaction'); 3 | const Wallet = require('../wallet'); 4 | const { cryptoHash } = require('../util'); 5 | const { REWARD_INPUT, MINING_REWARD } = require('../config'); 6 | 7 | class Blockchain { 8 | constructor() { 9 | this.chain = [Block.genesis()]; 10 | } 11 | 12 | addBlock({ data }) { 13 | const newBlock = Block.mineBlock({ 14 | lastBlock: this.chain[this.chain.length-1], 15 | data 16 | }); 17 | 18 | this.chain.push(newBlock); 19 | } 20 | 21 | replaceChain(chain, validateTransactions, onSuccess) { 22 | if (chain.length <= this.chain.length) { 23 | console.error('The incoming chain must be longer'); 24 | return; 25 | } 26 | 27 | if (!Blockchain.isValidChain(chain)) { 28 | console.error('The incoming chain must be valid'); 29 | return; 30 | } 31 | 32 | if (validateTransactions && !this.validTransactionData({ chain })) { 33 | console.error('The incoming chain has invalid data'); 34 | return; 35 | } 36 | 37 | if (onSuccess) onSuccess(); 38 | console.log('replacing chain with', chain); 39 | this.chain = chain; 40 | } 41 | 42 | validTransactionData({ chain }) { 43 | for (let i=1; i 1) { 53 | console.error('Miner rewards exceed limit'); 54 | return false; 55 | } 56 | 57 | if (Object.values(transaction.outputMap)[0] !== MINING_REWARD) { 58 | console.error('Miner reward amount is invalid'); 59 | return false; 60 | } 61 | } else { 62 | if (!Transaction.validTransaction(transaction)) { 63 | console.error('Invalid transaction'); 64 | return false; 65 | } 66 | 67 | const trueBalance = Wallet.calculateBalance({ 68 | chain: this.chain, 69 | address: transaction.input.address 70 | }); 71 | 72 | if (transaction.input.amount !== trueBalance) { 73 | console.error('Invalid input amount'); 74 | return false; 75 | } 76 | 77 | if (transactionSet.has(transaction)) { 78 | console.error('An identical transaction appears more than once in the block'); 79 | return false; 80 | } else { 81 | transactionSet.add(transaction); 82 | } 83 | } 84 | } 85 | } 86 | 87 | return true; 88 | } 89 | 90 | static isValidChain(chain) { 91 | if (JSON.stringify(chain[0]) !== JSON.stringify(Block.genesis())) { 92 | return false 93 | }; 94 | 95 | for (let i=1; i 1) return false; 107 | } 108 | 109 | return true; 110 | } 111 | } 112 | 113 | module.exports = Blockchain; -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const bodyParser = require('body-parser'); 2 | const express = require('express'); 3 | const request = require('request'); 4 | const path = require('path'); 5 | const Blockchain = require('./blockchain'); 6 | const PubSub = require('./app/pubsub'); 7 | const TransactionPool = require('./wallet/transaction-pool'); 8 | const Wallet = require('./wallet'); 9 | const TransactionMiner = require('./app/transaction-miner'); 10 | 11 | const isDevelopment = process.env.ENV === 'development'; 12 | 13 | const REDIS_URL = isDevelopment ? 14 | 'redis://127.0.0.1:6379' : 15 | 'redis://:p4433b3c407606ac98549b6b826f91bdc438f5185c12c4709ceda18007f7dabae@ec2-52-200-161-135.compute-1.amazonaws.com:9609' 16 | const DEFAULT_PORT = 3000; 17 | const ROOT_NODE_ADDRESS = `http://localhost:${DEFAULT_PORT}`; 18 | 19 | const app = express(); 20 | const blockchain = new Blockchain(); 21 | const transactionPool = new TransactionPool(); 22 | const wallet = new Wallet(); 23 | const pubsub = new PubSub({ blockchain, transactionPool, redisUrl: REDIS_URL }); 24 | // const pubsub = new PubSub({ blockchain, transactionPool, wallet }); // for PubNub 25 | const transactionMiner = new TransactionMiner({ blockchain, transactionPool, wallet, pubsub }); 26 | 27 | app.use(express.json()); 28 | app.use(express.static(path.join(__dirname, 'client/dist'))); 29 | 30 | app.get('/api/blocks', (req, res) => { 31 | res.json(blockchain.chain); 32 | }); 33 | 34 | app.get('/api/blocks/length', (req, res) => { 35 | res.json(blockchain.chain.length); 36 | }); 37 | 38 | app.get('/api/blocks/:id', (req, res) => { 39 | const { id } = req.params; 40 | const { length } = blockchain.chain; 41 | 42 | const blocksReversed = blockchain.chain.slice().reverse(); 43 | 44 | let startIndex = (id-1) * 5; 45 | let endIndex = id * 5; 46 | 47 | startIndex = startIndex < length ? startIndex : length; 48 | endIndex = endIndex < length ? endIndex : length; 49 | 50 | res.json(blocksReversed.slice(startIndex, endIndex)); 51 | }); 52 | 53 | app.post('/api/mine', (req, res) => { 54 | const { data } = req.body; 55 | 56 | blockchain.addBlock({ data }); 57 | 58 | pubsub.broadcastChain(); 59 | 60 | res.redirect('/api/blocks'); 61 | }); 62 | 63 | app.post('/api/transact', (req, res) => { 64 | const { amount, recipient } = req.body; 65 | 66 | let transaction = transactionPool 67 | .existingTransaction({ inputAddress: wallet.publicKey }); 68 | 69 | try { 70 | if (transaction) { 71 | transaction.update({ senderWallet: wallet, recipient, amount }); 72 | } else { 73 | transaction = wallet.createTransaction({ 74 | recipient, 75 | amount, 76 | chain: blockchain.chain 77 | }); 78 | } 79 | } catch(error) { 80 | return res.status(400).json({ type: 'error', message: error.message }); 81 | } 82 | 83 | transactionPool.setTransaction(transaction); 84 | 85 | pubsub.broadcastTransaction(transaction); 86 | 87 | res.json({ type: 'success', transaction }); 88 | }); 89 | 90 | app.get('/api/transaction-pool-map', (req, res) => { 91 | res.json(transactionPool.transactionMap); 92 | }); 93 | 94 | app.get('/api/mine-transactions', (req, res) => { 95 | transactionMiner.mineTransactions(); 96 | 97 | res.redirect('/api/blocks'); 98 | }); 99 | 100 | app.get('/api/wallet-info', (req, res) => { 101 | const address = wallet.publicKey; 102 | 103 | res.json({ 104 | address, 105 | balance: Wallet.calculateBalance({ chain: blockchain.chain, address }) 106 | }); 107 | }); 108 | 109 | app.get('/api/known-addresses', (req, res) => { 110 | const addressMap = {}; 111 | 112 | for (let block of blockchain.chain) { 113 | for (let transaction of block.data) { 114 | const recipient = Object.keys(transaction.outputMap); 115 | 116 | recipient.forEach(recipient => addressMap[recipient] = recipient); 117 | } 118 | } 119 | 120 | res.json(Object.keys(addressMap)); 121 | }); 122 | 123 | app.get('*', (req, res) => { 124 | res.sendFile(path.join(__dirname, 'client/dist/index.html')); 125 | }); 126 | 127 | const syncWithRootState = () => { 128 | request({ url: `${ROOT_NODE_ADDRESS}/api/blocks` }, (error, response, body) => { 129 | if (!error && response.statusCode === 200) { 130 | const rootChain = JSON.parse(body); 131 | 132 | console.log('replace chain on a sync with', rootChain); 133 | blockchain.replaceChain(rootChain); 134 | } 135 | }); 136 | 137 | request({ url: `${ROOT_NODE_ADDRESS}/api/transaction-pool-map` }, (error, response, body) => { 138 | if (!error && response.statusCode === 200) { 139 | const rootTransactionPoolMap = JSON.parse(body); 140 | 141 | console.log('replace transaction pool map on a sync with', rootTransactionPoolMap); 142 | transactionPool.setMap(rootTransactionPoolMap); 143 | } 144 | }); 145 | }; 146 | 147 | if (isDevelopment) { 148 | const walletFoo = new Wallet(); 149 | const walletBar = new Wallet(); 150 | 151 | const generateWalletTransaction = ({ wallet, recipient, amount }) => { 152 | const transaction = wallet.createTransaction({ 153 | recipient, amount, chain: blockchain.chain 154 | }); 155 | 156 | transactionPool.setTransaction(transaction); 157 | }; 158 | 159 | const walletAction = () => generateWalletTransaction({ 160 | wallet, recipient: walletFoo.publicKey, amount: 5 161 | }); 162 | 163 | const walletFooAction = () => generateWalletTransaction({ 164 | wallet: walletFoo, recipient: walletBar.publicKey, amount: 10 165 | }); 166 | 167 | const walletBarAction = () => generateWalletTransaction({ 168 | wallet: walletBar, recipient: wallet.publicKey, amount: 15 169 | }); 170 | 171 | for (let i=0; i<20; i++) { 172 | if (i%3 === 0) { 173 | walletAction(); 174 | walletFooAction(); 175 | } else if (i%3 === 1) { 176 | walletAction(); 177 | walletBarAction(); 178 | } else { 179 | walletFooAction(); 180 | walletBarAction(); 181 | } 182 | 183 | transactionMiner.mineTransactions(); 184 | } 185 | } 186 | 187 | let PEER_PORT; 188 | 189 | if (process.env.GENERATE_PEER_PORT === 'true') { 190 | PEER_PORT = DEFAULT_PORT + Math.ceil(Math.random() * 1000); 191 | } 192 | 193 | const PORT = process.env.PORT || PEER_PORT || DEFAULT_PORT; 194 | app.listen(PORT, () => { 195 | console.log(`listening at localhost:${PORT}`); 196 | 197 | if (PORT !== DEFAULT_PORT) { 198 | syncWithRootState(); 199 | } 200 | }); 201 | -------------------------------------------------------------------------------- /wallet/transaction.test.js: -------------------------------------------------------------------------------- 1 | const Transaction = require('./transaction'); 2 | const Wallet = require('./index'); 3 | const { verifySignature } = require('../util'); 4 | const { REWARD_INPUT, MINING_REWARD } = require('../config'); 5 | 6 | describe('Transaction', () => { 7 | let transaction, senderWallet, recipient, amount; 8 | 9 | beforeEach(() => { 10 | senderWallet = new Wallet(); 11 | recipient = 'recipient-public-key'; 12 | amount = 50; 13 | transaction = new Transaction({ senderWallet, recipient, amount }); 14 | }); 15 | 16 | it('has an `id`', () => { 17 | expect(transaction).toHaveProperty('id'); 18 | }); 19 | 20 | describe('outputMap', () => { 21 | it('has an `outputMap`', () => { 22 | expect(transaction).toHaveProperty('outputMap'); 23 | }); 24 | 25 | it('outputs the amount to the recipient', () => { 26 | expect(transaction.outputMap[recipient]).toEqual(amount); 27 | }); 28 | 29 | it('outputs the remaining balance for the `senderWallet`', () => { 30 | expect(transaction.outputMap[senderWallet.publicKey]) 31 | .toEqual(senderWallet.balance - amount); 32 | }); 33 | }); 34 | 35 | describe('input', () => { 36 | it('has an `input`', () => { 37 | expect(transaction).toHaveProperty('input'); 38 | }); 39 | 40 | it('has a `timestamp` in the input', () => { 41 | expect(transaction.input).toHaveProperty('timestamp'); 42 | }); 43 | 44 | it('sets the `amount` to the `senderWallet` balance', () => { 45 | expect(transaction.input.amount).toEqual(senderWallet.balance); 46 | }); 47 | 48 | it('sets the `address` to the `senderWallet` publicKey', () => { 49 | expect(transaction.input.address).toEqual(senderWallet.publicKey); 50 | }); 51 | 52 | it('signs the input', () => { 53 | expect( 54 | verifySignature({ 55 | publicKey: senderWallet.publicKey, 56 | data: transaction.outputMap, 57 | signature: transaction.input.signature 58 | }) 59 | ).toBe(true); 60 | }); 61 | }); 62 | 63 | describe('validTransaction()', () => { 64 | let errorMock; 65 | 66 | beforeEach(() => { 67 | errorMock = jest.fn(); 68 | 69 | global.console.error = errorMock; 70 | }); 71 | 72 | describe('when the transaction is valid', () => { 73 | it('returns true', () => { 74 | expect(Transaction.validTransaction(transaction)).toBe(true); 75 | }); 76 | }); 77 | 78 | describe('when the transaction is invalid', () => { 79 | describe('and a transaction outputMap value is invalid', () => { 80 | it('returns false and logs an error', () => { 81 | transaction.outputMap[senderWallet.publicKey] = 999999; 82 | 83 | expect(Transaction.validTransaction(transaction)).toBe(false); 84 | expect(errorMock).toHaveBeenCalled(); 85 | }); 86 | }); 87 | 88 | describe('and the transaction input signature is invalid', () => { 89 | it('returns false and logs an error', () => { 90 | transaction.input.signature = new Wallet().sign('data'); 91 | 92 | expect(Transaction.validTransaction(transaction)).toBe(false); 93 | expect(errorMock).toHaveBeenCalled(); 94 | }); 95 | }); 96 | }); 97 | }); 98 | 99 | describe('update()', () => { 100 | let originalSignature, originalSenderOutput, nextRecipient, nextAmount; 101 | 102 | describe('and the amount is invalid', () => { 103 | it('throws an error', () => { 104 | expect(() => { 105 | transaction.update({ 106 | senderWallet, recipient: 'foo', amount: 999999 107 | }) 108 | }).toThrow('Amount exceeds balance'); 109 | }); 110 | }); 111 | 112 | describe('and the amount is valid', () => { 113 | beforeEach(() => { 114 | originalSignature = transaction.input.signature; 115 | originalSenderOutput = transaction.outputMap[senderWallet.publicKey]; 116 | nextRecipient = 'next-recipient'; 117 | nextAmount = 50; 118 | 119 | transaction.update({ 120 | senderWallet, recipient: nextRecipient, amount: nextAmount 121 | }); 122 | }); 123 | 124 | it('outputs the amount to the next recipient', () => { 125 | expect(transaction.outputMap[nextRecipient]).toEqual(nextAmount); 126 | }); 127 | 128 | it('subtracts the amount from the original sender output amount', () => { 129 | expect(transaction.outputMap[senderWallet.publicKey]) 130 | .toEqual(originalSenderOutput - nextAmount); 131 | }); 132 | 133 | it('maintains a total output that matches the input amount', () => { 134 | expect( 135 | Object.values(transaction.outputMap) 136 | .reduce((total, outputAmount) => total + outputAmount) 137 | ).toEqual(transaction.input.amount); 138 | }); 139 | 140 | it('re-signs the transaction', () => { 141 | expect(transaction.input.signature).not.toEqual(originalSignature); 142 | }); 143 | 144 | describe('and another update for the same recipient', () => { 145 | let addedAmount; 146 | 147 | beforeEach(() => { 148 | addedAmount = 80; 149 | transaction.update({ 150 | senderWallet, recipient: nextRecipient, amount: addedAmount 151 | }); 152 | }); 153 | 154 | it('adds to the recipient amount', () => { 155 | expect(transaction.outputMap[nextRecipient]) 156 | .toEqual(nextAmount + addedAmount); 157 | }); 158 | 159 | it('subtracts the amount from the original sender output amount', () => { 160 | expect(transaction.outputMap[senderWallet.publicKey]) 161 | .toEqual(originalSenderOutput - nextAmount - addedAmount); 162 | }); 163 | }); 164 | }); 165 | }); 166 | 167 | describe('rewardTransaction()', () => { 168 | let rewardTransaction, minerWallet; 169 | 170 | beforeEach(() => { 171 | minerWallet = new Wallet(); 172 | rewardTransaction = Transaction.rewardTransaction({ minerWallet }); 173 | }); 174 | 175 | it('creates a transaction with the reward input', () => { 176 | expect(rewardTransaction.input).toEqual(REWARD_INPUT); 177 | }); 178 | 179 | it('creates one transaction for the miner with the `MINING_REWARD`', () => { 180 | expect(rewardTransaction.outputMap[minerWallet.publicKey]).toEqual(MINING_REWARD); 181 | }); 182 | }); 183 | }); -------------------------------------------------------------------------------- /wallet/index.test.js: -------------------------------------------------------------------------------- 1 | const Wallet = require('./index'); 2 | const Transaction = require('./transaction'); 3 | const { verifySignature } = require('../util'); 4 | const Blockchain = require('../blockchain'); 5 | const { STARTING_BALANCE } = require('../config'); 6 | 7 | describe('Wallet', () => { 8 | let wallet; 9 | 10 | beforeEach(() => { 11 | wallet = new Wallet(); 12 | }); 13 | 14 | it('has a `balance`', () => { 15 | expect(wallet).toHaveProperty('balance'); 16 | }); 17 | 18 | it('has a `publicKey`', () => { 19 | expect(wallet).toHaveProperty('publicKey'); 20 | }); 21 | 22 | describe('signing data', () => { 23 | const data = 'foobar'; 24 | 25 | it('verifies a signature', () => { 26 | expect( 27 | verifySignature({ 28 | publicKey: wallet.publicKey, 29 | data, 30 | signature: wallet.sign(data) 31 | }) 32 | ).toBe(true); 33 | }); 34 | 35 | it('does not verify an invalid signature', () => { 36 | expect( 37 | verifySignature({ 38 | publicKey: wallet.publicKey, 39 | data, 40 | signature: new Wallet().sign(data) 41 | }) 42 | ).toBe(false); 43 | }); 44 | }); 45 | 46 | describe('createTransaction()', () => { 47 | describe('and the amount exceeds the balance', () => { 48 | it('throws an error', () => { 49 | expect(() => wallet.createTransaction({ amount: 999999, recipient: 'foo-recipient' })) 50 | .toThrow('Amount exceeds balance'); 51 | }); 52 | }); 53 | 54 | describe('and the amount is valid', () => { 55 | let transaction, amount, recipient; 56 | 57 | beforeEach(() => { 58 | amount = 50; 59 | recipient = 'foo-recipient'; 60 | transaction = wallet.createTransaction({ amount, recipient }); 61 | }); 62 | 63 | it('creates an instance of `Transaction`', () => { 64 | expect(transaction instanceof Transaction).toBe(true); 65 | }); 66 | 67 | it('matches the transaction input with the wallet', () => { 68 | expect(transaction.input.address).toEqual(wallet.publicKey); 69 | }); 70 | 71 | it('outputs the amount the recipient', () => { 72 | expect(transaction.outputMap[recipient]).toEqual(amount); 73 | }); 74 | }); 75 | 76 | describe('and a chain is passed', () => { 77 | it('calls `Wallet.calculateBalance`', () => { 78 | const calculateBalanceMock = jest.fn(); 79 | 80 | const originalCalculateBalance = Wallet.calculateBalance; 81 | 82 | Wallet.calculateBalance = calculateBalanceMock; 83 | 84 | wallet.createTransaction({ 85 | recipient: 'foo', 86 | amount: 10, 87 | chain: new Blockchain().chain 88 | }); 89 | 90 | expect(calculateBalanceMock).toHaveBeenCalled(); 91 | 92 | Wallet.calculateBalance = originalCalculateBalance; 93 | }); 94 | }); 95 | }); 96 | 97 | describe('calculateBalance()', () => { 98 | let blockchain; 99 | 100 | beforeEach(() => { 101 | blockchain = new Blockchain(); 102 | }); 103 | 104 | describe('and there are no outputs for the wallet', () => { 105 | it('returns the `STARTING_BALANCE`', () => { 106 | expect( 107 | Wallet.calculateBalance({ 108 | chain: blockchain.chain, 109 | address: wallet.publicKey 110 | }) 111 | ).toEqual(STARTING_BALANCE); 112 | }); 113 | }); 114 | 115 | describe('and there are outputs for the wallet', () => { 116 | let transactionOne, transactionTwo; 117 | 118 | beforeEach(() => { 119 | transactionOne = new Wallet().createTransaction({ 120 | recipient: wallet.publicKey, 121 | amount: 50 122 | }); 123 | 124 | transactionTwo = new Wallet().createTransaction({ 125 | recipient: wallet.publicKey, 126 | amount: 60 127 | }); 128 | 129 | blockchain.addBlock({ data: [transactionOne, transactionTwo] }); 130 | }); 131 | 132 | it('adds the sum of all outputs to the wallet balance', () => { 133 | expect( 134 | Wallet.calculateBalance({ 135 | chain: blockchain.chain, 136 | address: wallet.publicKey 137 | }) 138 | ).toEqual( 139 | STARTING_BALANCE + 140 | transactionOne.outputMap[wallet.publicKey] + 141 | transactionTwo.outputMap[wallet.publicKey] 142 | ); 143 | }); 144 | 145 | describe('and the wallet has made a transaction', () => { 146 | let recentTransaction; 147 | 148 | 149 | beforeEach(() => { 150 | recentTransaction = wallet.createTransaction({ 151 | recipient: 'foo-address', 152 | amount: 30 153 | }); 154 | 155 | blockchain.addBlock({ data: [recentTransaction] }); 156 | }); 157 | 158 | it('returns the output amount of the recent transaction', () => { 159 | expect( 160 | Wallet.calculateBalance({ 161 | chain: blockchain.chain, 162 | address: wallet.publicKey 163 | }) 164 | ).toEqual(recentTransaction.outputMap[wallet.publicKey]); 165 | }); 166 | 167 | describe('and there are outputs next to and after the recent transaction', () => { 168 | let sameBlockTransaction, nextBlockTransaction; 169 | 170 | beforeEach(() => { 171 | recentTransaction = wallet.createTransaction({ 172 | recipient: 'later-foo-address', 173 | amount: 60 174 | }); 175 | 176 | sameBlockTransaction = Transaction.rewardTransaction({ minerWallet: wallet }); 177 | 178 | blockchain.addBlock({ data: [recentTransaction, sameBlockTransaction] }); 179 | 180 | nextBlockTransaction = new Wallet().createTransaction({ 181 | recipient: wallet.publicKey, amount: 75 182 | }); 183 | 184 | blockchain.addBlock({ data: [nextBlockTransaction] }); 185 | }); 186 | 187 | it('includes the output amounts in the returned balance', () => { 188 | expect( 189 | Wallet.calculateBalance({ 190 | chain: blockchain.chain, 191 | address: wallet.publicKey 192 | }) 193 | ).toEqual( 194 | recentTransaction.outputMap[wallet.publicKey] + 195 | sameBlockTransaction.outputMap[wallet.publicKey] + 196 | nextBlockTransaction.outputMap[wallet.publicKey] 197 | ); 198 | }); 199 | }); 200 | }); 201 | }); 202 | }); 203 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Full-Stack-Blockchain 2 | 3 | 4 |
5 |

6 | 7 | Logo 8 | 9 | 10 |

A complete blockchain-powered cryptocurrency Full-Stack Application

11 | 12 |

13 |
14 | View Demo 15 | · 16 | Report Bug 17 | · 18 | Request Feature 19 |

20 |

21 | 22 | 23 | 24 | 25 |
26 | Table of Contents 27 |
    28 |
  1. About The Project
  2. 29 |
  3. Project Takeaways
  4. 30 |
  5. Further Improvements
  6. 31 |
  7. Contributing
  8. 32 |
  9. Acknowledgements
  10. 33 | 34 |
35 |
36 | 37 | 38 | 39 | 40 | ## About The Project 41 | 42 | 43 | 44 | Star⭐ the repo if you like what you see😉. 45 | 46 | I’m a firm believer in the concept of building a technology from scratch to learn it thoroughly and know what it is doing under the hood.The best way to understand blockchain is to build one.. 47 | Naturally, my interest in blockchain technology has led me to try to build a blockchain and its concepts from scratch. The blockchain has become a magic bullet in the software world throughout the past few years. 48 | It’s proven that it has the power to revolutionize economic systems and so much more. 49 | 50 | The blockchain basic work flow can be seen in this below picture. 51 | [![Product work Flow][product-work-flow]](https://www.linkpicture.com/q/Blockchain-work.jpg) 52 | 53 | The project includes building a full backend,testing suite,a frontend web app and deploying the project.Before we deploy our application, we should thoroughly test it. 54 | I’ve included sample integration tests with JavaScript by using [Jest JS framework](https://jestjs.io/). Writing code in test-driven manner makes the application fully fucntional and unique too. 55 | The tests are written in .test.js files in various folders and its simple to confirm the functionality of every code segment. 56 | 57 | The project is deployed in [Heroku](https://www.heroku.com) - platform as a service (PaaS) tool that operates applications entirely in the cloud. 58 | The project application sample picture can be seen below. 59 | 60 | [![Product Name Screen Shot][product-screenshot]](https://www.linkpicture.com/q/blockchain.png) 61 | 62 | The list of resources that I used for building this project are listed in the acknowledgements. 63 | 64 | Built With [Node.js](https://nodejs.org/en/), JavaScript, [Express](https://expressjs.com/), APIs, Publish/Subscribe, [React](https://reactjs.org/) - all these technologies are incorporated in this full-stack project. 65 | 66 | 67 | 68 | ## Project Takeaways 69 | - Code a full-on backend with test-driven development. 70 | - Write a full test suite for the backend. 71 | - Build a Blockchain in the object-oriented programming style. 72 | - Create a full frontend React.js web application. 73 | - Deploy the application to production (with multiple servers). 74 | - Create an API around the Blockchain. 75 | - Create a real-time connected peer-to-peer server with a pub/sub implementation. 76 | - Implement a proof-of-work algorithm. 77 | - Sign Transactions with cryptography and digital signature. 78 | - Create a Transaction Pool for a real-time list of incoming data. 79 | - Include transactions in core blocks of the chain. 80 | 81 | 82 | 83 | ## Project Challeneges and Further Enhancements 84 | 85 | ##### I want to extend some of the below ideas in the future to make it more effective. 86 | 87 | - Download the Blockchain to the File System 88 | 89 | Currently, the blockchain completely lives in the JavaScript memory. Luckily, as long as there is one node in the system running, a copy of the current blockchain is stored. 90 | But if all nodes go down, the blockchain progress will die. One solution is to implement blockchain backups by adding a feature to download the blockchain to the file system. 91 | A straightforward option is to download the blockchain as json. 92 | 93 | 94 | - Load the Blockchain from the File System 95 | 96 | This follows up the previous challenge, which is to implement a feature where the blockchain can be downloaded from the file system. 97 | This challenge is to reload the blockchain in memory using an existing json file representing a chain. The benefit is quicker synchronization on startup for new peers, 98 | as well as restoring lost data if the JavaScript memory somehow loses the blockchain. 99 | 100 | 101 | 102 | - Transaction Pool Socket Updates 103 | 104 | Replace the polling logic in the transaction pool with real-time updates through socket.io. Continually polling the pool, even when there haven’t been any updates, 105 | could be overkill and eventually overload the server. But using socket.io for real-time updates is an alternate and more clean solution. 106 | 107 | 108 | - Refactor the React app to use Redux 109 | 110 | There are certain places in the application, where certain API requests are overdone. For example, the known-addresses, and wallet-info section are fetched on 111 | every component visit. But this can be fixed by redux which maintains an internal store. Plus, if the app has any logic where a smaller component needs to update 112 | global state, redux would definitely come in handy. 113 | 114 | 115 | - Fresh Keys Per Transaction 116 | 117 | This challenge is to implement a solution to a possible attack vector which tracks a public key’s usage throughout many transactions, and attempts to decrypt its 118 | private key. A solution to this, is to implement a wallet, that creates a fresh set of keys on every transactions. It’s a lot more overhead - but perhaps more secure. 119 | 120 | - Beef Up the Proof-of-Work and Display Mining Progress: 121 | 122 | Beef up the proof-of-work algorithm by significantly increasing the MINING_RATE. Display real-time feedback of the proof-of-work algorithm in 123 | the frontend (socket.io could come in handy). 124 | 125 | 126 | 127 | - Permissioned Access 128 | 129 | Currently, anyone can visit the frontend so long as they know its public url. This means that anyone can issue a mining request on that frontend’s behalf. 130 | With permissioned access, only authorized users should be able to access a frontend, and call its api requests (every api should check for an encrypted authorization header). 131 | 132 | 133 | - Redis Clusters 134 | 135 | To get servers communicating, they are all connecting to the same redis server. This has a limited number of connections. 136 | A solution is implement a cluster of redis urls as the connections increase. That way, a different redis server can handle other servers. 137 | The key is making sure that each redis server itself, communicates with each other, in a cluster fashion. 138 | 139 | 140 | 141 | ## Contributing 142 | Any contributions you make are **greatly appreciated**. 143 | 144 | 1. Fork the Project 145 | 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) 146 | 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`) 147 | 4. Push to the Branch (`git push origin feature/AmazingFeature`) 148 | 5. Open a Pull Request 149 | 150 | 151 | 152 | ## Acknowledgements 153 | * [Awesome github repo of BLockchain implementation in javascript](https://github.com/yjjnls/awesome-blockchain#implementation-of-blockchain) 154 | * [The complete Youtube series of Creating a blockchain with Javascript with Angular front end](https://www.youtube.com/watch?v=zVqczFZr124&list=PLzvRQMJ9HDiTqZmbtFisdXFxul5k0F-Q4&index=1) 155 | * [Huge thanks to this Github repo of David Katz cryptochain](https://github.com/15Dkatz/cryptochain) 156 | 157 | 158 | 159 | 160 | 161 | 162 | [product-screenshot]: images/blockchain.png 163 | [product-work-flow]: images/Blockchain-work.jpg 164 | -------------------------------------------------------------------------------- /blockchain/index.test.js: -------------------------------------------------------------------------------- 1 | const Blockchain = require('./index'); 2 | const Block = require('./block'); 3 | const { cryptoHash } = require('../util'); 4 | const Wallet = require('../wallet'); 5 | const Transaction = require('../wallet/transaction'); 6 | 7 | describe('Blockchain', () => { 8 | let blockchain, newChain, originalChain, errorMock; 9 | 10 | beforeEach(() => { 11 | blockchain = new Blockchain(); 12 | newChain = new Blockchain(); 13 | errorMock = jest.fn(); 14 | 15 | originalChain = blockchain.chain; 16 | global.console.error = errorMock; 17 | }); 18 | 19 | it('contains a `chain` Array instance', () => { 20 | expect(blockchain.chain instanceof Array).toBe(true); 21 | }); 22 | 23 | it('starts with the genesis block', () => { 24 | expect(blockchain.chain[0]).toEqual(Block.genesis()); 25 | }); 26 | 27 | it('adds a new block to the chain', () => { 28 | const newData = 'foo bar'; 29 | blockchain.addBlock({ data: newData }); 30 | 31 | expect(blockchain.chain[blockchain.chain.length-1].data).toEqual(newData); 32 | }); 33 | 34 | describe('isValidChain()', () => { 35 | describe('when the chain does not start with the genesis block', () => { 36 | it('returns false', () => { 37 | blockchain.chain[0] = { data: 'fake-genesis' }; 38 | 39 | expect(Blockchain.isValidChain(blockchain.chain)).toBe(false); 40 | }); 41 | }); 42 | 43 | describe('when the chain starts with the genesis block and has multiple blocks', () => { 44 | beforeEach(() => { 45 | blockchain.addBlock({ data: 'Bears' }); 46 | blockchain.addBlock({ data: 'Beets' }); 47 | blockchain.addBlock({ data: 'Battlestar Galactica' }); 48 | }); 49 | 50 | describe('and a lastHash reference has changed', () => { 51 | it('returns false', () => { 52 | blockchain.chain[2].lastHash = 'broken-lastHash'; 53 | 54 | expect(Blockchain.isValidChain(blockchain.chain)).toBe(false); 55 | }); 56 | }); 57 | 58 | describe('and the chain contains a block with an invalid field', () => { 59 | it('returns false', () => { 60 | blockchain.chain[2].data = 'some-bad-and-evil-data'; 61 | 62 | expect(Blockchain.isValidChain(blockchain.chain)).toBe(false); 63 | }); 64 | }); 65 | 66 | describe('and the chain contains a block with a jumped difficulty', () => { 67 | it('returns false', () => { 68 | const lastBlock = blockchain.chain[blockchain.chain.length-1]; 69 | const lastHash = lastBlock.hash; 70 | const timestamp = Date.now(); 71 | const nonce = 0; 72 | const data = []; 73 | const difficulty = lastBlock.difficulty - 3; 74 | const hash = cryptoHash(timestamp, lastHash, difficulty, nonce, data); 75 | const badBlock = new Block({ 76 | timestamp, lastHash, hash, nonce, difficulty, data 77 | }); 78 | 79 | blockchain.chain.push(badBlock); 80 | 81 | expect(Blockchain.isValidChain(blockchain.chain)).toBe(false); 82 | }); 83 | }); 84 | 85 | describe('and the chain does not contain any invalid blocks', () => { 86 | it('returns true', () => { 87 | expect(Blockchain.isValidChain(blockchain.chain)).toBe(true); 88 | }); 89 | }); 90 | }); 91 | }); 92 | 93 | describe('replaceChain()', () => { 94 | let logMock; 95 | 96 | beforeEach(() => { 97 | logMock = jest.fn(); 98 | 99 | global.console.log = logMock; 100 | }); 101 | 102 | describe('when the new chain is not longer', () => { 103 | beforeEach(() => { 104 | newChain.chain[0] = { new: 'chain' }; 105 | 106 | blockchain.replaceChain(newChain.chain); 107 | }); 108 | 109 | it('does not replace the chain', () => { 110 | expect(blockchain.chain).toEqual(originalChain); 111 | }); 112 | 113 | it('logs an error', () => { 114 | expect(errorMock).toHaveBeenCalled(); 115 | }); 116 | }); 117 | 118 | describe('when the new chain is longer', () => { 119 | beforeEach(() => { 120 | newChain.addBlock({ data: 'Bears' }); 121 | newChain.addBlock({ data: 'Beets' }); 122 | newChain.addBlock({ data: 'Battlestar Galactica' }); 123 | }); 124 | 125 | describe('and the chain is invalid', () => { 126 | beforeEach(() => { 127 | newChain.chain[2].hash = 'some-fake-hash'; 128 | 129 | blockchain.replaceChain(newChain.chain); 130 | }); 131 | 132 | it('does not replace the chain', () => { 133 | expect(blockchain.chain).toEqual(originalChain); 134 | }); 135 | 136 | it('logs an error', () => { 137 | expect(errorMock).toHaveBeenCalled(); 138 | }); 139 | }); 140 | 141 | describe('and the chain is valid', () => { 142 | beforeEach(() => { 143 | blockchain.replaceChain(newChain.chain); 144 | }); 145 | 146 | it('replaces the chain', () => { 147 | expect(blockchain.chain).toEqual(newChain.chain); 148 | }); 149 | 150 | it('logs about the chain replacement', () => { 151 | expect(logMock).toHaveBeenCalled(); 152 | }); 153 | }); 154 | }); 155 | 156 | describe('and the `validateTransactions` flag is true', () => { 157 | it('calls validTransactionData()', () => { 158 | const validTransactionDataMock = jest.fn(); 159 | 160 | blockchain.validTransactionData = validTransactionDataMock; 161 | 162 | newChain.addBlock({ data: 'foo' }); 163 | blockchain.replaceChain(newChain.chain, true); 164 | 165 | expect(validTransactionDataMock).toHaveBeenCalled(); 166 | }); 167 | }); 168 | }); 169 | 170 | describe('validTransactionData()', () => { 171 | let transaction, rewardTransaction, wallet; 172 | 173 | beforeEach(() => { 174 | wallet = new Wallet(); 175 | transaction = wallet.createTransaction({ recipient: 'foo-address', amount: 65 }); 176 | rewardTransaction = Transaction.rewardTransaction({ minerWallet: wallet }); 177 | }); 178 | 179 | describe('and the transaction data is valid', () => { 180 | it('returns true', () => { 181 | newChain.addBlock({ data: [transaction, rewardTransaction] }); 182 | 183 | expect(blockchain.validTransactionData({ chain: newChain.chain })).toBe(true); 184 | expect(errorMock).not.toHaveBeenCalled(); 185 | }); 186 | }); 187 | 188 | describe('and the transaction data has multiple rewards', () => { 189 | it('returns false and logs an error', () => { 190 | newChain.addBlock({ data: [transaction, rewardTransaction, rewardTransaction] }); 191 | 192 | expect(blockchain.validTransactionData({ chain: newChain.chain })).toBe(false); 193 | expect(errorMock).toHaveBeenCalled(); 194 | }); 195 | }); 196 | 197 | describe('and the transaction data has at least one malformed outputMap', () => { 198 | describe('and the transaction is not a reward transaction', () => { 199 | it('returns false and logs an error', () => { 200 | transaction.outputMap[wallet.publicKey] = 999999; 201 | 202 | newChain.addBlock({ data: [transaction, rewardTransaction] }); 203 | 204 | expect(blockchain.validTransactionData({ chain: newChain.chain })).toBe(false); 205 | expect(errorMock).toHaveBeenCalled(); 206 | }); 207 | }); 208 | 209 | describe('and the transaction is a reward transaction', () => { 210 | it('returns false and logs an error', () => { 211 | rewardTransaction.outputMap[wallet.publicKey] = 999999; 212 | 213 | newChain.addBlock({ data: [transaction, rewardTransaction] }); 214 | 215 | expect(blockchain.validTransactionData({ chain: newChain.chain })).toBe(false); 216 | expect(errorMock).toHaveBeenCalled(); 217 | }); 218 | }); 219 | }); 220 | 221 | describe('and the transaction data has at least one malformed input', () => { 222 | it('returns false and logs an error', () => { 223 | wallet.balance = 9000; 224 | 225 | const evilOutputMap = { 226 | [wallet.publicKey]: 8900, 227 | fooRecipient: 100 228 | }; 229 | 230 | const evilTransaction = { 231 | input: { 232 | timestamp: Date.now(), 233 | amount: wallet.balance, 234 | address: wallet.publicKey, 235 | signature: wallet.sign(evilOutputMap) 236 | }, 237 | outputMap: evilOutputMap 238 | } 239 | 240 | newChain.addBlock({ data: [evilTransaction, rewardTransaction] }); 241 | 242 | expect(blockchain.validTransactionData({ chain: newChain.chain })).toBe(false); 243 | expect(errorMock).toHaveBeenCalled(); 244 | }); 245 | }); 246 | 247 | describe('and a block contains multiple identical transactions', () => { 248 | it('returns false and logs an error', () => { 249 | newChain.addBlock({ 250 | data: [transaction, transaction, transaction, rewardTransaction] 251 | }); 252 | 253 | expect(blockchain.validTransactionData({ chain: newChain.chain })).toBe(false); 254 | expect(errorMock).toHaveBeenCalled(); 255 | }); 256 | }); 257 | }); 258 | }); --------------------------------------------------------------------------------