Skip to content
Snippets Groups Projects
Commit 05e89d84 authored by Cédric Moreau's avatar Cédric Moreau
Browse files

[enh] #1022 Reintroduce duniter-prover into Duniter core

parent 15eb7659
Branches
Tags
No related merge requests found
Showing
with 2245 additions and 15 deletions
......@@ -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.
......
"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);
}
This diff is collapsed.
"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
}
}
"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
};
"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
}
"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';
}
}
}
"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')
}
"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();
});
});
}
"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);
......@@ -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": {
......
......@@ -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..."
......
"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)
}))
});
"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);
}));
});
"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')
}
}));
});
......@@ -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));
};
}
......
......@@ -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');
......
......@@ -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);
......
......@@ -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();
......
......@@ -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
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment