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,19 +205,9 @@ 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){
logger.error(logSystem, logComponent,
'daemon did not give us back a transaction that we asked for: ' + r.txHash);
return;
}
r.category = tx.result.details[0].category;
if (r.category === 'generate'){
r.amount = tx.result.amount;
case 'generate':
/* Here we calculate the smallest unit in this coin's currency; the 'satoshi'. /* 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 The rpc.getblocktemplate.amount tells us how much we get in satoshis, while the
rpc.gettransaction.amount tells us how much we get in whole coin units. Therefore, rpc.gettransaction.amount tells us how much we get in whole coin units. Therefore,
@ -182,24 +215,27 @@ function SetupForPool(logger, poolOptions){
a better term than 'magnitude'. Sue me or do a pull request to fix it. */ a better term than 'magnitude'. Sue me or do a pull request to fix it. */
var roundMagnitude = r.reward / r.amount; var roundMagnitude = r.reward / r.amount;
if (!magnitude){ if (!magnitude) {
magnitude = roundMagnitude; magnitude = roundMagnitude;
if (roundMagnitude % 10 !== 0) if (roundMagnitude % 10 !== 0)
logger.error(logSystem, logComponent, logger.error(logSystem, logComponent,
'Satosihis in coin is not divisible by 10 which is very odd'); 'Satosihis in coin is not divisible by 10 which is very odd');
} }
else if (magnitude != roundMagnitude){ else if (magnitude != roundMagnitude) {
/* Magnitude for a coin should ALWAYS be the same. For BTC and most coins there are /* Magnitude for a coin should ALWAYS be the same. For BTC and most coins there are
100,000,000 satoshis in one coin unit. */ 100,000,000 satoshis in one coin unit. */
logger.error(logSystem, logComponent, logger.error(logSystem, logComponent,
'Magnitude in a round was different than in another round. HUGE PROBLEM.'); 'Magnitude in a round was different than in another round. HUGE PROBLEM.');
} }
return true; return true;
}
else if (r.category === 'orphan')
return true;
case 'dropkicked':
case 'orphan':
return true;
default:
return false;
}
}); });
@ -236,17 +272,25 @@ 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;
}
switch (round.category){
case 'orphan':
/* 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){ Object.keys(workerShares).forEach(function(worker){
orphanMergeCommands.push(['hincrby', coin + '_shares:roundCurrent', orphanMergeCommands.push(['hincrby', coin + '_shares:roundCurrent',
worker, workerShares[worker]]); worker, workerShares[worker]]);
}); });
} break;
else if (round.category === 'generate'){
case 'generate':
/* We found a confirmed block! Now get the reward for it and calculate how much /* We found a confirmed block! Now get the reward for it and calculate how much
we owe each miner based on the shares they submitted during that block round. */ we owe each miner based on the shares they submitted during that block round. */
var reward = round.reward * (1 - processingConfig.feePercent); var reward = round.reward * (1 - processingConfig.feePercent);
@ -261,7 +305,9 @@ function SetupForPool(logger, poolOptions){
if (!(worker in workerRewards)) workerRewards[worker] = 0; if (!(worker in workerRewards)) workerRewards[worker] = 0;
workerRewards[worker] += workerRewardTotal; 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