├── _config.yml ├── src ├── consensus │ ├── constants │ │ ├── MessageTypes.ts │ │ ├── NodeStates.ts │ │ ├── EventTypes.ts │ │ └── VoteTypes.ts │ ├── interfaces │ │ ├── ILoggerInterface.ts │ │ └── ISettingsInterface.ts │ ├── utils │ │ ├── utils.ts │ │ └── cryptoUtils.ts │ ├── models │ │ ├── PacketModel.ts │ │ ├── VoteModel.ts │ │ └── NodeModel.ts │ ├── api │ │ ├── MessageApi.ts │ │ ├── NodeApi.ts │ │ └── VoteApi.ts │ ├── services │ │ └── RequestProcessorService.ts │ ├── controllers │ │ └── HeartbeatController.ts │ └── main.ts ├── test │ ├── unit │ │ ├── BFT.spec.ts │ │ ├── components │ │ │ ├── VoteApi.spec.ts │ │ │ └── NodeApi.spec.ts │ │ └── bft │ │ │ └── testSuite.ts │ └── integration │ │ ├── consensus.spec.ts │ │ ├── workers │ │ ├── MokkaTCPWorker.ts │ │ └── MokkaRPCWorker.ts │ │ └── consensus │ │ └── testSuite.ts └── implementation │ ├── RPC.ts │ ├── TCP.ts │ └── ZMQ.ts ├── .npmignore ├── examples └── node │ ├── cluster │ ├── src │ │ ├── gen_keys.ts │ │ └── cluster.ts │ ├── tsconfig.json │ ├── package.json │ ├── tslint.json │ └── README.md │ └── decentralized-ganache │ ├── src │ ├── gen_keys.ts │ ├── config.ts │ └── server.ts │ ├── tsconfig.json │ ├── package.json │ ├── tslint.json │ └── README.md ├── .travis.yml ├── tsconfig.json ├── webpack.config.js ├── tslint.json ├── package.json ├── README.md └── LICENSE /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-slate -------------------------------------------------------------------------------- /src/consensus/constants/MessageTypes.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable 2 | export default { 3 | ACK: 10, 4 | VOTE: 11, 5 | VOTED: 12 6 | }; 7 | -------------------------------------------------------------------------------- /src/consensus/constants/NodeStates.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable 2 | export default { 3 | STOPPED: 0, 4 | LEADER: 1, 5 | CANDIDATE: 2, 6 | FOLLOWER: 3 7 | }; 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .travis.yml 3 | src 4 | tsconfig.json 5 | tslint.json 6 | dist/test 7 | _config.yml 8 | webpack.config.js 9 | webpack_report.html 10 | examples 11 | temp -------------------------------------------------------------------------------- /src/consensus/interfaces/ILoggerInterface.ts: -------------------------------------------------------------------------------- 1 | export interface ILoggerInterface { 2 | 3 | info(text: string): void; 4 | trace(text: string): void; 5 | error(text: string): void; 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/test/unit/BFT.spec.ts: -------------------------------------------------------------------------------- 1 | import {testSuite} from './bft/testSuite'; 2 | 3 | describe('BFT tests (4 nodes)', () => testSuite({}, 4)); 4 | 5 | describe('BFT tests (7 nodes)', () => testSuite({}, 7)); -------------------------------------------------------------------------------- /src/consensus/constants/EventTypes.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable 2 | export default { 3 | NODE_JOIN: 'join', 4 | NODE_LEAVE: 'leave', 5 | HEARTBEAT_TIMEOUT: 'heartbeat_timeout', 6 | STATE: 'state', 7 | ACK: 'ack' 8 | }; 9 | -------------------------------------------------------------------------------- /src/consensus/constants/VoteTypes.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable 2 | export default { 3 | NO_SHARE: 0, 4 | CANDIDATE_OUTDATED_BY_TERM: 1, 5 | CANDIDATE_OUTDATED_BY_HISTORY: 2, 6 | CANDIDATE_HAS_WRONG_HISTORY: 3 7 | }; 8 | -------------------------------------------------------------------------------- /examples/node/cluster/src/gen_keys.ts: -------------------------------------------------------------------------------- 1 | import crypto from 'crypto'; 2 | 3 | for (let i = 0; i < 3; i++) { 4 | const node = crypto.createECDH('secp256k1'); 5 | node.generateKeys('hex'); 6 | console.log(`pair[${i + 1}] {publicKey: ${node.getPublicKey('hex')}, secretKey: ${node.getPrivateKey('hex')}`); 7 | } 8 | -------------------------------------------------------------------------------- /examples/node/decentralized-ganache/src/gen_keys.ts: -------------------------------------------------------------------------------- 1 | import crypto from 'crypto'; 2 | 3 | for (let i = 0; i < 3; i++) { 4 | const node = crypto.createECDH('secp256k1'); 5 | node.generateKeys('hex'); 6 | console.log(`pair[${i + 1}] {publicKey: ${node.getPublicKey('hex', 'compressed')}, secretKey: ${node.getPrivateKey('hex')}`); 7 | } 8 | -------------------------------------------------------------------------------- /src/consensus/utils/utils.ts: -------------------------------------------------------------------------------- 1 | export const getCombinations = (elements, n, pairs = [], pair = []): string[][] => { 2 | return elements.reduce((accumulator, val, index) => { 3 | 4 | pair.push(val); 5 | 6 | if (n > 1) { 7 | getCombinations(elements.slice(index + 1), n - 1, accumulator, pair); 8 | } else { 9 | accumulator.push([...pair]); 10 | } 11 | 12 | pair.pop(); 13 | return accumulator; 14 | }, pairs); 15 | }; 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | dist: bionic 3 | os: linux 4 | group: stable 5 | language: node_js 6 | node_js: 12.1.0 7 | 8 | before_install: 9 | - sudo apt-get update 10 | - sudo apt-get install python2.7 git -y 11 | - npm install -g node-gyp 12 | 13 | install: 14 | - npm install 15 | - npm run tsc:build 16 | 17 | script: 18 | - mkdir dump 19 | - npm test 20 | 21 | notifications: 22 | email: false 23 | 24 | cache: 25 | directories: 26 | - node_modules -------------------------------------------------------------------------------- /examples/node/cluster/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDir": "./src", 4 | "module": "commonjs", 5 | "target": "es2018", 6 | "noImplicitAny": false, 7 | "sourceMap": true, 8 | "outDir": "./dist", 9 | "noEmitOnError": true, 10 | "declaration": true, 11 | "esModuleInterop": true, 12 | "lib": ["es2018","esnext.asynciterable"] 13 | }, 14 | "exclude": [ 15 | "node_modules" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /examples/node/decentralized-ganache/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDir": "./src", 4 | "module": "commonjs", 5 | "target": "es2018", 6 | "noImplicitAny": false, 7 | "sourceMap": true, 8 | "outDir": "./dist", 9 | "noEmitOnError": true, 10 | "declaration": true, 11 | "esModuleInterop": true, 12 | "lib": ["es2018","esnext.asynciterable"] 13 | }, 14 | "exclude": [ 15 | "node_modules" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /src/test/integration/consensus.spec.ts: -------------------------------------------------------------------------------- 1 | import {testSuite} from './consensus/testSuite'; 2 | 3 | describe('consensus tests (3 nodes, TCP, CFT)', () => testSuite({}, 3)); 4 | 5 | describe('consensus tests (4 nodes, TCP, CFT)', () => testSuite({}, 4)); 6 | 7 | describe('consensus tests (5 nodes, TCP, CFT)', () => testSuite({}, 5)); 8 | 9 | describe('consensus tests (5 nodes, TCP, BFT)', () => testSuite({}, 5, 'TCP', 'BFT')); 10 | 11 | describe('consensus tests (7 nodes, TCP, BFT)', () => testSuite({}, 7, 'TCP', 'BFT')); 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true, 4 | "rootDir": "./src", 5 | "module": "commonjs", 6 | "target": "es2018", 7 | "noImplicitAny": false, 8 | "sourceMap": true, 9 | "outDir": "./dist", 10 | "noEmitOnError": true, 11 | "declaration": true, 12 | "skipLibCheck": true, 13 | "esModuleInterop": true, 14 | "lib": ["es2018","esnext.asynciterable"] 15 | }, 16 | "exclude": [ 17 | "node_modules", 18 | "dist", 19 | "examples" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /examples/node/decentralized-ganache/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "decentralized-ganache", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "ts-node src/server.ts" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "axon": "^2.0.3", 13 | "bunyan": "^1.8.12", 14 | "detect-port": "^1.3.0", 15 | "ganache-core": "^2.5.5", 16 | "mokka": "file:../../../", 17 | "semaphore": "^1.1.0", 18 | "web3": "^1.0.0-beta.55" 19 | }, 20 | "devDependencies": { 21 | "tweetnacl": "^1.0.1" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/consensus/interfaces/ISettingsInterface.ts: -------------------------------------------------------------------------------- 1 | import {PacketModel} from '../models/PacketModel'; 2 | 3 | export interface ISettingsInterface { 4 | privateKey: string; 5 | address: string; 6 | heartbeat: number; 7 | electionTimeout: number; 8 | proofExpiration: number; 9 | crashModel?: 'CFT' | 'BFT'; 10 | customVoteRule?: (packet: PacketModel) => Promise; 11 | reqMiddleware?: (packet: PacketModel) => Promise; 12 | resMiddleware?: (packet: PacketModel, peerPublicKey: string) => Promise; 13 | logger: { 14 | error: () => void, 15 | info: () => void, 16 | trace: () => void 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /src/consensus/models/PacketModel.ts: -------------------------------------------------------------------------------- 1 | class PacketModel { 2 | 3 | public state: number; 4 | public term: number; 5 | public publicKey: string; 6 | public type: number; 7 | public data: any; 8 | public proof: string; 9 | public timestamp: number; 10 | 11 | constructor( 12 | type: number, 13 | state: number, 14 | term: number, 15 | publicKey: string, 16 | proof: string, 17 | data: any = null 18 | ) { 19 | this.state = state; 20 | this.type = type; 21 | this.term = term; 22 | this.publicKey = publicKey; 23 | this.data = data; 24 | this.proof = proof; 25 | this.timestamp = Date.now(); 26 | } 27 | 28 | } 29 | 30 | export { PacketModel }; 31 | -------------------------------------------------------------------------------- /src/consensus/models/VoteModel.ts: -------------------------------------------------------------------------------- 1 | class VoteModel { 2 | 3 | public readonly nonce: number; 4 | public readonly term: number; 5 | public readonly publicKeysRootForTerm: string; 6 | public readonly publicKeyToCombinationMap: Map; 7 | public readonly repliesPublicKeyToSignatureMap: Map; 8 | 9 | constructor( 10 | nonce: number, 11 | term: number, 12 | publicKeysRootForTerm: string 13 | ) { 14 | this.nonce = nonce; 15 | this.publicKeysRootForTerm = publicKeysRootForTerm; 16 | this.publicKeyToCombinationMap = new Map(); 17 | this.repliesPublicKeyToSignatureMap = new Map(); 18 | } 19 | 20 | } 21 | 22 | export {VoteModel}; 23 | -------------------------------------------------------------------------------- /examples/node/cluster/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mokka_cluster_example", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "gen_keys": "ts-node src/get_keys.ts", 8 | "run_1": "set INDEX=0 && ts-node src/cluster.ts", 9 | "run_2": "set INDEX=1 && ts-node src/cluster.ts", 10 | "run_3": "set INDEX=2 && ts-node src/cluster.ts" 11 | }, 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "axon": "^2.0.3", 16 | "bunyan": "^1.8.12", 17 | "mokka": "file:../../../", 18 | "readline": "^1.3.0" 19 | }, 20 | "devDependencies": { 21 | "ts-node": "^8.3.0", 22 | "tslint": "^5.18.0", 23 | "typescript": "^3.5.3" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; 3 | const NodePolyfillPlugin = require("node-polyfill-webpack-plugin") 4 | 5 | module.exports = { 6 | entry: './src/consensus/main.ts', 7 | module: { 8 | rules: [ 9 | { 10 | test: /\.tsx?$/, 11 | use: 'ts-loader', 12 | exclude: /node_modules/ 13 | } 14 | ] 15 | }, 16 | plugins: [ 17 | new NodePolyfillPlugin(), 18 | new BundleAnalyzerPlugin({ 19 | analyzerMode: 'static', 20 | reportFilename: path.join(__dirname, 'webpack_report.html') 21 | }) 22 | ], 23 | resolve: { 24 | extensions: ['.ts', '.js'] 25 | }, 26 | output: { 27 | filename: 'bundle.js', 28 | path: path.resolve(__dirname, 'dist', 'web'), 29 | library: 'Mokka', 30 | libraryTarget: 'umd' 31 | } 32 | }; -------------------------------------------------------------------------------- /examples/node/decentralized-ganache/src/config.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | nodes: [ 3 | { 4 | balance: '10'.padEnd(20, '0'), 5 | ganache: 8545, 6 | port: 3000, 7 | publicKey: '0263920784223e0f5aa31a4bcdae945304c1c85df68064e9106ebfff1511221ee9', 8 | secretKey: '507b1433f58edd4eff3e87d9ba939c74bd15b4b10c00c548214817c0295c521a' 9 | }, 10 | { 11 | balance: '10'.padEnd(20, '0'), 12 | ganache: 8546, 13 | port: 3001, 14 | publicKey: '024ad0ef01d3d62b41b80a354a5b748524849c28f5989f414c4c174647137c2587', 15 | secretKey: '3cb2530ded6b8053fabf339c9e10e46ceb9ffc2064d535f53df59b8bf36289a1' 16 | }, 17 | { 18 | balance: '10'.padEnd(20, '0'), 19 | ganache: 8547, 20 | port: 3002, 21 | publicKey: '02683e65682deeb98738b44f8eb9d1840852e5635114c7c4ef2e39f20806b96dbf', 22 | secretKey: '1c954bd0ecc1b2c713b88678e48ff011e53d53fc496458063116a2e3a81883b8' 23 | } 24 | ] 25 | }; 26 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "parser": "@typescript-eslint/parser", 4 | "rules": { 5 | "quotes": [ 6 | "error", "single" 7 | ], 8 | "quotemark": [true, "single"], 9 | "no-console": true, 10 | "no-empty": [true, "allow-empty-catch"], 11 | "no-shadowed-variable": false, 12 | "trailing-comma": [true, { 13 | "singleline": "never", 14 | "multiline": { 15 | "objects": "never", 16 | "arrays": "never", 17 | "functions": "never", 18 | "typeLiterals": "ignore" 19 | } 20 | }], 21 | "variable-name": [true, "ban-keywords", "check-format", "allow-leading-underscore"], 22 | "no-default-export": false, 23 | "curly": ["error", "multi"], 24 | "dot-location": [2, "property"], 25 | "eqeqeq": [2, "always", {"null": "ignore"}], 26 | "handle-callback-err": [2, "^(err|error)$"], 27 | "indent": [2, 2, {"SwitchCase": 1}], 28 | "space-before-function-paren": ["error", "always"] 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /examples/node/cluster/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "parser": "@typescript-eslint/parser", 4 | "rules": { 5 | "quotes": [ 6 | "error", "single" 7 | ], 8 | "quotemark": [true, "single"], 9 | "no-console": true, 10 | "no-empty": [true, "allow-empty-catch"], 11 | "no-shadowed-variable": false, 12 | "trailing-comma": [true, { 13 | "singleline": "never", 14 | "multiline": { 15 | "objects": "never", 16 | "arrays": "never", 17 | "functions": "never", 18 | "typeLiterals": "ignore" 19 | } 20 | }], 21 | "variable-name": [true, "ban-keywords", "check-format", "allow-leading-underscore"], 22 | "no-default-export": false, 23 | "curly": ["error", "multi"], 24 | "dot-location": [2, "property"], 25 | "eqeqeq": [2, "always", {"null": "ignore"}], 26 | "handle-callback-err": [2, "^(err|error)$"], 27 | "indent": [2, 2, {"SwitchCase": 1}], 28 | "space-before-function-paren": ["error", "always"] 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /examples/node/decentralized-ganache/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "parser": "@typescript-eslint/parser", 4 | "rules": { 5 | "quotes": [ 6 | "error", "single" 7 | ], 8 | "quotemark": [true, "single"], 9 | "no-console": true, 10 | "no-empty": [true, "allow-empty-catch"], 11 | "no-shadowed-variable": false, 12 | "trailing-comma": [true, { 13 | "singleline": "never", 14 | "multiline": { 15 | "objects": "never", 16 | "arrays": "never", 17 | "functions": "never", 18 | "typeLiterals": "ignore" 19 | } 20 | }], 21 | "variable-name": [true, "ban-keywords", "check-format", "allow-leading-underscore"], 22 | "no-default-export": false, 23 | "curly": ["error", "multi"], 24 | "dot-location": [2, "property"], 25 | "eqeqeq": [2, "always", {"null": "ignore"}], 26 | "handle-callback-err": [2, "^(err|error)$"], 27 | "indent": [2, 2, {"SwitchCase": 1}], 28 | "space-before-function-paren": ["error", "always"] 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/consensus/api/MessageApi.ts: -------------------------------------------------------------------------------- 1 | import states from '../constants/NodeStates'; 2 | import {Mokka} from '../main'; 3 | import {PacketModel} from '../models/PacketModel'; 4 | 5 | class MessageApi { 6 | 7 | private mokka: Mokka; 8 | 9 | constructor(mokka: Mokka) { 10 | this.mokka = mokka; 11 | } 12 | 13 | public async message(packet: PacketModel, peerPublicKey: string) { 14 | packet = await this.mokka.resMiddleware(packet, peerPublicKey); 15 | const node = this.mokka.nodes.get(peerPublicKey); 16 | await node.write(node.address, Buffer.from(JSON.stringify(packet))); 17 | } 18 | 19 | public packet(type: number, data: any = null): PacketModel { 20 | return new PacketModel( 21 | type, 22 | this.mokka.state, 23 | this.mokka.term, 24 | this.mokka.publicKey, 25 | this.mokka.state === states.LEADER ? this.mokka.proof : null, 26 | data); 27 | } 28 | 29 | public decodePacket(message: Buffer): PacketModel { 30 | return JSON.parse(message.toString()); 31 | } 32 | 33 | } 34 | 35 | export { MessageApi }; 36 | -------------------------------------------------------------------------------- /src/implementation/RPC.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import bodyParser from 'body-parser'; 3 | import express from 'express'; 4 | import {URL} from 'url'; 5 | import {Mokka} from '../consensus/main'; 6 | 7 | class RPCMokka extends Mokka { 8 | 9 | private app = express(); 10 | 11 | public initialize() { 12 | 13 | this.app.use(bodyParser.json()); 14 | 15 | this.app.post('/', (req, res) => { 16 | const packet = Buffer.from(req.body.data, 'hex'); 17 | this.emitPacket(packet); 18 | res.send({ok: 1}); 19 | }); 20 | 21 | const url = new URL(this.address); 22 | 23 | this.app.listen(url.port, () => { 24 | this.logger.info(`rpc started on port ${url.port}`); 25 | }); 26 | } 27 | 28 | /** 29 | * The message to write. 30 | * 31 | * @param {string} address The peer address 32 | * @param {Object} packet The packet to write to the connection. 33 | * @api private 34 | */ 35 | public async write(address: string, packet: Buffer): Promise { 36 | 37 | await axios.post(address, { 38 | data: packet.toString('hex') 39 | }, { 40 | timeout: this.heartbeat 41 | }).catch((e) => { 42 | this.logger.trace(`received error from ${address}: ${e}`); 43 | }); 44 | } 45 | 46 | public async disconnect(): Promise { 47 | await super.disconnect(); 48 | this.app.close(); 49 | } 50 | 51 | public async connect(): Promise { 52 | this.initialize(); 53 | await super.connect(); 54 | } 55 | 56 | } 57 | 58 | export default RPCMokka; 59 | -------------------------------------------------------------------------------- /src/implementation/TCP.ts: -------------------------------------------------------------------------------- 1 | import msg from 'axon'; 2 | 3 | import {Mokka} from '../consensus/main'; 4 | 5 | class TCPMokka extends Mokka { 6 | 7 | private sockets: Map = new Map(); 8 | 9 | public initialize() { 10 | this.logger.info(`initializing reply socket on port ${this.address}`); 11 | 12 | this.sockets.set(this.address, msg.socket('sub-emitter')); 13 | 14 | this.sockets.get(this.address).bind(this.address); 15 | this.sockets.get(this.address).on('message', (data: Buffer) => { 16 | this.emitPacket(data); 17 | }); 18 | 19 | this.sockets.get(this.address).on('error', () => { 20 | this.logger.error(`failed to initialize on port: ${this.address}`); 21 | }); 22 | } 23 | 24 | /** 25 | * The message to write. 26 | * 27 | * @param {string} address The peer address 28 | * @param {Object} packet The packet to write to the connection. 29 | * @api private 30 | */ 31 | public async write(address: string, packet: Buffer): Promise { 32 | 33 | if (!this.sockets.has(address)) { 34 | this.sockets.set(address, msg.socket('pub-emitter')); 35 | 36 | this.sockets.get(address).connect(address); 37 | } 38 | 39 | this.sockets.get(address).emit('message', packet); 40 | } 41 | 42 | public async disconnect(): Promise { 43 | await super.disconnect(); 44 | for (const socket of this.sockets.values()) { 45 | socket.close(); 46 | } 47 | } 48 | 49 | public async connect(): Promise { 50 | this.initialize(); 51 | await super.connect(); 52 | } 53 | 54 | } 55 | 56 | export default TCPMokka; 57 | -------------------------------------------------------------------------------- /src/implementation/ZMQ.ts: -------------------------------------------------------------------------------- 1 | import * as zmq from 'zeromq'; 2 | 3 | import {Mokka} from '../consensus/main'; 4 | 5 | class ZMQMokka extends Mokka { 6 | 7 | private sockets: Map = new Map(); 8 | 9 | public async initialize() { 10 | this.logger.info(`initializing reply socket on port ${this.address}`); 11 | 12 | const socket = new zmq.Pair({receiveHighWaterMark: 10}); 13 | await socket.bind(this.address); 14 | 15 | this.sockets.set(this.address, socket); 16 | 17 | for await (const [msg] of this.sockets.get(this.address)) { 18 | await this.emitPacket(msg); 19 | } 20 | 21 | } 22 | 23 | /** 24 | * The message to write. 25 | * 26 | * @param {string} address The peer address 27 | * @param {Object} packet The packet to write to the connection. 28 | * @api private 29 | */ 30 | public async write(address: string, packet: Buffer): Promise { 31 | 32 | if (!this.sockets.has(address)) { 33 | const socket = new zmq.Pair({receiveHighWaterMark: 10}); 34 | socket.connect(address); 35 | this.sockets.set(address, socket); 36 | } 37 | 38 | try { 39 | await this.sockets.get(address).send(packet); 40 | } catch (e) { 41 | this.sockets.get(address).close(); 42 | const socket = new zmq.Pair({receiveHighWaterMark: 10}); 43 | socket.connect(address); 44 | this.sockets.set(address, socket); 45 | } 46 | } 47 | 48 | public async disconnect(): Promise { 49 | await super.disconnect(); 50 | for (const socket of this.sockets.values()) { 51 | socket.close(); 52 | } 53 | } 54 | 55 | public async connect(): Promise { 56 | this.initialize(); 57 | await super.connect(); 58 | } 59 | 60 | } 61 | 62 | export default ZMQMokka; 63 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mokka", 3 | "version": "1.4.2", 4 | "description": "Mokka Consensus Algorithm implementation in Javascript", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/ega-forever/mokka.git" 8 | }, 9 | "scripts": { 10 | "tsc:watch": "tsc -w", 11 | "tsc:build": "tsc", 12 | "test": "mocha --timeout 180000 -r ts-node/register src/test/**/*.spec.ts src/test/**/**/*.spec.ts", 13 | "lint": "tslint --fix --project tsconfig.json", 14 | "build_web": "webpack", 15 | "build_dist": "rm -rf dist && npm run tsc:build && npm run build_web" 16 | }, 17 | "keywords": [ 18 | "mokka", 19 | "consensus", 20 | "rsm", 21 | "raft", 22 | "distributed" 23 | ], 24 | "main": "dist/consensus/main.js", 25 | "types": "dist/consensus/main.d.ts", 26 | "author": "zyev.egor@gmail.com", 27 | "license": "AGPLv3", 28 | "dependencies": { 29 | "bn.js": "^5.2.0", 30 | "elliptic": "^6.5.4" 31 | }, 32 | "devDependencies": { 33 | "@types/bluebird": "^3.5.36", 34 | "@types/bunyan": "^1.8.8", 35 | "@types/chai": "^4.3.0", 36 | "@types/mocha": "^9.1.0", 37 | "@types/node": "^16.4.0", 38 | "axios": "^0.25.0", 39 | "axon": "2.0.x", 40 | "bluebird": "^3.7.2", 41 | "body-parser": "^1.19.1", 42 | "bunyan": "^1.8.15", 43 | "chai": "^4.3.6", 44 | "express": "^4.17.2", 45 | "leveldown": "^6.1.0", 46 | "lodash": "^4.17.21", 47 | "mocha": "^9.2.0", 48 | "node-polyfill-webpack-plugin": "^1.1.4", 49 | "ts-loader": "^9.2.6", 50 | "ts-node": "^10.5.0", 51 | "tslint": "^6.1.3", 52 | "typescript": "^4.5.5", 53 | "webpack": "^5.68.0", 54 | "webpack-bundle-analyzer": "^4.5.0", 55 | "webpack-cli": "^4.9.2", 56 | "zeromq": "^6.0.0-beta.6" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/consensus/services/RequestProcessorService.ts: -------------------------------------------------------------------------------- 1 | import { MessageApi } from '../api/MessageApi'; 2 | import { NodeApi } from '../api/NodeApi'; 3 | import { VoteApi } from '../api/VoteApi'; 4 | import messageTypes from '../constants/MessageTypes'; 5 | import { Mokka } from '../main'; 6 | import { PacketModel } from '../models/PacketModel'; 7 | 8 | class RequestProcessorService { 9 | 10 | private voteApi: VoteApi; 11 | private mokka: Mokka; 12 | private messageApi: MessageApi; 13 | private nodeApi: NodeApi; 14 | 15 | private readonly actionMap: Map Promise)[]>; 16 | 17 | constructor(mokka: Mokka) { 18 | this.voteApi = new VoteApi(mokka); 19 | this.messageApi = new MessageApi(mokka); 20 | this.nodeApi = new NodeApi(mokka); 21 | this.mokka = mokka; 22 | this.actionMap = new Map Promise)[]>(); 23 | 24 | this.actionMap.set(messageTypes.VOTE, [ 25 | this.voteApi.vote.bind(this.voteApi) 26 | ]); 27 | 28 | this.actionMap.set(messageTypes.VOTED, [ 29 | this.voteApi.voted.bind(this.voteApi) 30 | ]); 31 | 32 | this.actionMap.set(messageTypes.ACK, [ 33 | this.voteApi.validateAndApplyLeader.bind(this.voteApi), 34 | this.nodeApi.pingFromLeader.bind(this.nodeApi) 35 | ]); 36 | } 37 | 38 | public async process(packet: PacketModel) { 39 | 40 | const node = this.mokka.nodes.get(packet.publicKey); 41 | 42 | if (!node || !this.actionMap.has(packet.type)) 43 | return; 44 | 45 | let reply: PacketModel | null | false = false; 46 | 47 | for (const action of this.actionMap.get(packet.type)) { 48 | if (reply === null) { 49 | continue; 50 | } 51 | reply = await action(reply === false ? packet : reply); 52 | } 53 | 54 | if (reply) 55 | await this.messageApi.message(reply, packet.publicKey); 56 | } 57 | 58 | } 59 | 60 | export { RequestProcessorService }; 61 | -------------------------------------------------------------------------------- /src/test/integration/workers/MokkaTCPWorker.ts: -------------------------------------------------------------------------------- 1 | import * as bunyan from 'bunyan'; 2 | import * as _ from 'lodash'; 3 | import eventTypes from '../../../consensus/constants/EventTypes'; 4 | import states from '../../../consensus/constants/NodeStates'; 5 | import {PacketModel} from '../../../consensus/models/PacketModel'; 6 | import TCPMokka from '../../../implementation/TCP'; 7 | 8 | let mokka: TCPMokka = null; 9 | 10 | const init = (params: any) => { 11 | 12 | const logger = bunyan.createLogger({name: `mokka.logger[${params.index}]`, level: 10}); 13 | 14 | logger.trace(`params ${JSON.stringify(params)}`); 15 | 16 | mokka = new TCPMokka({ 17 | address: `tcp://127.0.0.1:${2000 + params.index}/${params.publicKey || params.keys[params.index].publicKey}`, 18 | crashModel: params.settings.crashModel, 19 | electionTimeout: params.settings.electionTimeout, 20 | heartbeat: params.settings.heartbeat, 21 | logger, 22 | privateKey: params.keys[params.index].privateKey, 23 | proofExpiration: params.settings.proofExpiration, 24 | reqMiddleware: async (packet: PacketModel) => { 25 | return packet; 26 | } 27 | }); 28 | 29 | for (let i = 0; i < params.keys.length; i++) 30 | if (i !== params.index) 31 | mokka.nodeApi.join(`tcp://127.0.0.1:${2000 + i}/${params.keys[i].publicKey}`); 32 | 33 | mokka.on(eventTypes.STATE, () => { 34 | logger.info(`index #${params.index} state ${_.invert(states)[mokka.state]} with term ${mokka.term}`); 35 | process.send({type: 'state', args: [mokka.state, mokka.leaderPublicKey, mokka.term, params.index]}); 36 | }); 37 | 38 | mokka.on(eventTypes.HEARTBEAT_TIMEOUT, () => { 39 | logger.info(`index #${params.index} timeout with term ${mokka.term}`); 40 | }); 41 | 42 | }; 43 | 44 | const connect = () => { 45 | mokka.connect(); 46 | }; 47 | 48 | process.on('message', (m: any) => { 49 | if (m.type === 'init') 50 | init(m.args[0]); 51 | 52 | if (m.type === 'connect') 53 | connect(); 54 | }); 55 | -------------------------------------------------------------------------------- /src/test/integration/workers/MokkaRPCWorker.ts: -------------------------------------------------------------------------------- 1 | import * as bunyan from 'bunyan'; 2 | import * as _ from 'lodash'; 3 | import eventTypes from '../../../consensus/constants/EventTypes'; 4 | import states from '../../../consensus/constants/NodeStates'; 5 | import {PacketModel} from '../../../consensus/models/PacketModel'; 6 | import RPCMokka from '../../../implementation/RPC'; 7 | 8 | let mokka: RPCMokka = null; 9 | 10 | const init = (params: any) => { 11 | 12 | const logger = bunyan.createLogger({name: `mokka.logger[${params.index}]`, level: 10}); 13 | 14 | logger.trace(`params ${JSON.stringify(params)}`); 15 | 16 | mokka = new RPCMokka({ 17 | address: `http://127.0.0.1:${2000 + params.index}/${params.publicKey || params.keys[params.index].publicKey}`, 18 | crashModel: params.settings.crashModel, 19 | electionTimeout: params.settings.electionTimeout, 20 | heartbeat: params.settings.heartbeat, 21 | logger, 22 | privateKey: params.keys[params.index].privateKey, 23 | proofExpiration: params.settings.proofExpiration, 24 | reqMiddleware: async (packet: PacketModel) => { 25 | return packet; 26 | } 27 | }); 28 | 29 | for (let i = 0; i < params.keys.length; i++) 30 | if (i !== params.index) 31 | mokka.nodeApi.join(`http://127.0.0.1:${2000 + i}/${params.keys[i].publicKey}`); 32 | 33 | mokka.on(eventTypes.STATE, () => { 34 | logger.info(`index #${params.index} state ${_.invert(states)[mokka.state]} with term ${mokka.term}`); 35 | process.send({type: 'state', args: [mokka.state, mokka.leaderPublicKey, mokka.term, params.index]}); 36 | }); 37 | 38 | mokka.on(eventTypes.HEARTBEAT_TIMEOUT, () => { 39 | logger.info(`index #${params.index} timeout with term ${mokka.term}`); 40 | }); 41 | 42 | }; 43 | 44 | const connect = () => { 45 | mokka.connect(); 46 | }; 47 | 48 | process.on('message', (m: any) => { 49 | if (m.type === 'init') 50 | init(m.args[0]); 51 | 52 | if (m.type === 'connect') 53 | connect(); 54 | }); 55 | -------------------------------------------------------------------------------- /src/test/unit/components/VoteApi.spec.ts: -------------------------------------------------------------------------------- 1 | import Promise from 'bluebird'; 2 | import bunyan from 'bunyan'; 3 | import {expect} from 'chai'; 4 | import crypto from 'crypto'; 5 | import {MessageApi} from '../../../consensus/api/MessageApi'; 6 | import {VoteApi} from '../../../consensus/api/VoteApi'; 7 | import MessageTypes from '../../../consensus/constants/MessageTypes'; 8 | import NodeStates from '../../../consensus/constants/NodeStates'; 9 | import {Mokka} from '../../../consensus/main'; 10 | import TCPMokka from '../../../implementation/TCP'; 11 | 12 | describe('VoteApi tests', (ctx: any = {}) => { 13 | 14 | beforeEach(async () => { 15 | 16 | ctx.keys = []; 17 | 18 | ctx.nodes = []; 19 | 20 | for (let i = 0; i < 3; i++) { 21 | const node = crypto.createECDH('secp256k1'); 22 | node.generateKeys(); 23 | ctx.keys.push({ 24 | privateKey: node.getPrivateKey().toString('hex'), 25 | publicKey: node.getPublicKey('hex', 'compressed') 26 | }); 27 | } 28 | 29 | for (let index = 0; index < 3; index++) { 30 | const instance = new TCPMokka({ 31 | address: `tcp://127.0.0.1:2000/${ctx.keys[index].publicKey}`, 32 | electionTimeout: 300, 33 | heartbeat: 50, 34 | logger: bunyan.createLogger({name: 'mokka.logger', level: 60}), 35 | privateKey: ctx.keys[index].privateKey, 36 | proofExpiration: 5000 37 | }); 38 | 39 | for (let i = 0; i < 3; i++) 40 | if (i !== index) 41 | instance.nodeApi.join(`tcp://127.0.0.1:${2000 + i}/${ctx.keys[i].publicKey}`); 42 | 43 | ctx.nodes.push(instance); 44 | } 45 | 46 | }); 47 | 48 | it('should check vote', async () => { 49 | 50 | const candidateNode = ctx.nodes[0] as Mokka; 51 | const followerNode = ctx.nodes[1] as Mokka; 52 | 53 | candidateNode.setState(NodeStates.CANDIDATE, 2, ''); 54 | 55 | const packet = await candidateNode.messageApi.packet(MessageTypes.VOTE, { 56 | nonce: Date.now() 57 | }); 58 | 59 | const start = Date.now(); 60 | // @ts-ignore 61 | const result = await followerNode.requestProcessorService.voteApi.vote(packet); 62 | expect(Date.now() - start).to.be.lt(followerNode.heartbeatCtrl.safeHeartbeat() + 30); 63 | // tslint:disable-next-line:no-unused-expression 64 | expect(result.data.signature).to.not.be.undefined; 65 | 66 | }); 67 | 68 | afterEach(async () => { 69 | await Promise.delay(1000); 70 | }); 71 | 72 | }); 73 | -------------------------------------------------------------------------------- /src/consensus/controllers/HeartbeatController.ts: -------------------------------------------------------------------------------- 1 | import { MessageApi } from '../api/MessageApi'; 2 | import { NodeApi } from '../api/NodeApi'; 3 | import eventTypes from '../constants/EventTypes'; 4 | import messageTypes from '../constants/MessageTypes'; 5 | import states from '../constants/NodeStates'; 6 | import NodeStates from '../constants/NodeStates'; 7 | import { Mokka } from '../main'; 8 | 9 | class HeartbeatController { 10 | 11 | private mokka: Mokka; 12 | private adjustmentDate: number; 13 | private messageApi: MessageApi; 14 | private nodeApi: NodeApi; 15 | private runBeat: boolean; 16 | 17 | constructor(mokka: Mokka) { 18 | this.mokka = mokka; 19 | this.messageApi = new MessageApi(mokka); 20 | this.nodeApi = new NodeApi(mokka); 21 | this.runBeat = false; 22 | } 23 | 24 | public async stopBeat() { 25 | this.runBeat = false; 26 | if (this.adjustmentDate) 27 | await new Promise((res) => setTimeout(res, Date.now() - this.adjustmentDate, null)); 28 | } 29 | 30 | public async watchBeat() { 31 | 32 | this.runBeat = true; 33 | 34 | while (this.runBeat) { 35 | if (this.adjustmentDate > Date.now()) { 36 | await new Promise((res) => setTimeout(res, this.adjustmentDate - Date.now(), null)); 37 | continue; 38 | } 39 | 40 | if ( 41 | this.mokka.state === states.FOLLOWER || 42 | // tslint:disable-next-line:max-line-length 43 | (this.mokka.state === states.LEADER && this.mokka.getProofMintedTime() + this.mokka.proofExpiration < Date.now()) 44 | ) { 45 | this.mokka.emit(eventTypes.HEARTBEAT_TIMEOUT); 46 | this.mokka.setState(states.FOLLOWER, this.mokka.term, null, null); 47 | await this.nodeApi.promote(); 48 | if (this.mokka.state === NodeStates.FOLLOWER) { 49 | this.setNextBeat(Math.round(this.mokka.electionTimeout * (1 + 2 * Math.random()))); 50 | continue; 51 | } 52 | } 53 | 54 | this.mokka.logger.trace(`sending ack signal to peers`); 55 | await Promise.all( 56 | [...this.mokka.nodes.values()].map((node) => { 57 | const packet = this.messageApi.packet(messageTypes.ACK); 58 | return this.messageApi.message(packet, node.publicKey); 59 | })); 60 | 61 | this.mokka.logger.trace(`sent ack signal to peers`); 62 | this.adjustmentDate = Date.now() + this.mokka.heartbeat; 63 | } 64 | 65 | } 66 | 67 | public setNextBeat(duration: number) { 68 | this.mokka.logger.trace(`set next beat in ${ duration }`); 69 | this.adjustmentDate = Date.now() + duration; 70 | } 71 | 72 | public timeout() { 73 | return this.safeHeartbeat() + Math.round(this.mokka.heartbeat * Math.random()); 74 | } 75 | 76 | public safeHeartbeat() { 77 | return this.mokka.heartbeat * 1.5; 78 | } 79 | 80 | } 81 | 82 | export { HeartbeatController }; 83 | -------------------------------------------------------------------------------- /src/consensus/main.ts: -------------------------------------------------------------------------------- 1 | import {MessageApi} from './api/MessageApi'; 2 | import {NodeApi} from './api/NodeApi'; 3 | import {HeartbeatController} from './controllers/HeartbeatController'; 4 | import {ILoggerInterface} from './interfaces/ILoggerInterface'; 5 | import {ISettingsInterface} from './interfaces/ISettingsInterface'; 6 | import {NodeModel} from './models/NodeModel'; 7 | import {PacketModel} from './models/PacketModel'; 8 | import {VoteModel} from './models/VoteModel'; 9 | import {RequestProcessorService} from './services/RequestProcessorService'; 10 | 11 | class Mokka extends NodeModel { 12 | 13 | public readonly nodeApi: NodeApi; 14 | public readonly messageApi: MessageApi; 15 | public readonly heartbeatCtrl: HeartbeatController; 16 | public readonly reqMiddleware: (packet: PacketModel) => Promise; 17 | public readonly resMiddleware: (packet: PacketModel, peerPublicKey: string) => Promise; 18 | public readonly customVoteRule: (packet: PacketModel) => Promise; 19 | public vote: VoteModel; 20 | public readonly logger: ILoggerInterface; 21 | private readonly requestProcessorService: RequestProcessorService; 22 | 23 | constructor(options: ISettingsInterface) { 24 | super(options.privateKey, options.address, options.crashModel); 25 | 26 | this.heartbeat = options.heartbeat; 27 | this.electionTimeout = options.electionTimeout; 28 | this.proofExpiration = options.proofExpiration; 29 | this.logger = options.logger || { 30 | // tslint:disable-next-line 31 | error: console.log, 32 | // tslint:disable-next-line 33 | info: console.log, 34 | // tslint:disable-next-line 35 | trace: console.log 36 | }; 37 | 38 | this.reqMiddleware = options.reqMiddleware ? options.reqMiddleware : 39 | async (packet: PacketModel) => packet; 40 | 41 | this.resMiddleware = options.resMiddleware ? options.resMiddleware : 42 | async (packet: PacketModel) => packet; 43 | 44 | this.customVoteRule = options.customVoteRule ? options.customVoteRule : 45 | async (packet: PacketModel) => true; 46 | 47 | this.heartbeatCtrl = new HeartbeatController(this); 48 | this.requestProcessorService = new RequestProcessorService(this); 49 | this.nodeApi = new NodeApi(this); 50 | this.messageApi = new MessageApi(this); 51 | } 52 | 53 | public quorum(responses: number) { 54 | if (!this.nodes.size || !responses) return false; 55 | 56 | return responses >= this.majority(); 57 | } 58 | 59 | public setVote(vote: VoteModel): void { 60 | this.vote = vote; 61 | } 62 | 63 | public connect(): void { 64 | this.heartbeatCtrl.setNextBeat(this.heartbeatCtrl.timeout()); 65 | this.heartbeatCtrl.watchBeat(); 66 | } 67 | 68 | public async disconnect(): Promise { 69 | await this.heartbeatCtrl.stopBeat(); 70 | } 71 | 72 | public async emitPacket(packet: Buffer) { 73 | let parsedPacket = this.messageApi.decodePacket(packet); 74 | parsedPacket = await this.reqMiddleware(parsedPacket); 75 | await this.requestProcessorService.process(parsedPacket); 76 | } 77 | 78 | } 79 | 80 | export {Mokka}; 81 | -------------------------------------------------------------------------------- /src/test/unit/components/NodeApi.spec.ts: -------------------------------------------------------------------------------- 1 | import Promise from 'bluebird'; 2 | import bunyan from 'bunyan'; 3 | import {expect} from 'chai'; 4 | import crypto from 'crypto'; 5 | import NodeStates from '../../../consensus/constants/NodeStates'; 6 | import {Mokka} from '../../../consensus/main'; 7 | import TCPMokka from '../../../implementation/TCP'; 8 | 9 | describe('NodeApi tests', (ctx: any = {}) => { 10 | 11 | beforeEach(async () => { 12 | 13 | ctx.keys = []; 14 | 15 | ctx.nodes = []; 16 | 17 | for (let i = 0; i < 3; i++) { 18 | const node = crypto.createECDH('secp256k1'); 19 | node.generateKeys(); 20 | ctx.keys.push({ 21 | privateKey: node.getPrivateKey().toString('hex'), 22 | publicKey: node.getPublicKey().toString('hex') 23 | }); 24 | } 25 | 26 | for (let index = 0; index < 3; index++) { 27 | const instance = new TCPMokka({ 28 | address: `tcp://127.0.0.1:2000/${ctx.keys[index].publicKey}`, 29 | electionTimeout: 300, 30 | heartbeat: 50, 31 | logger: bunyan.createLogger({name: 'mokka.logger', level: 60}), 32 | privateKey: ctx.keys[index].privateKey, 33 | proofExpiration: 5000 34 | }); 35 | 36 | for (let i = 0; i < 3; i++) 37 | if (i !== index) 38 | instance.nodeApi.join(`tcp://127.0.0.1:${2000 + i}/${ctx.keys[i].publicKey}`); 39 | 40 | ctx.nodes.push(instance); 41 | } 42 | 43 | }); 44 | 45 | it('should pass promote, once received votes', async () => { 46 | 47 | const candidateNode = ctx.nodes[0] as Mokka; 48 | candidateNode.setState(NodeStates.FOLLOWER, 2, ''); 49 | 50 | await Promise.all([ 51 | candidateNode.nodeApi.promote(), 52 | new Promise((res) => { 53 | setTimeout(() => { 54 | candidateNode.setState(NodeStates.LEADER, 2, candidateNode.publicKey); 55 | res(); 56 | }, 150); 57 | }) 58 | ]); 59 | 60 | expect(candidateNode.state).to.be.eq(NodeStates.LEADER); 61 | await candidateNode.disconnect(); 62 | }); 63 | 64 | it('should not pass promote, because no votes received', async () => { 65 | 66 | const candidateNode = ctx.nodes[0] as Mokka; 67 | candidateNode.setState(NodeStates.FOLLOWER, 2, ''); 68 | 69 | await candidateNode.nodeApi.promote(); 70 | 71 | expect(candidateNode.state).to.be.eq(NodeStates.FOLLOWER); 72 | await candidateNode.disconnect(); 73 | }); 74 | 75 | it('concurrent promoting (called concurrently several times)', async () => { 76 | 77 | const candidateNode = ctx.nodes[0] as Mokka; 78 | candidateNode.setState(NodeStates.FOLLOWER, 2, ''); 79 | 80 | const pr1 = candidateNode.nodeApi.promote(); 81 | const pr2 = candidateNode.nodeApi.promote(); 82 | const pr3 = candidateNode.nodeApi.promote(); 83 | const pr4 = candidateNode.nodeApi.promote(); 84 | 85 | expect(candidateNode.term).to.be.eq(3); 86 | await Promise.all([pr1, pr2, pr3, pr4]); 87 | 88 | expect(candidateNode.term).to.be.eq(3); 89 | await candidateNode.disconnect(); 90 | }); 91 | 92 | it('another candidate took leader role during promote', async () => { 93 | 94 | const candidateNode = ctx.nodes[0] as Mokka; 95 | const leaderNode = ctx.nodes[1] as Mokka; 96 | candidateNode.setState(NodeStates.FOLLOWER, 2, ''); 97 | 98 | const pr = candidateNode.nodeApi.promote(); 99 | 100 | await new Promise((res) => setTimeout(res, 50)); 101 | 102 | candidateNode.setState(NodeStates.FOLLOWER, 3, leaderNode.publicKey); 103 | 104 | await pr; 105 | 106 | expect(candidateNode.term).to.be.eq(3); 107 | await candidateNode.disconnect(); 108 | }); 109 | 110 | afterEach(async () => { 111 | await Promise.delay(1000); 112 | }); 113 | 114 | }); 115 | -------------------------------------------------------------------------------- /src/consensus/models/NodeModel.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events'; 2 | import eventTypes from '../constants/EventTypes'; 3 | import NodeStates from '../constants/NodeStates'; 4 | 5 | class NodeModel extends EventEmitter { 6 | 7 | get state(): number { 8 | return this._state; 9 | } 10 | 11 | get term(): number { 12 | return this._term; 13 | } 14 | 15 | get leaderPublicKey(): string { 16 | return this._leaderPublicKey; 17 | } 18 | 19 | get address(): string { 20 | return this.nodeAddress; 21 | } 22 | 23 | get proof(): string { 24 | return this._proof; 25 | } 26 | 27 | public heartbeat: number; 28 | public proofExpiration: number; 29 | public electionTimeout: number; 30 | public publicKeysRoot: string; 31 | public publicKeysCombinationsInQuorum: string[][]; 32 | public readonly privateKey: string; 33 | public readonly publicKey: string; 34 | public readonly nodes: Map = new Map(); 35 | public readonly lastLeadersPublicKeyInTermMap: Map = new Map(); 36 | 37 | private _state: number; 38 | private _term: number = 0; 39 | private lastTermUpdateTime: number = 0; 40 | private _proof: string; 41 | private _leaderPublicKey: string = ''; 42 | private _proofMintedTime: number = 0; 43 | private readonly nodeAddress: string; 44 | private readonly crashModel: 'CFT' | 'BFT'; 45 | 46 | constructor( 47 | privateKey: string, 48 | multiaddr: string, 49 | crashModel: 'CFT' | 'BFT' = 'CFT', 50 | state: number = NodeStates.FOLLOWER 51 | ) { 52 | super(); 53 | this.privateKey = privateKey; 54 | this.publicKey = multiaddr.match(/\w+$/).toString(); 55 | this._state = state; 56 | this.nodeAddress = multiaddr.split(/\w+$/)[0].replace(/\/$/, ''); 57 | this.crashModel = crashModel; 58 | } 59 | 60 | public majority() { 61 | const clusterSize = this.nodes.size + 1; // peer nodes + self 62 | return clusterSize - Math.ceil((clusterSize - 1) / (this.crashModel === 'CFT' ? 2 : 3)); 63 | } 64 | 65 | public write(address: string, packet: Buffer): void { 66 | throw new Error('should be implemented!'); 67 | } 68 | 69 | public setState( 70 | state: number, 71 | term: number = this._term, 72 | leaderPublicKey: string, 73 | proof: string = null, 74 | proofMintedTime: number = 0) { 75 | this._state = state; 76 | 77 | if (this._term !== term) { 78 | this.lastTermUpdateTime = Date.now(); 79 | } 80 | 81 | this._term = term; 82 | this._leaderPublicKey = leaderPublicKey; 83 | this._proof = proof; 84 | this._proofMintedTime = proofMintedTime; 85 | this.emit(eventTypes.STATE); 86 | 87 | if (this.leaderPublicKey) { 88 | if (this.lastLeadersPublicKeyInTermMap.size >= this.majority() - 1) { 89 | const prevTermsToRemove = [...this.lastLeadersPublicKeyInTermMap.keys()].sort() 90 | .slice(this.lastLeadersPublicKeyInTermMap.size - this.majority()); 91 | 92 | for (const prevTerm of prevTermsToRemove) { 93 | this.lastLeadersPublicKeyInTermMap.delete(prevTerm); 94 | } 95 | } 96 | 97 | this.lastLeadersPublicKeyInTermMap.set(term, leaderPublicKey); 98 | } 99 | } 100 | 101 | public getProofMintedTime(): number { 102 | return this._proofMintedTime; 103 | } 104 | 105 | public checkPublicKeyCanBeLeaderNextRound(publicKey: string) { 106 | const values = [...this.lastLeadersPublicKeyInTermMap.values()]; 107 | return !values.includes(publicKey); 108 | } 109 | 110 | public checkTermNumber(term: number) { 111 | if (!this.lastTermUpdateTime) { 112 | return true; 113 | } 114 | 115 | const maxPossibleTerm = this._term + Math.ceil((Date.now() - this.lastTermUpdateTime) / this.heartbeat * 1.5); 116 | return maxPossibleTerm >= term; 117 | } 118 | 119 | } 120 | 121 | export { NodeModel }; 122 | -------------------------------------------------------------------------------- /src/consensus/utils/cryptoUtils.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:variable-name 2 | import BN from 'bn.js'; 3 | import * as crypto from 'crypto'; 4 | import { ec as EC } from 'elliptic'; 5 | 6 | const ec = new EC('secp256k1'); 7 | 8 | export const buildPublicKeysRoot = ( 9 | publicKeys: string[] 10 | ) => { 11 | 12 | let X = null; 13 | for (const publicKey of publicKeys) { 14 | const XI = pubKeyToPoint(Buffer.from(publicKey, 'hex')); 15 | X = X === null ? XI : X.add(XI); 16 | } 17 | 18 | return pointToPublicKey(X).toString('hex'); 19 | }; 20 | 21 | export const buildPublicKeysRootForTerm = ( 22 | publicKeysRoot: string, 23 | term: number, 24 | nonce: number | string, 25 | candidatePublicKey: string 26 | ) => { 27 | 28 | const mHash = crypto.createHash('sha256') 29 | .update(`${ nonce }:${ term }:${ candidatePublicKey }`) 30 | .digest('hex'); 31 | 32 | const X = pubKeyToPoint(Buffer.from(publicKeysRoot, 'hex')).mul(new BN(mHash, 16)); 33 | return pointToPublicKey(X).toString('hex'); 34 | }; 35 | 36 | /* X = X1 * a1 + X2 * a2 + ..Xn * an */ 37 | export const buildSharedPublicKeyX = ( 38 | publicKeys: string[], 39 | term: number, 40 | nonce: number | string, 41 | publicKeysRootForTerm: string 42 | ) => { 43 | 44 | const mHash = crypto.createHash('sha256') 45 | .update(`${ nonce }:${ term }:${ publicKeysRootForTerm }`) 46 | .digest('hex'); 47 | 48 | let X = null; 49 | for (const publicKey of publicKeys) { 50 | const XI = pubKeyToPoint(Buffer.from(publicKey, 'hex')).mul(new BN(mHash, 16)); 51 | X = X === null ? XI : X.add(XI); 52 | } 53 | 54 | return pointToPublicKey(X).toString('hex'); 55 | }; 56 | 57 | /* let s1 = (R1 + k1 * a1 * e) mod n, where n - is a curve param 58 | * the "n" has been introduced to reduce the signature size 59 | * */ 60 | export const buildPartialSignature = ( 61 | privateKeyK: string, 62 | term: number, 63 | nonce: number, 64 | publicKeysRootForTerm: string 65 | ): string => { 66 | const mHash = crypto.createHash('sha256') 67 | .update(`${ nonce }:${ term }:${ publicKeysRootForTerm }`) 68 | .digest('hex'); 69 | 70 | return new BN(privateKeyK, 16) 71 | .mul(new BN(mHash, 16)) 72 | .mod(ec.n) 73 | .toString(16); 74 | }; 75 | 76 | /* let s1 * G = k1 * a1 * e * G = k1 * a1 * G * e = X1 * a1 * e */ 77 | export const partialSignatureVerify = ( 78 | partialSignature: string, 79 | publicKey: string, 80 | nonce: number, 81 | term: number, 82 | sharedPublicKeyX: string): boolean => { 83 | 84 | const mHash = crypto.createHash('sha256') 85 | .update(`${ nonce }:${ term }:${ sharedPublicKeyX }`) 86 | .digest('hex'); 87 | 88 | const spG = ec.g.mul(partialSignature); 89 | const check = pubKeyToPoint(Buffer.from(publicKey, 'hex')).mul(mHash); 90 | return pointToPublicKey(spG).toString('hex') === pointToPublicKey(check).toString('hex'); 91 | }; 92 | 93 | /* s = s1 + s2 + ...sn */ 94 | export const buildSharedSignature = (partialSignatures: string[]): string => { 95 | let signature = new BN(0); 96 | 97 | for (const sig of partialSignatures) { 98 | signature = signature.add(new BN(sig, 16)); 99 | } 100 | 101 | return signature.toString(16); 102 | }; 103 | 104 | /* sG = X * e */ 105 | export const verify = ( 106 | signature: string, 107 | sharedPublicKeyX: string): boolean => { 108 | 109 | const sg = ec.g.mul(signature); 110 | const check = pubKeyToPoint(Buffer.from(sharedPublicKeyX, 'hex')); 111 | return pointToPublicKey(sg).toString('hex') === pointToPublicKey(check).toString('hex'); 112 | }; 113 | 114 | export const pubKeyToPoint = (pubKey) => { 115 | const pubKeyEven = (pubKey[0] - 0x02) === 0; 116 | return ec.curve.pointFromX(pubKey.slice(1, 33).toString('hex'), !pubKeyEven); 117 | }; 118 | 119 | export const pointToPublicKey = (P): Buffer => { 120 | const buffer = Buffer.allocUnsafe(1); 121 | // keep sign, if is odd 122 | buffer.writeUInt8(P.getY().isEven() ? 0x02 : 0x03, 0); 123 | return Buffer.concat([buffer, P.getX().toArrayLike(Buffer)]); 124 | }; 125 | -------------------------------------------------------------------------------- /src/test/integration/consensus/testSuite.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import Promise from 'bluebird'; 3 | import {fork} from 'child_process'; 4 | import crypto from 'crypto'; 5 | import * as _ from 'lodash'; 6 | import * as path from 'path'; 7 | import NodeStates from '../../../consensus/constants/NodeStates'; 8 | 9 | // tslint:disable-next-line:max-line-length 10 | export function testSuite(ctx: any = {}, nodesCount: number = 0, mokkaType: string = 'TCP', crashModel: string = 'CFT') { 11 | 12 | beforeEach(async () => { 13 | 14 | const mokkas: any = []; 15 | 16 | ctx.keys = []; 17 | ctx.settings = { 18 | crashModel, 19 | electionTimeout: 1000, 20 | heartbeat: 300, 21 | proofExpiration: 5000 22 | }; 23 | 24 | for (let i = 0; i < nodesCount; i++) { 25 | const node = crypto.createECDH('secp256k1'); 26 | node.generateKeys(); 27 | 28 | if (node.getPrivateKey().toString('hex').length !== 64) { 29 | i--; 30 | continue; 31 | } 32 | 33 | ctx.keys.push({ 34 | privateKey: node.getPrivateKey('hex'), 35 | publicKey: node.getPublicKey('hex', 'compressed') 36 | }); 37 | } 38 | 39 | const mokkaTypes = { 40 | RPC: 'MokkaRPCWorker.ts', 41 | TCP: 'MokkaTCPWorker.ts' 42 | }; 43 | 44 | for (let index = 0; index < ctx.keys.length; index++) { 45 | const instance = fork(path.join(__dirname, `../workers/${mokkaTypes[mokkaType]}`), [], { 46 | execArgv: ['-r', 'ts-node/register'] 47 | }); 48 | mokkas.push(instance); 49 | instance.send({ 50 | args: [ 51 | { 52 | index, 53 | keys: ctx.keys, 54 | settings: ctx.settings 55 | } 56 | ], 57 | type: 'init' 58 | }); 59 | } 60 | 61 | const kill = () => { 62 | for (const instance of mokkas) 63 | instance.kill(); 64 | }; 65 | 66 | process.on('SIGINT', kill); 67 | process.on('SIGTERM', kill); 68 | ctx.mokkas = mokkas; 69 | }); 70 | 71 | afterEach(async () => { 72 | for (const node of ctx.mokkas) { 73 | node.kill(); 74 | } 75 | }); 76 | 77 | it(`should find leader, once most nodes online`, async () => { 78 | 79 | const promises = []; 80 | const initialNodesAmount = ctx.mokkas.length - Math.ceil(ctx.mokkas.length - 1) / (crashModel === 'CFT' ? 2 : 3); 81 | 82 | let leaderPubKey = null; 83 | 84 | for (let i = 0; i < initialNodesAmount; i++) { 85 | ctx.mokkas[i].send({type: 'connect'}); 86 | 87 | const promise = new Promise((res) => { 88 | ctx.mokkas[i].on('message', (msg: any) => { 89 | if (msg.type !== 'state' || (msg.args[0] !== NodeStates.LEADER && !leaderPubKey)) 90 | return; 91 | 92 | if (msg.args[0] === NodeStates.LEADER) { 93 | leaderPubKey = msg.args[1]; 94 | return res({state: msg.args[0], publicKey: leaderPubKey, term: msg.args[2], index: msg.args[3]}); 95 | } 96 | 97 | if (msg.args[1] === leaderPubKey) { 98 | return res({state: msg.args[0], publicKey: leaderPubKey, term: msg.args[2], index: msg.args[3]}); 99 | } 100 | 101 | }); 102 | }); 103 | promises.push(promise); 104 | } 105 | 106 | // tslint:disable-next-line:max-line-length 107 | const result: { state: number, publicKey: string, term: number, index: number }[] = await Promise.all(promises); 108 | const leaderEventMap = result.reduce((acc, val) => { 109 | 110 | if (val.state !== NodeStates.LEADER) { 111 | return acc; 112 | } 113 | 114 | if (!acc.has(val.term)) { 115 | acc.set(val.term, []); 116 | } 117 | 118 | acc.get(val.term).push(val.index); 119 | return acc; 120 | }, new Map()); 121 | 122 | const maxTerm = _.max([...leaderEventMap.keys()]); 123 | const leaderIndex = leaderEventMap.get(maxTerm)[0]; 124 | const timer = Date.now(); 125 | 126 | await new Promise((res) => { 127 | ctx.mokkas[leaderIndex].on('message', (msg: any) => { 128 | if (msg.type !== 'state' || (msg.args[0] === NodeStates.LEADER)) 129 | return; 130 | 131 | res(); 132 | }); 133 | }); 134 | 135 | assert(Math.round(ctx.settings.proofExpiration / 2) <= Date.now() - timer); 136 | }); 137 | 138 | } 139 | -------------------------------------------------------------------------------- /src/consensus/api/NodeApi.ts: -------------------------------------------------------------------------------- 1 | import eventTypes from '../constants/EventTypes'; 2 | import EventTypes from '../constants/EventTypes'; 3 | import messageTypes from '../constants/MessageTypes'; 4 | import states from '../constants/NodeStates'; 5 | import {Mokka} from '../main'; 6 | import {NodeModel} from '../models/NodeModel'; 7 | import {PacketModel} from '../models/PacketModel'; 8 | import {VoteModel} from '../models/VoteModel'; 9 | import * as utils from '../utils/cryptoUtils'; 10 | import {getCombinations} from '../utils/utils'; 11 | import {MessageApi} from './MessageApi'; 12 | 13 | class NodeApi { 14 | 15 | private readonly mokka: Mokka; 16 | private messageApi: MessageApi; 17 | 18 | constructor(mokka: Mokka) { 19 | this.mokka = mokka; 20 | this.messageApi = new MessageApi(mokka); 21 | } 22 | 23 | public join(multiaddr: string): NodeModel { 24 | 25 | const publicKey = multiaddr.match(/\w+$/).toString(); 26 | 27 | if (this.mokka.publicKey === publicKey) 28 | return; 29 | 30 | const node = new NodeModel(null, multiaddr, 'CFT', states.STOPPED); 31 | 32 | node.write = this.mokka.write.bind(this.mokka); 33 | node.once('end', () => this.leave(node.publicKey)); 34 | 35 | this.mokka.nodes.set(publicKey, node); 36 | 37 | this.buildPublicKeysRootAndCombinations(); 38 | this.mokka.emit(eventTypes.NODE_JOIN, node); 39 | return node; 40 | } 41 | 42 | public buildPublicKeysRootAndCombinations() { 43 | const sortedPublicKeys = [...this.mokka.nodes.keys(), this.mokka.publicKey].sort(); 44 | this.mokka.publicKeysRoot = utils.buildPublicKeysRoot(sortedPublicKeys); 45 | this.mokka.publicKeysCombinationsInQuorum = getCombinations(sortedPublicKeys, this.mokka.majority()); 46 | } 47 | 48 | public leave(publicKey: string): void { 49 | 50 | const node = this.mokka.nodes.get(publicKey); 51 | this.mokka.nodes.delete(publicKey); 52 | 53 | this.buildPublicKeysRootAndCombinations(); 54 | this.mokka.emit(eventTypes.NODE_LEAVE, node); 55 | } 56 | 57 | public async promote(): Promise { 58 | 59 | if (this.mokka.state === states.CANDIDATE) { 60 | return; 61 | } 62 | 63 | const nonce = Date.now(); 64 | this.mokka.setState(states.CANDIDATE, this.mokka.term + 1, ''); 65 | 66 | const publicKeysRootForTerm = utils.buildPublicKeysRootForTerm( 67 | this.mokka.publicKeysRoot, 68 | this.mokka.term, 69 | nonce, 70 | this.mokka.publicKey); 71 | const vote = new VoteModel(nonce, this.mokka.term, publicKeysRootForTerm); 72 | 73 | for (const combination of this.mokka.publicKeysCombinationsInQuorum) { 74 | 75 | if (!combination.includes(this.mokka.publicKey)) { 76 | continue; 77 | } 78 | 79 | const sharedPublicKeyPartial = utils.buildSharedPublicKeyX( 80 | combination, 81 | this.mokka.term, 82 | nonce, 83 | publicKeysRootForTerm 84 | ); 85 | vote.publicKeyToCombinationMap.set(sharedPublicKeyPartial, combination); 86 | } 87 | 88 | this.mokka.setVote(vote); 89 | 90 | const votePayload = { 91 | nonce, 92 | publicKey: this.mokka.publicKey, 93 | term: this.mokka.term 94 | }; 95 | 96 | const selfVoteSignature = utils.buildPartialSignature( 97 | this.mokka.privateKey, 98 | votePayload.term, 99 | votePayload.nonce, 100 | publicKeysRootForTerm 101 | ); 102 | 103 | vote.repliesPublicKeyToSignatureMap.set(this.mokka.publicKey, selfVoteSignature); 104 | 105 | const packet = this.messageApi.packet(messageTypes.VOTE, { 106 | nonce 107 | }); 108 | 109 | await Promise.all( 110 | [...this.mokka.nodes.values()].map((node) => 111 | this.messageApi.message(packet, node.publicKey) 112 | )); 113 | 114 | await new Promise((res) => { 115 | 116 | const timeoutHandler = () => { 117 | this.mokka.removeListener(EventTypes.STATE, emitHandler); 118 | res(); 119 | }; 120 | 121 | const timeoutId = setTimeout(timeoutHandler, this.mokka.electionTimeout); 122 | 123 | const emitHandler = () => { 124 | clearTimeout(timeoutId); 125 | res(); 126 | }; 127 | 128 | this.mokka.once(EventTypes.STATE, emitHandler); 129 | }); 130 | 131 | if (this.mokka.state === states.CANDIDATE) { 132 | this.mokka.setState(states.FOLLOWER, this.mokka.term, null); 133 | } 134 | } 135 | 136 | public async pingFromLeader(packet: PacketModel | null): Promise { 137 | if (packet && packet.state === states.LEADER) { 138 | this.mokka.logger.trace(`accepted ack`); 139 | this.mokka.heartbeatCtrl.setNextBeat(this.mokka.heartbeatCtrl.timeout()); 140 | this.mokka.emit(EventTypes.ACK); 141 | } 142 | return null; 143 | } 144 | 145 | } 146 | 147 | export {NodeApi}; 148 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mokka 2 | 3 | [![Build Status](https://www.travis-ci.com/ega-forever/mokka.svg?branch=master)](https://travis-ci.com/ega-forever/mokka) 4 | 5 | Mokka Consensus Algorithm implementation in Node.js. 6 | 7 | [Concept description](https://arxiv.org/ftp/arxiv/papers/1901/1901.08435.pdf) (PDF) 8 | 9 | [Live Demo](https://ega-forever.github.io/mokka/) (in browser) 10 | 11 | 12 | consensus features 13 | * resistant to network splits 14 | * non-anonymous voting 15 | * voting validation with musig 16 | 17 | implementation features 18 | * Custom transport layer support: Mokka separates interface implementation and consensus 19 | * Fully customizable: you can create your own state machine around Mokka consensus (check out demos for more info) 20 | * Can run in CFT and BFT modes 21 | 22 | ## Installation 23 | 24 | ### Via npm 25 | ```bash 26 | $ npm install mokka --save 27 | ``` 28 | 29 | ### From repo 30 | ```bash 31 | $ npm run build 32 | ``` 33 | 34 | # API 35 | 36 | ### new Mokka (options) 37 | 38 | Returns a new Mokka instance. As Mokka is agnostic to protocol implementation, 39 | you have to create your own. 40 | Please check the ``Custom transport layer`` section. 41 | 42 | Arguments: 43 | 44 | * `address` (string): an address in custom format. The only rule is that address should include the public key in the end 45 | (example: `"tcp://127.0.0.1:2003/03fec1b3d32dbb0641877f65b4e77ba8466f37ab948c0b4780e4ed191be411d694"`) 46 | * `crashModel` (`"CFT" | "BFT"`): crash model, which should run the consensus. The difference is in quorum - CFT requires `f + 1` nodes for quorum, while BFT `2f + 1` 47 | * `heartbeat` (integer): leader heartbeat timeout 48 | * `electionTimeout` (integer): candidate election timeout (i.e. vote round) 49 | * `customVoteRule` (func): additional voting rule 50 | * `reqMiddleware` (func): request middleware (will be triggered on every new packet received) 51 | * `resMiddleware` (func): response middleware (will be triggered on every new packet sent) 52 | * `proofExpiration` (integer): when the leader's proof token should expire. 53 | * `logger` (ILoggerInterface): logger instance. If omitted, then console.log will be used 54 | * `privateKey`: the 64 length private key. Please take a look at [example](examples/node/decentralized-ganache/src/gen_keys.ts) key pair generator 55 | 56 | ### mokka.join(multiaddr: string): NodeModel 57 | 58 | Add new peer node by uri 59 | 60 | ### await mokka.connect(): Promise 61 | 62 | Start consensus. Should be called after all nodes has been added. 63 | 64 | ### mokka.messageApi.packet(type: number, data: any = null): PacketModel 65 | 66 | Create new packet, where ``type`` is packet type, and ``data`` some custom data 67 | 68 | ### mokka.messageApi.decodePacket(message: Buffer): PacketModel 69 | 70 | Decode packet from buffer 71 | 72 | ### await mokka.messageApi.message(packet: PacketModel, peerPublicKey: string): Promise 73 | 74 | Send message to peer 75 | 76 | ## Events 77 | 78 | A Mokka instance emits the following events (available at ``/components/shared/EventTypes.ts``): 79 | 80 | * `join`: once we add new peer 81 | * `leave`: once we remove peer 82 | * `heartbeat_timeout`: once we can't receive the heartbeat from leader in certain time (specified in config) 83 | * `state`: once the state of node changed (i.e. leader, candidate, follower) 84 | 85 | # Custom RSM 86 | 87 | Mokka is a log-less consensus algorithm and doesn't provide any RSM (i.e. replicated log). You have to implement your own. 88 | However, there is a good example of RSM implementation, which is [similar to RAFT](examples/node/cluster/README.md). 89 | 90 | # Custom transport layer 91 | 92 | In order to communicate between nodes, you have to implement the interface by yourself. As an example you can take a look at TCP implementation: ```src/implementation/TCP```. 93 | In order to write your own implementation you have to implement 2 methods: 94 | 95 | * The ```async initialize()``` function, which fires on Mokka start. This method is useful, when you want to open the connection, for instance, tcp one, or connect to certain message broker like rabbitMQ. 96 | 97 | * The ```async write(address: string, packet: Buffer)``` function, which fires each time Mokka wants to broadcast message to other peer (address param). 98 | 99 | Also, keep in mind, that Mokka doesn't handle the disconnected / dead peers, which means that Mokka will try to make requests to all presented members in cluster, 100 | even if they are not available. So, you need to handle it on your own. 101 | 102 | # Examples 103 | 104 | | Node.js | 105 | | --- | 106 | | [running cluster](examples/node/cluster/README.md) | 107 | | [running private blockchain](examples/node/decentralized-ganache/README.md) | - 108 | 109 | # Implemented protocols out of the box 110 | 111 | 112 | | Node.js | 113 | | --- | 114 | | [TCP](src/implementation/TCP.ts) | 115 | | [ZMQ](src/implementation/ZMQ.ts) | 116 | 117 | 118 | However, you still can implement your own protocol. 119 | 120 | # License 121 | 122 | [GNU AGPLv3](LICENSE) 123 | 124 | # Copyright 125 | 126 | Copyright (c) 2018-2021 Egor Zuev 127 | -------------------------------------------------------------------------------- /examples/node/cluster/src/cluster.ts: -------------------------------------------------------------------------------- 1 | import bunyan = require('bunyan'); 2 | import MokkaEvents from 'mokka/dist/consensus/constants/EventTypes'; 3 | import MessageTypes from 'mokka/dist/consensus/constants/MessageTypes'; 4 | import NodeStates from 'mokka/dist/consensus/constants/NodeStates'; 5 | import {PacketModel} from 'mokka/dist/consensus/models/PacketModel'; 6 | import TCPMokka from 'mokka/dist/implementation/TCP'; 7 | import * as readline from 'readline'; 8 | 9 | class ExtendedPacketModel extends PacketModel { 10 | public logIndex: number; 11 | public log: { key: string, value: string, index: number }; 12 | } 13 | 14 | // our generated key pairs 15 | const keys = [ 16 | { 17 | publicKey: '0263920784223e0f5aa31a4bcdae945304c1c85df68064e9106ebfff1511221ee9', 18 | secretKey: '507b1433f58edd4eff3e87d9ba939c74bd15b4b10c00c548214817c0295c521a' 19 | }, 20 | { 21 | publicKey: '024ad0ef01d3d62b41b80a354a5b748524849c28f5989f414c4c174647137c2587', 22 | secretKey: '3cb2530ded6b8053fabf339c9e10e46ceb9ffc2064d535f53df59b8bf36289a1' 23 | }, 24 | { 25 | publicKey: '02683e65682deeb98738b44f8eb9d1840852e5635114c7c4ef2e39f20806b96dbf', 26 | secretKey: '1c954bd0ecc1b2c713b88678e48ff011e53d53fc496458063116a2e3a81883b8' 27 | } 28 | ]; 29 | 30 | const logsStorage: Array<{ key: string, value: string, index: number }> = []; 31 | const knownPeersState = new Map(); 32 | 33 | const startPort = 2000; 34 | 35 | // init mokka instance, bootstrap other nodes, and call the askCommand 36 | const initMokka = async () => { 37 | const index = parseInt(process.env.INDEX, 10); 38 | const uris = []; 39 | for (let index1 = 0; index1 < keys.length; index1++) { 40 | if (index === index1) 41 | continue; 42 | uris.push(`tcp://127.0.0.1:${startPort + index1}/${keys[index1].publicKey}`); 43 | } 44 | 45 | const logger = bunyan.createLogger({name: 'mokka.logger', level: 30}); 46 | 47 | const reqMiddleware = async (packet: ExtendedPacketModel): Promise => { 48 | knownPeersState.set(packet.publicKey, packet.logIndex); 49 | const lastIndex = logsStorage[logsStorage.length - 1] ? logsStorage[logsStorage.length - 1].index : 0; 50 | 51 | if ( 52 | packet.state === NodeStates.LEADER && 53 | packet.type === MessageTypes.ACK && 54 | packet.log && 55 | packet.log.index > lastIndex) { 56 | logsStorage.push(packet.log); 57 | // @ts-ignore 58 | const replyPacket: ExtendedPacketModel = mokka.messageApi.packet(16); 59 | replyPacket.logIndex = packet.log.index; 60 | await mokka.messageApi.message(replyPacket, packet.publicKey); 61 | } 62 | 63 | return packet; 64 | }; 65 | 66 | const resMiddleware = async (packet: ExtendedPacketModel, peerPublicKey: string): Promise => { 67 | packet.logIndex = logsStorage.length ? logsStorage[logsStorage.length - 1].index : 0; 68 | const peerIndex = knownPeersState.get(peerPublicKey) || 0; 69 | 70 | if (mokka.state === NodeStates.LEADER && packet.type === MessageTypes.ACK && peerIndex < packet.logIndex) { 71 | packet.log = logsStorage.find((item) => item.index === peerIndex + 1); 72 | } 73 | 74 | return packet; 75 | }; 76 | 77 | const customVoteRule = async (packet: ExtendedPacketModel): Promise => { 78 | const lastIndex = logsStorage[logsStorage.length - 1] ? logsStorage[logsStorage.length - 1].index : 0; 79 | return packet.logIndex >= lastIndex; 80 | }; 81 | 82 | const mokka = new TCPMokka({ 83 | address: `tcp://127.0.0.1:${startPort + index}/${keys[index].publicKey}`, 84 | customVoteRule, 85 | electionTimeout: 300, 86 | heartbeat: 200, 87 | logger, 88 | privateKey: keys[index].secretKey, 89 | proofExpiration: 20000, 90 | reqMiddleware, 91 | resMiddleware 92 | }); 93 | mokka.on(MokkaEvents.STATE, () => { 94 | // logger.info(`changed state ${mokka.state} with term ${mokka.term}`); 95 | }); 96 | for (const peer of uris) 97 | mokka.nodeApi.join(peer); 98 | 99 | mokka.connect(); 100 | 101 | const rl = readline.createInterface({ 102 | input: process.stdin, 103 | output: process.stdout 104 | }); 105 | 106 | askCommand(rl, mokka); 107 | }; 108 | 109 | // listens to user's input via console 110 | const askCommand = (rl, mokka) => { 111 | rl.question('enter command > ', async (command) => { 112 | 113 | const args = command.split(' '); 114 | 115 | if (args[0] === 'add_log') { 116 | addLog(mokka, args[1], args[2]); 117 | } 118 | 119 | if (args[0] === 'get_log') { 120 | await getLog(mokka, args[1]); 121 | } 122 | 123 | if (args[0] === 'info') 124 | await getInfo(mokka); 125 | 126 | askCommand(rl, mokka); 127 | }); 128 | }; 129 | 130 | // add new log 131 | const addLog = async (mokka, key, value) => { 132 | 133 | if (mokka.state !== NodeStates.LEADER) { 134 | return console.log('i am not a leader'); 135 | } 136 | 137 | logsStorage.push({key, value, index: logsStorage.length + 1}); 138 | }; 139 | 140 | // get log by index 141 | 142 | const getLog = async (mokka, index) => { 143 | mokka.logger.info(logsStorage.find((item) => item.index === parseInt(index, 10))); 144 | }; 145 | 146 | // get info of current instance 147 | 148 | const getInfo = async (mokka) => { 149 | console.log({index: logsStorage.length, peersState: knownPeersState}); 150 | }; 151 | 152 | initMokka(); 153 | -------------------------------------------------------------------------------- /examples/node/decentralized-ganache/src/server.ts: -------------------------------------------------------------------------------- 1 | import bunyan from 'bunyan'; 2 | import detect = require('detect-port'); 3 | import ganache from 'ganache-core'; 4 | import Tx from 'ganache-core/lib/utils/transaction'; 5 | import Block from 'ganache-core/node_modules/ethereumjs-block'; 6 | import * as MokkaEvents from 'mokka/dist/consensus/constants/EventTypes'; 7 | import MessageTypes from 'mokka/dist/consensus/constants/MessageTypes'; 8 | import * as MokkaStates from 'mokka/dist/consensus/constants/NodeStates'; 9 | import NodeStates from 'mokka/dist/consensus/constants/NodeStates'; 10 | import {PacketModel} from 'mokka/dist/consensus/models/PacketModel'; 11 | import TCPMokka from 'mokka/dist/implementation/TCP'; 12 | import semaphore = require('semaphore'); 13 | import Web3 = require('web3'); 14 | import config from './config'; 15 | 16 | const logger = bunyan.createLogger({name: 'mokka.logger', level: 60}); 17 | const sem = semaphore(1); 18 | 19 | const logsStorage: Array<{ key: string, value: string }> = []; 20 | const knownPeersState = new Map(); 21 | 22 | class ExtendedPacketModel extends PacketModel { 23 | public logIndex: number; 24 | } 25 | 26 | const startGanache = async (node) => { 27 | 28 | const accounts = config.nodes.map((node) => ({ 29 | balance: node.balance, 30 | secretKey: `0x${node.secretKey.slice(0, 64)}` 31 | })); 32 | 33 | const server = ganache.server({ 34 | accounts, 35 | default_balance_ether: 500, 36 | network_id: 86, 37 | time: new Date('12-12-2018') 38 | }); 39 | 40 | await new Promise((res) => { 41 | server.listen(node.ganache, () => { 42 | console.log('started'); 43 | res(); 44 | }); 45 | }); 46 | 47 | return server; 48 | }; 49 | 50 | const startMokka = async (node, server) => { 51 | 52 | // @ts-ignore 53 | const web3 = new Web3(server.provider); 54 | 55 | const reqMiddleware = async (packet: ExtendedPacketModel): Promise => { 56 | knownPeersState.set(packet.publicKey, packet.logIndex); 57 | 58 | if ( 59 | packet.state === NodeStates.LEADER && 60 | packet.type === MessageTypes.ACK && 61 | packet.data && 62 | packet.logIndex > logsStorage.length) { 63 | 64 | sem.take(async () => { 65 | const block = new Block(Buffer.from(packet.data.value, 'hex')); 66 | block.transactions = block.transactions.map((tx) => new Tx(tx)); 67 | // @ts-ignore 68 | const replyPacket: ExtendedPacketModel = mokka.messageApi.packet(16); 69 | 70 | const savedBlock = await web3.eth.getBlock(packet.data.index); 71 | 72 | if (savedBlock) { 73 | replyPacket.logIndex = logsStorage.length; 74 | await mokka.messageApi.message(replyPacket, packet.publicKey); 75 | return sem.leave(); 76 | } 77 | 78 | await new Promise((res, rej) => { 79 | server.provider.manager.state.blockchain.processBlock( 80 | server.provider.manager.state.blockchain.vm, 81 | block, 82 | true, 83 | (err, data) => err ? rej(err) : res(data) 84 | ); 85 | }); 86 | 87 | logger.info(`new block added ${block.hash().toString('hex')}`); 88 | 89 | logsStorage.push(packet.data); 90 | replyPacket.logIndex = logsStorage.length; 91 | await mokka.messageApi.message(replyPacket, packet.publicKey); 92 | sem.leave(); 93 | }); 94 | } 95 | 96 | return packet; 97 | }; 98 | 99 | const resMiddleware = async (packet: ExtendedPacketModel, peerPublicKey: string): Promise => { 100 | packet.logIndex = logsStorage.length; 101 | const peerIndex = knownPeersState.get(peerPublicKey) || 0; 102 | 103 | if (mokka.state === NodeStates.LEADER && packet.type === MessageTypes.ACK && peerIndex < logsStorage.length) { 104 | packet.data = {...logsStorage[peerIndex], index: peerIndex + 1}; 105 | } 106 | 107 | return packet; 108 | }; 109 | 110 | const customVoteRule = async (packet: ExtendedPacketModel): Promise => { 111 | return packet.logIndex >= logsStorage.length; 112 | }; 113 | 114 | const mokka = new TCPMokka({ 115 | address: `tcp://127.0.0.1:${node.port}/${node.publicKey}`, 116 | customVoteRule, 117 | electionTimeout: 300, 118 | heartbeat: 200, 119 | logger, 120 | privateKey: node.secretKey, 121 | proofExpiration: 30000, 122 | reqMiddleware, 123 | resMiddleware 124 | }); 125 | mokka.on(MokkaEvents.default.STATE, () => { 126 | logger.info(`changed state ${mokka.state} with term ${mokka.term}`); 127 | }); 128 | 129 | config.nodes.filter((nodec) => nodec.publicKey !== node.publicKey).forEach((nodec) => { 130 | mokka.nodeApi.join(`tcp://127.0.0.1:${nodec.port}/${nodec.publicKey}`); 131 | }); 132 | 133 | await mokka.connect(); 134 | return mokka; 135 | }; 136 | 137 | const init = async () => { 138 | 139 | const allocated = await Promise.all( 140 | config.nodes.map(async (node) => 141 | node.ganache === await detect(node.ganache) 142 | ) 143 | ); 144 | 145 | const index = allocated.indexOf(true); 146 | 147 | if (index === -1) 148 | throw Error('all ports are busy'); 149 | 150 | const node = config.nodes[index]; 151 | 152 | const server = await startGanache(node); 153 | const mokka = await startMokka(node, server); 154 | 155 | server.provider.engine.on('rawBlock', async (blockJSON) => { 156 | 157 | const block: Block = await new Promise((res, rej) => { 158 | server.provider.manager.state.blockchain.getBlock(blockJSON.hash, (err, data) => err ? rej(data) : res(data)); 159 | }); 160 | 161 | if (mokka.state !== MokkaStates.default.LEADER) 162 | return; 163 | 164 | logsStorage.push({key: blockJSON.hash, value: block.serialize().toString('hex')}); 165 | }); 166 | 167 | const bound = server.provider.send; 168 | 169 | server.provider.send = async (payload, cb) => { 170 | 171 | if (mokka.state !== MokkaStates.default.LEADER && payload.method === 'eth_sendTransaction') { 172 | 173 | const node = config.nodes.find((node) => node.publicKey === mokka.leaderPublicKey); 174 | 175 | // @ts-ignore 176 | const web3 = new Web3(`http://localhost:${node.ganache}`); 177 | 178 | let hash; 179 | 180 | try { 181 | hash = await new Promise((res, rej) => 182 | web3.eth.sendTransaction(...payload.params, (err, result) => err ? rej(err) : res(result)) 183 | ); 184 | } catch (e) { 185 | return cb(e, null); 186 | } 187 | 188 | // await until tx will be processed 189 | await new Promise((res) => { 190 | const intervalPid = setInterval(async () => { 191 | 192 | const tx = await new Promise((res, rej) => 193 | server.provider.manager.eth_getTransactionByHash( 194 | hash, 195 | (err, result) => err ? rej(err) : res(result) 196 | ) 197 | ); 198 | 199 | if (tx) { 200 | clearInterval(intervalPid); 201 | res(); 202 | } 203 | 204 | }, 200); 205 | }); 206 | 207 | const reply = { 208 | id: payload.id, 209 | jsonrpc: payload.jsonrpc, 210 | result: hash 211 | }; 212 | 213 | return cb(null, reply); 214 | } 215 | 216 | return bound.call(server.provider, payload, cb); 217 | }; 218 | }; 219 | 220 | module.exports = init(); 221 | -------------------------------------------------------------------------------- /src/consensus/api/VoteApi.ts: -------------------------------------------------------------------------------- 1 | import messageTypes from '../constants/MessageTypes'; 2 | import states from '../constants/NodeStates'; 3 | import NodeStates from '../constants/NodeStates'; 4 | import { Mokka } from '../main'; 5 | import { PacketModel } from '../models/PacketModel'; 6 | import { VoteModel } from '../models/VoteModel'; 7 | import * as utils from '../utils/cryptoUtils'; 8 | import { MessageApi } from './MessageApi'; 9 | import EventTypes from '../constants/EventTypes'; 10 | 11 | class VoteApi { 12 | 13 | private mokka: Mokka; 14 | private messageApi: MessageApi; 15 | 16 | constructor(mokka: Mokka) { 17 | this.mokka = mokka; 18 | this.messageApi = new MessageApi(mokka); 19 | } 20 | 21 | public async vote(packet: PacketModel): Promise { 22 | 23 | if (!packet.data.nonce || 24 | packet.data.nonce > Date.now() || 25 | Date.now() - packet.data.nonce > this.mokka.electionTimeout) { 26 | this.mokka.logger.trace(`[vote] peer ${ packet.publicKey } hasn't provided a correct nonce`); 27 | return this.messageApi.packet(messageTypes.VOTED); 28 | } 29 | 30 | if ( 31 | this.mokka.term >= packet.term || 32 | (this.mokka.vote && this.mokka.vote.term >= packet.term) || 33 | !this.mokka.checkTermNumber(packet.term) || 34 | !this.mokka.checkPublicKeyCanBeLeaderNextRound(packet.publicKey) 35 | ) { 36 | return this.messageApi.packet(messageTypes.VOTED); 37 | } 38 | 39 | const wasAckFromLeader = await this.waitForNextAck(); 40 | 41 | if (wasAckFromLeader) { 42 | this.mokka.logger.trace(`[vote] peer ${ packet.publicKey } asked to vote with alive leader`); 43 | return this.messageApi.packet(messageTypes.VOTED); 44 | } 45 | 46 | const isCustomRulePassed = await this.mokka.customVoteRule(packet); 47 | 48 | if (!isCustomRulePassed) { 49 | return this.messageApi.packet(messageTypes.VOTED); 50 | } 51 | const publicKeysRootForTerm = utils.buildPublicKeysRootForTerm( 52 | this.mokka.publicKeysRoot, 53 | packet.term, 54 | packet.data.nonce, 55 | packet.publicKey 56 | ); 57 | 58 | const vote = new VoteModel(packet.data.nonce, packet.term, publicKeysRootForTerm); 59 | this.mokka.setVote(vote); 60 | 61 | const startBuildVote = Date.now(); 62 | const signature = utils.buildPartialSignature( 63 | this.mokka.privateKey, 64 | packet.term, 65 | packet.data.nonce, 66 | publicKeysRootForTerm 67 | ); 68 | this.mokka.logger.trace(`built vote in ${ Date.now() - startBuildVote }`); 69 | 70 | return this.messageApi.packet(messageTypes.VOTED, { 71 | signature 72 | }); 73 | } 74 | 75 | public async voted(packet: PacketModel): Promise { 76 | 77 | if (states.CANDIDATE !== this.mokka.state || !packet.data) { 78 | return null; 79 | } 80 | 81 | const startPartialSigVerificationTime = Date.now(); 82 | const isValidPartialSignature = utils.partialSignatureVerify( 83 | packet.data.signature, 84 | packet.publicKey, 85 | this.mokka.vote.nonce, 86 | this.mokka.term, 87 | this.mokka.vote.publicKeysRootForTerm 88 | ); 89 | this.mokka.logger.trace(`verified partial signature in ${ Date.now() - startPartialSigVerificationTime }`); 90 | 91 | if (!isValidPartialSignature) { 92 | this.mokka.logger.trace(`[voted] peer ${ packet.publicKey } provided bad signature`); 93 | return null; 94 | } 95 | 96 | if (!this.mokka.vote.repliesPublicKeyToSignatureMap.has(packet.publicKey)) { 97 | this.mokka.vote.repliesPublicKeyToSignatureMap.set(packet.publicKey, packet.data.signature); 98 | } 99 | 100 | const isQuorumReached = this.mokka.quorum(this.mokka.vote.repliesPublicKeyToSignatureMap.size); 101 | 102 | if (!isQuorumReached) 103 | return null; 104 | 105 | const fullSigBuildTime = Date.now(); 106 | const fullSignature = utils.buildSharedSignature( 107 | Array.from(this.mokka.vote.repliesPublicKeyToSignatureMap.values()) 108 | ); 109 | this.mokka.logger.trace(`full signature has been built in ${ Date.now() - fullSigBuildTime }`); 110 | 111 | const participantPublicKeys = Array.from(this.mokka.vote.repliesPublicKeyToSignatureMap.keys()).sort(); 112 | const sharedPublicKeyXs = Array.from(this.mokka.vote.publicKeyToCombinationMap.keys()); 113 | 114 | const sharedPublicKeyX = sharedPublicKeyXs.find((sharedPublicKey) => 115 | this.mokka.vote.publicKeyToCombinationMap.get(sharedPublicKey).join('') === participantPublicKeys.join('')); 116 | 117 | const fullSigVerificationTime = Date.now(); 118 | const isValid = utils.verify( 119 | fullSignature, 120 | sharedPublicKeyX 121 | ); 122 | this.mokka.logger.trace(`full signature has been verified in ${ Date.now() - fullSigVerificationTime }`); 123 | 124 | if (!isValid) { 125 | this.mokka.logger.trace('invalid full signature'); 126 | this.mokka.setState(states.FOLLOWER, this.mokka.term, null); 127 | return; 128 | } 129 | 130 | const compacted = `${ this.mokka.vote.nonce }:${ sharedPublicKeyX }:${ fullSignature }`; 131 | this.mokka.setState(states.LEADER, this.mokka.term, this.mokka.publicKey, compacted, this.mokka.vote.nonce); 132 | return null; 133 | } 134 | 135 | public async validateAndApplyLeader(packet: PacketModel): Promise { 136 | 137 | if (packet.term < this.mokka.term || !packet.proof) { 138 | this.mokka.logger.trace('no proof supplied or term is outdated'); 139 | return null; 140 | } 141 | 142 | if (this.mokka.proof && 143 | this.mokka.proof === packet.proof && 144 | this.mokka.getProofMintedTime() + this.mokka.proofExpiration < Date.now()) { 145 | this.mokka.logger.trace('proof expired'); 146 | return null; 147 | } 148 | 149 | if (this.mokka.proof !== packet.proof) { 150 | const startProofValidation = Date.now(); 151 | const [proofNonce, proofSharedPublicKeyX, proofSignature] = packet.proof.split(':'); 152 | 153 | const publicKeysRootForTerm = utils.buildPublicKeysRootForTerm( 154 | this.mokka.publicKeysRoot, 155 | packet.term, 156 | proofNonce, 157 | packet.publicKey 158 | ); 159 | 160 | const proofSharedPublicKeyCombination = this.mokka.publicKeysCombinationsInQuorum.find((combination) => { 161 | return utils.buildSharedPublicKeyX( 162 | combination, 163 | packet.term, 164 | proofNonce, 165 | publicKeysRootForTerm 166 | ) === proofSharedPublicKeyX; 167 | }); 168 | 169 | if (!proofSharedPublicKeyCombination) { 170 | this.mokka.logger.trace(`proof contains unknown public key`); 171 | return null; 172 | } 173 | 174 | if (!proofSharedPublicKeyCombination.includes(packet.publicKey)) { 175 | this.mokka.logger.trace(`proof is used by wrong node`); 176 | return null; 177 | } 178 | 179 | const isValid = utils.verify(proofSignature, proofSharedPublicKeyX); 180 | 181 | if (!isValid) { 182 | this.mokka.logger.trace(`wrong proof supplied`); 183 | return null; 184 | } 185 | 186 | this.mokka.setState( 187 | states.FOLLOWER, 188 | packet.term, 189 | packet.publicKey, 190 | packet.proof, 191 | parseInt(proofNonce, 10)); 192 | this.mokka.logger.trace(`proof validated in ${ Date.now() - startProofValidation }`); 193 | return packet; 194 | } 195 | 196 | return packet; 197 | } 198 | 199 | private async waitForNextAck(): Promise { 200 | return await new Promise((res) => { 201 | 202 | const timeoutHandler = () => { 203 | this.mokka.removeListener(EventTypes.ACK, emitHandler); 204 | res(false); 205 | }; 206 | 207 | const timeoutId = setTimeout(timeoutHandler, this.mokka.heartbeatCtrl.safeHeartbeat()); 208 | 209 | const emitHandler = () => { 210 | clearTimeout(timeoutId); 211 | res(true); 212 | }; 213 | 214 | this.mokka.once(EventTypes.ACK, emitHandler); 215 | }); 216 | } 217 | 218 | } 219 | 220 | export { VoteApi }; 221 | -------------------------------------------------------------------------------- /examples/node/cluster/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Running cluster 3 | 4 | In this tutorial we are going to create a simple cluster with 3 members. 5 | 6 | ## Installation 7 | 8 | First of all, let's install mokka (via npm): 9 | 10 | ```bash 11 | $ npm install mokka --save 12 | ``` 13 | 14 | ## Prepare keys 15 | As mokka use asymmetric cryptography, we have to create the key pairs for each member of cluster. 16 | 17 | ``src/gen_keys.ts`` 18 | ```javascript 19 | import crypto from 'crypto'; 20 | 21 | for (let i = 0; i < 3; i++) { 22 | const node = crypto.createECDH('secp256k1'); 23 | node.generateKeys('hex'); 24 | console.log(`pair[${i + 1}] {publicKey: ${node.getPublicKey('hex')}, secretKey: ${node.getPrivateKey('hex')}`); 25 | } 26 | ``` 27 | Now let's call the gen_keys: ```bash $ node gen_keys.ts``` 28 | The output should be similar to this one: 29 | ``` 30 | pair[1] {publicKey: 04c966d69cad83cba9290b4fb2cfd79aaef114215fa0ec3b361154d0cf6d54f2e34034c5628dfef87316b6dd07e6ac15f671debdaaa3d19e9d0353d212b2c0db43, secretKey: fe15d6a3e13b8c3d56fd529a6e2b315a0a8634c5519100b35c3078a847871b58 31 | pair[2] {publicKey: 044b2879f5f8360e060e5ed5bb8e954808dd81b57bcedf290611e98b8a78e39a163a11b9bc722a39554f4d761ca96e1ea280e0a628ab2aff64bae5ed0592133786, secretKey: 905bf7936ed38eb6dd975685716eed401aa988b760a75f5367321c20e5ade917 32 | pair[3] {publicKey: 04609211f57ff99d6039b1a2d7aa7c2ffdb72d06911b2fd82386b0b880e8514a72a00952be54ec337daf1d11853e7b66b3bbab9428cbae1bfc99c1c9b9bcf6c4b4, secretKey: 46d6e16dbaed771eb899aa8c845578ed354a01cef1928a7f2feccfe04c5b22a0 33 | ``` 34 | 35 | ## Mokka implementation 36 | 37 | As mokka is agnostic to protocol, we have to implement it, or take exciting one from implementations (in our case we will take TCP). 38 | Also we will need to install the a socket lib for messages exchange (used by TCP implementation): 39 | ```bash 40 | $ npm install axon --save 41 | ``` 42 | 43 | ## Cluster implementation 44 | 45 | Now we need a code, which will boot up the Mokka with certain params. 46 | Also, this code should accept user input for interaction purpose (in our case via console), 47 | and logger - for getting logs in appropriate format. 48 | Todo that, type: 49 | ```bash 50 | $ npm install readline bunyan --save 51 | ``` 52 | Now we need to write the cluster implementation ``src/cluster.ts`` 53 | 54 | ```javascript 55 | 56 | class ExtendedPacketModel extends PacketModel { 57 | public logIndex: number; 58 | } 59 | 60 | // our generated key pairs 61 | const keys = [ 62 | { 63 | publicKey: '0263920784223e0f5aa31a4bcdae945304c1c85df68064e9106ebfff1511221ee9', 64 | secretKey: '507b1433f58edd4eff3e87d9ba939c74bd15b4b10c00c548214817c0295c521a' 65 | }, 66 | { 67 | publicKey: '024ad0ef01d3d62b41b80a354a5b748524849c28f5989f414c4c174647137c2587', 68 | secretKey: '3cb2530ded6b8053fabf339c9e10e46ceb9ffc2064d535f53df59b8bf36289a1' 69 | }, 70 | { 71 | publicKey: '02683e65682deeb98738b44f8eb9d1840852e5635114c7c4ef2e39f20806b96dbf', 72 | secretKey: '1c954bd0ecc1b2c713b88678e48ff011e53d53fc496458063116a2e3a81883b8' 73 | } 74 | ]; 75 | 76 | const logsStorage: Array<{ key: string, value: string }> = []; 77 | const knownPeersState = new Map(); 78 | 79 | const startPort = 2000; 80 | 81 | // init mokka instance, bootstrap other nodes, and call the askCommand 82 | const initMokka = async () => { 83 | const index = parseInt(process.env.INDEX, 10); 84 | const uris = []; 85 | for (let index1 = 0; index1 < keys.length; index1++) { 86 | if (index === index1) 87 | continue; 88 | uris.push(`tcp://127.0.0.1:${startPort + index1}/${keys[index1].publicKey}`); 89 | } 90 | 91 | const logger = bunyan.createLogger({name: 'mokka.logger', level: 30}); 92 | 93 | const reqMiddleware = async (packet: ExtendedPacketModel): Promise => { 94 | knownPeersState.set(packet.publicKey, packet.logIndex); 95 | 96 | if ( 97 | packet.state === NodeStates.LEADER && 98 | packet.type === MessageTypes.ACK && 99 | packet.data && 100 | packet.logIndex > logsStorage.length) { 101 | logsStorage.push(packet.data); 102 | // @ts-ignore 103 | const replyPacket: ExtendedPacketModel = mokka.messageApi.packet(16); 104 | replyPacket.logIndex = logsStorage.length; 105 | await mokka.messageApi.message(replyPacket, packet.publicKey); 106 | } 107 | 108 | return packet; 109 | }; 110 | 111 | const resMiddleware = async (packet: ExtendedPacketModel, peerPublicKey: string): Promise => { 112 | packet.logIndex = logsStorage.length; 113 | const peerIndex = knownPeersState.get(peerPublicKey) || 0; 114 | 115 | if (mokka.state === NodeStates.LEADER && packet.type === MessageTypes.ACK && peerIndex < logsStorage.length) { 116 | packet.data = logsStorage[peerIndex]; 117 | } 118 | 119 | return packet; 120 | }; 121 | 122 | const customVoteRule = async (packet: ExtendedPacketModel): Promise => { 123 | return packet.logIndex >= logsStorage.length; 124 | }; 125 | 126 | const mokka = new TCPMokka({ 127 | address: `tcp://127.0.0.1:${startPort + index}/${keys[index].publicKey}`, 128 | customVoteRule, 129 | electionTimeout: 300, 130 | heartbeat: 200, 131 | logger, 132 | privateKey: keys[index].secretKey, 133 | proofExpiration: 60000, 134 | reqMiddleware, 135 | resMiddleware 136 | }); 137 | mokka.on(MokkaEvents.STATE, () => { 138 | // logger.info(`changed state ${mokka.state} with term ${mokka.term}`); 139 | }); 140 | for (const peer of uris) 141 | mokka.nodeApi.join(peer); 142 | 143 | mokka.connect(); 144 | 145 | const rl = readline.createInterface({ 146 | input: process.stdin, 147 | output: process.stdout 148 | }); 149 | 150 | askCommand(rl, mokka); 151 | }; 152 | 153 | // listens to user's input via console 154 | const askCommand = (rl, mokka) => { 155 | rl.question('enter command > ', async (command) => { 156 | 157 | const args = command.split(' '); 158 | 159 | if (args[0] === 'add_log') { 160 | addLog(mokka, args[1], args[2]); 161 | } 162 | 163 | if (args[0] === 'get_log') { 164 | await getLog(mokka, args[1]); 165 | } 166 | 167 | if (args[0] === 'info') 168 | await getInfo(mokka); 169 | 170 | askCommand(rl, mokka); 171 | }); 172 | }; 173 | 174 | // add new log 175 | const addLog = async (mokka, key, value) => { 176 | 177 | if (mokka.state !== NodeStates.LEADER) { 178 | return console.log('i am not a leader'); 179 | } 180 | 181 | logsStorage.push({key, value}); 182 | }; 183 | 184 | // get log by index 185 | 186 | const getLog = async (mokka, index) => { 187 | mokka.logger.info(logsStorage[index]); 188 | }; 189 | 190 | // get info of current instance 191 | 192 | const getInfo = async (mokka) => { 193 | console.log({index: logsStorage.length, peersState: knownPeersState}); 194 | }; 195 | 196 | initMokka(); 197 | 198 | ``` 199 | 200 | Each instance should have its own index, specified in env. 201 | By this index, we will pick up the current key pair and port 202 | for tcp server (i.e. 2000 + index). 203 | 204 | Also, you can move the start script to ``scripts`` section in package.json: 205 | ``` 206 | ... 207 | "scripts": { 208 | "run_1": "set INDEX=0 && node src/cluster_node.js", 209 | "run_2": "set INDEX=1 && node src/cluster_node.js", 210 | "run_3": "set INDEX=2 && node src/cluster_node.js" 211 | }, 212 | ... 213 | ``` 214 | 215 | 216 | 217 | ## Usage 218 | 219 | Now we can run out cluster. So, you have to open 3 terminals and type in each terminal the appropriate command: 220 | terminal 1: ```npm run run_1``` 221 | terminal 2: ```npm run run_2``` 222 | terminal 3: ```npm run run_3``` 223 | 224 | In order to generate new log with key "super" and value "test", type: 225 | ``` 226 | add_log super test 227 | ``` 228 | To get instance state, type: 229 | ``` 230 | info 231 | ``` 232 | 233 | To get log by index, for instance 3, type: 234 | ``` 235 | get_log 3 236 | ``` 237 | 238 | That's all, now you can easily boot your own cluster. 239 | All source code can be found under ``examples/node/cluster``. 240 | In case, you are going to run the demo from mokka's repo, then first run: ```npm run build_dist``` for generating mokka dist folder. -------------------------------------------------------------------------------- /examples/node/decentralized-ganache/README.md: -------------------------------------------------------------------------------- 1 | # Running decentralized ganache 2 | 3 | In this tutorial we are going to create a simple ganache instance with running mokka consensus behind. 4 | 5 | ## Installation 6 | 7 | First of all, let's install mokka, ganache and other required stuff (via npm): 8 | 9 | ```bash 10 | $ npm install mokka ganache-core tweetnacl bunyan web3 --save 11 | ``` 12 | 13 | 14 | ## Prepare keys 15 | As mokka use asymmetric cryptography, we have to create the key pairs for each member of cluster. 16 | 17 | ``src/gen_keys.ts`` 18 | ```javascript 19 | import crypto from 'crypto'; 20 | 21 | for (let i = 0; i < 3; i++) { 22 | const node = crypto.createECDH('secp256k1'); 23 | node.generateKeys('hex'); 24 | console.log(`pair[${i + 1}] {publicKey: ${node.getPublicKey('hex', 'compressed')}, secretKey: ${node.getPrivateKey('hex')}`); 25 | } 26 | 27 | ``` 28 | Now let's call the gen_keys: ```bash $ node src/gen_keys.ts``` 29 | The output should be similar to this one: 30 | ``` 31 | pair[1] {publicKey: d6c922bc69a0cc059565a80996188d11d29e78ded4115b1d24039ba25e655afb, secretKey: 4ca246e3ab29c0085b5cbb797f86e6deea778c6e6584a45764ec10f9e0cebd7fd6c922bc69a0cc059565a80996188d11d29e78ded4115b1d24039ba25e655afb 32 | pair[2] {publicKey: a757d4dbbeb8564e1a3575ba89a12fccaacf2940d86c453da8b3f881d1fcfdba, secretKey: 931f1c648e1f87b56cd22e8de7ed235b9bd4ade9696c9d8c75f212a1fa401d5da757d4dbbeb8564e1a3575ba89a12fccaacf2940d86c453da8b3f881d1fcfdba 33 | pair[3] {publicKey: 009d53a3733c81375c2b5dfd4e7c51c14be84919d6e118198d35afd80965a52c, secretKey: 7144046b5c55f38cf9b3b7ec53e3263ebb01ed7caf46fe8758d6337c87686077009d53a3733c81375c2b5dfd4e7c51c14be84919d6e118198d35afd80965a52c 34 | ``` 35 | 36 | ## Mokka implementation 37 | 38 | As mokka is agnostic to protocol, we have to implement it, or take exciting one from implementations (in our case we will take TCP). 39 | Also we will need to install the a socket lib for messages exchange (used by TCP implementation): 40 | ```bash 41 | $ npm install axon --save 42 | ``` 43 | 44 | ## Cluster implementation 45 | 46 | Each node in cluster will represent the ganache instance with mokka's client. 47 | In order to make the ganache decentralized, we have to think about 3 things: 48 | 1) an ability to broadcast pending changes (like unconfirmed tx) 49 | 2) an ability to mine and broadcast the blocks 50 | 3) concurrency and sync issues 51 | 52 | 53 | 54 | In order to overcome the first problem, we will override the send method in ganache instance, so in case the node (which accepts the request) 55 | is follower, it rebroadcast the request to the leader node. This logic only touch send_transaction request. 56 | 57 | The second problem will be resolved thanks to mokka consensus engine: once the leader receive the pending transaction, it applies it and mint new block. 58 | This block will be broadcasted with mokka to all followers. Then each follower apply this block to its state. 59 | 60 | The third issue will be resolved thanks to previous two. 61 | 62 | 63 | First of all, we have to create the config, where we will specify 64 | all options for each node ``src/config.ts``: 65 | 66 | ```javascript 67 | export default { 68 | nodes: [ 69 | { 70 | balance: '10'.padEnd(20, '0'), 71 | ganache: 8545, 72 | port: 3000, 73 | publicKey: '0263920784223e0f5aa31a4bcdae945304c1c85df68064e9106ebfff1511221ee9', 74 | secretKey: '507b1433f58edd4eff3e87d9ba939c74bd15b4b10c00c548214817c0295c521a' 75 | }, 76 | { 77 | balance: '10'.padEnd(20, '0'), 78 | ganache: 8546, 79 | port: 3001, 80 | publicKey: '024ad0ef01d3d62b41b80a354a5b748524849c28f5989f414c4c174647137c2587', 81 | secretKey: '3cb2530ded6b8053fabf339c9e10e46ceb9ffc2064d535f53df59b8bf36289a1' 82 | }, 83 | { 84 | balance: '10'.padEnd(20, '0'), 85 | ganache: 8547, 86 | port: 3002, 87 | publicKey: '02683e65682deeb98738b44f8eb9d1840852e5635114c7c4ef2e39f20806b96dbf', 88 | secretKey: '1c954bd0ecc1b2c713b88678e48ff011e53d53fc496458063116a2e3a81883b8' 89 | } 90 | ] 91 | }; 92 | ``` 93 | 94 | 95 | 96 | Now we need a code, which will boot up the Mokka and ganache with certain params. 97 | Also, for demo purpose, we will write the logic, where mokka bootup peer based on first free port among specified in config. 98 | Todo that, we need to install a lib, which will detect if certain port is free: 99 | ```bash 100 | $ npm install detect-port --save 101 | ``` 102 | 103 | Now we need to write the cluster implementation ``src/server.ts`` 104 | 105 | ```javascript 106 | import bunyan from 'bunyan'; 107 | import detect = require('detect-port'); 108 | import ganache from 'ganache-core'; 109 | import Tx from 'ganache-core/lib/utils/transaction'; 110 | import Block from 'ganache-core/node_modules/ethereumjs-block'; 111 | import * as MokkaEvents from 'mokka/dist/consensus/constants/EventTypes'; 112 | import MessageTypes from 'mokka/dist/consensus/constants/MessageTypes'; 113 | import * as MokkaStates from 'mokka/dist/consensus/constants/NodeStates'; 114 | import NodeStates from 'mokka/dist/consensus/constants/NodeStates'; 115 | import {PacketModel} from 'mokka/dist/consensus/models/PacketModel'; 116 | import TCPMokka from 'mokka/dist/implementation/TCP'; 117 | import semaphore = require('semaphore'); 118 | import Web3 = require('web3'); 119 | import config from './config'; 120 | 121 | const logger = bunyan.createLogger({name: 'mokka.logger', level: 60}); 122 | const sem = semaphore(1); 123 | 124 | const logsStorage: Array<{ key: string, value: string }> = []; 125 | const knownPeersState = new Map(); 126 | 127 | class ExtendedPacketModel extends PacketModel { 128 | public logIndex: number; 129 | } 130 | 131 | const startGanache = async (node) => { 132 | 133 | const accounts = config.nodes.map((node) => ({ 134 | balance: node.balance, 135 | secretKey: `0x${node.secretKey.slice(0, 64)}` 136 | })); 137 | 138 | const server = ganache.server({ 139 | accounts, 140 | default_balance_ether: 500, 141 | network_id: 86, 142 | time: new Date('12-12-2018') 143 | }); 144 | 145 | await new Promise((res) => { 146 | server.listen(node.ganache, () => { 147 | console.log('started'); 148 | res(); 149 | }); 150 | }); 151 | 152 | return server; 153 | }; 154 | 155 | const startMokka = async (node, server) => { 156 | 157 | // @ts-ignore 158 | const web3 = new Web3(server.provider); 159 | 160 | const reqMiddleware = async (packet: ExtendedPacketModel): Promise => { 161 | knownPeersState.set(packet.publicKey, packet.logIndex); 162 | 163 | if ( 164 | packet.state === NodeStates.LEADER && 165 | packet.type === MessageTypes.ACK && 166 | packet.data && 167 | packet.logIndex > logsStorage.length) { 168 | 169 | sem.take(async () => { 170 | const block = new Block(Buffer.from(packet.data.value, 'hex')); 171 | block.transactions = block.transactions.map((tx) => new Tx(tx)); 172 | // @ts-ignore 173 | const replyPacket: ExtendedPacketModel = mokka.messageApi.packet(16); 174 | 175 | const savedBlock = await web3.eth.getBlock(packet.data.index); 176 | 177 | if (savedBlock) { 178 | replyPacket.logIndex = logsStorage.length; 179 | await mokka.messageApi.message(replyPacket, packet.publicKey); 180 | return sem.leave(); 181 | } 182 | 183 | await new Promise((res, rej) => { 184 | server.provider.manager.state.blockchain.processBlock( 185 | server.provider.manager.state.blockchain.vm, 186 | block, 187 | true, 188 | (err, data) => err ? rej(err) : res(data) 189 | ); 190 | }); 191 | 192 | logger.info(`new block added ${block.hash().toString('hex')}`); 193 | 194 | logsStorage.push(packet.data); 195 | replyPacket.logIndex = logsStorage.length; 196 | await mokka.messageApi.message(replyPacket, packet.publicKey); 197 | sem.leave(); 198 | }); 199 | } 200 | 201 | return packet; 202 | }; 203 | 204 | const resMiddleware = async (packet: ExtendedPacketModel, peerPublicKey: string): Promise => { 205 | packet.logIndex = logsStorage.length; 206 | const peerIndex = knownPeersState.get(peerPublicKey) || 0; 207 | 208 | if (mokka.state === NodeStates.LEADER && packet.type === MessageTypes.ACK && peerIndex < logsStorage.length) { 209 | packet.data = {...logsStorage[peerIndex], index: peerIndex + 1}; 210 | } 211 | 212 | return packet; 213 | }; 214 | 215 | const customVoteRule = async (packet: ExtendedPacketModel): Promise => { 216 | return packet.logIndex >= logsStorage.length; 217 | }; 218 | 219 | const mokka = new TCPMokka({ 220 | address: `tcp://127.0.0.1:${node.port}/${node.publicKey}`, 221 | customVoteRule, 222 | electionTimeout: 300, 223 | heartbeat: 200, 224 | logger, 225 | privateKey: node.secretKey, 226 | proofExpiration: 30000, 227 | reqMiddleware, 228 | resMiddleware 229 | }); 230 | mokka.on(MokkaEvents.default.STATE, () => { 231 | logger.info(`changed state ${mokka.state} with term ${mokka.term}`); 232 | }); 233 | 234 | config.nodes.filter((nodec) => nodec.publicKey !== node.publicKey).forEach((nodec) => { 235 | mokka.nodeApi.join(`tcp://127.0.0.1:${nodec.port}/${nodec.publicKey}`); 236 | }); 237 | 238 | await mokka.connect(); 239 | return mokka; 240 | }; 241 | 242 | const init = async () => { 243 | 244 | const allocated = await Promise.all( 245 | config.nodes.map(async (node) => 246 | node.ganache === await detect(node.ganache) 247 | ) 248 | ); 249 | 250 | const index = allocated.indexOf(true); 251 | 252 | if (index === -1) 253 | throw Error('all ports are busy'); 254 | 255 | const node = config.nodes[index]; 256 | 257 | const server = await startGanache(node); 258 | const mokka = await startMokka(node, server); 259 | 260 | server.provider.engine.on('rawBlock', async (blockJSON) => { 261 | 262 | const block: Block = await new Promise((res, rej) => { 263 | server.provider.manager.state.blockchain.getBlock(blockJSON.hash, (err, data) => err ? rej(data) : res(data)); 264 | }); 265 | 266 | if (mokka.state !== MokkaStates.default.LEADER) 267 | return; 268 | 269 | logsStorage.push({key: blockJSON.hash, value: block.serialize().toString('hex')}); 270 | }); 271 | 272 | const bound = server.provider.send; 273 | 274 | server.provider.send = async (payload, cb) => { 275 | 276 | if (mokka.state !== MokkaStates.default.LEADER && payload.method === 'eth_sendTransaction') { 277 | 278 | const node = config.nodes.find((node) => node.publicKey === mokka.leaderPublicKey); 279 | 280 | // @ts-ignore 281 | const web3 = new Web3(`http://localhost:${node.ganache}`); 282 | 283 | let hash; 284 | 285 | try { 286 | hash = await new Promise((res, rej) => 287 | web3.eth.sendTransaction(...payload.params, (err, result) => err ? rej(err) : res(result)) 288 | ); 289 | } catch (e) { 290 | return cb(e, null); 291 | } 292 | 293 | // await until tx will be processed 294 | await new Promise((res) => { 295 | const intervalPid = setInterval(async () => { 296 | 297 | const tx = await new Promise((res, rej) => 298 | server.provider.manager.eth_getTransactionByHash( 299 | hash, 300 | (err, result) => err ? rej(err) : res(result) 301 | ) 302 | ); 303 | 304 | if (tx) { 305 | clearInterval(intervalPid); 306 | res(); 307 | } 308 | 309 | }, 200); 310 | }); 311 | 312 | const reply = { 313 | id: payload.id, 314 | jsonrpc: payload.jsonrpc, 315 | result: hash 316 | }; 317 | 318 | return cb(null, reply); 319 | } 320 | 321 | return bound.call(server.provider, payload, cb); 322 | }; 323 | }; 324 | 325 | module.exports = init(); 326 | ``` 327 | 328 | 329 | ## Usage 330 | 331 | Now we can run out cluster. So, you have to open 3 terminals and type in each terminal the appropriate command: 332 | terminal 1: ```npm start``` 333 | terminal 2: ```npm start``` 334 | terminal 3: ```npm start``` 335 | 336 | Now you can connect to any node (i.e. ports 8545, 8546 or 8547) via web3, or geth client and start using the cluster. 337 | All source code is available under the current directory. 338 | In case, you are going to run the demo from mokka's repo, then first run: ```npm run build_dist``` for generating mokka dist folder. -------------------------------------------------------------------------------- /src/test/unit/bft/testSuite.ts: -------------------------------------------------------------------------------- 1 | import Promise from 'bluebird'; 2 | import bunyan from 'bunyan'; 3 | import { expect } from 'chai'; 4 | import crypto from 'crypto'; 5 | import MessageTypes from '../../../consensus/constants/MessageTypes'; 6 | import messageTypes from '../../../consensus/constants/MessageTypes'; 7 | import NodeStates from '../../../consensus/constants/NodeStates'; 8 | import states from '../../../consensus/constants/NodeStates'; 9 | import { VoteModel } from '../../../consensus/models/VoteModel'; 10 | import * as utils from '../../../consensus/utils/cryptoUtils'; 11 | import TCPMokka from '../../../implementation/TCP'; 12 | 13 | export function testSuite(ctx: any = {}, nodesCount: number) { 14 | 15 | beforeEach(async () => { 16 | 17 | ctx.keys = []; 18 | 19 | ctx.nodes = []; 20 | 21 | for (let i = 0; i < nodesCount; i++) { 22 | const node = crypto.createECDH('secp256k1'); 23 | node.generateKeys(); 24 | ctx.keys.push({ 25 | privateKey: node.getPrivateKey().toString('hex'), 26 | publicKey: node.getPublicKey('hex', 'compressed') 27 | }); 28 | } 29 | 30 | for (let index = 0; index < nodesCount; index++) { 31 | const instance = new TCPMokka({ 32 | address: `tcp://127.0.0.1:2000/${ ctx.keys[index].publicKey }`, 33 | electionTimeout: 100 * nodesCount, 34 | crashModel: 'BFT', 35 | heartbeat: 50, 36 | logger: bunyan.createLogger({ name: 'mokka.logger', level: 60 }), 37 | privateKey: ctx.keys[index].privateKey, 38 | proofExpiration: 5000 39 | }); 40 | 41 | for (let i = 0; i < nodesCount; i++) 42 | if (i !== index) 43 | instance.nodeApi.join(`tcp://127.0.0.1:${ 2000 + i }/${ ctx.keys[i].publicKey }`); 44 | 45 | ctx.nodes.push(instance); 46 | } 47 | 48 | }); 49 | 50 | it(`should choose new leader between 51% of nodes each time (to prevent network freeze attack)`, async () => { 51 | 52 | const quorumCount = Math.ceil(nodesCount / 2) + 1; 53 | const arbitraryLeaders = ctx.nodes.slice(quorumCount); 54 | const healthyNodes = ctx.nodes.slice(0, quorumCount); 55 | let currentTerm = 1; 56 | 57 | for (const arbitraryNode of arbitraryLeaders) { 58 | currentTerm += 1; 59 | await Promise.delay(arbitraryNode.heartbeat); 60 | arbitraryNode.setState(NodeStates.CANDIDATE, currentTerm, ''); 61 | 62 | for (const healthyNode of healthyNodes) { 63 | const packetVote = await arbitraryNode.messageApi.packet(MessageTypes.VOTE, { 64 | nonce: Date.now() 65 | }); 66 | 67 | const result = await healthyNode.requestProcessorService.voteApi.vote(packetVote); 68 | // tslint:disable-next-line:no-unused-expression 69 | expect(result.data.signature).to.not.be.undefined; 70 | healthyNode.setState(NodeStates.FOLLOWER, currentTerm, arbitraryNode.publicKey); 71 | } 72 | } 73 | 74 | for (const arbitraryNode of arbitraryLeaders) { 75 | currentTerm += 1; 76 | await Promise.delay(arbitraryNode.heartbeat); 77 | arbitraryNode.setState(NodeStates.CANDIDATE, currentTerm, ''); 78 | 79 | for (const healthyNode of healthyNodes) { 80 | const packetVote = await arbitraryNode.messageApi.packet(MessageTypes.VOTE, { 81 | nonce: Date.now() 82 | }); 83 | 84 | const result = await healthyNode.requestProcessorService.voteApi.vote(packetVote); 85 | // tslint:disable-next-line:no-unused-expression 86 | expect(result.data).to.be.null; 87 | } 88 | } 89 | 90 | for (const healthyNode of healthyNodes) { 91 | const restNodes = ctx.nodes.filter((n) => n.publicKey !== healthyNode.publicKey); 92 | currentTerm += 1; 93 | await Promise.delay(healthyNode.heartbeat); 94 | healthyNode.setState(NodeStates.CANDIDATE, currentTerm, ''); 95 | 96 | for (const node of restNodes) { 97 | const packetVote = await healthyNode.messageApi.packet(MessageTypes.VOTE, { 98 | nonce: Date.now() 99 | }); 100 | 101 | const result = await node.requestProcessorService.voteApi.vote(packetVote); 102 | // tslint:disable-next-line:no-unused-expression 103 | expect(result.data.signature).to.not.be.undefined; 104 | healthyNode.setState(NodeStates.FOLLOWER, currentTerm, healthyNode.publicKey); 105 | } 106 | } 107 | }); 108 | 109 | it(`should prevent leader drop through voting (to prevent network freeze attack)`, async () => { 110 | 111 | const leaderNode = ctx.nodes[0]; 112 | const arbitraryNode = ctx.nodes[1]; 113 | const followerNodes = ctx.nodes.slice(2); 114 | const currentTerm = 1; 115 | 116 | const nonce = Date.now(); 117 | leaderNode.setState(states.CANDIDATE, currentTerm, ''); 118 | 119 | const publicKeysRootForTerm = utils.buildPublicKeysRootForTerm( 120 | leaderNode.publicKeysRoot, 121 | leaderNode.term, 122 | nonce, 123 | leaderNode.publicKey); 124 | const vote = new VoteModel(nonce, leaderNode.term, publicKeysRootForTerm); 125 | 126 | for (const combination of leaderNode.publicKeysCombinationsInQuorum) { 127 | 128 | if (!combination.includes(leaderNode.publicKey)) { 129 | continue; 130 | } 131 | 132 | const sharedPublicKeyPartial = utils.buildSharedPublicKeyX( 133 | combination, 134 | leaderNode.term, 135 | nonce, 136 | publicKeysRootForTerm 137 | ); 138 | vote.publicKeyToCombinationMap.set(sharedPublicKeyPartial, combination); 139 | } 140 | 141 | leaderNode.setVote(vote); 142 | 143 | const selfVoteSignature = utils.buildPartialSignature( 144 | leaderNode.privateKey, 145 | leaderNode.term, 146 | nonce, 147 | publicKeysRootForTerm 148 | ); 149 | 150 | vote.repliesPublicKeyToSignatureMap.set(leaderNode.publicKey, selfVoteSignature); 151 | 152 | const packetVote = await leaderNode.messageApi.packet(MessageTypes.VOTE, { 153 | nonce 154 | }); 155 | 156 | for (const followerNode of followerNodes) { 157 | const result = await followerNode.requestProcessorService.voteApi.vote(packetVote); 158 | await leaderNode.requestProcessorService.voteApi.voted(result); 159 | // tslint:disable-next-line:no-unused-expression 160 | expect(result.data.signature).to.not.be.undefined; 161 | } 162 | 163 | expect(leaderNode.state).eq(NodeStates.LEADER); 164 | 165 | const ackPacket = leaderNode.messageApi.packet(messageTypes.ACK); 166 | 167 | for (const followerNode of followerNodes) { 168 | await followerNode.requestProcessorService.voteApi.validateAndApplyLeader(ackPacket); 169 | // tslint:disable-next-line:no-unused-expression 170 | expect(followerNode.leaderPublicKey).eq(leaderNode.publicKey); 171 | } 172 | 173 | arbitraryNode.setState(NodeStates.CANDIDATE, currentTerm + 1, ''); 174 | 175 | const arbitraryNonce = Date.now(); 176 | 177 | const packetArbitraryVote = await arbitraryNode.messageApi.packet(MessageTypes.VOTE, { 178 | nonce: arbitraryNonce 179 | }); 180 | 181 | for (const followerNode of followerNodes) { 182 | const ackPacket = leaderNode.messageApi.packet(messageTypes.ACK); 183 | 184 | const [result] = await Promise.all([ 185 | followerNode.requestProcessorService.voteApi.vote(packetArbitraryVote), 186 | await Promise.delay(10).then(() => followerNode.nodeApi.pingFromLeader(ackPacket)) 187 | ]); 188 | // tslint:disable-next-line:no-unused-expression 189 | expect(result.data).to.be.null; 190 | // tslint:disable-next-line:no-unused-expression 191 | expect(followerNode.term).eq(leaderNode.term); 192 | expect(followerNode.leaderPublicKey).eq(leaderNode.publicKey); 193 | } 194 | 195 | }); 196 | 197 | it(`should ignore votes for too high term (to prevent number overflow)`, async () => { 198 | 199 | const leaderNode = ctx.nodes[0]; 200 | const arbitraryNode = ctx.nodes[1]; 201 | const followerNodes = ctx.nodes.slice(2); 202 | const currentTerm = 1; 203 | 204 | const nonce = Date.now(); 205 | leaderNode.setState(states.CANDIDATE, currentTerm, ''); 206 | 207 | const publicKeysRootForTerm = utils.buildPublicKeysRootForTerm( 208 | leaderNode.publicKeysRoot, 209 | leaderNode.term, 210 | nonce, 211 | leaderNode.publicKey); 212 | const vote = new VoteModel(nonce, leaderNode.term, publicKeysRootForTerm); 213 | 214 | for (const combination of leaderNode.publicKeysCombinationsInQuorum) { 215 | 216 | if (!combination.includes(leaderNode.publicKey)) { 217 | continue; 218 | } 219 | 220 | const sharedPublicKeyPartial = utils.buildSharedPublicKeyX( 221 | combination, 222 | leaderNode.term, 223 | nonce, 224 | publicKeysRootForTerm 225 | ); 226 | vote.publicKeyToCombinationMap.set(sharedPublicKeyPartial, combination); 227 | } 228 | 229 | leaderNode.setVote(vote); 230 | 231 | const selfVoteSignature = utils.buildPartialSignature( 232 | leaderNode.privateKey, 233 | leaderNode.term, 234 | nonce, 235 | publicKeysRootForTerm 236 | ); 237 | 238 | vote.repliesPublicKeyToSignatureMap.set(leaderNode.publicKey, selfVoteSignature); 239 | 240 | const packetVote = await leaderNode.messageApi.packet(MessageTypes.VOTE, { 241 | nonce 242 | }); 243 | 244 | for (const followerNode of followerNodes) { 245 | const result = await followerNode.requestProcessorService.voteApi.vote(packetVote); 246 | await leaderNode.requestProcessorService.voteApi.voted(result); 247 | // tslint:disable-next-line:no-unused-expression 248 | expect(result.data.signature).to.not.be.undefined; 249 | } 250 | 251 | expect(leaderNode.state).eq(NodeStates.LEADER); 252 | 253 | const ackPacket = leaderNode.messageApi.packet(messageTypes.ACK); 254 | 255 | for (const followerNode of followerNodes) { 256 | await followerNode.requestProcessorService.voteApi.validateAndApplyLeader(ackPacket); 257 | // tslint:disable-next-line:no-unused-expression 258 | expect(followerNode.leaderPublicKey).eq(leaderNode.publicKey); 259 | } 260 | 261 | arbitraryNode.setState(NodeStates.CANDIDATE, currentTerm + Date.now(), ''); 262 | 263 | const arbitraryNonce = Date.now(); 264 | 265 | const packetArbitraryVote = await arbitraryNode.messageApi.packet(MessageTypes.VOTE, { 266 | nonce: arbitraryNonce 267 | }); 268 | 269 | for (const followerNode of followerNodes) { 270 | const result = await followerNode.requestProcessorService.voteApi.vote(packetArbitraryVote); 271 | // tslint:disable-next-line:no-unused-expression 272 | expect(result.data).to.be.null; 273 | } 274 | 275 | }); 276 | 277 | it(`should ignore fake leader`, async () => { 278 | 279 | const arbitraryNode = ctx.nodes[0]; 280 | const followerNodes = ctx.nodes.slice(1); 281 | const currentTerm = 1; 282 | 283 | const nonce = Date.now(); 284 | arbitraryNode.setState(states.CANDIDATE, currentTerm, ''); 285 | 286 | const publicKeysRootForTerm = utils.buildPublicKeysRootForTerm( 287 | arbitraryNode.publicKeysRoot, 288 | arbitraryNode.term, 289 | nonce, 290 | arbitraryNode.publicKey); 291 | const vote = new VoteModel(nonce, arbitraryNode.term, publicKeysRootForTerm); 292 | 293 | for (const combination of arbitraryNode.publicKeysCombinationsInQuorum) { 294 | 295 | if (!combination.includes(arbitraryNode.publicKey)) { 296 | continue; 297 | } 298 | 299 | const sharedPublicKeyPartial = utils.buildSharedPublicKeyX( 300 | combination, 301 | arbitraryNode.term, 302 | nonce, 303 | publicKeysRootForTerm 304 | ); 305 | vote.publicKeyToCombinationMap.set(sharedPublicKeyPartial, combination); 306 | } 307 | 308 | arbitraryNode.setVote(vote); 309 | 310 | const selfVoteSignature = utils.buildPartialSignature( 311 | arbitraryNode.privateKey, 312 | arbitraryNode.term, 313 | nonce, 314 | publicKeysRootForTerm 315 | ); 316 | 317 | const fakeSignature = `${ nonce }:${ vote.publicKeyToCombinationMap.keys()[0] }:${ selfVoteSignature }`; 318 | arbitraryNode.setState(states.LEADER, currentTerm, arbitraryNode.publicKey, fakeSignature, nonce); 319 | 320 | const ackPacket = arbitraryNode.messageApi.packet(messageTypes.ACK); 321 | 322 | for (const followerNode of followerNodes) { 323 | const packet = await followerNode.requestProcessorService.voteApi.validateAndApplyLeader(ackPacket); 324 | // tslint:disable-next-line:no-unused-expression 325 | expect(packet).to.be.null; 326 | } 327 | }); 328 | 329 | it(`should ignore fake votes`, async () => { 330 | 331 | const leaderNode = ctx.nodes[0]; 332 | const arbitraryNode = ctx.nodes[1]; 333 | const followerNodes = ctx.nodes.slice(2); 334 | const currentTerm = 1; 335 | 336 | const nonce = Date.now(); 337 | leaderNode.setState(states.CANDIDATE, currentTerm, ''); 338 | 339 | const publicKeysRootForTerm = utils.buildPublicKeysRootForTerm( 340 | leaderNode.publicKeysRoot, 341 | leaderNode.term, 342 | nonce, 343 | leaderNode.publicKey); 344 | const vote = new VoteModel(nonce, leaderNode.term, publicKeysRootForTerm); 345 | 346 | for (const combination of leaderNode.publicKeysCombinationsInQuorum) { 347 | 348 | if (!combination.includes(leaderNode.publicKey)) { 349 | continue; 350 | } 351 | 352 | const sharedPublicKeyPartial = utils.buildSharedPublicKeyX( 353 | combination, 354 | leaderNode.term, 355 | nonce, 356 | publicKeysRootForTerm 357 | ); 358 | vote.publicKeyToCombinationMap.set(sharedPublicKeyPartial, combination); 359 | } 360 | 361 | leaderNode.setVote(vote); 362 | 363 | const selfVoteSignature = utils.buildPartialSignature( 364 | leaderNode.privateKey, 365 | leaderNode.term, 366 | nonce, 367 | publicKeysRootForTerm 368 | ); 369 | 370 | vote.repliesPublicKeyToSignatureMap.set(leaderNode.publicKey, selfVoteSignature); 371 | 372 | const fakeSignature = utils.buildPartialSignature( 373 | arbitraryNode.privateKey, 374 | arbitraryNode.term, 375 | Date.now(), 376 | publicKeysRootForTerm 377 | ); 378 | 379 | const packetVoted = await arbitraryNode.messageApi.packet(MessageTypes.VOTED, { 380 | signature: fakeSignature 381 | }); 382 | 383 | await leaderNode.requestProcessorService.voteApi.voted(packetVoted); 384 | 385 | const hasVoteBeingCounted = leaderNode.vote.repliesPublicKeyToSignatureMap.has(arbitraryNode.publicKey); 386 | expect(hasVoteBeingCounted).eq(false); 387 | }); 388 | 389 | afterEach(async () => { 390 | await Promise.delay(1000); 391 | }); 392 | 393 | } 394 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | 633 | Copyright (C) 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published 637 | by the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . 662 | --------------------------------------------------------------------------------