mirror of https://github.com/BTCPrivate/z-nomp.git
commit
6937e7b6e2
|
@ -6,7 +6,6 @@ var async = require('async');
|
||||||
var Stratum = require('stratum-pool');
|
var Stratum = require('stratum-pool');
|
||||||
var util = require('stratum-pool/lib/util.js');
|
var util = require('stratum-pool/lib/util.js');
|
||||||
|
|
||||||
|
|
||||||
module.exports = function(logger){
|
module.exports = function(logger){
|
||||||
|
|
||||||
var poolConfigs = JSON.parse(process.env.pools);
|
var poolConfigs = JSON.parse(process.env.pools);
|
||||||
|
@ -53,12 +52,15 @@ function SetupForPool(logger, poolOptions, setupFinished){
|
||||||
|
|
||||||
var minConfShield = 3;
|
var minConfShield = 3;
|
||||||
var minConfPayout = 3;
|
var minConfPayout = 3;
|
||||||
|
|
||||||
|
var maxBlocksPerPayment = processingConfig.maxBlocksPerPayment || 3;
|
||||||
|
|
||||||
var requireShielding = poolOptions.coin.requireShielding === true;
|
var requireShielding = poolOptions.coin.requireShielding === true;
|
||||||
var fee = parseFloat(poolOptions.coin.txfee) || parseFloat(0.0004);
|
var fee = parseFloat(poolOptions.coin.txfee) || parseFloat(0.0004);
|
||||||
|
|
||||||
logger.special(logSystem, logComponent, logComponent + ' requireShielding: ' + requireShielding);
|
logger.debug(logSystem, logComponent, logComponent + ' requireShielding: ' + requireShielding);
|
||||||
logger.special(logSystem, logComponent, logComponent + ' payments txfee reserve: ' + fee);
|
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){
|
var daemon = new Stratum.daemon.interface([processingConfig.daemon], function(severity, message){
|
||||||
logger[severity](logSystem, logComponent, message);
|
logger[severity](logSystem, logComponent, message);
|
||||||
|
@ -291,8 +293,8 @@ function SetupForPool(logger, poolOptions, setupFinished){
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO, this needs to be moved out of payments processor
|
||||||
function cacheNetworkStats () {
|
function cacheNetworkStats () {
|
||||||
var params = null;
|
var params = null;
|
||||||
daemon.cmd('getmininginfo', params,
|
daemon.cmd('getmininginfo', params,
|
||||||
|
@ -381,19 +383,19 @@ function SetupForPool(logger, poolOptions, setupFinished){
|
||||||
}
|
}
|
||||||
if (op.status == "failed") {
|
if (op.status == "failed") {
|
||||||
if (op.error) {
|
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 {
|
} else {
|
||||||
logger.error(logSystem, logComponent, "Payment operation failed " + op.id);
|
logger.error(logSystem, logComponent, "Shielding operation failed " + op.id);
|
||||||
}
|
}
|
||||||
} else {
|
} 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);
|
}, true, true);
|
||||||
} else if (op.status == "executing") {
|
} else if (op.status == "executing") {
|
||||||
if (opidCount == 0) {
|
if (opidCount == 0) {
|
||||||
opidCount++;
|
opidCount++;
|
||||||
logger.special(logSystem, logComponent, 'Payment operation in progress ' + op.id );
|
logger.special(logSystem, logComponent, 'Shielding operation in progress ' + op.id );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -418,9 +420,18 @@ function SetupForPool(logger, poolOptions, setupFinished){
|
||||||
};
|
};
|
||||||
|
|
||||||
function balanceRound(number) {
|
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
|
/* Deal with numbers in smallest possible units (satoshis) as much as possible. This greatly helps with accuracy
|
||||||
when rounding and whatnot. When we are storing numbers for only humans to see, store in whole coin units. */
|
when rounding and whatnot. When we are storing numbers for only humans to see, store in whole coin units. */
|
||||||
|
|
||||||
|
@ -442,8 +453,7 @@ function SetupForPool(logger, poolOptions, setupFinished){
|
||||||
|
|
||||||
async.waterfall([
|
async.waterfall([
|
||||||
|
|
||||||
/* Call redis to get an array of rounds - which are coinbase transactions and block heights from submitted
|
/* Call redis to get an array of rounds and balances - which are coinbase transactions and block heights from submitted blocks. */
|
||||||
blocks. */
|
|
||||||
function(callback){
|
function(callback){
|
||||||
startRedisTimer();
|
startRedisTimer();
|
||||||
redisClient.multi([
|
redisClient.multi([
|
||||||
|
@ -451,29 +461,102 @@ function SetupForPool(logger, poolOptions, setupFinished){
|
||||||
['smembers', coin + ':blocksPending']
|
['smembers', coin + ':blocksPending']
|
||||||
]).exec(function(error, results){
|
]).exec(function(error, results){
|
||||||
endRedisTimer();
|
endRedisTimer();
|
||||||
|
|
||||||
if (error){
|
if (error){
|
||||||
logger.error(logSystem, logComponent, 'Could not get blocks from redis ' + JSON.stringify(error));
|
logger.error(logSystem, logComponent, 'Could not get blocks from redis ' + JSON.stringify(error));
|
||||||
callback(true);
|
callback(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// build worker balances
|
||||||
var workers = {};
|
var workers = {};
|
||||||
for (var w in results[0]){
|
for (var w in results[0]){
|
||||||
workers[w] = {balance: coinsToSatoshies(parseFloat(results[0][w]))};
|
workers[w] = {balance: coinsToSatoshies(parseFloat(results[0][w]))};
|
||||||
}
|
}
|
||||||
|
// build initial rounds data from blocksPending
|
||||||
var rounds = results[1].map(function(r){
|
var rounds = results[1].map(function(r){
|
||||||
var details = r.split(':');
|
var details = r.split(':');
|
||||||
return {
|
return {
|
||||||
blockHash: details[0],
|
blockHash: details[0],
|
||||||
txHash: details[1],
|
txHash: details[1],
|
||||||
height: details[2],
|
height: details[2],
|
||||||
|
minedby: details[3],
|
||||||
|
duplicate: false,
|
||||||
serialized: r
|
serialized: r
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
// find duplicate blocks by height
|
||||||
callback(null, workers, rounds);
|
// 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);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -528,58 +611,12 @@ function SetupForPool(logger, poolOptions, setupFinished){
|
||||||
callback(true);
|
callback(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// check for invalid blocks by block hash
|
// get pending block transaction details from coin daemon
|
||||||
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
|
|
||||||
var batchRPCcommand = rounds.map(function(r){
|
var batchRPCcommand = rounds.map(function(r){
|
||||||
return ['gettransaction', [r.txHash]];
|
return ['gettransaction', [r.txHash]];
|
||||||
});
|
});
|
||||||
// guarantee a response for batchRPCcommand
|
// get account address (not implemented in zcash at this time..)
|
||||||
batchRPCcommand.push(['getaccount', [poolOptions.address]]);
|
batchRPCcommand.push(['getaccount', [poolOptions.address]]);
|
||||||
|
|
||||||
startRPCTimer();
|
startRPCTimer();
|
||||||
|
@ -653,25 +690,40 @@ function SetupForPool(logger, poolOptions, setupFinished){
|
||||||
return true;
|
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){
|
rounds = rounds.filter(function(r){
|
||||||
|
|
||||||
|
// only pay max blocks at a time
|
||||||
|
if (payingBlocks >= maxBlocksPerPayment)
|
||||||
|
return false;
|
||||||
|
|
||||||
switch (r.category) {
|
switch (r.category) {
|
||||||
case 'orphan':
|
case 'orphan':
|
||||||
case 'kicked':
|
case 'kicked':
|
||||||
r.canDeleteShares = canDeleteShares(r);
|
r.canDeleteShares = canDeleteShares(r);
|
||||||
|
return true;
|
||||||
case 'generate':
|
case 'generate':
|
||||||
|
payingBlocks++;
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// TODO: make tx fees dynamic
|
||||||
var feeSatoshi = fee * magnitude;
|
var feeSatoshi = fee * magnitude;
|
||||||
|
|
||||||
// calculate what the pool owes its miners
|
// calculate what the pool owes its miners
|
||||||
var totalOwed = parseInt(0);
|
var totalOwed = parseInt(0);
|
||||||
for (var i = 0; i < rounds.length; i++) {
|
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;
|
var notAddr = null;
|
||||||
|
@ -682,10 +734,10 @@ function SetupForPool(logger, poolOptions, setupFinished){
|
||||||
// check if we have enough tAddress funds to brgin payment processing
|
// check if we have enough tAddress funds to brgin payment processing
|
||||||
listUnspent(null, notAddr, minConfPayout, false, function (error, tBalance){
|
listUnspent(null, notAddr, minConfPayout, false, function (error, tBalance){
|
||||||
if (error) {
|
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);
|
return callback(true);
|
||||||
} else if (tBalance < totalOwed) {
|
} 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);
|
return callback(true);
|
||||||
} else {
|
} else {
|
||||||
// zcash daemon does not support account feature
|
// 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
|
/* 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
|
Get balance different for each address and pass it along as object of latest balances such as
|
||||||
{worker1: balance1, worker2, balance2}
|
{worker1: balance1, worker2, balance2}
|
||||||
|
|
|
@ -30,6 +30,7 @@
|
||||||
"paymentInterval": 57,
|
"paymentInterval": 57,
|
||||||
"_comment_paymentInterval": "Interval in seconds to check and perform payments.",
|
"_comment_paymentInterval": "Interval in seconds to check and perform payments.",
|
||||||
"minimumPayment": 0.1,
|
"minimumPayment": 0.1,
|
||||||
|
"maxBlocksPerPayment": 3,
|
||||||
"daemon": {
|
"daemon": {
|
||||||
"host": "127.0.0.1",
|
"host": "127.0.0.1",
|
||||||
"port": 8232,
|
"port": 8232,
|
||||||
|
|
|
@ -22,6 +22,7 @@
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"paymentInterval": 20,
|
"paymentInterval": 20,
|
||||||
"minimumPayment": 0.1,
|
"minimumPayment": 0.1,
|
||||||
|
"maxBlocksPerPayment": 3,
|
||||||
"daemon": {
|
"daemon": {
|
||||||
"host": "127.0.0.1",
|
"host": "127.0.0.1",
|
||||||
"port": 19332,
|
"port": 19332,
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"paymentInterval": 20,
|
"paymentInterval": 20,
|
||||||
"minimumPayment": 0.1,
|
"minimumPayment": 0.1,
|
||||||
|
"maxBlocksPerPayment": 1,
|
||||||
"daemon": {
|
"daemon": {
|
||||||
"host": "127.0.0.1",
|
"host": "127.0.0.1",
|
||||||
"port": 19332,
|
"port": 19332,
|
||||||
|
|
|
@ -27,6 +27,7 @@
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"paymentInterval": 20,
|
"paymentInterval": 20,
|
||||||
"minimumPayment": 0.1,
|
"minimumPayment": 0.1,
|
||||||
|
"maxBlocksPerPayment": 3,
|
||||||
"daemon": {
|
"daemon": {
|
||||||
"host": "127.0.0.1",
|
"host": "127.0.0.1",
|
||||||
"port": 8232,
|
"port": 8232,
|
||||||
|
|
Loading…
Reference in New Issue