├── .gitignore ├── README.md ├── account ├── index.js └── index.test.js ├── api-test.js ├── api ├── index.js └── pubsub.js ├── blockchain ├── block.js ├── block.test.js └── index.js ├── config.js ├── course_logo_udemy.png ├── interpreter ├── index.js └── index.test.js ├── lightning-smart-contract └── index.js ├── package-lock.json ├── package.json ├── store ├── state.js ├── trie.js └── trie.test.js ├── tmp.js ├── transaction ├── index.js ├── index.test.js └── transaction-queue.js └── util ├── index.js └── index.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | *node_modules* 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Build Ethereum From Scratch - Smart Contracts and More 2 | 3 | ![Course Logo](course_logo_udemy.png) 4 | 5 | This repository accompanies the "Build Ethereum From Scratch - Smart Contracts and More" 6 | course by David Katz. 7 | 8 | #### Take the course here: 9 | 10 | [https://www.udemy.com/build-ethereum-from-scratch?couponCode=GITHUB](https://www.udemy.com/build-ethereum-from-scratch?couponCode=GITHUB) 11 | 12 | In the course, you will build your own version of Ethereum. Ethereum can be described in two words. It's a: 13 | 14 | #### Decentralized Computer. 15 | 16 | A decentralized computer is like a normal computer. A normal computer executes a program using one machine. 17 | 18 | But a decentralized computer executes a program using multiple machines. Every machine needs to agree upon the output of the program for its results to become official. 19 | 20 | To build a decentralized computer, here are the essential elements: 21 | 22 | 1) A smart contract language. 23 | 2) A blockchain. 24 | 3) A network. 25 | 4) Transactions and accounts. 26 | 5) A state management data structure. 27 | 28 | Definitely take the course if you're interested in exploring the concepts behind this project more deeply. The course is a line-by-line tutorial of this entire repository. And by the end of the course, you'll have your own version of Ethereum. -------------------------------------------------------------------------------- /account/index.js: -------------------------------------------------------------------------------- 1 | const { ec, keccakHash } = require('../util'); 2 | const { STARTING_BALANCE } = require('../config'); 3 | 4 | class Account { 5 | constructor({ code } = {}) { 6 | this.keyPair = ec.genKeyPair(); 7 | this.address = this.keyPair.getPublic().encode('hex'); 8 | this.balance = STARTING_BALANCE; 9 | this.code = code || []; 10 | this.generateCodeHash(); 11 | } 12 | 13 | generateCodeHash() { 14 | this.codeHash = this.code.length > 0 15 | ? keccakHash(this.address + this.code) 16 | : null; 17 | } 18 | 19 | sign(data) { 20 | return this.keyPair.sign(keccakHash(data)); 21 | } 22 | 23 | toJSON() { 24 | return { 25 | address: this.address, 26 | balance: this.balance, 27 | code: this.code, 28 | codeHash: this.codeHash 29 | }; 30 | } 31 | 32 | static verifySignature({ publicKey, data, signature }) { 33 | const keyFromPublic = ec.keyFromPublic(publicKey, 'hex'); 34 | 35 | return keyFromPublic.verify(keccakHash(data), signature); 36 | } 37 | 38 | static calculateBalance({ address, state }) { 39 | return state.getAccount({ address }).balance; 40 | } 41 | } 42 | 43 | module.exports = Account; 44 | -------------------------------------------------------------------------------- /account/index.test.js: -------------------------------------------------------------------------------- 1 | const Account = require('./index'); 2 | 3 | describe('Account', () => { 4 | let account, data, signature; 5 | 6 | beforeEach(() => { 7 | account = new Account(); 8 | data = { foo: 'foo' }; 9 | signature = account.sign(data); 10 | }); 11 | 12 | describe('verifySignature()', () => { 13 | it('validates a signature generated by the account', () => { 14 | expect(Account.verifySignature({ 15 | publicKey: account.address, 16 | data, 17 | signature 18 | })).toBe(true); 19 | }); 20 | 21 | it('invalidates a signature not generated by the account', () => { 22 | expect(Account.verifySignature({ 23 | publicKey: new Account().address, 24 | data, 25 | signature 26 | })).toBe(false); 27 | }); 28 | }); 29 | }); -------------------------------------------------------------------------------- /api-test.js: -------------------------------------------------------------------------------- 1 | const request = require('request'); 2 | 3 | const { OPCODE_MAP } = require('./interpreter'); 4 | const { STOP, ADD, PUSH, STORE, LOAD } = OPCODE_MAP; 5 | 6 | const BASE_URL = 'http://localhost:3000'; 7 | 8 | const postTransact = ({ code, to, value, gasLimit }) => { 9 | return new Promise((resolve, reject) => { 10 | request(`${BASE_URL}/account/transact`, { 11 | method: 'POST', 12 | headers: { 'Content-Type': 'application/json' }, 13 | body: JSON.stringify({ code, to, value, gasLimit }) 14 | },(error, response, body) => { 15 | return resolve(JSON.parse(body)); 16 | }); 17 | }); 18 | } 19 | 20 | const getMine = () => { 21 | return new Promise((resolve, reject) => { 22 | setTimeout(() => { 23 | request(`${BASE_URL}/blockchain/mine`, (error, response, body) => { 24 | return resolve(JSON.parse(body)); 25 | }); 26 | }, 3000); 27 | }); 28 | } 29 | 30 | const getAccountBalance = ({ address } = {}) => { 31 | return new Promise((resolve, reject) => { 32 | request( 33 | `${BASE_URL}/account/balance` + (address ? `?address=${address}` : ''), 34 | (error, response, body) => { 35 | return resolve(JSON.parse(body)); 36 | } 37 | ); 38 | }); 39 | } 40 | 41 | let toAccountData; 42 | let smartContractAccountData; 43 | 44 | postTransact({}) 45 | .then(postTransactResponse => { 46 | console.log( 47 | 'postTransactResponse (Create Account Transaction)', 48 | postTransactResponse 49 | ); 50 | 51 | toAccountData = postTransactResponse.transaction.data.accountData; 52 | 53 | return getMine(); 54 | }).then(getMineResponse => { 55 | console.log('getMineResponse', getMineResponse); 56 | 57 | return postTransact({ to: toAccountData.address, value: 20 }); 58 | }) 59 | .then(postTransactResponse2 => { 60 | console.log( 61 | 'postTransactResponse2 (Standard Transaction)', 62 | postTransactResponse2 63 | ); 64 | 65 | const key = 'foo'; 66 | const value = 'bar'; 67 | const code = [PUSH, value, PUSH, key, STORE, PUSH, key, LOAD, STOP]; 68 | 69 | return postTransact({ code }); 70 | }) 71 | .then(postTransactResponse3 => { 72 | console.log( 73 | 'postTransactResponse3 (Smart Contract)', 74 | postTransactResponse3 75 | ); 76 | 77 | smartContractAccountData = postTransactResponse3 78 | .transaction 79 | .data 80 | .accountData; 81 | 82 | return getMine(); 83 | }) 84 | .then(getMineResponse2 => { 85 | console.log('getMineResponse2', getMineResponse2); 86 | 87 | return postTransact({ 88 | to: smartContractAccountData.codeHash, 89 | value: 0, 90 | gasLimit: 100 91 | }); 92 | }) 93 | .then(postTransactResponse4 => { 94 | console.log( 95 | 'postTransactResponse4 (to the smart contract)', 96 | postTransactResponse4 97 | ); 98 | return getMine(); 99 | }) 100 | .then(getMineResponse3 => { 101 | console.log('getMineResponse3', getMineResponse3); 102 | 103 | return getAccountBalance(); 104 | }) 105 | .then(getAccountBalanceResponse => { 106 | console.log('getAccountBalanceResponse', getAccountBalanceResponse); 107 | 108 | return getAccountBalance({ address: toAccountData.address }); 109 | }) 110 | .then(getAccountBalanceResponse2 => { 111 | console.log('getAccountBalanceResponse2', getAccountBalanceResponse2); 112 | }); 113 | -------------------------------------------------------------------------------- /api/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const bodyParser = require('body-parser'); 3 | const request = require('request'); 4 | const Account = require('../account'); 5 | const Blockchain = require('../blockchain'); 6 | const Block = require('../blockchain/block'); 7 | const PubSub = require('./pubsub'); 8 | const State = require('../store/state'); 9 | const Transaction = require('../transaction'); 10 | const TransactionQueue = require('../transaction/transaction-queue'); 11 | 12 | const app = express(); 13 | app.use(bodyParser.json()); 14 | 15 | const state = new State(); 16 | const blockchain = new Blockchain({ state }); 17 | const transactionQueue = new TransactionQueue(); 18 | const pubsub = new PubSub({ blockchain, transactionQueue }); 19 | const account = new Account(); 20 | const transaction = Transaction.createTransaction({ account }); 21 | 22 | setTimeout(() => { 23 | pubsub.broadcastTransaction(transaction); 24 | }, 500); 25 | 26 | app.get('/blockchain', (req, res, next) => { 27 | const { chain } = blockchain; 28 | 29 | res.json({ chain }); 30 | }); 31 | 32 | app.get('/blockchain/mine', (req, res, next) => { 33 | const lastBlock = blockchain.chain[blockchain.chain.length-1]; 34 | const block = Block.mineBlock({ 35 | lastBlock, 36 | beneficiary: account.address, 37 | transactionSeries: transactionQueue.getTransactionSeries(), 38 | stateRoot: state.getStateRoot() 39 | }); 40 | 41 | blockchain.addBlock({ block, transactionQueue }) 42 | .then(() => { 43 | pubsub.broadcastBlock(block); 44 | 45 | res.json({ block }); 46 | }) 47 | .catch(next); 48 | }); 49 | 50 | app.post('/account/transact', (req, res, next) => { 51 | const { code, gasLimit, to, value } = req.body; 52 | const transaction = Transaction.createTransaction({ 53 | account: !to ? new Account({ code }) : account, 54 | gasLimit, 55 | to, 56 | value 57 | }); 58 | 59 | pubsub.broadcastTransaction(transaction); 60 | 61 | res.json({ transaction }); 62 | }); 63 | 64 | app.get('/account/balance', (req, res, next) => { 65 | const { address } = req.query; 66 | 67 | const balance = Account.calculateBalance({ 68 | address: address || account.address, 69 | state 70 | }); 71 | 72 | res.json({ balance }); 73 | }); 74 | 75 | app.use((err, req, res, next) => { 76 | console.error('Internal server error:', err); 77 | 78 | res.status(500).json({ message: err.message }); 79 | }); 80 | 81 | const peer = process.argv.includes('--peer'); 82 | const PORT = peer 83 | ? Math.floor(2000 + Math.random() * 1000) 84 | : 3000; 85 | 86 | if (peer) { 87 | request('http://localhost:3000/blockchain', (error, response, body) => { 88 | const { chain } = JSON.parse(body); 89 | 90 | blockchain.replaceChain({ chain }) 91 | .then(() => console.log('Synchronized blockchain with the root node')) 92 | .catch(error => console.error('Synchronization error:', error.message)); 93 | }); 94 | } 95 | 96 | app.listen(PORT, () => console.log(`Listening at PORT: ${PORT}`)); 97 | -------------------------------------------------------------------------------- /api/pubsub.js: -------------------------------------------------------------------------------- 1 | const PubNub = require('pubnub'); 2 | const Transaction = require('../transaction'); 3 | 4 | const credentials = { 5 | publishKey: 'pub-c-2a9a12fd-bd7e-45ae-9743-23f7909dd90f', 6 | subscribeKey: 'sub-c-f6ad80a4-c085-11e9-8d65-be5536b78e9a', 7 | secretKey: 'sec-c-MTE0NDA5NDctNGRkZC00YjFmLTgyOGUtOWRlYTM0YmRiNWRh' 8 | }; 9 | 10 | const CHANNELS_MAP = { 11 | TEST: 'TEST', 12 | BLOCK: 'BLOCK', 13 | TRANSACTION: 'TRANSACTION' 14 | }; 15 | 16 | class PubSub { 17 | constructor({ blockchain, transactionQueue }) { 18 | this.pubnub = new PubNub(credentials); 19 | this.blockchain = blockchain; 20 | this.transactionQueue = transactionQueue; 21 | this.subscribeToChannels(); 22 | this.listen(); 23 | } 24 | 25 | subscribeToChannels() { 26 | this.pubnub.subscribe({ 27 | channels: Object.values(CHANNELS_MAP) 28 | }); 29 | } 30 | 31 | publish({ channel, message }) { 32 | this.pubnub.publish({ channel, message }); 33 | } 34 | 35 | listen() { 36 | this.pubnub.addListener({ 37 | message: messageObject => { 38 | const { channel, message } = messageObject; 39 | const parsedMessage = JSON.parse(message); 40 | 41 | console.log('Message received. Channel:', channel); 42 | 43 | switch (channel) { 44 | case CHANNELS_MAP.BLOCK: 45 | console.log('block message', message); 46 | 47 | this.blockchain.addBlock({ 48 | block: parsedMessage, 49 | transactionQueue: this.transactionQueue 50 | }).then(() => console.log('New block accepted', parsedMessage)) 51 | .catch(error => console.error('New block rejected:', error.message)); 52 | break; 53 | case CHANNELS_MAP.TRANSACTION: 54 | console.log(`Received transaction: ${parsedMessage.id}`); 55 | 56 | this.transactionQueue.add(new Transaction(parsedMessage)); 57 | 58 | break; 59 | default: 60 | return; 61 | } 62 | } 63 | }); 64 | } 65 | 66 | broadcastBlock(block) { 67 | this.publish({ 68 | channel: CHANNELS_MAP.BLOCK, 69 | message: JSON.stringify(block) 70 | }); 71 | } 72 | 73 | broadcastTransaction(transaction) { 74 | this.publish({ 75 | channel: CHANNELS_MAP.TRANSACTION, 76 | message: JSON.stringify(transaction) 77 | }); 78 | } 79 | } 80 | 81 | module.exports = PubSub; 82 | -------------------------------------------------------------------------------- /blockchain/block.js: -------------------------------------------------------------------------------- 1 | const { GENESIS_DATA, MINE_RATE } = require('../config'); 2 | const { keccakHash } = require('../util'); 3 | const Transaction = require('../transaction'); 4 | const Trie = require('../store/trie'); 5 | 6 | const HASH_LENGTH = 64; 7 | const MAX_HASH_VALUE = parseInt('f'.repeat(HASH_LENGTH), 16); 8 | const MAX_NONCE_VALUE = 2 ** 64; 9 | 10 | class Block { 11 | constructor({ blockHeaders, transactionSeries }) { 12 | this.blockHeaders = blockHeaders; 13 | this.transactionSeries = transactionSeries; 14 | } 15 | 16 | static calculateBlockTargetHash({ lastBlock }) { 17 | const value = (MAX_HASH_VALUE / lastBlock.blockHeaders.difficulty).toString(16); 18 | 19 | if (value.length > HASH_LENGTH) { 20 | return 'f'.repeat(HASH_LENGTH); 21 | } 22 | 23 | return '0'.repeat(HASH_LENGTH - value.length) + value; 24 | } 25 | 26 | static adjustDifficulty({ lastBlock, timestamp }) { 27 | const { difficulty } = lastBlock.blockHeaders; 28 | 29 | if ((timestamp - lastBlock.blockHeaders.timestamp) > MINE_RATE) { 30 | return difficulty - 1; 31 | } 32 | 33 | if (difficulty < 1) { 34 | return 1; 35 | } 36 | 37 | return difficulty + 1; 38 | } 39 | 40 | static mineBlock({ 41 | lastBlock, 42 | beneficiary, 43 | transactionSeries, 44 | stateRoot 45 | }) { 46 | const target = Block.calculateBlockTargetHash({ lastBlock }); 47 | const miningRewardTransaction = Transaction.createTransaction({ 48 | beneficiary 49 | }); 50 | transactionSeries.push(miningRewardTransaction); 51 | const transactionsTrie = Trie.buildTrie({ items: transactionSeries }); 52 | let timestamp, truncatedBlockHeaders, header, nonce, underTargetHash; 53 | 54 | do { 55 | timestamp = Date.now(); 56 | truncatedBlockHeaders = { 57 | parentHash: keccakHash(lastBlock.blockHeaders), 58 | beneficiary, 59 | difficulty: Block.adjustDifficulty({ lastBlock, timestamp }), 60 | number: lastBlock.blockHeaders.number + 1, 61 | timestamp, 62 | transactionsRoot: transactionsTrie.rootHash, 63 | stateRoot 64 | }; 65 | header = keccakHash(truncatedBlockHeaders); 66 | nonce = Math.floor(Math.random() * MAX_NONCE_VALUE); 67 | 68 | underTargetHash = keccakHash(header + nonce); 69 | } while (underTargetHash > target); 70 | 71 | return new this({ 72 | blockHeaders: { ...truncatedBlockHeaders, nonce }, 73 | transactionSeries 74 | }); 75 | } 76 | 77 | static genesis() { 78 | return new Block(GENESIS_DATA); 79 | } 80 | 81 | static validateBlock({ lastBlock, block, state }) { 82 | return new Promise((resolve, reject) => { 83 | if (keccakHash(block) === keccakHash(Block.genesis())) { 84 | return resolve(); 85 | } 86 | 87 | if (keccakHash(lastBlock.blockHeaders) !== block.blockHeaders.parentHash) { 88 | return reject( 89 | new Error("The parent hash must be a hash of the last block's headers") 90 | ); 91 | } 92 | 93 | if (block.blockHeaders.number !== lastBlock.blockHeaders.number + 1) { 94 | return reject(new Error('The block must increment the number by 1')); 95 | } 96 | 97 | if ( 98 | Math.abs(lastBlock.blockHeaders.difficulty - block.blockHeaders.difficulty) > 1 99 | ) { 100 | return reject(new Error('The difficulty must only adjust by 1')); 101 | } 102 | 103 | const rebuiltTransactionsTrie = Trie.buildTrie({ 104 | items: block.transactionSeries 105 | }); 106 | 107 | if (rebuiltTransactionsTrie.rootHash !== block.blockHeaders.transactionsRoot) { 108 | return reject( 109 | new Error( 110 | `The rebuilt transactions root does not match the block's ` + 111 | `transactions root: ${block.blockHeaders.transactionRoot}` 112 | ) 113 | ); 114 | } 115 | 116 | const target = Block.calculateBlockTargetHash({ lastBlock }); 117 | const { blockHeaders } = block; 118 | const { nonce } = blockHeaders; 119 | const truncatedBlockHeaders = { ...blockHeaders }; 120 | delete truncatedBlockHeaders.nonce; 121 | const header = keccakHash(truncatedBlockHeaders); 122 | const underTargetHash = keccakHash(header + nonce); 123 | 124 | if (underTargetHash > target) { 125 | return reject(new Error( 126 | 'The block does not meet the proof of work requirement' 127 | )); 128 | } 129 | 130 | Transaction.validateTransactionSeries({ 131 | state, transactionSeries: block.transactionSeries 132 | }).then(resolve) 133 | .catch(reject); 134 | }); 135 | } 136 | 137 | static runBlock({ block, state }) { 138 | for (let transaction of block.transactionSeries) { 139 | Transaction.runTransaction({ transaction, state }); 140 | } 141 | } 142 | } 143 | 144 | module.exports = Block; 145 | -------------------------------------------------------------------------------- /blockchain/block.test.js: -------------------------------------------------------------------------------- 1 | const Block = require('./block'); 2 | const State = require('../store/state'); 3 | const { keccakHash } = require('../util'); 4 | 5 | describe('Block', () => { 6 | describe('calculateBlockTargetHash()', () => { 7 | it('calculates the maximum hash when the last block difficulty is 1', () => { 8 | expect( 9 | Block 10 | .calculateBlockTargetHash({ lastBlock: { blockHeaders: { difficulty: 1 } } }) 11 | ).toEqual('f'.repeat(64)); 12 | }); 13 | 14 | it('calculates a low hash value when the last block difficulty is high', () => { 15 | expect( 16 | Block 17 | .calculateBlockTargetHash({ lastBlock: { blockHeaders: { difficulty: 500 } } }) 18 | < '1' 19 | ).toBe(true); 20 | }); 21 | }); 22 | 23 | describe('mineBlock()', () => { 24 | let lastBlock, minedBlock; 25 | 26 | beforeEach(() => { 27 | lastBlock = Block.genesis(); 28 | minedBlock = Block.mineBlock({ 29 | lastBlock, 30 | beneficiary: 'beneficiary', 31 | transactionSeries: [] 32 | }); 33 | }); 34 | 35 | it('mines a block', () => { 36 | expect(minedBlock).toBeInstanceOf(Block); 37 | }); 38 | 39 | it('mines a block that meets the proof of work requirement', () => { 40 | const target = Block.calculateBlockTargetHash({ lastBlock }); 41 | const { blockHeaders } = minedBlock; 42 | const { nonce } = blockHeaders; 43 | const truncatedBlockHeaders = { ...blockHeaders }; 44 | delete truncatedBlockHeaders.nonce; 45 | const header = keccakHash(truncatedBlockHeaders); 46 | const underTargetHash = keccakHash(header + nonce); 47 | 48 | expect(underTargetHash < target).toBe(true); 49 | }); 50 | }); 51 | 52 | describe('adjustDifficulty()', () => { 53 | it('keeps the difficulty above 0', () => { 54 | expect( 55 | Block.adjustDifficulty({ 56 | lastBlock: { blockHeaders: { difficulty: 0 } }, 57 | timestamp: Date.now() 58 | }) 59 | ).toEqual(1); 60 | }); 61 | 62 | it('increases the difficulty for a quickly mined block', () => { 63 | expect( 64 | Block.adjustDifficulty({ 65 | lastBlock: { blockHeaders: { difficulty: 5, timestamp: 1000 } }, 66 | timestamp: 3000 67 | }) 68 | ).toEqual(6); 69 | }); 70 | 71 | it('decreases the difficulty for a slowly mined block', () => { 72 | expect( 73 | Block.adjustDifficulty({ 74 | lastBlock: { blockHeaders: { difficulty: 5, timestamp: 1000 } }, 75 | timestamp: 20000 76 | }) 77 | ).toEqual(4); 78 | }); 79 | }); 80 | 81 | describe('validateBlock()', () => { 82 | let block, lastBlock, state; 83 | 84 | beforeEach(() => { 85 | lastBlock = Block.genesis(); 86 | block = Block.mineBlock({ 87 | lastBlock, 88 | beneficiary: 'beneficiary', 89 | transactionSeries: [] 90 | }); 91 | state = new State(); 92 | }); 93 | 94 | it('resolves when the block is the genesis block', () => { 95 | expect(Block.validateBlock({ 96 | block: Block.genesis(), 97 | state 98 | })).resolves; 99 | }); 100 | 101 | it('resolves if block is valid', () => { 102 | expect(Block.validateBlock({ lastBlock, block, state })).resolves; 103 | }); 104 | 105 | it('rejects when the parentHash is invalid', () => { 106 | block.blockHeaders.parentHash = 'foo'; 107 | 108 | expect(Block.validateBlock({ lastBlock, block, state })) 109 | .rejects 110 | .toMatchObject({ 111 | message: "The parent hash must be a hash of the last block's headers" 112 | }); 113 | }); 114 | 115 | it('rejects when the number is not increased by one', () => { 116 | block.blockHeaders.number = 500; 117 | 118 | expect(Block.validateBlock({ lastBlock, block, state })) 119 | .rejects 120 | .toMatchObject({ 121 | message: 'The block must increment the number by 1' 122 | }); 123 | }); 124 | 125 | it('rejects when the difficulty adjusts by more than 1', () => { 126 | block.blockHeaders.difficulty = 999; 127 | 128 | expect(Block.validateBlock({ lastBlock, block, state })) 129 | .rejects 130 | .toMatchObject({ 131 | message: 'The difficulty must only adjust by 1' 132 | }); 133 | }); 134 | 135 | it('rejects when the proof of work requirement is not met', () => { 136 | const originalCalculateBlockTargetHash = Block.calculateBlockTargetHash; 137 | Block.calculateBlockTargetHash = () => { 138 | return '0'.repeat(64); 139 | } 140 | 141 | expect(Block.validateBlock({ lastBlock, block, state })) 142 | .rejects 143 | .toMatchObject({ 144 | message: 'The block does not meet the proof of work requirement' 145 | }); 146 | 147 | Block.calculateBlockTargetHash = originalCalculateBlockTargetHash; 148 | }); 149 | 150 | it('rejects when the transactionSeries is not valid', () => { 151 | block.transactionSeries = ['foo']; 152 | 153 | expect(Block.validateBlock({ state, lastBlock, block })) 154 | .rejects 155 | .toMatchObject({ 156 | message: /rebuilt transactions root does not match/ 157 | }); 158 | }); 159 | }); 160 | }); 161 | -------------------------------------------------------------------------------- /blockchain/index.js: -------------------------------------------------------------------------------- 1 | const Block = require('./block'); 2 | 3 | class Blockchain { 4 | constructor({ state }) { 5 | this.chain = [Block.genesis()]; 6 | this.state = state; 7 | } 8 | 9 | addBlock({ block, transactionQueue }) { 10 | return new Promise((resolve, reject) => { 11 | Block.validateBlock({ 12 | lastBlock: this.chain[this.chain.length-1], 13 | block, 14 | state: this.state 15 | }).then(() => { 16 | this.chain.push(block); 17 | 18 | Block.runBlock({ block, state: this.state }); 19 | 20 | transactionQueue.clearBlockTransactions({ 21 | transactionSeries: block.transactionSeries 22 | }); 23 | 24 | return resolve(); 25 | }).catch(reject); 26 | }); 27 | } 28 | 29 | replaceChain({ chain }) { 30 | return new Promise(async (resolve, reject) => { 31 | for (let i=0; i= 0 ? chain[i-1] : null; 35 | 36 | try { 37 | await Block.validateBlock({ lastBlock, block, state: this.state }); 38 | Block.runBlock({ block, state: this.state }); 39 | } catch (error) { 40 | return reject(error); 41 | } 42 | 43 | console.log(`*-- Validated block number: ${block.blockHeaders.number}`); 44 | } 45 | 46 | this.chain = chain; 47 | 48 | return resolve(); 49 | }); 50 | } 51 | } 52 | 53 | module.exports = Blockchain; 54 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | const GENESIS_DATA = { 2 | blockHeaders: { 3 | parentHash: '--genesis-parent-hash--', 4 | beneficiary: '--genesis-beneficiary--', 5 | difficulty: 1, 6 | number: 0, 7 | timestamp: '--genesis-timestamp--', 8 | nonce: 0, 9 | transactionsRoot: '--genesis-transactions-root-', 10 | stateRoot: '--genesis-state-root--' 11 | }, 12 | transactionSeries: [] 13 | }; 14 | 15 | const MILLISECONDS = 1; 16 | const SECONDS = 1000 * MILLISECONDS; 17 | const MINE_RATE = 13 * SECONDS; 18 | 19 | const STARTING_BALANCE = 1000; 20 | const MINING_REWARD = 50; 21 | 22 | module.exports = { 23 | GENESIS_DATA, 24 | MINE_RATE, 25 | STARTING_BALANCE, 26 | MINING_REWARD 27 | }; 28 | -------------------------------------------------------------------------------- /course_logo_udemy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/15Dkatz/build-ethereum-from-scratch/10dabac807cca4033df3923e7067dcce0498ade1/course_logo_udemy.png -------------------------------------------------------------------------------- /interpreter/index.js: -------------------------------------------------------------------------------- 1 | const STOP = 'STOP'; 2 | const ADD = 'ADD'; 3 | const SUB = 'SUB'; 4 | const MUL = 'MUL'; 5 | const DIV = 'DIV'; 6 | const PUSH = 'PUSH'; 7 | const LT = 'LT'; 8 | const GT = 'GT'; 9 | const EQ = 'EQ'; 10 | const AND = 'AND'; 11 | const OR = 'OR'; 12 | const JUMP = 'JUMP'; 13 | const JUMPI = 'JUMPI'; 14 | const STORE = 'STORE'; 15 | const LOAD = 'LOAD'; 16 | 17 | const OPCODE_MAP = { 18 | STOP, 19 | ADD, 20 | SUB, 21 | MUL, 22 | DIV, 23 | PUSH, 24 | LT, 25 | GT, 26 | EQ, 27 | AND, 28 | OR, 29 | JUMP, 30 | JUMPI, 31 | STORE, 32 | LOAD 33 | }; 34 | 35 | const OPCODE_GAS_MAP = { 36 | STOP: 0, 37 | ADD: 1, 38 | SUB: 1, 39 | MUL: 1, 40 | DIV: 1, 41 | PUSH: 0, 42 | LT: 1, 43 | GT: 1, 44 | EQ: 1, 45 | AND: 1, 46 | OR: 1, 47 | JUMP: 2, 48 | JUMPI: 2, 49 | STORE: 5, 50 | LOAD: 5 51 | }; 52 | 53 | const EXECUTION_COMPLETE = 'Execution complete'; 54 | const EXECUTION_LIMIT = 10000; 55 | 56 | class Interpreter { 57 | constructor({ storageTrie } = {}) { 58 | this.state = { 59 | programCounter: 0, 60 | stack: [], 61 | code: [], 62 | executionCount: 0 63 | }; 64 | this.storageTrie = storageTrie; 65 | } 66 | 67 | jump() { 68 | const destination = this.state.stack.pop(); 69 | 70 | if ( 71 | destination < 0 72 | || destination > this.state.code.length 73 | ) { 74 | throw new Error(`Invalid destination: ${destination}`); 75 | } 76 | 77 | this.state.programCounter = destination; 78 | this.state.programCounter--; 79 | } 80 | 81 | runCode(code) { 82 | this.state.code = code; 83 | 84 | let gasUsed = 0; 85 | 86 | while (this.state.programCounter < this.state.code.length) { 87 | this.state.executionCount++; 88 | 89 | if (this.state.executionCount > EXECUTION_LIMIT) { 90 | throw new Error( 91 | `Check for an infinite loop. Execution limit of ${EXECUTION_LIMIT} exceeded` 92 | ); 93 | } 94 | 95 | const opCode = this.state.code[this.state.programCounter]; 96 | 97 | gasUsed += OPCODE_GAS_MAP[opCode]; 98 | 99 | let value; 100 | let key; 101 | 102 | try { 103 | switch (opCode) { 104 | case STOP: 105 | throw new Error(EXECUTION_COMPLETE); 106 | case PUSH: 107 | this.state.programCounter++; 108 | 109 | if (this.state.programCounter === this.state.code.length) { 110 | throw new Error(`The 'PUSH' instruction cannot be last.`); 111 | } 112 | 113 | value = this.state.code[this.state.programCounter]; 114 | this.state.stack.push(value); 115 | break; 116 | case ADD: 117 | case SUB: 118 | case MUL: 119 | case DIV: 120 | case LT: 121 | case GT: 122 | case EQ: 123 | case AND: 124 | case OR: 125 | const a = this.state.stack.pop(); 126 | const b = this.state.stack.pop(); 127 | 128 | let result; 129 | 130 | if (opCode === ADD) result = a + b; 131 | if (opCode === SUB) result = a - b; 132 | if (opCode === MUL) result = a * b; 133 | if (opCode === DIV) result = a / b; 134 | if (opCode === LT) result = a < b ? 1 : 0; 135 | if (opCode === GT) result = a > b ? 1 : 0; 136 | if (opCode === EQ) result = a === b ? 1 : 0; 137 | if (opCode === AND) result = a && b; 138 | if (opCode === OR) result = a || b; 139 | 140 | this.state.stack.push(result); 141 | break; 142 | case JUMP: 143 | this.jump(); 144 | break; 145 | case JUMPI: 146 | const condition = this.state.stack.pop(); 147 | 148 | if (condition === 1) { 149 | this.jump(); 150 | } 151 | break; 152 | case STORE: 153 | key = this.state.stack.pop(); 154 | value = this.state.stack.pop(); 155 | 156 | this.storageTrie.put({ key, value }); 157 | 158 | break; 159 | case LOAD: 160 | key = this.state.stack.pop(); 161 | value = this.storageTrie.get({ key }); 162 | 163 | this.state.stack.push(value); 164 | 165 | break; 166 | default: 167 | break; 168 | } 169 | } catch (error) { 170 | if (error.message === EXECUTION_COMPLETE) { 171 | return { 172 | result: this.state.stack[this.state.stack.length-1], 173 | gasUsed 174 | }; 175 | } 176 | 177 | throw error; 178 | } 179 | 180 | this.state.programCounter++; 181 | } 182 | } 183 | } 184 | 185 | Interpreter.OPCODE_MAP = OPCODE_MAP; 186 | module.exports = Interpreter; 187 | 188 | // let code = [PUSH, 2, PUSH, 3, ADD, STOP]; 189 | // let result = new Interpreter().runCode(code); 190 | // console.log('Result of 3 ADD 2:', result); 191 | 192 | // code = [PUSH, 2, PUSH, 3, SUB, STOP]; 193 | // result = new Interpreter().runCode(code); 194 | // console.log('Result of 3 SUB 2:', result); 195 | 196 | // code = [PUSH, 2, PUSH, 3, MUL, STOP]; 197 | // result = new Interpreter().runCode(code); 198 | // console.log('Result of 3 MUL 2:', result); 199 | 200 | // code = [PUSH, 2, PUSH, 3, DIV, STOP]; 201 | // result = new Interpreter().runCode(code); 202 | // console.log('Result of 3 DIV 2:', result); 203 | 204 | // code = [PUSH, 2, PUSH, 3, LT, STOP]; 205 | // result = new Interpreter().runCode(code); 206 | // console.log('Result of 3 LT 2:', result); 207 | 208 | // code = [PUSH, 2, PUSH, 3, GT, STOP]; 209 | // result = new Interpreter().runCode(code); 210 | // console.log('Result of 3 GT 2:', result); 211 | 212 | // code = [PUSH, 2, PUSH, 2, EQ, STOP]; 213 | // result = new Interpreter().runCode(code); 214 | // console.log('Result of 2 EQ 2:', result); 215 | 216 | // code = [PUSH, 1, PUSH, 0, AND, STOP]; 217 | // result = new Interpreter().runCode(code); 218 | // console.log('Result of 0 AND 1:', result); 219 | 220 | // code = [PUSH, 1, PUSH, 0, OR, STOP]; 221 | // result = new Interpreter().runCode(code); 222 | // console.log('Result of 0 OR 1:', result); 223 | 224 | // code = [PUSH, 6, JUMP, PUSH, 0, JUMP, PUSH, 'jump successful', STOP]; 225 | // result = new Interpreter().runCode(code); 226 | // console.log('Result of JUMP:', result); 227 | 228 | // code = [PUSH, 8, PUSH, 1, JUMPI, PUSH, 0, JUMP, PUSH, 'jump successful', STOP]; 229 | // result = new Interpreter().runCode(code); 230 | // console.log('Result of JUMPI:', result); 231 | 232 | // code = [PUSH, 99, JUMP, PUSH, 0, JUMP, PUSH, 'jump successful', STOP]; 233 | // try { 234 | // new Interpreter().runCode(code); 235 | // } catch (error) { 236 | // console.log('Invalid destination error:', error.message); 237 | // } 238 | 239 | // code = [PUSH, 0, PUSH]; 240 | // try { 241 | // new Interpreter().runCode(code); 242 | // } catch (error) { 243 | // console.log('Expected invalid PUSH error:', error.message); 244 | // } 245 | 246 | // code = [PUSH, 0, JUMP, STOP]; 247 | // try { 248 | // new Interpreter().runCode(code); 249 | // } catch (error) { 250 | // console.log('Expected invalid execution error:', error.message); 251 | // } 252 | -------------------------------------------------------------------------------- /interpreter/index.test.js: -------------------------------------------------------------------------------- 1 | const Interpreter = require('./index'); 2 | const Trie = require('../store/trie'); 3 | const { 4 | STOP, 5 | ADD, 6 | SUB, 7 | MUL, 8 | DIV, 9 | PUSH, 10 | LT, 11 | GT, 12 | EQ, 13 | AND, 14 | OR, 15 | JUMP, 16 | JUMPI, 17 | STORE, 18 | LOAD 19 | } = Interpreter.OPCODE_MAP; 20 | 21 | describe('Interpreter', () => { 22 | describe('runCode()', () => { 23 | describe('and the code inludes ADD', () => { 24 | it('adds two values', () => { 25 | expect( 26 | new Interpreter().runCode([PUSH, 2, PUSH, 3, ADD, STOP]).result 27 | ).toEqual(5); 28 | }); 29 | }); 30 | 31 | describe('and the code inludes SUB', () => { 32 | it('subtracts one value from another', () => { 33 | expect( 34 | new Interpreter().runCode([PUSH, 2, PUSH, 3, SUB, STOP]).result 35 | ).toEqual(1); 36 | }); 37 | }); 38 | 39 | describe('and the code inludes MUL', () => { 40 | it('products two values', () => { 41 | expect( 42 | new Interpreter().runCode([PUSH, 2, PUSH, 3, MUL, STOP]).result 43 | ).toEqual(6); 44 | }); 45 | }); 46 | 47 | describe('and the code inludes DIV', () => { 48 | it('divides one value from another', () => { 49 | expect( 50 | new Interpreter().runCode([PUSH, 2, PUSH, 3, DIV, STOP]).result 51 | ).toEqual(1.5); 52 | }); 53 | }); 54 | 55 | describe('and the code inludes LT', () => { 56 | it('checks if one value is less than another', () => { 57 | expect( 58 | new Interpreter().runCode([PUSH, 2, PUSH, 3, LT, STOP]).result 59 | ).toEqual(0); 60 | }); 61 | }); 62 | 63 | describe('and the code inludes GT', () => { 64 | it('checks if one value is greater than another', () => { 65 | expect( 66 | new Interpreter().runCode([PUSH, 2, PUSH, 3, GT, STOP]).result 67 | ).toEqual(1); 68 | }); 69 | }); 70 | 71 | describe('and the code inludes EQ', () => { 72 | it('checks if one value is equal to another', () => { 73 | expect( 74 | new Interpreter().runCode([PUSH, 2, PUSH, 3, EQ, STOP]).result 75 | ).toEqual(0); 76 | }); 77 | }); 78 | 79 | describe('and the code inludes AND', () => { 80 | it('ands two conditions', () => { 81 | expect( 82 | new Interpreter().runCode([PUSH, 1, PUSH, 0, AND, STOP]).result 83 | ).toEqual(0); 84 | }); 85 | }); 86 | 87 | describe('and the code inludes OR', () => { 88 | it('ors two conditions', () => { 89 | expect( 90 | new Interpreter().runCode([PUSH, 1, PUSH, 0, OR, STOP]).result 91 | ).toEqual(1); 92 | }); 93 | }); 94 | 95 | describe('and the code inludes JUMP', () => { 96 | it('jumps to a destination', () => { 97 | expect( 98 | new Interpreter().runCode( 99 | [PUSH, 6, JUMP, PUSH, 0, JUMP, PUSH, 'jump successful', STOP] 100 | ).result 101 | ).toEqual('jump successful'); 102 | }); 103 | }); 104 | 105 | describe('and the code inludes JUMPI', () => { 106 | it('jumps to a destination', () => { 107 | expect( 108 | new Interpreter().runCode( 109 | [PUSH, 8, PUSH, 1, JUMPI, PUSH, 0, JUMP, PUSH, 'jump successful', STOP] 110 | ).result 111 | ).toEqual('jump successful'); 112 | }); 113 | }); 114 | 115 | describe('and the code includes STORE', () => { 116 | const interpreter = new Interpreter({ 117 | storageTrie: new Trie() 118 | }); 119 | const key = 'foo'; 120 | const value = 'bar'; 121 | 122 | interpreter.runCode([PUSH, value, PUSH, key, STORE, STOP]); 123 | 124 | expect(interpreter.storageTrie.get({ key })).toEqual(value); 125 | }); 126 | 127 | describe('and the code includes LOAD', () => { 128 | const interpreter = new Interpreter({ 129 | storageTrie: new Trie() 130 | }); 131 | const key = 'foo'; 132 | const value = 'bar'; 133 | 134 | expect( 135 | interpreter.runCode( 136 | [PUSH, value, PUSH, key, STORE, PUSH, key, LOAD, STOP] 137 | ).result 138 | ).toEqual(value); 139 | }); 140 | 141 | describe('and the code includes an invalid JUMP destination', () => { 142 | it('throws an error', () => { 143 | expect( 144 | () => new Interpreter().runCode( 145 | [PUSH, 99, JUMP, PUSH, 0, JUMP, PUSH, 'jump successful', STOP] 146 | ) 147 | ).toThrow('Invalid destination: 99'); 148 | }); 149 | }); 150 | 151 | describe('and the code includes an invalid PUSH value', () => { 152 | it('throws an error', () => { 153 | expect( 154 | () => new Interpreter().runCode([PUSH, 0, PUSH]) 155 | ).toThrow("The 'PUSH' instruction cannot be last."); 156 | }); 157 | }); 158 | 159 | describe('and the code includes an infinite loop', () => { 160 | it('throws an error', () => { 161 | expect( 162 | () => new Interpreter().runCode([PUSH, 0, JUMP, STOP]) 163 | ).toThrow('Check for an infinite loop. Execution limit of 10000 exceeded'); 164 | }); 165 | }); 166 | }); 167 | }); -------------------------------------------------------------------------------- /lightning-smart-contract/index.js: -------------------------------------------------------------------------------- 1 | /* LIGHTNING SMART CONTRACT LANGUAGE */ 2 | 3 | const STOP = 'STOP'; 4 | const ADD = 'ADD'; 5 | const PUSH = 'PUSH'; 6 | 7 | const code = [PUSH, 2, PUSH, 3, ADD, STOP]; 8 | 9 | class Interpreter { 10 | constructor() { 11 | this.state = { 12 | programCounter: 0, 13 | stack: [], 14 | code: [] 15 | }; 16 | } 17 | 18 | runCode(code) { 19 | this.state.code = code; 20 | 21 | while (this.state.programCounter < this.state.code.length) { 22 | const opCode = this.state.code[this.state.programCounter]; 23 | 24 | try { 25 | switch (opCode) { 26 | case STOP: 27 | throw new Error('Execution complete'); 28 | 29 | case PUSH: 30 | this.state.programCounter++; 31 | const value = this.state.code[this.state.programCounter]; 32 | this.state.stack.push(value); 33 | break; 34 | 35 | case ADD: 36 | const a = this.state.stack.pop(); 37 | const b = this.state.stack.pop(); 38 | 39 | this.state.stack.push(a + b); 40 | break; 41 | default: 42 | break; 43 | 44 | } 45 | } catch (error) { 46 | 47 | 48 | 49 | 50 | return this.state.stack[this.state.stack.length - 1]; 51 | } 52 | 53 | this.state.programCounter++; 54 | } 55 | } 56 | } 57 | 58 | const interpreter = new Interpreter(); 59 | console.log(interpreter.runCode(code)); 60 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "smartchain", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "api-test": "node ./api-test.js", 8 | "dev": "nodemon ./api", 9 | "dev-peer": "nodemon ./api --peer", 10 | "start": "node ./api", 11 | "test": "jest --watchAll" 12 | }, 13 | "jest": { 14 | "testEnvironment": "node" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/15Dkatz/smartchain.git" 19 | }, 20 | "keywords": [], 21 | "author": "", 22 | "license": "ISC", 23 | "bugs": { 24 | "url": "https://github.com/15Dkatz/smartchain/issues" 25 | }, 26 | "homepage": "https://github.com/15Dkatz/smartchain#readme", 27 | "dependencies": { 28 | "body-parser": "^1.19.0", 29 | "elliptic": "^6.5.0", 30 | "express": "^4.17.1", 31 | "jest": "^24.8.0", 32 | "js-sha3": "^0.8.0", 33 | "lodash": "^4.17.15", 34 | "pubnub": "^4.25.0", 35 | "request": "^2.88.0", 36 | "uuid": "^3.3.2" 37 | }, 38 | "devDependencies": { 39 | "nodemon": "^1.19.1" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /store/state.js: -------------------------------------------------------------------------------- 1 | const Trie = require('./trie'); 2 | 3 | class State { 4 | constructor() { 5 | this.stateTrie = new Trie(); 6 | this.storageTrieMap = {}; 7 | } 8 | 9 | putAccount({ address, accountData }) { 10 | if (!this.storageTrieMap[address]) { 11 | this.storageTrieMap[address] = new Trie(); 12 | } 13 | 14 | this.stateTrie.put({ 15 | key: address, 16 | value: { 17 | ...accountData, 18 | storageRoot: this.storageTrieMap[address].rootHash 19 | } 20 | }); 21 | } 22 | 23 | getAccount({ address }) { 24 | return this.stateTrie.get({ key: address }); 25 | } 26 | 27 | getStateRoot() { 28 | return this.stateTrie.rootHash; 29 | } 30 | } 31 | 32 | module.exports = State; 33 | -------------------------------------------------------------------------------- /store/trie.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const { keccakHash } = require('../util'); 3 | 4 | class Node { 5 | constructor() { 6 | this.value = null; 7 | this.childMap = {}; 8 | } 9 | } 10 | 11 | class Trie { 12 | constructor() { 13 | this.head = new Node(); 14 | this.generateRootHash(); 15 | } 16 | 17 | generateRootHash() { 18 | this.rootHash = keccakHash(this.head); 19 | } 20 | 21 | get({ key }) { 22 | let node = this.head; 23 | 24 | for (let character of key) { 25 | if (node.childMap[character]) { 26 | node = node.childMap[character]; 27 | } 28 | } 29 | 30 | return _.cloneDeep(node.value); 31 | } 32 | 33 | put({ key, value }) { 34 | let node = this.head; 35 | 36 | for (let character of key) { 37 | if (!node.childMap[character]) { 38 | node.childMap[character] = new Node(); 39 | } 40 | 41 | node = node.childMap[character]; 42 | } 43 | 44 | node.value = value; 45 | 46 | this.generateRootHash(); 47 | } 48 | 49 | static buildTrie({ items }) { 50 | const trie = new this(); 51 | 52 | for (let item of items.sort((a, b) => keccakHash(a) > keccakHash(b))) { 53 | trie.put({ key: keccakHash(item), value: item }); 54 | } 55 | 56 | return trie; 57 | } 58 | } 59 | 60 | module.exports = Trie; 61 | -------------------------------------------------------------------------------- /store/trie.test.js: -------------------------------------------------------------------------------- 1 | const Trie = require('./trie'); 2 | const { keccakHash } = require('../util'); 3 | 4 | describe('Trie', () => { 5 | let trie; 6 | 7 | beforeEach(() => { 8 | trie = new Trie(); 9 | }); 10 | 11 | it('has a rootHash', () => { 12 | expect(trie.rootHash).not.toBe(undefined); 13 | }); 14 | 15 | describe('put()', () => { 16 | it('stores a value under a key', () => { 17 | const key = 'foo'; 18 | const value = 'bar'; 19 | trie.put({ key, value }); 20 | 21 | expect(trie.get({ key })).toEqual(value); 22 | }); 23 | 24 | it('generates a new root hash after entering the value', () => { 25 | const originalRootHash = trie.rootHash; 26 | 27 | trie.put({ key: 'foo', value: 'bar' }); 28 | 29 | expect(trie.rootHash).not.toEqual(originalRootHash); 30 | }); 31 | }); 32 | 33 | describe('get()', () => { 34 | it('returns a copy of the stored value', () => { 35 | const key = 'foo'; 36 | const value = { one: 1 }; 37 | trie.put({ key, value }); 38 | const gottenValue = trie.get({ key }); 39 | value.one = 2; 40 | 41 | expect(gottenValue).toEqual({ one: 1 }); 42 | }); 43 | }); 44 | 45 | describe('buildTrie()', () => { 46 | it('builds a trie where the items are accessible with their hashes', () => { 47 | const item1 = { foo: 'bar' }; 48 | const item2 = { foo2: 'bar2' }; 49 | 50 | trie = Trie.buildTrie({ items: [item1, item2] }); 51 | 52 | expect(trie.get({ key: keccakHash(item1) })).toEqual(item1); 53 | expect(trie.get({ key: keccakHash(item2) })).toEqual(item2); 54 | }); 55 | }); 56 | }); -------------------------------------------------------------------------------- /tmp.js: -------------------------------------------------------------------------------- 1 | const Trie = require('./store/trie'); 2 | const { keccakHash } = require('./util'); 3 | 4 | const trie = new Trie(); 5 | const accountData = { balance: 1000 }; 6 | const transaction = { data: accountData }; 7 | 8 | trie.put({ key: 'foo', value: transaction }); 9 | const retrievedTransaction = trie.get({ key: 'foo' }); 10 | const hash1 = keccakHash(retrievedTransaction); 11 | console.log('hash1', hash1); 12 | 13 | accountData.balance += 50; 14 | 15 | const hash2 = keccakHash(retrievedTransaction); 16 | console.log('hash2', hash2); 17 | -------------------------------------------------------------------------------- /transaction/index.js: -------------------------------------------------------------------------------- 1 | const uuid = require('uuid/v4'); 2 | const Account = require('../account'); 3 | const Interpreter = require('../interpreter'); 4 | const { MINING_REWARD } = require('../config'); 5 | 6 | const TRANSACTION_TYPE_MAP = { 7 | CREATE_ACCOUNT: 'CREATE_ACCOUNT', 8 | TRANSACT: 'TRANSACT', 9 | MINING_REWARD: 'MINING_REWARD' 10 | }; 11 | 12 | class Transaction { 13 | constructor({ id, from, to, value, data, signature, gasLimit }) { 14 | this.id = id || uuid(); 15 | this.from = from || '-'; 16 | this.to = to || '-'; 17 | this.value = value || 0; 18 | this.data = data || '-'; 19 | this.signature = signature || '-'; 20 | this.gasLimit = gasLimit || 0; 21 | } 22 | 23 | static createTransaction({ account, to, value, beneficiary, gasLimit }) { 24 | if (beneficiary) { 25 | return new Transaction({ 26 | to: beneficiary, 27 | value: MINING_REWARD, 28 | gasLimit, 29 | data: { type: TRANSACTION_TYPE_MAP.MINING_REWARD } 30 | }); 31 | } 32 | 33 | if (to) { 34 | const transactionData = { 35 | id: uuid(), 36 | from: account.address, 37 | to, 38 | value: value || 0, 39 | gasLimit: gasLimit || 0, 40 | data: { type: TRANSACTION_TYPE_MAP.TRANSACT } 41 | }; 42 | 43 | return new Transaction({ 44 | ...transactionData, 45 | signature: account.sign(transactionData) 46 | }); 47 | } 48 | 49 | return new Transaction({ 50 | data: { 51 | type: TRANSACTION_TYPE_MAP.CREATE_ACCOUNT, 52 | accountData: account.toJSON() 53 | } 54 | }); 55 | } 56 | 57 | static validateStandardTransaction({ state, transaction }) { 58 | return new Promise((resolve, reject) => { 59 | const { id, from, signature, value, to, gasLimit } = transaction; 60 | const transactionData = { ...transaction }; 61 | delete transactionData.signature; 62 | 63 | if (!Account.verifySignature({ 64 | publicKey: from, 65 | data: transactionData, 66 | signature 67 | })) { 68 | return reject(new Error(`Transaction ${id} signature is invalid`)); 69 | } 70 | 71 | const fromBalance = state.getAccount({ address: from }).balance; 72 | 73 | if ((value + gasLimit) > fromBalance) { 74 | return reject(new Error( 75 | `Transaction value and gasLimit: ${value} exceeds balance: ${fromBalance}` 76 | )); 77 | } 78 | 79 | const toAccount = state.getAccount({ address: to }); 80 | 81 | if (!toAccount) { 82 | return reject(new Error( 83 | `The to field: ${to} does not exist` 84 | )); 85 | } 86 | 87 | if (toAccount.codeHash) { 88 | const { gasUsed } = new Interpreter({ 89 | storageTrie: state.storageTrieMap[toAccount.codeHash] 90 | }).runCode(toAccount.code); 91 | 92 | if (gasUsed > gasLimit) { 93 | return reject(new Error( 94 | `Transaction needs more gas. Provided: ${gasLimit}. Needs: ${gasUsed}.` 95 | )); 96 | } 97 | } 98 | 99 | return resolve(); 100 | }); 101 | } 102 | 103 | static validateCreateAccountTransaction({ transaction }) { 104 | return new Promise((resolve, reject) => { 105 | const expectedAccountDataFields = Object.keys(new Account().toJSON()); 106 | const fields = Object.keys(transaction.data.accountData); 107 | 108 | if (fields.length !== expectedAccountDataFields.length) { 109 | return reject( 110 | new Error(`The transaction account data has an incorrect number of fields`) 111 | ); 112 | } 113 | 114 | fields.forEach(field => { 115 | if (!expectedAccountDataFields.includes(field)) { 116 | return reject(new Error( 117 | `The field: ${field}, is unexpected for account data` 118 | )); 119 | } 120 | }); 121 | 122 | return resolve(); 123 | }); 124 | } 125 | 126 | static validateMiningRewardTransaction({ transaction }) { 127 | return new Promise((resolve, reject) => { 128 | const { value } = transaction; 129 | 130 | if (value !== MINING_REWARD) { 131 | return reject(new Error( 132 | `The provided mining reward value: ${value} does not equal ` + 133 | `the official value: ${MINING_REWARD}` 134 | )); 135 | } 136 | 137 | return resolve(); 138 | }); 139 | } 140 | 141 | static validateTransactionSeries({ transactionSeries, state }) { 142 | return new Promise(async (resolve, reject) => { 143 | for (let transaction of transactionSeries) { 144 | try { 145 | switch (transaction.data.type) { 146 | case TRANSACTION_TYPE_MAP.CREATE_ACCOUNT: 147 | await Transaction.validateCreateAccountTransaction({ 148 | transaction 149 | }); 150 | break; 151 | case TRANSACTION_TYPE_MAP.TRANSACT: 152 | await Transaction.validateStandardTransaction({ 153 | state, 154 | transaction 155 | }); 156 | break; 157 | case TRANSACTION_TYPE_MAP.MINING_REWARD: 158 | await Transaction.validateMiningRewardTransaction({ 159 | state, 160 | transaction 161 | }); 162 | break; 163 | default: 164 | break; 165 | } 166 | } catch (error) { 167 | return reject(error); 168 | } 169 | } 170 | 171 | return resolve(); 172 | }); 173 | } 174 | 175 | static runTransaction({ state, transaction }) { 176 | switch(transaction.data.type) { 177 | case TRANSACTION_TYPE_MAP.TRANSACT: 178 | Transaction.runStandardTransaction({ state, transaction }); 179 | console.log( 180 | ' -- Updated account data to reflect the standard transaction' 181 | ); 182 | break; 183 | case TRANSACTION_TYPE_MAP.CREATE_ACCOUNT: 184 | Transaction.runCreateAccountTransaction({ state, transaction }); 185 | console.log(' -- Stored the account data'); 186 | break; 187 | case TRANSACTION_TYPE_MAP.MINING_REWARD: 188 | Transaction.runMiningRewardTransaction({ state, transaction }); 189 | console.log(' -- Updated account data to reflect the mining reward'); 190 | break; 191 | default: 192 | break; 193 | } 194 | } 195 | 196 | static runStandardTransaction({ state, transaction }) { 197 | const fromAccount = state.getAccount({ address: transaction.from }); 198 | const toAccount = state.getAccount({ address: transaction.to }); 199 | 200 | let gasUsed = 0; 201 | let result; 202 | 203 | if (toAccount.codeHash) { 204 | const interpreter = new Interpreter({ 205 | storageTrie: state.storageTrieMap[toAccount.codeHash] 206 | }); 207 | ({ gasUsed, result } = interpreter.runCode(toAccount.code)); 208 | 209 | console.log( 210 | ` -*- Smart contract execution: ${transaction.id} - RESULT: ${result}` 211 | ); 212 | } 213 | 214 | const { value, gasLimit } = transaction; 215 | const refund = gasLimit - gasUsed; 216 | 217 | fromAccount.balance -= value; 218 | fromAccount.balance -= gasLimit; 219 | fromAccount.balance += refund; 220 | toAccount.balance += value; 221 | toAccount.balance += gasUsed; 222 | 223 | state.putAccount({ address: transaction.from, accountData: fromAccount }); 224 | state.putAccount({ address: transaction.to, accountData: toAccount }); 225 | } 226 | 227 | static runCreateAccountTransaction({ state, transaction }) { 228 | const { accountData } = transaction.data; 229 | const { address, codeHash } = accountData; 230 | 231 | state.putAccount({ address: codeHash ? codeHash : address, accountData }); 232 | } 233 | 234 | static runMiningRewardTransaction({ state, transaction }) { 235 | const { to, value } = transaction; 236 | const accountData = state.getAccount({ address: to }); 237 | 238 | accountData.balance += value; 239 | 240 | state.putAccount({ address: to, accountData }); 241 | } 242 | } 243 | 244 | module.exports = Transaction; 245 | -------------------------------------------------------------------------------- /transaction/index.test.js: -------------------------------------------------------------------------------- 1 | const Transaction = require('./index'); 2 | const Account = require('../account'); 3 | const State = require('../store/state'); 4 | 5 | describe('Transaction', () => { 6 | let account; 7 | let standardTransaction; 8 | let createAccountTransaction; 9 | let state; 10 | let toAccount; 11 | let miningRewardTransaction; 12 | 13 | beforeEach(() => { 14 | account = new Account(); 15 | toAccount = new Account(); 16 | state = new State(); 17 | state.putAccount({ address: account.address, accountData: account }); 18 | state.putAccount({ address: toAccount.address, accountData: toAccount }); 19 | 20 | standardTransaction = Transaction.createTransaction({ 21 | account, 22 | to: toAccount.address, 23 | value: 50 24 | }); 25 | createAccountTransaction = Transaction.createTransaction({ 26 | account 27 | }); 28 | miningRewardTransaction = Transaction.createTransaction({ 29 | beneficiary: account.address 30 | }) 31 | }); 32 | 33 | describe('validateStandardTransaction()', () => { 34 | it('validates a valid transaction', () => { 35 | expect(Transaction.validateStandardTransaction({ 36 | transaction: standardTransaction, 37 | state 38 | })).resolves; 39 | }); 40 | 41 | it('does not validate a malformed transaction', () => { 42 | standardTransaction.to = 'different-recipient'; 43 | 44 | expect(Transaction.validateStandardTransaction({ 45 | transaction: standardTransaction, 46 | state 47 | })).rejects.toMatchObject({ message: /invalid/ }); 48 | }); 49 | 50 | it('does not validate when the value exceeds the balance', () => { 51 | standardTransaction = Transaction.createTransaction({ 52 | account, 53 | to: toAccount.address, 54 | value: 9001 55 | }); 56 | 57 | expect(Transaction.validateStandardTransaction({ 58 | transaction: standardTransaction, 59 | state 60 | })).rejects.toMatchObject({ message: /exceeds/ }); 61 | }); 62 | 63 | it('does not validate when the `to` address does not exist', () => { 64 | standardTransaction = Transaction.createTransaction({ 65 | account, 66 | to: 'foo-recipient', 67 | value: 50 68 | }); 69 | 70 | expect(Transaction.validateStandardTransaction({ 71 | transaction: standardTransaction, 72 | state 73 | })).rejects.toMatchObject({ message: /does not exist/ }); 74 | }); 75 | 76 | it('does not validate when the gasLimit exceeds the balance', () => { 77 | standardTransaction = Transaction.createTransaction({ 78 | account, 79 | to: 'foo-recipient', 80 | gasLimit: 9001 81 | }); 82 | 83 | expect(Transaction.validateStandardTransaction({ 84 | transaction: standardTransaction, 85 | state 86 | })).rejects.toMatchObject({ message: /exceeds/ }); 87 | }); 88 | 89 | it('does not validate when the gasUsed for the code exceeds the gasLimit', () => { 90 | const codeHash = 'foo-codeHash'; 91 | const code = ['PUSH', 1, 'PUSH', 2, 'ADD', 'STOP']; 92 | 93 | state.putAccount({ 94 | address: codeHash, 95 | accountData: { code, codeHash } 96 | }); 97 | 98 | standardTransaction = Transaction.createTransaction({ 99 | account, 100 | to: codeHash, 101 | gasLimit: 0 102 | }); 103 | 104 | expect(Transaction.validateStandardTransaction({ 105 | transaction: standardTransaction, 106 | state 107 | })).rejects.toMatchObject({ message: /Transaction needs more gas/ }); 108 | }); 109 | }); 110 | 111 | describe('validateCreateAccountTransaction()', () => { 112 | it('validates a create account transaction', () => { 113 | expect(Transaction.validateCreateAccountTransaction({ 114 | transaction: createAccountTransaction 115 | })).resolves; 116 | }); 117 | 118 | it('does not validate a non create account transaction', () => { 119 | expect(Transaction.validateCreateAccountTransaction({ 120 | transaction: standardTransaction 121 | })).rejects.toMatchObject({ message: /incorrect/ }); 122 | }); 123 | }); 124 | 125 | describe('validateMiningRewardTransaction', () => { 126 | it('validates a mining reward transaction', () => { 127 | expect(Transaction.validateMiningRewardTransaction({ 128 | transaction: miningRewardTransaction 129 | })).resolves; 130 | }); 131 | 132 | it('does not validate a tampered with mining reward transaction', () => { 133 | miningRewardTransaction.value = 9001; 134 | 135 | expect(Transaction.validateMiningRewardTransaction({ 136 | transaction: miningRewardTransaction 137 | })).rejects.toMatchObject({ message: /does not equal the official/ }); 138 | }); 139 | }); 140 | }); -------------------------------------------------------------------------------- /transaction/transaction-queue.js: -------------------------------------------------------------------------------- 1 | class TransactionQueue { 2 | constructor() { 3 | this.transactionMap = {}; 4 | } 5 | 6 | add(transaction) { 7 | this.transactionMap[transaction.id] = transaction; 8 | } 9 | 10 | getTransactionSeries() { 11 | return Object.values(this.transactionMap); 12 | } 13 | 14 | clearBlockTransactions({ transactionSeries }) { 15 | for (let transaction of transactionSeries) { 16 | delete this.transactionMap[transaction.id]; 17 | } 18 | } 19 | } 20 | 21 | module.exports = TransactionQueue; 22 | -------------------------------------------------------------------------------- /util/index.js: -------------------------------------------------------------------------------- 1 | const keccak256 = require('js-sha3').keccak256; 2 | const EC = require('elliptic').ec; 3 | 4 | const ec = new EC('secp256k1'); 5 | 6 | const sortCharacters = data => { 7 | return JSON.stringify(data).split('').sort().join(''); 8 | } 9 | 10 | const keccakHash = data => { 11 | const hash = keccak256.create(); 12 | 13 | hash.update(sortCharacters(data)); 14 | 15 | return hash.hex(); 16 | } 17 | 18 | module.exports = { 19 | sortCharacters, 20 | keccakHash, 21 | ec 22 | }; -------------------------------------------------------------------------------- /util/index.test.js: -------------------------------------------------------------------------------- 1 | const { sortCharacters, keccakHash } = require('./index'); 2 | 3 | describe('util', () => { 4 | describe('sortCharacters()', () => { 5 | it('creates the same string for objects with the same properties in a different order', () => { 6 | expect(sortCharacters({ foo: 'foo', bar: 'bar' })) 7 | .toEqual(sortCharacters({ bar: 'bar', foo: 'foo' })); 8 | }); 9 | 10 | it('creates a different string for different objects', () => { 11 | expect(sortCharacters({ foo: 'foo' })) 12 | .not.toEqual(sortCharacters({ bar: 'bar' })); 13 | }); 14 | }); 15 | 16 | describe('keccakHash()', () => { 17 | it('produces a keccak256 hash', () => { 18 | expect(keccakHash('foo')) 19 | .toEqual('b2a7ad9b4a2ee6d984cc5c2ad81d0c2b2902fa410670aa3f2f4f668a1f80611c'); 20 | }); 21 | }); 22 | }); 23 | --------------------------------------------------------------------------------