From 05e89d84b45f8ac36c536c4cdc0e24e2facefd03 Mon Sep 17 00:00:00 2001 From: cgeek <cem.moreau@gmail.com> Date: Tue, 27 Jun 2017 13:44:57 +0200 Subject: [PATCH] [enh] #1022 Reintroduce duniter-prover into Duniter core --- README.md | 1 - app/modules/prover/index.js | 204 ++++++ app/modules/prover/lib/blockGenerator.js | 757 ++++++++++++++++++++++ app/modules/prover/lib/blockProver.js | 176 +++++ app/modules/prover/lib/constants.js | 20 + app/modules/prover/lib/engine.js | 47 ++ app/modules/prover/lib/permanentProver.js | 209 ++++++ app/modules/prover/lib/powCluster.js | 230 +++++++ app/modules/prover/lib/proof.js | 292 +++++++++ app/modules/prover/lib/prover.js | 44 ++ package.json | 2 - release/arch/windows/build.bat | 11 +- test/fast/prover/pow-1-cluster.js | 76 +++ test/fast/prover/pow-2-engine.js | 89 +++ test/fast/prover/pow-3-prover.js | 92 +++ test/integration/http_api.js | 2 +- test/integration/identity-expiry.js | 2 +- test/integration/identity-kicking.js | 2 +- test/integration/identity-test.js | 2 +- test/integration/proof-of-work.js | 2 +- test/integration/start_generate_blocks.js | 2 +- test/integration/tools/commit.js | 4 +- test/integration/tools/node.js | 4 +- test/integration/tools/toolbox.js | 6 +- yarn.lock | 51 +- 25 files changed, 2278 insertions(+), 49 deletions(-) create mode 100644 app/modules/prover/index.js create mode 100644 app/modules/prover/lib/blockGenerator.js create mode 100644 app/modules/prover/lib/blockProver.js create mode 100644 app/modules/prover/lib/constants.js create mode 100644 app/modules/prover/lib/engine.js create mode 100644 app/modules/prover/lib/permanentProver.js create mode 100644 app/modules/prover/lib/powCluster.js create mode 100644 app/modules/prover/lib/proof.js create mode 100644 app/modules/prover/lib/prover.js create mode 100644 test/fast/prover/pow-1-cluster.js create mode 100644 test/fast/prover/pow-2-engine.js create mode 100644 test/fast/prover/pow-3-prover.js diff --git a/README.md b/README.md index 0b387b757..6e4caa01f 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,6 @@ If you wish to participate/debate on Duniter, you can: Duniter is using modules on different git repositories: - [Common](https://github.com/duniter/duniter-common): commons tools for Duniter core and modules. - [Crawler](https://github.com/duniter/duniter-crawler): network crawler. -- [Prover](https://github.com/duniter/duniter-prover): handle Proof-of-Work. - [BMA API](https://github.com/duniter/duniter-bma): Basic Merkled API. - [Keypair](https://github.com/duniter/duniter-keypair): provide the cryptographic keypair. - [WotB](https://github.com/duniter/wotb): compute Web of Trust. diff --git a/app/modules/prover/index.js b/app/modules/prover/index.js new file mode 100644 index 000000000..f1079ab86 --- /dev/null +++ b/app/modules/prover/index.js @@ -0,0 +1,204 @@ +"use strict"; + +const co = require('co'); +const async = require('async'); +const contacter = require('duniter-crawler').duniter.methods.contacter; +const common = require('duniter-common'); +const constants = require('./lib/constants'); +const Prover = require('./lib/prover'); +const blockGenerator = require('./lib/blockGenerator'); +const blockProver = require('./lib/blockProver'); + +const Peer = common.document.Peer + +module.exports = { + + duniter: { + + /*********** Permanent prover **************/ + config: { + onLoading: (conf) => co(function*() { + if (conf.cpu === null || conf.cpu === undefined) { + conf.cpu = constants.DEFAULT_CPU; + } + conf.powSecurityRetryDelay = constants.POW_SECURITY_RETRY_DELAY; + conf.powMaxHandicap = constants.POW_MAXIMUM_ACCEPTABLE_HANDICAP; + }), + beforeSave: (conf) => co(function*() { + delete conf.powSecurityRetryDelay; + delete conf.powMaxHandicap; + }) + }, + + service: { + output: (server, conf, logger) => { + const generator = blockGenerator(server); + server.generatorGetJoinData = generator.getSinglePreJoinData.bind(generator) + server.generatorComputeNewCerts = generator.computeNewCerts.bind(generator) + server.generatorNewCertsToLinks = generator.newCertsToLinks.bind(generator) + return new Prover(server, conf, logger) + } + }, + + methods: { + hookServer: (server) => { + const generator = blockGenerator(server); + server.generatorGetJoinData = generator.getSinglePreJoinData.bind(generator) + server.generatorComputeNewCerts = generator.computeNewCerts.bind(generator) + server.generatorNewCertsToLinks = generator.newCertsToLinks.bind(generator) + }, + blockProver: blockProver, + prover: (server, conf, logger) => new Prover(server, conf, logger), + blockGenerator: (server, prover) => blockGenerator(server, prover), + generateTheNextBlock: (server, manualValues) => co(function*() { + const prover = blockProver(server); + const generator = blockGenerator(server, prover); + return generator.nextBlock(manualValues); + }), + generateAndProveTheNext: (server, block, trial, manualValues) => co(function*() { + const prover = blockProver(server); + const generator = blockGenerator(server, prover); + let res = yield generator.makeNextBlock(block, trial, manualValues); + return res + }) + }, + + /*********** CLI gen-next + gen-root **************/ + + cliOptions: [ + {value: '--show', desc: 'With gen-next or gen-root commands, displays the generated block.'}, + {value: '--check', desc: 'With gen-next: just check validity of generated block.'}, + {value: '--at <medianTime>', desc: 'With gen-next --show --check: allows to try in a future time.', parser: parseInt } + ], + + cli: [{ + name: 'gen-next [host] [port] [difficulty]', + desc: 'Tries to generate the next block of the blockchain.', + onDatabaseExecute: (server, conf, program, params) => co(function*() { + const host = params[0]; + const port = params[1]; + const difficulty = params[2]; + const generator = blockGenerator(server, null); + return generateAndSend(program, host, port, difficulty, server, () => generator.nextBlock); + }) + }, { + name: 'gen-root [host] [port] [difficulty]', + desc: 'Tries to generate the next block of the blockchain.', + preventIfRunning: true, + onDatabaseExecute: (server, conf, program, params) => co(function*() { + const host = params[0]; + const port = params[1]; + const difficulty = params[2]; + const generator = blockGenerator(server, null); + let toDelete, catched = true; + do { + try { + yield generateAndSend(program, host, port, difficulty, server, () => generator.nextBlock); + catched = false; + } catch (e) { + toDelete = yield server.dal.idtyDAL.query('SELECT * FROM idty i WHERE 5 > (SELECT count(*) from cert c where c.`to` = i.pubkey)'); + console.log('Deleting', toDelete.map(i => i.pubkey)); + yield server.dal.idtyDAL.exec('DELETE FROM idty WHERE pubkey IN (' + toDelete.map(i => "'" + i.pubkey + "'").join(',') + ')'); + yield server.dal.idtyDAL.exec('DELETE FROM cert WHERE `to` IN (' + toDelete.map(i => "'" + i.pubkey + "'").join(',') + ')'); + yield server.dal.idtyDAL.exec('DELETE FROM cert WHERE `from` IN (' + toDelete.map(i => "'" + i.pubkey + "'").join(',') + ')'); + } + } while (catched && toDelete.length); + console.log('Done'); + }) + }, { + name: 'gen-root-choose [host] [port] [difficulty]', + desc: 'Tries to generate root block, with choice of root members.', + preventIfRunning: true, + onDatabaseExecute: (server, conf, program, params, startServices, stopServices) => co(function*() { + const host = params[0]; + const port = params[1]; + const difficulty = params[2]; + if (!host) { + throw 'Host is required.'; + } + if (!port) { + throw 'Port is required.'; + } + if (!difficulty) { + throw 'Difficulty is required.'; + } + const generator = blockGenerator(server, null); + return generateAndSend(program, host, port, difficulty, server, () => generator.manualRoot); + }) + }] + } +} + +function generateAndSend(program, host, port, difficulty, server, getGenerationMethod) { + const logger = server.logger; + return new Promise((resolve, reject) => { + async.waterfall([ + function (next) { + const method = getGenerationMethod(server); + co(function*(){ + const simulationValues = {} + if (program.show && program.check) { + if (program.at && !isNaN(program.at)) { + simulationValues.medianTime = program.at + } + } + const block = yield method(null, simulationValues); + next(null, block); + }); + }, + function (block, next) { + if (program.check) { + block.time = block.medianTime; + program.show && console.log(block.getRawSigned()); + co(function*(){ + try { + const parsed = common.parsers.parseBlock.syncWrite(block.getRawSigned()); + yield server.BlockchainService.checkBlock(parsed, false); + logger.info('Acceptable block'); + next(); + } catch (e) { + next(e); + } + }); + } + else { + logger.debug('Block to be sent: %s', block.getRawInnerPart()); + async.waterfall([ + function (subNext) { + proveAndSend(program, server, block, server.conf.pair.pub, parseInt(difficulty), host, parseInt(port), subNext); + } + ], next); + } + } + ], (err, data) => { + err && reject(err); + !err && resolve(data); + }); + }); +} + +function proveAndSend(program, server, block, issuer, difficulty, host, port, done) { + const logger = server.logger; + async.waterfall([ + function (next) { + block.issuer = issuer; + program.show && console.log(block.getRawSigned()); + co(function*(){ + try { + const prover = blockProver(server); + const proven = yield prover.prove(block, difficulty); + const peer = Peer.fromJSON({ + endpoints: [['BASIC_MERKLED_API', host, port].join(' ')] + }); + program.show && console.log(proven.getRawSigned()); + logger.info('Posted block ' + proven.getRawSigned()); + const p = Peer.fromJSON(peer); + const contact = contacter(p.getHostPreferDNS(), p.getPort()); + yield contact.postBlock(proven.getRawSigned()); + } catch(e) { + next(e); + } + }); + } + ], done); +} diff --git a/app/modules/prover/lib/blockGenerator.js b/app/modules/prover/lib/blockGenerator.js new file mode 100644 index 000000000..4549c2249 --- /dev/null +++ b/app/modules/prover/lib/blockGenerator.js @@ -0,0 +1,757 @@ +"use strict"; +const _ = require('underscore'); +const co = require('co'); +const moment = require('moment'); +const inquirer = require('inquirer'); +const common = require('duniter-common'); + +const keyring = common.keyring; +const hashf = common.hashf; +const rawer = common.rawer; +const Block = common.document.Block; +const Membership = common.document.Membership; +const Transaction = common.document.Transaction; +const Identity = common.document.Identity; +const Certification = common.document.Certification; +const constants = common.constants +const indexer = common.indexer +const rules = common.rules + +module.exports = (server, prover) => { + return new BlockGenerator(server, prover); +}; + +function BlockGenerator(server, prover) { + + const that = this; + const conf = server.conf; + const dal = server.dal; + const mainContext = server.BlockchainService.getContext(); + const selfPubkey = conf.pair.pub; + const logger = server.logger; + + this.nextBlock = (manualValues, simulationValues) => generateNextBlock(new NextBlockGenerator(mainContext, conf, dal, logger), manualValues, simulationValues); + + this.manualRoot = () => co(function *() { + let current = yield dal.getCurrentBlockOrNull(); + if (current) { + throw 'Cannot generate root block: it already exists.'; + } + return generateNextBlock(new ManualRootGenerator()); + }); + + this.makeNextBlock = (block, trial, manualValues) => co(function *() { + const unsignedBlock = block || (yield that.nextBlock(manualValues)); + const trialLevel = trial || (yield mainContext.getIssuerPersonalizedDifficulty(selfPubkey)); + return prover.prove(unsignedBlock, trialLevel, (manualValues && manualValues.time) || null); + }); + + /** + * Generate next block, gathering both updates & newcomers + */ + const generateNextBlock = (generator, manualValues, simulationValues) => co(function *() { + const vHEAD_1 = yield mainContext.getvHEAD_1() + if (simulationValues && simulationValues.medianTime) { + vHEAD_1.medianTime = simulationValues.medianTime + } + const current = yield dal.getCurrentBlockOrNull(); + const revocations = yield dal.getRevocatingMembers(); + const exclusions = yield dal.getToBeKickedPubkeys(); + const newCertsFromWoT = yield generator.findNewCertsFromWoT(current); + const newcomersLeavers = yield findNewcomersAndLeavers(current, generator.filterJoiners); + const transactions = yield findTransactions(current); + const joinData = newcomersLeavers[2]; + const leaveData = newcomersLeavers[3]; + const newCertsFromNewcomers = newcomersLeavers[4]; + const certifiersOfNewcomers = _.uniq(_.keys(joinData).reduce((theCertifiers, newcomer) => { + return theCertifiers.concat(_.pluck(joinData[newcomer].certs, 'from')); + }, [])); + const certifiers = [].concat(certifiersOfNewcomers); + // Merges updates + _(newCertsFromWoT).keys().forEach(function(certified){ + newCertsFromWoT[certified] = newCertsFromWoT[certified].filter((cert) => { + // Must not certify a newcomer, since it would mean multiple certifications at same time from one member + const isCertifier = certifiers.indexOf(cert.from) != -1; + if (!isCertifier) { + certifiers.push(cert.from); + } + return !isCertifier; + }); + }); + _(newCertsFromNewcomers).keys().forEach((certified) => { + newCertsFromWoT[certified] = (newCertsFromWoT[certified] || []).concat(newCertsFromNewcomers[certified]); + }); + // Revocations + // Create the block + return createBlock(current, joinData, leaveData, newCertsFromWoT, revocations, exclusions, transactions, manualValues); + }); + + const findNewcomersAndLeavers = (current, filteringFunc) => co(function*() { + const newcomers = yield findNewcomers(current, filteringFunc); + const leavers = yield findLeavers(current); + + const cur = newcomers.current; + const newWoTMembers = newcomers.newWotMembers; + const finalJoinData = newcomers.finalJoinData; + const updates = newcomers.updates; + + return [cur, newWoTMembers, finalJoinData, leavers, updates]; + }); + + const findTransactions = (current) => co(function*() { + const versionMin = current ? Math.min(common.constants.LAST_VERSION_FOR_TX, current.version) : common.constants.DOCUMENTS_VERSION; + const txs = yield dal.getTransactionsPending(versionMin); + const transactions = []; + const passingTxs = []; + for (const obj of txs) { + obj.currency = conf.currency + const tx = Transaction.fromJSON(obj); + try { + yield new Promise((resolve, reject) => { + rules.HELPERS.checkBunchOfTransactions(passingTxs.concat(tx), (err, res) => { + if (err) return reject(err) + return resolve(res) + }) + }) + const nextBlockWithFakeTimeVariation = { medianTime: current.medianTime + 1 }; + yield rules.HELPERS.checkSingleTransaction(tx, nextBlockWithFakeTimeVariation, conf, dal); + yield rules.HELPERS.checkTxBlockStamp(tx, dal); + transactions.push(tx); + passingTxs.push(tx); + logger.info('Transaction %s added to block', tx.hash); + } catch (err) { + logger.error(err); + const currentNumber = (current && current.number) || 0; + const blockstamp = tx.blockstamp || (currentNumber + '-'); + const txBlockNumber = parseInt(blockstamp.split('-')[0]); + // 10 blocks before removing the transaction + if (currentNumber - txBlockNumber + 1 >= common.constants.TRANSACTION_MAX_TRIES) { + yield dal.removeTxByHash(tx.hash); + } + } + } + return transactions; + }); + + const findLeavers = (current) => co(function*() { + const leaveData = {}; + const memberships = yield dal.findLeavers(); + const leavers = []; + memberships.forEach((ms) => leavers.push(ms.issuer)); + for (const ms of memberships) { + const leave = { identity: null, ms: ms, key: null, idHash: '' }; + leave.idHash = (hashf(ms.userid + ms.certts + ms.issuer) + "").toUpperCase(); + let block; + if (current) { + block = yield dal.getBlock(ms.number); + } + else { + block = {}; + } + const identity = yield dal.getIdentityByHashOrNull(leave.idHash); + const currentMembership = yield dal.mindexDAL.getReducedMS(ms.issuer); + const currentMSN = currentMembership ? parseInt(currentMembership.created_on) : -1; + if (identity && block && currentMSN < leave.ms.number && identity.member) { + // MS + matching cert are found + leave.identity = identity; + leaveData[identity.pubkey] = leave; + } + } + return leaveData; + }); + + const findNewcomers = (current, filteringFunc) => co(function*() { + const updates = {}; + const preJoinData = yield getPreJoinData(current); + const joinData = yield filteringFunc(preJoinData); + const members = yield dal.getMembers(); + const wotMembers = _.pluck(members, 'pubkey'); + // Checking step + const newcomers = _(joinData).keys(); + const nextBlockNumber = current ? current.number + 1 : 0; + try { + const realNewcomers = yield iteratedChecking(newcomers, (someNewcomers) => co(function*() { + const nextBlock = { + number: nextBlockNumber, + joiners: someNewcomers, + identities: _.filter(newcomers.map((pub) => joinData[pub].identity), { wasMember: false }).map((idty) => idty.pubkey) + }; + const theNewLinks = yield computeNewLinks(nextBlockNumber, someNewcomers, joinData, updates); + yield checkWoTConstraints(nextBlock, theNewLinks, current); + })); + const newLinks = yield computeNewLinks(nextBlockNumber, realNewcomers, joinData, updates); + const newWoT = wotMembers.concat(realNewcomers); + const finalJoinData = {}; + realNewcomers.forEach((newcomer) => { + // Only keep membership of selected newcomers + finalJoinData[newcomer] = joinData[newcomer]; + // Only keep certifications from final members + const keptCerts = []; + joinData[newcomer].certs.forEach((cert) => { + const issuer = cert.from; + if (~newWoT.indexOf(issuer) && ~newLinks[cert.to].indexOf(issuer)) { + keptCerts.push(cert); + } + }); + joinData[newcomer].certs = keptCerts; + }); + return { + current: current, + newWotMembers: wotMembers.concat(realNewcomers), + finalJoinData: finalJoinData, + updates: updates + } + } catch(err) { + logger.error(err); + throw err; + } + }); + + const checkWoTConstraints = (block, newLinks, current) => co(function*() { + if (block.number < 0) { + throw 'Cannot compute WoT constraint for negative block number'; + } + const newcomers = block.joiners.map((inlineMS) => inlineMS.split(':')[0]); + const realNewcomers = block.identities; + for (const newcomer of newcomers) { + if (block.number > 0) { + try { + // Will throw an error if not enough links + yield mainContext.checkHaveEnoughLinks(newcomer, newLinks); + // This one does not throw but returns a boolean + const isOut = yield rules.HELPERS.isOver3Hops(newcomer, newLinks, realNewcomers, current, conf, dal); + if (isOut) { + throw 'Key ' + newcomer + ' is not recognized by the WoT for this block'; + } + } catch (e) { + logger.debug(e); + throw e; + } + } + } + }); + + const iteratedChecking = (newcomers, checkWoTForNewcomers) => co(function*() { + const passingNewcomers = []; + let hadError = false; + for (const newcomer of newcomers) { + try { + yield checkWoTForNewcomers(passingNewcomers.concat(newcomer)); + passingNewcomers.push(newcomer); + } catch (err) { + hadError = hadError || err; + } + } + if (hadError) { + return yield iteratedChecking(passingNewcomers, checkWoTForNewcomers); + } else { + return passingNewcomers; + } + }); + + const getPreJoinData = (current) => co(function*() { + const preJoinData = {}; + const memberships = yield dal.findNewcomers(current && current.medianTime) + const joiners = []; + memberships.forEach((ms) =>joiners.push(ms.issuer)); + for (const ms of memberships) { + try { + if (ms.block !== common.constants.SPECIAL_BLOCK) { + let msBasedBlock = yield dal.getBlockByBlockstampOrNull(ms.block); + if (!msBasedBlock) { + throw constants.ERRORS.BLOCKSTAMP_DOES_NOT_MATCH_A_BLOCK; + } + let age = current.medianTime - msBasedBlock.medianTime; + if (age > conf.msWindow) { + throw constants.ERRORS.TOO_OLD_MEMBERSHIP; + } + } + const idtyHash = (hashf(ms.userid + ms.certts + ms.issuer) + "").toUpperCase(); + const join = yield that.getSinglePreJoinData(current, idtyHash, joiners); + join.ms = ms; + const currentMembership = yield dal.mindexDAL.getReducedMS(ms.issuer); + const currentMSN = currentMembership ? parseInt(currentMembership.created_on) : -1; + if (!join.identity.revoked && currentMSN < parseInt(join.ms.number)) { + preJoinData[join.identity.pubkey] = join; + } + } catch (err) { + if (err && !err.uerr) { + logger.warn(err); + } + } + } + return preJoinData; + }); + + const computeNewLinks = (forBlock, theNewcomers, joinData, updates) => co(function *() { + let newCerts = yield that.computeNewCerts(forBlock, theNewcomers, joinData); + return that.newCertsToLinks(newCerts, updates); + }); + + this.newCertsToLinks = (newCerts, updates) => { + let newLinks = {}; + _.mapObject(newCerts, function(certs, pubkey) { + newLinks[pubkey] = _.pluck(certs, 'from'); + }); + _.mapObject(updates, function(certs, pubkey) { + newLinks[pubkey] = (newLinks[pubkey] || []).concat(_.pluck(certs, 'pubkey')); + }); + return newLinks; + }; + + this.computeNewCerts = (forBlock, theNewcomers, joinData) => co(function *() { + const newCerts = {}, certifiers = []; + const certsByKey = _.mapObject(joinData, function(val){ return val.certs; }); + for (const newcomer of theNewcomers) { + // New array of certifiers + newCerts[newcomer] = newCerts[newcomer] || []; + // Check wether each certification of the block is from valid newcomer/member + for (const cert of certsByKey[newcomer]) { + const isAlreadyCertifying = certifiers.indexOf(cert.from) !== -1; + if (!(isAlreadyCertifying && forBlock > 0)) { + if (~theNewcomers.indexOf(cert.from)) { + // Newcomer to newcomer => valid link + newCerts[newcomer].push(cert); + certifiers.push(cert.from); + } else { + let isMember = yield dal.isMember(cert.from); + // Member to newcomer => valid link + if (isMember) { + newCerts[newcomer].push(cert); + certifiers.push(cert.from); + } + } + } + } + } + return newCerts; + }); + + this.getSinglePreJoinData = (current, idHash, joiners) => co(function *() { + const identity = yield dal.getIdentityByHashOrNull(idHash); + let foundCerts = []; + const vHEAD_1 = yield mainContext.getvHEAD_1(); + if (!identity) { + throw 'Identity with hash \'' + idHash + '\' not found'; + } + if (current && identity.buid == common.constants.SPECIAL_BLOCK && !identity.wasMember) { + throw constants.ERRORS.TOO_OLD_IDENTITY; + } + else if (!identity.wasMember && identity.buid != common.constants.SPECIAL_BLOCK) { + const idtyBasedBlock = yield dal.getBlock(identity.buid); + const age = current.medianTime - idtyBasedBlock.medianTime; + if (age > conf.idtyWindow) { + throw constants.ERRORS.TOO_OLD_IDENTITY; + } + } + const idty = Identity.fromJSON(identity); + idty.currency = conf.currency; + const createIdentity = idty.rawWithoutSig(); + const verified = keyring.verify(createIdentity, idty.sig, idty.pubkey); + if (!verified) { + throw constants.ERRORS.IDENTITY_WRONGLY_SIGNED; + } + const isIdentityLeaving = yield dal.isLeaving(idty.pubkey); + if (!isIdentityLeaving) { + if (!current) { + // Look for certifications from initial joiners + const certs = yield dal.certsNotLinkedToTarget(idHash); + foundCerts = _.filter(certs, function(cert){ + // Add 'joiners && ': special case when block#0 not written ANd not joiner yet (avoid undefined error) + return joiners && ~joiners.indexOf(cert.from); + }); + } else { + // Look for certifications from WoT members + let certs = yield dal.certsNotLinkedToTarget(idHash); + const certifiers = []; + for (const cert of certs) { + try { + const basedBlock = yield dal.getBlock(cert.block_number); + if (!basedBlock) { + throw 'Unknown timestamp block for identity'; + } + if (current) { + const age = current.medianTime - basedBlock.medianTime; + if (age > conf.sigWindow || age > conf.sigValidity) { + throw 'Too old certification'; + } + } + // Already exists a link not replayable yet? + let exists = yield dal.existsNonReplayableLink(cert.from, cert.to); + if (exists) { + throw 'It already exists a similar certification written, which is not replayable yet'; + } + // Already exists a link not chainable yet? + exists = yield dal.existsNonChainableLink(cert.from, vHEAD_1, conf.sigStock); + if (exists) { + throw 'It already exists a written certification from ' + cert.from + ' which is not chainable yet'; + } + const isMember = yield dal.isMember(cert.from); + const doubleSignature = ~certifiers.indexOf(cert.from) ? true : false; + if (isMember && !doubleSignature) { + const isValid = yield rules.HELPERS.checkCertificationIsValidForBlock(cert, { number: current.number + 1, currency: current.currency }, identity, conf, dal); + if (isValid) { + certifiers.push(cert.from); + foundCerts.push(cert); + } + } + } catch (e) { + logger.debug(e.stack || e.message || e); + // Go on + } + } + } + } + return { + identity: identity, + key: null, + idHash: idHash, + certs: foundCerts + }; + }); + + const createBlock = (current, joinData, leaveData, updates, revocations, exclusions, transactions, manualValues) => { + return co(function *() { + + if (manualValues && manualValues.excluded) { + exclusions = manualValues.excluded; + } + if (manualValues && manualValues.revoked) { + revocations = []; + } + + const vHEAD = yield mainContext.getvHeadCopy(); + const vHEAD_1 = yield mainContext.getvHEAD_1(); + const maxLenOfBlock = indexer.DUP_HELPERS.getMaxBlockSize(vHEAD); + let blockLen = 0; + // Revocations have an impact on exclusions + revocations.forEach((idty) => exclusions.push(idty.pubkey)); + // Prevent writing joins/updates for excluded members + exclusions = _.uniq(exclusions); + exclusions.forEach((excluded) => { + delete updates[excluded]; + delete joinData[excluded]; + delete leaveData[excluded]; + }); + _(leaveData).keys().forEach((leaver) => { + delete updates[leaver]; + delete joinData[leaver]; + }); + const block = new Block(); + block.number = current ? current.number + 1 : 0; + // Compute the new MedianTime + if (block.number == 0) { + block.medianTime = moment.utc().unix() - conf.rootoffset; + } + else { + block.medianTime = vHEAD.medianTime; + } + // Choose the version + block.version = (manualValues && manualValues.version) || (yield rules.HELPERS.getMaxPossibleVersionNumber(current)); + block.currency = current ? current.currency : conf.currency; + block.nonce = 0; + if (!conf.dtReeval) { + conf.dtReeval = conf.dt; + } + if (!conf.udTime0) { + conf.udTime0 = block.medianTime + conf.dt; + } + if (!conf.udReevalTime0) { + conf.udReevalTime0 = block.medianTime + conf.dtReeval; + } + block.parameters = block.number > 0 ? '' : [ + conf.c, conf.dt, conf.ud0, + conf.sigPeriod, conf.sigStock, conf.sigWindow, conf.sigValidity, + conf.sigQty, conf.idtyWindow, conf.msWindow, conf.xpercent, conf.msValidity, + conf.stepMax, conf.medianTimeBlocks, conf.avgGenTime, conf.dtDiffEval, + (conf.percentRot == 1 ? "1.0" : conf.percentRot), + conf.udTime0, + conf.udReevalTime0, + conf.dtReeval + ].join(':'); + block.previousHash = current ? current.hash : ""; + block.previousIssuer = current ? current.issuer : ""; + if (selfPubkey) + block.issuer = selfPubkey; + // Members merkle + const joiners = _(joinData).keys(); + joiners.sort() + const previousCount = current ? current.membersCount : 0; + if (joiners.length == 0 && !current) { + throw constants.ERRORS.CANNOT_ROOT_BLOCK_NO_MEMBERS; + } + + // Kicked people + block.excluded = exclusions; + + /***** + * Priority 1: keep the WoT sane + */ + // Certifications from the WoT, to the WoT + _(updates).keys().forEach((certifiedMember) => { + const certs = updates[certifiedMember] || []; + certs.forEach((cert) => { + if (blockLen < maxLenOfBlock) { + block.certifications.push(Certification.fromJSON(cert).inline()); + blockLen++; + } + }); + }); + // Renewed + joiners.forEach((joiner) => { + const data = joinData[joiner]; + // Join only for non-members + if (data.identity.member) { + if (blockLen < maxLenOfBlock) { + block.actives.push(Membership.fromJSON(data.ms).inline()); + blockLen++; + } + } + }); + // Leavers + const leavers = _(leaveData).keys(); + leavers.forEach((leaver) => { + const data = leaveData[leaver]; + // Join only for non-members + if (data.identity.member) { + if (blockLen < maxLenOfBlock) { + block.leavers.push(Membership.fromJSON(data.ms).inline()); + blockLen++; + } + } + }); + + /***** + * Priority 2: revoked identities + */ + revocations.forEach((idty) => { + if (blockLen < maxLenOfBlock) { + block.revoked.push([idty.pubkey, idty.revocation_sig].join(':')); + blockLen++; + } + }); + + /***** + * Priority 3: newcomers/renewcomers + */ + let countOfCertsToNewcomers = 0; + // Newcomers + // Newcomers + back people + joiners.forEach((joiner) => { + const data = joinData[joiner]; + // Identities only for never-have-been members + if (!data.identity.member && !data.identity.wasMember) { + block.identities.push(Identity.fromJSON(data.identity).inline()); + } + // Join only for non-members + if (!data.identity.member) { + block.joiners.push(Membership.fromJSON(data.ms).inline()); + } + }); + block.identities = _.sortBy(block.identities, (line) => { + const sp = line.split(':'); + return sp[2] + sp[3]; + }); + + // Certifications from the WoT, to newcomers + joiners.forEach((joiner) => { + const data = joinData[joiner] || []; + data.certs.forEach((cert) => { + countOfCertsToNewcomers++; + block.certifications.push(Certification.fromJSON(cert).inline()); + }); + }); + + // Eventually revert newcomers/renewcomer + if (block.number > 0 && Block.getLen(block) > maxLenOfBlock) { + for (let i = 0; i < block.identities.length; i++) { + block.identities.pop(); + block.joiners.pop(); + } + for (let i = 0; i < countOfCertsToNewcomers; i++) { + block.certifications.pop(); + } + } + + // Final number of members + block.membersCount = previousCount + block.joiners.length - block.excluded.length; + + vHEAD.membersCount = block.membersCount; + + /***** + * Priority 4: transactions + */ + block.transactions = []; + blockLen = Block.getLen(block); + if (blockLen < maxLenOfBlock) { + transactions.forEach((tx) => { + const txLen = Transaction.getLen(tx); + if (txLen <= common.constants.MAXIMUM_LEN_OF_COMPACT_TX && blockLen + txLen <= maxLenOfBlock && tx.version == common.constants.TRANSACTION_VERSION) { + block.transactions.push({ raw: tx.compact() }); + } + blockLen += txLen; + }); + } + + /** + * Finally handle the Universal Dividend + */ + block.powMin = vHEAD.powMin; + + // Universal Dividend + if (vHEAD.new_dividend) { + + // BR_G13 + // Recompute according to block.membersCount + indexer.prepareDividend(vHEAD, vHEAD_1, conf); + // BR_G14 + indexer.prepareUnitBase(vHEAD, vHEAD_1, conf); + + // Fix BR_G14 double call + vHEAD.unitBase = Math.min(vHEAD_1.unitBase + 1, vHEAD.unitBase); + + block.dividend = vHEAD.dividend; + block.unitbase = vHEAD.unitBase; + } else { + block.unitbase = block.number == 0 ? 0 : current.unitbase; + } + // Rotation + block.issuersCount = vHEAD.issuersCount; + block.issuersFrame = vHEAD.issuersFrame; + block.issuersFrameVar = vHEAD.issuersFrameVar; + // Manual values before hashing + if (manualValues) { + _.extend(block, _.omit(manualValues, 'time')); + } + // InnerHash + block.time = block.medianTime; + block.inner_hash = hashf(rawer.getBlockInnerPart(block)).toUpperCase(); + return block; + }); + } +} + +/** + * Class to implement strategy of automatic selection of incoming data for next block. + * @constructor + */ +function NextBlockGenerator(mainContext, conf, dal, logger) { + + this.findNewCertsFromWoT = (current) => co(function *() { + const updates = {}; + const updatesToFrom = {}; + const certs = yield dal.certsFindNew(); + const vHEAD_1 = yield mainContext.getvHEAD_1(); + for (const cert of certs) { + const targetIdty = yield dal.getIdentityByHashOrNull(cert.target); + // The identity must be known + if (targetIdty) { + const certSig = cert.sig; + // Do not rely on certification block UID, prefer using the known hash of the block by its given number + const targetBlock = yield dal.getBlock(cert.block_number); + // Check if writable + let duration = current && targetBlock ? current.medianTime - parseInt(targetBlock.medianTime) : 0; + if (targetBlock && duration <= conf.sigWindow) { + cert.sig = ''; + cert.currency = conf.currency; + cert.issuer = cert.from; + cert.idty_issuer = targetIdty.pubkey; + cert.idty_uid = targetIdty.uid; + cert.idty_buid = targetIdty.buid; + cert.idty_sig = targetIdty.sig; + cert.buid = current ? [cert.block_number, targetBlock.hash].join('-') : common.constants.SPECIAL_BLOCK; + const rawCert = Certification.fromJSON(cert).getRaw(); + if (keyring.verify(rawCert, certSig, cert.from)) { + cert.sig = certSig; + let exists = false; + if (current) { + // Already exists a link not replayable yet? + exists = yield dal.existsNonReplayableLink(cert.from, cert.to); + } + if (!exists) { + // Already exists a link not chainable yet? + // No chainability block means absolutely nobody can issue certifications yet + exists = yield dal.existsNonChainableLink(cert.from, vHEAD_1, conf.sigStock); + if (!exists) { + // It does NOT already exists a similar certification written, which is not replayable yet + // Signatory must be a member + const isSignatoryAMember = yield dal.isMember(cert.from); + const isCertifiedANonLeavingMember = isSignatoryAMember && (yield dal.isMemberAndNonLeaver(cert.to)); + // Certified must be a member and non-leaver + if (isSignatoryAMember && isCertifiedANonLeavingMember) { + updatesToFrom[cert.to] = updatesToFrom[cert.to] || []; + updates[cert.to] = updates[cert.to] || []; + if (updatesToFrom[cert.to].indexOf(cert.from) == -1) { + updates[cert.to].push(cert); + updatesToFrom[cert.to].push(cert.from); + } + } + } + } + } + } + } + } + return updates; + }); + + this.filterJoiners = (preJoinData) => co(function*() { + const filtered = {}; + const filterings = []; + const filter = (pubkey) => co(function*() { + try { + // No manual filtering, takes all BUT already used UID or pubkey + let exists = yield rules.HELPERS.checkExistsUserID(preJoinData[pubkey].identity.uid, dal); + if (exists && !preJoinData[pubkey].identity.wasMember) { + throw 'UID already taken'; + } + exists = yield rules.HELPERS.checkExistsPubkey(pubkey, dal); + if (exists && !preJoinData[pubkey].identity.wasMember) { + throw 'Pubkey already taken'; + } + filtered[pubkey] = preJoinData[pubkey]; + } + catch (err) { + logger.warn(err); + } + }); + _.keys(preJoinData).forEach( (joinPubkey) => filterings.push(filter(joinPubkey))); + yield filterings; + return filtered; + }); +} + +/** + * Class to implement strategy of manual selection of root members for root block. + * @constructor + */ +function ManualRootGenerator() { + + this.findNewCertsFromWoT = () => Promise.resolve({}); + + this.filterJoiners = (preJoinData) => co(function*() { + const filtered = {}; + const newcomers = _(preJoinData).keys(); + const uids = []; + newcomers.forEach((newcomer) => uids.push(preJoinData[newcomer].ms.userid)); + + if (newcomers.length > 0) { + const answers = yield inquirer.prompt([{ + type: "checkbox", + name: "uids", + message: "Newcomers to add", + choices: uids, + default: uids[0] + }]); + newcomers.forEach((newcomer) => { + if (~answers.uids.indexOf(preJoinData[newcomer].ms.userid)) + filtered[newcomer] = preJoinData[newcomer]; + }); + if (answers.uids.length == 0) + throw 'No newcomer selected'; + return filtered + } else { + throw 'No newcomer found'; + } + }); +} diff --git a/app/modules/prover/lib/blockProver.js b/app/modules/prover/lib/blockProver.js new file mode 100644 index 000000000..c01019626 --- /dev/null +++ b/app/modules/prover/lib/blockProver.js @@ -0,0 +1,176 @@ +"use strict"; +const co = require('co'); +const engine = require('./engine'); +const querablep = require('querablep'); +const common = require('duniter-common'); +const constants = require('./constants'); + +const Block = common.document.Block + +const POW_FOUND = true; +const POW_NOT_FOUND_YET = false; + +module.exports = (server) => new BlockProver(server); + +function BlockProver(server) { + + let conf = server.conf; + let pair = conf.pair; + let logger = server.logger; + let waitResolve; + + let workerFarmPromise; + + function getWorker() { + return (workerFarmPromise || (workerFarmPromise = co(function*() { + return new WorkerFarm(); + }))); + } + + const debug = process.execArgv.toString().indexOf('--debug') !== -1; + if(debug) { + //Set an unused port number. + process.execArgv = []; + } + + this.cancel = (gottenBlock) => co(function*() { + // If no farm was instanciated, there is nothing to do yet + if (workerFarmPromise) { + let farm = yield getWorker(); + if (farm.isComputing() && !farm.isStopping()) { + yield farm.stopPoW(gottenBlock); + } + if (waitResolve) { + waitResolve(); + waitResolve = null; + } + } + }); + + this.prove = function (block, difficulty, forcedTime) { + + if (waitResolve) { + waitResolve(); + waitResolve = null; + } + + const remainder = difficulty % 16; + const nbZeros = (difficulty - remainder) / 16; + const highMark = common.constants.PROOF_OF_WORK.UPPER_BOUND[remainder]; + + return co(function*() { + + let powFarm = yield getWorker(); + + if (block.number == 0) { + // On initial block, difficulty is the one given manually + block.powMin = difficulty; + } + + // Start + powFarm.setOnAlmostPoW(function(pow, matches, aBlock, found) { + powEvent(found, pow); + if (matches && matches[1].length >= constants.MINIMAL_ZEROS_TO_SHOW_IN_LOGS) { + logger.info('Matched %s zeros %s with Nonce = %s for block#%s by %s', matches[1].length, pow, aBlock.nonce, aBlock.number, aBlock.issuer.slice(0,6)); + } + }); + + block.nonce = 0; + logger.info('Generating proof-of-work with %s leading zeros followed by [0-' + highMark + ']... (CPU usage set to %s%) for block#%s', nbZeros, (conf.cpu * 100).toFixed(0), block.number, block.issuer.slice(0,6)); + const start = Date.now(); + let result = yield powFarm.askNewProof({ + newPoW: { conf: conf, block: block, zeros: nbZeros, highMark: highMark, forcedTime: forcedTime, pair } + }); + if (!result) { + logger.info('GIVEN proof-of-work for block#%s with %s leading zeros followed by [0-' + highMark + ']! stop PoW for %s', block.number, nbZeros, pair.pub.slice(0,6)); + throw 'Proof-of-work computation canceled because block received'; + } else { + const proof = result.block; + const testsCount = result.testsCount; + const duration = (Date.now() - start); + const testsPerSecond = (testsCount / (duration / 1000)).toFixed(2); + logger.info('Done: #%s, %s in %ss (%s tests, ~%s tests/s)', block.number, proof.hash, (duration / 1000).toFixed(2), testsCount, testsPerSecond); + logger.info('FOUND proof-of-work with %s leading zeros followed by [0-' + highMark + ']!', nbZeros); + return Block.fromJSON(proof); + } + }); + }; + + this.changeCPU = (cpu) => co(function*() { + conf.cpu = cpu; + const farm = yield getWorker(); + return farm.changeCPU(cpu); + }); + + this.changePoWPrefix = (prefix) => co(function*() { + const farm = yield getWorker(); + return farm.changePoWPrefix(prefix); + }); + + function powEvent(found, hash) { + server && server.push({ pow: { found, hash } }); + } + + function WorkerFarm() { + // Create + const theEngine = engine(server.conf, server.logger) + + let onAlmostPoW + + // An utility method to filter the pow notifications + const checkPoWandNotify = (hash, block, found) => { + const matches = hash.match(/^(0{2,})[^0]/); + if (matches && onAlmostPoW) { + onAlmostPoW(hash, matches, block, found); + } + } + + // Keep track of PoW advancement + theEngine.setOnInfoMessage((message) => { + if (message.error) { + logger.error('Error in engine#%s:', theEngine.id, message.error) + } else if (message.pow) { + // A message about the PoW + const msg = message.pow + checkPoWandNotify(msg.pow, msg.block, POW_NOT_FOUND_YET) + } + }) + + // We use as much cores as available, but not more than CORES_MAXIMUM_USE_IN_PARALLEL + + let powPromise = null + let stopPromise = null + + this.changeCPU = (cpu) => theEngine.setConf({ cpu }) + + this.changePoWPrefix = (prefix) => theEngine.setConf({ prefix }) + + this.isComputing = () => powPromise !== null && !powPromise.isResolved() + + this.isStopping = () => stopPromise !== null && !stopPromise.isResolved() + + /** + * Eventually stops the engine PoW if one was computing + */ + this.stopPoW = (gottenBlock) => { + stopPromise = querablep(theEngine.cancel(gottenBlock)) + return stopPromise; + }; + + /** + * Starts a new computation of PoW + * @param stuff The necessary data for computing the PoW + */ + this.askNewProof = (stuff) => co(function*() { + // Starts the PoW + powPromise = querablep(theEngine.prove(stuff)) + const res = yield powPromise + if (res) { + checkPoWandNotify(res.pow.pow, res.pow.block, POW_FOUND); + } + return res && res.pow + }) + + this.setOnAlmostPoW = (onPoW) => onAlmostPoW = onPoW + } +} diff --git a/app/modules/prover/lib/constants.js b/app/modules/prover/lib/constants.js new file mode 100644 index 000000000..4ed3b0765 --- /dev/null +++ b/app/modules/prover/lib/constants.js @@ -0,0 +1,20 @@ +"use strict"; + +module.exports = { + + PULLING_MAX_DURATION: 10 * 1000, // 10 seconds + + CORES_MAXIMUM_USE_IN_PARALLEL: 8, + + MINIMAL_ZEROS_TO_SHOW_IN_LOGS: 3, + + POW_MINIMAL_TO_SHOW: 2, + DEFAULT_CPU: 0.6, + + NONCE_RANGE: 1000 * 1000 * 1000 * 100, + + POW_MAXIMUM_ACCEPTABLE_HANDICAP: 64, + + // When to trigger the PoW process again if no PoW is triggered for a while. In milliseconds. + POW_SECURITY_RETRY_DELAY: 10 * 60 * 1000 +}; diff --git a/app/modules/prover/lib/engine.js b/app/modules/prover/lib/engine.js new file mode 100644 index 000000000..942ffba65 --- /dev/null +++ b/app/modules/prover/lib/engine.js @@ -0,0 +1,47 @@ +"use strict"; + +const os = require('os') +const co = require('co') +const querablep = require('querablep') +const powCluster = require('./powCluster') +const constants = require('./constants') + +module.exports = function (conf, logger) { + return new PowEngine(conf, logger); +}; + +function PowEngine(conf, logger) { + + // Super important for Node.js debugging + const debug = process.execArgv.toString().indexOf('--debug') !== -1; + if(debug) { + //Set an unused port number. + process.execArgv = []; + } + + const nbWorkers = require('os').cpus().slice(0, conf && conf.nbCores || constants.CORES_MAXIMUM_USE_IN_PARALLEL).length + const cluster = powCluster(nbWorkers, logger) + + this.forceInit = () => cluster.initCluster() + + this.id = cluster.clusterId + + this.prove = (stuff) => co(function*() { + + if (cluster.hasProofPending) { + yield cluster.cancelWork() + } + + if (os.arch().match(/arm/)) { + stuff.conf.cpu /= 2; // Don't know exactly why is ARM so much saturated by PoW, so let's divide by 2 + } + let res = yield cluster.proveByWorkers(stuff) + return res + }) + + this.cancel = () => cluster.cancelWork() + + this.setConf = (value) => cluster.changeConf(value) + + this.setOnInfoMessage = (callback) => cluster.onInfoMessage = callback +} diff --git a/app/modules/prover/lib/permanentProver.js b/app/modules/prover/lib/permanentProver.js new file mode 100644 index 000000000..62d45039a --- /dev/null +++ b/app/modules/prover/lib/permanentProver.js @@ -0,0 +1,209 @@ +"use strict"; + +const co = require('co'); +const querablep = require('querablep'); +const common = require('duniter-common'); +const constants = require('./constants'); +const blockProver = require('./blockProver'); +const blockGenerator = require('./blockGenerator'); + +module.exports = (server) => new PermanentProver(server); + +function PermanentProver(server) { + + const dos2unix = common.dos2unix; + const parsers = common.parsers; + const logger = server.logger; + const conf = server.conf; + const prover = this.prover = blockProver(server); + const generator = blockGenerator(server, prover); + const that = this; + + let blockchainChangedResolver = null, + promiseOfWaitingBetween2BlocksOfOurs = null, + lastComputedBlock = null; + + // Promises triggering the prooving lopp + let resolveContinuePromise = null; + let continuePromise = new Promise((resolve) => resolveContinuePromise = resolve); + + let pullingResolveCallback = null; + let timeoutPullingCallback = null, timeoutPulling; + let pullingFinishedPromise = querablep(Promise.resolve()); + + this.allowedToStart = () => { + resolveContinuePromise(true); + }; + + // When we detected a pulling, we stop the PoW loop + this.pullingDetected = () => { + if (pullingFinishedPromise.isResolved()) { + pullingFinishedPromise = querablep(Promise.race([ + // We wait for end of pulling signal + new Promise((res) => pullingResolveCallback = res), + // Security: if the end of pulling signal is not emitted after some, we automatically trigger it + new Promise((res) => timeoutPullingCallback = () => { + logger.warn('Pulling not finished after %s ms, continue PoW', constants.PULLING_MAX_DURATION); + res(); + }) + ])); + } + // Delay the triggering of pulling timeout + if (timeoutPulling) { + clearTimeout(timeoutPulling); + } + timeoutPulling = setTimeout(timeoutPullingCallback, constants.PULLING_MAX_DURATION); + }; + + this.pullingFinished = () => pullingResolveCallback && pullingResolveCallback(); + + this.loops = 0; + + /****************** + * Main proof loop + *****************/ + co(function*() { + while (yield continuePromise) { + try { + const waitingRaces = []; + + // By default, we do not make a new proof + let doProof = false; + + try { + const selfPubkey = server.keyPair.publicKey; + const dal = server.dal; + const theConf = server.conf; + if (!selfPubkey) { + throw 'No self pubkey found.'; + } + let current; + const isMember = yield dal.isMember(selfPubkey); + if (!isMember) { + throw 'Local node is not a member. Waiting to be a member before computing a block.'; + } + current = yield dal.getCurrentBlockOrNull(); + if (!current) { + throw 'Waiting for a root block before computing new blocks'; + } + const trial = yield server.getBcContext().getIssuerPersonalizedDifficulty(selfPubkey); + checkTrialIsNotTooHigh(trial, current, selfPubkey); + const lastIssuedByUs = current.issuer == selfPubkey; + if (pullingFinishedPromise && !pullingFinishedPromise.isFulfilled()) { + logger.warn('Waiting for the end of pulling...'); + yield pullingFinishedPromise; + logger.warn('Pulling done. Continue proof-of-work loop.'); + } + if (lastIssuedByUs && !promiseOfWaitingBetween2BlocksOfOurs) { + promiseOfWaitingBetween2BlocksOfOurs = new Promise((resolve) => setTimeout(resolve, theConf.powDelay)); + logger.warn('Waiting ' + theConf.powDelay + 'ms before starting to compute next block...'); + } else { + // We have waited enough + promiseOfWaitingBetween2BlocksOfOurs = null; + // But under some conditions, we can make one + doProof = true; + } + } catch (e) { + logger.warn(e); + } + + if (doProof) { + + /******************* + * COMPUTING A BLOCK + ******************/ + yield Promise.race([ + + // We still listen at eventual blockchain change + co(function*() { + // If the blockchain changes + yield new Promise((resolve) => blockchainChangedResolver = resolve); + // Then cancel the generation + yield prover.cancel(); + }), + + // The generation + co(function*() { + try { + const current = yield server.dal.getCurrentBlockOrNull(); + const selfPubkey = server.keyPair.publicKey; + const trial2 = yield server.getBcContext().getIssuerPersonalizedDifficulty(selfPubkey); + checkTrialIsNotTooHigh(trial2, current, selfPubkey); + lastComputedBlock = yield generator.makeNextBlock(null, trial2); + try { + const obj = parsers.parseBlock.syncWrite(dos2unix(lastComputedBlock.getRawSigned())); + yield server.singleWritePromise(obj); + } catch (err) { + logger.warn('Proof-of-work self-submission: %s', err.message || err); + } + } catch (e) { + logger.warn('The proof-of-work generation was canceled: %s', (e && e.message) || e || 'unkonwn reason'); + } + }) + ]); + } else { + + /******************* + * OR WAITING PHASE + ******************/ + if (promiseOfWaitingBetween2BlocksOfOurs) { + waitingRaces.push(promiseOfWaitingBetween2BlocksOfOurs); + } + + let raceDone = false; + + yield Promise.race(waitingRaces.concat([ + + // The blockchain has changed! We or someone else found a proof, we must make a gnu one + new Promise((resolve) => blockchainChangedResolver = () => { + logger.warn('Blockchain changed!'); + resolve(); + }), + + // Security: if nothing happens for a while, trigger the whole process again + new Promise((resolve) => setTimeout(() => { + if (!raceDone) { + logger.warn('Security trigger: proof-of-work process seems stuck'); + resolve(); + } + }, conf.powSecurityRetryDelay)) + ])); + + raceDone = true; + } + } catch (e) { + logger.warn(e); + } + + that.loops++; + // Informative variable + logger.trace('PoW loops = %s', that.loops); + } + }); + + this.blockchainChanged = (gottenBlock) => co(function*() { + if (server && (!gottenBlock || !lastComputedBlock || gottenBlock.hash !== lastComputedBlock.hash)) { + // Cancel any processing proof + yield prover.cancel(gottenBlock); + // If we were waiting, stop it and process the continuous generation + blockchainChangedResolver && blockchainChangedResolver(); + } + }); + + this.stopEveryting = () => co(function*() { + // First: avoid continuing the main loop + continuePromise = new Promise((resolve) => resolveContinuePromise = resolve); + // Second: stop any started proof + yield prover.cancel(); + // If we were waiting, stop it and process the continuous generation + blockchainChangedResolver && blockchainChangedResolver(); + }); + + function checkTrialIsNotTooHigh(trial, current, selfPubkey) { + if (trial > (current.powMin + conf.powMaxHandicap)) { + logger.debug('Trial = %s, powMin = %s, pubkey = %s', trial, current.powMin, selfPubkey.slice(0, 6)); + throw 'Too high difficulty: waiting for other members to write next block'; + } + } +} + diff --git a/app/modules/prover/lib/powCluster.js b/app/modules/prover/lib/powCluster.js new file mode 100644 index 000000000..811e5e8c2 --- /dev/null +++ b/app/modules/prover/lib/powCluster.js @@ -0,0 +1,230 @@ +"use strict"; + +const co = require('co'); +const _ = require('underscore') +const nuuid = require('node-uuid'); +const moment = require('moment'); +const cluster = require('cluster') +const querablep = require('querablep') +const constants = require('./constants') + +let clusterId = 0 + +if (cluster.isMaster) { + + // Super important for Node.js debugging + const debug = process.execArgv.toString().indexOf('--debug') !== -1; + if(debug) { + //Set an unused port number. + process.execArgv = []; + } + + /** + * Cluster controller, handles the messages between the main program and the PoW cluster. + */ + class Master { + + constructor(nbCores, logger) { + this.clusterId = clusterId++ + this.nbCores = nbCores + this.logger = logger || Master.defaultLogger() + this.currentPromise = null + this.slaves = [] + this.slavesMap = {} + this.conf = {} + this.onInfoMessage = (message) => { + this.logger.info(`${message.pow.pow} nonce = ${message.pow.block.nonce}`) + } + } + + get nbWorkers() { + return this.slaves.length + } + + get hasProofPending() { + return !!this.currentPromise + } + + set onInfoMessage(callback) { + this.onInfoCallback = callback + } + + onWorkerMessage(worker, message) { + // this.logger.info(`worker#${this.slavesMap[worker.id].index} sent message:${message}`) + if (message.pow && message.pow.pow) { + this.onInfoCallback && this.onInfoCallback(message) + } + if (this.currentPromise && message.uuid === this.currentPromise.extras.uuid && !this.currentPromise.isResolved() && message.answer) { + this.logger.info(`ENGINE c#${this.clusterId}#${this.slavesMap[worker.id].index} HAS FOUND A PROOF #${message.answer.pow.pow}`) + this.currentPromise.extras.resolve(message.answer) + // Stop the slaves' current work + this.cancelWork() + } + this.logger.debug(`ENGINE c#${this.clusterId}#${this.slavesMap[worker.id].index}:`, message) + } + + initCluster() { + // Setup master + cluster.setupMaster({ + exec: __filename + }) + + this.slaves = Array.from({ length: this.nbCores }).map((value, index) => { + const worker = cluster.fork() + this.logger.info(`Creating worker c#${this.clusterId}#w#${worker.id}`) + this.slavesMap[worker.id] = { + + // The Node.js worker + worker, + + // Inner identifier + index, + + // Worker ready + online: (function onlinePromise() { + let resolve + const p = querablep(new Promise(res => resolve = res)) + p.extras = { resolve } + return p + })(), + + // Each worker has his own chunk of possible nonces + nonceBeginning: this.nbCores === 1 ? 0 : (index + 1) * constants.NONCE_RANGE + } + return this.slavesMap[worker.id] + }) + + cluster.on('exit', (worker, code, signal) => { + this.logger.info(`worker ${worker.process.pid} died with code ${code} and signal ${signal}`) + }) + + cluster.on('online', (worker) => { + // We just listen to the workers of this Master + if (this.slavesMap[worker.id]) { + this.logger.info(`[online] worker c#${this.clusterId}#w#${worker.id}`) + this.slavesMap[worker.id].online.extras.resolve() + worker.send({ + command: 'conf', + value: this.conf + }) + } + }) + + cluster.on('message', (worker, msg) => { + // Message for this cluster + if (this.slavesMap[worker.id]) { + this.onWorkerMessage(worker, msg) + } + }) + + this.workersOnline = this.slaves.map(s => s.online) + return this.workersOnline + } + + changeConf(conf) { + this.logger.info(`Changing conf to: ${JSON.stringify(conf)} on PoW cluster`) + this.conf.cpu = this.conf.cpu || conf.cpu + this.conf.prefix = this.conf.prefix || conf.prefix + this.slaves.forEach(s => { + s.worker.send({ + command: 'conf', + value: this.conf + }) + }) + return Promise.resolve(_.clone(conf)) + } + + cancelWork() { + this.logger.info(`Cancelling the work on PoW cluster`) + this.slaves.forEach(s => { + s.worker.send({ + command: 'cancel' + }) + }) + + // Eventually force the end of current promise + if (this.currentPromise && !this.currentPromise.isFulfilled()) { + this.currentPromise.extras.resolve(null) + } + + // Current promise is done + this.currentPromise = null + + return Promise.resolve() + } + + newPromise(uuid) { + let resolve + const p = querablep(new Promise(res => resolve = res)) + p.extras = { resolve, uuid } + return p + } + + proveByWorkers(stuff) { + + // Eventually spawn the workers + if (this.slaves.length === 0) { + this.initCluster() + } + + // Register the new proof uuid + const uuid = nuuid.v4() + this.currentPromise = this.newPromise(uuid) + + const that = this + + return co(function*() { + yield that.workersOnline + + if (!that.currentPromise) { + that.logger.info(`Proof canceled during workers' initialization`) + return null + } + + // Start the salves' job + that.slaves.forEach(s => { + s.worker.send({ + uuid, + command: 'newPoW', + value: { + block: stuff.newPoW.block, + nonceBeginning: s.nonceBeginning, + zeros: stuff.newPoW.zeros, + highMark: stuff.newPoW.highMark, + pair: _.clone(stuff.newPoW.pair), + forcedTime: stuff.newPoW.forcedTime, + turnDuration: stuff.newPoW.turnDuration, + conf: { + medianTimeBlocks: stuff.newPoW.conf.medianTimeBlocks, + avgGenTime: stuff.newPoW.conf.avgGenTime, + cpu: stuff.newPoW.conf.cpu, + prefix: stuff.newPoW.conf.prefix + } + } + }) + }) + + let res = yield that.currentPromise + return res + }) + } + + static defaultLogger() { + return { + info: (message) => {} + } + } + } + + module.exports = (nbCores, logger) => new Master(nbCores, logger) + +} else { + + process.on("SIGTERM", function() { + console.log(`SIGTERM received, closing worker ${process.pid}`); + process.exit(0) + }); + + require('./proof') +} + diff --git a/app/modules/prover/lib/proof.js b/app/modules/prover/lib/proof.js new file mode 100644 index 000000000..64d68645d --- /dev/null +++ b/app/modules/prover/lib/proof.js @@ -0,0 +1,292 @@ +"use strict"; +const co = require('co'); +const moment = require('moment'); +const hashf = require('duniter-common').hashf; +const dos2unix = require('duniter-common').dos2unix; +const querablep = require('querablep'); +const constants = require('./constants'); +const keyring = require('duniter-common').keyring; +const rawer = require('duniter-common').rawer; + +const PAUSES_PER_TURN = 5; + +// This value can be changed +let TURN_DURATION_IN_MILLISEC = 100; + +let computing = querablep(Promise.resolve(null)); +let askedStop = false; + +// By default, we do not prefix the PoW by any number +let prefix = 0; + +let signatureFunc, lastSecret, currentCPU = 1; + +process.on('uncaughtException', (err) => { + console.error(err.stack || Error(err)); + process.send({error: err}); +}); + +process.on('message', (message) => co(function*() { + + switch (message.command) { + + case 'newPoW': + co(function*() { + askedStop = true + + // Very important: do not yield if the computation is already done, to keep the lock on JS engine + if (!computing.isFulfilled()) { + yield computing; + } + + const res = yield beginNewProofOfWork(message.value); + answer(message, res); + }); + break; + + case 'cancel': + if (!computing.isFulfilled()) { + askedStop = true; + } + break; + + case 'conf': + if (message.value.cpu !== undefined) { + currentCPU = message.value.cpu + } + if (message.value.prefix !== undefined) { + prefix = message.value.prefix + } + answer(message, { currentCPU, prefix }); + break; + } + +})); + +function beginNewProofOfWork(stuff) { + askedStop = false; + computing = querablep(co(function*() { + + /***************** + * PREPARE POW STUFF + ****************/ + + let nonce = 0; + const conf = stuff.conf; + const block = stuff.block; + const nonceBeginning = stuff.nonceBeginning; + const nbZeros = stuff.zeros; + const pair = stuff.pair; + const forcedTime = stuff.forcedTime; + currentCPU = conf.cpu || constants.DEFAULT_CPU; + prefix = parseInt(conf.prefix || prefix) * 10 * constants.NONCE_RANGE; + const highMark = stuff.highMark; + const turnDuration = stuff.turnDuration || TURN_DURATION_IN_MILLISEC + let sigFunc = null; + if (signatureFunc && lastSecret === pair.sec) { + sigFunc = signatureFunc; + } + else { + lastSecret = pair.sec; + sigFunc = keyring.Key(pair.pub, pair.sec).signSync; + } + signatureFunc = sigFunc; + let pow = "", sig = "", raw = ""; + + /***************** + * GO! + ****************/ + + let testsCount = 0; + let found = false; + let score = 0; + let turn = 0; + + while (!found && !askedStop) { + + /***************** + * A TURN + ****************/ + + yield Promise.race([ + + // I. Stop the turn if it exceeds `turnDuration` ms + countDown(turnDuration), + + // II. Process the turn's PoW + co(function*() { + + /***************** + * A TURN OF POW ~= 100ms by default + * -------------------- + * + * The concept of "turn" is required to limit the CPU usage. + * We need a time reference to have the speed = nb tests / period of time. + * Here we have: + * + * - speed = testsCount / turn + * + * We have taken 1 turn = 100ms to control the CPU usage after 100ms of PoW. This means that during the + * very first 100ms of the PoW, CPU usage = 100%. Then it becomes controlled to the %CPU set. + ****************/ + + // Prove + let i = 0; + const thisTurn = turn; + const pausePeriod = score ? score / PAUSES_PER_TURN : 10; // number of pauses per turn + // We limit the number of tests according to CPU usage + const testsPerRound = score ? Math.floor(score * currentCPU) : 1000 * 1000 * 1000 + + // Time is updated regularly during the proof + block.time = getBlockTime(block, conf, forcedTime) + if (block.number === 0) { + block.medianTime = block.time + } + block.inner_hash = getBlockInnerHash(block); + + /***************** + * Iterations of a turn + ****************/ + + while(!found && i < testsPerRound && thisTurn === turn && !askedStop) { + + // Nonce change (what makes the PoW change if the time field remains the same) + nonce++ + + /***************** + * A PROOF OF WORK + ****************/ + + // The final nonce is composed of 3 parts + block.nonce = prefix + nonceBeginning + nonce + raw = dos2unix("InnerHash: " + block.inner_hash + "\nNonce: " + block.nonce + "\n") + sig = dos2unix(sigFunc(raw)) + pow = hashf("InnerHash: " + block.inner_hash + "\nNonce: " + block.nonce + "\n" + sig + "\n").toUpperCase() + + /***************** + * Check the POW result + ****************/ + + let j = 0, charOK = true; + while (j < nbZeros && charOK) { + charOK = pow[j] === '0'; + j++; + } + if (charOK) { + found = pow[nbZeros].match(new RegExp('[0-' + highMark + ']')); + } + if (!found && nbZeros > 0 && j - 1 >= constants.POW_MINIMAL_TO_SHOW) { + pSend({ pow: { pow: pow, block: block, nbZeros: nbZeros }}); + } + + /***************** + * - Update local vars + * - Allow to receive stop signal + ****************/ + + if (!found && !askedStop) { + i++; + testsCount++; + if (i % pausePeriod === 0) { + yield countDown(0); // Very low pause, just the time to process eventual end of the turn + } + } + } + + /***************** + * Check the POW result + ****************/ + if (!found) { + + // CPU speed recording + if (turn > 0 && !score) { + score = testsCount; + } + + /***************** + * UNLOAD CPU CHARGE + ****************/ + // We wait for a maximum time of `turnDuration`. + // This will trigger the end of the turn by the concurrent race I. During that time, the proof.js script + // just does nothing: this gives of a bit of breath to the CPU. Tthe amount of "breath" depends on the "cpu" + // parameter. + yield countDown(turnDuration); + } + }) + ]); + + // Next turn + turn++ + } + + /***************** + * POW IS OVER + * ----------- + * + * We either have found a valid POW or a stop event has been detected. + ****************/ + + if (askedStop) { + + // PoW stopped + askedStop = false; + return null + + } else { + + // PoW success + block.hash = pow + block.signature = sig + return { + pow: { + block: block, + testsCount: testsCount, + pow: pow + } + } + } + })); + + return computing; +} + +function countDown(duration) { + return new Promise((resolve) => setTimeout(resolve, duration)); +} + +function getBlockInnerHash(block) { + const raw = rawer.getBlockInnerPart(block); + return hash(raw); +} + +function hash(str) { + return hashf(str).toUpperCase(); +} + +function getBlockTime (block, conf, forcedTime) { + if (forcedTime) { + return forcedTime; + } + const now = moment.utc().unix(); + const maxAcceleration = require('duniter-common').rules.HELPERS.maxAcceleration(conf); + const timeoffset = block.number >= conf.medianTimeBlocks ? 0 : conf.rootoffset || 0; + const medianTime = block.medianTime; + const upperBound = block.number === 0 ? medianTime : Math.min(medianTime + maxAcceleration, now - timeoffset); + return Math.max(medianTime, upperBound); +} + +function answer(message, theAnswer) { + return pSend({ + uuid: message.uuid, + answer: theAnswer + }) +} + +function pSend(stuff) { + return new Promise(function (resolve, reject) { + process.send(stuff, function (error) { + !error && resolve(); + error && reject(); + }); + }); +} diff --git a/app/modules/prover/lib/prover.js b/app/modules/prover/lib/prover.js new file mode 100644 index 000000000..504ceb1a4 --- /dev/null +++ b/app/modules/prover/lib/prover.js @@ -0,0 +1,44 @@ +"use strict"; + +const co = require('co'); +const util = require('util'); +const stream = require('stream'); +const permanentProver = require('./permanentProver'); + +module.exports = Prover; + +function Prover(server) { + + const permaProver = this.permaProver = permanentProver(server); + + stream.Transform.call(this, { objectMode: true }); + + this._write = function (obj, enc, done) { + // Never close the stream + if (obj && obj.membersCount) { + permaProver.blockchainChanged(obj); + } else if (obj.nodeIndexInPeers !== undefined) { + permaProver.prover.changePoWPrefix((obj.nodeIndexInPeers + 1) * 10); // We multiply by 10 to give room to computers with < 100 cores + } else if (obj.cpu !== undefined) { + permaProver.prover.changeCPU(obj.cpu); // We multiply by 10 to give room to computers with < 100 cores + } else if (obj.pulling !== undefined) { + if (obj.pulling === 'processing') { + permaProver.pullingDetected(); + } + else if (obj.pulling === 'finished') { + permaProver.pullingFinished(); + } + } + done && done(); + }; + + this.startService = () => co(function*() { + permaProver.allowedToStart(); + }); + + this.stopService = () => co(function*() { + permaProver.stopEveryting(); + }); +} + +util.inherits(Prover, stream.Transform); diff --git a/package.json b/package.json index a40f8aa97..63387d7bb 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,6 @@ "duniter-bma": "1.3.x", "duniter-crawler": "1.3.x", "duniter-keypair": "1.3.X", - "duniter-prover": "1.4.x", "duniter-ui": "1.3.x", "eslint": "3.13.1", "eslint-plugin-mocha": "4.8.0", @@ -97,7 +96,6 @@ "duniter-bma": "1.3.x", "duniter-crawler": "1.3.x", "duniter-keypair": "1.3.X", - "duniter-prover": "1.4.x", "duniter-ui": "1.3.x" }, "bin": { diff --git a/release/arch/windows/build.bat b/release/arch/windows/build.bat index 9be0fd426..d4d9c042c 100644 --- a/release/arch/windows/build.bat +++ b/release/arch/windows/build.bat @@ -3,7 +3,6 @@ set DUNITER_BRANCH=1.3.x set VER_UI=%DUNITER_BRANCH% set VER_BMA=%DUNITER_BRANCH% set VER_CRAWLER=%DUNITER_BRANCH% -set VER_PROVER=%DUNITER_BRANCH% set VER_KEYPAIR=%DUNITER_BRANCH% set ADDON_VERSION=48 @@ -51,15 +50,13 @@ call npm install --production REM call npm test echo "Retrait des modules 'dev'..." call npm prune --production -echo "Ajout du module 1/5..." +echo "Ajout du module 1/4..." call npm install duniter-bma@%VER_BMA% --save --production -echo "Ajout du module 2/5..." +echo "Ajout du module 2/4..." call npm install duniter-crawler@%VER_CRAWLER% --save --production -echo "Ajout du module 3/5..." +echo "Ajout du module 3/4..." call npm install duniter-keypair@%VER_KEYPAIR% --save --production -echo "Ajout du module 4/5..." -call npm install duniter-prover@%VER_PROVER% --save --production -echo "Ajout du module 5/5..." +echo "Ajout du module 4/4..." call npm install duniter-ui@%VER_UI% --save --production REM echo ">> VM: installing peerDependencies installer..." diff --git a/test/fast/prover/pow-1-cluster.js b/test/fast/prover/pow-1-cluster.js new file mode 100644 index 000000000..9f1a20b63 --- /dev/null +++ b/test/fast/prover/pow-1-cluster.js @@ -0,0 +1,76 @@ +"use strict"; + +const co = require('co') +const should = require('should') +const powCluster = require('../../../app/modules/prover/lib/powCluster') +const logger = require('../../../app/lib/logger')() + +let master + +describe('PoW Cluster', () => { + + before(() => { + master = powCluster(1, logger) + }) + + it('should have an empty cluster if no PoW was asked', () => { + master.nbWorkers.should.equal(0) + }) + + it('should answer for a basic PoW in more than 50ms (cold)', () => co(function*(){ + const start = Date.now() + yield master.proveByWorkers({ + newPoW: { + block: { + number: 0 + }, + zeros: 0, + highMark: 'F', + conf: { + medianTimeBlocks: 1, + avgGenTime: 100, + cpu: 0.8, + prefix: '8' + }, + pair: { + pub: 'HgTTJLAQ5sqfknMq7yLPZbehtuLSsKj9CxWN7k8QvYJd', + sec: '51w4fEShBk1jCMauWu4mLpmDVfHksKmWcygpxriqCEZizbtERA6de4STKRkQBpxmMUwsKXRjSzuQ8ECwmqN1u2DP' + }, + turnDuration: 10 + } + }) + const delay = Date.now() - start + delay.should.be.above(50) + })) + + it('should have an non-empty cluster after a PoW was asked', () => { + master.nbWorkers.should.above(0) + }) + + it('should answer within 50ms for a basic PoW (warm)', () => co(function*(){ + const start = Date.now() + yield master.proveByWorkers({ + newPoW: { + block: { + number: 0 + }, + zeros: 0, + highMark: 'F', + conf: { + medianTimeBlocks: 1, + avgGenTime: 100, + cpu: 0.8, + prefix: '8' + }, + pair: { + pub: 'HgTTJLAQ5sqfknMq7yLPZbehtuLSsKj9CxWN7k8QvYJd', + sec: '51w4fEShBk1jCMauWu4mLpmDVfHksKmWcygpxriqCEZizbtERA6de4STKRkQBpxmMUwsKXRjSzuQ8ECwmqN1u2DP' + }, + turnDuration: 100 + } + }) + const delay = Date.now() - start + delay.should.be.below(50) + })) + +}); diff --git a/test/fast/prover/pow-2-engine.js b/test/fast/prover/pow-2-engine.js new file mode 100644 index 000000000..f9b189905 --- /dev/null +++ b/test/fast/prover/pow-2-engine.js @@ -0,0 +1,89 @@ +"use strict"; + +const co = require('co'); +const should = require('should'); +const engine = require('../../../app/modules/prover/lib/engine'); +const logger = require('../../../app/lib/logger')() + +describe('PoW Engine', () => { + + it('should be configurable', () => co(function*(){ + const e1 = engine({ nbCores: 1 }, logger); + (yield e1.setConf({ cpu: 0.2, prefix: '34' })).should.deepEqual({ cpu: 0.2, prefix: '34' }); + })); + + it('should be able to make a proof', () => co(function*(){ + const e1 = engine({ nbCores: 1 }, logger); + const block = { number: 35 }; + const zeros = 2; + const highMark = 'A'; + const pair = { + pub: 'HgTTJLAQ5sqfknMq7yLPZbehtuLSsKj9CxWN7k8QvYJd', + sec: '51w4fEShBk1jCMauWu4mLpmDVfHksKmWcygpxriqCEZizbtERA6de4STKRkQBpxmMUwsKXRjSzuQ8ECwmqN1u2DP' + }; + const forcedTime = 1; + const medianTimeBlocks = 20; + const avgGenTime = 5 * 60; + const proof = yield e1.prove({ + newPoW: { + block, + zeros, + highMark, + pair, + forcedTime, + conf: { + medianTimeBlocks, + avgGenTime + } + } + } + ) + proof.should.deepEqual({ + pow: { + block: { + number: 35, + time: 1, + inner_hash: '51937F1192447A96537D10968689F4F48859E2DD6F8F9E8DE1006C9697C6C940', + nonce: 212, + hash: '009A52E6E2E4EA7DE950A2DA673114FA55B070EBE350D75FF0C62C6AAE9A37E5', + signature: 'bkmLGX7LNVkuOUMc+/HT6fXJajQtR5uk87fetIntMbGRZjychzu0whl5+AOOGlf+ilp/ara5UK6ppxyPcJIJAg==' + }, + testsCount: 211, + pow: '009A52E6E2E4EA7DE950A2DA673114FA55B070EBE350D75FF0C62C6AAE9A37E5' + } + }); + })); + + it('should be able to stop a proof', () => co(function*(){ + const e1 = engine({ nbCores: 1 }, logger); + yield e1.forceInit() + const block = { number: 26 }; + const zeros = 10; // Requires hundreds of thousands of tries probably + const highMark = 'A'; + const pair = { + pub: 'HgTTJLAQ5sqfknMq7yLPZbehtuLSsKj9CxWN7k8QvYJd', + sec: '51w4fEShBk1jCMauWu4mLpmDVfHksKmWcygpxriqCEZizbtERA6de4STKRkQBpxmMUwsKXRjSzuQ8ECwmqN1u2DP' + }; + const forcedTime = 1; + const medianTimeBlocks = 20; + const avgGenTime = 5 * 60; + const proofPromise = e1.prove({ + newPoW: { + block, + zeros, + highMark, + pair, + forcedTime, + conf: { + medianTimeBlocks, + avgGenTime + } + } + } + ) + yield new Promise((res) => setTimeout(res, 10)) + yield e1.cancel() + // const proof = yield proofPromise; + // should.not.exist(proof); + })); +}); diff --git a/test/fast/prover/pow-3-prover.js b/test/fast/prover/pow-3-prover.js new file mode 100644 index 000000000..2568abd77 --- /dev/null +++ b/test/fast/prover/pow-3-prover.js @@ -0,0 +1,92 @@ +"use strict"; + +const co = require('co') +const should = require('should') +const moment = require('moment') +const winston = require('winston') +const blockProver = require('../../../app/modules/prover/lib/blockProver'); + +// Mute logger +winston.remove(winston.transports.Console) + +describe('PoW block prover', () => { + + let prover + + before(() => { + prover = blockProver({ + conf: { + nbCores: 1, + medianTimeBlocks: 20, + avgGenTime: 5 * 60, + pair: { + pub: 'HgTTJLAQ5sqfknMq7yLPZbehtuLSsKj9CxWN7k8QvYJd', + sec: '51w4fEShBk1jCMauWu4mLpmDVfHksKmWcygpxriqCEZizbtERA6de4STKRkQBpxmMUwsKXRjSzuQ8ECwmqN1u2DP' + } + }, + push: () => {}, + logger: winston + }) + }) + + it('should be configurable', () => co(function*(){ + const res1 = yield prover.changeCPU(0.2) + res1.should.deepEqual({ cpu: 0.2 }) + const res2 = yield prover.changePoWPrefix('34') + res2.should.deepEqual({ prefix: '34' }) + })); + + it('should be able to make a proof', () => co(function*(){ + const block = { + number: 35, + issuer: 'HgTTJLAQ5sqfknMq7yLPZbehtuLSsKj9CxWN7k8QvYJd' + } + const forcedTime = 1; + const proof = yield prover.prove(block, 24, forcedTime) + proof.should.containEql({ + version: 10, + nonce: 34000000000010, + number: 35, + time: 1, + currency: '', + issuer: 'HgTTJLAQ5sqfknMq7yLPZbehtuLSsKj9CxWN7k8QvYJd', + signature: 'iG9XEEIoGvCuFLRXqXIcGKFeK88K/A0J9MfKWAGvkRHtf6+VtMR/VDtPP67UzfnVdJb4QfMqrNsPMH2+7bTTAA==', + hash: '07573FEA1248562F47B1FA7DABDAF93C93B7328AA528F470B488249D5806F66D', + parameters: '', + previousHash: null, + previousIssuer: null, + inner_hash: 'A31455535488AE74B819FD920CA0BDFEFB6E753BDF1EF17E1661A144A0D6B3EB', + dividend: null, + identities: [], + joiners: [], + actives: [], + leavers: [], + revoked: [], + excluded: [], + certifications: [], + transactions: [] + }); + })); + + it('should be able to stop a proof', () => co(function*(){ + const block = { + number: 35, + issuer: 'HgTTJLAQ5sqfknMq7yLPZbehtuLSsKj9CxWN7k8QvYJd' + } + const forcedTime = 1; + const proofPromise = prover.prove(block, 70, forcedTime) + yield new Promise((res) => setTimeout(res, 20)) + yield prover.cancel() + let err = '' + try { + yield proofPromise + } catch (e) { + err = e + } finally { + if (!err) { + throw "Should have thrown!" + } + err.should.equal('Proof-of-work computation canceled because block received') + } + })); +}); diff --git a/test/integration/http_api.js b/test/integration/http_api.js index 86171f3f5..287303214 100644 --- a/test/integration/http_api.js +++ b/test/integration/http_api.js @@ -58,7 +58,7 @@ describe("HTTP API", function() { function makeBlockAndPost(theServer) { return function() { - return require('duniter-prover').duniter.methods.generateAndProveTheNext(theServer) + return require('../../app/modules/prover').duniter.methods.generateAndProveTheNext(theServer) .then(postBlock(theServer)); }; } diff --git a/test/integration/identity-expiry.js b/test/integration/identity-expiry.js index b19e084af..ad5ca6e28 100644 --- a/test/integration/identity-expiry.js +++ b/test/integration/identity-expiry.js @@ -5,7 +5,7 @@ const co = require('co'); const should = require('should'); const duniter = require('../../index'); const bma = require('duniter-bma').duniter.methods.bma; -const prover = require('duniter-prover').duniter.methods; +const prover = require('../../app/modules/prover').duniter.methods; const user = require('./tools/user'); const constants = require('../../app/lib/constants'); const rp = require('request-promise'); diff --git a/test/integration/identity-kicking.js b/test/integration/identity-kicking.js index 7846cc522..00b76b845 100644 --- a/test/integration/identity-kicking.js +++ b/test/integration/identity-kicking.js @@ -50,7 +50,7 @@ describe("Identities kicking", function() { const now = Math.round(new Date().getTime() / 1000); yield s1.initWithDAL().then(bma).then((bmapi) => bmapi.openConnections()); - require('duniter-prover').duniter.methods.hookServer(s1); + require('../../app/modules/prover').duniter.methods.hookServer(s1); yield cat.createIdentity(); yield tac.createIdentity(); yield cat.cert(tac); diff --git a/test/integration/identity-test.js b/test/integration/identity-test.js index 03bcdd091..aa38506a5 100644 --- a/test/integration/identity-test.js +++ b/test/integration/identity-test.js @@ -54,7 +54,7 @@ describe("Identities collision", function() { return co(function *() { yield s1.initWithDAL().then(bma).then((bmapi) => bmapi.openConnections()); - require('duniter-prover').duniter.methods.hookServer(s1); + require('../../app/modules/prover').duniter.methods.hookServer(s1); yield cat.createIdentity(); yield tac.createIdentity(); yield toc.createIdentity(); diff --git a/test/integration/proof-of-work.js b/test/integration/proof-of-work.js index 078c8c366..9488ed3e1 100644 --- a/test/integration/proof-of-work.js +++ b/test/integration/proof-of-work.js @@ -6,7 +6,7 @@ const toolbox = require('./tools/toolbox'); const Block = require('../../app/lib/entity/block'); const constants = require('../../app/lib/constants'); const logger = require('../../app/lib/logger')(); -const blockProver = require('duniter-prover').duniter.methods.blockProver; +const blockProver = require('../../app/modules/prover').duniter.methods.blockProver; /*** conf.medianTimeBlocks diff --git a/test/integration/start_generate_blocks.js b/test/integration/start_generate_blocks.js index f1a12056a..3b471cde1 100644 --- a/test/integration/start_generate_blocks.js +++ b/test/integration/start_generate_blocks.js @@ -73,7 +73,7 @@ describe("Generation", function() { yield server.bma.openConnections(); require('../../app/modules/router').duniter.methods.routeToNetwork(server); yield server.PeeringService.generateSelfPeer(server.conf, 0); - const prover = require('duniter-prover').duniter.methods.prover(server); + const prover = require('../../app/modules/prover').duniter.methods.prover(server); server.startBlockComputation = () => prover.startService(); server.stopBlockComputation = () => prover.stopService(); } diff --git a/test/integration/tools/commit.js b/test/integration/tools/commit.js index edd9a3799..046e59756 100644 --- a/test/integration/tools/commit.js +++ b/test/integration/tools/commit.js @@ -13,8 +13,8 @@ module.exports = function makeBlockAndPost(theServer, extraProps) { } return co(function *() { if (!theServer._utProver) { - theServer._utProver = require('duniter-prover').duniter.methods.blockProver(theServer) - theServer._utGenerator = require('duniter-prover').duniter.methods.blockGenerator(theServer, theServer._utProver) + theServer._utProver = require('../../../app/modules/prover').duniter.methods.blockProver(theServer) + theServer._utGenerator = require('../../../app/modules/prover').duniter.methods.blockGenerator(theServer, theServer._utProver) } let proven = yield theServer._utGenerator.makeNextBlock(null, null, manualValues) const block = yield postBlock(theServer)(proven); diff --git a/test/integration/tools/node.js b/test/integration/tools/node.js index f430bd39d..fd25c02f4 100644 --- a/test/integration/tools/node.js +++ b/test/integration/tools/node.js @@ -79,9 +79,9 @@ function Node (dbName, options) { block: function(callback){ co(function *() { try { - const block2 = yield require('duniter-prover').duniter.methods.generateTheNextBlock(that.server, params); + const block2 = yield require('../../../app/modules/prover').duniter.methods.generateTheNextBlock(that.server, params); const trial2 = yield that.server.getBcContext().getIssuerPersonalizedDifficulty(that.server.keyPair.publicKey); - const block = yield require('duniter-prover').duniter.methods.generateAndProveTheNext(that.server, block2, trial2, params); + const block = yield require('../../../app/modules/prover').duniter.methods.generateAndProveTheNext(that.server, block2, trial2, params); callback(null, block); } catch (e) { callback(e); diff --git a/test/integration/tools/toolbox.js b/test/integration/tools/toolbox.js index aadc5d3c8..e9fbc3b92 100644 --- a/test/integration/tools/toolbox.js +++ b/test/integration/tools/toolbox.js @@ -256,7 +256,7 @@ module.exports = { }); server.makeNext = (overrideProps) => co(function*() { - const block = yield require('duniter-prover').duniter.methods.generateAndProveTheNext(server, null, null, overrideProps || {}); + const block = yield require('../../../app/modules/prover').duniter.methods.generateAndProveTheNext(server, null, null, overrideProps || {}); return Block.statics.fromJSON(block); }); @@ -302,13 +302,13 @@ module.exports = { server.bma = bmaAPI; require('../../../app/modules/router').duniter.methods.routeToNetwork(server); // Extra: for /wot/requirements URL - require('duniter-prover').duniter.methods.hookServer(server); + require('../../../app/modules/prover').duniter.methods.hookServer(server); }); let prover; server.startBlockComputation = () => { if (!prover) { - prover = require('duniter-prover').duniter.methods.prover(server); + prover = require('../../../app/modules/prover').duniter.methods.prover(server); server.permaProver = prover.permaProver; server.pipe(prover); } diff --git a/yarn.lock b/yarn.lock index 593d66637..d3c1fba6d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -78,6 +78,10 @@ ansi-regex@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" +ansi-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" + ansi-styles@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" @@ -793,20 +797,6 @@ duniter-keypair@1.3.X, duniter-keypair@1.3.x: tweetnacl "0.14.5" tweetnacl-util "0.15.0" -duniter-prover@1.4.x: - version "1.4.0" - resolved "https://registry.yarnpkg.com/duniter-prover/-/duniter-prover-1.4.0.tgz#dabaa4408b4957366e7d7fea8144f217da65e79a" - dependencies: - async "2.2.0" - co "4.6.0" - duniter-common "1.3.x" - duniter-crawler "1.3.x" - inquirer "3.0.6" - moment "2.18.1" - node-uuid "1.4.8" - querablep "0.1.0" - underscore "1.8.3" - duniter-ui@1.3.x: version "1.3.11" resolved "https://registry.yarnpkg.com/duniter-ui/-/duniter-ui-1.3.11.tgz#de22d5bff5b8313e4a563b6fa994c746f1908c39" @@ -1771,13 +1761,20 @@ js-yaml@3.0.1: argparse "~ 0.1.11" esprima "~ 1.0.2" -js-yaml@3.8.2, js-yaml@3.x, js-yaml@^3.2.5, js-yaml@^3.5.1: +js-yaml@3.8.2: version "3.8.2" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.8.2.tgz#02d3e2c0f6beab20248d412c352203827d786721" dependencies: argparse "^1.0.7" esprima "^3.1.1" +js-yaml@3.x, js-yaml@^3.2.5, js-yaml@^3.5.1: + version "3.8.4" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.8.4.tgz#520b4564f86573ba96662af85a8cafa7b4b5a6f6" + dependencies: + argparse "^1.0.7" + esprima "^3.1.1" + jsbn@~0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" @@ -2179,7 +2176,7 @@ node-pre-gyp@0.6.34, node-pre-gyp@^0.6.34, node-pre-gyp@~0.6.28: tar "^2.2.1" tar-pack "^3.4.0" -node-uuid@1.4.8, node-uuid@~1.4.0: +node-uuid@~1.4.0: version "1.4.8" resolved "https://registry.yarnpkg.com/node-uuid/-/node-uuid-1.4.8.tgz#b040eb0923968afabf8d32fb1f17f1167fdab907" @@ -2210,8 +2207,8 @@ normalize-path@^2.0.0: remove-trailing-separator "^1.0.1" npmlog@^4.0.1, npmlog@^4.0.2: - version "4.1.0" - resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.0.tgz#dc59bee85f64f00ed424efb2af0783df25d1c0b5" + version "4.1.2" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" dependencies: are-we-there-yet "~1.1.2" console-control-strings "~1.1.0" @@ -2935,11 +2932,11 @@ string-width@^1.0.1, string-width@^1.0.2: strip-ansi "^3.0.0" string-width@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.0.0.tgz#635c5436cc72a6e0c387ceca278d4e2eec52687e" + version "2.1.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.0.tgz#030664561fc146c9423ec7d978fe2457437fe6d0" dependencies: is-fullwidth-code-point "^2.0.0" - strip-ansi "^3.0.0" + strip-ansi "^4.0.0" string_decoder@~0.10.x: version "0.10.31" @@ -2967,6 +2964,12 @@ strip-ansi@^3.0.0, strip-ansi@^3.0.1: dependencies: ansi-regex "^2.0.0" +strip-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + dependencies: + ansi-regex "^3.0.0" + strip-bom@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" @@ -3116,11 +3119,7 @@ tough-cookie@>=0.12.0, tough-cookie@~2.3.0: dependencies: punycode "^1.4.1" -traverse@>=0.2.4: - version "0.6.6" - resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.6.6.tgz#cbdf560fd7b9af632502fed40f918c157ea97137" - -"traverse@>=0.3.0 <0.4": +traverse@>=0.2.4, "traverse@>=0.3.0 <0.4": version "0.3.9" resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.3.9.tgz#717b8f220cc0bb7b44e40514c22b2e8bbc70d8b9" -- GitLab