├── .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 |
38 | {paginatedId}
39 | {' '}
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 |
38 | Show Less
39 |
40 |
41 | )
42 | }
43 |
44 | return (
45 |
46 |
Data: {dataDisplay}
47 |
52 | Show More
53 |
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 |
63 | Mine the Transactions
64 |
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 |
76 | Submit
77 |
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 |
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 | About The Project
29 | Project Takeaways
30 | Further Improvements
31 | Contributing
32 | Acknowledgements
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 | });
--------------------------------------------------------------------------------