├── .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 |
4 |
5 |
6 | SavjeeCoin
7 |
8 |
9 |
10 | [](https://github.com/Savjee/SavjeeCoin/actions/workflows/ci.yml)
11 | [](https://coveralls.io/github/Savjee/SavjeeCoin?branch=master)
12 | [](https://github.com/Savjee/SavjeeCoin/issues)
13 | [](https://github.com/Savjee/SavjeeCoin/pulls)
14 | [](/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://www.youtube.com/watch?v=zVqczFZr124) | [](https://www.youtube.com/watch?v=HneatE69814)
86 | | Video 3: Mining rewards & transactions | Video 4: Signing transactions |
87 | [](https://www.youtube.com/watch?v=fRV6cGXVQ4I) | [](https://www.youtube.com/watch?v=kWQ84S13-hw)
88 | | Video 5: Building a front-end in Angular
89 | [](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 |
--------------------------------------------------------------------------------