diff --git a/README.md b/README.md index fab0052..7b426ec 100644 --- a/README.md +++ b/README.md @@ -89,9 +89,16 @@ var myCoin = { "name": "Dogecoin", "symbol": "DOGE", "algorithm": "scrypt", - "nValue": 1024, //optional. Defaults to 1024 - "rValue": 1, //optional. Defaults to 1 - "txMessages": false, //or true (not required, defaults to false) + "nValue": 1024, //optional - defaults to 1024 + "rValue": 1, //optional - defaults to 1 + "txMessages": false, //optional - defaults to false, + + /* Magic value only required for setting up p2p block notifications. It is found in the daemon + source code as the pchMessageStart variable. + For example, litecoin mainnet magic: http://git.io/Bi8YFw + And for litecoin testnet magic: http://git.io/NXBYJA */ + "peerMagic": "fbc0b6db" //optional + "peerMagicTestnet": "fcc1b7dc" //optional }; ``` @@ -154,6 +161,18 @@ var pool = Stratum.createPool({ "coin": myCoin, "address": "mi4iBXbBsydtcc5yFmsff2zCFVX4XG7qJc", //Address to where block rewards are given + + /* Block rewards go to the configured pool wallet address to later be paid out to miners, + except for a percentages that can go to pool operator(s) as pool fees or donations. + Addresses or hashed public keys can be used. */ + "rewardRecipients": { + "n37vuNFkXfk15uFnGoVyHZ6PYQxppD3QqK": 1.5, //1.5% goes to pool op + "mirj3LtZxbSTharhtXvotqtJXUY7ki5qfx": 0.5, //0.5% goes to a pool co-owner + + //0.1% donation to NOMP to help support development + "22851477d63a085dbc2398c8430af1c09e7343f6": 0.1 + }, + "blockRefreshInterval": 1000, //How often to poll RPC daemons for new blocks, in milliseconds /* How many milliseconds should have passed before new block transactions will trigger a new @@ -220,7 +239,6 @@ var pool = Stratum.createPool({ } }, - /* Recommended to have at least two daemon instances running in case one drops out-of-sync or offline. For redundancy, all instances will be polled for block/transaction updates and be used for submitting blocks. Creating a backup daemon involves spawning a daemon @@ -246,8 +264,8 @@ var pool = Stratum.createPool({ /* This allows the pool to connect to the daemon as a node peer to receive block updates. It may be the most efficient way to get block updates (faster than polling, less - intensive than blocknotify script). It requires additional setup: the 'magic' field must - be exact (extracted from the coin source code). */ + intensive than blocknotify script). It requires the additional field "peerMagic" in + the coin config. */ "p2p": { "enabled": false, @@ -260,13 +278,8 @@ var pool = Stratum.createPool({ /* If your coin daemon is new enough (i.e. not a shitcoin) then it will support a p2p feature that prevents the daemon from spamming our peer node with unnecessary transaction data. Assume its supported but if you have problems try disabling it. */ - "disableTransactions": true, + "disableTransactions": true - /* Magic value is different for main/testnet and for each coin. It is found in the daemon - source code as the pchMessageStart variable. - For example, litecoin mainnet magic: http://git.io/Bi8YFw - And for litecoin testnet magic: http://git.io/NXBYJA */ - "magic": "fcc1b7dc" } }, function(ip, workerName, password, callback){ //stratum authorization function @@ -289,7 +302,8 @@ Listen to pool events ip: '71.33.19.37', //ip address of client worker: 'matt.worker1', //stratum worker name height: 443795, //block height - reward: 5000000000, //the number of satoshis received as payment for solving this block + poolReward: 4900000000, //the number of satoshis sent to the configured pool address + blockReward: 5000000000, //the number of satoshis received as payment for solving this block difficulty: 64, //stratum worker difficulty shareDiff: 78, //actual difficulty of the share blockDiff: 3349, //block difficulty adjusted for share padding diff --git a/lib/blockTemplate.js b/lib/blockTemplate.js index 8f7aecf..af157fa 100644 --- a/lib/blockTemplate.js +++ b/lib/blockTemplate.js @@ -9,7 +9,7 @@ var util = require('./util.js'); * The BlockTemplate class holds a single job. * and provides several methods to validate and submit it to the daemon coin **/ -var BlockTemplate = module.exports = function BlockTemplate(jobId, rpcData, publicKey, extraNoncePlaceholder, reward, txMessages){ +var BlockTemplate = module.exports = function BlockTemplate(jobId, rpcData, poolAddressScript, extraNoncePlaceholder, reward, txMessages, recipients){ //private members @@ -34,6 +34,14 @@ var BlockTemplate = module.exports = function BlockTemplate(jobId, rpcData, publ this.rpcData = rpcData; this.jobId = jobId; + this.poolReward = (function(){ + var pReward = rpcData.coinbasevalue; + for (var i = 0; i < recipients.length; i++){ + var recipientReward = Math.floor(recipients[i].percent * rpcData.coinbasevalue); + pReward -= recipientReward; + } + return pReward; + })(); //Use the 'target' field if available, but some daemons only return the 'bits' field @@ -55,10 +63,11 @@ var BlockTemplate = module.exports = function BlockTemplate(jobId, rpcData, publ this.merkleBranch = getMerkleHashes(this.merkleTree.steps); this.generationTransaction = transactions.CreateGeneration( rpcData, - publicKey, + poolAddressScript, extraNoncePlaceholder, reward, - txMessages + txMessages, + recipients ); this.serializeCoinbase = function(extraNonce1, extraNonce2){ diff --git a/lib/daemon.js b/lib/daemon.js index 5075c65..58ec495 100644 --- a/lib/daemon.js +++ b/lib/daemon.js @@ -81,7 +81,7 @@ function DaemonInterface(options){ } } if (typeof(dataJson) !== 'undefined'){ - callback(dataJson.error, dataJson); + callback(dataJson.error, dataJson, data); } else callback(parsingError); @@ -141,20 +141,20 @@ function DaemonInterface(options){ /* Sends a JSON RPC (http://json-rpc.org/wiki/specification) command to every configured daemon. The callback function is fired once with the result from each daemon unless streamResults is set to true. */ - function cmd(method, params, callback, streamResults){ + function cmd(method, params, callback, streamResults, returnRawData){ var results = []; async.each(instances, function(instance, eachCallback){ - var itemFinished = function(error, result){ + var itemFinished = function(error, result, data){ var returnObj = { error: error, response: (result || {}).result, instance: instance }; - + if (returnRawData) returnObj.data = data; if (streamResults) callback(returnObj); else results.push(returnObj); eachCallback(); @@ -167,8 +167,8 @@ function DaemonInterface(options){ id: Date.now() + Math.floor(Math.random() * 10) }); - performHttpRequest(instance, requestJson, function(error, result){ - itemFinished(error, result); + performHttpRequest(instance, requestJson, function(error, result, data){ + itemFinished(error, result, data); }); diff --git a/lib/jobManager.js b/lib/jobManager.js index c3dce5e..173101c 100644 --- a/lib/jobManager.js +++ b/lib/jobManager.js @@ -109,14 +109,15 @@ var JobManager = module.exports = function JobManager(options){ } })(); - this.updateCurrentJob = function(publicKey){ + this.updateCurrentJob = function(){ var tmpBlockTemplate = new blockTemplate( jobCounter.next(), _this.currentJob.rpcData, - publicKey, + options.poolAddressScript, _this.extraNoncePlaceholder, options.coin.reward, - options.coin.txMessages + options.coin.txMessages, + options.recipients ); _this.currentJob = tmpBlockTemplate; @@ -128,7 +129,7 @@ var JobManager = module.exports = function JobManager(options){ }; //returns true if processed a new block - this.processTemplate = function(rpcData, publicKey){ + this.processTemplate = function(rpcData){ /* Block is new if A) its the first block we have seen so far or B) the blockhash is different and the block height is greater than the one we have */ @@ -159,10 +160,11 @@ var JobManager = module.exports = function JobManager(options){ var tmpBlockTemplate = new blockTemplate( jobCounter.next(), rpcData, - publicKey, + options.poolAddressScript, _this.extraNoncePlaceholder, options.coin.reward, - options.coin.txMessages + options.coin.txMessages, + options.recipients ); this.currentJob = tmpBlockTemplate; @@ -283,7 +285,8 @@ var JobManager = module.exports = function JobManager(options){ ip: ipAddress, worker: workerName, height: job.rpcData.height, - reward: job.rpcData.coinbasevalue, + blockReward: job.rpcData.coinbasevalue, + poolReward: job.poolReward, difficulty: difficulty, shareDiff: shareDiff.toFixed(8), blockDiff : blockDiffAdjusted, diff --git a/lib/peer.js b/lib/peer.js index a3b4365..5435c3b 100644 --- a/lib/peer.js +++ b/lib/peer.js @@ -46,7 +46,7 @@ var Peer = module.exports = function (options) { var _this = this; var client; - var magic = new Buffer(options.p2p.magic, 'hex'); + var magic = new Buffer(options.testnet ? options.coin.peerMagicTestnet : options.coin.peerMagic, 'hex'); var magicInt = magic.readUInt32LE(0); var verack = false; var validConnectionConfig = true; diff --git a/lib/pool.js b/lib/pool.js index 8448537..4e9b66a 100644 --- a/lib/pool.js +++ b/lib/pool.js @@ -9,21 +9,6 @@ var jobManager = require('./jobManager.js'); var util = require('./util.js'); -var bignum = require('bignum'); - -/** - * Main pool object. It emits the following events: - * - started() - when the pool is effectively started - * - share(isValidShare, isValidBlock, shareData) - When a share is submitted - * - log(severity, key, text) - for debug, warning, and error messages - * - * It initializes and connects: - * - JobManager - for generating miner work, processing block templates and shares - * - DaemonInterface - for RPC communication with daemon - * - StratumServer - for TCP socket communication with miners - * - */ - var pool = module.exports = function pool(options, authorizeFn){ this.options = options; @@ -44,13 +29,6 @@ var pool = module.exports = function pool(options, authorizeFn){ throw new Error(); } - //var diff1 = options.coin.diffShift ? - // util.getTruncatedDiff(options.coin.diffShift) : - // algos[options.coin.algorithm].diff; - - - //Which number to use as dividend when converting difficulty to target - //var maxDifficulty = algos[options.coin.algorithm].maxDiff; this.start = function(){ @@ -58,6 +36,7 @@ var pool = module.exports = function pool(options, authorizeFn){ SetupApi(); SetupDaemonInterface(function(){ DetectCoinData(function(){ + SetupRecipients(); SetupJobManager(); OnBlockchainSynced(function(){ GetFirstJob(function(){ @@ -122,7 +101,8 @@ var pool = module.exports = function pool(options, authorizeFn){ 'Current Block Diff:\t' + _this.jobManager.currentJob.difficulty * algos[options.coin.algorithm].multiplier, 'Network Difficulty:\t' + options.initStats.difficulty, 'Network Hash Rate:\t' + util.getReadableHashRateString(options.initStats.networkHashRate), - 'Stratum Port(s):\t' + _this.options.initStats.stratumPorts.join(', ') + 'Stratum Port(s):\t' + _this.options.initStats.stratumPorts.join(', '), + 'Pool Fee Percent:\t' + _this.options.feePercent + '%' ]; if (typeof options.blockRefreshInterval === "number" && options.blockRefreshInterval > 0) @@ -198,6 +178,16 @@ var pool = module.exports = function pool(options, authorizeFn){ function SetupPeer(){ if (!options.p2p || !options.p2p.enabled) return; + + if (options.testnet && !options.coin.peerMagicTestnet){ + emitErrorLog('p2p cannot be enabled in testnet without peerMagicTestnet set in coin configuration'); + return; + } + else if (!options.coin.peerMagic){ + emitErrorLog('p2p cannot be enabled without peerMagic set in coin configuration'); + return; + } + _this.peer = new peer(options); _this.peer.on('connected', function() { emitLog('p2p connection successful'); @@ -263,6 +253,33 @@ var pool = module.exports = function pool(options, authorizeFn){ } + function SetupRecipients(){ + var recipients = []; + options.feePercent = 0; + options.rewardRecipients = options.rewardRecipients || {}; + for (var r in options.rewardRecipients){ + var percent = options.rewardRecipients[r]; + var rObj = { + percent: percent / 100 + }; + try { + if (r.length === 40) + rObj.script = util.miningKeyToScript(r); + else + rObj.script = util.addressToScript(r); + recipients.push(rObj); + options.feePercent += percent; + } + catch(e){ + emitErrorLog('Error generating transaction output script for ' + r + ' in rewardRecipients'); + } + } + if (recipients.length === 0){ + emitErrorLog('No rewardRecipients have been setup which means no fees will be taken'); + } + options.recipients = recipients; + } + function SetupJobManager(){ _this.jobManager = new jobManager(options); @@ -385,7 +402,7 @@ var pool = module.exports = function pool(options, authorizeFn){ return; } - options.publicKeyBuffer = (function(){ + options.poolAddressScript = (function(){ switch(options.coin.reward){ case 'POS': return util.pubkeyToScript(rpcResults.validateaddress.pubkey); @@ -433,7 +450,7 @@ var pool = module.exports = function pool(options, authorizeFn){ }).on('broadcastTimeout', function(){ emitLog('No new work for ' + options.jobRebroadcastTimeout + ' seconds - updating & rebroadcasting current job'); - _this.jobManager.updateCurrentJob(options.publicKeyBuffer); + _this.jobManager.updateCurrentJob(); }).on('client.connected', function(client){ if (typeof(_this.varDiff[client.socket.localPort]) !== 'undefined') { @@ -541,16 +558,7 @@ var pool = module.exports = function pool(options, authorizeFn){ result.instance.index + ' with error ' + JSON.stringify(result.error)); callback(result.error); } else { - var processedNewBlock = _this.jobManager.processTemplate(result.response, options.publicKeyBuffer); - - if (processedNewBlock) { - - Object.keys(_this.varDiff).forEach(function(port){ - _this.varDiff[port].setNetworkDifficulty(_this.jobManager.currentJob.difficulty); - }); - } - - + var processedNewBlock = _this.jobManager.processTemplate(result.response); callback(null, result.response, processedNewBlock); callback = function(){}; } diff --git a/lib/transactions.js b/lib/transactions.js index e3d4f95..62a405b 100644 --- a/lib/transactions.js +++ b/lib/transactions.js @@ -123,9 +123,42 @@ For some (probably outdated and incorrect) documentation about whats kinda going see: https://en.bitcoin.it/wiki/Protocol_specification#tx */ +var generateOutputTransactions = function(poolRecipient, recipients, reward){ + + var totalOutputs = 1; + + var txOutputBuffers = []; + + var totalToRecipients = 0; + + for (var i = 0; i < recipients.length; i++){ + var recipientReward = Math.floor(recipients[i].percent * reward); + totalToRecipients += recipientReward; + totalOutputs++; + txOutputBuffers.push(Buffer.concat([ + util.packInt64LE(recipientReward), + util.varIntBuffer(recipients[i].script.length), + recipients[i].script + ])); + } + + var rewardToPool = reward - totalToRecipients; + + txOutputBuffers.push(Buffer.concat([ + util.packInt64LE(rewardToPool), + util.varIntBuffer(poolRecipient.length), + poolRecipient + ])); + + return Buffer.concat([ + util.varIntBuffer(totalOutputs), + Buffer.concat(txOutputBuffers) + ]); + +}; -exports.CreateGeneration = function(rpcData, publicKey, extraNoncePlaceholder, reward, txMessages){ +exports.CreateGeneration = function(rpcData, publicKey, extraNoncePlaceholder, reward, txMessages, recipients){ var txInputsCount = 1; var txOutputsCount = 1; @@ -158,9 +191,9 @@ exports.CreateGeneration = function(rpcData, publicKey, extraNoncePlaceholder, r var p1 = Buffer.concat([ util.packUInt32LE(txVersion), txTimestamp, - util.varIntBuffer(txInputsCount), //transaction input + util.varIntBuffer(txInputsCount), util.uint256BufferFromHash(txInPrevOutHash), util.packUInt32LE(txInPrevOutIndex), util.varIntBuffer(scriptSigPart1.length + extraNoncePlaceholder.length + scriptSigPart2.length), @@ -179,12 +212,8 @@ exports.CreateGeneration = function(rpcData, publicKey, extraNoncePlaceholder, r util.packUInt32LE(txInSequence), //end transaction input - util.varIntBuffer(txOutputsCount), - //transaction output - util.packInt64LE(rpcData.coinbasevalue), - util.varIntBuffer(publicKey.length), - publicKey, + generateOutputTransactions(publicKey, recipients, rpcData.coinbasevalue), //end transaction ouput util.packUInt32LE(txLockTime), diff --git a/lib/util.js b/lib/util.js index 62f7146..453939c 100644 --- a/lib/util.js +++ b/lib/util.js @@ -253,6 +253,11 @@ exports.pubkeyToScript = function(key){ }; +exports.miningKeyToScript = function(key){ + var keyBuffer = new Buffer(key, 'hex'); + return Buffer.concat([new Buffer([0x76, 0xa9, 0x14]), keyBuffer, new Buffer([0x88, 0xac])]); +}; + /* For POW coins - used to format wallet address for use in generation transaction's output */ diff --git a/lib/varDiff.js b/lib/varDiff.js index 350a4ea..062ee75 100644 --- a/lib/varDiff.js +++ b/lib/varDiff.js @@ -48,8 +48,6 @@ function toFixed(num, len) { var varDiff = module.exports = function varDiff(port, varDiffOptions){ var _this = this; - var networkDifficulty; - var bufferSize, tMin, tMax; //if (!varDiffOptions) return; @@ -61,9 +59,6 @@ var varDiff = module.exports = function varDiff(port, varDiffOptions){ tMin = varDiffOptions.targetTime - variance; tMax = varDiffOptions.targetTime + variance; - this.setNetworkDifficulty = function(diff){ - networkDifficulty = diff; - }; this.manageClient = function(client){ @@ -113,7 +108,6 @@ var varDiff = module.exports = function varDiff(port, varDiffOptions){ if (options.x2mode) { ddiff = 2; } - //var diffMax = networkDifficulty < options.maxDiff ? networkDifficulty : options.maxDiff; var diffMax = options.maxDiff; if (ddiff * client.difficulty > diffMax) { ddiff = diffMax / client.difficulty;