├── .gitignore ├── spec └── support │ ├── jasmine.json │ └── keys_spec.js ├── utils ├── request.js ├── log.js ├── proxy.js └── log.html ├── spend.js ├── .vscode └── launch.json ├── package.json ├── launcher.js ├── plan-talk.md ├── operation.js ├── peer.js ├── keys.js ├── db.js ├── p2p.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /data 3 | -------------------------------------------------------------------------------- /spec/support/jasmine.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "spec", 3 | "spec_files": [ 4 | "**/*[sS]pec.js" 5 | ], 6 | "helpers": [ 7 | "helpers/**/*.js" 8 | ], 9 | "stopSpecOnExpectationFailure": false, 10 | "random": false 11 | } 12 | -------------------------------------------------------------------------------- /utils/request.js: -------------------------------------------------------------------------------- 1 | let rp = require('request-promise-native'); 2 | 3 | module.exports = { 4 | send: (peer, data) => { 5 | let options = { 6 | method: 'GET', 7 | uri: 'http://localhost:' + (5000 + parseInt(peer.id)), 8 | json: true, 9 | body: Object.assign(data, { origin: global.id }) 10 | }; 11 | return rp(options); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /utils/log.js: -------------------------------------------------------------------------------- 1 | let http = require('http'); 2 | 3 | module.exports = (txt) => { 4 | console.log((global.id ? '[' + global.id + '] ' : '') + txt); 5 | let data = { from: global.id, ts: (new Date().getTime()), "log": txt }; 6 | let req = http.request({ host: '127.0.0.1', port: 4001, method: 'POST', headers: { 'Content-Type': 'application/json' } }); 7 | req.on('error', () => { }); 8 | req.write(JSON.stringify(data)); 9 | req.end(); 10 | }; 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /spend.js: -------------------------------------------------------------------------------- 1 | let request = require('./utils/request.js'); 2 | let crypto = require('./keys.js'); 3 | 4 | let message = { 5 | operation: 'transaction', 6 | fromId: process.argv[2], 7 | toId: process.argv[3], 8 | amount: parseInt(process.argv[4]) 9 | }; 10 | let privateKey = parseInt(process.argv[5]); 11 | 12 | 13 | crypto.crypt(JSON.stringify(message), privateKey) 14 | .then(signature => { 15 | let body = Object.assign({}, message, {signature}); 16 | 17 | console.log(body); 18 | request.send({ id: message.fromId }, body); 19 | }) 20 | .then(response => console.log('Response: ',response)); 21 | 22 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible Node.js debug attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "program": "${workspaceRoot}\\peer.js", 12 | "args": ["2"] 13 | }, 14 | { 15 | "type": "node", 16 | "request": "attach", 17 | "name": "Attach to Process", 18 | "address": "localhost", 19 | "port": 5858 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodejs-blockchain", 3 | "version": "1.0.0", 4 | "description": "Fabriquer sa propre blockchain en NodeJs en utilisant un process naïf et itératif :", 5 | "main": "db.js", 6 | "dependencies": { 7 | "bluebird": "^3.5.0", 8 | "md5": "^2.2.1", 9 | "node-static": "^0.7.9", 10 | "request": "^2.81.0", 11 | "request-promise-native": "^1.0.3", 12 | "sha1": "^1.1.1" 13 | }, 14 | "devDependencies": { 15 | "jasmine": "^2.5.3", 16 | "node-inspector": "^0.12.10" 17 | }, 18 | "scripts": { 19 | "proxy": "node utils/proxy.js", 20 | "test": "jasmine" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/AntoineViau/nodejs-blockchain.git" 25 | }, 26 | "author": "", 27 | "license": "ISC", 28 | "bugs": { 29 | "url": "https://github.com/AntoineViau/nodejs-blockchain/issues" 30 | }, 31 | "homepage": "https://github.com/AntoineViau/nodejs-blockchain#readme" 32 | } 33 | -------------------------------------------------------------------------------- /launcher.js: -------------------------------------------------------------------------------- 1 | var spawn = require('child_process').spawn; 2 | 3 | let nbPeers = process.argv[2] || (!console.error('Need nb peers') && process.exit(1)); 4 | console.log('Starting ' + nbPeers + ' peers'); 5 | 6 | function launch(id) { 7 | let prc = spawn('node', ['peer.js', id]); 8 | let stderr = ''; 9 | prc.stdout.setEncoding('utf8'); 10 | prc.stdout.on('data', function (data) { 11 | var str = data.toString() 12 | var lines = str.split(/(\r?\n)/g); 13 | console.log(lines.join("").trim()); 14 | }); 15 | prc.stderr.on('data', function (buf) { 16 | stderr += buf; 17 | console.log('Err: '+stderr); 18 | }); 19 | prc.on('close', function (code) { 20 | console.log('Peer ' + id + ' exited with code ' + code); 21 | console.log('stderr ' + stderr); 22 | }); 23 | } 24 | 25 | launch(0); 26 | (new Array(nbPeers - 1)).fill(null).reduce((prev, cur, index) => { 27 | let delay = prev + 1000; //Math.floor(250 + Math.random() * 250); 28 | setTimeout(() => launch(index + 1), delay); 29 | return delay; 30 | }, 0); 31 | -------------------------------------------------------------------------------- /utils/proxy.js: -------------------------------------------------------------------------------- 1 | 2 | let http = require('http'); 3 | let static = require('node-static'); 4 | 5 | let logs = []; 6 | let lastSend = 0; 7 | let inputPort = 4001; 8 | let outputPort = 4002; 9 | 10 | // Input server 11 | http.createServer((req, res) => { 12 | var body = []; 13 | req.on('data', (chunk) => { 14 | body.push(chunk); 15 | }).on('end', () => { 16 | body = Buffer.concat(body).toString(); 17 | logs.push(JSON.parse(body)); 18 | console.log('Received : ' + body); 19 | }) 20 | .on('close', () => { 21 | 22 | }); 23 | }).listen(inputPort); 24 | console.log('Input server is listenning on ' + inputPort); 25 | 26 | // Output server 27 | http.createServer((req, res) => { 28 | res.setHeader('Access-Control-Allow-Origin', '*') 29 | res.writeHead(200, { 'Content-Type': 'application/json' }); 30 | res.end(JSON.stringify(logs.filter(log => log.ts > lastSend))); 31 | console.log('Sent : ', logs); 32 | lastSend = (new Date()).getTime(); 33 | }).listen(outputPort); 34 | console.log('Input server is listenning on ' + outputPort); 35 | 36 | var file = new static.Server('./utils'); 37 | require('http').createServer(function (request, response) { 38 | request.addListener('end', function () { 39 | file.serve(request, response); 40 | }).resume(); 41 | }).listen(8080); 42 | console.log('Web server listenning on 8080'); -------------------------------------------------------------------------------- /plan-talk.md: -------------------------------------------------------------------------------- 1 | 1) Présentation 2 | 3 | 2) Pourquoi une BlockChain ? 4 | - un monde de tiers de confiance 5 | - définition d'une blockchain 6 | 7 | 3) Réseau P2P 8 | - NodeJs 9 | - peer 10 | - recherche des peers 11 | 12 | 4) Livre 101 13 | - comptes bancaires 14 | - un compte 15 | - compteur de transactions 16 | 17 | 5) Légitimité 18 | - Effectuer une virement : un cri d'amour vers le réseau 19 | - Problème de l'authentification traditionnelle 20 | - Crypto symétrique 21 | - Crypto asymétrique 22 | - Application : clef publique dans compte 23 | - Application : message chiffré avec clef privée 24 | 25 | 6) Problèmes conceptuels 26 | - le compteur de transaction et les incohérences spatio-temporelles dans le réseau P2P 27 | - problème : livre est un état global mutable 28 | - solution : immutabilité, un historique de l'état des comptes, fonctionne sur comptes distincts 29 | - problème : la double dépense quand N1 et N2 sont sur les mêmes comptes 30 | - solution : aller à la source de l'information, les transactions 31 | 32 | 7) Ecrire et protéger l'Histoire 33 | - rappel du fonctionnement : majoritaire, fork, confirmation 34 | - problème : Vilain peut modifier le livre 35 | - solution : figer le livre avec des hash 36 | - problème : Vilain a la mainmise sur le réseau 37 | - solution : preuve de travail 38 | 39 | 8) Scalabilité 40 | - une preuve de travai pour chaque transaction 41 | - rassembler les transactions : des blocs 42 | - chaîner les blocs comme on le faisait pour les transactions 43 | - résultat : une BlockChain. 44 | 45 | 9) Présentation code 46 | 47 | 10) Questions ? -------------------------------------------------------------------------------- /operation.js: -------------------------------------------------------------------------------- 1 | let rp = require('request-promise-native'); 2 | let log = require('./utils/log.js'); 3 | let db = require('./db.js'); 4 | let Promise = require("bluebird"); 5 | let p2p = require('./p2p.js'); 6 | let crypto = require('./keys.js'); 7 | 8 | module.exports = { 9 | process: (data) => { 10 | 11 | //log('Process: ' + JSON.stringify(data)); 12 | 13 | switch (data.operation) { 14 | 15 | case 'getPeers': 16 | return Promise.resolve(JSON.stringify(p2p.getPeers())); 17 | 18 | case 'getState': 19 | let state = db.getHash(); 20 | return Promise.resolve(JSON.stringify({ state })); 21 | 22 | case 'getNbTransactions': 23 | return Promise.resolve(JSON.stringify({ nbTransactions: db.getNbTransactions() })); 24 | 25 | case 'getDatabase': 26 | log('Process getDatabase: ' + response) 27 | return Promise.resolve(JSON.stringify(db.getDatabase())); 28 | 29 | case 'transaction': 30 | log('Process transaction: ' + JSON.stringify(data)); 31 | let publicKey = db.getAccount(data.fromId).publicKey; 32 | let message = { operation: 'transaction', fromId: data.fromId, toId: data.toId, amount: data.amount }; 33 | let messageStr = JSON.stringify(message); 34 | return crypto.uncrypt(data.signature, publicKey) 35 | .then(uncrypted => { 36 | if (uncrypted !== messageStr) { 37 | log('Nope, invalid signature'); 38 | return Promise.throw('Invalid signature !') 39 | } 40 | return db.transaction(data.fromId, data.toId, data.amount) 41 | }) 42 | .then(() => p2p.broadcast()) 43 | .then(() => JSON.stringify({ status: 'ok' })) 44 | .catch(message => JSON.stringify({ status: 'error', message })) 45 | 46 | default: 47 | return Promise.resolve('Operation unknown'); 48 | } 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /peer.js: -------------------------------------------------------------------------------- 1 | let Promise = require("bluebird"); 2 | let http = require('http'); 3 | let fs = require('fs'); 4 | let log = require('./utils/log.js'); 5 | let p2p = require('./p2p.js'); 6 | let db = require('./db.js'); 7 | let crypto = require('./keys.js'); 8 | let request = require('./utils/request.js'); 9 | let operation = require('./operation.js'); 10 | 11 | global.id = process.argv[2] || (!console.error('Need port/id') && process.exit(1)); 12 | 13 | log('Now starting peer ' + global.id); 14 | 15 | let reset = () => { 16 | log('Reset'); 17 | [ 18 | './data/' + global.id + '-peers.json', 19 | './data/' + global.id + '-db.json', 20 | './data/' + global.id + '-keys.json', 21 | ].forEach(file => fs.existsSync(file) && fs.unlinkSync(file)); 22 | }; 23 | 24 | let launchServer = () => { 25 | log('LaunchServer'); 26 | let port = 5000 + parseInt(global.id); 27 | http.createServer((req, res) => { 28 | res.setHeader('Access-Control-Allow-Origin', '*') 29 | res.writeHead(200, { 'Content-Type': 'application/json' }); 30 | var body = []; 31 | req 32 | .on('data', chunk => body.push(chunk)) 33 | .on('end', () => { 34 | let data = JSON.parse(Buffer.concat(body).toString()); 35 | p2p.addPeer({ id: data.origin }) 36 | .then(() => operation.process(data)) 37 | .then(responseContent => { 38 | //log('Response : ' + responseContent); 39 | res.end(responseContent) 40 | }); 41 | }); 42 | }) 43 | .listen(port) 44 | .on('error', e => console.log(`Server error: ${e}`)) 45 | log(`Listening on port ${port}`); 46 | } 47 | 48 | process.argv[3] === 'reset' && reset(); 49 | 50 | let createAccountIfNeeded = () => { 51 | if (!db.getAccount(global.id)) { 52 | return db.createAccount(global.id); 53 | } 54 | return Promise.resolve(); 55 | } 56 | 57 | Promise.resolve() 58 | .then(() => p2p.updatePeersList()) 59 | .then(() => db.loadDatabase()) 60 | .then(() => createAccountIfNeeded()) 61 | .then(() => launchServer()) 62 | .then(() => { 63 | setInterval(() => { 64 | log('Refresh peers and database'); 65 | p2p.updatePeersList().then(() => db.loadDatabase()); 66 | }, 1000 * 60); 67 | }); 68 | -------------------------------------------------------------------------------- /spec/support/keys_spec.js: -------------------------------------------------------------------------------- 1 | var crypt = require('../../keys.js'); 2 | 3 | describe("My own crypto lib", () => { 4 | 5 | it("generates added keys", (done) => { 6 | crypt.generateKeys(1) 7 | .then(keys => { 8 | expect((keys.priv + keys.pub) === 256).toBe(true); 9 | }) 10 | .finally(done); 11 | }); 12 | 13 | it("crypt with public key and uncrypt with private key", (done) => { 14 | let clear = 'coucou fait le coucou'; 15 | let _keys; 16 | crypt.generateKeys(1) 17 | .then(keys => { 18 | _keys = keys; 19 | return crypt.crypt(clear, keys.pub); 20 | }) 21 | .then(crypted => { 22 | expect(crypted).toBeTruthy() 23 | return crypt.uncrypt(crypted, _keys.priv); 24 | }) 25 | .then(uncrypted => { 26 | console.log('uncrypted', uncrypted); 27 | expect(uncrypted).toBe(clear) 28 | }) 29 | .catch((msg) => console.log(msg)) 30 | .finally(done); 31 | }); 32 | 33 | it("crypt with private key and uncrypt with public key", (done) => { 34 | let clear = 'coucou fait le coucou'; 35 | crypt.generateKeys(1) 36 | .then(keys => { 37 | return crypt.crypt(clear, keys.priv); 38 | }) 39 | .then(crypted => { 40 | expect(crypted).toBeTruthy() 41 | return crypt.uncrypt(crypted, keys.pub); 42 | }) 43 | .then(uncrypted => { 44 | expect(uncrypted).toBe(clear) 45 | }) 46 | .finally(done); 47 | }); 48 | 49 | it("hash to the same value", (done) => { 50 | let clear = 'coucou fait le coucou'; 51 | let _hash; 52 | crypt.hash(clear) 53 | .then(hash => { 54 | _hash = hash; 55 | }) 56 | .then(() => { 57 | return crypt.hash(clear); 58 | }) 59 | .then(hash2 => { 60 | expect(hash2 === _hash).toBe(true) 61 | }) 62 | .finally(done); 63 | }); 64 | 65 | it("hash to a different value", (done) => { 66 | let clear = 'coucou fait le coucou'; 67 | let _hash; 68 | crypt.hash(clear) 69 | .then(hash => { 70 | _hash = hash; 71 | }) 72 | .then(() => { 73 | return crypt.hash(clear+' '); 74 | }) 75 | .then(hash2 => { 76 | expect(hash2 === _hash).toBe(false) 77 | }) 78 | .finally(done); 79 | }); 80 | }); -------------------------------------------------------------------------------- /keys.js: -------------------------------------------------------------------------------- 1 | let Promise = require("bluebird"); 2 | let readFile = Promise.promisify(require("fs").readFile); 3 | let writeFile = Promise.promisify(require("fs").writeFile); 4 | let log = require('./utils/log.js'); 5 | 6 | let domain = 256; 7 | let _keys = {}; 8 | 9 | module.exports = { 10 | 11 | generateKeys: (id) => { 12 | log('Generate keys'); 13 | let pub = 1 + parseInt(id); 14 | let priv = domain - pub; 15 | _keys = { priv, pub }; 16 | return Promise.resolve(_keys); 17 | }, 18 | getKeys: () => { 19 | return _keys; 20 | }, 21 | saveKeys: () => { 22 | log('Save keys'); 23 | return writeFile('./data/' + global.id + '-keys.json', JSON.stringify(_keys), 'utf8'); 24 | }, 25 | loadKeys: () => { 26 | return readFile('./data/' + global.id + '-keys.json', 'utf8').then(content => _keys = JSON.parse(content)); 27 | }, 28 | crypt: (text, key) => { 29 | return Promise.resolve( 30 | Array.from(text) 31 | .map(c => (c.charCodeAt(0) + key) % domain) 32 | .map(code => code.toString(16)) 33 | .map(c => (c.length === 1 ? '0' : '') + c) 34 | .join(' ') 35 | ); 36 | }, 37 | uncrypt: (hexText, key) => { 38 | return Promise.resolve( 39 | hexText.split(' ') 40 | .map(hex => parseInt(hex, 16)) 41 | .map(code => (code + key) % domain) 42 | .map(code => String.fromCharCode(code)) 43 | .join('') 44 | ); 45 | }, 46 | hash: (text) => { 47 | let nbVoyelles = 0; 48 | let nbConsonnes = 0; 49 | let nbSpaces = 0; 50 | let sum = 0; 51 | Array.from(text).forEach(c => { 52 | if (['a', 'e', 'i', 'o', 'u'].indexOf(c.toLowerCase()) !== -1) { 53 | nbVoyelles++; 54 | } 55 | if (['B', 'C', 'D', 'F', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'V', 'W', 'X', 'Z'].indexOf(c.toUpperCase()) !== -1) { 56 | nbConsonnes++; 57 | } 58 | if (c === ' ') { 59 | nbSpaces++; 60 | } 61 | sum += c.charCodeAt(0); 62 | }); 63 | let h = text.length.toString() + nbVoyelles.toString() + nbConsonnes.toString() + sum.toString(); 64 | return Promise.resolve(h); 65 | }, 66 | sign: (text, privateKey) => { 67 | let hash = module.exports.hash(text); 68 | return module.exports.crypt(hash, privateKey); 69 | }, 70 | checkSignature: (text, signature, publicKey) => { 71 | let expectedHash = module.exports.hash(text); 72 | return module.exports.uncrypt(signature, publicKey) 73 | .then(hash => hash === expectedHash); 74 | } 75 | } -------------------------------------------------------------------------------- /db.js: -------------------------------------------------------------------------------- 1 | let Promise = require("bluebird"); 2 | let readFile = Promise.promisify(require("fs").readFile); 3 | let writeFile = Promise.promisify(require("fs").writeFile); 4 | let log = require('./utils/log.js'); 5 | let crypto = require('./keys.js'); 6 | let fs = require('fs'); 7 | let p2p = require('./p2p.js'); 8 | let request = require('./utils/request.js'); 9 | let md5 = require('md5'); 10 | 11 | let _db = { 12 | lastOperationTimestamp: 0, 13 | nbTransactions: 0, 14 | accounts: [] 15 | } 16 | 17 | module.exports = { 18 | loadDatabase: () => { 19 | log('Load database'); 20 | if (!fs.exists('./data/' + global.id + '-db.json')) { 21 | log('No local database found'); 22 | return module.exports.fetchDatabase(); 23 | } 24 | return readFile('./data/' + global.id + '-db.json', 'utf8') 25 | .then(content => _db = JSON.parse(content)) 26 | .catch(() => { }); 27 | }, 28 | fetchDatabase: () => { 29 | log('Fetch database'); 30 | return p2p.getUpToDatePeer() 31 | // .then(peer => { if (!peer) throw 'No peer'; else return peer; }) 32 | // .then(peer => request.send(peer, { operation: 'getDatabase' })) 33 | // .then(content => _db = JSON.parse(content)) 34 | // .catch((msg) => { log('Could not fetch database: '+msg); }) 35 | }, 36 | saveDatabase: () => { 37 | log('Save database'); 38 | _db.lastOperationTimestamp = (newDate()).getTime(); 39 | return writeFile('./data/' + global.id + '-db.json', JSON.stringify(_db), 'utf8'); 40 | }, 41 | createAccount: (id) => { 42 | log('Create account'); 43 | return crypto.generateKeys(id) 44 | .then(() => crypto.saveKeys()) 45 | .then(() => _db.accounts.push({ id, balance: 100, publicKey: crypto.getKeys().pub })) 46 | .then(() => module.exports.saveDatabase()); 47 | }, 48 | getDatabase: () => _db, 49 | getState: () => _db, 50 | compareStates: (s1, s2) => { 51 | }, 52 | getNbTransactions: () => _db.nbTransactions, 53 | getAccounts: () => _db.accounts, 54 | getAccount: accountId => _db.accounts.find(account => account.id === accountId), 55 | transaction: (fromId, toId, amount) => { 56 | log('transaction from ' + fromId + ' to ' + toId + ' for an amount of ' + amount); 57 | let from = module.exports.getAccount(fromId); 58 | if (!from) { 59 | return Promise.reject('Source account ' + fromId + ' not found'); 60 | } 61 | let to = module.exports.getAccount(toId); 62 | if (!to) { 63 | return Promise.reject('Destination account ' + toId + ' not found'); 64 | } 65 | if (amount > from.balance) { 66 | return Promise.reject('Not enough fund'); 67 | } 68 | from.balance -= amount; 69 | to.balance += amount; 70 | return Promise.resolve(); 71 | } 72 | }; -------------------------------------------------------------------------------- /utils/log.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /p2p.js: -------------------------------------------------------------------------------- 1 | let Promise = require("bluebird"); 2 | let readFile = Promise.promisify(require("fs").readFile); 3 | let writeFile = Promise.promisify(require("fs").writeFile); 4 | let request = require('./utils/request.js'); 5 | let log = require('./utils/log.js'); 6 | let db = require('./db.js'); 7 | let _peers = []; 8 | 9 | module.exports = { 10 | updatePeersList: () => { 11 | log('Update peers list from known peers'); 12 | return loadKnownPeers() 13 | .then(knownPeers => { 14 | return Promise.each( 15 | knownPeers.filter(knownPeer => knownPeer.id !== global.id).map(knowPeer => askPeersOf(knowPeer)), 16 | peers => peers.forEach(peer => addPeer(peer)) 17 | ); 18 | }) 19 | .then(() => savePeers()) 20 | .then(() => _peers); 21 | }, 22 | addPeer: (peer) => { 23 | addPeer(peer); 24 | return savePeers(); 25 | }, 26 | getPeers: () => { 27 | return _peers 28 | }, 29 | getUpToDatePeer: () => { 30 | log('Get most up to date peer'); 31 | let localState = db.getState(); 32 | let max = -1; 33 | let upToDatePeer = undefined; 34 | return Promise.each( 35 | _peers.map(peer => askStateOf(peer)), 36 | (remoteState, index) => { 37 | if (remoteState) 38 | upToDatePeer = upToDatePeer || _peers[index]; 39 | if (state > max) { 40 | max = state; 41 | upToDatePeer = _peers[index]; 42 | } 43 | }) 44 | .then(() => upToDatePeer); 45 | }, 46 | broadcast: (fromId, toId, amount) => { 47 | log('Broadcast transaction from ' + fromId + ' to ' + toId + ' for an amount of ' + amount); 48 | return Promise.each( 49 | knownPeers 50 | .filter(knownPeer => knownPeer.id !== global.id) 51 | .map(knowPeer => request.send(knownPeer, { operation: 'transaction', fromId, toId, amount })), 52 | () => { }); 53 | } 54 | }; 55 | 56 | loadKnownPeers = () => { 57 | log('Load known peers'); 58 | return readFile('./data/' + global.id + '-peers.json', 'utf8') 59 | .then(content => JSON.parse(content)) 60 | .then(knownPeers => { 61 | knownPeers.forEach(peer => addPeer(peer)) 62 | log(knownPeers.length + ' peers loaded: ' + knownPeers.map(p => p.id).join(' ')); 63 | return knownPeers; 64 | }) 65 | // Aucun peer connu, on force au Peer Zero 66 | .catch(() => [{ id: '0' }]); 67 | } 68 | 69 | askPeersOf = (peer) => { 70 | log('Ask peers list of ' + peer.id); 71 | return request.send(peer, { origin: global.id, operation: 'getPeers' }) 72 | .catch(() => { 73 | log('Peer ' + peer.id + ' is not available (getPeers)'); 74 | return []; 75 | }); 76 | }; 77 | 78 | addPeer = (peer) => { 79 | if (peer && peer.id && !_peers.find(p => p.id === peer.id) && peer.id !== global.id) { 80 | _peers.push({ id: peer.id }); 81 | } 82 | }; 83 | 84 | savePeers = () => { 85 | log('Save ' + _peers.length + ' peers: ' + _peers.map(p => p.id).join(' ')); 86 | return writeFile('./data/' + global.id + '-peers.json', JSON.stringify(_peers), 'utf8'); 87 | }; 88 | 89 | askStateOf = (peer) => { 90 | log('Ask state to ' + peer.id); 91 | let _response; 92 | return request.send(peer, { operation: 'getDatabase' }) 93 | .then(response => { 94 | log(`getState to ${peer.id}`); 95 | return response; 96 | }) 97 | .catch(() => { 98 | log(`Peer ${peer.id} not available (getState)`); 99 | return 0; 100 | }) 101 | }; 102 | 103 | compareStates = (local, remote) => { 104 | 105 | }; 106 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fabriquer sa propre BlockChain avec NodeJS 2 | 3 | ## Objectif 4 | Fabriquer sa propre BlockChain en NodeJs pour comprendre le comment et le pourquoi d'un tel système. Et pour cela, rien ne vaut le douloureux et magnifique cheminement du try-fail-fix-retry ! 5 | 6 | ## Pourquoi une BlockChain ? 7 | Quand des entités (personnes, sociétés, administrations, etc.) s'entendent sur une relation entre elles, elle doivent généralement passer par un tiers de confiance. 8 | Exemple : transférer de l'argent d'une personne A à une personne B implique une banque en tant que tiers de confiance. A et B font confiance à la banque pour s'assurer que le transfert sera valide. La banque vérifiera les identités de A et B, le solde de B, procèdera au transfert, mettra à jour le compte de B, etc. 9 | Dans ce système le tiers détient un pouvoir considérable. Comment faire pour ne plus avoir à passer par un tiers ? 10 | Passer par une BlockChain ! 11 | 12 | ## Principes de base d'une BlockChain 13 | Une BlockChain est **un grand livre** dans lequel tout le monde peut écrire. Ce qui est écrit fait office de vérité : comptes bancaires, contrats, cadastres, etc. 14 | Le livre est **partagé, maintenu et surveillé** non plus par un tiers, mais **par un réseau** constitué par tous ceux qui ont intérêt dans cette BlockChain. Cet intérêt peut provenir de diverses sources : opinions politiques, opportunités commerciales, rémunération, etc. Le livre est donc présent sur tous les noeuds du réseau. L'important est que tout le monde possède une même version valide, car c'est elle qui fera office de vérité. Une version différente (manipulée !) doit donc être rejetée par le réseau. 15 | 16 | Exemple de BlockChain sur contrat : la société A et la société B contractualisent une opération (vente, service, partenariat...). Le contrat est écrit et signé dans le livre par A, et si B est d'accord avec les termes, il le signe dans le livre. Le réseau s'occupe de s'assurer que : 17 | * A et B sont ceux qu'ils prétendent être 18 | * ni A, ni B, ni personne ne peut modifier le contrat 19 | 20 | ## Mise en pratique avec une monnaie 21 | Une monnaie est juste une unité qui permet de réprésenter une valeur lié à des produits ou services. Nous utilisons la monnaie via des tiers de confiance imposés : les états et les banques. Les états créent la monnaie à leur guise : Banque Centrale Européenne, Federal Reserve américaine... Les banques commerciales fournissent les outils pour manipuler la monnaie : virements, chéquiers, cartes bancaires, mais aussi prêts, placements, etc. 22 | 23 | Comment faire pour se passer des états et des banques ? 24 | En les remplaçant par une BlockChain bien sûr ! 25 | Notre objectif est donc de pouvoir manipuler une monnaie au travers d'un grand livre partagé, maintenu et surveillé par un réseau. 26 | 27 | ## Mise en place du réseau 28 | Chaque machine du réseau possède une liste des autres machines (les _peers_) qu'elle connaît. Lors de son entrée sur le réseau, la liste de peers est vide. Nous utiliserons donc un _Peer Zero_ pour obtenir une première liste. Cela fait, notre machine se connectera aux peers, et récupèrera leurs listes. Et bien sûr, elle mettra à disposition sa propre liste. 29 | Pour se simplifier la vie en terme de connexions : 30 | * Utilisation du protocole HTTP. 31 | * Afin de faire tourner le réseau sur une seule machine, on se connecte via les ports à partir du port 5000. 32 | 33 | Donc pour lancer un peer : 34 | 35 | node peer.js [port] 36 | 37 | ## Simuler une banque 38 | Nous avons un réseau qui se maintient à peu près tout seul. A présent il nous faut le livre. Mais que mettre dedans ? 39 | Notre approche naïve nous amène à simplement vouloir "faire la banque" telle que nous la connaissons. 40 | Nous écrirons donc des "comptes" dans le livre : 41 | 42 | * un numéro d'identification 43 | * un solde 44 | 45 | Une opération de transfert/paiement consistera donc en la modification de deux comptes. 46 | Par ailleurs, pour nous simplifier la vie chaque peer ne disposera que d'un seul compte. 47 | Pour réprésenter le livre il nous faut donc une base de données. 48 | A ce stade, un simple fichier JSON fera l'affaire : 49 | 50 | [ 51 | { accountId: "12345", balance: 100}, 52 | { accountId: "98765", balance: 250}, 53 | { accountId: "49875", balance: 123}, 54 | ] 55 | 56 | Transférer de l'argent consiste à envoyer un ordre au réseau. Il nous faut donc un protocole. 57 | Comme nous reposons sur HTTP et que que nous voulons écrire le moins de code possible ("moins de code, moins de bugs !"), l'ordre sera transmis sous forme d'une requête POST avec du JSON dans le corps : 58 | 59 | { operation: "transaction", from: "1111", to: "2222", amount: 25 } 60 | 61 | Ca se code facile avec NodeJs : 62 | 63 | let port = 5000 + parseInt(global.id); 64 | http.createServer((req, res) => { 65 | var body = []; 66 | req.on('data', (chunk) => { 67 | body.push(chunk); 68 | }).on('end', () => { 69 | body = Buffer.concat(body).toString(); 70 | let data = JSON.parse(body); 71 | switch (data.operation) { 72 | // ... 73 | } 74 | }); 75 | }).listen(port); 76 | 77 | ## Synchronisation 78 | Nous voulons maintenir la même copie sur l'ensemble du réseau. Pour cela, les peers vont s'échanger en permanence l'état de leur livre. Quand un peer voit une version plus récente que la sienne, il la télécharge. 79 | Mais afin d'éviter la notion de temps pour déterminer la dernière version, on passe par un compteur de transactions plutôt qu'un timestamp. Notre base devient donc : 80 | 81 | { 82 | nbTransactions: 42, 83 | accounts: [ 84 | { accountId: "12345", balance: 100}, 85 | { accountId: "98765", balance: 250}, 86 | { accountId: "49875", balance: 123}, 87 | ] 88 | } 89 | 90 | **En pratique ça ne fonctionne pas du tout. 91 | Mais mettons cela de côté pour le moment et retenons que la version la plus récente est diffusée sur le réseau.** 92 | 93 | ## Authentification 94 | Nous avons donc un réseau peer-to-peer et une base de données synchronisée. A présent, lorsque quelqu'un veut faire un transfert d'argent, il va modifier son compte et celui du destinataire. Comment s'assurer que celui qui provoque le transfert est bien le propriétaire du compte ? 95 | Avec un login et un mot de passe ? 96 | Mais qui va alors gérer la base de couples login/password ? Et où serait-elle stockée ? 97 | Il ne sert à rien de les mettre dans la même base que les comptes puisque c'est une base publique, partagée et synchronisée. Donc tout le monde connaîtrait les mots de passe. Et même si nous les hashons, ça resterait trop limite au niveau sécurité. 98 | Et bien sûr, confier la base à une entité tierce invalide totalement tout le concept de notre BlockChain décentralisée. 99 | 100 | La solution réside dans les principes de la cryptographie asymétrique : 101 | 102 | * On crée deux clefs : A et B 103 | * Ce qui est chiffré avec A peut être déchiffré avec B 104 | * Et inversement, ce qui est chiffré avec B peut être déchiffré avec A 105 | 106 | On peut donc décider d'avoir une clef connue par tout le monde - la *clef publique* - et une clef connue uniquement par son propriétaire - la *clef privée*. A partir de là, nous disposons d'une solution : 107 | 108 | * Au tout premier lancement d'un peer nous allons générer un couple clefs privée/publique et créé un compte 109 | * La clef publique va être stockée avec le compte dans la base (partagée et synchronisée) 110 | * Imaginons qu'Alice veut dépenser de l'argent de son compte 111 | * Pour cela celle écrit un message : *"moi Alice, propriétaire du compte 1234 je veux transférer 25 au compte 5678"* 112 | * Elle chiffre son message avec sa clef privée 113 | * Elle envoie au réseau le message en clair et en chiffré 114 | * Le réseau reçoit les deux versions et déchiffre la version chiffrée avec la clef publique de compte 115 | * Si le résultat du déchiffrage correspond à la version en clair, cela prouve que c'est bien Alice qui a écrit ce message avec sa clef privée. 116 | * Et donc qu'elle est bien propriétaire de ce compte et habilitée à en dépenser de l'argent 117 | 118 | On notera que ce système authentifie l'émetteur d'un ordre de virement, et qu'il protège aussi contre la falsification. Un Vilain est un peer, il reçoit la transaction et cherche à la modifier. Sans la clef privée il ne peut rien faire. 119 | 120 | ## Commandes 121 | Notre réseau est en place, notre base de données aussi, il reste à pouvoir faire des transferts d'argent. 122 | Nous partons du principe que chaque peer possède un seul compte et que seul son propriétaire peut transférer de l'argent. Un peer doit donc être en mesure de recevoir une commande lui disant d'opérer un transfert. 123 | Niveau user-interface il suffit de simples requêtes avec CURL : 124 | 125 | # Pour que le peer à l'écoute sur le port 5123 126 | # effectue un transfert depuis son compte vers le compte 567 127 | # pour un montant de 25 128 | curl 127.0.0.1:5123 --data '{"operation": "transaction", "from": "123", "to": "567", "amount": 25}' 129 | 130 | Le fichier `spend.js` permet de faire de même. 131 | 132 | ## Dépenser de l'argent 133 | Le peer reçoit donc un ordre de transfert. Il vérifie dans sa version du livre que l'opération est possible : le compte destination existe-t'il ? les fonds sont-ils suffisants ? 134 | Si le transfert est valide, il est effectué dans son livre. Il lui reste alors à informer les noeuds du réseau qu'il a une nouvelle version à leur fournir. Evidemment, on ne va pas envoyer l'intégralité de la base de données : elle va s'alourdir avec le nombre d'utilisateurs jusqu'à très probablement devenir impraticable. 135 | On peut alors envoyer un diff/patch de la base. Ce qui revient en fait à envoyer une simple transaction ! 136 | 137 | Donc un peer reçoit une transaction, la valide et la renvoie au réseau, ie à tous ses peers. 138 | Lorsqu'un peer reçoit une nouvelle transaction, il procède de la même façon. A bout du compte chacun à la même version du livre. 139 | 140 | ## Problèmes conceptuels 141 | Nous n'avons pas encore traité sérieusement la synchronisation de la base de données. Compter les transactions n'est absolument pas viable : notre réseau est - par nature - asynchrone, et notre livre évolue. On a donc une double mutabilité : spatiale (le réseau) et temporelle (la base). Concrètement, à un moment donné un peer peut avoir 10 transactions, un autre peut aussi avoir 10 transactions, alors que ce ne sont pas (ou partiellement) les mêmes. Il y a deux états qui sont potentiellement impossible à fusionner. 142 | La source du problème vient du fait que nous voyons notre livre comme un état global et mutable : il évolue dans le temps et on perd son historique. 143 | 144 | On peut mettre de côté la mutabilité en rendant le livre immutable : les données ne sont pas modifiées, il n'y a que des ajouts. On stocke alors un historique de comptes bancaires : 145 | 146 | accountsHistory: [ 147 | [ { id: "1", balance: 100}, { id: "2", balance: 100}, { id: "3", balance: 100}, ... ], 148 | [ { id: "1", balance: 120}, { id: "2", balance: 80}, { id: "3", balance: 100}, ... ], 149 | [ { id: "1", balance: 110}, { id: "2", balance: 80}, { id: "3", balance: 110}, ... ], 150 | ... 151 | ] 152 | 153 | Dans ce système, les peers reçoivent les transactions, les appliquent à la dernière version du livre de leur historique. Ils peuvent ensuite échanger celui-ci avec le reste du réseau. Les peers ayant l'historique le plus important sont les référents pour les autres. Cela fonctionne à condition que les décalages entre les différentes partie du réseau ne concernent que des comptes différents : 154 | 155 | * si A, B, C, D, E sont des comptes. A possède un solde de 100. 156 | * si N1 est une sous-partie du réseau 157 | * si N2 est une autre sous-partie du réseau 158 | * N1 reçoit A-70->B, B-20->C et met à jour l'historique en conséquence 159 | * N2 reçoit D-10->E et fait de même 160 | * quand N1 et N2 s'échangent leurs historiques, pas de problème : ils ne travaillent pas sur les mêmes comptes. 161 | 162 | Mais si N2 reçoit A-50->C ? Une des deux transactions issues de A est invalide puisque 70 + 50 > 100. Mais cette *double dépense* est impossible à détecter puisqu'on ne connaît que le résultat des transactions : les soldes des comptes. Même si on ajoute un timestamp sur chaque historique, il nous manque toujours l'information principale : les transactions. Tout ce qu'on sait : à un moment donné, sur telle partie du réseau la transaction est valide. Mais ça ne suffit pour la valider sur l'ensemble du réseau ! 163 | 164 | Il faut pallier au manque d'information inhérent à l'emploi de comptes bancaires et aller à la source des soldes : **nous allons désormais stocker les transactions**. 165 | Les soldes des comptes seront facile à déduire : il suffit d'additionner les transactions. 166 | Désormais notre livre prend cette forme : 167 | 168 | { 169 | transactions: [ 170 | { from: "123", to: "456", amount: 100 }, 171 | { from: "789", to: "852", amount: 180 }, 172 | { from: "357", to: "412", amount: 320 }, 173 | ... 174 | ] 175 | } 176 | 177 | ## Faire et protéger l'Histoire 178 | Dans notre nouveau système il n'y a plus d'échange de livres. Les transactions sont envoyées sur le réseau et inscrites par chaque peer qui les valide. En cas de double dépense, une partie du réseau aura raison et l'autre tort. La partie la plus rapide va se répandre sur une plus grande surface et prendre le pas sur l'autre. Pour prendre un exemple : 179 | 180 | * le compte A possède 100 181 | * il envoie 80 à B (transaction T1) 182 | * puis très vite 40 à C (transaction T2) 183 | 184 | Imaginons que T1 se répande plus vite sur le réseau recouvre la surface du sous-réseau N1. Tout peer qui proposera T2 à N1 se la verra refusée (plus assez de fond sur le compte A). Le sous-réseau N2 qui avait accepté T2 va se trouver en minorité. Et elle va s'en rendre compte parce que N1 va continuer de recevoir de nouvelles transactions. N2 saura qu'elle est en retard en comptant le nombre de transactions de N1, et devra donc abandonner les siennes pour prendre celles de N1. 185 | 186 | 190 | 191 | Il nous faut protéger l'historique des transactions. En effet, à ce stade notre livre de transactions reste falsifiable par un Vilain et il n'y a pas moyen de le savoir. Il faut trouver un moyen pour qu'une modification de l'historique soit immédiatement repérée par les peers honnêtes. 192 | Pour cela, nous allons "verrouiller" l'historique en chaînant les transactions entre elles : 193 | 194 | * la toute première transaction arrive sur le réseau : 195 | `T0 = { operation: "transaction", from: "25", to: "30", amount: "100" }` 196 | * on calcule son hash : 197 | `H0 = hash(T0)` 198 | * une nouvelle transaction T1 arrive : 199 | `T1 = { operation: "transaction", from: "40", to: "50", amount: "12" }` 200 | * cette fois on calcule le hash de T1+H0 et on le stocke dans T1 : 201 | `H1 = hash(T1+H0)` 202 | `T1 = { hash: H1, operation: "transaction", from: "40", to: "50", amount: "12" }` 203 | * encore une transaction T2 : 204 | `T2 = { operation: "transaction", from: "123", to: "456", amount: "789" }` 205 | `H2 = hash(T2+H1)` 206 | `T2 = { hash: H2, operation: "transaction", from: "123", to: "456", amount: "789" }` 207 | * et on recommence pour les transactions suivantes... 208 | 209 | On le voit, le hash de chaque nouvelle transaction dépend de la précédente, dont le hash dépend lui-même de la précédente, et ainsi de suite. Si un Vilain modifie une transaction, il va devoir reconstituer toute la chaîne depuis son point de modification. 210 | 211 | ## Le point sur les attaques possibles 212 | Le Vilain veut modifier une transaction qu'il a reçue. Comme la transaction est signée il ne peut pas la trafiquer. 213 | 214 | Notre Vilain émet une transaction illégale (fonds insuffisants) et l'envoie sur le réseau. Chaque peer consulte son historique de transactions, reconstitue le solde du Vilain et constate l'illégalité : la transaction est refusée. 215 | 216 | Le Vilain modifie sa version du livre. Pour cela il doit recalculer tous les hash de toutes les transactions à partir de celle qu'il a modifié. Il ré-écrit donc l'histoire à partir d'un certain moment. S'il détient une majorité du réseau il peut l'empoisonner progressivement pour que sa version du livre soit celle qui domine. Nous allons voir comment contrer cela. 217 | 218 | Le Vilain joue l'asychronicité du réseau : il émet une première transaction, puis une seconde qui est illégale (fonds insuffisants). Une partie du réseau traite la première, une autre traite la seconde. Séparément ces transactions sont valides. Ensemble elles ne devraient pas passer : Alice possède 100, dépense 70 sur une transaction, puis 50 sur une seconde. Ce type d'attaque est appelée "double dépense". 219 | Elle ne pose problème que si l'on fait confiance trop tôt à un peer. En effet, l'une des deux transactions va se répandre plus vite que l'autre sur le réseau. En allant plus vite, et avec l'addition de nouvelles transactions, on trouvera une chaîne de transactions plus longue chez certains peers. Ca sera celle-ci qui fera office de vérité. 220 | L'attaque n'est effective que si on ne laisse pas le temps aux tentatives de double dépense d'être "absorbée" par le réseau. 221 | On remarque qu'un Vilain en possession d'une majorité du réseau est en mesure de faire des double dépenses. 222 | 223 | ## Protéger le réseau 224 | Il faut protéger notre réseau d'une éventuelle mainmise sur une majorité de peers. 225 | Chaque fois qu'un peer veut modifier le livre (traiter une transaction) on va lui demander de résoudre un problème. Celui-ci doit être suffisamment coûteux en ressources afin que le coût de maintenance d'un grand nombre de peers soit trop important pour rendre l'opération rentable : c'est la *proof-of-work* (POW). 226 | 227 | Une POW possibile consiste à deviner un nombre. Pour cela on se base sur un hash. Par exemple, nous calculons le hash sur 16 bits de "Hello world". Cela va donner un nombre allant de 0 à 65535. 228 | Nous allons ensuite modifier notre chaîne "Hello world" en une nouvelle chaîne dont nous allons espérer que le 229 | hash soit inférieur à une certaine valeur. Plus cette valeur est petite plus la difficulté est grande. 230 | 231 | **Exemple :** 232 | Nous décidons d'avoir une difficulté élevée, il faudra trouver un nombre compris entre 0 et 100 à partir du hash de "Hello world". Nous allons ajouter un nombre (nonce) à notre chaîne et hasher le résultat. Tant que la valeur du hash est plus grande que 100, on recommence en incrémentant le nombre : 233 | * hash("Hello world0") = 43393 234 | * hash("Hello world1") = 43650 235 | * hash("Hello world2") = 43907 236 | * ... 237 | * hash("Hello world592") = 42 **BINGO !** 238 | 239 | Dans le cas de notre BlockChain, comment choisir le hash de départ et la difficulté ? 240 | Pour le hash on peut se baser sur n'importe quelle données instable dans le temps. Par exemple, la base de données elle-même. On la hash à chaque nouvelle modification et on se base sur cette valeur de départ pour trouver un nombre inférieur à notre repère de difficulté. 241 | 242 | La difficulté doit être en adéquation avec la taille du réseau et la puissance mise à disposition par les technologies. On peut utiliser la taille de la base de données par exemple : on peut supposer que plus elle est importante, plus le réseau est grand, plus il y a les ressources pour affronter la POW. 243 | 244 | Un peer qui a résolu en premier la POW va pouvoir modifier le livre en ajoutant la transaction. Au passage il insère sa solution trouvée. Ce faisant, il sécurise encore plus notre BlockChain : chacun peut vérifier qu'il a effectivement fait la POW qui correspond à la transaction. 245 | 246 | ## Les blocs 247 | Dans notre système, chaque transaction doit être validée individuellement par une POW. Comme les transactions sont chaînées entre elles, nous avons une scalabilité catastrophique. 248 | Pour que notre réseau soit efficace, nous allons réunir les transactions dans des blocs. Une fois un bloc rempli, la POW est appliqué dessus, et on chaîne les blocs entre eux comme nous le faisions avec les transactions. Nous avons alors littéralement une chaîne de blocs : une *BlockChain*. --------------------------------------------------------------------------------