Payment processing seems to be working well

This commit is contained in:
Matt 2014-03-27 16:29:43 -06:00
parent fc56d94092
commit 4a651dec89
5 changed files with 142 additions and 77 deletions

View File

@ -1,4 +1,4 @@
# NOMP # NOMP ![NOMP Logo](http://zone117x.github.io/node-open-mining-portal/logo.svg "NOMP Logo")
#### Node Open Mining Portal #### Node Open Mining Portal
This portal is an extremely efficient, highly scalable, all-in-one, easy to setup cryptocurrency mining pool written This portal is an extremely efficient, highly scalable, all-in-one, easy to setup cryptocurrency mining pool written

View File

@ -62,9 +62,9 @@ function SetupForPool(logger, poolOptions){
redisClient.on('ready', function(){ redisClient.on('ready', function(){
clearTimeout(reconnectTimeout); clearTimeout(reconnectTimeout);
logger.debug(logSystem, logComponent, 'Successfully connected to redis database'); logger.debug(logSystem, logComponent, 'Successfully connected to redis database');
}).on('error', function(err){ })/*).on('error', function(err){
logger.error(logSystem, logComponent, 'Redis client had an error: ' + JSON.stringify(err)) logger.error(logSystem, logComponent, 'Redis client had an error: ' + JSON.stringify(err))
}).on('end', function(){ })*/.on('end', function(){
logger.error(logSystem, logComponent, 'Connection to redis database as been ended'); logger.error(logSystem, logComponent, 'Connection to redis database as been ended');
logger.warning(logSystem, logComponent, 'Trying reconnection to redis in 3 seconds...'); logger.warning(logSystem, logComponent, 'Trying reconnection to redis in 3 seconds...');
reconnectTimeout = setTimeout(function(){ reconnectTimeout = setTimeout(function(){
@ -86,6 +86,10 @@ function SetupForPool(logger, poolOptions){
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. */
var processPayments = function(){ var processPayments = function(){
var startPaymentProcess = Date.now();
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 - which are coinbase transactions and block heights from submitted
@ -108,10 +112,10 @@ function SetupForPool(logger, poolOptions){
var details = r.split(':'); var details = r.split(':');
return { return {
category: details[0].category, category: details[0].category,
txHash: details[0], solution: details[0],
height: details[1], txHash: details[1],
reward: details[2], height: details[2],
solution: details[3], reward: details[3],
serialized: r serialized: r
}; };
}); });
@ -120,6 +124,29 @@ function SetupForPool(logger, poolOptions){
}); });
}, },
/* First get data from all pending blocks via batch RPC call, we need the coinbase txHash. */
/*(function(rounds, callback){
var batchRPCcommand = rounds.map(function(r){
return ['getblock', [r.solution]];
});
daemon.batchCmd(batchRPCcommand, function(error, blocks) {
if (error || !blocks) {
callback('Check finished - daemon rpc error with batch gettransactions ' +
JSON.stringify(error));
return;
}
blocks.forEach(function (b, i) {
if (b.error || b.result.hash !== rounds[i].solution){
logger.error(logSystem, logComponent, "Did daemon drop a block? " + rounds[i].solution);
return;
}
rounds[i].txHash = b.result.tx[0];
});
callback(null, rounds);
});
},*/
/* Does a batch rpc call to daemon with all the transaction hashes to see if they are confirmed yet. /* Does a batch rpc call to daemon with all the transaction hashes to see if they are confirmed yet.
It also adds the block reward amount to the round object - which the daemon gives also gives us. */ It also adds the block reward amount to the round object - which the daemon gives also gives us. */
@ -138,23 +165,39 @@ function SetupForPool(logger, poolOptions){
} }
txDetails.forEach(function(tx, i){ txDetails.forEach(function(tx, i){
if (tx.error && tx.error.code === -5){ var round = rounds[i];
/* Block was dropped from coin daemon even after it happily accepted it earlier.
Must be a bug in the daemon code or maybe only something that happens in testnet.
We handle this by treating it like an orphaned block. */
logger.error(logSystem, logComponent,
'Daemon dropped a previously valid block - we are treating it as an orphaned block');
//These changes to the tx will convert it from dropped to orphan if (tx.error && tx.error.code === -5){
tx.result = {
txid: rounds[i].txHash, /* Block was dropped from coin daemon even after it happily accepted it earlier. */
details: [{category: 'orphan'}]
}; //If we find another block at the same height then this block was drop-kicked orphaned
var dropKicked = !!rounds.filter(function(r){
return r.height === round.height && r.solution !== round.solution && r.category !== 'dropkicked';
}).length;
if (dropKicked){
logger.warning(logSystem, logComponent,
'A block was drop-kicked orphaned'
+ ' - we found a better block at the same height, solution '
+ round.solution + " round " + round.height);
round.category = 'dropkicked';
}
else{
/* We have no other blocks that match this height so convert to orphan in order for
shares from the round to be rewarded. */
round.category = 'orphan';
}
} }
else if (tx.error || !tx.result){ else if (tx.error || !tx.result){
logger.error(logSystem, logComponent, logger.error(logSystem, logComponent,
'error with requesting transaction from block daemon: ' + JSON.stringify(tx)); 'error with requesting transaction from block daemon: ' + JSON.stringify(tx));
} }
else{
round.category = tx.result.details[0].category;
if (round.category === 'generate')
round.amount = tx.result.amount;
}
}); });
@ -162,44 +205,37 @@ function SetupForPool(logger, poolOptions){
//Filter out all rounds that are immature (not confirmed or orphaned yet) //Filter out all rounds that are immature (not confirmed or orphaned yet)
rounds = rounds.filter(function(r){ rounds = rounds.filter(function(r){
var tx = txDetails.filter(function(tx){return tx.result.txid === r.txHash})[0]; switch (r.category) {
if (!tx){ case 'generate':
logger.error(logSystem, logComponent, /* Here we calculate the smallest unit in this coin's currency; the 'satoshi'.
'daemon did not give us back a transaction that we asked for: ' + r.txHash); The rpc.getblocktemplate.amount tells us how much we get in satoshis, while the
return; rpc.gettransaction.amount tells us how much we get in whole coin units. Therefore,
} we simply divide the two to get the magnitude. I don't know math, there is probably
a better term than 'magnitude'. Sue me or do a pull request to fix it. */
var roundMagnitude = r.reward / r.amount;
r.category = tx.result.details[0].category; if (!magnitude) {
magnitude = roundMagnitude;
if (r.category === 'generate'){ if (roundMagnitude % 10 !== 0)
r.amount = tx.result.amount; logger.error(logSystem, logComponent,
'Satosihis in coin is not divisible by 10 which is very odd');
/* Here we calculate the smallest unit in this coin's currency; the 'satoshi'. }
The rpc.getblocktemplate.amount tells us how much we get in satoshis, while the else if (magnitude != roundMagnitude) {
rpc.gettransaction.amount tells us how much we get in whole coin units. Therefore, /* Magnitude for a coin should ALWAYS be the same. For BTC and most coins there are
we simply divide the two to get the magnitude. I don't know math, there is probably 100,000,000 satoshis in one coin unit. */
a better term than 'magnitude'. Sue me or do a pull request to fix it. */
var roundMagnitude = r.reward / r.amount;
if (!magnitude){
magnitude = roundMagnitude;
if (roundMagnitude % 10 !== 0)
logger.error(logSystem, logComponent, logger.error(logSystem, logComponent,
'Satosihis in coin is not divisible by 10 which is very odd'); 'Magnitude in a round was different than in another round. HUGE PROBLEM.');
} }
else if (magnitude != roundMagnitude){ return true;
/* Magnitude for a coin should ALWAYS be the same. For BTC and most coins there are
100,000,000 satoshis in one coin unit. */
logger.error(logSystem, logComponent,
'Magnitude in a round was different than in another round. HUGE PROBLEM.');
}
return true;
}
else if (r.category === 'orphan')
return true;
case 'dropkicked':
case 'orphan':
return true;
default:
return false;
}
}); });
@ -236,32 +272,42 @@ function SetupForPool(logger, poolOptions){
rounds.forEach(function(round, i){ rounds.forEach(function(round, i){
var workerShares = allWorkerShares[i]; var workerShares = allWorkerShares[i];
if (round.category === 'orphan'){ if (!workerShares){
/* Each block that gets orphaned, all the shares go into the current round so that miners logger.error(logSystem, logComponent, 'No worker shares for round: '
still get a reward for their work. This seems unfair to those that just started mining + round.height + ' solution: ' + round.solution);
during this current round, but over time it balances out and rewards loyal miners. */ return;
Object.keys(workerShares).forEach(function(worker){
orphanMergeCommands.push(['hincrby', coin + '_shares:roundCurrent',
worker, workerShares[worker]]);
});
} }
else if (round.category === 'generate'){
/* We found a confirmed block! Now get the reward for it and calculate how much switch (round.category){
we owe each miner based on the shares they submitted during that block round. */ case 'orphan':
var reward = round.reward * (1 - processingConfig.feePercent); /* Each block that gets orphaned, all the shares go into the current round so that
miners still get a reward for their work. This seems unfair to those that just
started mining during this current round, but over time it balances out and rewards
loyal miners. */
Object.keys(workerShares).forEach(function(worker){
orphanMergeCommands.push(['hincrby', coin + '_shares:roundCurrent',
worker, workerShares[worker]]);
});
break;
var totalShares = Object.keys(workerShares).reduce(function(p, c){ case 'generate':
return p + parseInt(workerShares[c]) /* We found a confirmed block! Now get the reward for it and calculate how much
}, 0); we owe each miner based on the shares they submitted during that block round. */
var reward = round.reward * (1 - processingConfig.feePercent);
for (var worker in workerShares){ var totalShares = Object.keys(workerShares).reduce(function(p, c){
var percent = parseInt(workerShares[worker]) / totalShares; return p + parseInt(workerShares[c])
var workerRewardTotal = Math.floor(reward * percent); }, 0);
if (!(worker in workerRewards)) workerRewards[worker] = 0;
workerRewards[worker] += workerRewardTotal; for (var worker in workerShares){
} var percent = parseInt(workerShares[worker]) / totalShares;
var workerRewardTotal = Math.floor(reward * percent);
if (!(worker in workerRewards)) workerRewards[worker] = 0;
workerRewards[worker] += workerRewardTotal;
}
break;
} }
}); });
callback(null, rounds, magnitude, workerRewards, orphanMergeCommands); callback(null, rounds, magnitude, workerRewards, orphanMergeCommands);
@ -380,7 +426,14 @@ function SetupForPool(logger, poolOptions){
var movePendingCommands = []; var movePendingCommands = [];
var roundsToDelete = []; var roundsToDelete = [];
rounds.forEach(function(r){ rounds.forEach(function(r){
var destinationSet = r.category === 'orphan' ? '_blocksOrphaned' : '_blocksConfirmed';
var destinationSet = (function(){
switch(r.category){
case 'orphan': return '_blocksOrphaned';
case 'generate': return '_blocksConfirmed';
case 'dropkicked': return '_blocksDropKicked';
}
})();
movePendingCommands.push(['smove', coin + '_blocksPending', coin + destinationSet, r.serialized]); movePendingCommands.push(['smove', coin + '_blocksPending', coin + destinationSet, r.serialized]);
roundsToDelete.push(coin + '_shares:round' + r.height) roundsToDelete.push(coin + '_shares:round' + r.height)
}); });
@ -477,11 +530,15 @@ function SetupForPool(logger, poolOptions){
} }
} }
], function(error, result){ ], function(error, result){
var paymentProcessTime = Date.now() - startPaymentProcess;
if (error) if (error)
logger.debug(logSystem, logComponent, error); logger.debug(logSystem, logComponent, '[' + paymentProcessTime + 'ms] ' + error);
else{ else{
logger.debug(logSystem, logComponent, result); logger.debug(logSystem, logComponent, '[' + paymentProcessTime + 'ms] ' + result);
// not sure if we need some time to let daemon update the wallet balance // not sure if we need some time to let daemon update the wallet balance
setTimeout(withdrawalProfit, 1000); setTimeout(withdrawalProfit, 1000);
} }
@ -523,7 +580,13 @@ function SetupForPool(logger, poolOptions){
}; };
setInterval(processPayments, processingConfig.paymentInterval * 1000); setInterval(function(){
try {
processPayments();
} catch(e){
throw e;
}
}, processingConfig.paymentInterval * 1000);
setTimeout(processPayments, 100); setTimeout(processPayments, 100);
}; };

View File

@ -69,7 +69,7 @@ module.exports = function(logger, poolConfig){
if (isValidBlock){ if (isValidBlock){
redisCommands.push(['rename', coin + '_shares:roundCurrent', coin + '_shares:round' + shareData.height]); redisCommands.push(['rename', coin + '_shares:roundCurrent', coin + '_shares:round' + shareData.height]);
redisCommands.push(['sadd', coin + '_blocksPending', shareData.tx + ':' + shareData.height + ':' + shareData.reward + ':' + shareData.solution]); redisCommands.push(['sadd', coin + '_blocksPending', [shareData.solution, shareData.tx, shareData.height, shareData.reward].join(':')]);
redisCommands.push(['hincrby', coin + '_stats', 'validBlocks', 1]); redisCommands.push(['hincrby', coin + '_stats', 'validBlocks', 1]);
} }
else if (shareData.solution){ else if (shareData.solution){

View File

@ -4,6 +4,8 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<link rel="icon" type="image/png" href="/static/favicon.png"/>
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/font-awesome/4.0.3/css/font-awesome.min.css"> <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/font-awesome/4.0.3/css/font-awesome.min.css">
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/pure/0.4.2/pure-min.css"> <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/pure/0.4.2/pure-min.css">
<script src="//cdnjs.cloudflare.com/ajax/libs/Chart.js/0.2.0/Chart.min.js"></script> <script src="//cdnjs.cloudflare.com/ajax/libs/Chart.js/0.2.0/Chart.min.js"></script>

BIN
website/static/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 413 B