diff --git a/.gitignore b/.gitignore index 88d9a093..2e0af75e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules/ .idea/ -config.json \ No newline at end of file +config.json +logs/ diff --git a/README.md b/README.md index 59efd9d9..4cc1b499 100644 --- a/README.md +++ b/README.md @@ -232,6 +232,7 @@ Explanation for each field: "enabled": true, "interval": 600, //how often to run in seconds "maxAddresses": 50, //split up payments if sending to more than this many addresses + "mixin": 3, //number of transactions yours is indistinguishable from "transferFee": 5000000000, //fee to pay for each transaction "minPayment": 100000000000, //miner balance required before sending payment "denomination": 100000000000 //truncate to this precision and store remainder @@ -247,8 +248,9 @@ Explanation for each field: /* Block depth required for a block to unlocked/mature. Found in daemon source as the variable CRYPTONOTE_MINED_MONEY_UNLOCK_WINDOW */ "depth": 60, - "poolFee": 2, //2% pool fee - "devDonation": 0.1 //0.1% donation to send to pool dev - only works with Monero + "poolFee": 1.8, //1.8% pool fee (2% total fee total including donations) + "devDonation": 0.1, //0.1% donation to send to pool dev - only works with Monero + "coreDevDonation": 0.1 //0.1% donation to send to core devs - only works with Monero }, /* AJAX API used for front-end website. */ @@ -256,7 +258,10 @@ Explanation for each field: "enabled": true, "hashrateWindow": 600, //how many second worth of shares used to estimate hash rate "updateInterval": 3, //gather stats and broadcast every this many seconds - "port": 8117 + "port": 8117, + "blocks": 30, //amount of blocks to send at a time + "payments": 30, //amount of payments to send at a time + "password": "test" //password required for admin stats }, /* Coin daemon connection details. */ @@ -322,11 +327,11 @@ node init.js -module=api #### 5) Host the front-end -Simply host the contents of the `website` directory on file server capable of serving simple static files. +Simply host the contents of the `website_example` directory on file server capable of serving simple static files. -In the `website` directory copy `config_example.js` to `config.js` then edit the variables in -the file to use your pool's specific configuration. Variable explanations: +Edit the variables in the `website_example/config.js` file to use your pool's specific configuration. +Variable explanations: ```javascript @@ -351,22 +356,22 @@ var cryptonatorWidget = ["XMR-BTC", "XMR-USD", "XMR-EUR", "XMR-GBP"]; /* Download link to cryptonote-easy-miner for Windows users. */ var easyminerDownload = "https://github.com/zone117x/cryptonote-easy-miner/releases/"; -/* Download link to simplewallet for your configured coin. */ -var simplewalletDownload = "http://bit.ly/monero-starter-pack"; - /* Used for front-end block links. For other coins it can be changed, for example with Bytecoin you can use "https://minergate.com/blockchain/bcn/block/". */ var blockchainExplorer = "http://monerochain.info/block/"; +/* Used by front-end transaction links. Change for other coins. */ +var transactionExplorer = "http://monerochain.info/tx/"; + ``` #### 6) Customize your website The following files are included so that you can customize your pool website without having to make significant changes -to `index.html` thus reducing the difficulty of merging updates to `index.html` with your own changes: -* `additional.css` for creating your own pool style -* `addtional.js` for changing the functionality of your pool website -* `info.html` for display news/updates/information on your site +to `index.html` or other front-end files thus reducing the difficulty of merging updates with your own changes: +* `custom.css` for creating your own pool style +* `custom.js` for changing the functionality of your pool website + Then simply serve the files via nginx, Apache, Google Drive, or anything that can host static content. diff --git a/config_example.json b/config_example.json index ae0ae591..8c788678 100644 --- a/config_example.json +++ b/config_example.json @@ -35,6 +35,12 @@ "port": 7777, "difficulty": 10000, "desc": "High end hardware" + }, + { + "port": 8888, + "difficulty": 10000, + "desc": "Hidden port", + "hidden": true } ], "varDiff": { @@ -64,6 +70,7 @@ "enabled": true, "interval": 600, "maxAddresses": 50, + "mixin": 3, "transferFee": 5000000000, "minPayment": 100000000000, "denomination": 100000000000 @@ -74,7 +81,8 @@ "interval": 30, "depth": 60, "poolFee": 2, - "devDonation": 0.1 + "devDonation": 0.1, + "coreDevDonation": 0.1 }, "api": { @@ -82,6 +90,8 @@ "hashrateWindow": 600, "updateInterval": 5, "port": 8117, + "blocks": 30, + "payments": 30, "password": "your_password" }, diff --git a/init.js b/init.js index 67b49a5e..e13596a1 100644 --- a/init.js +++ b/init.js @@ -5,24 +5,7 @@ var os = require('os'); var redis = require('redis'); -var configFile = (function(){ - for (var i = 0; i < process.argv.length; i++){ - if (process.argv[i].indexOf('-config=') === 0) - return process.argv[i].split('=')[1]; - } - return 'config.json'; -})(); - - -try { - global.config = JSON.parse(fs.readFileSync(configFile)); -} -catch(e){ - console.error('Failed to read config file ' + configFile + '\n\n' + e); - return; -} - -config.version = "v0.99.0.6"; +require('./lib/configReader.js'); require('./lib/logger.js'); diff --git a/lib/api.js b/lib/api.js index 5d17737c..7bc627c0 100644 --- a/lib/api.js +++ b/lib/api.js @@ -4,7 +4,6 @@ var url = require("url"); var zlib = require('zlib'); var async = require('async'); -var redis = require('redis'); var apiInterfaces = require('./apiInterfaces.js')(config.daemon, config.wallet); @@ -13,13 +12,16 @@ require('./exceptionWriter.js')(logSystem); var redisCommands = [ ['zremrangebyscore', config.coin + ':hashrate', '-inf', ''], - ['zrangebyscore', config.coin + ':hashrate', '', '+inf'], - ['hgetall', config.coin + ':stats'], - ['smembers', config.coin + ':blocksPending'], - ['smembers', config.coin + ':blocksUnlocked'], - ['smembers', config.coin + ':blocksOrphaned'], + ['zrange', config.coin + ':hashrate', 0, -1], + ['hgetall', config.coin + ':stats'], + ['zrange', config.coin + ':blocks:candidates', 0, -1, 'WITHSCORES'], + ['zrevrange', config.coin + ':blocks:matured', 0, config.api.blocks - 1, 'WITHSCORES'], ['hgetall', config.coin + ':shares:roundCurrent'], - ['hgetall', config.coin + ':stats'] + ['hgetall', config.coin + ':stats'], + ['zcard', config.coin + ':blocks:matured'], + ['zrevrange', config.coin + ':payments:all', 0, config.api.payments - 1, 'WITHSCORES'], + ['zcard', config.coin + ':payments:all'], + ['keys', config.coin + ':payments:*'] ]; var currentStats = ""; @@ -40,7 +42,6 @@ function collectStats(){ var windowTime = (((Date.now() / 1000) - config.api.hashrateWindow) | 0).toString(); redisCommands[0][3] = '(' + windowTime; - redisCommands[1][2] = windowTime; async.parallel({ pool: function(callback){ @@ -56,11 +57,11 @@ function collectStats(){ var data = { stats: replies[2], - blocks: { - pending: replies[3], - unlocked: replies[4], - orphaned: replies[5] - } + blocks: replies[3].concat(replies[4]), + totalBlocks: parseInt(replies[7]) + (replies[3].length / 2), + payments: replies[8], + totalPayments: parseInt(replies[9]), + totalMinersPaid: replies[10].length - 1 }; var hashrates = replies[1]; @@ -86,14 +87,14 @@ function collectStats(){ data.roundHashes = 0; - if (replies[6]){ - for (var miner in replies[6]){ - data.roundHashes += parseInt(replies[6][miner]); + if (replies[5]){ + for (var miner in replies[5]){ + data.roundHashes += parseInt(replies[5][miner]); } } - if (replies[7]) { - data.lastBlockFound = replies[7].lastBlockFound; + if (replies[6]) { + data.lastBlockFound = replies[6].lastBlockFound; } callback(null, data); @@ -119,14 +120,18 @@ function collectStats(){ }, config: function(callback){ callback(null, { - ports: config.poolServer.ports, + ports: getPublicPorts(config.poolServer.ports), hashrateWindow: config.api.hashrateWindow, fee: config.blockUnlocker.poolFee, coin: config.coin, symbol: config.symbol, depth: config.blockUnlocker.depth, - version: config.version, - donation: config.blockUnlocker.devDonation + donation: config.blockUnlocker.devDonation, + coreDonation: config.blockUnlocker.coreDevDonation, + doDonations: doDonations, + version: version, + minPaymentThreshold: config.payments.minPayment, + denominationUnit: config.payments.denomination }); } }, function(error, results){ @@ -150,6 +155,12 @@ function collectStats(){ } +function getPublicPorts(ports){ + return ports.filter(function(port) { + return !port.hidden; + }); +} + function getReadableHashRateString(hashrate){ var i = 0; var byteUnits = [' H', ' KH', ' MH', ' GH', ' TH', ' PH' ]; @@ -172,16 +183,23 @@ function broadcastLiveStats(){ var redisCommands = []; for (var address in addressConnections){ redisCommands.push(['hgetall', config.coin + ':workers:' + address]); + redisCommands.push(['zrevrange', config.coin + ':payments:' + address, 0, config.api.payments - 1, 'WITHSCORES']); } redisClient.multi(redisCommands).exec(function(error, replies){ var addresses = Object.keys(addressConnections); for (var i = 0; i < addresses.length; i++){ + var offset = i * 2; var address = addresses[i]; - var stats = replies[i]; + var stats = replies[offset]; var res = addressConnections[address]; - res.end(stats ? formatMinerStats(stats, address) : '{"error": "not found"'); + if (!stats){ + res.end(JSON.stringify({error: "not found"})); + return; + } + stats.hashrate = minerStats[address]; + res.end(JSON.stringify({stats: stats, payments: replies[offset + 1]})); } }); } @@ -209,21 +227,84 @@ function handleMinerStats(urlParts, response){ }); } else{ - redisClient.hgetall(config.coin + ':workers:' + address, function(error, stats){ - if (!stats){ + redisClient.multi([ + ['hgetall', config.coin + ':workers:' + address], + ['zrevrange', config.coin + ':payments:' + address, 0, config.api.payments - 1, 'WITHSCORES'] + ]).exec(function(error, replies){ + if (error || !replies[0]){ response.end(JSON.stringify({error: 'not found'})); return; } - response.end(formatMinerStats(stats, address)); + var stats = replies[0]; + stats.hashrate = minerStats[address]; + response.end(JSON.stringify({stats: stats, payments: replies[1]})); }); } } -function formatMinerStats(redisData, address){ - redisData.hashrate = minerStats[address]; - redisData.symbol = config.symbol; - return JSON.stringify({stats: redisData}); +function handleGetPayments(urlParts, response){ + var paymentKey = ':payments:all'; + + if (urlParts.query.address) + paymentKey = ':payments:' + urlParts.query.address; + + redisClient.zrevrangebyscore( + config.coin + paymentKey, + '(' + urlParts.query.time, + '-inf', + 'WITHSCORES', + 'LIMIT', + 0, + config.api.payments, + function(err, result){ + + var reply; + + if (err) + reply = JSON.stringify({error: 'query failed'}); + else + reply = JSON.stringify(result); + + response.writeHead("200", { + 'Access-Control-Allow-Origin': '*', + 'Cache-Control': 'no-cache', + 'Content-Type': 'application/json', + 'Content-Length': reply.length + }); + response.end(reply); + + } + ) +} + +function handleGetBlocks(urlParts, response){ + redisClient.zrevrangebyscore( + config.coin + ':blocks:matured', + '(' + urlParts.query.height, + '-inf', + 'WITHSCORES', + 'LIMIT', + 0, + config.api.blocks, + function(err, result){ + + var reply; + + if (err) + reply = JSON.stringify({error: 'query failed'}); + else + reply = JSON.stringify(result); + + response.writeHead("200", { + 'Access-Control-Allow-Origin': '*', + 'Cache-Control': 'no-cache', + 'Content-Type': 'application/json', + 'Content-Length': reply.length + }); + response.end(reply); + + }); } @@ -252,21 +333,24 @@ function handleAdminStats(response){ async.waterfall([ - //Get worker keys + //Get worker keys & unlocked blocks function(callback){ - redisClient.keys(config.coin + ':workers:*', function(error, result) { + redisClient.multi([ + ['keys', config.coin + ':workers:*'], + ['zrange', config.coin + ':blocks:matured', 0, -1] + ]).exec(function(error, replies) { if (error) { - log('error', logSystem, 'Error trying to get worker balances from redis %j', [error]); + log('error', logSystem, 'Error trying to get admin data from redis %j', [error]); callback(true); return; } - callback(null, result); + callback(null, replies[0], replies[1]); }); }, //Get worker balances - function(keys, callback){ - var redisCommands = keys.map(function(k){ + function(workerKeys, blocks, callback){ + var redisCommands = workerKeys.map(function(k){ return ['hmget', k, 'balance', 'paid']; }); redisClient.multi(redisCommands).exec(function(error, replies){ @@ -276,19 +360,42 @@ function handleAdminStats(response){ return; } - var stats = { - totalOwed: 0, - totalPaid: 0 - }; + callback(null, replies, blocks); + }); + }, + function(workerData, blocks, callback){ + var stats = { + totalOwed: 0, + totalPaid: 0, + totalRevenue: 0, + totalDiff: 0, + totalShares: 0, + blocksOrphaned: 0, + blocksUnlocked: 0, + totalWorkers: 0 + }; + + for (var i = 0; i < workerData.length; i++){ + stats.totalOwed += parseInt(workerData[i][0]) || 0; + stats.totalPaid += parseInt(workerData[i][1]) || 0; + stats.totalWorkers++; + } - for (var i = 0; i < replies.length; i++){ - stats.totalOwed += parseInt(replies[i][0]) || 0; - stats.totalPaid += parseInt(replies[i][1]) || 0; + for (var i = 0; i < blocks.length; i++){ + var block = blocks[i].split(':'); + if (block[5]) { + stats.blocksUnlocked++; + stats.totalDiff += parseInt(block[2]); + stats.totalShares += parseInt(block[3]); + stats.totalRevenue += parseInt(block[5]); } - - callback(null, stats); - }); - }], function(error, stats){ + else{ + stats.blocksOrphaned++; + } + } + callback(null, stats); + } + ], function(error, stats){ if (error){ response.end(JSON.stringify({error: 'error collecting stats'})); return; @@ -346,6 +453,12 @@ var server = http.createServer(function(request, response){ case '/stats_address': handleMinerStats(urlParts, response); break; + case '/get_payments': + handleGetPayments(urlParts, response); + break; + case '/get_blocks': + handleGetBlocks(urlParts, response); + break; case '/admin_stats': if (!authorize(request, response)) return; diff --git a/lib/blockUnlocker.js b/lib/blockUnlocker.js index 1ea50aa3..fa2fe333 100644 --- a/lib/blockUnlocker.js +++ b/lib/blockUnlocker.js @@ -24,36 +24,37 @@ apiInterfaces.batchRpcDaemon(batchArray, function(error, response){ */ -var devDonationAddress = '45Jmf8PnJKziGyrLouJMeBFw2yVyX1QB52sKEQ4S1VSU2NVsaVGPNu4bWKkaHaeZ6tWCepP6iceZk8XhTLzDaEVa72QrtVh'; - -var doDonations = config.blockUnlocker.devDonation > 0 && devDonationAddress[0] === config.poolServer.poolAddress[0]; - - function runInterval(){ async.waterfall([ - //Get all pending blocks in redis + //Get all block candidates in redis function(callback){ - redisClient.smembers(config.coin + ':blocksPending', function(error, result){ + redisClient.zrange(config.coin + ':blocks:candidates', 0, -1, 'WITHSCORES', function(error, results){ if (error){ log('error', logSystem, 'Error trying to get pending blocks from redis %j', [error]); callback(true); return; } - if (result.length === 0){ - log('info', logSystem, 'No pending blocks in redis'); + if (results.length === 0){ + log('info', logSystem, 'No blocks candidates in redis'); callback(true); return; } - var blocks = result.map(function(item){ - var parts = item.split(':'); - return { - height: parseInt(parts[0]), - difficulty: parseInt(parts[1]), - hash: parts[2], - serialized: item - }; - }); + + var blocks = []; + + for (var i = 0; i < results.length; i += 2){ + var parts = results[i].split(':'); + blocks.push({ + serialized: results[i], + height: parseInt(results[i + 1]), + hash: parts[0], + time: parts[1], + difficulty: parts[2], + shares: parts[3] + }); + } + callback(null, blocks); }); }, @@ -75,7 +76,7 @@ function runInterval(){ return; } var blockHeader = result.block_header; - block.orphan = (blockHeader.hash !== block.hash); + block.orphaned = blockHeader.hash === block.hash ? 0 : 1; block.unlocked = blockHeader.depth >= config.blockUnlocker.depth; block.reward = blockHeader.reward; mapCback(block.unlocked); @@ -83,7 +84,7 @@ function runInterval(){ }, function(unlockedBlocks){ if (unlockedBlocks.length === 0){ - log('info', logSystem, 'No pending blocks are unlocked or orphaned yet (%d pending)', [blocks.length]); + log('info', logSystem, 'No pending blocks are unlocked yet (%d pending)', [blocks.length]); callback(true); return; } @@ -110,9 +111,6 @@ function runInterval(){ for (var i = 0; i < replies.length; i++){ var workerShares = replies[i]; blocks[i].workerShares = workerShares; - blocks[i].totalShares = Object.keys(workerShares).reduce(function(p, c){ - return p + parseInt(workerShares[c]) - }, 0); } callback(null, blocks); }); @@ -121,19 +119,29 @@ function runInterval(){ //Handle orphaned blocks function(blocks, callback){ var orphanCommands = []; + blocks.forEach(function(block){ - if (!block.orphan) return; - var workerShares = block.workerShares; - orphanCommands.push(['del', config.coin + ':shares:round' + block.height]); + if (!block.orphaned) return; - orphanCommands.push(['srem', config.coin + ':blocksPending', block.serialized]); - orphanCommands.push(['sadd', config.coin + ':blocksOrphaned', block.serialized + ':' + block.totalShares]); + orphanCommands.push(['del', config.coin + ':shares:round' + block.height]); - Object.keys(workerShares).forEach(function(worker){ - orphanCommands.push(['hincrby', config.coin + ':shares:roundCurrent', - worker, workerShares[worker]]); - }); + orphanCommands.push(['zrem', config.coin + ':blocks:candidates', block.serialized]); + orphanCommands.push(['zadd', config.coin + ':blocks:matured', block.height, [ + block.hash, + block.time, + block.difficulty, + block.shares, + block.orphaned + ].join(':')]); + + if (block.workerShares) { + var workerShares = block.workerShares; + Object.keys(workerShares).forEach(function (worker) { + orphanCommands.push(['hincrby', config.coin + ':shares:roundCurrent', worker, workerShares[worker]]); + }); + } }); + if (orphanCommands.length > 0){ redisClient.multi(orphanCommands).exec(function(error, replies){ if (error){ @@ -155,30 +163,43 @@ function runInterval(){ var payments = {}; var totalBlocksUnlocked = 0; blocks.forEach(function(block){ - if (block.orphan) return; + if (block.orphaned) return; totalBlocksUnlocked++; unlockedBlocksCommands.push(['del', config.coin + ':shares:round' + block.height]); - - unlockedBlocksCommands.push(['srem', config.coin + ':blocksPending', block.serialized]); - unlockedBlocksCommands.push(['sadd', config.coin + ':blocksUnlocked', block.serialized + ':' + block.totalShares]); + unlockedBlocksCommands.push(['zrem', config.coin + ':blocks:candidates', block.serialized]); + unlockedBlocksCommands.push(['zadd', config.coin + ':blocks:matured', block.height, [ + block.hash, + block.time, + block.difficulty, + block.shares, + block.orphaned, + block.reward + ].join(':')]); var feePercent = config.blockUnlocker.poolFee / 100; if (doDonations) { feePercent += config.blockUnlocker.devDonation / 100; + feePercent += config.blockUnlocker.coreDevDonation / 100; + var devDonation = block.reward * (config.blockUnlocker.devDonation / 100); payments[devDonationAddress] = devDonation; + + var coreDevDonation = block.reward * (config.blockUnlocker.coreDevDonation / 100); + payments[coreDevDonationAddress] = coreDevDonation; } var reward = block.reward - (block.reward * feePercent); - var totalShares = block.totalShares; - Object.keys(block.workerShares).forEach(function(worker){ - var percent = block.workerShares[worker] / totalShares; - var workerReward = reward * percent; - payments[worker] = (payments[worker] || 0) + workerReward; - }); + if (block.workerShares) { + var totalShares = parseInt(block.shares); + Object.keys(block.workerShares).forEach(function (worker) { + var percent = block.workerShares[worker] / totalShares; + var workerReward = reward * percent; + payments[worker] = (payments[worker] || 0) + workerReward; + }); + } }); for (var worker in payments) { @@ -205,9 +226,7 @@ function runInterval(){ log('info', logSystem, 'Unlocked %d blocks and update balances for %d workers', [totalBlocksUnlocked, Object.keys(payments).length]); callback(null); }); - } - ], function(error, result){ setTimeout(runInterval, config.blockUnlocker.interval * 1000); }) diff --git a/lib/configReader.js b/lib/configReader.js new file mode 100644 index 00000000..a5c922e9 --- /dev/null +++ b/lib/configReader.js @@ -0,0 +1,25 @@ +var fs = require('fs'); + +var configFile = (function(){ + for (var i = 0; i < process.argv.length; i++){ + if (process.argv[i].indexOf('-config=') === 0) + return process.argv[i].split('=')[1]; + } + return 'config.json'; +})(); + + +try { + global.config = JSON.parse(fs.readFileSync(configFile)); +} +catch(e){ + console.error('Failed to read config file ' + configFile + '\n\n' + e); + return; +} + +global.version = "v0.99.3.1"; +global.devDonationAddress = '45Jmf8PnJKziGyrLouJMeBFw2yVyX1QB52sKEQ4S1VSU2NVsaVGPNu4bWKkaHaeZ6tWCepP6iceZk8XhTLzDaEVa72QrtVh'; +global.coreDevDonationAddress = '46BeWrHpwXmHDpDEUmZBWZfoQpdc6HaERCNmx1pEYL2rAcuwufPN9rXHHtyUA4QVy66qeFQkn6sfK8aHYjA3jk3o1Bv16em'; +global.doDonations = devDonationAddress[0] === config.poolServer.poolAddress[0] && ( + config.blockUnlocker.devDonation > 0 || config.blockUnlocker.coreDevDonation > 0 +); \ No newline at end of file diff --git a/lib/paymentProcessor.js b/lib/paymentProcessor.js index 1d2c6bd9..5008ddb3 100644 --- a/lib/paymentProcessor.js +++ b/lib/paymentProcessor.js @@ -77,10 +77,11 @@ function runInterval(){ for (var i = 0; i < transferCommandsLength; i++){ transferCommands.push({ redis: [], + amount : 0, rpc: { destinations: [], fee: config.payments.transferFee, - mixin: 0, + mixin: config.payments.mixin, unlock_time: 0 } }); @@ -94,6 +95,7 @@ function runInterval(){ transferCommands[commandIndex].rpc.destinations.push({amount: amount, address: worker}); transferCommands[commandIndex].redis.push(['hincrby', config.coin + ':workers:' + worker, 'balance', -amount]); transferCommands[commandIndex].redis.push(['hincrby', config.coin + ':workers:' + worker, 'paid', amount]); + transferCommands[commandIndex].amount += amount; addresses++; if (addresses >= config.payments.maxAddresses){ @@ -102,6 +104,8 @@ function runInterval(){ } } + var timeOffset = 0; + async.filter(transferCommands, function(transferCmd, cback){ apiInterfaces.rpcWallet('transfer', transferCmd.rpc, function(error, result){ if (error){ @@ -110,6 +114,31 @@ function runInterval(){ cback(false); return; } + + var now = (timeOffset++) + Date.now() / 1000 | 0; + var txHash = result.tx_hash.replace('<', '').replace('>', ''); + + + transferCmd.redis.push(['zadd', config.coin + ':payments:all', now, [ + txHash, + transferCmd.amount, + transferCmd.rpc.fee, + transferCmd.rpc.mixin, + Object.keys(transferCmd.rpc.destinations).length + ].join(':')]); + + + for (var i = 0; i < transferCmd.rpc.destinations.length; i++){ + var destination = transferCmd.rpc.destinations[i]; + transferCmd.redis.push(['zadd', config.coin + ':payments:' + destination.address, now, [ + txHash, + destination.amount, + transferCmd.rpc.fee, + transferCmd.rpc.mixin + ].join(':')]); + } + + log('info', logSystem, 'Payments sent via wallet daemon %j', [result]); redisClient.multi(transferCmd.redis).exec(function(error, replies){ if (error){ diff --git a/lib/pool.js b/lib/pool.js index 2042d401..540c3e5b 100644 --- a/lib/pool.js +++ b/lib/pool.js @@ -154,7 +154,7 @@ function processBlockTemplate(template){ if (currentBlockTemplate) validBlockTemplates.push(currentBlockTemplate); - if (validBlockTemplates.length > 10) + if (validBlockTemplates.length > 3) validBlockTemplates.shift(); currentBlockTemplate = new BlockTemplate(template); @@ -335,7 +335,7 @@ Miner.prototype = { -function recordShareData(miner, job, shareDiff, blockCandidate, hashHex, shareType){ +function recordShareData(miner, job, shareDiff, blockCandidate, hashHex, shareType, blockTemplate){ var dateNow = Date.now(); var dateNowSeconds = dateNow / 1000 | 0; @@ -348,15 +348,33 @@ function recordShareData(miner, job, shareDiff, blockCandidate, hashHex, shareTy ]; if (blockCandidate){ - redisCommands.push(['sadd', config.coin + ':blocksPending', [job.height, currentBlockTemplate.difficulty, hashHex, Date.now() / 1000 | 0].join(':')]); - redisCommands.push(['rename', config.coin + ':shares:roundCurrent', config.coin + ':shares:round' + job.height]); redisCommands.push(['hset', config.coin + ':stats', 'lastBlockFound', Date.now()]); + redisCommands.push(['rename', config.coin + ':shares:roundCurrent', config.coin + ':shares:round' + job.height]); + redisCommands.push(['hgetall', config.coin + ':shares:round' + job.height]); } redisClient.multi(redisCommands).exec(function(err, replies){ if (err){ - log('error', logSystem, 'Failed to insert share data into redis %j', [err]); + log('error', logSystem, 'Failed to insert share data into redis %j \n %j', [err, redisCommands]); + return; + } + if (blockCandidate){ + var workerShares = replies[replies.length - 1]; + var totalShares = Object.keys(workerShares).reduce(function(p, c){ + return p + parseInt(workerShares[c]) + }, 0); + redisClient.zadd(config.coin + ':blocks:candidates', job.height, [ + hashHex, + Date.now() / 1000 | 0, + blockTemplate.difficulty, + totalShares + ].join(':'), function(err, result){ + if (err){ + log('error', logSystem, 'Failed inserting block candidate %s \n %j', [hashHex, err]); + } + }); } + }); log('info', logSystem, 'Accepted %s share at difficulty %d/%d from %s@%s', [shareType, job.difficulty, shareDiff, miner.login, miner.ip]); @@ -385,7 +403,7 @@ function processShare(miner, job, blockTemplate, nonce, resultHash){ if (hash.toString('hex') !== resultHash) { - log('error', logSystem, 'Bad hash from miner %s@%s', [miner.login, miner.ip]); + log('warn', logSystem, 'Bad hash from miner %s@%s', [miner.login, miner.ip]); return false; } @@ -394,22 +412,22 @@ function processShare(miner, job, blockTemplate, nonce, resultHash){ var hashNum = bignum.fromBuffer(new Buffer(hashArray)); var hashDiff = diff1.div(hashNum); - var blockFastHash; + if (hashDiff.ge(blockTemplate.difficulty)){ - blockFastHash = cnUtil.get_block_id(shareBuffer).toString('hex'); apiInterfaces.rpcDaemon('submitblock', [shareBuffer.toString('hex')], function(error, result){ if (error){ - log('error', logSystem, 'Error submitting block at height %d - %j', [job.height, error]); + log('error', logSystem, 'Error submitting block at height %d from %s@%s, share type: "%s" - %j', [job.height, miner.login, miner.ip, shareType, error]); recordShareData(miner, job, hashDiff.toString(), false, null, shareType); } else{ + var blockFastHash = cnUtil.get_block_id(shareBuffer).toString('hex'); log('info', logSystem, 'Block %s found at height %d by miner %s@%s - submit result: %j', [blockFastHash.substr(0, 6), job.height, miner.login, miner.ip, result] ); - recordShareData(miner, job, hashDiff.toString(), true, blockFastHash, shareType); + recordShareData(miner, job, hashDiff.toString(), true, blockFastHash, shareType, blockTemplate); jobRefresh(); } }); diff --git a/redisBlocksUpgrade.js b/redisBlocksUpgrade.js new file mode 100644 index 00000000..3e6a762b --- /dev/null +++ b/redisBlocksUpgrade.js @@ -0,0 +1,191 @@ + + +/* + +This script converts the block data in redis from the old format (v0.99.0.6 and earlier) to the new format +used in v0.99.1+ + +*/ + +var util = require('util'); + +var async = require('async'); + +var redis = require('redis'); + +require('./lib/configReader.js'); + +var apiInterfaces = require('./lib/apiInterfaces.js')(config.daemon, config.wallet); + + +function log(severity, system, text, data){ + + var formattedMessage = text; + + if (data) { + data.unshift(text); + formattedMessage = util.format.apply(null, data); + } + + console.log(severity + ': ' + formattedMessage); +} + + +var logSystem = 'reward script'; + +var redisClient = redis.createClient(config.redis.port, config.redis.host); + +function getTotalShares(height, callback){ + + redisClient.hgetall(config.coin + ':shares:round' + height, function(err, workerShares){ + + if (err) { + callback(err); + return; + } + + var totalShares = Object.keys(workerShares).reduce(function(p, c){ + return p + parseInt(workerShares[c]) + }, 0); + + callback(null, totalShares); + + }); +} + + +async.series([ + function(callback){ + redisClient.smembers(config.coin + ':blocksUnlocked', function(error, result){ + if (error){ + log('error', logSystem, 'Error trying to get unlocke blocks from redis %j', [error]); + callback(); + return; + } + if (result.length === 0){ + log('info', logSystem, 'No unlocked blocks in redis'); + callback(); + return; + } + + var blocks = result.map(function(item){ + var parts = item.split(':'); + return { + height: parseInt(parts[0]), + difficulty: parts[1], + hash: parts[2], + time: parts[3], + shares: parts[4], + orphaned: 0 + }; + }); + + async.map(blocks, function(block, mapCback){ + apiInterfaces.rpcDaemon('getblockheaderbyheight', {height: block.height}, function(error, result){ + if (error){ + log('error', logSystem, 'Error with getblockheaderbyheight RPC request for block %s - %j', [block.serialized, error]); + mapCback(null, block); + return; + } + if (!result.block_header){ + log('error', logSystem, 'Error with getblockheaderbyheight, no details returned for %s - %j', [block.serialized, result]); + mapCback(null, block); + return; + } + var blockHeader = result.block_header; + block.reward = blockHeader.reward; + mapCback(null, block); + }); + }, function(err, blocks){ + + if (blocks.length === 0){ + log('info', logSystem, 'No unlocked blocks'); + callback(); + return; + } + + var zaddCommands = [config.coin + ':blocks:matured']; + + for (var i = 0; i < blocks.length; i++){ + var block = blocks[i]; + zaddCommands.push(block.height); + zaddCommands.push([ + block.hash, + block.time, + block.difficulty, + block.shares, + block.orphaned, + block.reward + ].join(':')); + } + + redisClient.zadd(zaddCommands, function(err, result){ + if (err){ + console.log('failed zadd ' + JSON.stringify(err)); + callback(); + return; + } + console.log('successfully converted unlocked blocks to matured blocks'); + callback(); + }); + + + }); + }); + }, + function(callback){ + redisClient.smembers(config.coin + ':blocksPending', function(error, result) { + if (error) { + log('error', logSystem, 'Error trying to get pending blocks from redis %j', [error]); + callback(); + return; + } + if (result.length === 0) { + log('info', logSystem, 'No pending blocks in redis'); + callback(); + return; + } + + async.map(result, function(item, mapCback){ + var parts = item.split(':'); + var block = { + height: parseInt(parts[0]), + difficulty: parts[1], + hash: parts[2], + time: parts[3], + serialized: item + }; + getTotalShares(block.height, function(err, shares){ + block.shares = shares; + mapCback(null, block); + }); + }, function(err, blocks){ + + var zaddCommands = [config.coin + ':blocks:candidates']; + + for (var i = 0; i < blocks.length; i++){ + var block = blocks[i]; + zaddCommands.push(block.height); + zaddCommands.push([ + block.hash, + block.time, + block.difficulty, + block.shares + ].join(':')); + } + + redisClient.zadd(zaddCommands, function(err, result){ + if (err){ + console.log('failed zadd ' + JSON.stringify(err)); + return; + } + console.log('successfully converted pending blocks to block candidates'); + }); + + }); + + }); + } +], function(){ + process.exit(); +}); \ No newline at end of file diff --git a/website/admin.html b/website/admin.html deleted file mode 100644 index 2bbbe605..00000000 --- a/website/admin.html +++ /dev/null @@ -1,54 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - -
- Total Owed: -
-
- Total Paid: -
- - - \ No newline at end of file diff --git a/website/index.html b/website/index.html deleted file mode 100644 index a3d213d6..00000000 --- a/website/index.html +++ /dev/null @@ -1,782 +0,0 @@ - - - - - - - - Cryptonote Mining Pool - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- -
- -
-
-

Network

-
Hash Rate:
-
Block Found:
-
Difficulty:
-
Blockchain Height:
-
Last Reward:
-
Last Hash:
-
- -
-

Pool

-
Hash Rate:
-
Block Found:
-
Round Hashes:
-
Connected Miners:
-
Fee:
-
Block Found Every: (est.)
-
- -
-

Market

-
Updated:
-
Powered by Cryptonator
-
-
- -
-

Your Stats

-
- - -
-
Address:
-
Pending Balance:
-
Total Paid:
-
Last Share Submitted:
-
Hash Rate:
-
Total Hashes Submitted:
-
- -
- -
-
-

Block Candidates

- Maturing - Unlocked - Orphaned - Maturity requires blocks - Luck Average -
-
-
- - - - - - - - - - - - - - -
Height Maturity Difficulty Block Hash Time Found Luck
-
-
- -
-

Getting Started

- -
- -
-

Mining Host

-
Pool Address:
-
- -

Mining Ports

-
-
-
Port:
-
Starting Difficulty:
-
Description:
-
-
- -
- -

For Windows users

-
-

- You can Download and run cryptonote-easy-miner which will automatically generate your wallet address - and manage multiple instances of simpleminer to take advantage of all your CPU Cores. -

- -
- -

Or download the the binaries for your system

-
-
    -
  1. Download simplewallet for your system
  2. -
  3. Run simplewallet to generate your public address
  4. -
  5. Download CPUMiner-multi
  6. -
  7. Choose a port and point your miner to our pool with your address, for example: -

    - -
  8. -
-
- -
-

Chat Room

- -

Contact

-

Email pool support at

-
- -
- - - - - - \ No newline at end of file diff --git a/website/info.html b/website/info.html deleted file mode 100644 index ec415870..00000000 --- a/website/info.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - \ No newline at end of file diff --git a/website_example/admin.html b/website_example/admin.html new file mode 100644 index 00000000..c07bf1f7 --- /dev/null +++ b/website_example/admin.html @@ -0,0 +1,155 @@ + + + + + + + + + + + + + + + + + + + + + + + + +
+ +

Admin Center

+ +
+ +

Stats

+
+
Total Owed
...
+
Total Paid
...
+
Total Mined
...
+
Profit (before tx fees)
...
+
Average Luck
...
+
Orphan Percent
...
+
Registered Addresses
...
+
+
+ +
+ +

Miner Lookup

+ + +
+ + + \ No newline at end of file diff --git a/website/config_example.js b/website_example/config.js similarity index 73% rename from website/config_example.js rename to website_example/config.js index 9786e819..634fd0ea 100644 --- a/website/config_example.js +++ b/website_example/config.js @@ -12,6 +12,6 @@ var cryptonatorWidget = ["XMR-BTC", "XMR-USD", "XMR-EUR", "XMR-GBP"]; var easyminerDownload = "https://github.com/zone117x/cryptonote-easy-miner/releases/"; -var simplewalletDownload = "http://bit.ly/monero-starter-pack"; +var blockchainExplorer = "http://monerochain.info/block/"; -var blockchainExplorer = "http://monerochain.info/block/"; \ No newline at end of file +var transactionExplorer = "http://monerochain.info/tx/"; \ No newline at end of file diff --git a/website/additional.css b/website_example/custom.css similarity index 100% rename from website/additional.css rename to website_example/custom.css diff --git a/website/additional.js b/website_example/custom.js similarity index 100% rename from website/additional.js rename to website_example/custom.js diff --git a/website_example/index.html b/website_example/index.html new file mode 100644 index 00000000..06d7a363 --- /dev/null +++ b/website_example/index.html @@ -0,0 +1,359 @@ + + + + + + + + Cryptonote Mining Pool + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +

+ +
+ + + + + + \ No newline at end of file diff --git a/website_example/pages/getting_started.html b/website_example/pages/getting_started.html new file mode 100644 index 00000000..69b33458 --- /dev/null +++ b/website_example/pages/getting_started.html @@ -0,0 +1,206 @@ + + + +

Connection Details

+
+
Mining Pool Address:
+
+ +

Mining Ports

+
+
+
Port:
+
Starting Difficulty:
+
Description:
+
+
+ +
+ +

For Windows users new to mining

+

+ You can Download + and run cryptonote-easy-miner + which will automatically generate your wallet address and run CPUMiner with the proper parameters. +

+ +
+ +

Wallet & Daemon Software

+

+

+

+ +
+ +

Mining Apps

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
App Name Architecture Downloads Discussion Source Code
CPUMiner (forked by LucasJones & Wolf)CPUBitcoinTalkBitcoinTalkGithub
+ Example: + minerd -a cryptonight -o stratum+tcp://: -u YOUR_WALLET_ADDRESS -p x +
YAM Miner (by yvg1900)CPUMEGATwitterProprietary
+ Example: + yam -c x -M stratum+tcp://YOUR_WALLET_ADDRESS:x@:/xmr +
Claymore CPU MinerCPUBitcoinTalkBitcoinTalkProprietary
+ Example: + NsCpuCNMiner64 -o stratum+tcp://: -u YOUR_WALLET_ADDRESS -p x +
Claymore GPU MinerOpenCL (AMD)BitcoinTalkDiscussionProprietary
+ Example: + NsGpuCNMiner -o stratum+tcp://: -u YOUR_WALLET_ADDRESS -p x +
ccminer (forked by tsiv)CUDA (Nvidia)GithubBitcoinTalkGithub
+ Example: + ccminer -o stratum+tcp://: -u YOUR_WALLET_ADDRESS -p x +
+
+ + + \ No newline at end of file diff --git a/website_example/pages/home.html b/website_example/pages/home.html new file mode 100644 index 00000000..3d6ad3da --- /dev/null +++ b/website_example/pages/home.html @@ -0,0 +1,467 @@ + + + +
+ +
+ +
+
+

Network

+
Hash Rate:
+
Block Found:
+
Difficulty:
+
Blockchain Height:
+
Last Reward:
+
Last Hash:
+
+ +
+

Our Pool

+
Hash Rate:
+
Block Found:
+
Connected Miners:
+
Donations:
+
Total Pool Fee:
+
Block Found Every: (est.)
+
+ +
+

Market

+
Updated:
+
Powered by Cryptonator
+
+
+ +
+ +
+

Estimate Mining Profits

+
+
+ +
+ + +
+ = + /day +
+
+
+ +
+ +
+

Your Stats & Payment History

+ +
+ + +
+ +
+
Address:
+
Pending Balance:
+
Total Paid:
+
Last Share Submitted:
+
Hash Rate:
+
Total Hashes Submitted:
+ +
+ +

Payments

+
+ + + + + + + + + + + + +
Time Sent Transaction Hash Amount Mixin
+
+

+ +

+ +
+ + \ No newline at end of file diff --git a/website_example/pages/payments.html b/website_example/pages/payments.html new file mode 100644 index 00000000..9b179265 --- /dev/null +++ b/website_example/pages/payments.html @@ -0,0 +1,93 @@ + + +
+ Total Payments: + Total Miners Paid: + Minimum Payment Threshold: + Denomination Unit: +
+ +
+ +
+ + + + + + + + + + + + + + +
Time Sent Transaction Hash Amount Fee Mixin Payees
+
+ +

+ +

+ + + \ No newline at end of file diff --git a/website_example/pages/pool_blocks.html b/website_example/pages/pool_blocks.html new file mode 100644 index 00000000..d34d45c3 --- /dev/null +++ b/website_example/pages/pool_blocks.html @@ -0,0 +1,199 @@ + + +
+ Total Blocks Mined: + Maturity Depth Requirement: +
+ +
+ +
+ + + + + + + + + + + + + + +
Height Maturity Difficulty Block Hash Time Found Luck
+
+ +

+ +

+ + \ No newline at end of file diff --git a/website_example/pages/support.html b/website_example/pages/support.html new file mode 100644 index 00000000..66c9652e --- /dev/null +++ b/website_example/pages/support.html @@ -0,0 +1,24 @@ +

Contact

+

Email pool support at

+ +

Chat Room

+ + + \ No newline at end of file