diff --git a/README.md b/README.md index 8766f6710..7405f702d 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,7 @@ var myCoin = { "nValue": 1024, //optional - defaults to 1024 "rValue": 1, //optional - defaults to 1 "txMessages": false, //optional - defaults to false, + "coinbase": "MyFunPool", // So explorers can see who mined the block /* Magic value only required for setting up p2p block notifications. It is found in the daemon source code as the pchMessageStart variable. diff --git a/lib/algoProperties.js b/lib/algoProperties.js index 5f559fce0..fb4c06450 100644 --- a/lib/algoProperties.js +++ b/lib/algoProperties.js @@ -139,6 +139,14 @@ var algos = module.exports = global.algos = { } } }, + allium: { + multiplier: Math.pow(2, 8), + hash: function(){ + return function(){ + return multiHashing.allium.apply(this, arguments); + } + } + }, blake: { multiplier: Math.pow(2, 8), hash: function(){ @@ -184,12 +192,35 @@ var algos = module.exports = global.algos = { } } }, + lyra2: { + multiplier: Math.pow(2, 8), + hash: function(){ + return function(){ + return multiHashing.lyra2re.apply(this, arguments); + } + } + }, + lyra2v2: { + multiplier: Math.pow(2, 8), + hash: function(){ + return function(){ + return multiHashing.lyra2rev2.apply(this, arguments); + } + } + }, qubit: { hash: function(){ return function(){ return multiHashing.qubit.apply(this, arguments); } } + }, + cryptonight_turtle: { + hash: function(){ + return function(){ + return multiHashing.cryptonightturtle.apply(this, arguments); + } + } } }; diff --git a/lib/blockTemplate.js b/lib/blockTemplate.js index 89a3079c5..419c91e7a 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, poolAddressScript, extraNoncePlaceholder, reward, txMessages, recipients){ +var BlockTemplate = module.exports = function BlockTemplate(jobId, rpcData, poolAddressScript, extraNoncePlaceholder, reward, txMessages, recipients, coinbase){ //private members @@ -71,7 +71,8 @@ var BlockTemplate = module.exports = function BlockTemplate(jobId, rpcData, pool extraNoncePlaceholder, reward, txMessages, - recipients + recipients, + coinbase ); this.serializeCoinbase = function(extraNonce1, extraNonce2){ diff --git a/lib/index.js b/lib/index.js index fd5b7bad8..bf21bbe07 100644 --- a/lib/index.js +++ b/lib/index.js @@ -11,6 +11,16 @@ exports.varDiff = require('./varDiff.js'); exports.createPool = function(poolOptions, authorizeFn){ + + if(poolOptions.OLD === true) + { + pool = require('./pool_O.js'); + } + else + { + pool = require('./pool_N.js'); + } + var newPool = new pool(poolOptions, authorizeFn); return newPool; }; diff --git a/lib/jobManager.js b/lib/jobManager.js index 1fc31b23c..ec7f83f19 100644 --- a/lib/jobManager.js +++ b/lib/jobManager.js @@ -127,7 +127,8 @@ var JobManager = module.exports = function JobManager(options){ _this.extraNoncePlaceholder, options.coin.reward, options.coin.txMessages, - options.recipients + options.recipients, + options.coin.coinbase ); _this.currentJob = tmpBlockTemplate; @@ -162,7 +163,8 @@ var JobManager = module.exports = function JobManager(options){ _this.extraNoncePlaceholder, options.coin.reward, options.coin.txMessages, - options.recipients + options.recipients, + options.coin.coinbase ); this.currentJob = tmpBlockTemplate; @@ -268,7 +270,21 @@ var JobManager = module.exports = function JobManager(options){ //Check if share is a block candidate (matched network difficulty) if (job.target.ge(headerBigNum)){ blockHex = job.serializeBlock(headerBuffer, coinbaseBuffer).toString('hex'); - blockHash = blockHasher(headerBuffer, nTime).toString('hex'); + if (options.coin.algorithm === 'blake' + || options.coin.algorithm === 'neoscrypt' + || options.coin.algorithm === 'lyra2' + || options.coin.algorithm === 'allium' + || options.coin.algorithm === 'lyra2v2' + || options.coin.algorithm === 'qubit' + ) { + blockHash = util.reverseBuffer(util.sha256d(headerBuffer, nTime)).toString('hex'); + } else if(options.coin.algorithm === 'x11'){ + blockHash = util.reverseBuffer(hashDigest(headerBuffer, nTime)).toString('hex'); + } + else { + blockHash = blockHasher(headerBuffer, nTime).toString('hex'); + } + //blockHash = blockHasher(headerBuffer, nTime).toString('hex'); /*console.log("processShare - Block Candidate", { "blockHex": blockHex, "blockHash": blockHash diff --git a/lib/pool_N.js b/lib/pool_N.js new file mode 100644 index 000000000..71700c905 --- /dev/null +++ b/lib/pool_N.js @@ -0,0 +1,697 @@ +var events = require('events'); +var async = require('async'); + +var varDiff = require('./varDiff.js'); +var daemon = require('./daemon.js'); +var peer = require('./peer.js'); +var stratum = require('./stratum.js'); +var jobManager = require('./jobManager.js'); +var util = require('./util.js'); + +/*process.on('uncaughtException', function(err) { + console.log(err.stack); + throw err; +});*/ + +var pool = module.exports = function pool(options, authorizeFn){ + + this.options = options; + + var _this = this; + var blockPollingIntervalId; + + + var emitLog = function(text) { _this.emit('log', 'debug' , text); }; + var emitWarningLog = function(text) { _this.emit('log', 'warning', text); }; + var emitErrorLog = function(text) { _this.emit('log', 'error' , text); }; + var emitSpecialLog = function(text) { _this.emit('log', 'special', text); }; + + + + if (!(options.coin.algorithm in algos)){ + emitErrorLog('The ' + options.coin.algorithm + ' hashing algorithm is not supported.'); + throw new Error(); + } + + + + this.start = function(){ + SetupVarDiff(); + SetupApi(); + SetupDaemonInterface(function(){ + DetectCoinData(function(){ + SetupRecipients(); + SetupJobManager(); + OnBlockchainSynced(function(){ + GetFirstJob(function(){ + SetupBlockPolling(); + SetupPeer(); + StartStratumServer(function(){ + OutputPoolInfo(); + _this.emit('started'); + }); + }); + }); + }); + }); + }; + + + + function GetFirstJob(finishedCallback){ + + GetBlockTemplate(function(error, result){ + if (error) { + emitErrorLog('Error with getblocktemplate on creating first job, server cannot start'); + return; + } + + var portWarnings = []; + + var networkDiffAdjusted = options.initStats.difficulty; + + Object.keys(options.ports).forEach(function(port){ + var portDiff = options.ports[port].diff; + if (networkDiffAdjusted < portDiff) + portWarnings.push('port ' + port + ' w/ diff ' + portDiff); + }); + + //Only let the first fork show synced status or the log wil look flooded with it + if (portWarnings.length > 0 && (!process.env.forkId || process.env.forkId === '0')) { + var warnMessage = 'Network diff of ' + networkDiffAdjusted + ' is lower than ' + + portWarnings.join(' and '); + emitWarningLog(warnMessage); + } + + finishedCallback(); + + }); + } + + + function OutputPoolInfo(){ + + var startMessage = 'Stratum Pool Server Started for ' + options.coin.name + + ' [' + options.coin.symbol.toUpperCase() + '] {' + options.coin.algorithm + '}'; + if (process.env.forkId && process.env.forkId !== '0'){ + emitLog(startMessage); + return; + } + var infoLines = [startMessage, + 'Network Connected:\t' + (options.testnet ? 'Testnet' : 'Mainnet'), + 'Detected Reward Type:\t' + options.coin.reward, + 'Current Block Height:\t' + _this.jobManager.currentJob.rpcData.height, + 'Current Connect Peers:\t' + options.initStats.connections, + '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(', '), + 'Pool Fee Percent:\t' + _this.options.feePercent + '%' + ]; + + if (typeof options.blockRefreshInterval === "number" && options.blockRefreshInterval > 0) + infoLines.push('Block polling every:\t' + options.blockRefreshInterval + ' ms'); + + emitSpecialLog(infoLines.join('\n\t\t\t\t\t\t')); + } + + + function OnBlockchainSynced(syncedCallback){ + + var checkSynced = function(displayNotSynced){ + _this.daemon.cmd('getblocktemplate', [{"capabilities": [ "coinbasetxn", "workid", "coinbase/append" ], "rules": [ "segwit" ]}], function(results){ + var synced = results.every(function(r){ + return !r.error || r.error.code !== -10; + }); + if (synced){ + syncedCallback(); + } + else{ + if (displayNotSynced) displayNotSynced(); + setTimeout(checkSynced, 5000); + + //Only let the first fork show synced status or the log wil look flooded with it + if (!process.env.forkId || process.env.forkId === '0') + generateProgress(); + } + + }); + }; + checkSynced(function(){ + //Only let the first fork show synced status or the log wil look flooded with it + if (!process.env.forkId || process.env.forkId === '0') + emitErrorLog('Daemon is still syncing with network (download blockchain) - server will be started once synced'); + }); + + + var generateProgress = function(){ + + _this.daemon.cmd('getblockchaininfo', [], function(results) { + var blockCount = results.sort(function (a, b) { + return b.response.blocks - a.response.blocks; + })[0].response.blocks; + + //get list of peers and their highest block height to compare to ours + _this.daemon.cmd('getpeerinfo', [], function(results){ + + var peers = results[0].response; + var totalBlocks = peers.sort(function(a, b){ + return b.startingheight - a.startingheight; + })[0].startingheight; + + var percent = (blockCount / totalBlocks * 100).toFixed(2); + emitWarningLog('Downloaded ' + percent + '% of blockchain from ' + peers.length + ' peers'); + }); + + }); + }; + + } + + + function SetupApi() { + if (typeof(options.api) !== 'object' || typeof(options.api.start) !== 'function') { + return; + } else { + options.api.start(_this); + } + } + + + 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'); + }).on('connectionRejected', function(){ + emitErrorLog('p2p connection failed - likely incorrect p2p magic value'); + }).on('disconnected', function(){ + emitWarningLog('p2p peer node disconnected - attempting reconnection...'); + }).on('connectionFailed', function(e){ + emitErrorLog('p2p connection failed - likely incorrect host or port'); + }).on('socketError', function(e){ + emitErrorLog('p2p had a socket error ' + JSON.stringify(e)); + }).on('error', function(msg){ + emitWarningLog('p2p had an error ' + msg); + }).on('blockFound', function(hash){ + _this.processBlockNotify(hash, 'p2p'); + }); + } + + + function SetupVarDiff(){ + _this.varDiff = {}; + Object.keys(options.ports).forEach(function(port) { + if (options.ports[port].varDiff) + _this.setVarDiff(port, options.ports[port].varDiff); + }); + } + + + /* + Coin daemons either use submitblock or getblocktemplate for submitting new blocks + */ + function SubmitBlock(blockHex, callback){ + + var rpcCommand, rpcArgs; + if (options.hasSubmitMethod){ + rpcCommand = 'submitblock'; + rpcArgs = [blockHex]; + } + else{ + rpcCommand = 'getblocktemplate'; + rpcArgs = [{'mode': 'submit', 'data': blockHex}]; + } + + + _this.daemon.cmd(rpcCommand, + rpcArgs, + function(results){ + for (var i = 0; i < results.length; i++){ + var result = results[i]; + if (result.error) { + emitErrorLog('rpc error with daemon instance ' + + result.instance.index + ' when submitting block with ' + rpcCommand + ' ' + + JSON.stringify(result.error) + ); + return; + } + else if (result.response === 'rejected') { + emitErrorLog('Daemon instance ' + result.instance.index + ' rejected a supposedly valid block'); + return; + } + } + emitLog('Submitted Block using ' + rpcCommand + ' successfully to daemon instance(s)'); + callback(); + } + ); + + } + + + 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); + + _this.jobManager.on('newBlock', function(blockTemplate){ + //Check if stratumServer has been initialized yet + if (_this.stratumServer) { + _this.stratumServer.broadcastMiningJobs(blockTemplate.getJobParams()); + } + }).on('updatedBlock', function(blockTemplate){ + //Check if stratumServer has been initialized yet + if (_this.stratumServer) { + var job = blockTemplate.getJobParams(); + job[8] = false; + _this.stratumServer.broadcastMiningJobs(job); + } + }).on('share', function(shareData, blockHex){ + var isValidShare = !shareData.error; + var isValidBlock = !!blockHex; + var emitShare = function(){ + _this.emit('share', isValidShare, isValidBlock, shareData); + }; + + /* + If we calculated that the block solution was found, + before we emit the share, lets submit the block, + then check if it was accepted using RPC getblock + */ + if (!isValidBlock) + emitShare(); + else{ + SubmitBlock(blockHex, function(){ + CheckBlockAccepted(shareData.blockHash, function(isAccepted, tx){ + isValidBlock = isAccepted; + shareData.txHash = tx; + emitShare(); + + GetBlockTemplate(function(error, result, foundNewBlock){ + if (foundNewBlock) + emitLog('Block notification via RPC after block submission'); + }); + + }); + }); + } + }).on('log', function(severity, message){ + _this.emit('log', severity, message); + }); + } + + + function SetupDaemonInterface(finishedCallback){ + + if (!Array.isArray(options.daemons) || options.daemons.length < 1){ + emitErrorLog('No daemons have been configured - pool cannot start'); + return; + } + + _this.daemon = new daemon.interface(options.daemons, function(severity, message){ + _this.emit('log', severity , message); + }); + + _this.daemon.once('online', function(){ + finishedCallback(); + + }).on('connectionFailed', function(error){ + emitErrorLog('Failed to connect daemon(s): ' + JSON.stringify(error)); + + }).on('error', function(message){ + emitErrorLog(message); + + }); + + _this.daemon.init(); + } + + + function DetectCoinData(finishedCallback){ + + var batchRpcCalls = [ + ['validateaddress', [options.address]], + ['getdifficulty', []], + ['getblockchaininfo', []], + ['getnetworkinfo', []], + ['getmininginfo', []], + ['submitblock', []] + ]; + + _this.daemon.batchCmd(batchRpcCalls, function(error, results){ + if (error || !results){ + emitErrorLog('Could not start pool, error with init batch RPC call: ' + JSON.stringify(error)); + return; + } + + var rpcResults = {}; + + for (var i = 0; i < results.length; i++){ + var rpcCall = batchRpcCalls[i][0]; + var r = results[i]; + rpcResults[rpcCall] = r.result || r.error; + + if (rpcCall !== 'submitblock' && (r.error || !r.result)){ + emitErrorLog('Could not start pool, error with init RPC ' + rpcCall + ' - ' + JSON.stringify(r.error)); + return; + } + } + + if (!rpcResults.validateaddress.isvalid){ + emitErrorLog('Daemon reports address is not valid'); + return; + } + + if (!options.coin.reward) { + if (isNaN(rpcResults.getdifficulty) && 'proof-of-stake' in rpcResults.getdifficulty) + options.coin.reward = 'POS'; + else + options.coin.reward = 'POW'; + } + + + /* POS coins must use the pubkey in coinbase transaction, and pubkey is + only given if address is owned by wallet.*/ + if (options.coin.reward === 'POS' && typeof(rpcResults.validateaddress.pubkey) == 'undefined') { + emitErrorLog('The address provided is not from the daemon wallet - this is required for POS coins.'); + return; + } + + options.testnet = (rpcResults.getblockchaininfo.chain === 'test') ? true : false; + + options.network = (options.testnet ? options.coin.testnet : options.coin.mainnet); + + options.poolAddressScript = (function(){ + switch(options.coin.reward){ + case 'POS': + return util.pubkeyToScript(rpcResults.validateaddress.pubkey); + case 'POW': + return util.addressToScript(rpcResults.validateaddress.address); + } + })(); + + options.protocolVersion = rpcResults.getnetworkinfo.protocolversion; + + options.initStats = { + connections: rpcResults.getnetworkinfo.connections, + difficulty: rpcResults.getblockchaininfo.difficulty * algos[options.coin.algorithm].multiplier, + networkHashRate: rpcResults.getmininginfo.networkhashps + }; + + + if (rpcResults.submitblock.message === 'Method not found'){ + options.hasSubmitMethod = false; + } + else if (rpcResults.submitblock.code === -1){ + options.hasSubmitMethod = true; + } + else { + emitErrorLog('Could not detect block submission RPC method, ' + JSON.stringify(results)); + return; + } + + finishedCallback(); + + }); + } + + + + function StartStratumServer(finishedCallback){ + _this.stratumServer = new stratum.Server(options, authorizeFn); + + _this.stratumServer.on('started', function(){ + options.initStats.stratumPorts = Object.keys(options.ports); + _this.stratumServer.broadcastMiningJobs(_this.jobManager.currentJob.getJobParams()); + finishedCallback(); + + }).on('broadcastTimeout', function(){ + emitLog('No new blocks for ' + options.jobRebroadcastTimeout + ' seconds - updating transactions & rebroadcasting work'); + + GetBlockTemplate(function(error, rpcData, processedBlock){ + if (error || processedBlock) return; + _this.jobManager.updateCurrentJob(rpcData); + }); + + }).on('client.connected', function(client){ + if (typeof(_this.varDiff[client.socket.localPort]) !== 'undefined') { + _this.varDiff[client.socket.localPort].manageClient(client); + } + + client.on('difficultyChanged', function(diff){ + _this.emit('difficultyUpdate', client.workerName, diff); + + }).on('subscription', function(params, resultCallback){ + + var extraNonce = _this.jobManager.extraNonceCounter.next(); + var extraNonce2Size = _this.jobManager.extraNonce2Size; + resultCallback(null, + extraNonce, + extraNonce2Size + ); + + if (typeof(options.ports[client.socket.localPort]) !== 'undefined' && options.ports[client.socket.localPort].diff) { + this.sendDifficulty(options.ports[client.socket.localPort].diff); + } else { + this.sendDifficulty(8); + } + + this.sendMiningJob(_this.jobManager.currentJob.getJobParams()); + + }).on('submit', function(params, resultCallback){ + var result =_this.jobManager.processShare( + params.jobId, + client.previousDifficulty, + client.difficulty, + client.extraNonce1, + params.extraNonce2, + params.nTime, + params.nonce, + client.remoteAddress, + client.socket.localPort, + params.name + ); + + resultCallback(result.error, result.result ? true : null); + + }).on('malformedMessage', function (message) { + emitWarningLog('Malformed message from ' + client.getLabel() + ': ' + message); + + }).on('socketError', function(err) { + emitWarningLog('Socket error from ' + client.getLabel() + ': ' + JSON.stringify(err)); + + }).on('socketTimeout', function(reason){ + emitWarningLog('Connected timed out for ' + client.getLabel() + ': ' + reason) + + }).on('socketDisconnect', function() { + //emitLog('Socket disconnected from ' + client.getLabel()); + + }).on('kickedBannedIP', function(remainingBanTime){ + emitLog('Rejected incoming connection from ' + client.remoteAddress + ' banned for ' + remainingBanTime + ' more seconds'); + + }).on('forgaveBannedIP', function(){ + emitLog('Forgave banned IP ' + client.remoteAddress); + + }).on('unknownStratumMethod', function(fullMessage) { + emitLog('Unknown stratum method from ' + client.getLabel() + ': ' + fullMessage.method); + + }).on('socketFlooded', function() { + emitWarningLog('Detected socket flooding from ' + client.getLabel()); + + }).on('tcpProxyError', function(data) { + emitErrorLog('Client IP detection failed, tcpProxyProtocol is enabled yet did not receive proxy protocol message, instead got data: ' + data); + + }).on('bootedBannedWorker', function(){ + emitWarningLog('Booted worker ' + client.getLabel() + ' who was connected from an IP address that was just banned'); + + }).on('triggerBan', function(reason){ + emitWarningLog('Banned triggered for ' + client.getLabel() + ': ' + reason); + _this.emit('banIP', client.remoteAddress, client.workerName); + }); + }); + } + + + + function SetupBlockPolling(){ + if (typeof options.blockRefreshInterval !== "number" || options.blockRefreshInterval <= 0){ + emitLog('Block template polling has been disabled'); + return; + } + + var pollingInterval = options.blockRefreshInterval; + + blockPollingIntervalId = setInterval(function () { + GetBlockTemplate(function(error, result, foundNewBlock){ + if (foundNewBlock) + emitLog('Block notification via RPC polling'); + }); + }, pollingInterval); + } + + + + function GetBlockTemplate(callback){ + _this.daemon.cmd('getblocktemplate', + [{"capabilities": [ "coinbasetxn", "workid", "coinbase/append" ], "rules": [ "segwit" ]}], + function(result){ + if (result.error){ + emitErrorLog('getblocktemplate call failed for daemon instance ' + + result.instance.index + ' with error ' + JSON.stringify(result.error)); + callback(result.error); + } else { + var processedNewBlock = _this.jobManager.processTemplate(result.response); + callback(null, result.response, processedNewBlock); + callback = function(){}; + } + }, true + ); + } + + + + function CheckBlockAccepted(blockHash, callback){ + //setTimeout(function(){ + _this.daemon.cmd('getblock', + [blockHash], + function(results){ + var validResults = results.filter(function(result){ + return result.response && (result.response.hash === blockHash) + }); + + if (validResults.length >= 1){ + callback(true, validResults[0].response.tx[0]); + } + else{ + callback(false); + } + } + ); + //}, 500); + } + + + + /** + * This method is being called from the blockNotify so that when a new block is discovered by the daemon + * We can inform our miners about the newly found block + **/ + this.processBlockNotify = function(blockHash, sourceTrigger) { + emitLog('Block notification via ' + sourceTrigger + ' ' + util.reverseHex(blockHash)); + if (typeof(_this.jobManager.currentJob) !== 'undefined' && blockHash !== _this.jobManager.currentJob.rpcData.previousblockhash){ + GetBlockTemplate(function(error, result){ + if (error) + emitErrorLog('Block notify error getting block template for ' + options.coin.name); + }) + } + }; + + + this.relinquishMiners = function(filterFn, resultCback) { + var origStratumClients = this.stratumServer.getStratumClients(); + + var stratumClients = []; + Object.keys(origStratumClients).forEach(function (subId) { + stratumClients.push({subId: subId, client: origStratumClients[subId]}); + }); + async.filter( + stratumClients, + filterFn, + function (clientsToRelinquish) { + clientsToRelinquish.forEach(function(cObj) { + cObj.client.removeAllListeners(); + _this.stratumServer.removeStratumClientBySubId(cObj.subId); + }); + + process.nextTick(function () { + resultCback( + clientsToRelinquish.map( + function (item) { + return item.client; + } + ) + ); + }); + } + ) + }; + + + this.attachMiners = function(miners) { + miners.forEach(function (clientObj) { + _this.stratumServer.manuallyAddStratumClient(clientObj); + }); + _this.stratumServer.broadcastMiningJobs(_this.jobManager.currentJob.getJobParams()); + + }; + + + this.getStratumServer = function() { + return _this.stratumServer; + }; + + + this.setVarDiff = function(port, varDiffConfig) { + if (typeof(_this.varDiff[port]) != 'undefined' ) { + _this.varDiff[port].removeAllListeners(); + } + var varDiffInstance = new varDiff(port, varDiffConfig); + _this.varDiff[port] = varDiffInstance; + _this.varDiff[port].on('newDifficulty', function(client, newDiff) { + + /* We request to set the newDiff @ the next difficulty retarget + (which should happen when a new job comes in - AKA BLOCK) */ + client.enqueueNextDifficulty(newDiff); + + /*if (options.varDiff.mode === 'fast'){ + //Send new difficulty, then force miner to use new diff by resending the + //current job parameters but with the "clean jobs" flag set to false + //so the miner doesn't restart work and submit duplicate shares + client.sendDifficulty(newDiff); + var job = _this.jobManager.currentJob.getJobParams(); + job[8] = false; + client.sendMiningJob(job); + }*/ + + }); + }; + +}; +pool.prototype.__proto__ = events.EventEmitter.prototype; diff --git a/lib/pool_O.js b/lib/pool_O.js new file mode 100644 index 000000000..c12075a3d --- /dev/null +++ b/lib/pool_O.js @@ -0,0 +1,696 @@ +var events = require('events'); +var async = require('async'); + +var varDiff = require('./varDiff.js'); +var daemon = require('./daemon.js'); +var peer = require('./peer.js'); +var stratum = require('./stratum.js'); +var jobManager = require('./jobManager.js'); +var util = require('./util.js'); + +/*process.on('uncaughtException', function(err) { + console.log(err.stack); + throw err; +});*/ + +var pool = module.exports = function pool(options, authorizeFn){ + + this.options = options; + + var _this = this; + var blockPollingIntervalId; + + + var emitLog = function(text) { _this.emit('log', 'debug' , text); }; + var emitWarningLog = function(text) { _this.emit('log', 'warning', text); }; + var emitErrorLog = function(text) { _this.emit('log', 'error' , text); }; + var emitSpecialLog = function(text) { _this.emit('log', 'special', text); }; + + + + if (!(options.coin.algorithm in algos)){ + emitErrorLog('The ' + options.coin.algorithm + ' hashing algorithm is not supported.'); + throw new Error(); + } + + + + this.start = function(){ + SetupVarDiff(); + SetupApi(); + SetupDaemonInterface(function(){ + DetectCoinData(function(){ + SetupRecipients(); + SetupJobManager(); + OnBlockchainSynced(function(){ + GetFirstJob(function(){ + SetupBlockPolling(); + SetupPeer(); + StartStratumServer(function(){ + OutputPoolInfo(); + _this.emit('started'); + }); + }); + }); + }); + }); + }; + + + + function GetFirstJob(finishedCallback){ + + GetBlockTemplate(function(error, result){ + if (error) { + emitErrorLog('Error with getblocktemplate on creating first job, server cannot start'); + return; + } + + var portWarnings = []; + + var networkDiffAdjusted = options.initStats.difficulty; + + Object.keys(options.ports).forEach(function(port){ + var portDiff = options.ports[port].diff; + if (networkDiffAdjusted < portDiff) + portWarnings.push('port ' + port + ' w/ diff ' + portDiff); + }); + + //Only let the first fork show synced status or the log wil look flooded with it + if (portWarnings.length > 0 && (!process.env.forkId || process.env.forkId === '0')) { + var warnMessage = 'Network diff of ' + networkDiffAdjusted + ' is lower than ' + + portWarnings.join(' and '); + emitWarningLog(warnMessage); + } + + finishedCallback(); + + }); + } + + + function OutputPoolInfo(){ + + var startMessage = 'Stratum Pool Server Started for ' + options.coin.name + + ' [' + options.coin.symbol.toUpperCase() + '] {' + options.coin.algorithm + '}'; + if (process.env.forkId && process.env.forkId !== '0'){ + emitLog(startMessage); + return; + } + var infoLines = [startMessage, + 'Network Connected:\t' + (options.testnet ? 'Testnet' : 'Mainnet'), + 'Detected Reward Type:\t' + options.coin.reward, + 'Current Block Height:\t' + _this.jobManager.currentJob.rpcData.height, + 'Current Connect Peers:\t' + options.initStats.connections, + '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(', '), + 'Pool Fee Percent:\t' + _this.options.feePercent + '%' + ]; + + if (typeof options.blockRefreshInterval === "number" && options.blockRefreshInterval > 0) + infoLines.push('Block polling every:\t' + options.blockRefreshInterval + ' ms'); + + emitSpecialLog(infoLines.join('\n\t\t\t\t\t\t')); + } + + + function OnBlockchainSynced(syncedCallback){ + + var checkSynced = function(displayNotSynced){ + _this.daemon.cmd('getblocktemplate', [{"capabilities": [ "coinbasetxn", "workid", "coinbase/append" ], "rules": [ "segwit" ]}], function(results){ + var synced = results.every(function(r){ + return !r.error || r.error.code !== -10; + }); + if (synced){ + syncedCallback(); + } + else{ + if (displayNotSynced) displayNotSynced(); + setTimeout(checkSynced, 5000); + + //Only let the first fork show synced status or the log wil look flooded with it + if (!process.env.forkId || process.env.forkId === '0') + generateProgress(); + } + + }); + }; + checkSynced(function(){ + //Only let the first fork show synced status or the log wil look flooded with it + if (!process.env.forkId || process.env.forkId === '0') + emitErrorLog('Daemon is still syncing with network (download blockchain) - server will be started once synced'); + }); + + + var generateProgress = function(){ + + _this.daemon.cmd('getinfo', [], function(results) { + var blockCount = results.sort(function (a, b) { + return b.response.blocks - a.response.blocks; + })[0].response.blocks; + + //get list of peers and their highest block height to compare to ours + _this.daemon.cmd('getpeerinfo', [], function(results){ + + var peers = results[0].response; + var totalBlocks = peers.sort(function(a, b){ + return b.startingheight - a.startingheight; + })[0].startingheight; + + var percent = (blockCount / totalBlocks * 100).toFixed(2); + emitWarningLog('Downloaded ' + percent + '% of blockchain from ' + peers.length + ' peers'); + }); + + }); + }; + + } + + + function SetupApi() { + if (typeof(options.api) !== 'object' || typeof(options.api.start) !== 'function') { + return; + } else { + options.api.start(_this); + } + } + + + 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'); + }).on('connectionRejected', function(){ + emitErrorLog('p2p connection failed - likely incorrect p2p magic value'); + }).on('disconnected', function(){ + emitWarningLog('p2p peer node disconnected - attempting reconnection...'); + }).on('connectionFailed', function(e){ + emitErrorLog('p2p connection failed - likely incorrect host or port'); + }).on('socketError', function(e){ + emitErrorLog('p2p had a socket error ' + JSON.stringify(e)); + }).on('error', function(msg){ + emitWarningLog('p2p had an error ' + msg); + }).on('blockFound', function(hash){ + _this.processBlockNotify(hash, 'p2p'); + }); + } + + + function SetupVarDiff(){ + _this.varDiff = {}; + Object.keys(options.ports).forEach(function(port) { + if (options.ports[port].varDiff) + _this.setVarDiff(port, options.ports[port].varDiff); + }); + } + + + /* + Coin daemons either use submitblock or getblocktemplate for submitting new blocks + */ + function SubmitBlock(blockHex, callback){ + + var rpcCommand, rpcArgs; + if (options.hasSubmitMethod){ + rpcCommand = 'submitblock'; + rpcArgs = [blockHex]; + } + else{ + rpcCommand = 'getblocktemplate'; + rpcArgs = [{'mode': 'submit', 'data': blockHex}]; // TODO: is mode submit even correct? + } + + + _this.daemon.cmd(rpcCommand, + rpcArgs, + function(results){ + for (var i = 0; i < results.length; i++){ + var result = results[i]; + if (result.error) { + emitErrorLog('rpc error with daemon instance ' + + result.instance.index + ' when submitting block with ' + rpcCommand + ' ' + + JSON.stringify(result.error) + ); + return; + } + else if (result.response === 'rejected') { + emitErrorLog('Daemon instance ' + result.instance.index + ' rejected a supposedly valid block'); + return; + } + } + emitLog('Submitted Block using ' + rpcCommand + ' successfully to daemon instance(s)'); + callback(); + } + ); + + } + + + 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); + + _this.jobManager.on('newBlock', function(blockTemplate){ + //Check if stratumServer has been initialized yet + if (_this.stratumServer) { + _this.stratumServer.broadcastMiningJobs(blockTemplate.getJobParams()); + } + }).on('updatedBlock', function(blockTemplate){ + //Check if stratumServer has been initialized yet + if (_this.stratumServer) { + var job = blockTemplate.getJobParams(); + job[8] = false; + _this.stratumServer.broadcastMiningJobs(job); + } + }).on('share', function(shareData, blockHex){ + var isValidShare = !shareData.error; + var isValidBlock = !!blockHex; + var emitShare = function(){ + _this.emit('share', isValidShare, isValidBlock, shareData); + }; + + /* + If we calculated that the block solution was found, + before we emit the share, lets submit the block, + then check if it was accepted using RPC getblock + */ + if (!isValidBlock) + emitShare(); + else{ + SubmitBlock(blockHex, function(){ + CheckBlockAccepted(shareData.blockHash, function(isAccepted, tx){ + isValidBlock = isAccepted; + shareData.txHash = tx; + emitShare(); + + GetBlockTemplate(function(error, result, foundNewBlock){ + if (foundNewBlock) + emitLog('Block notification via RPC after block submission'); + }); + + }); + }); + } + }).on('log', function(severity, message){ + _this.emit('log', severity, message); + }); + } + + + function SetupDaemonInterface(finishedCallback){ + + if (!Array.isArray(options.daemons) || options.daemons.length < 1){ + emitErrorLog('No daemons have been configured - pool cannot start'); + return; + } + + _this.daemon = new daemon.interface(options.daemons, function(severity, message){ + _this.emit('log', severity , message); + }); + + _this.daemon.once('online', function(){ + finishedCallback(); + + }).on('connectionFailed', function(error){ + emitErrorLog('Failed to connect daemon(s): ' + JSON.stringify(error)); + + }).on('error', function(message){ + emitErrorLog(message); + + }); + + _this.daemon.init(); + } + + + function DetectCoinData(finishedCallback){ + + var batchRpcCalls = [ + ['validateaddress', [options.address]], + ['getdifficulty', []], + ['getinfo', []], + ['getmininginfo', []], + ['submitblock', []] + ]; + + _this.daemon.batchCmd(batchRpcCalls, function(error, results){ + if (error || !results){ + emitErrorLog('Could not start pool, error with init batch RPC call: ' + JSON.stringify(error)); + return; + } + + var rpcResults = {}; + + for (var i = 0; i < results.length; i++){ + var rpcCall = batchRpcCalls[i][0]; + var r = results[i]; + rpcResults[rpcCall] = r.result || r.error; + + if (rpcCall !== 'submitblock' && (r.error || !r.result)){ + emitErrorLog('Could not start pool, error with init RPC ' + rpcCall + ' - ' + JSON.stringify(r.error)); + return; + } + } + + if (!rpcResults.validateaddress.isvalid){ + emitErrorLog('Daemon reports address is not valid'); + return; + } + + if (!options.coin.reward) { + if (isNaN(rpcResults.getdifficulty) && 'proof-of-stake' in rpcResults.getdifficulty) + options.coin.reward = 'POS'; + else + options.coin.reward = 'POW'; + } + + + /* POS coins must use the pubkey in coinbase transaction, and pubkey is + only given if address is owned by wallet.*/ + if (options.coin.reward === 'POS' && typeof(rpcResults.validateaddress.pubkey) == 'undefined') { + emitErrorLog('The address provided is not from the daemon wallet - this is required for POS coins.'); + return; + } + + options.poolAddressScript = (function(){ + switch(options.coin.reward){ + case 'POS': + return util.pubkeyToScript(rpcResults.validateaddress.pubkey); + case 'POW': + return util.addressToScript(rpcResults.validateaddress.address); + } + })(); + + options.testnet = rpcResults.getinfo.testnet; + options.protocolVersion = rpcResults.getinfo.protocolversion; + + options.initStats = { + connections: rpcResults.getinfo.connections, + difficulty: rpcResults.getinfo.difficulty * algos[options.coin.algorithm].multiplier, + networkHashRate: rpcResults.getmininginfo.networkhashps + }; + + + if (rpcResults.submitblock.message === 'Method not found'){ + options.hasSubmitMethod = false; + } + else if (rpcResults.submitblock.code === -1){ + options.hasSubmitMethod = true; + } + else { + emitErrorLog('Could not detect block submission RPC method, ' + JSON.stringify(results)); + return; + } + + finishedCallback(); + + }); + } + + + + function StartStratumServer(finishedCallback){ + _this.stratumServer = new stratum.Server(options, authorizeFn); + + _this.stratumServer.on('started', function(){ + options.initStats.stratumPorts = Object.keys(options.ports); + _this.stratumServer.broadcastMiningJobs(_this.jobManager.currentJob.getJobParams()); + finishedCallback(); + + }).on('broadcastTimeout', function(){ + emitLog('No new blocks for ' + options.jobRebroadcastTimeout + ' seconds - updating transactions & rebroadcasting work'); + + GetBlockTemplate(function(error, rpcData, processedBlock){ + if (error || processedBlock) return; + _this.jobManager.updateCurrentJob(rpcData); + }); + + }).on('client.connected', function(client){ + if (typeof(_this.varDiff[client.socket.localPort]) !== 'undefined') { + _this.varDiff[client.socket.localPort].manageClient(client); + } + + client.on('difficultyChanged', function(diff){ + _this.emit('difficultyUpdate', client.workerName, diff); + + }).on('subscription', function(params, resultCallback){ + + var extraNonce = _this.jobManager.extraNonceCounter.next(); + var extraNonce2Size = _this.jobManager.extraNonce2Size; + resultCallback(null, + extraNonce, + extraNonce2Size + ); + + if (typeof(options.ports[client.socket.localPort]) !== 'undefined' && options.ports[client.socket.localPort].diff) { + this.sendDifficulty(options.ports[client.socket.localPort].diff); + } else { + this.sendDifficulty(8); + } + + this.sendMiningJob(_this.jobManager.currentJob.getJobParams()); + + }).on('submit', function(params, resultCallback){ + var result =_this.jobManager.processShare( + params.jobId, + client.previousDifficulty, + client.difficulty, + client.extraNonce1, + params.extraNonce2, + params.nTime, + params.nonce, + client.remoteAddress, + client.socket.localPort, + params.name + ); + + resultCallback(result.error, result.result ? true : null); + + }).on('malformedMessage', function (message) { + emitWarningLog('Malformed message from ' + client.getLabel() + ': ' + message); + + }).on('socketError', function(err) { + emitWarningLog('Socket error from ' + client.getLabel() + ': ' + JSON.stringify(err)); + + }).on('socketTimeout', function(reason){ + emitWarningLog('Connected timed out for ' + client.getLabel() + ': ' + reason) + + }).on('socketDisconnect', function() { + //emitLog('Socket disconnected from ' + client.getLabel()); + + }).on('kickedBannedIP', function(remainingBanTime){ + emitLog('Rejected incoming connection from ' + client.remoteAddress + ' banned for ' + remainingBanTime + ' more seconds'); + + }).on('forgaveBannedIP', function(){ + emitLog('Forgave banned IP ' + client.remoteAddress); + + }).on('unknownStratumMethod', function(fullMessage) { + emitLog('Unknown stratum method from ' + client.getLabel() + ': ' + fullMessage.method); + + }).on('socketFlooded', function() { + emitWarningLog('Detected socket flooding from ' + client.getLabel()); + + }).on('tcpProxyError', function(data) { + emitErrorLog('Client IP detection failed, tcpProxyProtocol is enabled yet did not receive proxy protocol message, instead got data: ' + data); + + }).on('bootedBannedWorker', function(){ + emitWarningLog('Booted worker ' + client.getLabel() + ' who was connected from an IP address that was just banned'); + + }).on('triggerBan', function(reason){ + emitWarningLog('Banned triggered for ' + client.getLabel() + ': ' + reason); + _this.emit('banIP', client.remoteAddress, client.workerName); + }); + }); + } + + + + function SetupBlockPolling(){ + if (typeof options.blockRefreshInterval !== "number" || options.blockRefreshInterval <= 0){ + emitLog('Block template polling has been disabled'); + return; + } + + var pollingInterval = options.blockRefreshInterval; + + blockPollingIntervalId = setInterval(function () { + GetBlockTemplate(function(error, result, foundNewBlock){ + if (foundNewBlock) + emitLog('Block notification via RPC polling'); + }); + }, pollingInterval); + } + + + + function GetBlockTemplate(callback){ + _this.daemon.cmd('getblocktemplate', + [{"capabilities": [ "coinbasetxn", "workid", "coinbase/append" ], "rules": [ "segwit" ]}], + function(result){ + if (result.error){ + console.log("GetBlockTemplate()", result); + emitErrorLog('getblocktemplate call failed for daemon instance ' + + result.instance.index + ' with error ' + JSON.stringify(result.error)); + callback(result.error); + } else { + var processedNewBlock = _this.jobManager.processTemplate(result.response); + callback(null, result.response, processedNewBlock); + callback = function(){}; + } + }, true + ); + } + + + + function CheckBlockAccepted(blockHash, callback){ + //setTimeout(function(){ + _this.daemon.cmd('getblock', + [blockHash], + function(results){ + var validResults = results.filter(function(result){ + return result.response && (result.response.hash === blockHash) + }); + + if (validResults.length >= 1){ + callback(true, validResults[0].response.tx[0]); + } + else{ + callback(false); + } + } + ); + //}, 500); + } + + + + /** + * This method is being called from the blockNotify so that when a new block is discovered by the daemon + * We can inform our miners about the newly found block + **/ + this.processBlockNotify = function(blockHash, sourceTrigger) { + emitLog('Block notification via ' + sourceTrigger); + if (typeof(_this.jobManager.currentJob) !== 'undefined' && blockHash !== _this.jobManager.currentJob.rpcData.previousblockhash){ + GetBlockTemplate(function(error, result){ + if (error) + emitErrorLog('Block notify error getting block template for ' + options.coin.name); + }) + } + }; + + + this.relinquishMiners = function(filterFn, resultCback) { + var origStratumClients = this.stratumServer.getStratumClients(); + + var stratumClients = []; + Object.keys(origStratumClients).forEach(function (subId) { + stratumClients.push({subId: subId, client: origStratumClients[subId]}); + }); + //TODO: Think of a way to use API 8's async/await and promises to replace async lib + async.filter( + stratumClients, + filterFn, + function (clientsToRelinquish) { + clientsToRelinquish.forEach(function(cObj) { + cObj.client.removeAllListeners(); + _this.stratumServer.removeStratumClientBySubId(cObj.subId); + }); + + process.nextTick(function () { + resultCback( + clientsToRelinquish.map( + function (item) { + return item.client; + } + ) + ); + }); + } + ); + + }; + + + this.attachMiners = function(miners) { + miners.forEach(function (clientObj) { + _this.stratumServer.manuallyAddStratumClient(clientObj); + }); + _this.stratumServer.broadcastMiningJobs(_this.jobManager.currentJob.getJobParams()); + + }; + + + this.getStratumServer = function() { + return _this.stratumServer; + }; + + + this.setVarDiff = function(port, varDiffConfig) { + if (typeof(_this.varDiff[port]) != 'undefined' ) { + _this.varDiff[port].removeAllListeners(); + } + var varDiffInstance = new varDiff(port, varDiffConfig); + _this.varDiff[port] = varDiffInstance; + _this.varDiff[port].on('newDifficulty', function(client, newDiff) { + + /* We request to set the newDiff @ the next difficulty retarget + (which should happen when a new job comes in - AKA BLOCK) */ + client.enqueueNextDifficulty(newDiff); + + /*if (options.varDiff.mode === 'fast'){ + //Send new difficulty, then force miner to use new diff by resending the + //current job parameters but with the "clean jobs" flag set to false + //so the miner doesn't restart work and submit duplicate shares + client.sendDifficulty(newDiff); + var job = _this.jobManager.currentJob.getJobParams(); + job[8] = false; + client.sendMiningJob(job); + }*/ + + }); + }; + +}; +pool.prototype.__proto__ = events.EventEmitter.prototype; diff --git a/lib/stratum.js b/lib/stratum.js index 9377e5eab..0dac6663c 100644 --- a/lib/stratum.js +++ b/lib/stratum.js @@ -177,8 +177,8 @@ var StratumClient = function(options){ name : message.params[0], jobId : message.params[1], extraNonce2 : message.params[2], - nTime : message.params[3], - nonce : message.params[4] + nTime : message.params[3].toLowerCase(), + nonce : message.params[4].toLowerCase() }, function(error, result){ if (!considerBan(result)){ diff --git a/lib/transactions.js b/lib/transactions.js index dcd9daa53..483a318e5 100644 --- a/lib/transactions.js +++ b/lib/transactions.js @@ -15,9 +15,48 @@ var generateOutputTransactions = function(poolRecipient, recipients, rpcData){ var txOutputBuffers = []; + /* Dash 12.1 */ + if (rpcData.masternode && rpcData.superblock) { + if (rpcData.masternode.payee) { + var payeeReward = 0; + + payeeReward = rpcData.masternode.amount; + reward -= payeeReward; + rewardToPool -= payeeReward; + + var payeeScript = util.addressToScript(rpcData.masternode.payee); + txOutputBuffers.push(Buffer.concat([ + util.packInt64LE(payeeReward), + util.varIntBuffer(payeeScript.length), + payeeScript + ])); + } else if (rpcData.superblock.length > 0) { + for(var i in rpcData.superblock){ + var payeeReward = 0; + + payeeReward = rpcData.superblock[i].amount; + reward -= payeeReward; + rewardToPool -= payeeReward; + + var payeeScript = util.addressToScript(rpcData.superblock[i].payee); + txOutputBuffers.push(Buffer.concat([ + util.packInt64LE(payeeReward), + util.varIntBuffer(payeeScript.length), + payeeScript + ])); + } + } + } if (rpcData.payee) { - var payeeReward = Math.ceil(reward / 5); + var payeeReward = 0; + + if (rpcData.payee_amount) { + payeeReward = rpcData.payee_amount; + } else { + payeeReward = Math.ceil(reward / 5); + } + reward -= payeeReward; rewardToPool -= payeeReward; @@ -67,7 +106,7 @@ var generateOutputTransactions = function(poolRecipient, recipients, rpcData){ }; -exports.CreateGeneration = function(rpcData, publicKey, extraNoncePlaceholder, reward, txMessages, recipients){ +exports.CreateGeneration = function(rpcData, publicKey, extraNoncePlaceholder, reward, txMessages, recipients, coinbase){ var txInputsCount = 1; var txOutputsCount = 1; @@ -94,8 +133,8 @@ exports.CreateGeneration = function(rpcData, publicKey, extraNoncePlaceholder, r util.serializeNumber(Date.now() / 1000 | 0), Buffer.from([extraNoncePlaceholder.length]) ]); - - var scriptSigPart2 = util.serializeString('/nodeStratum/'); + if(!coinbase) coinbase = 'nodeStartum'; + var scriptSigPart2 = util.serializeString('/' + coinbase + '/'); var p1 = Buffer.concat([ util.packUInt32LE(txVersion), diff --git a/package.json b/package.json index d1e3f589e..9fded40cb 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "async": "^2.5.0", "base58-native": "^0.1.4", "bignum": "^0.12.5", - "multi-hashing": "git://github.com/krisklosterman/node-multi-hashing.git" + "multi-hashing": "git://github.com/swordfish6975/node-multi-hashing.git" }, "engines": { "node": ">=0.10"