Merge pull request #116 from hellcatz/patch-4

Solve Issue #86, #113
This commit is contained in:
Procrastinator 2017-04-20 16:05:23 -04:00 committed by GitHub
commit 6937e7b6e2
5 changed files with 127 additions and 72 deletions

View File

@ -6,7 +6,6 @@ var async = require('async');
var Stratum = require('stratum-pool');
var util = require('stratum-pool/lib/util.js');
module.exports = function(logger){
var poolConfigs = JSON.parse(process.env.pools);
@ -54,11 +53,14 @@ function SetupForPool(logger, poolOptions, setupFinished){
var minConfShield = 3;
var minConfPayout = 3;
var maxBlocksPerPayment = processingConfig.maxBlocksPerPayment || 3;
var requireShielding = poolOptions.coin.requireShielding === true;
var fee = parseFloat(poolOptions.coin.txfee) || parseFloat(0.0004);
logger.special(logSystem, logComponent, logComponent + ' requireShielding: ' + requireShielding);
logger.special(logSystem, logComponent, logComponent + ' payments txfee reserve: ' + fee);
logger.debug(logSystem, logComponent, logComponent + ' requireShielding: ' + requireShielding);
logger.debug(logSystem, logComponent, logComponent + ' payments txfee reserve: ' + fee);
logger.debug(logSystem, logComponent, logComponent + ' maxBlocksPerPayment: ' + maxBlocksPerPayment);
var daemon = new Stratum.daemon.interface([processingConfig.daemon], function(severity, message){
logger[severity](logSystem, logComponent, message);
@ -292,7 +294,7 @@ function SetupForPool(logger, poolOptions, setupFinished){
);
}
// TODO, this needs to be moved out of payments processor
function cacheNetworkStats () {
var params = null;
daemon.cmd('getmininginfo', params,
@ -381,19 +383,19 @@ function SetupForPool(logger, poolOptions, setupFinished){
}
if (op.status == "failed") {
if (op.error) {
logger.error(logSystem, logComponent, "Payment operation failed " + op.id + " " + op.error.code +", " + op.error.message);
logger.error(logSystem, logComponent, "Shielding operation failed " + op.id + " " + op.error.code +", " + op.error.message);
} else {
logger.error(logSystem, logComponent, "Payment operation failed " + op.id);
logger.error(logSystem, logComponent, "Shielding operation failed " + op.id);
}
} else {
logger.special(logSystem, logComponent, 'Payment operation success ' + op.id + ' txid: ' + op.result.txid);
logger.special(logSystem, logComponent, 'Shielding operation success ' + op.id + ' txid: ' + op.result.txid);
}
}
}, true, true);
} else if (op.status == "executing") {
if (opidCount == 0) {
opidCount++;
logger.special(logSystem, logComponent, 'Payment operation in progress ' + op.id );
logger.special(logSystem, logComponent, 'Shielding operation in progress ' + op.id );
}
}
});
@ -418,7 +420,16 @@ function SetupForPool(logger, poolOptions, setupFinished){
};
function balanceRound(number) {
return parseFloat((Math.round(number * 100000000) / 100000000).toFixed(8));
return parseFloat((Math.round(number * 100000000) / 100000000).toFixed(8));
}
function checkForDuplicateBlockHeight(rounds, height) {
var count = 0;
for (var i = 0; i < rounds.length; i++) {
if (rounds[i].height == height)
count++;
}
return count > 1;
}
/* Deal with numbers in smallest possible units (satoshis) as much as possible. This greatly helps with accuracy
@ -442,8 +453,7 @@ function SetupForPool(logger, poolOptions, setupFinished){
async.waterfall([
/* Call redis to get an array of rounds - which are coinbase transactions and block heights from submitted
blocks. */
/* Call redis to get an array of rounds and balances - which are coinbase transactions and block heights from submitted blocks. */
function(callback){
startRedisTimer();
redisClient.multi([
@ -451,29 +461,102 @@ function SetupForPool(logger, poolOptions, setupFinished){
['smembers', coin + ':blocksPending']
]).exec(function(error, results){
endRedisTimer();
if (error){
logger.error(logSystem, logComponent, 'Could not get blocks from redis ' + JSON.stringify(error));
callback(true);
return;
}
// build worker balances
var workers = {};
for (var w in results[0]){
workers[w] = {balance: coinsToSatoshies(parseFloat(results[0][w]))};
}
// build initial rounds data from blocksPending
var rounds = results[1].map(function(r){
var details = r.split(':');
return {
blockHash: details[0],
txHash: details[1],
height: details[2],
minedby: details[3],
duplicate: false,
serialized: r
};
});
callback(null, workers, rounds);
// find duplicate blocks by height
// this can happen when two or more solutions are submitted at the same block height
var duplicateFound = false;
for (var i = 0; i < rounds.length; i++) {
if (checkForDuplicateBlockHeight(rounds, rounds[i].height) === true) {
rounds[i].duplicate = true;
duplicateFound = true;
}
}
// handle duplicates if needed
if (duplicateFound) {
var dups = rounds.filter(function(round){ return round.duplicate; });
logger.warning(logSystem, logComponent, 'Duplicate pending blocks found: ' + JSON.stringify(dups));
// attempt to find the invalid duplicates
var rpcDupCheck = dups.map(function(r){
return ['getblock', [r.blockHash]];
});
startRPCTimer();
daemon.batchCmd(rpcDupCheck, function(error, blocks){
endRPCTimer();
if (error || !blocks) {
logger.error(logSystem, logComponent, 'Error with duplicate block check rpc call getblock ' + JSON.stringify(error));
return;
}
// look for the invalid duplicate block
var validBlocks = {}; // hashtable for unique look up
var invalidBlocks = []; // array for redis work
blocks.forEach(function(block, i) {
if (block && block.result) {
// invalid duplicate submit blocks have negative confirmations
if (block.result.confirmations < 0) {
logger.warning(logSystem, logComponent, 'Remove invalid duplicate block ' + block.result.height + ' > ' + block.result.hash);
// move from blocksPending to blocksDuplicate...
invalidBlocks.push(['smove', coin + ':blocksPending', coin + ':blocksDuplicate', dups[i].serialized]);
} else {
// block must be valid, make sure it is unique
if (validBlocks.hasOwnProperty(dups[i].blockHash)) {
// not unique duplicate block
logger.warning(logSystem, logComponent, 'Remove non-unique duplicate block ' + block.result.height + ' > ' + block.result.hash);
// move from blocksPending to blocksDuplicate...
invalidBlocks.push(['smove', coin + ':blocksPending', coin + ':blocksDuplicate', dups[i].serialized]);
} else {
// keep unique valid block
validBlocks[dups[i].blockHash] = dups[i].serialized;
logger.debug(logSystem, logComponent, 'Keep valid duplicate block ' + block.result.height + ' > ' + block.result.hash);
}
}
}
});
// filter out all duplicates to prevent double payments
rounds = rounds.filter(function(round){ return !round.duplicate; });
// if we detected the invalid duplicates, move them
if (invalidBlocks.length > 0) {
// move invalid duplicate blocks in redis
startRedisTimer();
redisClient.multi(invalidBlocks).exec(function(error, kicked){
endRedisTimer();
if (error) {
logger.error(logSystem, logComponent, 'Error could not move invalid duplicate blocks in redis ' + JSON.stringify(error));
}
// continue payments normally
callback(null, workers, rounds);
});
} else {
// notify pool owner that we are unable to find the invalid duplicate blocks, manual intervention required...
logger.error(logSystem, logComponent, 'Unable to detect invalid duplicate blocks, duplicate block payments on hold.');
// continue payments normally
callback(null, workers, rounds);
}
});
} else {
// no duplicates, continue payments normally
callback(null, workers, rounds);
}
});
},
@ -529,57 +612,11 @@ function SetupForPool(logger, poolOptions, setupFinished){
return;
}
// check for invalid blocks by block hash
blockDetails.forEach(function(block, i) {
// this is just the response from getblockcount
if (i === blockDetails.length - 1){
return;
}
// help track duplicate or invalid blocks by block hash
if (block && block.result && block.result.hash) {
// find the round for this block hash
for (var k=0; k < rounds.length; k++) {
if (rounds[k].blockHash == block.result.hash) {
var round = rounds[k];
var dupFound = false;
// duplicate, invalid, kicked, orphaned blocks will have negative confirmations
if (block.result.confirmations < 0) {
// check if this is an invalid duplicate
// we need to kick invalid duplicates now, as this will cause a double payout...
for (var d=0; d < rounds.length; d++) {
if (rounds[d].height == block.result.height && rounds[d].blockHash != block.result.hash) {
logger.warning(logSystem, logComponent, 'Kicking invalid duplicate block ' +round.height + ' > ' + round.blockHash);
dupFound = true;
// kick this round now, its completely invalid!
var kickNow = [];
kickNow.push(['smove', coin + ':blocksPending', coin + ':blocksDuplicate', round.serialized]);
startRedisTimer();
redisClient.multi(kickNow).exec(function(error, kicked){
endRedisTimer();
if (error){
logger.error(logSystem, logComponent, 'Error could not kick invalid duplicate block ' + JSON.stringify(kicked));
}
});
// filter the duplicate out now, just in case we are actually paying this time around...
rounds = rounds.filter(function(item){ return item.txHash != round.txHash; });
}
}
// unknown reason why this block failed, possible orphan or kicked soon
// not sure if we should take any action or just wait it out...
if (!dupFound) {
logger.warning(logSystem, logComponent, 'Daemon reports negative confirmations '+block.result.confirmations+' for block: ' +round.height + ' > ' + round.blockHash);
}
}
}
}
}
});
// now check block transaction ids
// get pending block transaction details from coin daemon
var batchRPCcommand = rounds.map(function(r){
return ['gettransaction', [r.txHash]];
});
// guarantee a response for batchRPCcommand
// get account address (not implemented in zcash at this time..)
batchRPCcommand.push(['getaccount', [poolOptions.address]]);
startRPCTimer();
@ -653,25 +690,40 @@ function SetupForPool(logger, poolOptions, setupFinished){
return true;
};
//Filter out all rounds that are immature (not confirmed or orphaned yet)
// limit blocks paid per payment round
var payingBlocks = 0;
//filter out all rounds that are immature (not confirmed or orphaned yet)
rounds = rounds.filter(function(r){
// only pay max blocks at a time
if (payingBlocks >= maxBlocksPerPayment)
return false;
switch (r.category) {
case 'orphan':
case 'kicked':
r.canDeleteShares = canDeleteShares(r);
case 'generate':
return true;
case 'generate':
payingBlocks++;
return true;
default:
return false;
}
});
// TODO: make tx fees dynamic
var feeSatoshi = fee * magnitude;
// calculate what the pool owes its miners
var totalOwed = parseInt(0);
for (var i = 0; i < rounds.length; i++) {
totalOwed = totalOwed + Math.round(rounds[i].reward * magnitude) - feeSatoshi; // TODO: make tx fees dynamic
// only pay generated blocks, not orphaned or kicked
if (rounds[i].category == 'generate') {
totalOwed = totalOwed + Math.round(rounds[i].reward * magnitude) - feeSatoshi;
}
}
var notAddr = null;
@ -682,10 +734,10 @@ function SetupForPool(logger, poolOptions, setupFinished){
// check if we have enough tAddress funds to brgin payment processing
listUnspent(null, notAddr, minConfPayout, false, function (error, tBalance){
if (error) {
logger.error(logSystem, logComponent, 'Error checking pool balance before processing payments. (Unable to begin payment process)');
logger.error(logSystem, logComponent, 'Error checking pool balance before processing payments.');
return callback(true);
} else if (tBalance < totalOwed) {
logger.error(logSystem, logComponent, 'Insufficient funds to process payments ('+(tBalance / magnitude).toFixed(8) + ' < ' + (totalOwed / magnitude).toFixed(8)+'). Possibly waiting for shielding process.');
logger.error(logSystem, logComponent, 'Insufficient funds to process payments for ' + payingBlocks + ' blocks ('+(tBalance / magnitude).toFixed(8) + ' < ' + (totalOwed / magnitude).toFixed(8)+'). Possibly waiting for shielding process.');
return callback(true);
} else {
// zcash daemon does not support account feature
@ -756,7 +808,6 @@ function SetupForPool(logger, poolOptions, setupFinished){
});
},
/* Calculate if any payments are ready to be sent and trigger them sending
Get balance different for each address and pass it along as object of latest balances such as
{worker1: balance1, worker2, balance2}

View File

@ -30,6 +30,7 @@
"paymentInterval": 57,
"_comment_paymentInterval": "Interval in seconds to check and perform payments.",
"minimumPayment": 0.1,
"maxBlocksPerPayment": 3,
"daemon": {
"host": "127.0.0.1",
"port": 8232,

View File

@ -22,6 +22,7 @@
"enabled": false,
"paymentInterval": 20,
"minimumPayment": 0.1,
"maxBlocksPerPayment": 3,
"daemon": {
"host": "127.0.0.1",
"port": 19332,

View File

@ -23,6 +23,7 @@
"enabled": false,
"paymentInterval": 20,
"minimumPayment": 0.1,
"maxBlocksPerPayment": 1,
"daemon": {
"host": "127.0.0.1",
"port": 19332,

View File

@ -27,6 +27,7 @@
"enabled": true,
"paymentInterval": 20,
"minimumPayment": 0.1,
"maxBlocksPerPayment": 3,
"daemon": {
"host": "127.0.0.1",
"port": 8232,