├── .eslintrc.js ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── src ├── blockchain.js ├── keygenerator.js └── main.js └── tests ├── .eslintrc.js ├── block.test.js ├── blockchain.test.js ├── helpers.js └── transaction.test.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "root": true, 3 | "extends": "standard", 4 | "rules": { 5 | "semi": ["error", "always"], 6 | "indent": ["error", 2], 7 | "space-before-function-paren": ["error", "never"], 8 | "no-trailing-spaces": ["error", { "skipBlankLines": true }], 9 | "no-multiple-empty-lines": ["error", { "max": 4, "maxEOF": 2 }], 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request: 3 | push: 4 | jobs: 5 | build-test: 6 | name: Build & Test 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | 11 | - name: Install dependencies 12 | run: npm ci 13 | 14 | - name: Run unit tests 15 | run: npm test 16 | 17 | - name: Run coverage 18 | run: npm run coverage 19 | 20 | - name: Coveralls 21 | uses: coverallsapp/github-action@master 22 | with: 23 | github-token: ${{ secrets.GITHUB_TOKEN }} 24 | 25 | eslint: 26 | name: ESLint 27 | runs-on: ubuntu-latest 28 | steps: 29 | - uses: actions/checkout@v2 30 | - run: npm ci 31 | - run: npm run lint 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vscode 3 | coverage 4 | .nyc_output 5 | yarn.lockcoverage 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | tests/ 2 | src/main.js 3 | src/keygenerator.js 4 | .eslintrc.js 5 | .travis.yml 6 | .nyc_output -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Xavier Decuyper 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Project logo 4 |

5 | 6 |

SavjeeCoin

7 | 8 |
9 | 10 | [![.github/workflows/ci.yml](https://github.com/Savjee/SavjeeCoin/actions/workflows/ci.yml/badge.svg)](https://github.com/Savjee/SavjeeCoin/actions/workflows/ci.yml) 11 | [![Coverage Status](https://coveralls.io/repos/github/Savjee/SavjeeCoin/badge.svg?branch=master)](https://coveralls.io/github/Savjee/SavjeeCoin?branch=master) 12 | [![GitHub Issues](https://img.shields.io/github/issues/Savjee/SavjeeCoin.svg)](https://github.com/Savjee/SavjeeCoin/issues) 13 | [![GitHub Pull Requests](https://img.shields.io/github/issues-pr/Savjee/SavjeeCoin.svg)](https://github.com/Savjee/SavjeeCoin/pulls) 14 | [![License](https://img.shields.io/badge/license-MIT-blue.svg)](/LICENSE) 15 | 16 |
17 | 18 | --- 19 | 20 | *⚠️ For education purposes only. This is by no means a complete implementation and it is by no means secure!* 21 | 22 | ## Features 23 | 24 | * Simple proof-of-work algorithm 25 | * Verify blockchain (to prevent tampering) 26 | * Generate wallet (private/public key) 27 | * Sign transactions 28 | 29 | ## 🏁 Getting Started 30 | 31 | ### Install library 32 | ``` 33 | npm install --save savjeecoin 34 | ``` 35 | 36 | ### Generate a keypair 37 | To make transactions on this blockchain you need a keypair. The public key becomes your wallet address and the private key is used to sign transactions. 38 | 39 | ```js 40 | const EC = require('elliptic').ec; 41 | const ec = new EC('secp256k1'); 42 | 43 | const myKey = ec.genKeyPair(); 44 | ``` 45 | 46 | The `myKey` object now contains your public & private key: 47 | 48 | ```js 49 | console.log('Public key:', myKey.getPublic('hex')); 50 | console.log('Private key:', myKey.getPrivate('hex')); 51 | ``` 52 | 53 | ### Create a blockchain instance 54 | Now you can create a new instance of a Blockchain: 55 | 56 | ```js 57 | const {Blockchain, Transaction} = require('savjeecoin'); 58 | 59 | const myChain = new Blockchain(); 60 | ``` 61 | 62 | ### Adding transactions 63 | ```js 64 | // Transfer 100 coins from my wallet to "toAddress" 65 | const tx = new Transaction(myKey.getPublic('hex'), 'toAddress', 100); 66 | tx.sign(myKey); 67 | 68 | myChain.addTransaction(tx); 69 | ``` 70 | 71 | To finalize this transaction, we have to mine a new block. We give this method our wallet address because we will receive a mining reward: 72 | 73 | ```js 74 | myChain.minePendingTransactions(myKey.getPublic('hex')); 75 | ``` 76 | 77 | 78 | --- 79 | 80 | ## 📽 Video tutorial 81 | This source code comes from [my video series on YouTube](https://www.youtube.com/watch?v=zVqczFZr124&list=PLzvRQMJ9HDiTqZmbtFisdXFxul5k0F-Q4). You can check them here: 82 | 83 | | Video 1: Simple implementation | Video 2: Adding Proof-of-work | 84 | :-------------------------:|:-------------------------: 85 | [![](https://img.youtube.com/vi/zVqczFZr124/maxresdefault.jpg)](https://www.youtube.com/watch?v=zVqczFZr124) | [![](https://img.youtube.com/vi/HneatE69814/maxresdefault.jpg)](https://www.youtube.com/watch?v=HneatE69814) 86 | | Video 3: Mining rewards & transactions | Video 4: Signing transactions | 87 | [![](https://img.youtube.com/vi/fRV6cGXVQ4I/maxresdefault.jpg)](https://www.youtube.com/watch?v=fRV6cGXVQ4I) | [![](https://img.youtube.com/vi/kWQ84S13-hw/maxresdefault.jpg)](https://www.youtube.com/watch?v=kWQ84S13-hw) 88 | | Video 5: Building a front-end in Angular 89 | [![](https://img.youtube.com/vi/AQV0WNpE_3g/maxresdefault.jpg)](https://www.youtube.com/watch?v=AQV0WNpE_3g) | 90 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "savjeecoin", 3 | "version": "1.0.1", 4 | "description": "Simple Blockchain implementation in Javascript. For educational purposes only.", 5 | "keywords": [ 6 | "blockchain", 7 | "transactions", 8 | "education" 9 | ], 10 | "main": "src/blockchain.js", 11 | "scripts": { 12 | "test": "nyc mocha tests/*.js", 13 | "lint": "npm run lint:eslint && npm run lint:prettier", 14 | "lint:eslint": "eslint src/** tests/**", 15 | "lint:prettier": "prettier src/**/*.js", 16 | "coverage": "nyc --reporter=lcov npm run test" 17 | }, 18 | "author": "Xavier Decuyper ", 19 | "license": "MIT", 20 | "repository": "github:SavjeeTutorials/SavjeeCoin", 21 | "bugs": { 22 | "url": "https://github.com/SavjeeTutorials/SavjeeCoin/issues" 23 | }, 24 | "homepage": "https://github.com/SavjeeTutorials/SavjeeCoin", 25 | "dependencies": { 26 | "debug": "^4.1.1", 27 | "elliptic": "^6.5.4" 28 | }, 29 | "devDependencies": { 30 | "coveralls": "^3.0.6", 31 | "eslint": "^8.8.0", 32 | "eslint-config-standard": "^17.0.0-0", 33 | "eslint-plugin-import": "^2.18.2", 34 | "eslint-plugin-node": "^11.0.0", 35 | "eslint-plugin-promise": "^6.0.0", 36 | "eslint-plugin-standard": "^5.0.0", 37 | "mocha": "^10.0.0", 38 | "nyc": "^15.1.0", 39 | "prettier": "^2.7.1" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/blockchain.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const crypto = require('crypto'); 3 | const EC = require('elliptic').ec; 4 | const ec = new EC('secp256k1'); 5 | const debug = require('debug')('savjeecoin:blockchain'); 6 | 7 | class Transaction { 8 | /** 9 | * @param {string} fromAddress 10 | * @param {string} toAddress 11 | * @param {number} amount 12 | */ 13 | constructor(fromAddress, toAddress, amount) { 14 | this.fromAddress = fromAddress; 15 | this.toAddress = toAddress; 16 | this.amount = amount; 17 | this.timestamp = Date.now(); 18 | } 19 | 20 | /** 21 | * Creates a SHA256 hash of the transaction 22 | * 23 | * @returns {string} 24 | */ 25 | calculateHash() { 26 | return crypto 27 | .createHash('sha256') 28 | .update(this.fromAddress + this.toAddress + this.amount + this.timestamp) 29 | .digest('hex'); 30 | } 31 | 32 | /** 33 | * Signs a transaction with the given signingKey (which is an Elliptic keypair 34 | * object that contains a private key). The signature is then stored inside the 35 | * transaction object and later stored on the blockchain. 36 | * 37 | * @param {string} signingKey 38 | */ 39 | sign(signingKey) { 40 | // You can only send a transaction from the wallet that is linked to your 41 | // key. So here we check if the fromAddress matches your publicKey 42 | if (signingKey.getPublic('hex') !== this.fromAddress) { 43 | throw new Error('You cannot sign transactions for other wallets!'); 44 | } 45 | 46 | // Calculate the hash of this transaction, sign it with the key 47 | // and store it inside the transaction object 48 | const hashTx = this.calculateHash(); 49 | const sig = signingKey.sign(hashTx, 'base64'); 50 | 51 | this.signature = sig.toDER('hex'); 52 | } 53 | 54 | /** 55 | * Checks if the signature is valid (transaction has not been tampered with). 56 | * It uses the fromAddress as the public key. 57 | * 58 | * @returns {boolean} 59 | */ 60 | isValid() { 61 | // If the transaction doesn't have a from address we assume it's a 62 | // mining reward and that it's valid. You could verify this in a 63 | // different way (special field for instance) 64 | if (this.fromAddress === null) return true; 65 | 66 | if (!this.signature || this.signature.length === 0) { 67 | throw new Error('No signature in this transaction'); 68 | } 69 | 70 | const publicKey = ec.keyFromPublic(this.fromAddress, 'hex'); 71 | return publicKey.verify(this.calculateHash(), this.signature); 72 | } 73 | } 74 | 75 | class Block { 76 | /** 77 | * @param {number} timestamp 78 | * @param {Transaction[]} transactions 79 | * @param {string} previousHash 80 | */ 81 | constructor(timestamp, transactions, previousHash = '') { 82 | this.previousHash = previousHash; 83 | this.timestamp = timestamp; 84 | this.transactions = transactions; 85 | this.nonce = 0; 86 | this.hash = this.calculateHash(); 87 | } 88 | 89 | /** 90 | * Returns the SHA256 of this block (by processing all the data stored 91 | * inside this block) 92 | * 93 | * @returns {string} 94 | */ 95 | calculateHash() { 96 | return crypto 97 | .createHash('sha256') 98 | .update( 99 | this.previousHash + 100 | this.timestamp + 101 | JSON.stringify(this.transactions) + 102 | this.nonce 103 | ) 104 | .digest('hex'); 105 | } 106 | 107 | /** 108 | * Starts the mining process on the block. It changes the 'nonce' until the hash 109 | * of the block starts with enough zeros (= difficulty) 110 | * 111 | * @param {number} difficulty 112 | */ 113 | mineBlock(difficulty) { 114 | while ( 115 | this.hash.substring(0, difficulty) !== Array(difficulty + 1).join('0') 116 | ) { 117 | this.nonce++; 118 | this.hash = this.calculateHash(); 119 | } 120 | 121 | debug(`Block mined: ${this.hash}`); 122 | } 123 | 124 | /** 125 | * Validates all the transactions inside this block (signature + hash) and 126 | * returns true if everything checks out. False if the block is invalid. 127 | * 128 | * @returns {boolean} 129 | */ 130 | hasValidTransactions() { 131 | for (const tx of this.transactions) { 132 | if (!tx.isValid()) { 133 | return false; 134 | } 135 | } 136 | 137 | return true; 138 | } 139 | } 140 | 141 | class Blockchain { 142 | constructor() { 143 | this.chain = [this.createGenesisBlock()]; 144 | this.difficulty = 2; 145 | this.pendingTransactions = []; 146 | this.miningReward = 100; 147 | } 148 | 149 | /** 150 | * @returns {Block} 151 | */ 152 | createGenesisBlock() { 153 | return new Block(Date.parse('2017-01-01'), [], '0'); 154 | } 155 | 156 | /** 157 | * Returns the latest block on our chain. Useful when you want to create a 158 | * new Block and you need the hash of the previous Block. 159 | * 160 | * @returns {Block[]} 161 | */ 162 | getLatestBlock() { 163 | return this.chain[this.chain.length - 1]; 164 | } 165 | 166 | /** 167 | * Takes all the pending transactions, puts them in a Block and starts the 168 | * mining process. It also adds a transaction to send the mining reward to 169 | * the given address. 170 | * 171 | * @param {string} miningRewardAddress 172 | */ 173 | minePendingTransactions(miningRewardAddress) { 174 | const rewardTx = new Transaction( 175 | null, 176 | miningRewardAddress, 177 | this.miningReward 178 | ); 179 | this.pendingTransactions.push(rewardTx); 180 | 181 | const block = new Block( 182 | Date.now(), 183 | this.pendingTransactions, 184 | this.getLatestBlock().hash 185 | ); 186 | block.mineBlock(this.difficulty); 187 | 188 | debug('Block successfully mined!'); 189 | this.chain.push(block); 190 | 191 | this.pendingTransactions = []; 192 | } 193 | 194 | /** 195 | * Add a new transaction to the list of pending transactions (to be added 196 | * next time the mining process starts). This verifies that the given 197 | * transaction is properly signed. 198 | * 199 | * @param {Transaction} transaction 200 | */ 201 | addTransaction(transaction) { 202 | if (!transaction.fromAddress || !transaction.toAddress) { 203 | throw new Error('Transaction must include from and to address'); 204 | } 205 | 206 | // Verify the transactiion 207 | if (!transaction.isValid()) { 208 | throw new Error('Cannot add invalid transaction to chain'); 209 | } 210 | 211 | if (transaction.amount <= 0) { 212 | throw new Error('Transaction amount should be higher than 0'); 213 | } 214 | 215 | // Making sure that the amount sent is not greater than existing balance 216 | const walletBalance = this.getBalanceOfAddress(transaction.fromAddress); 217 | if (walletBalance < transaction.amount) { 218 | throw new Error('Not enough balance'); 219 | } 220 | 221 | // Get all other pending transactions for the "from" wallet 222 | const pendingTxForWallet = this.pendingTransactions.filter( 223 | tx => tx.fromAddress === transaction.fromAddress 224 | ); 225 | 226 | // If the wallet has more pending transactions, calculate the total amount 227 | // of spend coins so far. If this exceeds the balance, we refuse to add this 228 | // transaction. 229 | if (pendingTxForWallet.length > 0) { 230 | const totalPendingAmount = pendingTxForWallet 231 | .map(tx => tx.amount) 232 | .reduce((prev, curr) => prev + curr); 233 | 234 | const totalAmount = totalPendingAmount + transaction.amount; 235 | if (totalAmount > walletBalance) { 236 | throw new Error( 237 | 'Pending transactions for this wallet is higher than its balance.' 238 | ); 239 | } 240 | } 241 | 242 | this.pendingTransactions.push(transaction); 243 | debug('transaction added: %s', transaction); 244 | } 245 | 246 | /** 247 | * Returns the balance of a given wallet address. 248 | * 249 | * @param {string} address 250 | * @returns {number} The balance of the wallet 251 | */ 252 | getBalanceOfAddress(address) { 253 | let balance = 0; 254 | 255 | for (const block of this.chain) { 256 | for (const trans of block.transactions) { 257 | if (trans.fromAddress === address) { 258 | balance -= trans.amount; 259 | } 260 | 261 | if (trans.toAddress === address) { 262 | balance += trans.amount; 263 | } 264 | } 265 | } 266 | 267 | debug('getBalanceOfAdrees: %s', balance); 268 | return balance; 269 | } 270 | 271 | /** 272 | * Returns a list of all transactions that happened 273 | * to and from the given wallet address. 274 | * 275 | * @param {string} address 276 | * @return {Transaction[]} 277 | */ 278 | getAllTransactionsForWallet(address) { 279 | const txs = []; 280 | 281 | for (const block of this.chain) { 282 | for (const tx of block.transactions) { 283 | if (tx.fromAddress === address || tx.toAddress === address) { 284 | txs.push(tx); 285 | } 286 | } 287 | } 288 | 289 | debug('get transactions for wallet count: %s', txs.length); 290 | return txs; 291 | } 292 | 293 | /** 294 | * Loops over all the blocks in the chain and verify if they are properly 295 | * linked together and nobody has tampered with the hashes. By checking 296 | * the blocks it also verifies the (signed) transactions inside of them. 297 | * 298 | * @returns {boolean} 299 | */ 300 | isChainValid() { 301 | // Check if the Genesis block hasn't been tampered with by comparing 302 | // the output of createGenesisBlock with the first block on our chain 303 | const realGenesis = JSON.stringify(this.createGenesisBlock()); 304 | 305 | if (realGenesis !== JSON.stringify(this.chain[0])) { 306 | return false; 307 | } 308 | 309 | // Check the remaining blocks on the chain to see if there hashes and 310 | // signatures are correct 311 | for (let i = 1; i < this.chain.length; i++) { 312 | const currentBlock = this.chain[i]; 313 | const previousBlock = this.chain[i - 1]; 314 | 315 | if (previousBlock.hash !== currentBlock.previousHash) { 316 | return false; 317 | } 318 | 319 | if (!currentBlock.hasValidTransactions()) { 320 | return false; 321 | } 322 | 323 | if (currentBlock.hash !== currentBlock.calculateHash()) { 324 | return false; 325 | } 326 | } 327 | 328 | return true; 329 | } 330 | } 331 | 332 | module.exports.Blockchain = Blockchain; 333 | module.exports.Block = Block; 334 | module.exports.Transaction = Transaction; 335 | -------------------------------------------------------------------------------- /src/keygenerator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const EC = require('elliptic').ec; 3 | 4 | // You can use any elliptic curve you want 5 | const ec = new EC('secp256k1'); 6 | 7 | // Generate a new key pair and convert them to hex-strings 8 | const key = ec.genKeyPair(); 9 | const publicKey = key.getPublic('hex'); 10 | const privateKey = key.getPrivate('hex'); 11 | 12 | // Print the keys to the console 13 | console.log(); 14 | console.log( 15 | 'Your public key (also your wallet address, freely shareable)\n', 16 | publicKey 17 | ); 18 | 19 | console.log(); 20 | console.log( 21 | 'Your private key (keep this secret! To sign transactions)\n', 22 | privateKey 23 | ); 24 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { Blockchain, Transaction } = require('./blockchain'); 3 | const EC = require('elliptic').ec; 4 | const ec = new EC('secp256k1'); 5 | 6 | // Your private key goes here 7 | const myKey = ec.keyFromPrivate( 8 | '7c4c45907dec40c91bab3480c39032e90049f1a44f3e18c3e07c23e3273995cf' 9 | ); 10 | 11 | // From that we can calculate your public key (which doubles as your wallet address) 12 | const myWalletAddress = myKey.getPublic('hex'); 13 | 14 | // Create new instance of Blockchain class 15 | const savjeeCoin = new Blockchain(); 16 | 17 | // Mine first block 18 | savjeeCoin.minePendingTransactions(myWalletAddress); 19 | 20 | // Create a transaction & sign it with your key 21 | const tx1 = new Transaction(myWalletAddress, 'address2', 100); 22 | tx1.sign(myKey); 23 | savjeeCoin.addTransaction(tx1); 24 | 25 | // Mine block 26 | savjeeCoin.minePendingTransactions(myWalletAddress); 27 | 28 | // Create second transaction 29 | const tx2 = new Transaction(myWalletAddress, 'address1', 50); 30 | tx2.sign(myKey); 31 | savjeeCoin.addTransaction(tx2); 32 | 33 | // Mine block 34 | savjeeCoin.minePendingTransactions(myWalletAddress); 35 | 36 | console.log(); 37 | console.log( 38 | `Balance of xavier is ${savjeeCoin.getBalanceOfAddress(myWalletAddress)}` 39 | ); 40 | 41 | // Uncomment this line if you want to test tampering with the chain 42 | // savjeeCoin.chain[1].transactions[0].amount = 10; 43 | 44 | // Check if the chain is valid 45 | console.log(); 46 | console.log('Blockchain valid?', savjeeCoin.isChainValid() ? 'Yes' : 'No'); 47 | -------------------------------------------------------------------------------- /tests/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "extends": "../.eslintrc.js", 3 | "globals": { 4 | "describe": "readonly", 5 | "it": "readonly", 6 | "beforeEach": "readonly" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tests/block.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const { Block } = require('../src/blockchain'); 3 | const { createSignedTx } = require('./helpers'); 4 | 5 | let blockObj = null; 6 | 7 | beforeEach(function() { 8 | blockObj = new Block(1000, [createSignedTx()], 'a1'); 9 | }); 10 | 11 | describe('Block class', function() { 12 | describe('Constructor', function() { 13 | it('should correctly save parameters', function() { 14 | assert.strict.equal(blockObj.previousHash, 'a1'); 15 | assert.strict.equal(blockObj.timestamp, 1000); 16 | assert.strict.deepEqual(blockObj.transactions, [createSignedTx()]); 17 | assert.strict.equal(blockObj.nonce, 0); 18 | }); 19 | 20 | it('should correctly save parameters, without giving "previousHash"', function() { 21 | blockObj = new Block(1000, [createSignedTx()]); 22 | assert.strict.equal(blockObj.previousHash, ''); 23 | assert.strict.equal(blockObj.timestamp, 1000); 24 | assert.strict.deepEqual(blockObj.transactions, [createSignedTx()]); 25 | assert.strict.equal(blockObj.nonce, 0); 26 | }); 27 | }); 28 | 29 | describe('Calculate hash', function() { 30 | it('should correct calculate the SHA256', function() { 31 | blockObj.timestamp = 1; 32 | blockObj.mineBlock(1); 33 | 34 | assert.strict.equal( 35 | blockObj.hash, 36 | '07d2992ddfcb8d538075fea2a6a33e7fb546c18038ae1a8c0214067ed66dc393' 37 | ); 38 | }); 39 | 40 | it('should change when we tamper with the tx', function() { 41 | const origHash = blockObj.calculateHash(); 42 | blockObj.timestamp = 100; 43 | 44 | assert.strict.notEqual( 45 | blockObj.calculateHash(), 46 | origHash 47 | ); 48 | }); 49 | }); 50 | 51 | describe('has valid transactions', function() { 52 | it('should return true with all valid tx', function() { 53 | blockObj.transactions = [ 54 | createSignedTx(), 55 | createSignedTx(), 56 | createSignedTx() 57 | ]; 58 | 59 | assert(blockObj.hasValidTransactions()); 60 | }); 61 | 62 | it('should return false when a single tx is bad', function() { 63 | const badTx = createSignedTx(); 64 | badTx.amount = 1337; 65 | 66 | blockObj.transactions = [ 67 | createSignedTx(), 68 | badTx 69 | ]; 70 | 71 | assert(!blockObj.hasValidTransactions()); 72 | }); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /tests/blockchain.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const { Blockchain, Transaction } = require('../src/blockchain'); 3 | const { createSignedTx, signingKey, createBlockchainWithTx, createBCWithMined } = require('./helpers'); 4 | 5 | let blockchain = null; 6 | 7 | beforeEach(function() { 8 | blockchain = new Blockchain(); 9 | }); 10 | 11 | describe('Blockchain class', function() { 12 | describe('Constructor', function() { 13 | it('should properly initialize fields', function() { 14 | assert.strict.equal(blockchain.difficulty, 2); 15 | assert.strict.deepEqual(blockchain.pendingTransactions, []); 16 | assert.strict.equal(blockchain.miningReward, 100); 17 | }); 18 | }); 19 | 20 | describe('addTransaction', function() { 21 | it('should correctly add new tx', function() { 22 | const blockchain = createBCWithMined(); 23 | const validTx = createSignedTx(); 24 | blockchain.addTransaction(validTx); 25 | 26 | assert.strict.deepEqual(blockchain.pendingTransactions[0], validTx); 27 | }); 28 | 29 | it('should fail for tx without from address', function() { 30 | const validTx = createSignedTx(); 31 | validTx.fromAddress = null; 32 | 33 | assert.throws(() => { blockchain.addTransaction(validTx); }, Error); 34 | }); 35 | 36 | it('should fail for tx without to address', function() { 37 | const validTx = createSignedTx(); 38 | validTx.toAddress = null; 39 | 40 | assert.throws(() => { blockchain.addTransaction(validTx); }, Error); 41 | }); 42 | 43 | it('should fail when tx is not valid', function() { 44 | const validTx = createSignedTx(); 45 | validTx.amount = 1000; 46 | 47 | assert.throws(() => { blockchain.addTransaction(validTx); }, Error); 48 | }); 49 | 50 | it('should fail when tx has negative or zero amount', function() { 51 | const tx1 = createSignedTx(0); 52 | assert.throws(() => { blockchain.addTransaction(tx1); }, Error); 53 | 54 | const tx2 = createSignedTx(-20); 55 | assert.throws(() => { blockchain.addTransaction(tx2); }, Error); 56 | }); 57 | 58 | it('should fail when not having enough balance', function() { 59 | const tx1 = createSignedTx(); 60 | assert.throws(() => { blockchain.addTransaction(tx1); }, Error); 61 | }); 62 | }); 63 | 64 | describe('wallet balance', function() { 65 | it('should give mining rewards', function() { 66 | const blockchain = createBCWithMined(); 67 | const validTx = createSignedTx(); 68 | blockchain.addTransaction(validTx); 69 | blockchain.addTransaction(validTx); 70 | 71 | blockchain.minePendingTransactions('b2'); 72 | 73 | assert.strict.equal(blockchain.getBalanceOfAddress('b2'), 100); 74 | }); 75 | 76 | it('should correctly reduce wallet balance', function() { 77 | const walletAddr = signingKey.getPublic('hex'); 78 | const blockchain = createBlockchainWithTx(); 79 | 80 | blockchain.minePendingTransactions(walletAddr); 81 | assert.strict.equal(blockchain.getBalanceOfAddress(walletAddr), 180); 82 | }); 83 | 84 | // It should be allowed to create a transaction from and to the same address 85 | // This tests make sure that it works and that the balance of the wallet 86 | // stays the same. 87 | // Discussion: https://github.com/Savjee/SavjeeCoin/pull/52 88 | it('should work with cyclic transactions', function() { 89 | const walletAddr = signingKey.getPublic('hex'); 90 | const blockchain = createBlockchainWithTx(); 91 | 92 | assert.strict.equal(blockchain.getBalanceOfAddress(walletAddr), 80); 93 | 94 | // Create new transaction to self 95 | const tx = new Transaction(walletAddr, walletAddr, 80); 96 | tx.timestamp = 1; 97 | tx.sign(signingKey); 98 | 99 | blockchain.addTransaction(tx); 100 | blockchain.minePendingTransactions('no_addr'); 101 | assert.strict.equal(blockchain.getBalanceOfAddress(walletAddr), 80); 102 | }); 103 | }); 104 | 105 | describe('minePendingTransactions', function() { 106 | // It should not be possible for a user to create multiple pending 107 | // transactions for a total amount higher than his balance. 108 | // In this test we start with this situation: 109 | // - Wallet "walletAddr" -> 80 coins (100 mining reward - 2 test tx) 110 | // - Wallet "wallet2" -> 0 coins 111 | it('should not allow pending transactions to go below zero', function() { 112 | const blockchain = createBlockchainWithTx(); 113 | const walletAddr = signingKey.getPublic('hex'); 114 | 115 | // Verify that the wallets have the correct balance 116 | assert.strict.equal(blockchain.getBalanceOfAddress('wallet2'), 0); 117 | assert.strict.equal(blockchain.getBalanceOfAddress(walletAddr), 80); 118 | 119 | // Create a transaction for 80 coins (from walletAddr -> "wallet2") 120 | blockchain.addTransaction(createSignedTx(80)); 121 | 122 | // Try tro create another transaction for which we don't have the balance. 123 | // Blockchain should refuse this! 124 | assert.throws(() => { blockchain.addTransaction(createSignedTx(80)); }, Error); 125 | 126 | // Mine transactions, send rewards to another address 127 | blockchain.minePendingTransactions(1); 128 | 129 | // Verify that the first transaction did go through. 130 | assert.strict.equal(blockchain.getBalanceOfAddress(walletAddr), 0); 131 | assert.strict.equal(blockchain.getBalanceOfAddress('wallet2'), 80); 132 | }); 133 | }); 134 | 135 | describe('helper functions', function() { 136 | it('should correctly set first block to genesis block', function() { 137 | assert.strict.deepEqual(blockchain.chain[0], blockchain.createGenesisBlock()); 138 | }); 139 | }); 140 | 141 | describe('isChainValid', function() { 142 | it('should return true if no tampering', function() { 143 | const blockchain = createBlockchainWithTx(); 144 | assert(blockchain.isChainValid()); 145 | }); 146 | 147 | it('should fail when genesis block has been tampered with', function() { 148 | blockchain.chain[0].timestamp = 39708; 149 | assert(!blockchain.isChainValid()); 150 | }); 151 | 152 | it('should fail when a tx is invalid', function() { 153 | const blockchain = createBlockchainWithTx(); 154 | blockchain.chain[2].transactions[1].amount = 897397; 155 | assert(!blockchain.isChainValid()); 156 | }); 157 | 158 | it('should fail when a block has been changed', function() { 159 | const blockchain = createBlockchainWithTx(); 160 | blockchain.chain[1].timestamp = 897397; 161 | assert(!blockchain.isChainValid()); 162 | }); 163 | 164 | it('should fail when a previous block hash has been changed', function() { 165 | const blockchain = createBlockchainWithTx(); 166 | blockchain.chain[1].transactions[0].amount = 897397; 167 | blockchain.chain[1].hash = blockchain.chain[1].calculateHash(); 168 | assert(!blockchain.isChainValid()); 169 | }); 170 | }); 171 | 172 | describe('getAllTransactionsForWallet', function() { 173 | it('should get all Transactions for a Wallet', function() { 174 | const blockchain = createBCWithMined(); 175 | const validTx = createSignedTx(); 176 | blockchain.addTransaction(validTx); 177 | blockchain.addTransaction(validTx); 178 | 179 | blockchain.minePendingTransactions('b2'); 180 | blockchain.addTransaction(validTx); 181 | blockchain.addTransaction(validTx); 182 | blockchain.minePendingTransactions('b2'); 183 | 184 | assert.strict.equal(blockchain.getAllTransactionsForWallet('b2').length, 2); 185 | assert.strict.equal(blockchain.getAllTransactionsForWallet(signingKey.getPublic('hex')).length, 5); 186 | for (const trans of blockchain.getAllTransactionsForWallet('b2')) { 187 | assert.strict.equal(trans.amount, 100); 188 | assert.strict.equal(trans.fromAddress, null); 189 | assert.strict.equal(trans.toAddress, 'b2'); 190 | } 191 | }); 192 | }); 193 | }); 194 | -------------------------------------------------------------------------------- /tests/helpers.js: -------------------------------------------------------------------------------- 1 | const { Transaction, Blockchain } = require('../src/blockchain'); 2 | const EC = require('elliptic').ec; 3 | const ec = new EC('secp256k1'); 4 | const signingKey = ec.keyFromPrivate('3d6f54430830d388052865b95c10b4aeb1bbe33c01334cf2cfa8b520062a0ce3'); 5 | 6 | function createSignedTx(amount = 10) { 7 | const txObject = new Transaction(signingKey.getPublic('hex'), 'wallet2', amount); 8 | txObject.timestamp = 1; 9 | txObject.sign(signingKey); 10 | 11 | return txObject; 12 | } 13 | 14 | function createBCWithMined() { 15 | const blockchain = new Blockchain(); 16 | blockchain.minePendingTransactions(signingKey.getPublic('hex')); 17 | 18 | return blockchain; 19 | } 20 | 21 | function createBlockchainWithTx() { 22 | const blockchain = new Blockchain(); 23 | blockchain.minePendingTransactions(signingKey.getPublic('hex')); 24 | 25 | const validTx = new Transaction(signingKey.getPublic('hex'), 'b2', 10); 26 | validTx.sign(signingKey); 27 | 28 | blockchain.addTransaction(validTx); 29 | blockchain.addTransaction(validTx); 30 | blockchain.minePendingTransactions(1); 31 | 32 | return blockchain; 33 | } 34 | 35 | module.exports.signingKey = signingKey; 36 | module.exports.createSignedTx = createSignedTx; 37 | module.exports.createBlockchainWithTx = createBlockchainWithTx; 38 | module.exports.createBCWithMined = createBCWithMined; 39 | -------------------------------------------------------------------------------- /tests/transaction.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | 3 | const { Transaction } = require('../src/blockchain'); 4 | const { createSignedTx, signingKey } = require('./helpers'); 5 | 6 | describe('Transaction class', function() { 7 | let txObject = null; 8 | const fromAddress = 'fromAddress'; 9 | const toAddress = 'toAddress'; 10 | const amount = 100; 11 | 12 | beforeEach(function() { 13 | txObject = new Transaction(fromAddress, toAddress, amount); 14 | }); 15 | 16 | describe('constructor', function() { 17 | it('should automatically set the current date', function() { 18 | const actual = txObject.timestamp; 19 | const minTime = Date.now() - 1000; 20 | const maxTime = Date.now() + 1000; 21 | 22 | assert(actual > minTime && actual < maxTime, 'Tx does not have a good timestamp'); 23 | }); 24 | 25 | it('should correctly save from, to and amount', function() { 26 | assert.strict.equal(txObject.fromAddress, fromAddress); 27 | assert.strict.equal(txObject.toAddress, toAddress); 28 | assert.strict.equal(txObject.amount, amount); 29 | }); 30 | }); 31 | 32 | describe('calculateHash', function() { 33 | it('should correctly calculate the SHA256 hash', function() { 34 | txObject.timestamp = 1; 35 | 36 | assert.strict.equal( 37 | txObject.calculateHash(), 38 | '4be9c20f87f7baac191aa246a33b5d44af1f96f23598ac06e5f71ee222f40abf' 39 | ); 40 | }); 41 | 42 | it('should output a different hash if data is tampered in the transaction', function() { 43 | // Tamper the amount making the hash different 44 | txObject.amount = 50; 45 | 46 | assert.strict.notEqual( 47 | txObject.calculateHash(), 48 | txObject.hash 49 | ); 50 | }); 51 | }); 52 | 53 | describe('sign', function() { 54 | it('should correctly sign transactions', function() { 55 | txObject = createSignedTx(); 56 | 57 | assert.strict.equal( 58 | txObject.signature, 59 | '3044022023fb1d818a0888f7563e1a3ccdd68b28e23070d6c0c1c5004721ee1013f1d7' + 60 | '69022037da026cda35f95ef1ee5ced5b9f7d70e102fcf841e6240950c61e8f9b6ef9f8' 61 | ); 62 | }); 63 | 64 | it('should not sign transactions with fromAddresses that does not belogs to the private key', function() { 65 | txObject.fromAddress = 'some-other-address'; 66 | 67 | assert.throws(() => { 68 | txObject.sign(signingKey); 69 | }, Error); 70 | }); 71 | }); 72 | 73 | describe('isValid', function() { 74 | it('should return true for mining reward transactions', function() { 75 | txObject.fromAddress = null; 76 | assert(txObject.isValid()); 77 | }); 78 | 79 | it('should throw error if signature is invalid', function() { 80 | delete txObject.signature; 81 | assert.throws(() => { txObject.isValid(); }, Error); 82 | 83 | txObject.signature = ''; 84 | assert.throws(() => { txObject.isValid(); }, Error); 85 | }); 86 | 87 | it('should return false for badly signed transactions', function() { 88 | txObject = createSignedTx(10); 89 | 90 | // Tamper the amount making the signature invalid 91 | txObject.amount = 50; 92 | 93 | assert(!txObject.isValid()); 94 | }); 95 | 96 | it('should return true for correctly signed tx', function() { 97 | txObject = createSignedTx(10); 98 | assert(txObject.isValid()); 99 | }); 100 | }); 101 | }); 102 | --------------------------------------------------------------------------------