diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d13cc4b --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/config.json b/config.json new file mode 100644 index 0000000..3cc7378 --- /dev/null +++ b/config.json @@ -0,0 +1,25 @@ +{ + "loglevel": -1, + "miners": [ + { + "cryptoname": "hns", + "minername": ["Goldshell-HS1", "Goldshell-HS1-Plus"], + "pool": { + "host": "hns.ss.dxpool.com", + "port": 3008, + "user": "peterfu.bb", + "pass": "x" + } + }, + { + "cryptoname": "lbc", + "minername": ["Goldshell-LB1"], + "pool": { + "host": "lbc.viabtc.com", + "port": 3002, + "user": "sfgaqw.1", + "pass": "x" + } + } + ] +} diff --git a/dashboard.js b/dashboard.js new file mode 100644 index 0000000..a66800f --- /dev/null +++ b/dashboard.js @@ -0,0 +1,84 @@ +var blessed = require('blessed') +, contrib = require('blessed-contrib') +var version = require('./package.json').version +var platform = require('os').platform() +class CliDraw { + constructor() { + this.screen = null + this.grid = null + this.totalTable = null + this.detailsTable = null + + this.initBlessed() + } + + initBlessed() { + this.screen = blessed.screen() + this.grid = new contrib.grid({rows: 12, cols: 12, screen: this.screen}) + + this.screen.key(['escape', 'q', 'C-c', 'C-z'], function(ch, key) { + this.screen.destroy(); + return process.exit(0); + }); + + this.initSurface() + } + + initSurface() { + this.totalTable = this.grid.set(0, 0, 12, 2, contrib.table, { + keys: true, + fg: 'white', + selectedFg: 'white', + selectedBg: 'blue', + interactive: false, + label: 'Goldshell Miner Total', + width: '100%', + height: '100%', + border: { + type: "line", + fg: "cyan" + }, + columnSpacing: 2, //in chars + columnWidth: [18, 10] /*in chars*/ + }) + var localcolumnWidth = null; + if (platform === 'darwin') { + localcolumnWidth = [8, 18, 8, 8, 8, 36, 14, 14, 10, 10, 10, 16, 12] + } else if (platform === 'win32') { + localcolumnWidth = [8, 18, 8, 8, 8, 8, 14, 14, 10, 10, 10, 16, 12] + } else { + localcolumnWidth = [8, 18, 8, 8, 8, 12, 14, 14, 10, 10, 10, 16, 12] + } + this.detailsTable = this.grid.set(0, 2, 12, 10, contrib.table, { + keys: true, + fg: 'white', + selectedFg: 'white', + selectedBg: 'blue', + interactive: true, + label: 'Goldshell Miner Details' + ' (' + version + ')', + width: '100%', + height: '100%', + border: { + type: "line", + fg: "cyan" + }, + columnSpacing: 2, //in chars + columnWidth: localcolumnWidth + }) + + this.detailsTable.focus() + this.screen.render() + } + + updateTotalTable(headers, stats) { + this.totalTable.setData({ headers: headers, data: stats}) + this.screen.render() + } + + updateDetailsTable(headers, stats) { + this.detailsTable.setData({ headers: headers, data: stats}) + this.screen.render() + } +} + +module.exports = CliDraw; diff --git a/entrance.js b/entrance.js new file mode 100644 index 0000000..8e5dd35 --- /dev/null +++ b/entrance.js @@ -0,0 +1,250 @@ +const IntMiner = require('./src'); +const CFonts = require('cfonts'); +const Debug = require('./src/log')(); +const argv = require('minimist')(process.argv.slice(2)); +const fs = require('fs'); +const CliDraw = require("./dashboard"); + +const supports = [ + { + algoname: 'blake2bsha3', + minername: ['Goldshell-HS1', 'Goldshell-HS1-Plus'], + cryptoname: 'hns', + protocolname: 'stratum', + pool: null + }, + { + algoname: 'lbry', + minername: ['Goldshell-LB1'], + cryptoname: 'lbc', + protocolname: 'stratum', + pool: null + } +] + +function help() { + const text = require('fs').createReadStream(`${__dirname}/help`); + text.pipe(process.stderr); + text.on('close', () => process.exit(1)); +} + +function isSubSet(Arr_A, Arr_B) { + if(!(Arr_A instanceof Array)||!(Arr_B instanceof Array)||((Arr_A.length > Arr_B.length))) { + return false; + } + return Arr_A.every(function(A) { + return Arr_B.includes(A); + }) +} + +function isJson(obj){ + var isjson = typeof(obj) == "object" && Object.prototype.toString.call(obj).toLowerCase() == "[object object]" && !obj.length; + return isjson; +} + +function existParameter(obj, name) { + if (isJson(obj) && name && obj.hasOwnProperty(name)) { + return true; + } + return false; +} + +function getMinerParameters(config) { + var Miners = []; + for (var i = 0; i < supports.length; i++) { + for (var j = 0; j < config.length; j++) { + if (supports[i].cryptoname === config[j].cryptoname && isSubSet(config[j].minername, supports[i].minername)) { + Miners.push(supports[i]); + } + } + } + return Miners; +} + +const shows = [ + { h: "miningName", v: " Model"}, + { h: "state", v: "Status"}, + { h: "miningType", v: "Type"}, + { h: "version", v: "Version"}, + // { h: "miningSN", v: " MinerID"}, + { h: "comm", v: " Port"}, + { h: "hashrate", v: "Hashrate 20s"}, + { h: "avHashrate", v: "Hashrate avg"}, + { h: "hardwareErr", v: "HW Error" }, + { h: "rejected", v: "Rejected"}, + { h: "accepted", v: "Accepted"}, + { h: "temperatue", v: "Temperature ℃"}, + { h: "elapsed", v: " Elapsed"} +]; + +const sumHeader = [ + " Model", + "Count" +]; + +function convertHeaders() { + var showHeads = []; + + for (var i = 0; i < shows.length; i++) { + if (i === 0) + showHeads.push("Index"); + showHeads.push(shows[i].v); + } + return showHeads; +} + +function convertContent(miners) { + var showMinerParameters = []; + + for (var j = 0; j < miners.length; j++) { + var showMinerParameter = []; + for (var i = 0; i < shows.length; i++) { + if (i === 0) + showMinerParameter.push(j); + showMinerParameter.push(miners[j][shows[i].h]); + } + showMinerParameters.push(showMinerParameter); + } + return showMinerParameters; +} + +function sumup(miners) { + var sums = []; + var active = 0; + + sums.push(["Active Sum", 0]); + sums.push(["Inactive Sum", 0]); + sums.push(['', '']) + for (var j = 0; j < miners.length; j++) { + if (miners[j].state === 'on') + active++; + + for (var i = 0; i < sums.length; i++) { + if (miners[j].miningName === sums[i][0]) { + sums[i][1]++; + break; + } + } + + if (i === sums.length) { + sums.push([miners[j].miningName, 1]); + } + } + + if (miners.length) { + sums[0][1] = active; + sums[1][1] = miners.length - active; + } + return sums; +} + +(async () => { + var devState = [] + var MinerParameters = null; + var debugScreen = false; + var configFile = null; + var loglevel = 1; + var config = argv.config || './config.json'; + var dashboard = null; + var headers = null; + + if (argv.help || argv.h) { + help(); + return; + } + + if (fs.existsSync(config)) { + try { + configFile = JSON.parse(fs.readFileSync(config, 'utf-8')); + } catch(e) { + help(); + return; + } + } else { + console.log('Config file is not exist:', config); + return; + } + + loglevel = argv.loglevel ? argv.loglevel : (existParameter(configFile, 'loglevel') ? configFile.loglevel : 1); + if (loglevel !== 1) { + Debug.IbctSetLogLevel(loglevel); + if (loglevel < 0) + debugScreen = true; + } + + for (i = 0; i < configFile.miners.length; i++) { + if (!existParameter(configFile.miners[i], 'cryptoname')) { + console.log('Parameter cryptoname is not exist in config.json'); + help(); + return null + } + if (!existParameter(configFile.miners[i], 'minername')) { + console.log('Parameter minername is not exist in config.json'); + help(); + return null + } + if (!existParameter(configFile.miners[i], 'pool')) { + console.log('Parameter pool is not exist in config.json'); + help(); + return null + } + } + MinerParameters = getMinerParameters(configFile.miners); + if (!MinerParameters.length) { + help(); + return null + } + + if (debugScreen === true) { + dashboard = new CliDraw(); + headers = convertHeaders(); + } + + const miner = await IntMiner({MinerParameters: MinerParameters}); + + // init Mining + await miner.initMining(); + // set pool + for (i = 0; i < configFile.miners.length; i++) { + miner.setMiningConfig('pool', configFile.miners[i].cryptoname, configFile.miners[i].pool); + } + // start Mining + await miner.connectMining(); + await miner.startMining(null); + + miner.on('plug-in', async (data) => { + Debug.IbctLogDbg('plug-in: ', data.devID); + await miner.connectMining({'devID': data.devID}); + miner.startMining({ + 'devID': data.devID + }); + }); + + miner.on('plug-out', data => { + Debug.IbctLogDbg('plug-out: ', data.devID); + // miner.stopMining({ 'devId': data.devID }); + }); + + miner.on("error", function (devID, data) { + if (devID) + Debug.IbctLogErr('Miner' + devID + ':', data); + else + Debug.IbctLogErr(data); + }); + + miner.on("warning", function (devID, data) { + if (devID) + Debug.IbctLogDbg('Miner' + devID + ':', data); + else + Debug.IbctLogDbg(data); + }); + + setInterval(function () { + devState = miner.getMiningStatus(); + if (debugScreen === true) { + dashboard.updateTotalTable(sumHeader, sumup(devState)) + dashboard.updateDetailsTable(headers, convertContent(devState)) + } else + console.log(JSON.stringify(devState)); + }, 1000); +})(); diff --git a/help b/help new file mode 100644 index 0000000..1fbd0a6 --- /dev/null +++ b/help @@ -0,0 +1,25 @@ +Usage: + +goldshellminer-win || goldshellminer-linux || goldshellminer-macos + +Exe cmd: + + goldshellminer-* --config [YOUR-CONFIG-FILE] + Options: + --config Config file + --loglevel -1 (dashboard mode), 0 ~ 3 (debug mode: debug level from err to info, 3 show all log) + --help + +Json settings: + + loglevel: -1 (dashboard mode), 0 ~ 3 (debug mode: debug level from err to info, 3 show all log) + miners: + cryptoname: support hns + minername: support Goldshell-HS1, Goldshell-HS1-Plus + pool: (pool settings, like this) + { + host: `hns.ss.dxpool.com`, + port: 3008, + user: `user.worker`, + pass: 'x' + } diff --git a/package.json b/package.json new file mode 100644 index 0000000..5250f45 --- /dev/null +++ b/package.json @@ -0,0 +1,103 @@ +{ + "_from": "goldshellminer", + "_id": "goldshellminer@2.2.4", + "_inBundle": false, + "_integrity": "sha512-ezVtXyC8NONvXKiGMRYYbLXl1ZfuJLs2n2q4XGeLYVnRGYB4n8TlplM/Y8JHsDbdodGv8POxfz3PvrdYuctkmw==", + "_location": "/goldshellminer", + "_phantomChildren": {}, + "_requested": { + "type": "tag", + "registry": true, + "raw": "goldshellminer", + "name": "goldshellminer", + "escapedName": "goldshellminer", + "rawSpec": "", + "saveSpec": null, + "fetchSpec": "latest" + }, + "_requiredBy": [ + "#USER", + "/" + ], + "bin": "entrance.js", + "pkg": { + "assets": [ + "./help", + "node_modules/blessed/**/*" + ] + }, + "scripts": { + "build": "pkg . --out-path ./bin", + "dashboard": "node entrance.js" + }, + "bundleDependencies": false, + "dependencies": { + "@types/node": "^8.0.53", + "@types/ws": "^3.2.0", + "axios": "^0.16.1", + "basic-auth": "^2.0.0", + "bindings": "^1.3.0", + "blessed": "^0.1.81", + "blessed-contrib": "^4.8.21", + "blessed-xterm": "^1.4.1", + "cfonts": "^2.9.1", + "chacha20": "^0.1.4", + "chalk": "^4.1.0", + "crc32": "^0.2.2", + "elegant-spinner": "^1.0.1", + "express": "^4.15.4", + "i18n": "^0.10.0", + "ibctminerscrypt": "^1.0.12", + "locks": "^0.2.2", + "log-update": "^2.1.0", + "minimist": "^1.2.0", + "moment": "^2.19.1", + "nan": "^2.13.2", + "net-ping": "^1.2.3", + "node-getopt": "^0.3.2", + "pmx": "^1.5.5", + "prebuild-install": "^5.1.0", + "serialport": "^7.1.4", + "sha1": "^1.1.1", + "stratum": "^0.2.4", + "tty-table": "^2.5.5", + "typescript": "^2.6.1", + "usb-detection": "^4.3.0", + "uuid": "^3.1.0", + "wait-until": "0.0.2", + "watch": "^1.0.2", + "word-table": "^1.0.3", + "ws": "^3.2.0" + }, + "deprecated": false, + "devDependencies": { + "expect": "^21.1.0", + "mocha": "^3.5.3", + "node-gyp": "^3.8.0", + "prebuild": "^8.0.1" + }, + "engines": { + "node": ">=8.0", + "npm": ">=5.0" + }, + "keywords": [ + "monero miner", + "xmr miner", + "monero", + "electroneum", + "xmr", + "etn", + "crypto", + "cryptocurrency", + "cryptocurrencies", + "mining", + "miner", + "bitcoin", + "stratum", + "pool" + ], + "license": "MIT", + "main": "src/index.js", + "name": "goldshellminer", + "version": "2.2.5" +} \ No newline at end of file diff --git a/src/algo/algo.js b/src/algo/algo.js new file mode 100644 index 0000000..62b9c6a --- /dev/null +++ b/src/algo/algo.js @@ -0,0 +1,52 @@ +const blake2bsha3Api = require('./blake2bsha3Api'); +const lbryApi = require('./lbryApi'); +const EventEmitter = require('events'); +const Debug = require('../log')(); +const COMP = '[algo]'; + +const algorithms = [ + { + name: 'lbry', + api: lbryApi + }, + { + name: 'blake2bsha3', + api: blake2bsha3Api + } +]; + +class algoApi extends EventEmitter { + constructor({ + name + }) { + super(); + var _this = this; + + _this.name = name; + _this.algorithm = this.GetAlgorithmApi(_this.name); + if (!_this.algorithm) { + _this.emit('error', __('不支持此种算法:'), _this.name); + } + + return _this; + } + GetAlgorithmApi(name) { + var algo = null; + algorithms.forEach(function (algorithm, index) { + if (algorithm.name === name) { + algo = algorithm; + } + }) + return algo ? algo.api() : null; + } + getAlgoName() { + return this.algorithm ? this.algorithm.getAlgoName() : null; + } + genHash(data, length, varity) { + return this.algorithm ? this.algorithm.genHash(data, length, varity) : null; + } +}; + +module.exports = function RunAlgoApi(options = {}) { + return new algoApi(options); +}; diff --git a/src/algo/base/assert.js b/src/algo/base/assert.js new file mode 100644 index 0000000..0e57fb8 --- /dev/null +++ b/src/algo/base/assert.js @@ -0,0 +1,28 @@ +/*! + * assert.js - assert for bcrypto + * Copyright (c) 2020, Christopher Jeffrey (MIT License). + * https://github.com/bcoin-org/bcrypto + */ + +'use strict'; + +/* + * Assert + */ + +function assert(val, msg) { + if (!val) { + const err = new Error(msg || 'Assertion failed'); + + if (Error.captureStackTrace) + Error.captureStackTrace(err, assert); + + throw err; + } +} + +/* + * Expose + */ + +module.exports = assert; diff --git a/src/algo/base/blake2b.js b/src/algo/base/blake2b.js new file mode 100644 index 0000000..ff1d6a8 --- /dev/null +++ b/src/algo/base/blake2b.js @@ -0,0 +1,356 @@ +/*! + * blake2b.js - BLAKE2b implementation for bcrypto + * Copyright (c) 2017-2019, Christopher Jeffrey (MIT License). + * https://github.com/bcoin-org/bcrypto + * + * Parts of this software are based on dcposch/blakejs: + * Daniel Clemens Posch (CC0) + * https://github.com/dcposch/blakejs/blob/master/blake2b.js + * + * Resources: + * https://en.wikipedia.org/wiki/BLAKE_(hash_function) + * https://tools.ietf.org/html/rfc7693 + */ + +'use strict'; + +const assert = require('./assert'); +const HMAC = require('./hmac'); + +/* + * Constants + */ + +const FINALIZED = 0x80000000; + +const IV = new Uint32Array([ + 0xf3bcc908, 0x6a09e667, 0x84caa73b, 0xbb67ae85, + 0xfe94f82b, 0x3c6ef372, 0x5f1d36f1, 0xa54ff53a, + 0xade682d1, 0x510e527f, 0x2b3e6c1f, 0x9b05688c, + 0xfb41bd6b, 0x1f83d9ab, 0x137e2179, 0x5be0cd19 +]); + +const SIGMA = new Uint8Array([ + 0x00, 0x02, 0x04, 0x06, 0x08, 0x0a, 0x0c, 0x0e, + 0x10, 0x12, 0x14, 0x16, 0x18, 0x1a, 0x1c, 0x1e, + 0x1c, 0x14, 0x08, 0x10, 0x12, 0x1e, 0x1a, 0x0c, + 0x02, 0x18, 0x00, 0x04, 0x16, 0x0e, 0x0a, 0x06, + 0x16, 0x10, 0x18, 0x00, 0x0a, 0x04, 0x1e, 0x1a, + 0x14, 0x1c, 0x06, 0x0c, 0x0e, 0x02, 0x12, 0x08, + 0x0e, 0x12, 0x06, 0x02, 0x1a, 0x18, 0x16, 0x1c, + 0x04, 0x0c, 0x0a, 0x14, 0x08, 0x00, 0x1e, 0x10, + 0x12, 0x00, 0x0a, 0x0e, 0x04, 0x08, 0x14, 0x1e, + 0x1c, 0x02, 0x16, 0x18, 0x0c, 0x10, 0x06, 0x1a, + 0x04, 0x18, 0x0c, 0x14, 0x00, 0x16, 0x10, 0x06, + 0x08, 0x1a, 0x0e, 0x0a, 0x1e, 0x1c, 0x02, 0x12, + 0x18, 0x0a, 0x02, 0x1e, 0x1c, 0x1a, 0x08, 0x14, + 0x00, 0x0e, 0x0c, 0x06, 0x12, 0x04, 0x10, 0x16, + 0x1a, 0x16, 0x0e, 0x1c, 0x18, 0x02, 0x06, 0x12, + 0x0a, 0x00, 0x1e, 0x08, 0x10, 0x0c, 0x04, 0x14, + 0x0c, 0x1e, 0x1c, 0x12, 0x16, 0x06, 0x00, 0x10, + 0x18, 0x04, 0x1a, 0x0e, 0x02, 0x08, 0x14, 0x0a, + 0x14, 0x04, 0x10, 0x08, 0x0e, 0x0c, 0x02, 0x0a, + 0x1e, 0x16, 0x12, 0x1c, 0x06, 0x18, 0x1a, 0x00, + 0x00, 0x02, 0x04, 0x06, 0x08, 0x0a, 0x0c, 0x0e, + 0x10, 0x12, 0x14, 0x16, 0x18, 0x1a, 0x1c, 0x1e, + 0x1c, 0x14, 0x08, 0x10, 0x12, 0x1e, 0x1a, 0x0c, + 0x02, 0x18, 0x00, 0x04, 0x16, 0x0e, 0x0a, 0x06 +]); + +/** + * BLAKE2b + */ + +class BLAKE2b { + constructor() { + this.state = new Uint32Array(16); + this.V = new Uint32Array(32); + this.M = new Uint32Array(32); + this.block = Buffer.allocUnsafe(128); + this.size = 32; + this.count = 0; + this.pos = FINALIZED; + } + + init(size, key) { + if (size == null) + size = 32; + + assert((size >>> 0) === size); + assert(key == null || Buffer.isBuffer(key)); + + if (size === 0 || size > 64) + throw new Error('Bad output length.'); + + if (key && key.length > 64) + throw new Error('Bad key length.'); + + const klen = key ? key.length : 0; + + for (let i = 0; i < 16; i++) + this.state[i] = IV[i]; + + this.size = size; + this.count = 0; + this.pos = 0; + + this.state[0] ^= 0x01010000 ^ (klen << 8) ^ this.size; + + if (klen > 0) { + const block = Buffer.alloc(128, 0x00); + + key.copy(block, 0); + + this.update(block); + } + + return this; + } + + update(data) { + //assert(Buffer.isBuffer(data)); + //assert(!(this.pos & FINALIZED), 'Context is not initialized.'); + + let off = 0; + let len = data.length; + + if (len > 0) { + const left = this.pos; + const fill = 128 - left; + + if (len > fill) { + this.pos = 0; + + data.copy(this.block, left, off, off + fill); + + this.count += 128; + this._compress(this.block, 0, false); + + off += fill; + len -= fill; + + while (len > 128) { + this.count += 128; + this._compress(data, off, false); + off += 128; + len -= 128; + } + } + + data.copy(this.block, this.pos, off, off + len); + + this.pos += len; + } + + return this; + } + + final() { + //assert(!(this.pos & FINALIZED), 'Context is not initialized.'); + + this.count += this.pos; + this.block.fill(0, this.pos, 128); + this._compress(this.block, 0, true); + this.pos = FINALIZED; + + const out = Buffer.allocUnsafe(this.size); + + for (let i = 0; i < this.size; i++) + out[i] = this.state[i >>> 2] >>> (8 * (i & 3)); + + for (let i = 0; i < 16; i++) + this.state[i] = 0; + + for (let i = 0; i < 32; i++) { + this.V[i] = 0; + this.M[i] = 0; + } + + for (let i = 0; i < 128; i++) + this.block[i] = 0; + + return out; + } + + _compress(block, off, last) { + const { V, M } = this; + + for (let i = 0; i < 16; i++) { + V[i] = this.state[i]; + V[i + 16] = IV[i]; + } + + // uint128 + V[24] ^= this.count; + V[25] ^= this.count * (1 / 0x100000000); + V[26] ^= 0; + V[27] ^= 0; + + if (last) { + // last block + V[28] ^= -1; + V[29] ^= -1; + + // last node + V[29] ^= 0; + V[30] ^= 0; + } + + for (let i = 0; i < 32; i++) { + M[i] = readU32(block, off); + off += 4; + } + + for (let i = 0; i < 12; i++) { + G(V, M, 0, 8, 16, 24, SIGMA[i * 16 + 0], SIGMA[i * 16 + 1]); + G(V, M, 2, 10, 18, 26, SIGMA[i * 16 + 2], SIGMA[i * 16 + 3]); + G(V, M, 4, 12, 20, 28, SIGMA[i * 16 + 4], SIGMA[i * 16 + 5]); + G(V, M, 6, 14, 22, 30, SIGMA[i * 16 + 6], SIGMA[i * 16 + 7]); + G(V, M, 0, 10, 20, 30, SIGMA[i * 16 + 8], SIGMA[i * 16 + 9]); + G(V, M, 2, 12, 22, 24, SIGMA[i * 16 + 10], SIGMA[i * 16 + 11]); + G(V, M, 4, 14, 16, 26, SIGMA[i * 16 + 12], SIGMA[i * 16 + 13]); + G(V, M, 6, 8, 18, 28, SIGMA[i * 16 + 14], SIGMA[i * 16 + 15]); + } + + for (let i = 0; i < 16; i++) + this.state[i] ^= V[i] ^ V[i + 16]; + } + + static hash() { + return new BLAKE2b(); + } + + static hmac(size) { + return new HMAC(BLAKE2b, 128, [size]); + } + + static digest(data, size, key) { + const { ctx } = BLAKE2b; + + ctx.init(size, key); + ctx.update(data); + + return ctx.final(); + } + + static root(left, right, size, key) { + if (size == null) + size = 32; + + //assert(Buffer.isBuffer(left) && left.length === size); + //assert(Buffer.isBuffer(right) && right.length === size); + + const { ctx } = BLAKE2b; + + ctx.init(size, key); + ctx.update(left); + ctx.update(right); + + return ctx.final(); + } + + static multi(x, y, z, size, key) { + const { ctx } = BLAKE2b; + + ctx.init(size, key); + ctx.update(x); + ctx.update(y); + + if (z) + ctx.update(z); + + return ctx.final(); + } + + static mac(data, key, size) { + return BLAKE2b.hmac(size).init(key).update(data).final(); + } +} + +/* + * Static + */ + +BLAKE2b.native = 0; +BLAKE2b.id = 'BLAKE2B256'; +BLAKE2b.size = 32; +BLAKE2b.bits = 256; +BLAKE2b.blockSize = 128; +BLAKE2b.zero = Buffer.alloc(32, 0x00); +BLAKE2b.ctx = new BLAKE2b(); + +/* + * Helpers + */ + +function sum64i(v, a, b) { + const o0 = v[a + 0] + v[b + 0]; + const o1 = v[a + 1] + v[b + 1]; + const c = (o0 >= 0x100000000) | 0; + + v[a + 0] = o0; + v[a + 1] = o1 + c; +} + +function sum64w(v, a, b0, b1) { + const o0 = v[a + 0] + b0; + const o1 = v[a + 1] + b1; + const c = (o0 >= 0x100000000) | 0; + + v[a + 0] = o0; + v[a + 1] = o1 + c; +} + +function G(v, m, a, b, c, d, ix, iy) { + const x0 = m[ix + 0]; + const x1 = m[ix + 1]; + const y0 = m[iy + 0]; + const y1 = m[iy + 1]; + + sum64i(v, a, b); + sum64w(v, a, x0, x1); + + const xor0 = v[d + 0] ^ v[a + 0]; + const xor1 = v[d + 1] ^ v[a + 1]; + + v[d + 0] = xor1; + v[d + 1] = xor0; + + sum64i(v, c, d); + + const xor2 = v[b + 0] ^ v[c + 0]; + const xor3 = v[b + 1] ^ v[c + 1]; + + v[b + 0] = (xor2 >>> 24) ^ (xor3 << 8); + v[b + 1] = (xor3 >>> 24) ^ (xor2 << 8); + + sum64i(v, a, b); + sum64w(v, a, y0, y1); + + const xor4 = v[d + 0] ^ v[a + 0]; + const xor5 = v[d + 1] ^ v[a + 1]; + + v[d + 0] = (xor4 >>> 16) ^ (xor5 << 16); + v[d + 1] = (xor5 >>> 16) ^ (xor4 << 16); + + sum64i(v, c, d); + + const xor6 = v[b + 0] ^ v[c + 0]; + const xor7 = v[b + 1] ^ v[c + 1]; + + v[b + 0] = (xor7 >>> 31) ^ (xor6 << 1); + v[b + 1] = (xor6 >>> 31) ^ (xor7 << 1); +} + +function readU32(data, off) { + return (data[off++] + + data[off++] * 0x100 + + data[off++] * 0x10000 + + data[off] * 0x1000000); +} + +/* + * Expose + */ + +module.exports = BLAKE2b; diff --git a/src/algo/base/hmac.js b/src/algo/base/hmac.js new file mode 100644 index 0000000..8f32703 --- /dev/null +++ b/src/algo/base/hmac.js @@ -0,0 +1,118 @@ +/*! + * hmac.js - hmac for bcrypto + * Copyright (c) 2016-2019, Christopher Jeffrey (MIT License). + * https://github.com/bcoin-org/bcrypto + * + * Parts of this software are based on indutny/hash.js: + * Copyright (c) 2014, Fedor Indutny (MIT License). + * https://github.com/indutny/hash.js + * + * Resources: + * https://en.wikipedia.org/wiki/HMAC + * https://tools.ietf.org/html/rfc2104 + * https://github.com/indutny/hash.js/blob/master/lib/hash/hmac.js + */ + +'use strict'; + +const assert = require('./assert'); + +/** + * HMAC + */ + +class HMAC { + /** + * Create an HMAC. + * @param {Function} Hash + * @param {Number} size + * @param {Array} [x=[]] + * @param {Array} [y=[]] + */ + + constructor(Hash, size, x = [], y = []) { + assert(typeof Hash === 'function'); + assert((size >>> 0) === size); + assert(Array.isArray(x)); + assert(Array.isArray(y)); + + this.hash = Hash; + this.size = size; + this.x = x; + this.y = y; + + this.inner = new Hash(); + this.outer = new Hash(); + } + + /** + * Initialize HMAC context. + * @param {Buffer} data + */ + + init(key) { + assert(Buffer.isBuffer(key)); + + // Shorten key + if (key.length > this.size) { + const Hash = this.hash; + const h = new Hash(); + + h.init(...this.x); + h.update(key); + + key = h.final(...this.y); + + assert(key.length <= this.size); + } + + // Pad key + const pad = Buffer.allocUnsafe(this.size); + + for (let i = 0; i < key.length; i++) + pad[i] = key[i] ^ 0x36; + + for (let i = key.length; i < pad.length; i++) + pad[i] = 0x36; + + this.inner.init(...this.x); + this.inner.update(pad); + + for (let i = 0; i < key.length; i++) + pad[i] = key[i] ^ 0x5c; + + for (let i = key.length; i < pad.length; i++) + pad[i] = 0x5c; + + this.outer.init(...this.x); + this.outer.update(pad); + + return this; + } + + /** + * Update HMAC context. + * @param {Buffer} data + */ + + update(data) { + this.inner.update(data); + return this; + } + + /** + * Finalize HMAC context. + * @returns {Buffer} + */ + + final() { + this.outer.update(this.inner.final(...this.y)); + return this.outer.final(...this.y); + } +} + +/* + * Expose + */ + +module.exports = HMAC; diff --git a/src/algo/base/keccak.js b/src/algo/base/keccak.js new file mode 100644 index 0000000..7c3929f --- /dev/null +++ b/src/algo/base/keccak.js @@ -0,0 +1,427 @@ +/*! + * keccak.js - Keccak/SHA3 implementation for bcrypto + * Copyright (c) 2017-2019, Christopher Jeffrey (MIT License). + * https://github.com/bcoin-org/bcrypto + * + * Parts of this software are based on emn178/js-sha3: + * Copyright (c) 2015-2017, Chen, Yi-Cyuan (MIT License). + * https://github.com/emn178/js-sha3 + * + * Parts of this software are based on rhash/RHash: + * Copyright (c) 2005-2014, Aleksey Kravchenko + * https://github.com/rhash/RHash + * + * Resources: + * https://en.wikipedia.org/wiki/SHA-3 + * https://keccak.team/specifications.html + * https://csrc.nist.gov/projects/hash-functions/sha-3-project/sha-3-standardization + * http://dx.doi.org/10.6028/NIST.FIPS.202 + * https://github.com/rhash/RHash/blob/master/librhash/sha3.c + * https://github.com/emn178/js-sha3/blob/master/src/sha3.js + */ + +'use strict'; + +const assert = require('./assert'); +const HMAC = require('./hmac'); + +/* + * Constants + */ + +const FINALIZED = 0x80000000; + +const ROUND_CONST = new Uint32Array([ + 0x00000001, 0x00000000, 0x00008082, 0x00000000, + 0x0000808a, 0x80000000, 0x80008000, 0x80000000, + 0x0000808b, 0x00000000, 0x80000001, 0x00000000, + 0x80008081, 0x80000000, 0x00008009, 0x80000000, + 0x0000008a, 0x00000000, 0x00000088, 0x00000000, + 0x80008009, 0x00000000, 0x8000000a, 0x00000000, + 0x8000808b, 0x00000000, 0x0000008b, 0x80000000, + 0x00008089, 0x80000000, 0x00008003, 0x80000000, + 0x00008002, 0x80000000, 0x00000080, 0x80000000, + 0x0000800a, 0x00000000, 0x8000000a, 0x80000000, + 0x80008081, 0x80000000, 0x00008080, 0x80000000, + 0x80000001, 0x00000000, 0x80008008, 0x80000000 +]); + +/** + * Keccak + */ + +class Keccak { + constructor() { + this.state = new Uint32Array(50); + this.block = Buffer.allocUnsafe(168); + this.bs = 136; + this.pos = FINALIZED; + } + + init(bits) { + if (bits == null) + bits = 256; + + assert((bits & 0xffff) === bits); + assert(bits >= 128); + assert(bits <= 512); + + const rate = 1600 - bits * 2; + + assert(rate >= 0 && (rate & 63) === 0); + + this.bs = rate / 8; + this.pos = 0; + + return this; + } + + update(data) { + assert(Buffer.isBuffer(data)); + assert(!(this.pos & FINALIZED), 'Context is not initialized.'); + + let len = data.length; + let pos = this.pos; + let off = 0; + + this.pos = (this.pos + len) % this.bs; + + if (pos > 0) { + let want = this.bs - pos; + + if (want > len) + want = len; + + data.copy(this.block, pos, off, off + want); + + pos += want; + len -= want; + off += want; + + if (pos < this.bs) + return this; + + this._transform(this.block, 0); + } + + while (len >= this.bs) { + this._transform(data, off); + off += this.bs; + len -= this.bs; + } + + if (len > 0) + data.copy(this.block, 0, off, off + len); + + return this; + } + + final(pad, len) { + if (pad == null) + pad = 0x01; + + if (len == null || len === 0) + len = 100 - this.bs / 2; + + assert((pad & 0xff) === pad); + assert((len >>> 0) === len); + assert(!(this.pos & FINALIZED), 'Context is not initialized.'); + + this.block.fill(0, this.pos, this.bs); + this.block[this.pos] |= pad; + this.block[this.bs - 1] |= 0x80; + this._transform(this.block, 0); + this.pos = FINALIZED; + + assert(len < this.bs); + + const out = Buffer.allocUnsafe(len); + + for (let i = 0; i < len; i++) + out[i] = this.state[i >>> 2] >>> (8 * (i & 3)); + + for (let i = 0; i < 50; i++) + this.state[i] = 0; + + for (let i = 0; i < this.bs; i++) + this.block[i] = 0; + + return out; + } + + _transform(block, off) { + const count = this.bs / 4; + const s = this.state; + + for (let i = 0; i < count; i++) + s[i] ^= readU32(block, off + i * 4); + + for (let n = 0; n < 48; n += 2) { + const c0 = s[0] ^ s[10] ^ s[20] ^ s[30] ^ s[40]; + const c1 = s[1] ^ s[11] ^ s[21] ^ s[31] ^ s[41]; + const c2 = s[2] ^ s[12] ^ s[22] ^ s[32] ^ s[42]; + const c3 = s[3] ^ s[13] ^ s[23] ^ s[33] ^ s[43]; + const c4 = s[4] ^ s[14] ^ s[24] ^ s[34] ^ s[44]; + const c5 = s[5] ^ s[15] ^ s[25] ^ s[35] ^ s[45]; + const c6 = s[6] ^ s[16] ^ s[26] ^ s[36] ^ s[46]; + const c7 = s[7] ^ s[17] ^ s[27] ^ s[37] ^ s[47]; + const c8 = s[8] ^ s[18] ^ s[28] ^ s[38] ^ s[48]; + const c9 = s[9] ^ s[19] ^ s[29] ^ s[39] ^ s[49]; + + const h0 = c8 ^ ((c2 << 1) | (c3 >>> 31)); + const l0 = c9 ^ ((c3 << 1) | (c2 >>> 31)); + const h1 = c0 ^ ((c4 << 1) | (c5 >>> 31)); + const l1 = c1 ^ ((c5 << 1) | (c4 >>> 31)); + const h2 = c2 ^ ((c6 << 1) | (c7 >>> 31)); + const l2 = c3 ^ ((c7 << 1) | (c6 >>> 31)); + const h3 = c4 ^ ((c8 << 1) | (c9 >>> 31)); + const l3 = c5 ^ ((c9 << 1) | (c8 >>> 31)); + const h4 = c6 ^ ((c0 << 1) | (c1 >>> 31)); + const l4 = c7 ^ ((c1 << 1) | (c0 >>> 31)); + + s[0] ^= h0; + s[1] ^= l0; + s[10] ^= h0; + s[11] ^= l0; + s[20] ^= h0; + s[21] ^= l0; + s[30] ^= h0; + s[31] ^= l0; + s[40] ^= h0; + s[41] ^= l0; + + s[2] ^= h1; + s[3] ^= l1; + s[12] ^= h1; + s[13] ^= l1; + s[22] ^= h1; + s[23] ^= l1; + s[32] ^= h1; + s[33] ^= l1; + s[42] ^= h1; + s[43] ^= l1; + + s[4] ^= h2; + s[5] ^= l2; + s[14] ^= h2; + s[15] ^= l2; + s[24] ^= h2; + s[25] ^= l2; + s[34] ^= h2; + s[35] ^= l2; + s[44] ^= h2; + s[45] ^= l2; + + s[6] ^= h3; + s[7] ^= l3; + s[16] ^= h3; + s[17] ^= l3; + s[26] ^= h3; + s[27] ^= l3; + s[36] ^= h3; + s[37] ^= l3; + s[46] ^= h3; + s[47] ^= l3; + + s[8] ^= h4; + s[9] ^= l4; + s[18] ^= h4; + s[19] ^= l4; + s[28] ^= h4; + s[29] ^= l4; + s[38] ^= h4; + s[39] ^= l4; + s[48] ^= h4; + s[49] ^= l4; + + const b0 = s[0]; + const b1 = s[1]; + const b32 = (s[11] << 4) | (s[10] >>> 28); + const b33 = (s[10] << 4) | (s[11] >>> 28); + const b14 = (s[20] << 3) | (s[21] >>> 29); + const b15 = (s[21] << 3) | (s[20] >>> 29); + const b46 = (s[31] << 9) | (s[30] >>> 23); + const b47 = (s[30] << 9) | (s[31] >>> 23); + const b28 = (s[40] << 18) | (s[41] >>> 14); + const b29 = (s[41] << 18) | (s[40] >>> 14); + const b20 = (s[2] << 1) | (s[3] >>> 31); + const b21 = (s[3] << 1) | (s[2] >>> 31); + const b2 = (s[13] << 12) | (s[12] >>> 20); + const b3 = (s[12] << 12) | (s[13] >>> 20); + const b34 = (s[22] << 10) | (s[23] >>> 22); + const b35 = (s[23] << 10) | (s[22] >>> 22); + const b16 = (s[33] << 13) | (s[32] >>> 19); + const b17 = (s[32] << 13) | (s[33] >>> 19); + const b48 = (s[42] << 2) | (s[43] >>> 30); + const b49 = (s[43] << 2) | (s[42] >>> 30); + const b40 = (s[5] << 30) | (s[4] >>> 2); + const b41 = (s[4] << 30) | (s[5] >>> 2); + const b22 = (s[14] << 6) | (s[15] >>> 26); + const b23 = (s[15] << 6) | (s[14] >>> 26); + const b4 = (s[25] << 11) | (s[24] >>> 21); + const b5 = (s[24] << 11) | (s[25] >>> 21); + const b36 = (s[34] << 15) | (s[35] >>> 17); + const b37 = (s[35] << 15) | (s[34] >>> 17); + const b18 = (s[45] << 29) | (s[44] >>> 3); + const b19 = (s[44] << 29) | (s[45] >>> 3); + const b10 = (s[6] << 28) | (s[7] >>> 4); + const b11 = (s[7] << 28) | (s[6] >>> 4); + const b42 = (s[17] << 23) | (s[16] >>> 9); + const b43 = (s[16] << 23) | (s[17] >>> 9); + const b24 = (s[26] << 25) | (s[27] >>> 7); + const b25 = (s[27] << 25) | (s[26] >>> 7); + const b6 = (s[36] << 21) | (s[37] >>> 11); + const b7 = (s[37] << 21) | (s[36] >>> 11); + const b38 = (s[47] << 24) | (s[46] >>> 8); + const b39 = (s[46] << 24) | (s[47] >>> 8); + const b30 = (s[8] << 27) | (s[9] >>> 5); + const b31 = (s[9] << 27) | (s[8] >>> 5); + const b12 = (s[18] << 20) | (s[19] >>> 12); + const b13 = (s[19] << 20) | (s[18] >>> 12); + const b44 = (s[29] << 7) | (s[28] >>> 25); + const b45 = (s[28] << 7) | (s[29] >>> 25); + const b26 = (s[38] << 8) | (s[39] >>> 24); + const b27 = (s[39] << 8) | (s[38] >>> 24); + const b8 = (s[48] << 14) | (s[49] >>> 18); + const b9 = (s[49] << 14) | (s[48] >>> 18); + + s[0] = b0 ^ (~b2 & b4); + s[1] = b1 ^ (~b3 & b5); + s[10] = b10 ^ (~b12 & b14); + s[11] = b11 ^ (~b13 & b15); + s[20] = b20 ^ (~b22 & b24); + s[21] = b21 ^ (~b23 & b25); + s[30] = b30 ^ (~b32 & b34); + s[31] = b31 ^ (~b33 & b35); + s[40] = b40 ^ (~b42 & b44); + s[41] = b41 ^ (~b43 & b45); + s[2] = b2 ^ (~b4 & b6); + s[3] = b3 ^ (~b5 & b7); + s[12] = b12 ^ (~b14 & b16); + s[13] = b13 ^ (~b15 & b17); + s[22] = b22 ^ (~b24 & b26); + s[23] = b23 ^ (~b25 & b27); + s[32] = b32 ^ (~b34 & b36); + s[33] = b33 ^ (~b35 & b37); + s[42] = b42 ^ (~b44 & b46); + s[43] = b43 ^ (~b45 & b47); + s[4] = b4 ^ (~b6 & b8); + s[5] = b5 ^ (~b7 & b9); + s[14] = b14 ^ (~b16 & b18); + s[15] = b15 ^ (~b17 & b19); + s[24] = b24 ^ (~b26 & b28); + s[25] = b25 ^ (~b27 & b29); + s[34] = b34 ^ (~b36 & b38); + s[35] = b35 ^ (~b37 & b39); + s[44] = b44 ^ (~b46 & b48); + s[45] = b45 ^ (~b47 & b49); + s[6] = b6 ^ (~b8 & b0); + s[7] = b7 ^ (~b9 & b1); + s[16] = b16 ^ (~b18 & b10); + s[17] = b17 ^ (~b19 & b11); + s[26] = b26 ^ (~b28 & b20); + s[27] = b27 ^ (~b29 & b21); + s[36] = b36 ^ (~b38 & b30); + s[37] = b37 ^ (~b39 & b31); + s[46] = b46 ^ (~b48 & b40); + s[47] = b47 ^ (~b49 & b41); + s[8] = b8 ^ (~b0 & b2); + s[9] = b9 ^ (~b1 & b3); + s[18] = b18 ^ (~b10 & b12); + s[19] = b19 ^ (~b11 & b13); + s[28] = b28 ^ (~b20 & b22); + s[29] = b29 ^ (~b21 & b23); + s[38] = b38 ^ (~b30 & b32); + s[39] = b39 ^ (~b31 & b33); + s[48] = b48 ^ (~b40 & b42); + s[49] = b49 ^ (~b41 & b43); + + s[0] ^= ROUND_CONST[n + 0]; + s[1] ^= ROUND_CONST[n + 1]; + } + } + + static hash() { + return new Keccak(); + } + + static hmac(bits, pad, len) { + if (bits == null) + bits = 256; + + assert((bits >>> 0) === bits); + + const rate = 1600 - bits * 2; + + return new HMAC(Keccak, rate / 8, [bits], [pad, len]); + } + + static digest(data, bits, pad, len) { + return Keccak.ctx.init(bits).update(data).final(pad, len); + } + + static root(left, right, bits, pad, len) { + if (bits == null) + bits = 256; + + if (len == null) + len = 0; + + if (len === 0) { + assert((bits >>> 0) === bits); + len = bits >>> 3; + } + + assert((len >>> 0) === len); + assert(Buffer.isBuffer(left) && left.length === len); + assert(Buffer.isBuffer(right) && right.length === len); + + return Keccak.ctx.init(bits).update(left).update(right).final(pad, len); + } + + static multi(x, y, z, bits, pad, len) { + const { ctx } = Keccak; + + ctx.init(bits); + ctx.update(x); + ctx.update(y); + + if (z) + ctx.update(z); + + return ctx.final(pad, len); + } + + static mac(data, key, bits, pad, len) { + return Keccak.hmac(bits, pad, len).init(key).update(data).final(); + } +} + +/* + * Static + */ + +Keccak.native = 0; +Keccak.id = 'KECCAK256'; +Keccak.size = 32; +Keccak.bits = 256; +Keccak.blockSize = 136; +Keccak.zero = Buffer.alloc(32, 0x00); +Keccak.ctx = new Keccak(); + +/* + * Helpers + */ + +function readU32(data, off) { + return (data[off++] + + data[off++] * 0x100 + + data[off++] * 0x10000 + + data[off] * 0x1000000); +} + +/* + * Expose + */ + +module.exports = Keccak; diff --git a/src/algo/base/sha3.js b/src/algo/base/sha3.js new file mode 100644 index 0000000..3effa9f --- /dev/null +++ b/src/algo/base/sha3.js @@ -0,0 +1,71 @@ +/*! + * sha3.js - SHA3 implementation for bcrypto + * Copyright (c) 2017-2019, Christopher Jeffrey (MIT License). + * https://github.com/bcoin-org/bcrypto + * + * Resources: + * https://en.wikipedia.org/wiki/SHA-3 + * https://keccak.team/specifications.html + * https://csrc.nist.gov/projects/hash-functions/sha-3-project/sha-3-standardization + * http://dx.doi.org/10.6028/NIST.FIPS.202 + */ + +'use strict'; + +const Keccak = require('./keccak'); + +/** + * SHA3 + */ + +class SHA3 extends Keccak { + constructor() { + super(); + } + + final() { + return super.final(0x06, null); + } + + static hash() { + return new SHA3(); + } + + static hmac(bits) { + return super.hmac(bits, 0x06, null); + } + + static digest(data, bits) { + return super.digest(data, bits, 0x06, null); + } + + static root(left, right, bits) { + return super.root(left, right, bits, 0x06, null); + } + + static multi(x, y, z, bits) { + return super.multi(x, y, z, bits, 0x06, null); + } + + static mac(data, key, bits) { + return super.mac(data, key, bits, 0x06, null); + } +} + +/* + * Static + */ + +SHA3.native = 0; +SHA3.id = 'SHA3_256'; +SHA3.size = 32; +SHA3.bits = 256; +SHA3.blockSize = 136; +SHA3.zero = Buffer.alloc(32, 0x00); +SHA3.ctx = new SHA3(); + +/* + * Expose + */ + +module.exports = SHA3; diff --git a/src/algo/base/sha3hns.js b/src/algo/base/sha3hns.js new file mode 100644 index 0000000..622debc --- /dev/null +++ b/src/algo/base/sha3hns.js @@ -0,0 +1,227 @@ +'use strict'; + +const ROUND_CONST = new Uint32Array([ + 0x00000001, 0x00000000, 0x00008082, 0x00000000, + 0x0000808a, 0x80000000, 0x80008000, 0x80000000, + 0x0000808b, 0x00000000, 0x80000001, 0x00000000, + 0x80008081, 0x80000000, 0x00008009, 0x80000000, + 0x0000008a, 0x00000000, 0x00000088, 0x00000000, + 0x80008009, 0x00000000, 0x8000000a, 0x00000000, + 0x8000808b, 0x00000000, 0x0000008b, 0x80000000, + 0x00008089, 0x80000000, 0x00008003, 0x80000000, + 0x00008002, 0x80000000, 0x00000080, 0x80000000, + 0x0000800a, 0x00000000, 0x8000000a, 0x80000000, + 0x80008081, 0x80000000, 0x00008080, 0x80000000, + 0x80000001, 0x00000000, 0x80008008, 0x80000000 +]); + +function update(data, s) { + const count = 34; + + for (let i = 0; i < count; i++) + s[i] ^= data.readUInt32LE(i * 4); + + for (let n = 0; n < 48; n += 2) { + const c0 = s[0] ^ s[10] ^ s[20] ^ s[30] ^ s[40]; + const c1 = s[1] ^ s[11] ^ s[21] ^ s[31] ^ s[41]; + const c2 = s[2] ^ s[12] ^ s[22] ^ s[32] ^ s[42]; + const c3 = s[3] ^ s[13] ^ s[23] ^ s[33] ^ s[43]; + const c4 = s[4] ^ s[14] ^ s[24] ^ s[34] ^ s[44]; + const c5 = s[5] ^ s[15] ^ s[25] ^ s[35] ^ s[45]; + const c6 = s[6] ^ s[16] ^ s[26] ^ s[36] ^ s[46]; + const c7 = s[7] ^ s[17] ^ s[27] ^ s[37] ^ s[47]; + const c8 = s[8] ^ s[18] ^ s[28] ^ s[38] ^ s[48]; + const c9 = s[9] ^ s[19] ^ s[29] ^ s[39] ^ s[49]; + + const h0 = c8 ^ ((c2 << 1) | (c3 >>> 31)); + const l0 = c9 ^ ((c3 << 1) | (c2 >>> 31)); + const h1 = c0 ^ ((c4 << 1) | (c5 >>> 31)); + const l1 = c1 ^ ((c5 << 1) | (c4 >>> 31)); + const h2 = c2 ^ ((c6 << 1) | (c7 >>> 31)); + const l2 = c3 ^ ((c7 << 1) | (c6 >>> 31)); + const h3 = c4 ^ ((c8 << 1) | (c9 >>> 31)); + const l3 = c5 ^ ((c9 << 1) | (c8 >>> 31)); + const h4 = c6 ^ ((c0 << 1) | (c1 >>> 31)); + const l4 = c7 ^ ((c1 << 1) | (c0 >>> 31)); + + s[0] ^= h0; + s[1] ^= l0; + s[10] ^= h0; + s[11] ^= l0; + s[20] ^= h0; + s[21] ^= l0; + s[30] ^= h0; + s[31] ^= l0; + s[40] ^= h0; + s[41] ^= l0; + + s[2] ^= h1; + s[3] ^= l1; + s[12] ^= h1; + s[13] ^= l1; + s[22] ^= h1; + s[23] ^= l1; + s[32] ^= h1; + s[33] ^= l1; + s[42] ^= h1; + s[43] ^= l1; + + s[4] ^= h2; + s[5] ^= l2; + s[14] ^= h2; + s[15] ^= l2; + s[24] ^= h2; + s[25] ^= l2; + s[34] ^= h2; + s[35] ^= l2; + s[44] ^= h2; + s[45] ^= l2; + + s[6] ^= h3; + s[7] ^= l3; + s[16] ^= h3; + s[17] ^= l3; + s[26] ^= h3; + s[27] ^= l3; + s[36] ^= h3; + s[37] ^= l3; + s[46] ^= h3; + s[47] ^= l3; + + s[8] ^= h4; + s[9] ^= l4; + s[18] ^= h4; + s[19] ^= l4; + s[28] ^= h4; + s[29] ^= l4; + s[38] ^= h4; + s[39] ^= l4; + s[48] ^= h4; + s[49] ^= l4; + + const b0 = s[0]; + const b1 = s[1]; + const b32 = (s[11] << 4) | (s[10] >>> 28); + const b33 = (s[10] << 4) | (s[11] >>> 28); + const b14 = (s[20] << 3) | (s[21] >>> 29); + const b15 = (s[21] << 3) | (s[20] >>> 29); + const b46 = (s[31] << 9) | (s[30] >>> 23); + const b47 = (s[30] << 9) | (s[31] >>> 23); + const b28 = (s[40] << 18) | (s[41] >>> 14); + const b29 = (s[41] << 18) | (s[40] >>> 14); + const b20 = (s[2] << 1) | (s[3] >>> 31); + const b21 = (s[3] << 1) | (s[2] >>> 31); + const b2 = (s[13] << 12) | (s[12] >>> 20); + const b3 = (s[12] << 12) | (s[13] >>> 20); + const b34 = (s[22] << 10) | (s[23] >>> 22); + const b35 = (s[23] << 10) | (s[22] >>> 22); + const b16 = (s[33] << 13) | (s[32] >>> 19); + const b17 = (s[32] << 13) | (s[33] >>> 19); + const b48 = (s[42] << 2) | (s[43] >>> 30); + const b49 = (s[43] << 2) | (s[42] >>> 30); + const b40 = (s[5] << 30) | (s[4] >>> 2); + const b41 = (s[4] << 30) | (s[5] >>> 2); + const b22 = (s[14] << 6) | (s[15] >>> 26); + const b23 = (s[15] << 6) | (s[14] >>> 26); + const b4 = (s[25] << 11) | (s[24] >>> 21); + const b5 = (s[24] << 11) | (s[25] >>> 21); + const b36 = (s[34] << 15) | (s[35] >>> 17); + const b37 = (s[35] << 15) | (s[34] >>> 17); + const b18 = (s[45] << 29) | (s[44] >>> 3); + const b19 = (s[44] << 29) | (s[45] >>> 3); + const b10 = (s[6] << 28) | (s[7] >>> 4); + const b11 = (s[7] << 28) | (s[6] >>> 4); + const b42 = (s[17] << 23) | (s[16] >>> 9); + const b43 = (s[16] << 23) | (s[17] >>> 9); + const b24 = (s[26] << 25) | (s[27] >>> 7); + const b25 = (s[27] << 25) | (s[26] >>> 7); + const b6 = (s[36] << 21) | (s[37] >>> 11); + const b7 = (s[37] << 21) | (s[36] >>> 11); + const b38 = (s[47] << 24) | (s[46] >>> 8); + const b39 = (s[46] << 24) | (s[47] >>> 8); + const b30 = (s[8] << 27) | (s[9] >>> 5); + const b31 = (s[9] << 27) | (s[8] >>> 5); + const b12 = (s[18] << 20) | (s[19] >>> 12); + const b13 = (s[19] << 20) | (s[18] >>> 12); + const b44 = (s[29] << 7) | (s[28] >>> 25); + const b45 = (s[28] << 7) | (s[29] >>> 25); + const b26 = (s[38] << 8) | (s[39] >>> 24); + const b27 = (s[39] << 8) | (s[38] >>> 24); + const b8 = (s[48] << 14) | (s[49] >>> 18); + const b9 = (s[49] << 14) | (s[48] >>> 18); + + s[0] = b0 ^ (~b2 & b4); + s[1] = b1 ^ (~b3 & b5); + s[10] = b10 ^ (~b12 & b14); + s[11] = b11 ^ (~b13 & b15); + s[20] = b20 ^ (~b22 & b24); + s[21] = b21 ^ (~b23 & b25); + s[30] = b30 ^ (~b32 & b34); + s[31] = b31 ^ (~b33 & b35); + s[40] = b40 ^ (~b42 & b44); + s[41] = b41 ^ (~b43 & b45); + s[2] = b2 ^ (~b4 & b6); + s[3] = b3 ^ (~b5 & b7); + s[12] = b12 ^ (~b14 & b16); + s[13] = b13 ^ (~b15 & b17); + s[22] = b22 ^ (~b24 & b26); + s[23] = b23 ^ (~b25 & b27); + s[32] = b32 ^ (~b34 & b36); + s[33] = b33 ^ (~b35 & b37); + s[42] = b42 ^ (~b44 & b46); + s[43] = b43 ^ (~b45 & b47); + s[4] = b4 ^ (~b6 & b8); + s[5] = b5 ^ (~b7 & b9); + s[14] = b14 ^ (~b16 & b18); + s[15] = b15 ^ (~b17 & b19); + s[24] = b24 ^ (~b26 & b28); + s[25] = b25 ^ (~b27 & b29); + s[34] = b34 ^ (~b36 & b38); + s[35] = b35 ^ (~b37 & b39); + s[44] = b44 ^ (~b46 & b48); + s[45] = b45 ^ (~b47 & b49); + s[6] = b6 ^ (~b8 & b0); + s[7] = b7 ^ (~b9 & b1); + s[16] = b16 ^ (~b18 & b10); + s[17] = b17 ^ (~b19 & b11); + s[26] = b26 ^ (~b28 & b20); + s[27] = b27 ^ (~b29 & b21); + s[36] = b36 ^ (~b38 & b30); + s[37] = b37 ^ (~b39 & b31); + s[46] = b46 ^ (~b48 & b40); + s[47] = b47 ^ (~b49 & b41); + s[8] = b8 ^ (~b0 & b2); + s[9] = b9 ^ (~b1 & b3); + s[18] = b18 ^ (~b10 & b12); + s[19] = b19 ^ (~b11 & b13); + s[28] = b28 ^ (~b20 & b22); + s[29] = b29 ^ (~b21 & b23); + s[38] = b38 ^ (~b30 & b32); + s[39] = b39 ^ (~b31 & b33); + s[48] = b48 ^ (~b40 & b42); + s[49] = b49 ^ (~b41 & b43); + + s[0] ^= ROUND_CONST[n + 0]; + s[1] ^= ROUND_CONST[n + 1]; + + //dump(s, `s:n=${n}`, 4) + } +} +function sha3hns(data) { + const state = new Uint32Array(50); + + update(data, state); + + const final = Buffer.alloc(136, 0); + final[0] |= 6; + final[135] |= 0x80; + update(final, state); + + const out = Buffer.alloc(32); + for (let i = 0; i < 32; i++) + out[i] = state[i >> 2] >> (8 * (i & 3)); + + return out; +} + +module.exports = sha3hns; diff --git a/src/algo/blake2bsha3Api.js b/src/algo/blake2bsha3Api.js new file mode 100644 index 0000000..8a18616 --- /dev/null +++ b/src/algo/blake2bsha3Api.js @@ -0,0 +1,39 @@ +const EventEmitter = require('events'); +const Debug = require('../log')(); +const BLAKE2B = require('./base/blake2b') +const sha3hns = require('./base/sha3hns'); +const COMP = '[blake2bsha3Api]'; + +class blake2bsha3Api extends EventEmitter { + constructor({}) { + super(); + var _this = this; + + } + getAlgoName() { + return 'blake2bsha3'; + } + + genHash(data, length, varity) { + //const data = Buffer.from('40ad0f00d163845e00000000962b1c413636e85858fdf91d152b42056662b7fc000000000000005b20195afe280a27276d4517c7f80f5a61843eac78b1af2c82962b1c413636e80378e4a3e33d2165220b27a03b7f0eb30bb515b160961657af5b9f8af4996c2566621ecd0f71c6a77cf80c2e6b6e816aae44d07d91830caa07', 'hex'); + //Debug.IbctLogDbg(COMP, 'genhash++++', data.toString('hex')); + const pad8 = Buffer.alloc(8); + const pad32 = Buffer.alloc(32); + for (let i = 0; i < 32; i++) { + if (i < 8) + pad8.writeUInt8(data.readUInt8(i + 32) ^ data.readUInt8(i + 64), i); + pad32.writeUInt8(data.readUInt8(i + 32) ^ data.readUInt8(i + 64), i); + } + + const left = BLAKE2B.digest(data, 64); + //const rxx = SHA3.multi(data, pad8); + const right = sha3hns(Buffer.concat([data, pad8])); + const hash = BLAKE2B.multi(left, pad32, right); + //Debug.IbctLogDbg(COMP, 'HASH IS', hash.toString('hex')); + return hash.toString('hex'); + } +}; + +module.exports = function Runblake2bsha3Api(options = {}) { + return new blake2bsha3Api(options); +}; diff --git a/src/algo/lbryApi.js b/src/algo/lbryApi.js new file mode 100644 index 0000000..24bdb67 --- /dev/null +++ b/src/algo/lbryApi.js @@ -0,0 +1,22 @@ +const EventEmitter = require('events'); +var multiHashing = require('ibctminerscrypt'); +const Debug = require('../log')(); +const COMP = '[lbryApi]'; + +class lbryApi extends EventEmitter { + constructor({}) { + super(); + var _this = this; + _this.genhashFunc = multiHashing['lbry']; + } + getAlgoName() { + return 'lbry'; + } + genHash(data) { + return this.genhashFunc(data); + } +}; + +module.exports = function RunlbryApi(options = {}) { + return new lbryApi(options); +}; diff --git a/src/cryptocurrency/cryptocurrency.js b/src/cryptocurrency/cryptocurrency.js new file mode 100644 index 0000000..618a75f --- /dev/null +++ b/src/cryptocurrency/cryptocurrency.js @@ -0,0 +1,99 @@ +const EventEmitter = require('events'); +const hns = require('./hns'); +const lbc = require('./lbc'); +const Debug = require('../log')(); +const COMP = '[cryptoCurrencys]'; + +var cryptoCurrencys = [ + { + name: 'hns', + api: hns + }, + { + name: 'lbc', + api: lbc + } +]; + +class CryptoCurrency extends EventEmitter { + constructor({ + name + }) { + super(); + var _this = this; + + Debug.IbctLogDbg(COMP, name); + _this.name = name; + _this.cryptoApi = _this.GetCryptoApi(_this.name); + if (!_this.cryptoApi) { + _this.emit('error', __('不支持此币种:'), _this.name); + } + + _this.on('error', function (error) { + Debug.IbctLogErr(COMP, "error:", error); + }); + + return _this; + } + GetCryptoApi(name) { + var crypto = null; + cryptoCurrencys.forEach(function (cryptoCurrency, index) { + if (cryptoCurrency.name === name) { + crypto = cryptoCurrency; + } + }) + return crypto ? crypto.api() : null; + } + + getCryptoName() { + return this.cryptoApi ? this.cryptoApi.getCryptoName() : null; + } + stratum_subscribe(data) { + return this.cryptoApi ? this.cryptoApi.stratum_subscribe(data) : null; + } + jsonrpc_subscibe(data) { + return this.cryptoApi ? this.cryptoApi.jsonrpc_subscibe(data) : null; + } + + stratum_diff(data) { + return this.cryptoApi ? this.cryptoApi.stratum_diff(data) : null; + } + jsonrpc_diff(data) { + return this.cryptoApi ? this.cryptoApi.jsonrpc_diff(data) : null; + } + + stratum_notify(data) { + return this.cryptoApi ? this.cryptoApi.stratum_notify(data) : null; + } + jsonrpc_notify(data) { + return this.cryptoApi ? this.cryptoApi.jsonrpc_notify(data) : null; + } + JobtoWork(Job, Work) { + return this.cryptoApi ? this.cryptoApi.JobtoWork(Job, Work) : null; + } + setWorkData(work, mode, data) { + return this.cryptoApi ? this.cryptoApi.setWorkData(work, mode, data) : null; + } + checkHash(target, hash) { + return this.cryptoApi ? this.cryptoApi.checkHash(target, hash) : null; + } + calHash(Device, nonce) { + return this.cryptoApi ? this.cryptoApi.calHash(Device, nonce) : null; + } + getSubmitParams(Device, nonce, hash) { + return this.cryptoApi ? this.cryptoApi.getSubmitParams(Device, nonce, hash) : null; + } + targetToDiff(target) { + return this.cryptoApi ? this.cryptoApi.targetToDiff(target) : null; + } + diffToTarget(difficulty) { + return this.cryptoApi ? this.cryptoApi.diffToTarget(difficulty) : null; + } + reverseBuffer(src) { + return this.cryptoApi ? this.cryptoApi.reverseBuffer(src) : null; + } +}; + +module.exports = function GetCryptoCurrency(options = {}) { + return new CryptoCurrency(options); +}; \ No newline at end of file diff --git a/src/cryptocurrency/hns.js b/src/cryptocurrency/hns.js new file mode 100644 index 0000000..ccfaa1b --- /dev/null +++ b/src/cryptocurrency/hns.js @@ -0,0 +1,226 @@ +const EventEmitter = require('events'); +const Debug = require('../log')(); +const BLAKE2B = require('../algo/base/blake2b'); +const COMP = '[hns]'; + +class Hns extends EventEmitter { + constructor({ }) { + super(); + var _this = this; + _this.nce2sz = 24; + _this.nce1hex = null; + _this.curdiff = 1; + } + + stratum_subscribe(data) { + //Debug.IbctLogDbg(COMP, 'subscribe ++++++++'); + var _this = this; + _this.nce1hex = data[1]; + //_this.nce2sz = data[2]; + Debug.IbctLogDbg(COMP, 'subscribe',_this.nce1hex, _this.nce2sz); + return 0; + } + + stratum_diff(data) { + var _this = this; + _this.curdiff = data[0]; + Debug.IbctLogDbg(COMP, 'setdiff to', _this.curdiff); + return 0; + } + + stratum_notify(data) { + var packet = { + job_id: null, + preHash: null, + merkleRoot: null, + witnessRoot: null, + treeRoot: null, + reserveRoot: null, + bbVersion: null, + nbit: null, + ntime: null, + clean: null + }; + //Debug.IbctLogDbg(COMP, 'hns', JSON.stringify(packet)); + packet.job_id = data[0]; + packet.preHash = data[1]; + packet.merkleRoot = data[2]; + packet.witnessRoot = data[3]; + packet.treeRoot = data[4]; + packet.reserveRoot = data[5]; + packet.bbVersion = data[6]; + packet.nbit = data[7]; + packet.ntime = data[8]; + packet.nonce2 = 0; + packet.clean = true; + //Debug.IbctLogDbg(COMP, 'stratum_notify', JSON.stringify(packet)); + return packet; + } + + getCryptoName() { + return 'hns'; + } + + padding(size, prevBlock, treeRoot) { + const pad = Buffer.alloc(size); + for (let i = 0; i < size; i++) + pad[i] = prevBlock[i % 32] ^ treeRoot[i % 32]; + return pad; + } + + targetToDiff(target) { + var Ntarget = parseInt('0x' + target, 16); + var difficulty = Math.round(26959946667150639794667015087019630673637144422540572481103610249215.0 / Ntarget * 0xffffffff); + return difficulty; + } + + diffToTarget(difficulty) { + var temp = Math.floor(0xffffffff / difficulty).toString(16); + var target = Buffer.from('0'.repeat(16 - temp.length) + temp + 'f'.repeat(48), 'hex'); + return target; + } + + JobtoWork(Job, Work) { + if (!Job || !Work) { + return false; + } + var _this = this; + Debug.IbctLogInfo(COMP, 'JobtoWork+++++', JSON.stringify(Job)); + + const preHash = Buffer.from(Job.preHash, 'hex'); + const treeRoot = Buffer.from(Job.treeRoot, 'hex'); + const reserveRoot = Buffer.from(Job.reserveRoot, 'hex'); + const witnessRoot = Buffer.from(Job.witnessRoot, 'hex'); + const merkleRoot = Buffer.from(Job.merkleRoot, 'hex'); + const ntime = parseInt('0x' + Job.ntime, 16); + const nbit = parseInt('0x' + Job.nbit, 16); + const bbVersion = parseInt('0x' + Job.bbVersion, 16); + ////////////////////////////////////////////////////////////////////////////////////////////////////////// + // For Stratum test + // const preHash = Buffer.from('000000000000025a108f1cb72ba240f4ba7a328335df544b51576de46f32a1d2', 'hex'); + // const treeRoot = Buffer.from('6818edbdbf9d45e6ee969f190fea65f282c35704a2ca9de702e732dba1331853', 'hex'); + // const reserveRoot = Buffer.from('0000000000000000000000000000000000000000000000000000000000000000', 'hex'); + // const witnessRoot = Buffer.from('51cb87c5c422ad753accd7743d6cd9cc6dd54e88c57419b802e1dd88fbac86ae', 'hex'); + // const merkleRoot = Buffer.from('39aeb10e8a858e53c81a7af2b44abc7dffd5b24227d092148b8480f28332ddef', 'hex'); + // const ntime = 0x5ed7427a; + // const nbit = 0x1a0350df + // _this.nce1hex = '02cea75b'; + // Job.nonce2 = 1; + ////////////////////////////////////////////////////////////////////////////////////////////////////////// + + const mask = Buffer.alloc(32); + const subHead = Buffer.alloc(128); + const extraNonce = Buffer.alloc(_this.nce2sz); + const pad20 = _this.padding(20, preHash, treeRoot); + const maskHash = BLAKE2B.multi(preHash, mask, 32); + extraNonce.writeUInt32BE(parseInt(_this.nce1hex, 16), 0); + extraNonce.writeUInt32BE(Job.nonce2, 4); + extraNonce.copy(subHead, 0); + reserveRoot.copy(subHead, 24); + witnessRoot.copy(subHead, 56); + merkleRoot.copy(subHead, 88); + subHead.writeUInt32LE(bbVersion, 120); + subHead.writeUInt32LE(nbit, 124); + //Debug.IbctLogDbg(COMP, 'JobtoWork+++++ extraNonce', extraNonce.toString('hex')); + //Debug.IbctLogDbg(COMP, 'JobtoWork+++++ subHead', subHead.toString('hex')); + //Debug.IbctLogDbg(COMP, 'JobtoWork+++++ pad20', pad20.toString('hex')); + + const subHeader = BLAKE2B.digest(subHead, 32); + const commitHash = BLAKE2B.multi(subHeader, maskHash, 32); + Work.job_id = Job.job_id; + Work.snonce = ntime * 0x100000000; + Work.enonce = 0; + Work.nonce2 = Job.nonce2++; + Work.difficulty = _this.curdiff; + Work.target = _this.diffToTarget(Work.difficulty); + Work.ntime = ntime; + Work.clean = Job.clean; + + //////////////////////////////// + //make work data // + Work.data = Buffer.alloc(128); + Work.data.writeUInt32LE(0, 0); + Work.data.writeUInt32LE(Work.ntime, 4); + pad20.copy(Work.data, 12); + preHash.copy(Work.data, 32); + treeRoot.copy(Work.data, 64); + commitHash.copy(Work.data,96); + //Debug.IbctLogDbg(COMP, 'JobtoWork+++++ WorkNonce2:', Work.nonce2, 'WorkData', Work.data.toString('hex')); + return true; + } + + + + setWorkData(work, mode, data) { + //Debug.IbctLogInfo(COMP, 'setWorkData: ', data.toString('hex')); + if (mode === 'start nonce') { + // Debug.IbctLogDbg(COMP, ((data & 0xffffffff) >>> 0).toString('16')); + // work.data.writeUIntLE((data & 0xffffffff), 0, 4); + // Debug.IbctLogDbg(COMP, (Math.floor(data / 0x100000000) >>> 0).toString('16')); + // work.data.writeUIntLE(Math.floor(data / 0x100000000), 4, 4); + data.copy(work.data, 0); + } + } + + checkHash(target, hash) { + //Debug.IbctLogDbg(COMP, 'checkHash', 'target', target.toString('hex'), hash.toString('hex')); + return Buffer.compare(target, hash) > 0; + } + + reverseBuffer(src) { + const buffer = Buffer.alloc(src.length); + for (let i = 0, j = src.length - 1; i <= j; ++i, --j) { + buffer[i] = src[j]; + buffer[j] = src[i]; + } + return buffer; + } + + calHash(Device, nonce) { + var work = Device.dev.work; + var data = Buffer.from(work.data); + var algo = Device.dev.algorithm; + //var data = Buffer.from(work.data); + var result = null; + + if (!algo) { + return false; + } + + //Debug.IbctLogDbg(COMP, 'calHash++ Worknonce2', work.nonce2, 'data', data.toString('hex')); + + nonce.copy(data, 0); + result = algo.genHash(data, data.length, 0); + return Buffer.from(result, 'hex'); + } + + getSubmitParams(Device, nonce, hash) { + var nce2hex = Buffer.alloc(4); + var ntime1 = this.reverseBuffer(Buffer.from(nonce.slice(8,16), 'hex')); + var nce = this.reverseBuffer(Buffer.from(nonce.slice(0,8), 'hex')); + nce2hex.writeUInt32BE(Device.dev.work.nonce2, 0); + var submit = { + job_id: null, + params: { + id: null, + submit: [] + } + }; + //var ntime = new Date(); + submit.job_id = Device.dev.work.job_id; + submit.params = { + id: Device.dev.poolId, + submit: [ + // Device.dev.pool.user, submit.job_id, "0", Math.floor(ntime.getTime() / 1000).toString('16'), nonce.toString('16') + Device.dev.pool.user, submit.job_id, nce2hex.toString('hex'), ntime1.toString('hex'), nce.toString('hex'), Buffer.alloc(32).toString('hex') + ] + } + Debug.IbctLogDbg(COMP, 'getSubmitParams+++', JSON.stringify(submit)); + + return submit; + } +}; + +module.exports = function GetHns(options = {}) { + return new Hns(options); +}; \ No newline at end of file diff --git a/src/cryptocurrency/lbc.js b/src/cryptocurrency/lbc.js new file mode 100644 index 0000000..9d40fce --- /dev/null +++ b/src/cryptocurrency/lbc.js @@ -0,0 +1,280 @@ +const EventEmitter = require('events'); +var multiHashing = require('ibctminerscrypt'); +const Debug = require('../log')(); +const COMP = '[lbc]'; + +class Lbc extends EventEmitter { + constructor({}) { + super(); + var _this = this; + _this.nce2sz = 4; + _this.nce1hex = null; + _this.curdiff = 1; + _this.cnt = 0; + } + + stratum_subscribe(data) { + //console.log(COMP, 'subscribe ++++++++', data); + var _this = this; + _this.nce1hex = data[1]; + _this.nce2sz = data[2]; + Debug.IbctLogDbg(COMP, 'subscribe',_this.nce1hex, _this.nce2sz); + return 0; + } + + stratum_diff(data) { + var _this = this; + _this.curdiff = data[0]; + Debug.IbctLogDbg(COMP, 'setdiff to', _this.curdiff); + _this.curdiff /= 256.0; + return 0; + } + + stratum_notify(data) { + var packet = { + job_id: null, + preHash: null, + claimHash: null, + coinb1: null, + coinb2: null, + merkleRoot: null, + bbVersion: null, + nbit: null, + ntime: null, + clean: null + }; + //Debug.IbctLogDbg(COMP, JSON.stringify(packet)); + //console.log("*****stratum data", data); + + packet.job_id = data[0]; + packet.preHash = data[1]; + packet.claimHash = data[2]; + packet.coinb1 = data[3]; + packet.coinb2 = data[4]; + packet.merkleRoot = data[5]; + packet.bbVersion = data[6]; + packet.nbit = data[7]; + packet.ntime = data[8]; + packet.clean = true; + packet.nonce2 = 0; + //Debug.IbctLogDbg(COMP, 'stratum_notify', JSON.stringify(packet)); + return packet; + } + + getCryptoName() { + return 'lbc'; + } + + targetToDiff(target) { + var Ntarget = parseInt('0x' + target, 16); + var difficulty = Math.round(26959946667150639794667015087019630673637144422540572481103610249215.0 / Ntarget * 0xffffffff); + return difficulty; + } + + diffToTarget(difficulty) { + var temp = Math.floor(0xffffffff / difficulty).toString(16); + var target = Buffer.from('0'.repeat(16 - temp.length) + temp + 'f'.repeat(48), 'hex'); + return target; + } + + JobtoWork(Job, Work) { + if (!Job || !Work) { + return false; + } + var _this = this; + var s = []; + //console.log(COMP, 'JobtoWork+++++', Job); + + const preHash = Buffer.from(Job.preHash, 'hex'); + const claimHash = Buffer.from(Job.claimHash, 'hex'); + const coinb1 = Buffer.from(Job.coinb1, 'hex'); + const coinb2 = Buffer.from(Job.coinb2, 'hex'); + const merkleCount = Job.merkleRoot.length; + const merkleRoot = Job.merkleRoot; + //const bbVersion = parseInt('0x' + Job.bbVersion, 16); + const ntime = Buffer.from(Job.ntime, 'hex'); + const nbit = Buffer.from(Job.nbit, 'hex'); + const bbVersion = Buffer.from(Job.bbVersion, 'hex'); + + for (var i = 0; i < merkleCount; i++) + { + s[i] = Buffer.from(merkleRoot[i], 'hex'); + } + //console.log("preHash", preHash.toString('hex')); + //console.log("claimHash", claimHash.toString('hex')); + //console.log("coinb1", coinb1.toString('hex')); + //console.log("coinb2", coinb2.toString('hex')); + //console.log("merklecount", merkleCount); + //console.log("merkleroot", s); + //console.log("ntime", ntime.toString('hex')); + //console.log("nbit", nbit.toString('hex')); + //console.log("bbversion", bbVersion.toString('hex')); + + var coinb1_size = coinb1.length; + var coinb2_size = coinb2.length; + var nonce1 = Buffer.from(_this.nce1hex, 'hex'); + //console.log("nonce1", nonce1.toString('hex')); + var coinbase_size = coinb1_size + coinb2_size + nonce1.length + _this.nce2sz; + //console.log("coinbase_size", coinbase_size); + var coinbase = Buffer.alloc(coinbase_size, 0); + coinb1.copy(coinbase, 0); + nonce1.copy(coinbase, coinb1_size); + var nonce2 = Buffer.alloc(4, 0); + nonce2.writeUInt32BE(Job.nonce2); + nonce2.copy(coinbase, coinb1_size + nonce1.length); + coinb2.copy(coinbase, coinb1_size + nonce1.length + _this.nce2sz); + //console.log("coinbase", coinbase.toString('hex')); + var sha256d = multiHashing['sha256d']; + var merkle_root = Buffer.alloc(64, 0); + var m1 = sha256d(coinbase); + //console.log("m 1", m1.toString('hex')); + m1.copy(merkle_root, 0); + //console.log("m 2", merkle_root.toString('hex')); + + var m2; + for (var i = 0; i < merkleCount; i++) + { + s[i].copy(merkle_root, 32); + //console.log("m", i, merkle_root.toString('hex')); + m2 = sha256d(merkle_root); + //console.log("m 2", m2.toString('hex')); + m2.copy(merkle_root, 0); + //console.log("*m", i, merkle_root.toString('hex')); + } + //console.log("merkleroot", merkle_root.toString('hex')); + var merkle = Buffer.alloc(32); + + for(var i = 0; i < 8; i++) + { + merkle.writeUInt32BE(merkle_root.readUInt32LE(i*4), i*4); + } + //console.log("merkle ", merkle.toString('hex')); + + + //console.log("***Work", Work); + Work.job_id = Job.job_id; + //Work.snonce = ntime * 0x100000000; + Work.snonce = 0; + Work.enonce = 0xffffffff; + Work.nonce2 = Job.nonce2++; + //console.log("nnnnnnonce2", Work.nonce2); + Work.difficulty = _this.curdiff; + Work.target = _this.diffToTarget(Work.difficulty); + //Work.ntime = parseInt('0x' + Job.ntime, 16); + Work.ntime = ntime; + Work.clean = Job.clean; + var data1 = Buffer.alloc(112, 0); + bbVersion.copy(data1, 0); + preHash.copy(data1, 4); + merkle.copy(data1, 36); + claimHash.copy(data1, 68); + ntime.copy(data1, 100); + nbit.copy(data1, 104); + + //console.log("data1", data1.toString('hex')); + + var data2 = Buffer.alloc(112, 0); + + for(var i = 0; i < 28; i++) + { + data2.writeUInt32BE(data1.readUInt32LE(i*4), i*4); + } + //console.log("data2", data2.toString('hex')); + var genhash = multiHashing['lbcgenhash']; + var prehash1 = genhash(data2); + //console.log("prehash1", prehash1.toString('hex')); + + Work.data = Buffer.alloc(136, 0); + Work.sdata = Buffer.alloc(112, 0); + data2.copy(Work.sdata); + var prehash2 = Buffer.alloc(32, 0); + for(var i = 0; i < 8; i++) + { + prehash2.writeUInt32BE(prehash1.readUInt32LE(i*4), i*4); + } + //console.log("prehash2", prehash2.toString('hex')); + prehash2.copy(Work.data, 0); + data2.copy(Work.data, 32, 64); + //console.log(COMP, 'JobtoWork+++++ WorkNonce2:', Work.nonce2, 'WorkData', Work.data.toString('hex')); + return true; + } + + setWorkData(work, mode, data) { + //Debug.IbctLogInfo(COMP, 'setWorkData: ', data.toString('hex')); + if (mode === 'start nonce') { + // Debug.IbctLogDbg(COMP, ((data & 0xffffffff) >>> 0).toString('16')); + // work.data.writeUIntLE((data & 0xffffffff), 0, 4); + // Debug.IbctLogDbg(COMP, (Math.floor(data / 0x100000000) >>> 0).toString('16')); + // work.data.writeUIntLE(Math.floor(data / 0x100000000), 4, 4); + data.copy(work.data, 0); + } + } + + checkHash(target, hash) { + //Debug.IbctLogDbg(COMP, 'checkHash', 'target', target.toString('hex'), hash.toString('hex')); + this.cnt++; + if (this.cnt === 1) { + if(Buffer.compare(target, hash) > 0) { + return true; + } else { + this.cnt = 0; + return false; + } + } else if (this.cnt === 2) { + this.cnt = 0; + return Buffer.compare(this.diffToTarget(this.curdiff), hash) > 0; + } + } + + reverseBuffer(src) { + const buffer = Buffer.alloc(src.length); + for (let i = 0, j = src.length - 1; i <= j; ++i, --j) { + buffer[i] = src[j]; + buffer[j] = src[i]; + } + return buffer; + } + + calHash(Device, nonce) { + var work = Device.dev.work; + var data = Buffer.from(work.sdata); + var algo = Device.dev.algorithm; + var result = null; + + //console.log("$$$$$$$$$$$$$", data.toString('hex')); + + if (!algo) return false; + + //Debug.IbctLogDbg(COMP, 'calHash++ Worknonce2', work.nonce2, 'data', data.toString('hex')); + + nonce.copy(data, 108); + var tt = nonce.readUInt32LE(4); + var ntime = data.readUInt32LE(100); + ntime += tt; + data.writeUInt32LE(ntime, 100); + //console.log("(((((((((((((", data.toString('hex')); + result = algo.genHash(data); + //console.log("result", result.toString('hex')); + return this.reverseBuffer(result); + } + + getSubmitParams(Device, nonce, hash) { + var nce2hex = Buffer.alloc(4); + var nce = this.reverseBuffer(Buffer.from(nonce.slice(0,8), 'hex')); + var ntime = Device.dev.work.ntime.readUInt32BE(0); + var tt = this.reverseBuffer(Buffer.from(nonce.slice(8,16), 'hex')); + var tt1 = tt.readUInt32BE(0); + ntime += tt1; + var ntime1 = Buffer.alloc(4); + ntime1.writeUInt32BE(ntime, 0) + nce2hex.writeUInt32BE(Device.dev.work.nonce2, 0); + var submit = [Device.dev.pool.user, Device.dev.work.job_id, nce2hex.toString('hex'), ntime1.toString('hex'), nce.toString('hex')]; + //console.log(COMP, 'getSubmitParams+++', JSON.stringify(submit)); + + return submit; + } +}; + +module.exports = function GetLbc(options = {}) { + return new Lbc(options); +}; diff --git a/src/detect.js b/src/detect.js new file mode 100644 index 0000000..e4cd0a4 --- /dev/null +++ b/src/detect.js @@ -0,0 +1,171 @@ +const EventEmitter = require('events'); +const UsbDetect = require('usb-detection'); +const SerialPort = require('serialport'); +const Fs = require('fs') +const Debug = require('./log')(); +const locks = require('locks'); +const COMP = '[Detect]'; + +class UsbMiner extends EventEmitter { + constructor({}) { + super(); + var _this = this + _this.devices = []; + _this.runDevices = []; + _this.detectMutexs = []; + _this.id = 0; + // UsbDevice Vid&Pid + _this.vendorId = '1155'; + _this.productId = '22336'; + + UsbDetect.startMonitoring(); + + _this.on('error', function (error) { + Debug.IbctLogErr(COMP, "error:", error); + }); + + var listener = 'add' + ':' + _this.vendorId + ':' + _this.productId + UsbDetect.on(listener, function (Device) { + Debug.IbctLogErr(COMP, 'add:', JSON.stringify(Device)); + _this.AddUsbMiner(Device); + }); + + listener = 'remove' + ':' + _this.vendorId + ':' + _this.productId + UsbDetect.on(listener, function (Device) { + if (_this.hasExistUsbMiner(Device)) { + Debug.IbctLogErr(COMP, 'remove:', JSON.stringify(Device)); + _this.RemoveUsbMiner(Device); + } + }); + } + async ExitUsbMiner() { + await UsbDetect.stopMonitoring(); + } + GetUsbMiner() { + return this.devices; + } + getDetectMutex(port) { + var mutex = null; + for (var i = 0; i < this.detectMutexs.length; i++) { + if (this.detectMutexs[i].port === port) { + mutex = this.detectMutexs[i].mutex; + break; + } + } + if (!mutex) { + mutex = locks.createMutex(); + this.detectMutexs.push({ + port: port, + mutex: mutex + }); + } + return mutex; + } + async ListUsbMiner() { + var _this = this; + var UsbMiner = []; + + await UsbDetect.find(parseInt(_this.vendorId), parseInt(_this.productId), function(err, Devices) { + if (err) { + Debug.IbctLogErr(COMP, err); + return; + } + UsbMiner = Devices; + }) + + for (var i = 0; i < UsbMiner.length; i++) { + await _this.getUsbMinerPort(UsbMiner[i], 0, function (err, port) { + if (err) { + return; + } + _this.devices.push({ + devID: _this.id++, + port: port, + mutex: _this.getDetectMutex(port), + miner: UsbMiner[i] + }); + }); + } + } + + async getSerialPort(Device, callback) { + var portName = null; + await SerialPort.list(function (err, ports) { + if (err) { + callback(err); + return; + } + + ports.forEach(function (port) { + if (Device.serialNumber === port.serialNumber) { + portName = port.comName; + callback(null, portName); + return; + } + }) + callback('Cannot Find Port'); + }) + } + + async getUsbMinerPort(Device, Timeout, callback) { + var _this = this; + if (!Device) { + callback('Device Error') + return; + } + if (process.platform !== 'darwin') { + await this.getSerialPort(Device, callback); + } else { + // In darwin, serialport have some bug + if (!Timeout) + await this.getSerialPort(Device, callback); + else { + setTimeout(function() { + _this.getSerialPort(Device, callback); + }, Timeout); + } + } + } + hasExistUsbMiner(Device) { + return this.devices.some(function (dev) { + return JSON.stringify(dev.miner) === JSON.stringify(Device); + }); + } + hasUsbMiner() { + return ((this.devices.length > 0) ? true : false); + } + AddUsbMiner(Device) { + var _this = this; + var device = { + devID: _this.id++, + port: null, + mutex: null, + miner: Device + }; + _this.getUsbMinerPort(Device, 3000, function (err, port) { + if (err) { + return; + } + device.port = port; + device.mutex = _this.getDetectMutex(port); + _this.devices.push(device); + _this.emit('plug-in', device); + }) + } + RemoveUsbMiner(Device) { + var delDevice = {} + this.devices = this.devices.filter(function (dev) { + if (JSON.stringify(dev.miner) !== JSON.stringify(Device)) { + return true; + } else { + delDevice = dev; + return false; + } + }); + this.emit('plug-out', delDevice); + } +} + +module.exports = function DectectUsbMiner(options = {}) { + return new UsbMiner(options); +}; diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..1700041 --- /dev/null +++ b/src/index.js @@ -0,0 +1,113 @@ +const EventEmitter = require('events'); +const DetectUsbMiner = require('./detect'); +const Miner = require('./miner'); +const Debug = require('./log')(); +const I18n = require('i18n'); +const COMP = '[index]' + +class IbctMiner extends EventEmitter { + constructor({ + MinerParameters + }) { + super(); + var _this = this + + _this.initMiningLanguage(); + _this.detectUsbMiner = DetectUsbMiner({}); + _this.Miner = Miner({MinerParameters: MinerParameters}); + + _this.on("error", function(ID, data) { + Debug.IbctLogDbg(COMP, (ID !== undefined) ? "Miner " + ID + ":" + data : data) + }) + + _this.on("warning", function(ID, data) { + Debug.IbctLogDbg(COMP, (ID !== undefined) ? "Miner " + ID + ":" + data : data) + }) + + if (_this.Miner) { + _this.Miner.on("error", function (data, Device, ID) { + return _this.emit("error", ID, data); + }); + + _this.Miner.on("warning", function (data, Device, ID) { + return _this.emit("warning", ID, data); + }); + } + + _this.detectUsbMiner.on('plug-in', async (Device) => { + await _this.addMining(Device); + return _this.emit('plug-in', Device); + }) + _this.detectUsbMiner.on('plug-out', async (Device) => { + await _this.removeMining(Device); + return _this.emit('plug-out', Device); + }) + } + async exitMining() { + await this.detectUsbMiner.ExitUsbMiner(); + this.detectUsbMiner.removeAllListeners('plug-in'); + this.detectUsbMiner.removeAllListeners('plug-out'); + await this.stopMining(null); + await this.Miner.ExitMiner(); + this.Miner.removeAllListeners('error'); + this.Miner.removeAllListeners('warning'); + } + initMiningLanguage() { + I18n.configure({ + locales: ['en', 'zh'], + staticCatalog: { + en: require('./translate/en.json'), + zh: require('./translate/zh.json') + }, + register: global + }); + I18n.setLocale('zh'); + } + setMiningLanguage(m) { + I18n.setLocale(m); + } + async initMining() { + await this.detectUsbMiner.ListUsbMiner(); + await this.addMining(); + } + async startMining(Device) { + return (Device ? await this.Miner.EnableMiner(Device) : await this.Miner.EnableMiners(this.listDevices())); + } + async stopMining(Device) { + return (Device ? await this.Miner.DisableMiner(Device) : await this.Miner.DisableMiners(this.listDevices())); + } + async addMining(Device) { + return (Device ? await this.Miner.AddMiner(Device) : await this.Miner.AddMiners(this.listDevices())); + } + async connectMining(Device) { + return (Device ? await this.Miner.connectMiner(Device) : await this.Miner.connectMiners(this.listDevices())); + } + async removeMining(Device) { + return (Device ? await this.Miner.RemoveMiner(Device) : await this.Miner.RemoveMiners(this.listDevices())); + } + setMiningConfig(setName, cryptoname, settings) { + if (!setName || !cryptoname || !settings) + return + + return this.Miner.SetMinerConfig(setName, cryptoname, settings) + } + getMiningStatus(Device) { + return (Device ? this.Miner.GetMinerStatus(Device) : this.Miner.GetMinersStatus(this.listDevices())); + } + RebootMining(Device) { + return (Device ? this.Miner.RebootMiner(Device) : this.Miner.RebootMiners(this.listDevices())); + } + SetMiningLed(Device, Enable) { + return (Device ? this.Miner.SetMinerLed(Device, Enable) : this.Miner.SetMinersLed(this.listDevices(), Enable)); + } + async BurnMiningFirmware(Device, Image, Callback) { + return (Device ? await this.Miner.BurnMinerFirmware(Device, Image, Callback) : await this.Miner.BurnMinersFirmware(this.listDevices(), Image, Callback)); + } + listDevices() { + return this.detectUsbMiner.GetUsbMiner(); + } +} + +module.exports = function GetIbctMiner(options = {}) { + return new IbctMiner(options); +}; diff --git a/src/log.js b/src/log.js new file mode 100644 index 0000000..c939df8 --- /dev/null +++ b/src/log.js @@ -0,0 +1,111 @@ +const EventEmitter = require('events'); +const fs = require('fs'); +const IBCT_NONE = -1; +const IBCT_ERR = 0; +const IBCT_DBG = 1; +const IBCT_INFO = 2; +const IBCT_ALL = 3; +const USER_HOME = process.env.HOME || process.env.USERPROFILE; + +class Log extends EventEmitter { + constructor({}) { + super(); + var _this = this; + + global.loglevel = IBCT_DBG; + _this.SaveLevel = IBCT_ERR; + _this.timeEn = true; + _this.curTime = (new Date()).getDate(); + _this.logSavePath = this.GetFileName(); + // _this.logSavePath = null; + } + IbctSetLogLevel(level) { + global.loglevel = level + } + GetFileName() { + return USER_HOME+'/usb_log_' + (new Date()).toLocaleDateString().replace(/\//g, "-") + '.txt'; + } + IbctOpen(Path) { + return fs.openSync(Path, 'a'); + } + IbctWrite(fd, str) { + return fs.writeSync(fd, str); + } + IbctClose(fd) { + fs.closeSync(fd); + } + IbctSaveLog(Path, str) { + var fd; + var now; + + if (!Path) + return; + + now = (new Date()).getDate(); + if (now !== this.curTime) { + Path = this.GetFileName(); + this.curTime = now; + this.logSavePath = Path; + } + + fd = this.IbctOpen(Path); + if (!fd) + return; + this.IbctWrite(fd, str); + this.IbctClose(fd); + } + GetString(strs) { + var buf = this.timeEn ? (new Date()).toLocaleString() : ''; + for (var i = 0; i < strs.length; i++) { + buf += ' ' + strs[i]; + } + return buf; + } + IbctLogDbg() { + var buf = null; + + if (global.loglevel >= IBCT_DBG) { + buf = this.GetString(arguments); + console.log(buf); + } + + if (this.SaveLevel >= IBCT_DBG) { + if (buf === null) { + buf = this.GetString(arguments); + } + this.IbctSaveLog(this.logSavePath, buf + '\n'); + } + } + IbctLogErr() { + var buf = null; + if (global.loglevel >= IBCT_ERR) { + buf = this.GetString(arguments); + console.log(buf); + } + + if (this.SaveLevel >= IBCT_ERR) { + if (buf === null) { + buf = this.GetString(arguments); + } + this.IbctSaveLog(this.logSavePath, buf + '\n'); + } + } + IbctLogInfo() { + var buf = null; + if (global.loglevel >= IBCT_INFO) { + buf = this.GetString(arguments); + console.log(buf); + } + + if (this.SaveLevel >= IBCT_INFO) { + if (buf === null) { + buf = this.GetString(arguments); + } + this.IbctSaveLog(this.logSavePath, buf + '\n'); + } + } +} + +module.exports = function GetLog(options = {}) { + return new Log(options); +}; \ No newline at end of file diff --git a/src/miner.js b/src/miner.js new file mode 100644 index 0000000..c9681eb --- /dev/null +++ b/src/miner.js @@ -0,0 +1,1352 @@ +const EventEmitter = require('events'); +const uuid = require("uuid"); +const Proxy = require('./proxy/build'); +const Queue_1 = require('./proxy/build/Queue'); +const cryptocurrency = require('./cryptocurrency/cryptocurrency'); +const Miner_1 = require('./miner/minerApi'); +const Algo_1 = require('./algo/algo'); +const Debug = require('./log')(); +const Ping = require('./ping'); +const crc32 = require('crc32'); +const Q = require('bluebird'); +const locks = require('locks'); +const SN = require('./sn')(); +const COMP = '[Miner]'; + +var __assign = (this && this.__assign) || Object.assign || function (t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) + if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; +}; + +class Miner extends EventEmitter { + constructor({ + MinerParameters + }) { + super(); + var _this = this + + _this.MinerSupport = MinerParameters; + _this.hwPoolMutex = locks.createMutex(); + _this.RunningMiner = []; + _this.checkNet = null; + _this.unlink = 0; + _this.unlinkShutdown = false; + _this.netThread() + _this.on('error', function (error, Device) { + if (typeof error === 'object') + Debug.IbctLogErr(COMP, "error:", JSON.stringify(error)); + else + Debug.IbctLogErr(COMP, "error:", error); + + if (Array.isArray(Device)) { + _this.DisableMiners(Device); + } else if (Device) { + _this.DisableMiner(Device); + } + }); + _this.on('warning', function (error) { + Debug.IbctLogDbg(COMP, error); + }); + } + + getPools() { + var pools = []; + + for (var i = 0; i < this.MinerSupport.length; i++) { + if (this.MinerSupport[i].pool !== null) { + pools.push(this.MinerSupport[i].pool); + } + } + return pools; + } + + getMinerParameter(Device, name) { + if (!Device) + return null + if (Device.dev.miningParameter !== null) + return Device.dev.miningParameter[name]; + else + return null; + } + + startMiner(Device) { + var _this = this + if (_this.unlinkShutdown) { + setTimeout(function() { + _this.startMiner(Device); + }, 1000); + return + } + _this.EnableMiner(Device); + } + + restartMiner(Device) { + var state = this.GetMinerRunningState(Device) + var status = this.GetMinerRunningStatus(Device) + var _this = this + if (state === 'miner' && !status) { + setTimeout(function() { + _this.restartMiner(Device); + }, 1000); + return + } + + if (_this.unlinkShutdown) { + Q.try(async () => { + await _this.DisableMiner(Device); + _this.startMiner(Device); + }) + return + } + + Q.try(async () => { + await this.DisableMiner(Device); + await this.EnableMiner(Device); + }) + } + + netThread() { + var _this = this; + var pools = null; + + pools = _this.getPools(); + if (!pools.length) return; + + if (_this.checkNet) { + clearInterval(_this.checkNet); + _this.checkNet = null; + _this.unlink = 0; + _this.unlinkShutdown = false; + } + + _this.checkNet = setInterval(function () { + _this.netPing(pools, function (err) { + if (err) { + _this.unlink++; + } else { + _this.unlink = 0; + if (_this.unlinkShutdown) { + _this.unlinkShutdown = false; + _this.emit("warning", __('网络重新连通')); + } + } + if (_this.unlink > 2) { + _this.unlink = 0; + if (!_this.unlinkShutdown) { + _this.unlinkShutdown = true; + _this.emit("error", __('网络失去连接'), null); + } + } + }) + }, 12000); + } + + netPing(pools, callback) { + var total = 0; + var wr = 0; + pools.forEach(pool => { + Ping.ping({ + address: pool.host, + port: pool.port, + attempts: 1, + timeout: 2000 + }, function (error, target) { + if (error) { + Debug.IbctLogDbg(COMP, target + ": " + error.toString()); + // callback(target + ": " + error.toString()) + wr++; + } + total++; + if (total === pools.length) + callback((wr === pools.length) ? error.toString() : null); + }) + }) + } + + async ExitMiner() { + + } + + proxyConnect(Device) { + var _this = this; + var pool = _this.getMinerParameter(Device, 'pool'); + var user = pool.user; + if (pool.user.includes('.')) { + user += ('_' + Device.dev.sn); + } else { + user += ('.' + Device.dev.sn); + } + if (Device.dev.blacklist > 0) { + user += '_bk' + Device.dev.blacklist.toString() + } + + Device.dev.Proxy = new Proxy({ + host: pool.host, + port: pool.port, + user: user, + pass: pool.pass, + protocolname: _this.getMinerParameter(Device, 'protocolname'), + cryptoname: _this.getMinerParameter(Device, 'cryptoname') + }); + + Device.dev.Proxy.on("open", function (data) { + Debug.IbctLogInfo(COMP, 'Proxy open'); + return _this.emit("open", data); + }); + Device.dev.Proxy.on("authed", function (data) { + Debug.IbctLogInfo(COMP, 'Proxy authed'); + return _this.emit("authed", data); + }); + Device.dev.Proxy.on("job", function (data) { + Debug.IbctLogInfo(COMP, 'Proxy job'); + return _this.emit("job", data); + }); + Device.dev.Proxy.on("found", function (data) { + Debug.IbctLogInfo(COMP, 'Proxy found'); + return _this.emit("found", data); + }); + Device.dev.Proxy.on("accepted", function (data) { + Debug.IbctLogInfo(COMP, 'Proxy accepted'); + return _this.emit("accepted", data); + }); + Device.dev.Proxy.on("rejected", function (data) { + Debug.IbctLogInfo(COMP, 'Proxy rejected'); + return _this.emit("rejected", data); + }); + Device.dev.Proxy.on("close", function (data) { + Debug.IbctLogInfo(COMP, 'Proxy close'); + return _this.emit("close", data); + }); + Device.dev.Proxy.on("error", function (data) { + var stringData = data; + if (typeof data === 'object') { + stringData = JSON.stringify(data) + } + if (stringData.indexOf('You are blacklisted') >= 0) { + // blacklisted + Device.dev.blacklist++; + } + setTimeout(function() { + _this.restartMiner(Device) + }, 1000); + + return _this.emit("error", data, null, Device.devID); + }); + } + + proxyKill(Device) { + Device.dev.Proxy.removeAllListeners("open"); + Device.dev.Proxy.removeAllListeners("authed"); + Device.dev.Proxy.removeAllListeners("job"); + Device.dev.Proxy.removeAllListeners("found"); + Device.dev.Proxy.removeAllListeners("accepted"); + Device.dev.Proxy.removeAllListeners("rejected"); + Device.dev.Proxy.removeAllListeners("close"); + Device.dev.Proxy.removeAllListeners("error"); + Device.dev.Proxy.kill(); + } + + version_compare(version1, version2) { + let v1 = version1.split('.'); + let v2 = version2.split('.'); + let r0 = parseInt(v1[0]); + let r1 = parseInt(v1[1]); + let r2 = parseInt(v1[2]); + let s = r0 * 100 + r1 * 10 + r2; + r0 = parseInt(v2[0]); + r1 = parseInt(v2[1]); + r2 = parseInt(v2[2]); + let d = r0 * 100 + r1 * 10 + r2; + if (s === d) + return 0; + else if (s < d) + return 1; + else + return 0; + } + + async controlMinerSN(Device) { + var minerInfo = this.GetMinerInfo(Device); + var _this = this; + var ret; + return new Promise(async (resolve)=> { + if (Device.dev.sn) { + resolve(0); + return + } + Device.dev.workDepth = minerInfo.workDepth; + Device.dev.sn = minerInfo.sn; + if (Device.dev.sn) { + resolve(0); + return + } + + if (minerInfo.modelName === 'simplenode') { + ret = _this.version_compare(minerInfo.firmwareVer, '0.0.9'); + if (ret) { + resolve(0); + _this.emit("warning", __('此矿机版本过低, 请先升级'), null, Device.dev.DevID); + Device.dev.sn = Device.devID; + return + } + } + + Device.dev.sn = await SN.GetSN(minerInfo.snAbbr); + if (!Device.dev.sn) { + resolve(1); + return + } + ret = await _this.BurnMinerSNInfo(Device, { sn: Device.dev.sn }); + resolve(ret ? 1 : 0) + }) + } + async findMiner(Device, callback) { + var _this = this; + var ret; + var i, j; + + for (i = 0; i < _this.MinerSupport.length; i++) { + var minernames = _this.MinerSupport[i].minername + Device.dev.algorithm = Algo_1({name: _this.MinerSupport[i].algoname}); + Device.dev.crypto = cryptocurrency({name: _this.MinerSupport[i].cryptoname}); + for (j = 0; j < minernames.length; j++) { + Device.dev.miner = Miner_1({ + name: minernames[j], + devPath: Device.port, + algo: Device.dev.algorithm, + varity: 0, + crypto: Device.dev.crypto + }); + ret = await this.DetectMiner(Device, minernames[j]); + if (!ret) { + Debug.IbctLogDbg('Find Miner:', minernames[j]); + Device.dev.miningName = minernames[j]; + Device.dev.miningParameter = _this.MinerSupport[i]; + break; + } else + await _this.ReleaseMiner(Device); + + if (ret === 2) { + // can not connect with miner, so reboot it + Debug.IbctLogErr('Connect error when find miner'); + Device.dev.miner = Miner_1({ + name: 'unknow', + devPath: Device.port, + algo: Device.dev.algorithm, + varity: 0, + crypto: Device.dev.crypto + }); + Device.dev.miningName = 'unknow'; + break; + } + } + if (!ret || ret === 2) + // connect error or have found + break; + } + + if (i === _this.MinerSupport.length) { + Debug.IbctLogErr('Error to find Miner'); + Device.dev.miner = Miner_1({ + name: 'unknow', + devPath: Device.port, + algo: Device.dev.algorithm, + varity: 0, + crypto: Device.dev.crypto + }); + Device.dev.miningName = 'unknow'; + } + + Device.dev.miner.on("error", function (data) { + return _this.emit("error", data, Device, Device.devID); + }) + Device.dev.miner.on("warning", function (data) { + return _this.emit("warning", data, Device, Device.devID); + }) + if (Device.dev.miner) { + callback(Device); + } else { + callback(Device, 'Error to find Miner'); + } + } + async InitMiner(Device) { + return Device.dev.miner ? await Device.dev.miner.init() : null; + } + GetMinerInfo(Device) { + return Device.dev.miner ? Device.dev.miner.getInfo() : null; + } + SetMinerDevice(Device) { + return Device.dev.miner ? Device.dev.miner.setDevice() : null; + } + MinerScanWork(Device, work, callback) { + return Device.dev.miner ? Device.dev.miner.scanWork(work, callback) : null; + } + async DetectMiner(Device, DevName) { + return Device.dev.miner ? await Device.dev.miner.detect(DevName) : null; + } + async BurnMinerSNInfo(Device, ptInfo) { + return Device.dev.miner ? await Device.dev.miner.burnSNInfo(ptInfo) : null; + } + StopMiner(Device) { + return Device.dev.miner ? Device.dev.miner.stop(false) : null; + } + async ReleaseMiner(Device) { + return Device.dev.miner ? await Device.dev.miner.release() : null; + } + SetMinerLedStatus(Device, Enable) { + return Device.dev.miner ? Device.dev.miner.setLed(Enable) : null; + } + RebootHWMiner(Device) { + return Device.dev.miner ? Device.dev.miner.reboot() : null; + } + UpdateMinerImage(Device, Image, Callback) { + return Device.dev.miner ? Device.dev.miner.updateImage(Image, Callback) : null; + } + GetMinerState(Device) { + return Device.dev.miner ? Device.dev.miner.getState() : null; + } + stopScanWork(Device) { + return Device.dev.miner ? Device.dev.miner.stopScanWork() : null; + } + PoolLogin(Device) { + Device.dev.pool.handleMessage({ + type: 'auth', + params: { + site_key: null, + user: null + } + }); + } + PoolSubmit(Device, nonce, hash) { + Device.dev.pool.handleMessage({ + type: 'submit', + params: Device.dev.crypto.getSubmitParams(Device, nonce.toString('hex'), hash) + }); + } + PutNonceToPoolQueue(Device) { + var work = Device.dev.work; + Device.dev.hwPoolQueue.push({ + job_id: work.job_id, + difficulty: work.difficulty + }); + } + CleanPoolQueue(Device, submit) { + var dev = Device.dev; + dev.hwPoolQueue = dev.hwPoolQueue.filter(function (data) { + if (data.job_id === submit.job_id) { + return false; + } else { + return true; + } + }); + } + CleanAllPoolQueue(Device) { + if (Device.dev.hwPoolQueue.length) { + Device.dev.hwPoolQueue.splice(0, Device.dev.hwPoolQueue.length); + } + } + CheckNonce(Device, nonce) { + var hwTarget = Device.dev.work.hwTarget ? Device.dev.work.hwTarget : Device.dev.work.target; + var target = Device.dev.work.target; + var hash = null; + hash = Device.dev.crypto.calHash(Device, nonce); + if (Device.dev.crypto.checkHash(hwTarget, hash)) { + if (Device.dev.crypto.checkHash(target, hash)) { + // Device.dev.minerstatus.txtotal++; + this.PutNonceToPoolQueue(Device); + this.PoolSubmit(Device, nonce, hash); + } + return true + } else { + Device.dev.minerstatus.hardwareErr++; + Debug.IbctLogDbg(COMP, 'HardwareErr: nonce', nonce.toString('hex'), '; hwTarget', hwTarget.toString('hex'), '; calTarget', hash.toString('hex')); + return false + } + } + setMinerTargetByWork(Device, Work) { + var minerstatus = Device.dev.minerstatus; + if (Work.hwdifficulty) + return; + + minerstatus.target = Work.hwTarget ? Work.hwTarget : Work.target; + minerstatus.target = minerstatus.target.toString('hex'); + Work.hwdifficulty = Device.dev.crypto.targetToDiff(minerstatus.target); + minerstatus.difficulty = Work.hwdifficulty; + } + GetTime() { + return Math.floor(((new Date())).getTime() / 1000); + } + ConvertUnion(Hashrate) { + if (Hashrate < 1000) + return Hashrate.toFixed(2) + ' H/s'; + else if (Hashrate < 1000000) + return (Hashrate / 1000).toFixed(2) + ' kH/s'; + else if (Hashrate < 1000000000) + return (Hashrate / 1000000).toFixed(2) + ' MH/s'; + else + return (Hashrate / 1000000000).toFixed(2) + ' GH/s'; + } + updateMinerAvHashrate(Device, onlyCal) { + var dev = Device.dev; + var minerstatus = dev.minerstatus; + var curtime = this.GetTime(); + + if (onlyCal) { + minerstatus.avHashrate = this.ConvertUnion(dev.hwCal / (curtime - dev.stime)); + return; + } + + if (!minerstatus.difficulty) + return; + + dev.hwCal += minerstatus.difficulty; + minerstatus.avHashrate = this.ConvertUnion(dev.hwCal / (curtime - dev.stime)); + } + updateMinerPoolHashrate(Device, submit) { + var minerstatus = Device.dev.minerstatus; + var hwPool = Device.dev.hwPool; + var _this = this; + var allTime = 0; + var allhash = 0; + var times = 0; + var newHash = null; + var i = 0; + + Device.dev.hwPoolQueue = Device.dev.hwPoolQueue.filter(function (data) { + if (!submit || data.job_id === submit.job_id) { + newHash = { + difficulty: submit ? data.difficulty : 0, + time: _this.GetTime() + }; + + _this.hwPoolMutex.lock(async () => { + allhash = 0; + hwPool.push(newHash); + minerstatus.share += data.difficulty; + Device.dev.hwPool = hwPool.filter(function (dev) { + times = newHash.time - dev.time; + i++; + if (times > 900) { + return false; + } else { + if (times > allTime) + allTime = times; + allhash += dev.difficulty; + return true; + } + }) + + times = newHash.time - Device.dev.stime; + if (times < 40) { + minerstatus.plHashrate = '0 H/s' + } else { + if (_this.getMinerParameter(Device, 'cryptoname') === 'xmr') { + minerstatus.plHashrate = _this.ConvertUnion(allhash / allTime); + }else{ + minerstatus.plHashrate = _this.ConvertUnion(allhash * 0x100000000 / allTime); + } + } + _this.hwPoolMutex.unlock(); + }) + + return false; + } else { + return true; + } + }); + } + updateMinerInstantHashrate(Device, onlyCal) { + var hwInstant = Device.dev.hwInstant; + var minerstatus = Device.dev.minerstatus; + var work = Device.dev.work; + var newHash = { + difficulty: onlyCal ? 0 : work.hwdifficulty, + time: this.GetTime() + }; + var allTime = 0; + var allhash = 0; + var times = 0; + + hwInstant.push(newHash); + Device.dev.hwInstant = hwInstant.filter(function (dev) { + times = newHash.time - dev.time; + if (times > 360) { + return false; + } else { + if (times > allTime) + allTime = times; + allhash += dev.difficulty; + return true; + } + }) + times = newHash.time - Device.dev.stime; + if (times < 40) { + minerstatus.hashrate = '0 H/s' + } else { + minerstatus.hashrate = this.ConvertUnion(allhash / allTime); + } + } + updateMinerRate(Device) { + this.updateMinerAvHashrate(Device, false); + this.updateMinerInstantHashrate(Device, false); + } + updateMinerTemperature(Device) { + var minerstatus = Device.dev.minerstatus; + var minerstate = this.GetMinerState(Device); + if(minerstate.rpm) + minerstatus.rpm = minerstate.rpm; + minerstatus.temperatue = minerstate.temp.toString() + ' ℃'; + } + DumpMinerStatus(Device, ms) { + var _this = this; + Device.dev.dump = setInterval(function () { + _this.updateMinerTemperature(Device); + Debug.IbctLogDbg(COMP, JSON.stringify(Device.dev.minerstatus)); + }, ms); + } + DisableDumpMinerStatus(Device) { + if (Device.dev.dump) { + clearInterval(Device.dev.dump); + } + } + MinerPutJob(Device, data) { + Device.dev.jobQueue.push(data); + } + MinerCleanJob(Device) { + if (Device.dev.jobQueue.length < 4) return + for (var i = 0; i < (Device.dev.jobQueue.length - 4); i++) { + Device.dev.jobQueue.shift(); + } + } + MinerCleanAllJob(Device) { + if (Device.dev.jobQueue.length) { + Device.dev.jobQueue.splice(0, Device.dev.jobQueue.length); + } + } + MinerGetJob(Device) { + return Device.dev.jobQueue.pop(); + } + MinerThread(Device) { + var _this = this; + var job = {}; + + + if (!_this.GetMinerRunningStatus(Device)) { + return; + } + + if(Device.dev.minerTimeout === true) { + //Debug.IbctLogDbg(COMP, 'Miner start with Old Job...' ); + job = Device.dev.curJob; + } else { + //Debug.IbctLogDbg(COMP, 'Miner start with New Job...' ); + job = _this.MinerGetJob(Device); + if (job === undefined) { + setTimeout(function () { + _this.MinerThread(Device); + }, 100); + return; + } + } + Debug.IbctLogDbg(COMP, 'Miner start with ', Device.dev.minerTimeout === true?'Old Job':'New Job'); + + Device.dev.curJob = job; + for(let i=0; i 100) + Device.dev.submitResult.shift(); + } + checkSubmitResult(Device) { + var res = 0; + + if (!Device.dev.submitResult.length) + return false; + + for (var i = 0; i < Device.dev.submitResult.length; i++) + res += Device.dev.submitResult[i]; + + return (res > 30) ? true : false; + } + cleanAllSubmitResult(Device) { + if (Device.dev.submitResult.length) { + Device.dev.submitResult.splice(0, Device.dev.submitResult.length); + } + } + errRelease(Device) { + var _this = this; + Debug.IbctLogDbg(COMP, 'errRelease'); + + _this.proxyKill(Device); + Device.dev.poolQueue.stop(); + _this.DisableDumpMinerStatus(Device); + _this.CleanAllPoolQueue(Device); + _this.MinerCleanAllJob(Device); + _this.cleanAllSubmitResult(Device); + _this.SetMinerRunningState(Device, 'standy'); + Device.dev.poolQueue.removeAllListeners("job:" + Device.dev.id); + Device.dev.poolQueue.removeAllListeners("subscribe:" + Device.dev.id); + Device.dev.poolQueue.removeAllListeners("diff:" + Device.dev.id); + Device.dev.poolQueue.removeAllListeners("authed:" + Device.dev.id); + Device.dev.poolQueue.removeAllListeners("accepted:" + Device.dev.id); + Device.dev.poolQueue.removeAllListeners("rejected:" + Device.dev.id); + Device.dev.poolQueue.removeAllListeners("error:" + Device.dev.id); + } + async StartMinerThread(Device, done) { + var _this = this; + + Debug.IbctLogDbg(COMP, 'StartMinerThread'); + if (_this.GetMinerRunningStatus(Device)) { + return; + } + _this.DumpMinerStatus(Device, 5000); + Device.dev.stime = _this.GetTime(); + Device.dev.hwCal = 0; + Device.dev.poolQueue.start(); + _this.proxyConnect(Device); + _this.SetMinerRunningState(Device, 'miner'); + Device.dev.pool = Device.dev.Proxy.createProxy(Device.dev.id, Device.dev.poolQueue); + Device.dev.poolQueue.on("subscribe:" + Device.dev.id, function (data) { + if (_this.getMinerParameter(Device, 'protocolname') === 'stratum') { + Device.dev.crypto.stratum_subscribe(data); + } else { + Device.dev.crypto.jsonrpc_subscribe(data); + } + }) + Device.dev.poolQueue.on("diff:" + Device.dev.id, function (data) { + if (_this.getMinerParameter(Device, 'protocolname') === 'stratum') { + Device.dev.crypto.stratum_diff(data); + } else { + Device.dev.crypto.jsonrpc_diff(data); + } + }) + + Device.dev.poolQueue.on("job:" + Device.dev.id, function (data) { + var packet = null; + if (_this.getMinerParameter(Device, 'protocolname') === 'stratum') { + packet = Device.dev.crypto.stratum_notify(data); + } else { + packet = Device.dev.crypto.jsonrpc_notify(data); + } + // Debug.IbctLogInfo(COMP, 'job poolQueue: ', packet); + if (packet.clean) { + _this.stopScanWork(Device) + } + _this.MinerPutJob(Device, packet); + _this.MinerCleanJob(Device); + }) + Device.dev.poolQueue.on("authed:" + Device.dev.id, function (data) { + Debug.IbctLogDbg(COMP, 'authed poolQueue: ', data); + Device.dev.poolId = data.auth; + }) + Device.dev.poolQueue.on("accepted:" + Device.dev.id, function (data) { + Debug.IbctLogDbg(COMP, 'accepted poolQueue: ', JSON.stringify(data.nonce)); + _this.setSubmitResult(Device, 0); + Device.dev.minerstatus.accepted++; + _this.updateMinerPoolHashrate(Device, data.nonce) + }) + Device.dev.poolQueue.on("rejected:" + Device.dev.id, function (data) { + Debug.IbctLogDbg(COMP, 'rejected poolQueue: ', JSON.stringify(data.nonce)); + Debug.IbctLogDbg(COMP, 'rejected poolQueue: ', JSON.stringify(data.err)); + _this.CleanPoolQueue(Device, data.nonce); + _this.setSubmitResult(Device, 1); + Device.dev.minerstatus.rejected++; + if (data.err && data.err.message && data.err.message.indexOf('You are in blacklist') >= 0) { + Device.dev.blacklist++; + _this.restartMiner(Device); + _this.emit("error", __('矿机进入黑名单状态,将换用户名重启'), null, Device.devID); + } else if (data.err && (data.err instanceof Array) && data.err[1].indexOf('You are in blacklist') >= 0) { + Device.dev.blacklist++; + _this.emit("error", __('矿机进入黑名单状态,将换用户名重启'), null, Device.devID); + } + + if (_this.checkSubmitResult(Device)) { + _this.restartMiner(Device); + _this.emit("error", __('矿机rejected过多,将重启'), null, Device.devID); + } + }) + Device.dev.poolQueue.on("error:" + Device.dev.id, function (data) { + Debug.IbctLogDbg(COMP, 'error poolQueue: ', data); + }) + + var ret = await _this.InitMiner(Device); + if (ret) { + _this.emit("error", __('初始化矿机失败'), null, Device.devID); + _this.errRelease(Device); + return false; + } + _this.PoolLogin(Device); + // for MinerThread + _this.SetMinerRunningStatus(Device, true); + _this.MinerThread(Device); + return true + } + async StopMinerThread(Device, flags) { + var _this = this; + Debug.IbctLogDbg(COMP, 'StopMinerThread'); + if (!_this.GetMinerRunningStatus(Device)) { + return; + } + + _this.proxyKill(Device); + Device.dev.poolQueue.stop(); + if (flags) { + Device.dev.miner.removeAllListeners("error"); + Device.dev.miner.removeAllListeners("warning"); + await _this.ReleaseMiner(Device); + } else { + await _this.StopMiner(Device); + } + _this.DisableDumpMinerStatus(Device); + _this.SetMinerRunningState(Device, 'standy'); + _this.CleanAllPoolQueue(Device); + _this.MinerCleanAllJob(Device); + _this.cleanAllSubmitResult(Device); + Device.dev.poolQueue.removeAllListeners("job:" + Device.dev.id); + Device.dev.poolQueue.removeAllListeners("subscribe:" + Device.dev.id); + Device.dev.poolQueue.removeAllListeners("diff:" + Device.dev.id); + Device.dev.poolQueue.removeAllListeners("authed:" + Device.dev.id); + Device.dev.poolQueue.removeAllListeners("accepted:" + Device.dev.id); + Device.dev.poolQueue.removeAllListeners("rejected:" + Device.dev.id); + Device.dev.poolQueue.removeAllListeners("error:" + Device.dev.id); + } + HasExistMiner(Device) { + return this.RunningMiner.some(function (dev) { + return dev.devID === Device.devID; + }); + } + GetMinerByDevID(id) { + var device = null; + + this.RunningMiner.forEach(function (Dev) { + if (Dev.devID === id) + device = Dev; + }); + + return device; + } + GetMinerRunningStatus(Device) { + return this.RunningMiner.some(function (Dev) { + if (Dev.devID === Device.devID) + return Dev.enable; + }); + } + SetMinerRunningStatus(Device, status) { + this.RunningMiner.forEach(function (Dev, index) { + if (Dev.devID === Device.devID) { + Dev.enable = status; + // set minerstatus state + Dev.dev.minerstatus.state = status ? 'on' : 'off'; + } + }) + } + GetMinerRunningState(Device) { + var status = null; + this.RunningMiner.forEach(function (Dev) { + if (Dev.devID === Device.devID) + status = Dev.status; + }); + return status; + } + SetMinerRunningState(Device, status) { + this.RunningMiner.forEach(function (Dev, index) { + if (Dev.devID === Device.devID) { + Dev.status = status; + } + }) + } + minuteToString(s) { + var d = Math.floor(s / 86400); + s %= 86400; + var h = Math.floor(s / 3600); + s %= 3600; + var m = Math.floor(s / 60); + return d + 'd ' + h + 'h ' + m + 'm'; + } + + GetMinerStatus(Device) { + var status = { + devID: 0, + miningName: null, + miningSN: null, + comm: null, + state: 'off', + version: '1.0.0', + miningType: 'ltc', + hashrate: '0 KH/s', + avHashrate: '0 KH/s', + plHashrate: '0 KH/s', + share: 0, + hardwareErr: 0, + rejected: 0, + nonces: 0, + accepted: 0, + temperatue: '0 ℃', + elapsed: '0d 0h 0m', + pools: null + }; + var dev = null; + var minerInfo = null; + var minerstatus = null; + dev = this.GetMinerByDevID(Device.devID); + if (!dev) + return null; + status.comm = dev.port; + status.devID = dev.devID; + status.miningType = this.getMinerParameter(dev, 'cryptoname'); + status.miningSN = dev.dev.sn; + minerInfo = this.GetMinerInfo(dev); + if (minerInfo) { + status.miningName = minerInfo.modelName; + status.version = minerInfo.firmwareVer; + } + if (this.GetMinerRunningStatus(dev)) { + minerstatus = dev.dev.minerstatus; + status.state = minerstatus.state; + this.updateMinerInstantHashrate(dev, true); + status.hashrate = minerstatus.hashrate; + this.updateMinerAvHashrate(dev, true); + status.avHashrate = minerstatus.avHashrate; + this.updateMinerPoolHashrate(dev, null); + status.plHashrate = minerstatus.plHashrate; + status.share = minerstatus.share; + status.accepted = minerstatus.accepted; + status.rejected = minerstatus.rejected; + status.hardwareErr = minerstatus.hardwareErr; + status.nonces = minerstatus.total; + status.temperatue = minerstatus.temperatue; + status.rpm = minerstatus.rpm; + // status.elapsed = this.minuteToString(this.GetTime() - dev.dev.stime) + '_' + dev.dev.jobQueue.length.toString() + '_' + dev.dev.hwPoolQueue.length.toString() + '_' + dev.dev.hwInstant.length.toString(); + status.elapsed = this.minuteToString(this.GetTime() - dev.dev.stime); + status.pools = this.getMinerParameter(dev, 'pool'); + } + return status; + } + GetMinersStatus(Devices) { + var status = []; + var temp = null; + var _this = this; + + Devices.forEach(function (Dev, index) { + temp = _this.GetMinerStatus(Dev); + if (temp) { + status.push(temp); + } + }) + return status; + } + async EnableMiner(Device) { + var _this = this; + var dev = null; + var ret; + + dev = _this.GetMinerByDevID(Device.devID); + if (!dev) + return; + + if (_this.getMinerParameter(dev, 'pool') === null) { + this.emit("error", __('开始挖矿前,请先设置矿池')); + return; + } + + if (dev.dev.miningName === 'unknow') + return; + + dev.dev.mutex.lock(async () => { + if (dev && !_this.GetMinerRunningStatus(dev)) { + // mining or burn status, need not start to mine + if (_this.GetMinerRunningState(dev) === 'burn' || _this.GetMinerRunningState(dev) === 'miner') + return; + + ret = await _this.StartMinerThread(dev); + if (ret) + _this.SetMinerRunningStatus(dev, true); + } + dev.dev.mutex.unlock(); + }) + } + async EnableMiners(Devices) { + var dev = null; + for (var i = 0; i < Devices.length; i++) { + dev = this.GetMinerByDevID(Devices[i].devID); + if (!dev) + continue; + + if (this.getMinerParameter(dev, 'pool') === null) { + this.emit("error", __('开始挖矿前,请先设置矿池')); + return; + } + } + + for (var i = 0; i < Devices.length; i++) { + await this.EnableMiner(Devices[i]); + } + } + SetMinerConfig(setName, cryptoname, settings) { + var alive = false; + var i; + if (setName === 'pool') { + for (i = 0; i < this.RunningMiner.length; i++) { + if (this.GetMinerRunningStatus(this.RunningMiner[i]) && + this.getMinerParameter(this.RunningMiner[i], 'cryptoname') === cryptoname) { + alive = true; + break; + } + } + if (alive) { + this.emit("error", __('设置矿池前,请先停止挖矿')); + return; + } else { + for (i = 0; i < this.MinerSupport.length; i++) { + if (this.MinerSupport[i].cryptoname === cryptoname) { + this.MinerSupport[i].pool = settings; + break; + } + } + this.netThread(); + } + } + } + async DisableMiner(Device) { + var _this = this; + var dev = null; + + dev = _this.GetMinerByDevID(Device.devID); + if (!dev) + return + + if (!dev.dev.miningName === 'unknow') + return + dev.dev.mutex.lock(async () => { + if (dev && _this.GetMinerRunningStatus(dev)) { + await _this.StopMinerThread(dev, false); + _this.SetMinerRunningStatus(dev, false); + } + dev.dev.mutex.unlock(); + }) + } + async DisableMiners(Devices) { + for (var i = 0; i < Devices.length; i++) { + await this.DisableMiner(Devices[i]); + } + } + async AddMiner(Device) { + var _this = this; + return new Promise(function (resolve, reject) { + Device.mutex.lock(async () => { + if (!_this.HasExistMiner(Device)) { + var Dev = __assign({}, Device, { + enable: false, + dev: { + // miner name + miningName: null, + // miner parameter + miningParameter: null, + // miner id + id: uuid.v4(), + // miner status: standby; miner; burn + status: 'standy', + // miner SN + sn: null, + // for blacklist + blacklist: 0, + // mutex + mutex: locks.createMutex(), + // pool set id to miner + poolId: 0, + // proxy + Proxy: null, + // connected pool + pool: null, + // communication with proxy + poolQueue: new Queue_1.default(), + // job queue + jobQueue: [], + //currrent job + curJob:null, + //minerTimeout + minerTimeout:false, + // miner device + miner: null, + // miner algorithm + algorithm: null, + // crypto + crypto: null, + // current work + work: null, + // device workDepth + workDepth:1, + // job queue + workQueue: [], + // setInterval + dump: null, + // rejected + submitResult: [], + // start run time + stime: 0, + // Chip calulate number for avhashrate + hwCal: 0, + // 20s hashrate + hwInstant: [], + // pool 15min hw hashrate + hwPool: [], + // pool nonce id&difficult queue + hwPoolQueue: [], + // miner status + minerstatus: { + state: 'off', + target: 0, + difficulty: 0, + share: 0, + hashrate: '0 KH/s', + avHashrate: '0 KH/s', + plHashrate: '0 KH/s', + accepted: 0, + rejected: 0, + hardwareErr: 0, + total: 0, + rpm: -1, + temperatue: '0 ℃' + } + } + }) + + _this.findMiner(Dev, function (data, err) { + if (err) { + Debug.IbctLogErr(__('查找矿机失败'), Dev.devID); + Device.mutex.unlock(); + return resolve(1); + } + _this.RunningMiner.push(Dev); + Device.mutex.unlock(); + resolve(0); + }) + } + }) + }) + } + async AddMiners(Devices) { + for (var i = 0; i < Devices.length; i++) { + await this.AddMiner(Devices[i]); + } + } + async connectMiner(Device) { + var ret; + var dev; + + dev = this.GetMinerByDevID(Device.devID); + if (!dev) + return false + + ret = await this.DetectMiner(dev, dev.dev.miningName); + if (ret) { + this.emit("error", __('探测矿机失败'), null, dev.devID); + return false + } + ret = await this.controlMinerSN(dev); + if (ret) { + this.emit("error", __('处理矿机条码失败'), null, dev.devID); + return false + } + return true + } + async connectMiners(Devices) { + for (var i = 0; i < Devices.length; i++) { + await this.connectMiner(Devices[i]); + } + } + async RemoveMiner(Device) { + var _this = this; + var dev = null; + + await Device.mutex.lock(async () => { + if (_this.HasExistMiner(Device)) { + dev = _this.GetMinerByDevID(Device.devID); + if (!dev) + return; + + dev.dev.mutex.lock(async () => { + if (dev) { + if (_this.GetMinerRunningStatus(dev)) { + await _this.StopMinerThread(dev, true); + _this.SetMinerRunningStatus(dev, false); + } else { + // throngh have stop miner, but need to remove miner totally + dev.dev.miner.removeAllListeners("error"); + dev.dev.miner.removeAllListeners("warning"); + await _this.ReleaseMiner(dev); + } + } + + _this.RunningMiner = _this.RunningMiner.filter(function (dev) { + if (dev.devID !== Device.devID) { + return true; + } else { + return false; + } + }); + // dev.dev.mutex.resetQueue(); + dev.dev.mutex._waiting = []; + dev.dev.mutex.unlock(); + }) + } + Device.mutex.unlock(); + }) + } + async RemoveMiners(Devices) { + var _this = this; + for (var i = 0; i < Devices.length; i++) { + await _this.RemoveMiner(Devices[i]); + } + } + RebootMiner(Device) { + var dev; + dev = this.GetMinerByDevID(Device.devID); + if (!dev) + return null; + // reboot miner may cause usb plug-in & plug-out + this.RebootHWMiner(dev); + } + RebootMiners(Devices) { + var _this = this; + Devices.forEach(function (Device, index) { + _this.RebootMiner(Device); + }) + } + SetMinerLed(Device, Enable) { + var dev = this.GetMinerByDevID(Device.devID); + if (!dev) + return null; + this.SetMinerLedStatus(dev, Enable); + } + SetMinersLed(Devices, Enable) { + var _this = this; + Devices.forEach(function (Device, index) { + _this.SetMinerLed(Device, Enable); + }) + } + + CheckFirmware(Device, Image, Callback) { + var temp = Buffer.from(Image, 0, 64); + var head = { + magic: 0, + model_name: null, + version: null, + crc: 0, + res: null + } + if (!Image) + return Callback(__('非法固件,请联系Intchains')); + + if (Device.dev.miningName === 'Goldshell-HS1-Plus') { + head.magic = temp.readUInt32LE(0); + head.model_name = temp.toString('utf8', 4, 23); + head.version = temp.toString('utf8', 24, 31); + head.crc = temp.readUInt32LE(32); + head.res = temp.toString('utf8', 36, 63); + } else { + head.magic = temp.readUInt32LE(0); + head.model_name = temp.toString('utf8', 4, 19); + head.version = temp.toString('utf8', 20, 27); + head.crc = temp.readUInt32LE(28); + head.res = temp.toString('utf8', 32, 63); + } + + if (head.magic !== 0x20190428) + return Callback(__('非法固件,请联系Intchains')); + + if (head.model_name.split('\0')[0] !== Device.dev.miningName) + return Callback(__('矿机对应固件版本错误,请联系Intchains')); + var crcValue = crc32(Image.slice(64)); + if (parseInt('0x' + crcValue, 16) !== head.crc) + return Callback(__('非法固件,请联系Intchains')); + Debug.IbctLogDbg('Burn Image Check OK'); + return Callback(null); + } + async BurnMinerFirmware(Device, Image, Callback) { + var _this = this; + var dev = this.GetMinerByDevID(Device.devID); + if (!dev) + return; + if (dev.dev.miningName === 'unknow') + return; + await this.DisableMiner(dev); + this.SetMinerRunningState(dev, 'burn'); + + this.CheckFirmware(dev, Image, function(err) { + if (err) { + Callback(err) + return + } + _this.UpdateMinerImage(dev, Image.slice(64), Callback); + }) + } + async BurnMinersFirmware(Devices, Image, Callback) { + for (var i = 0; i < Devices.length; i++) { + await this.BurnMinerFirmware(Devices[i], Image, Callback); + } + } +} + +module.exports = function RunMiner(options = {}) { + return new Miner(options); +}; diff --git a/src/miner/config/minerconfig.js b/src/miner/config/minerconfig.js new file mode 100644 index 0000000..14f6479 --- /dev/null +++ b/src/miner/config/minerconfig.js @@ -0,0 +1,26 @@ +export const minerconfig = { + model: "simplenode", + algo: "scrypt", + varity: 0x30, + powerplan: [ + { + plan: "HashRate", + voltage: 810, + freq: 850, + }, + { + plan: "Balance", + voltage: 720, + freq: 725, + }, + { + plan: "LowePower", + voltage: 670, + freq: 600, + } + ], + powerdefault: "HashRate", + temptarget: 65, + tempwarn: 70, + tempcutoff: 90 + }; \ No newline at end of file diff --git a/src/miner/cpu.js b/src/miner/cpu.js new file mode 100644 index 0000000..495ef02 --- /dev/null +++ b/src/miner/cpu.js @@ -0,0 +1,129 @@ +const EventEmitter = require('events'); +const Debug = require('../log')(); +const COMP = '[cpu]'; + +class cpu extends EventEmitter { + constructor({ + devPath, + algo, + varity, + crypto + }) { + super(); + var _this = this; + + _this.devPath = devPath; + _this.algo = algo; + _this.varity = varity; + _this.crypto = crypto; + _this.MinerShouldStop = false; + _this.info = { + firmwareVer: 'V0.0.1', + modelName: 'cpu', + sn:'CPU666666', + hashRation: 0, + workDepth:2, + }; + + _this.status = { + chips:1, + temp: 40, + votage:1, + freq:1000, + varity:0x0, + cores:1, + goodcores:1, + scanbits:0, + scantime:0, + tempwarn:80 + }; + Debug.IbctLogDbg(COMP, 'DevPath: ', _this.devPath, '; Miner:CPU; Algo:', _this.algo.getAlgoName(), '; Crypto: ', _this.crypto.getCryptoName()); + } + + + async init(params) { + /* + 初始化硬件 + + */ + Debug.IbctLogDbg(COMP, 'CPU init'); + } + + async detect(modelName) { + var _this = this; + Debug.IbctLogErr(COMP, 'API: detect...', modelName); + return 0; + } + + getInfo() { + var _this = this; + return _this.info; + } + + async setDevice() { + /* + 设置Miner参数,电压, 频率,目标温度,报警温度。 + */ + Debug.IbctLogDbg(COMP, 'setDevice'); + } + async stopScanWork() { + this.MinerShouldStop = true; + } + async scanWork(workQueue, Callback) { + /* + 更新Work + */ + var _this = this; + var result; + var nonce = Buffer.alloc(8); + //var highNonce = parseInt((Job.snonce) / 0x100000000) >>> 0; + if (!_this.algo || !_this.crypto) { + Callback('Set Algo or Crypto First'); + return + } + + _this.MinerShouldStop = false; + for(let i=0; i<_this.info.workDepth;i++) { + Debug.IbctLogDbg(COMP, 'work', i, JSON.stringify(workQueue.pop())); + } + // _this.work = Job; + // for (let lowNonce = 0; lowNonce < 0x5; lowNonce++) { + // //Debug.IbctLogDbg(COMP, 'algo', _this.algo.name.toString('hex'), 'scanWork ', i.toString('16'), '12: ', (Job.snonce + i).toString('16')); + // //nonce.writeUIntLE(((Job.snonce + i) & 0xffffffff), 0, 4); + // //nonce.writeUIntLE(Math.floor((Job.snonce + i) / 0x100000000), 4, 4); + // nonce.writeUInt32LE(highNonce, 4); + // nonce.writeUInt32LE(lowNonce, 0); + // if (_this.crypto.setWorkData) { + // _this.crypto.setWorkData(_this.work, 'start nonce', nonce); + // } + // if (_this.algo.genHash) { + // result = _this.algo.genHash(_this.work.data, _this.work.data.length, 0); + // Debug.IbctLogDbg(COMP, result, _this.crypto.checkHash(_this.work.target, Buffer.from(result, 'hex'))); + // if (_this.crypto.checkHash && _this.crypto.checkHash(_this.work.target, Buffer.from(result, 'hex'))) { + // Debug.IbctLogDbg(COMP, 'find Nonce:', (Job.snonce + i).toString('16')); + // Callback(null, nonce, work); + // } + // } + // } + Callback(null, null,work); + } + async stop() { + /* + 停止硬件工作并关闭硬件 + */ + Debug.IbctLogDbg(COMP, 'stop'); + } + + getState() { + /* + 获取当前设备状态, 温度,电压,频率,功耗等 + */ + var _this = this; + Debug.IbctLogDbg(COMP, 'getState'); + return _this.status; + } +} + +module.exports = function Getcpu(options = {}) { + return new cpu(options); +}; \ No newline at end of file diff --git a/src/miner/hs1.js b/src/miner/hs1.js new file mode 100644 index 0000000..fd56f30 --- /dev/null +++ b/src/miner/hs1.js @@ -0,0 +1,873 @@ +const Delimiter = require('@serialport/parser-delimiter'); +const Debug = require('../log')(); +var SerialPort = require('serialport'); +const EventEmitter = require('events'); +// var waitUntil = require('wait-until'); +var waitUntil = require('../waitUntil'); +var crc32 = require('crc32'); +const COMP = '[HS1]:'; + +const hwTarget = Buffer.from([0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]); + +const PV = 0x10; +//const PV20 = 0x20; +const TypeSetBootMode = 0xA3; +const TypeUpdateFW = 0xAA; +const TypeQueryInfo = 0xA4; +const TypeProductTest = 0xAB; +const TypeSetLED = 0xA6; +const TypeSendWork = 0xA1; +const TypeSetHWParams = 0xA2; +const TypeReboot = 0xAC; +const TypeRecvNonce = 0x51; +const TypeRecvJob = 0x55; +const TypeRecvState = 0X52; +const TypeRecvBootMode = 0x53; +const TypeRecvInfo = 0x54; +const TypeRecvFWState = 0x5A; +const TypeRecvTestResult = 0x5B; + +const pktHeader = Buffer.from([0xA5, 0x3C, 0x96]); +const pktEnder = Buffer.from([0x69, 0xC3, 0x5A]); +const typeOffset = 0; + +const hs1cfg = { + model: "Goldshell-HS1", + algo: "blake2bsha3", + varity: 0x4, + targetFreq: 650, //MHz + targetVoltage: 410, //mv + targetTemp: 65, + warnTemp: 115, + offTemp: 125 +}; + +class hs1 extends EventEmitter { + constructor({devPath, algo, varity, crypto}) { + super(); + var _this = this; + + _this.devPath = devPath; + _this.algo = algo; + _this.crypto = crypto; + _this.MinerShouldStop = false; + _this.inited = false; + _this.txTimeoutCnt = 0; + _this.work = [{ jobID: 0 }, { jobID: 0 }, { jobID: 0 }, { jobID: 0 }]; + _this.Job = []; + _this.fakeJob = { overScan:false }; + _this.job_id = null; + + _this.info = { + firmwareVer: 'V0.0.1', + modelName: 'Goldshell-HS1', + sn:'unknown', + snAbbr: 'H10', + //hashRation:0, + workDepth:4 + }; + + _this.firmware = { + retryCnt : 2, + FWPageSize : 256, + curState : 0, + curID : 0 + } + _this.submitNonce = null; + _this.curJobid = 4; + _this.jobsDone = 0; + + _this.status = { + chips:0, + temp:0, + voltage:0, + freq:0, + varity:0x4, + cores:0, + goodcores:0, + scanbits:0, + scantime:0, + tempwarn:0, + fanwarn:0, + powerwarn:0, + rpm:0 + }; + + _this.port = new SerialPort(_this.devPath, { + baudRate: 115200, + dataBits:8, + stopBits:1, + parity:'none', + rtscts:false + }, function (err) { + if (err) { + Debug.IbctLogErr(COMP, _this.devPath, '打开串口失败: ', err.message); + return; + } else + Debug.IbctLogErr(COMP, _this.devPath, '打开串口成功'); + + }); + + const parser = _this.port.pipe(new Delimiter({ delimiter: pktEnder})); + parser.on('data', function(data) { + _this.hs1ParseNotify(_this, data); + }); + + _this.on("error", function(err) { + Debug.IbctLogDbg(COMP, err); + }) + + _this.on("warning", function(err) { + Debug.IbctLogDbg(COMP, err); + }) + } + + hs1ParseNotify(_this, data) { + //Debug.IbctLogInfo(COMP, _this.devPath, 'Recv Pkt', data.toString('hex')); + var location = data.indexOf(pktHeader); + if(location === -1) { + Debug.IbctLogErr(COMP, _this.devPath, 'Recv Invalid PKT', data); + return; + } + var typeLocation = location + pktHeader.length + typeOffset; + switch(data[typeLocation]) { + + case TypeRecvNonce: + var jobID = data[9]; + //var chipID = data[10]; + //var coreID = data[11]; + var nonce_l = data.readUInt32LE(12); + var nonce_h = data.readUInt32LE(16); + var nonce = Buffer.alloc(8); + nonce.writeUInt32LE(nonce_l, 0); + nonce.writeUInt32LE(nonce_h, 4); + if(jobID !== _this.work[0].jobID && jobID !== _this.work[1].jobID && jobID !== _this.work[2].jobID && jobID !== _this.work[3].jobID) { + Debug.IbctLogDbg(COMP, _this.devPath, 'Find Stale ', jobID, _this.work[0].jobID, _this.work[1].jobID, nonce.toString('hex')); + } else { + //Debug.IbctLogDbg(COMP, _this.devPath, 'Find Nonce ', nonce.toString('hex')); + let i; + if (jobID === _this.work[0].jobID) i = 0; + else if (jobID === _this.work[1].jobID) i = 1; + else if (jobID === _this.work[2].jobID) i = 2; + else i = 3; + + if(_this.submitNonce) + _this.submitNonce(null, nonce, _this.Job[i]); + } + break; + + case TypeRecvInfo: + _this.recvInfoPkt = true; + //Debug.IbctLogDbg(COMP, _this.devPath, 'RecvInfoPkt:', data.toString('hex')); + _this.info.modelName = data.toString('utf8', 10, 10 + data[9]); + _this.info.firmwareVer = data.toString('utf8', 27, 27 + data[26]); + _this.info.sn = data.toString('utf8', 36, 36 + data[35]); + _this.info.snAbbr = 'H10'; + //if (data[typeLocation+1] === PV20) { + //_this.info.workDepth = data[53]; + //} else { + //_this.info.hashRation = data.readUInt16LE(69); + //_this.info.workDepth = data[101]; + //} + Debug.IbctLogDbg(_this.info); + break; + + case TypeRecvFWState: + _this.recvFWStatePkt = true; + _this.firmware.curID = data.readUInt32LE(9); + _this.firmware.curState = data[13]; + break; + + case TypeRecvJob: + _this.recvJobPkt = true; + break; + + case TypeRecvBootMode: + _this.recvBootModePkt = true; + break; + + case TypeRecvState: + _this.recvStatePkt = true; + _this.recvQueryStatePkt = true; + //Debug.IbctLogInfo(COMP, _this.devPath, 'RecvState pkt:', data.toString('hex')); + _this.status.chips = data[9]; + _this.status.cores = data[10]; + _this.status.goodcores = data[11]; + _this.status.scanbits = data[12]; + _this.status.scantime = data.readUInt16LE(13) * 100; //ms + _this.status.voltage = data.readUInt16LE(15); //mV + _this.status.freq = data.readUInt16LE(17); //MHz + _this.status.varity = data.readUInt32LE(19); + _this.status.temp = data[23]; + _this.status.hwreboot = data[24]; + _this.status.tempwarn = data[25]; + _this.status.fanwarn = data[26]; + _this.status.powerwarn = data[27]; + _this.status.rpm = data.readUInt16LE(28); + break; + + case TypeRecvTestResult: + _this.recvPTInfoPkt = true; + break; + + default: + Debug.IbctLogErr(COMP, _this.devPath, 'Recv Unsupported PKT Type', data[location + typeOffset]) + break; + } + return; + } + + hs1SendPkt (pkt) { + var _this = this; + var offset = 0; + var length = 0; + Object.keys(pkt).forEach(function(key) { + if(Buffer.isBuffer(pkt[key])) { + length += pkt[key].length; + } + }); + + var msg = Buffer.alloc(length); + + Object.keys(pkt).forEach(function(key) { + if(Buffer.isBuffer(pkt[key])) { + pkt[key].copy(msg, offset); + offset += pkt[key].length; + } + }); + + //Debug.IbctLogDbg(COMP, _this.devPath, 'Send pkt', msg.toString('hex')); + _this.port.write(msg, function(err) { + if (err) { + Debug.IbctLogErr(COMP, _this.devPath, 'Error on write: ', err.message) + } + _this.port.drain(function(err) { + if (err) { + Debug.IbctLogErr(COMP, _this.devPath, 'Error on Drain: ', _this.devPath, err.message) + } + }) + }) + } + + hs1GetState(_this) { + var pktQueryStatus = { + header: Buffer.from(pktHeader), + type: Buffer.from([TypeSetHWParams]), + version: Buffer.from([PV]), + pktlen: Buffer.from([0x7, 0x0 ,0x0 ,0x0]), + flag:Buffer.from([0x52]), + ender:Buffer.from(pktEnder) + }; + _this.recvQueryStatePkt = false; + _this.hs1SendPkt(pktQueryStatus); + /*TODO Wait response here*/ + return true; + } + + async hs1GetStaticInfo(modelName) { + var _this = this; + var pktQueryInfo = { + header: Buffer.from(pktHeader), + type: Buffer.from([TypeQueryInfo]), + version: Buffer.from([PV]), + pktlen: Buffer.from([0x6, 0x0 ,0x0 ,0x0]), + ender:Buffer.from(pktEnder) + }; + _this.recvInfoPkt = false; + _this.hs1SendPkt(pktQueryInfo); + return new Promise(function (resolve, reject) { + waitUntil() + .interval(50) + .times(10) + .condition(function() { + return _this.recvInfoPkt + }) + .done(function(result) { + if(result === false) { + Debug.IbctLogErr(COMP, _this.devPath, '获取矿机信息出错'); + resolve(2); + } else { + if(modelName === _this.info.modelName) { + resolve(0); + } else { + resolve(1); + } + } + }) + }); + } + + hs1SetBootMode() { + var _this = this; + var pktSetBootMode = { + header: Buffer.from(pktHeader), + type: Buffer.from([TypeSetBootMode]), + version: Buffer.from([PV]), + pktlen: Buffer.from([0x7, 0x0 ,0x0 ,0x0]), + ender:Buffer.from(pktEnder) + }; + _this.recvBootModePkt = false; + _this.hs1SendPkt(pktSetBootMode); + } + + hs1BurnFWInit() { + var _this = this; + var pktSetBootMode = { + header: Buffer.from(pktHeader), + type: Buffer.from([TypeSetBootMode]), + version: Buffer.from([PV]), + pktlen: Buffer.from([0x6, 0x0 ,0x0 ,0x0]), + ender:Buffer.from(pktEnder) + }; + + _this.recvBootModePkt = false; + _this.hs1SendPkt(pktSetBootMode); + return new Promise(function (resolve, reject) { + waitUntil() + .interval(20) + .times(50) + .condition(function() { + return _this.recvBootModePkt + }) + .done(function(result) { + if(result === false) { + resolve(1); + } else { + resolve(0); + } + }) + }) + } + + hs1SetHWParams(varity, freq, voltage) { + var _this = this; + var pktSetParam = { + header: Buffer.from(pktHeader), + type: Buffer.from([TypeSetHWParams]), + version: Buffer.from([PV]), + pktlen: Buffer.from([0x10, 0x00, 0x00, 0x00]), + flag:Buffer.from([0xA2]), + voltage: Buffer.alloc(2), + freq:Buffer.alloc(2), + varity:Buffer.alloc(4), + targettemp:Buffer.from([80]), + ender:Buffer.from(pktEnder) + }; + + pktSetParam.varity.writeUInt32LE(varity, 0); + pktSetParam.freq.writeUInt16LE(freq, 0); + pktSetParam.voltage.writeUInt16LE(voltage, 0); + _this.recvStatePkt = false; + _this.hs1SendPkt(pktSetParam); + /*TODO Wait response here*/ + return true; + } + + hs1SetHWParamsAndWait(varity, freq, voltage) { + var _this = this; + var pktSetParam = { + header: Buffer.from(pktHeader), + type: Buffer.from([TypeSetHWParams]), + version: Buffer.from([PV]), + pktlen: Buffer.from([0x10, 0x00, 0x00, 0x00]), + flag:Buffer.from([0xA2]), + voltage: Buffer.alloc(2), + freq:Buffer.alloc(2), + varity:Buffer.alloc(4), + targettemp:Buffer.from([80]), + ender:Buffer.from(pktEnder) + }; + pktSetParam.varity.writeUInt32LE(varity, 0); + pktSetParam.freq.writeUInt16LE(freq, 0); + pktSetParam.voltage.writeUInt16LE(voltage, 0); + _this.recvStatePkt = false; + _this.hs1SendPkt(pktSetParam); + + return new Promise(function (resolve, reject) { + waitUntil() + .interval(20) + .times(50) + .condition(function() { + return _this.recvStatePkt + }) + .done(function(result) { + if(result === false) { + Debug.IbctLogErr(COMP, _this.devPath, '设置矿机参数出错'); + resolve(1); + } else { + resolve(0); + } + }) + }) + } + + async hs1WriteJob(jobID, snonce, enonce, target, data) { + var _this = this; + _this.recvJobPkt = false; + var pktSendJob = { + header: Buffer.from(pktHeader), + type: Buffer.from([TypeSendWork]), + version: Buffer.from([PV]), + pktlen: Buffer.alloc(4), + target:Buffer.alloc(8), + startNonce: Buffer.alloc(8), + endNonce:Buffer.alloc(8), + jobNum: Buffer.from([1]), + jobID: Buffer.alloc(1), + jobData:Buffer.alloc(128), + ender:Buffer.from(pktEnder) + }; + + pktSendJob.pktlen.writeUInt32LE(160, 0); //pktlen + target.copy(pktSendJob.target, 0); + snonce.copy(pktSendJob.startNonce, 0); + enonce.copy(pktSendJob.endNonce, 0); + pktSendJob.jobID[0] = jobID; + data.copy(pktSendJob.jobData, 0); + + _this.hs1SendPkt(pktSendJob); + return new Promise(function (resolve, reject) { + waitUntil() + .interval(40) + .times(50) + .condition(function() { + return _this.recvJobPkt + }) + .done(function(result) { + if(result === false) { + Debug.IbctLogErr(COMP, _this.devPath, '发送Work失败'); + reject(new Error(__('发送Work失败')).message); + } else { + resolve(0); + } + }) + }) + } + + hs1SendFWPktAndWait(cnt, FWPKT) { + var _this = this; + _this.recvFWStatePkt = false; + _this.firmware.curState = 0; + _this.firmware.curID = 0; + _this.hs1SendPkt(FWPKT); + return new Promise(function (resolve, reject) { + waitUntil() + .interval(20) + .times(50) + .condition(function() { + return _this.recvFWStatePkt + }) + .done(function(result) { + if(result === false) { + Debug.IbctLogErr(COMP, _this.devPath, '烧录连接超时'); + reject(new Error(__('烧录连接超时')).message); + } else if(_this.firmware.curState === 0x02) { + if(cnt < _this.firmware.retryCnt){ + resolve(1); + } else { + Debug.IbctLogErr(COMP, _this.devPath, '串口出错'); + reject(new Error(__('串口出错')).message); + } + } else { + resolve(0); + } + }) + }) + } + + async stopScanWork() { + var _this = this; + Debug.IbctLogDbg(COMP, _this.devPath, 'stopScanWork ...'); + //_this.curJobid = 0; + _this.MinerShouldStop = true; + } + + async scanWork(workQueue, callback) { + var _this = this; + Debug.IbctLogInfo(COMP, _this.devPath, 'scanWork Begin ...'); + + if (!_this.inited) { + callback(null, null, null); + return; + } + + if(_this.status.scantime === 0) { + _this.status.scantime = 7000; //10s default + } + + if (_this.MinerShouldStop) { + _this.MinerShouldStop = false; + if (_this.job_id === workQueue[0].job_id) { + // new job arrived + _this.fakeJob.overScan = false; + // clean all workQueue + workQueue.splice(0, workQueue.length); + callback(null, null, _this.fakeJob); + Debug.IbctLogDbg("new job arrived, scanWork again"); + return; + } + } + _this.job_id = workQueue[0].job_id; + + for(let i = 0; i < _this.info.workDepth; i++) { + _this.Job[i] = workQueue.pop(); + + _this.work[i].target = Buffer.alloc(8); + var target = _this.Job[i].target; + if(Buffer.compare(hwTarget, target) > 0) { + hwTarget.copy(_this.work[i].target, 0); + _this.Job[i].hwTarget = hwTarget; + } else { + target.copy(_this.work[i].target, 0); + _this.Job[i].hwTarget = target; + } + + _this.work[i].target = _this.work[i].target.swap64(); + + _this.work[i].highNonce = parseInt((_this.Job[i].snonce) / 0x100000000) >>> 0; + //_this.work[i].payload = Buffer.alloc(128); + _this.work[i].data = Buffer.from(_this.Job[i].data); + + _this.work[i].snonce = Buffer.alloc(8); + _this.work[i].snonce.writeUInt32LE(0, 0); + _this.work[i].snonce.writeUInt32LE(_this.work[i].highNonce, 4); + //Job.data.copy(_this.work[i].payload, 0); + //Debug.IbctLogDbg(COMP, _this.work[i].payload.toString('hex')); + + _this.work[i].enonce = Buffer.alloc(8); + _this.work[i].enonce.writeUInt32LE(0xffffffff, 0); + _this.work[i].enonce.writeUInt32LE(_this.work[i].highNonce + 32, 4); + + _this.submitNonce = callback; + // _this.MinerShouldStop = false; + _this.jobsDone++; + + _this.work[i].jobID = _this.curJobid; + _this.curJobid++; + if (_this.curJobid === 16) _this.curJobid = 4; + /* + Debug.IbctLogInfo(COMP, _this.devPath, 'Work Target:', _this.work[i].target.toString('hex'), + 'Work highNonce:', _this.work[i].highNonce.toString(16), + 'Work jobID:', _this.work[i].jobID); + */ + + await _this.hs1WriteJob(_this.work[i].jobID, _this.work[i].snonce, _this.work[i].enonce, _this.work[i].target, _this.work[i].data); + } + + var interval = 50; //ms + var times = 7000 / interval; + + waitUntil() + .interval(interval) + .times(times) + .condition(function() { + return _this.MinerShouldStop + }) + .done(function(result) { + Debug.IbctLogDbg(COMP, _this.devPath, 'ScanWork Exit', result ? "(NewJob)...": "(ScanTime Out)..."); + if (result === false) + _this.fakeJob.overScan = true; + else + _this.fakeJob.overScan = false; + callback(null, null, _this.fakeJob); + }); + } + + async setDevice(varity, freq, voltage) { + var _this = this; + _this.hs1SetHWParams(varity, freq, voltage); + } + + getInfo() { + var _this = this; + return _this.info; + } + + getState() { + var _this = this; + return _this.status; + } + + async detect(modelName) { + var _this = this; + Debug.IbctLogErr(COMP, _this.devPath, 'hs1 detect...'); + return await _this.hs1GetStaticInfo(modelName); + } + + async init(params) { + var _this = this; + Debug.IbctLogErr(COMP, _this.devPath, 'hs1 init...'); + if(_this.firmware.updating === true){ + Debug.IbctLogErr(COMP, _this.devPath, 'Still Updating'); + return 1; + } + if( _this.inited === true){ + Debug.IbctLogErr(COMP, _this.devPath, 'Already Inited. return now'); + return 0; + } + + //_this.hs1SetHWParams(hs1cfg.varity, hs1cfg.targetFreq, hs1cfg.targetVoltage); + var ret = await _this.hs1SetHWParamsAndWait(_this.varity, hs1cfg.targetFreq, hs1cfg.targetVoltage); + if (ret) + return ret; + + if (_this.intervalObj) { + clearInterval(_this.intervalObj); + _this.intervalObj = null; + } + + _this.intervalObj = setInterval(function() { + Debug.IbctLogErr(COMP, _this.devPath, 'Temp', _this.status.temp, 'RPM', _this.status.rpm, 'Tempwarn', _this.status.tempwarn, 'Fanwarn', _this.status.fanwarn, 'Powerwarn', _this.status.powerwarn,'Freq', _this.status.freq, 'Jobs', _this.jobsDone); + _this.hs1GetState(_this); + waitUntil() + .interval(50) + .times(10) + .condition(function() { + return _this.recvQueryStatePkt + }) + .done(function(result) { + if(result === false) { + _this.txTimeoutCnt++; + Debug.IbctLogErr(COMP, _this.devPath, 'hs1GetState Timeout ', _this.txTimeoutCnt); + if(_this.txTimeoutCnt > 10) { + _this.emit("error", __('获得矿机状态超时')); + _this.txTimeoutCnt = 0; + } + } else { + _this.txTimeoutCnt = 0; + + if(_this.status.fanwarn) + _this.emit("error", __('矿机风扇异常')); + + if(_this.status.powerwarn) + _this.emit("error", __('矿机电源异常')); + + if((_this.status.temp > hs1cfg.offTemp) || (_this.status.tempwarn)) { + _this.emit("error", __('矿机高温关机')); + } else if(_this.status.temp > hs1cfg.warnTemp && _this.status.temp < hs1cfg.offTemp) { + _this.emit("warning", __('矿机高温警报')); + } + } + }); + }, 5000); + _this.inited = true; + return 0; + } + + async stop(enable, wait) { + var _this = this; + Debug.IbctLogErr(COMP, _this.devPath, 'hs1', enable ? 'remove...' : 'stop...'); + if (!enable) { + await _this.hs1SetHWParamsAndWait(0, 0, 0); + } + _this.inited = false; + _this.txTimeoutCnt = 0; + _this.freq = 0; + _this.voltage = 0; + _this.work.jobID = 0; + _this.jobsDone = 0; + _this.MinerShouldStop = true; + + if (_this.intervalObj) { + clearInterval(_this.intervalObj); + _this.intervalObj = null; + } + if (enable) { + if (wait) { + await _this.port.flush(function (err) { + _this.port.close(function(err) { + if(err) + Debug.IbctLogDbg(COMP, err.message); + }); + }); + } else { + await this.port.flush() + await this.port.close(function(err) { + if(err) + Debug.IbctLogDbg(COMP, err.message); + }); + return new Promise(function (resolve) { + waitUntil() + .interval(10) + .times(50) + .condition(function() { + return !_this.port.isOpen; + }) + .done(function() { + resolve(0); + }) + }) + } + } + } + + async burnFirmware(firmware, callback) { + var _this = this; + if(_this.firmware.updating === true) { + callback(__('升级中,请等待')); + return; + } + if(_this.inited === true) { + callback(__('挖矿中,请先暂停挖矿')); + return; + } + + var ret = await _this.hs1GetStaticInfo(_this.info.modelName); + if (ret === 1) { + callback(__('无法获取当前版本号')); + return; + } + + var init = await _this.hs1BurnFWInit(); + if(init === 0) { + _this.firmware.updating = true; + } else { + _this.firmware.updating = false; + callback(__('烧入固件初始化失败')); + return; + } + var fwLen = firmware.length; + var totalPktNum = Math.ceil(firmware.length / _this.firmware.FWPageSize) + var id = 0; + Debug.IbctLogErr(COMP, _this.devPath, 'BurnFW, CurVersion:', _this.info.firmwareVer, 'FW lenth' , fwLen , "PKTnum", totalPktNum); + try { + while(fwLen > 0) { + var currentLen = (fwLen > _this.firmware.FWPageSize) ? _this.firmware.FWPageSize : fwLen; + var curStart = firmware.length - fwLen; + var curEnd = curStart + currentLen; + + var pktUpdateFW = { + header: Buffer.from(pktHeader), + type: Buffer.from([TypeUpdateFW]), + version: Buffer.from([PV]), + pktLen: Buffer.alloc(4), + pktID:Buffer.alloc(4), + pageSize:Buffer.alloc(4), + curLen:Buffer.alloc(4), + flag:Buffer.alloc(1), + crc32: Buffer.alloc(4), + fwData:Buffer.alloc(currentLen), + ender:Buffer.from(pktEnder) + }; + pktUpdateFW.curLen.writeUInt32LE(currentLen, 0); + pktUpdateFW.pktID.writeUInt32LE(id, 0); + pktUpdateFW.pageSize.writeUInt32LE(_this.firmware.FWPageSize, 0); + pktUpdateFW.pktLen.writeUInt32LE(23 + currentLen); + firmware.copy(pktUpdateFW.fwData, 0, curStart, curEnd); + fwLen -= currentLen; + + if(fwLen) { + pktUpdateFW.flag[0] = 0x00; + } else { + pktUpdateFW.flag[0] = 0x01;//last + } + var msg = Buffer.alloc(23 + currentLen); + //console.log('3====>', curStart, curEnd, currentLen, fwLen); + pktUpdateFW.type.copy(msg, 0); + pktUpdateFW.version.copy(msg, 1); + pktUpdateFW.pktLen.copy(msg, 2); + pktUpdateFW.pktID.copy(msg, 6); + pktUpdateFW.pageSize.copy(msg, 10); + pktUpdateFW.curLen.copy(msg, 14); + pktUpdateFW.flag.copy(msg, 18); + pktUpdateFW.crc32.copy(msg, 19); + pktUpdateFW.fwData.copy(msg, 23); + + + var crcValue = parseInt(crc32(msg), 16); + pktUpdateFW.crc32.writeUInt32LE(crcValue, 0); + for(var i = 0; i < _this.firmware.retryCnt; i++) { + var success = await _this.hs1SendFWPktAndWait(i + 1, pktUpdateFW); + if(success === 0) + break; + } + + if(fwLen === 0) { + _this.firmware.updating = false; + } + callback(null, ((id + 1) / totalPktNum).toFixed(3)); + id++; + } + } + catch(err) { + Debug.IbctLogErr(COMP, _this.devPath, 'Updat firmware failed ' + err.message); + _this.firmware.updating = false; + callback(err.message); + } + } + + async rebootDev() { + Debug.IbctLogErr(COMP, this.devPath, 'reboot'); + var _this = this; + var pktReboot = { + header: Buffer.from(pktHeader), + type: Buffer.from([TypeReboot]), + version: Buffer.from([PV]), + pktlen: Buffer.from([0x6, 0x0 ,0x0 ,0x0]), + ender:Buffer.from(pktEnder) + }; + await _this.stop(true, true) + _this.hs1SendPkt(pktReboot); + } + + setLed(Enable) { + Debug.IbctLogErr(COMP, 'Set', this.devPath, 'Led to', Enable ? 'ON' : 'OFF'); + var _this = this; + var ledFlag = Enable === true ? 1 : 0; + var pktSetLED = { + header: Buffer.from(pktHeader), + type: Buffer.from([TypeSetLED]), + version: Buffer.from([PV]), + pktlen: Buffer.from([0xb, 0x0 ,0x0 ,0x0]), + Flag:Buffer.from([ledFlag]), + led:Buffer.from([0xE8, 0x3, 0xC8, 0x0]), //ON 1s OFF:200ms + ender:Buffer.from(pktEnder) + }; + _this.hs1SendPkt(pktSetLED); + } + + async burnSNInfo(ptinfo) { + var _this = this + Debug.IbctLogErr(COMP, _this.devPath, 'Burn Sn Num..', ptinfo) + var pktPTInfo = { + header: Buffer.from(pktHeader), + type: Buffer.from([TypeProductTest]), + version: Buffer.from([PV]), + pktlen: Buffer.from([70, 0x0, 0x0, 0x0]), + SNInfo: Buffer.alloc(32), + HashInfo: Buffer.alloc(32), + ender: Buffer.from(pktEnder) + } + if (ptinfo.sn) { + var sn = Buffer.from(ptinfo.sn) + pktPTInfo.SNInfo[0] = sn.length + sn.copy(pktPTInfo.SNInfo, 1) + } else { + pktPTInfo.SNInfo[0] = 0 + } + _this.recvPTInfoPkt = false; + _this.hs1SendPkt(pktPTInfo) + + return new Promise(function (resolve, reject) { + waitUntil() + .interval(50) + .times(10) + .condition(function () { + return _this.recvPTInfoPkt + }) + .done(function (result) { + if (result === false) { + Debug.IbctLogErr(COMP, _this.devPath, 'Burn SN num failed..') + resolve(__('写入SN序列号失败')) + } else { + resolve(null) + } + }) + }) + } +} + +module.exports = function Geths1(options = {}) { + return new hs1(options); +} diff --git a/src/miner/hs1plus.js b/src/miner/hs1plus.js new file mode 100644 index 0000000..02822c0 --- /dev/null +++ b/src/miner/hs1plus.js @@ -0,0 +1,909 @@ +const Delimiter = require('@serialport/parser-delimiter'); +const Debug = require('../log')(); +var SerialPort = require('serialport'); +const EventEmitter = require('events'); +// var waitUntil = require('wait-until'); +var waitUntil = require('../waitUntil'); +var crc32 = require('crc32'); +const COMP = '[HS1PLUS]:'; + +const hwTarget = Buffer.from([0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]); + +const PV = 0x10; +const TypeSetBootMode = 0xA3; +const TypeUpdateFW = 0xAA; +const TypeQueryInfo = 0xA4; +const TypeProductTest = 0xAB; +const TypePlt = 0xAD +const TypeSetLED = 0xA6; +const TypeSendWork = 0xA1; +const TypeSetHWParams = 0xA2; +const TypeReboot = 0xAC; +const TypeRecvNonce = 0x51; +const TypeRecvJob = 0x55; +const TypeRecvState = 0X52; +const TypeRecvBootMode = 0x53; +const TypeRecvInfo = 0x54; +const TypeRecvFWState = 0x5A; +const TypeRecvTestResult = 0x5B; +const TypeRecvPltResult = 0x5D + +const pktHeader = Buffer.from([0xA5, 0x3C, 0x96]); +const pktEnder = Buffer.from([0x69, 0xC3, 0x5A]); +const typeOffset = 0; + +const hs1pcfg = { + model: "Goldshell-HS1-Plus", + algo: "blake2bsha3", + varity: 0x4, + targetFreq: 700, //MHz + targetVoltage: 420, //mv + targetTemp: 65, + warnTemp: 115, + offTemp: 125 +}; + +class hs1p extends EventEmitter { + constructor({devPath, algo, varity, crypto}) { + super(); + var _this = this; + + _this.devPath = devPath; + _this.algo = algo; + _this.crypto = crypto; + _this.MinerShouldStop = false; + _this.inited = false; + _this.txTimeoutCnt = 0; + _this.work = [{ jobID: 0 }, { jobID: 0 }, { jobID: 0 }, { jobID: 0 }, { jobID: 0 }, { jobID: 0 }, { jobID: 0 }, { jobID: 0 }]; + _this.Job = []; + _this.fakeJob = { overScan:false }; + _this.job_id = null; + + _this.info = { + firmwareVer: 'V0.0.1', + modelName: 'Goldshell-HS1-Plus', + sn:'unknown', + snAbbr: 'H11', + workDepth:8 + }; + + _this.firmware = { + retryCnt : 2, + FWPageSize : 256, + curState : 0, + curID : 0 + } + _this.submitNonce = null; + _this.curJobid = 1; + _this.jobsDone = 0; + + _this.status = { + chips:0, + temp:0, + voltage:0, + freq:0, + varity:0x4, + cores:0, + goodcores:0, + scanbits:0, + scantime:0, + tempwarn:0, + fanwarn:0, + powerwarn:0, + rpm:0 + }; + + _this.plt = { + chipid: 0, + reason: '' + } + + _this.port = new SerialPort(_this.devPath, { + baudRate: 115200, + dataBits:8, + stopBits:1, + parity:'none', + rtscts:false + }, function (err) { + if (err) { + Debug.IbctLogErr(COMP, _this.devPath, '打开串口失败: ', err.message); + return; + } else + Debug.IbctLogErr(COMP, _this.devPath, '打开串口成功'); + + }); + + const parser = _this.port.pipe(new Delimiter({ delimiter: pktEnder})); + parser.on('data', function(data) { + _this.hs1pParseNotify(_this, data); + }); + + _this.on("error", function(err) { + Debug.IbctLogDbg(COMP, err); + }) + + _this.on("warning", function(err) { + Debug.IbctLogDbg(COMP, err); + }) + } + + hs1pParseNotify(_this, data) { + //Debug.IbctLogInfo(COMP, _this.devPath, 'Recv Pkt', data.toString('hex')); + var location = data.indexOf(pktHeader); + if(location === -1) { + Debug.IbctLogErr(COMP, _this.devPath, 'Recv Invalid PKT', data); + return; + } + var typeLocation = location + pktHeader.length + typeOffset; + switch(data[typeLocation]) { + + case TypeRecvNonce: + var jobID = data[9]; + //var chipID = data[10]; + //var coreID = data[11]; + var nonce_l = data.readUInt32LE(12); + var nonce_h = data.readUInt32LE(16); + var nonce = Buffer.alloc(8); + nonce.writeUInt32LE(nonce_l, 0); + nonce.writeUInt32LE(nonce_h, 4); + if(jobID !== _this.work[0].jobID && + jobID !== _this.work[1].jobID && + jobID !== _this.work[2].jobID && + jobID !== _this.work[3].jobID && + jobID !== _this.work[4].jobID && + jobID !== _this.work[5].jobID && + jobID !== _this.work[6].jobID && + jobID !== _this.work[7].jobID) { + Debug.IbctLogDbg(COMP, _this.devPath, 'Find Stale ', jobID, _this.work[0].jobID, _this.work[1].jobID, nonce.toString('hex')); + } else { + //Debug.IbctLogDbg(COMP, _this.devPath, 'Find Nonce ', nonce.toString('hex')); + let i; + if (jobID === _this.work[0].jobID) i = 0; + else if (jobID === _this.work[1].jobID) i = 1; + else if (jobID === _this.work[2].jobID) i = 2; + else if (jobID === _this.work[3].jobID) i = 3; + else if (jobID === _this.work[4].jobID) i = 4; + else if (jobID === _this.work[5].jobID) i = 5; + else if (jobID === _this.work[6].jobID) i = 6; + else i = 7; + + if(_this.submitNonce) + _this.submitNonce(null, nonce, _this.Job[i]); + } + break; + + case TypeRecvInfo: + _this.recvInfoPkt = true; + //Debug.IbctLogDbg(COMP, _this.devPath, 'RecvInfoPkt:', data.toString('hex')); + _this.info.modelName = data.toString('utf8', 10, 10 + data[9]); + _this.info.firmwareVer = data.toString('utf8', 31, 31 + data[30]); + _this.info.sn = data.toString('utf8', 40, 40 + data[39]); + _this.info.snAbbr = 'H11'; + //_this.info.workDepth = data[57]; + Debug.IbctLogDbg(_this.info); + break; + + case TypeRecvFWState: + _this.recvFWStatePkt = true; + _this.firmware.curID = data.readUInt32LE(9); + _this.firmware.curState = data[13]; + break; + + case TypeRecvJob: + _this.recvJobPkt = true; + break; + + case TypeRecvBootMode: + _this.recvBootModePkt = true; + break; + + case TypeRecvPltResult: + _this.recvPltPkt = true; + _this.plt.chipid = data[9]; + _this.plt.reason = data.toString('utf8', 10, 11); + break; + + case TypeRecvState: + _this.recvStatePkt = true; + _this.recvQueryStatePkt = true; + //Debug.IbctLogInfo(COMP, _this.devPath, 'RecvState pkt:', data.toString('hex')); + _this.status.chips = data[9]; + _this.status.cores = data[10]; + _this.status.goodcores = data[11]; + _this.status.scanbits = data[12]; + _this.status.scantime = data.readUInt16LE(13) * 100; //ms + _this.status.voltage = data.readUInt16LE(15); //mV + _this.status.freq = data.readUInt16LE(17); //MHz + _this.status.varity = data.readUInt32LE(19); + _this.status.temp = data[23]; + _this.status.hwreboot = data[24]; + _this.status.tempwarn = data[25]; + _this.status.fanwarn = data[26]; + _this.status.powerwarn = data[27]; + _this.status.rpm = data.readUInt16LE(28); + break; + + case TypeRecvTestResult: + _this.recvPTInfoPkt = true; + break; + + default: + Debug.IbctLogErr(COMP, _this.devPath, 'Recv Unsupported PKT Type', data[location + typeOffset]) + break; + } + return; + } + + hs1pSendPkt (pkt) { + var _this = this; + var offset = 0; + var length = 0; + Object.keys(pkt).forEach(function(key) { + if(Buffer.isBuffer(pkt[key])) { + length += pkt[key].length; + } + }); + + var msg = Buffer.alloc(length); + + Object.keys(pkt).forEach(function(key) { + if(Buffer.isBuffer(pkt[key])) { + pkt[key].copy(msg, offset); + offset += pkt[key].length; + } + }); + + //Debug.IbctLogDbg(COMP, _this.devPath, 'Send pkt', msg.toString('hex')); + _this.port.write(msg, function(err) { + if (err) { + Debug.IbctLogErr(COMP, _this.devPath, 'Error on write: ', err.message) + } + _this.port.drain(function(err) { + if (err) { + Debug.IbctLogErr(COMP, _this.devPath, 'Error on Drain: ', _this.devPath, err.message) + } + }) + }) + } + + hs1pGetState(_this) { + var pktQueryStatus = { + header: Buffer.from(pktHeader), + type: Buffer.from([TypeSetHWParams]), + version: Buffer.from([PV]), + pktlen: Buffer.from([0x7, 0x0 ,0x0 ,0x0]), + flag:Buffer.from([0x52]), + ender:Buffer.from(pktEnder) + }; + _this.recvQueryStatePkt = false; + _this.hs1pSendPkt(pktQueryStatus); + /*TODO Wait response here*/ + return true; + } + + async hs1pGetStaticInfo(modelName) { + var _this = this; + var pktQueryInfo = { + header: Buffer.from(pktHeader), + type: Buffer.from([TypeQueryInfo]), + version: Buffer.from([PV]), + pktlen: Buffer.from([0x6, 0x0 ,0x0 ,0x0]), + ender:Buffer.from(pktEnder) + }; + _this.recvInfoPkt = false; + _this.hs1pSendPkt(pktQueryInfo); + return new Promise(function (resolve, reject) { + waitUntil() + .interval(50) + .times(10) + .condition(function() { + return _this.recvInfoPkt + }) + .done(function(result) { + if(result === false) { + Debug.IbctLogErr(COMP, _this.devPath, '获取矿机信息出错'); + resolve(2); + } else { + if(modelName === _this.info.modelName) { + resolve(0); + } else { + resolve(1); + } + } + }) + }); + } + + hs1pSetBootMode() { + var _this = this; + var pktSetBootMode = { + header: Buffer.from(pktHeader), + type: Buffer.from([TypeSetBootMode]), + version: Buffer.from([PV]), + pktlen: Buffer.from([0x7, 0x0 ,0x0 ,0x0]), + ender:Buffer.from(pktEnder) + }; + _this.recvBootModePkt = false; + _this.hs1pSendPkt(pktSetBootMode); + } + + hs1pBurnFWInit() { + var _this = this; + var pktSetBootMode = { + header: Buffer.from(pktHeader), + type: Buffer.from([TypeSetBootMode]), + version: Buffer.from([PV]), + pktlen: Buffer.from([0x6, 0x0 ,0x0 ,0x0]), + ender:Buffer.from(pktEnder) + }; + + _this.recvBootModePkt = false; + _this.hs1pSendPkt(pktSetBootMode); + return new Promise(function (resolve, reject) { + waitUntil() + .interval(20) + .times(50) + .condition(function() { + return _this.recvBootModePkt + }) + .done(function(result) { + if(result === false) { + resolve(1); + } else { + resolve(0); + } + }) + }) + } + + hs1pSetHWParams(varity, freq, voltage) { + var _this = this; + var pktSetParam = { + header: Buffer.from(pktHeader), + type: Buffer.from([TypeSetHWParams]), + version: Buffer.from([PV]), + pktlen: Buffer.from([0x10, 0x00, 0x00, 0x00]), + flag:Buffer.from([0xA2]), + voltage: Buffer.alloc(2), + freq:Buffer.alloc(2), + varity:Buffer.alloc(4), + targettemp:Buffer.from([80]), + ender:Buffer.from(pktEnder) + }; + + pktSetParam.varity.writeUInt32LE(varity, 0); + pktSetParam.freq.writeUInt16LE(freq, 0); + pktSetParam.voltage.writeUInt16LE(voltage, 0); + _this.recvStatePkt = false; + _this.hs1pSendPkt(pktSetParam); + /*TODO Wait response here*/ + return true; + } + + hs1pSetHWParamsAndWait(varity, freq, voltage) { + var _this = this; + var pktSetParam = { + header: Buffer.from(pktHeader), + type: Buffer.from([TypeSetHWParams]), + version: Buffer.from([PV]), + pktlen: Buffer.from([0x10, 0x00, 0x00, 0x00]), + flag:Buffer.from([0xA2]), + voltage: Buffer.alloc(2), + freq:Buffer.alloc(2), + varity:Buffer.alloc(4), + targettemp:Buffer.from([80]), + ender:Buffer.from(pktEnder) + }; + pktSetParam.varity.writeUInt32LE(varity, 0); + pktSetParam.freq.writeUInt16LE(freq, 0); + pktSetParam.voltage.writeUInt16LE(voltage, 0); + _this.recvStatePkt = false; + _this.hs1pSendPkt(pktSetParam); + + return new Promise(function (resolve, reject) { + waitUntil() + .interval(20) + .times(50) + .condition(function() { + return _this.recvStatePkt + }) + .done(function(result) { + if(result === false) { + Debug.IbctLogErr(COMP, _this.devPath, '设置矿机参数出错'); + resolve(1); + } else { + resolve(0); + } + }) + }) + } + + async hs1pWriteJob(jobID, snonce, enonce, target, data) { + var _this = this; + _this.recvJobPkt = false; + var pktSendJob = { + header: Buffer.from(pktHeader), + type: Buffer.from([TypeSendWork]), + version: Buffer.from([PV]), + pktlen: Buffer.alloc(4), + target:Buffer.alloc(8), + startNonce: Buffer.alloc(8), + endNonce:Buffer.alloc(8), + jobNum: Buffer.from([1]), + jobID: Buffer.alloc(1), + jobData:Buffer.alloc(128), + ender:Buffer.from(pktEnder) + }; + + pktSendJob.pktlen.writeUInt32LE(160, 0); //pktlen + target.copy(pktSendJob.target, 0); + snonce.copy(pktSendJob.startNonce, 0); + enonce.copy(pktSendJob.endNonce, 0); + pktSendJob.jobID[0] = jobID; + data.copy(pktSendJob.jobData, 0); + + _this.hs1pSendPkt(pktSendJob); + return new Promise(function (resolve, reject) { + waitUntil() + .interval(40) + .times(50) + .condition(function() { + return _this.recvJobPkt + }) + .done(function(result) { + if(result === false) { + Debug.IbctLogErr(COMP, _this.devPath, '发送Work失败'); + reject(new Error(__('发送Work失败')).message); + } else { + resolve(0); + } + }) + }) + } + + hs1pSendFWPktAndWait(cnt, FWPKT) { + var _this = this; + _this.recvFWStatePkt = false; + _this.firmware.curState = 0; + _this.firmware.curID = 0; + _this.hs1pSendPkt(FWPKT); + return new Promise(function (resolve, reject) { + waitUntil() + .interval(20) + .times(50) + .condition(function() { + return _this.recvFWStatePkt + }) + .done(function(result) { + if(result === false) { + Debug.IbctLogErr(COMP, _this.devPath, '烧录连接超时'); + reject(new Error(__('烧录连接超时')).message); + } else if(_this.firmware.curState === 0x02) { + if(cnt < _this.firmware.retryCnt){ + resolve(1); + } else { + Debug.IbctLogErr(COMP, _this.devPath, '串口出错'); + reject(new Error(__('串口出错')).message); + } + } else { + resolve(0); + } + }) + }) + } + + async stopScanWork() { + var _this = this; + Debug.IbctLogDbg(COMP, _this.devPath, 'stopScanWork ...'); + //_this.curJobid = 0; + _this.MinerShouldStop = true; + } + + async scanWork(workQueue, callback) { + var _this = this; + Debug.IbctLogInfo(COMP, _this.devPath, 'scanWork Begin ...'); + + if (!_this.inited) { + callback(null, null, null); + return; + } + + if(_this.status.scantime === 0) { + _this.status.scantime = 7000; //10s default + } + + if (_this.MinerShouldStop) { + _this.MinerShouldStop = false; + if (_this.job_id === workQueue[0].job_id) { + // new job arrived + _this.fakeJob.overScan = false; + // clean all workQueue + workQueue.splice(0, workQueue.length); + callback(null, null, _this.fakeJob); + Debug.IbctLogDbg("new job arrived, scanWork again"); + return; + } + } + _this.job_id = workQueue[0].job_id; + + for(let i = 0; i < _this.info.workDepth; i++) { + _this.Job[i] = workQueue.pop(); + + _this.work[i].target = Buffer.alloc(8); + var target = _this.Job[i].target; + if(Buffer.compare(hwTarget, target) > 0) { + hwTarget.copy(_this.work[i].target, 0); + _this.Job[i].hwTarget = hwTarget; + } else { + target.copy(_this.work[i].target, 0); + _this.Job[i].hwTarget = target; + } + + _this.work[i].target = _this.work[i].target.swap64(); + + _this.work[i].highNonce = parseInt((_this.Job[i].snonce) / 0x100000000) >>> 0; + _this.work[i].data = Buffer.from(_this.Job[i].data); + + _this.work[i].snonce = Buffer.alloc(8); + _this.work[i].snonce.writeUInt32LE(0, 0); + _this.work[i].snonce.writeUInt32LE(_this.work[i].highNonce, 4); + //Job.data.copy(_this.work[i].payload, 0); + //Debug.IbctLogDbg(COMP, _this.work[i].payload.toString('hex')); + + _this.work[i].enonce = Buffer.alloc(8); + _this.work[i].enonce.writeUInt32LE(0xffffffff, 0); + _this.work[i].enonce.writeUInt32LE(_this.work[i].highNonce + 32, 4); + + _this.submitNonce = callback; + // _this.MinerShouldStop = false; + _this.jobsDone++; + + _this.work[i].jobID = _this.curJobid; + _this.curJobid = (_this.curJobid + 1) & 0xff; + if (!_this.curJobid) _this.curJobid = 1; + /* + Debug.IbctLogInfo(COMP, _this.devPath, 'Work Target:', _this.work[i].target.toString('hex'), + 'Work highNonce:', _this.work[i].highNonce.toString(16), + 'Work jobID:', _this.work[i].jobID); + */ + + await _this.hs1pWriteJob(_this.work[i].jobID, _this.work[i].snonce, _this.work[i].enonce, _this.work[i].target, _this.work[i].data); + } + + var interval = 50; //ms + var times = 7000 / interval; + + waitUntil() + .interval(interval) + .times(times) + .condition(function() { + return _this.MinerShouldStop + }) + .done(function(result) { + Debug.IbctLogDbg(COMP, _this.devPath, 'ScanWork Exit', result ? "(NewJob)...": "(ScanTime Out)..."); + if (result === false) + _this.fakeJob.overScan = true; + else + _this.fakeJob.overScan = false; + callback(null, null, _this.fakeJob); + }); + } + + async setDevice(varity, freq, voltage) { + var _this = this; + _this.hs1pSetHWParams(varity, freq, voltage); + } + + getInfo() { + var _this = this; + return _this.info; + } + + getPlt() { + var _this = this + return _this.plt + } + + getState() { + var _this = this; + return _this.status; + } + + async detect(modelName) { + var _this = this; + Debug.IbctLogErr(COMP, _this.devPath, 'hs1p detect...'); + return await _this.hs1pGetStaticInfo(modelName); + } + + async init(params) { + var _this = this; + Debug.IbctLogErr(COMP, _this.devPath, 'hs1p init...'); + if(_this.firmware.updating === true){ + Debug.IbctLogErr(COMP, _this.devPath, 'Still Updating'); + return 1; + } + if( _this.inited === true){ + Debug.IbctLogErr(COMP, _this.devPath, 'Already Inited. return now'); + return 0; + } + + //_this.hs1pSetHWParams(hs1pcfg.varity, hs1pcfg.targetFreq, hs1pcfg.targetVoltage); + var ret = await _this.hs1pSetHWParamsAndWait(_this.varity, hs1pcfg.targetFreq, hs1pcfg.targetVoltage); + if (ret) + return ret; + + if (_this.intervalObj) { + clearInterval(_this.intervalObj); + _this.intervalObj = null; + } + + _this.intervalObj = setInterval(function() { + Debug.IbctLogErr(COMP, _this.devPath, 'Temp', _this.status.temp, 'RPM', _this.status.rpm, 'Tempwarn', _this.status.tempwarn, 'Fanwarn', _this.status.fanwarn, 'Powerwarn', _this.status.powerwarn,'Freq', _this.status.freq, 'Jobs', _this.jobsDone); + _this.hs1pGetState(_this); + waitUntil() + .interval(50) + .times(10) + .condition(function() { + return _this.recvQueryStatePkt + }) + .done(function(result) { + if(result === false) { + _this.txTimeoutCnt++; + Debug.IbctLogErr(COMP, _this.devPath, 'hs1pGetState Timeout ', _this.txTimeoutCnt); + if(_this.txTimeoutCnt > 10) { + _this.emit("error", __('获得矿机状态超时')); + _this.txTimeoutCnt = 0; + } + } else { + _this.txTimeoutCnt = 0; + + if(_this.status.fanwarn) + _this.emit("error", __('矿机风扇异常')); + + if(_this.status.powerwarn) + _this.emit("error", __('矿机电源异常')); + + if((_this.status.temp > hs1pcfg.offTemp) || (_this.status.tempwarn)) { + _this.emit("error", __('矿机高温关机')); + } else if(_this.status.temp > hs1pcfg.warnTemp && _this.status.temp < hs1pcfg.offTemp) { + _this.emit("warning", __('矿机高温警报')); + } + } + }); + }, 5000); + _this.inited = true; + return 0; + } + + async stop(enable, wait) { + var _this = this; + Debug.IbctLogErr(COMP, _this.devPath, 'hs1p', enable ? 'remove...' : 'stop...'); + if (!enable) { + await _this.hs1pSetHWParamsAndWait(0, 0, 0); + } + _this.inited = false; + _this.txTimeoutCnt = 0; + _this.freq = 0; + _this.voltage = 0; + _this.work.jobID = 0; + _this.jobsDone = 0; + _this.MinerShouldStop = true; + + if (_this.intervalObj) { + clearInterval(_this.intervalObj); + _this.intervalObj = null; + } + if (enable) { + if (wait === true) { + await _this.port.flush(function (err) { + _this.port.close(function(err) { + if(err) + Debug.IbctLogDbg(COMP, err.message); + }); + }); + } else { + await this.port.flush() + await this.port.close(function(err) { + if(err) + Debug.IbctLogDbg(COMP, err.message); + }); + return new Promise(function (resolve) { + waitUntil() + .interval(10) + .times(50) + .condition(function() { + return !_this.port.isOpen; + }) + .done(function() { + resolve(0); + }) + }) + } + } + } + + async burnFirmware(firmware, callback) { + var _this = this; + if(_this.firmware.updating === true) { + callback(__('升级中,请等待')); + return; + } + if(_this.inited === true) { + callback(__('挖矿中,请先暂停挖矿')); + return; + } + + var ret = await _this.hs1pGetStaticInfo(_this.info.modelName); + if (ret === 1) { + callback(__('无法获取当前版本号')); + return; + } + + var init = await _this.hs1pBurnFWInit(); + if(init === 0) { + _this.firmware.updating = true; + } else { + _this.firmware.updating = false; + callback(__('烧入固件初始化失败')); + return; + } + var fwLen = firmware.length; + var totalPktNum = Math.ceil(firmware.length / _this.firmware.FWPageSize) + var id = 0; + Debug.IbctLogErr(COMP, _this.devPath, 'BurnFW, CurVersion:', _this.info.firmwareVer, 'FW lenth' , fwLen , "PKTnum", totalPktNum); + try { + while(fwLen > 0) { + var currentLen = (fwLen > _this.firmware.FWPageSize) ? _this.firmware.FWPageSize : fwLen; + var curStart = firmware.length - fwLen; + var curEnd = curStart + currentLen; + + var pktUpdateFW = { + header: Buffer.from(pktHeader), + type: Buffer.from([TypeUpdateFW]), + version: Buffer.from([PV]), + pktLen: Buffer.alloc(4), + pktID:Buffer.alloc(4), + pageSize:Buffer.alloc(4), + curLen:Buffer.alloc(4), + flag:Buffer.alloc(1), + crc32: Buffer.alloc(4), + fwData:Buffer.alloc(currentLen), + ender:Buffer.from(pktEnder) + }; + pktUpdateFW.curLen.writeUInt32LE(currentLen, 0); + pktUpdateFW.pktID.writeUInt32LE(id, 0); + pktUpdateFW.pageSize.writeUInt32LE(_this.firmware.FWPageSize, 0); + pktUpdateFW.pktLen.writeUInt32LE(23 + currentLen); + firmware.copy(pktUpdateFW.fwData, 0, curStart, curEnd); + fwLen -= currentLen; + + if(fwLen) { + pktUpdateFW.flag[0] = 0x00; + } else { + pktUpdateFW.flag[0] = 0x01;//last + } + var msg = Buffer.alloc(23 + currentLen); + //console.log('3====>', curStart, curEnd, currentLen, fwLen); + pktUpdateFW.type.copy(msg, 0); + pktUpdateFW.version.copy(msg, 1); + pktUpdateFW.pktLen.copy(msg, 2); + pktUpdateFW.pktID.copy(msg, 6); + pktUpdateFW.pageSize.copy(msg, 10); + pktUpdateFW.curLen.copy(msg, 14); + pktUpdateFW.flag.copy(msg, 18); + pktUpdateFW.crc32.copy(msg, 19); + pktUpdateFW.fwData.copy(msg, 23); + + + var crcValue = parseInt(crc32(msg), 16); + pktUpdateFW.crc32.writeUInt32LE(crcValue, 0); + for(var i = 0; i < _this.firmware.retryCnt; i++) { + var success = await _this.hs1pSendFWPktAndWait(i + 1, pktUpdateFW); + if(success === 0) + break; + } + + if(fwLen === 0) { + _this.firmware.updating = false; + } + callback(null, ((id + 1) / totalPktNum).toFixed(3)); + id++; + } + } + catch(err) { + Debug.IbctLogErr(COMP, _this.devPath, 'Updat firmware failed ' + err.message); + _this.firmware.updating = false; + callback(err.message); + } + } + + async rebootDev() { + Debug.IbctLogErr(COMP, this.devPath, 'reboot'); + var _this = this; + var pktReboot = { + header: Buffer.from(pktHeader), + type: Buffer.from([TypeReboot]), + version: Buffer.from([PV]), + pktlen: Buffer.from([0x6, 0x0 ,0x0 ,0x0]), + ender:Buffer.from(pktEnder) + }; + await _this.stop(true, true) + _this.hs1pSendPkt(pktReboot); + } + + setPlt() { + var _this = this + console.log('function test') + _this.recvPltPkt = false + var pktPLT = { + header: Buffer.from(pktHeader), + type: Buffer.from([TypePlt]), + version: Buffer.from([PV]), + pktlen: Buffer.from([0x6, 0x0, 0x0, 0x0]), + ender: Buffer.from(pktEnder) + } + + _this.hs1pSendPkt(pktPLT) + } + + setLed(Enable) { + Debug.IbctLogErr(COMP, 'Set', this.devPath, 'Led to', Enable ? 'ON' : 'OFF'); + var _this = this; + var ledFlag = Enable === true ? 1 : 0; + var pktSetLED = { + header: Buffer.from(pktHeader), + type: Buffer.from([TypeSetLED]), + version: Buffer.from([PV]), + pktlen: Buffer.from([0xb, 0x0 ,0x0 ,0x0]), + Flag:Buffer.from([ledFlag]), + led:Buffer.from([0xE8, 0x3, 0xC8, 0x0]), //ON 1s OFF:200ms + ender:Buffer.from(pktEnder) + }; + _this.hs1pSendPkt(pktSetLED); + } + + async burnSNInfo(ptinfo) { + var _this = this + Debug.IbctLogErr(COMP, _this.devPath, 'Burn Sn Num..', ptinfo) + var pktPTInfo = { + header: Buffer.from(pktHeader), + type: Buffer.from([TypeProductTest]), + version: Buffer.from([PV]), + pktlen: Buffer.from([70, 0x0, 0x0, 0x0]), + SNInfo: Buffer.alloc(32), + HashInfo: Buffer.alloc(32), + ender: Buffer.from(pktEnder) + } + if (ptinfo.sn) { + var sn = Buffer.from(ptinfo.sn) + pktPTInfo.SNInfo[0] = sn.length + sn.copy(pktPTInfo.SNInfo, 1) + } else { + pktPTInfo.SNInfo[0] = 0 + } + _this.recvPTInfoPkt = false; + _this.hs1pSendPkt(pktPTInfo) + + return new Promise(function (resolve, reject) { + waitUntil() + .interval(50) + .times(10) + .condition(function () { + return _this.recvPTInfoPkt + }) + .done(function (result) { + if (result === false) { + Debug.IbctLogErr(COMP, _this.devPath, 'Burn SN num failed..') + resolve(__('写入SN序列号失败')) + } else { + resolve(null) + } + }) + }) + } +} + +module.exports = function Geths1p(options = {}) { + return new hs1p(options); +} diff --git a/src/miner/lb1.js b/src/miner/lb1.js new file mode 100644 index 0000000..79ec390 --- /dev/null +++ b/src/miner/lb1.js @@ -0,0 +1,922 @@ +const Delimiter = require('@serialport/parser-delimiter'); +const Debug = require('../log')(); +var SerialPort = require('serialport'); +const EventEmitter = require('events'); +var waitUntil = require('../waitUntil'); +var crc32 = require('crc32'); +const COMP = '[LB1]:'; + +const hwTarget = Buffer.from([0x00, 0x00, 0x00, 0x00, 0x33, 0x33, 0x33, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]); + +const PV = 0x10; +const TypeSetBootMode = 0xA3; +const TypeUpdateFW = 0xAA; +const TypeQueryInfo = 0xA4; +const TypeProductTest = 0xAB; +const TypePlt = 0xAD +const TypeSetLED = 0xA6; +const TypeSendWork = 0xA1; +const TypeSetHWParams = 0xA2; +const TypeReboot = 0xAC; +const TypeRecvNonce = 0x51; +const TypeRecvJob = 0x55; +const TypeRecvState = 0X52; +const TypeRecvBootMode = 0x53; +const TypeRecvInfo = 0x54; +const TypeRecvFWState = 0x5A; +const TypeRecvTestResult = 0x5B; +const TypeRecvPltResult = 0x5D + +const pktHeader = Buffer.from([0xA5, 0x3C, 0x96]); +const pktEnder = Buffer.from([0x69, 0xC3, 0x5A]); +const typeOffset = 0; + +const lb1cfg = { + model: "Goldshell-LB1", + algo: "lbry", + varity: 0x11, + targetFreq: 750, //MHz + targetVoltage: 430, //mv + targetTemp: 65, + warnTemp: 115, + offTemp: 125 +}; + +class lb1 extends EventEmitter { + constructor({devPath, algo, varity, crypto}) { + super(); + var _this = this; + + _this.devPath = devPath; + _this.algo = algo; + _this.crypto = crypto; + _this.MinerShouldStop = false; + _this.inited = false; + _this.txTimeoutCnt = 0; + _this.work = [{ jobID: 0 }, { jobID: 0 }, { jobID: 0 }, { jobID: 0 }, { jobID: 0 }, { jobID: 0 }, { jobID: 0 }, { jobID: 0 }]; + _this.Job = []; + _this.fakeJob = { overScan:false }; + _this.job_id = null; + + _this.info = { + firmwareVer: 'V0.0.1', + modelName: 'Goldshell-LB1', + sn: 'unknown', + mc: 'unknown', + snAbbr: 'LB10', + workDepth: 8 + }; + + _this.firmware = { + retryCnt : 2, + FWPageSize : 256, + curState : 0, + curID : 0 + } + _this.submitNonce = null; + _this.curJobid = 1; + _this.jobsDone = 0; + + _this.status = { + chips:0, + temp:0, + voltage:0, + freq:0, + varity:0x11, + cores:0, + goodcores:0, + scanbits:0, + scantime:0, + tempwarn:0, + fanwarn:0, + powerwarn:0, + rpm:0 + }; + + _this.plt = { + chipid: 0, + reason: '' + } + + _this.port = new SerialPort(_this.devPath, { + baudRate: 115200, + dataBits:8, + stopBits:1, + parity:'none', + rtscts:false + }, function (err) { + if (err) { + Debug.IbctLogErr(COMP, _this.devPath, '打开串口失败: ', err.message); + return; + } else + Debug.IbctLogErr(COMP, _this.devPath, '打开串口成功'); + + }); + + const parser = _this.port.pipe(new Delimiter({ delimiter: pktEnder})); + parser.on('data', function(data) { + _this.lb1ParseNotify(_this, data); + }); + + _this.on("error", function(err) { + Debug.IbctLogDbg(COMP, err); + }) + + _this.on("warning", function(err) { + Debug.IbctLogDbg(COMP, err); + }) + } + + lb1ParseNotify(_this, data) { + //Debug.IbctLogInfo(COMP, _this.devPath, 'Recv Pkt', data.toString('hex')); + var location = data.indexOf(pktHeader); + if(location === -1) { + Debug.IbctLogErr(COMP, _this.devPath, 'Recv Invalid PKT', data); + return; + } + var typeLocation = location + pktHeader.length + typeOffset; + switch(data[typeLocation]) { + + case TypeRecvNonce: + var jobID = data[9]; + //var chipID = data[10]; + //var coreID = data[11]; + var nonce_l = data.readUInt32LE(12); + var tt = data.readUInt32LE(16); + var nonce = Buffer.alloc(8, 0); + nonce.writeUInt32LE(nonce_l, 0); + nonce.writeUInt32LE(tt, 4); + if(jobID !== _this.work[0].jobID && + jobID !== _this.work[1].jobID && + jobID !== _this.work[2].jobID && + jobID !== _this.work[3].jobID && + jobID !== _this.work[4].jobID && + jobID !== _this.work[5].jobID && + jobID !== _this.work[6].jobID && + jobID !== _this.work[7].jobID) { + Debug.IbctLogDbg(COMP, _this.devPath, 'Find Stale ', jobID, _this.work[0].jobID, _this.work[1].jobID, nonce.toString('hex')); + } else { + //console.log("noncel", nonce_l.toString(16)); + //console.log("ntime", ntime.toString(16)); + //Debug.IbctLogDbg(COMP, _this.devPath, 'Find Nonce ', nonce.toString('hex')); + let i; + if (jobID === _this.work[0].jobID) i = 0; + else if (jobID === _this.work[1].jobID) i = 1; + else if (jobID === _this.work[2].jobID) i = 2; + else if (jobID === _this.work[3].jobID) i = 3; + else if (jobID === _this.work[4].jobID) i = 4; + else if (jobID === _this.work[5].jobID) i = 5; + else if (jobID === _this.work[6].jobID) i = 6; + else i = 7; + + if(_this.submitNonce) + _this.submitNonce(null, nonce, _this.Job[i]); + } + break; + + case TypeRecvInfo: + _this.recvInfoPkt = true; + //Debug.IbctLogDbg(COMP, _this.devPath, 'RecvInfoPkt:', data.toString('hex')); + _this.info.modelName = data.toString('utf8', 10, 10 + data[9]); + _this.info.firmwareVer = data.toString('utf8', 27, 27 + data[26]); + _this.info.sn = data.toString('utf8', 36, 36 + data[35]); + _this.info.mc = data.toString('utf8', 53, 55); + _this.info.snAbbr = 'LB10'; + //console.log(_this.info); + break; + + case TypeRecvFWState: + _this.recvFWStatePkt = true; + _this.firmware.curID = data.readUInt32LE(9); + _this.firmware.curState = data[13]; + break; + + case TypeRecvJob: + _this.recvJobPkt = true; + break; + + case TypeRecvBootMode: + _this.recvBootModePkt = true; + break; + + case TypeRecvPltResult: + _this.recvPltPkt = true; + _this.plt.chipid = data[9]; + _this.plt.reason = data.toString('utf8', 10, 11); + break; + + case TypeRecvState: + _this.recvStatePkt = true; + _this.recvQueryStatePkt = true; + //Debug.IbctLogInfo(COMP, _this.devPath, 'RecvState pkt:', data.toString('hex')); + _this.status.chips = data[9]; + _this.status.cores = data[10]; + _this.status.goodcores = data[11]; + _this.status.scanbits = data[12]; + _this.status.scantime = data.readUInt16LE(13) * 100; //ms + _this.status.voltage = data.readUInt16LE(15); //mV + _this.status.freq = data.readUInt16LE(17); //MHz + _this.status.varity = data.readUInt32LE(19); + _this.status.temp = data[23]; + _this.status.hwreboot = data[24]; + _this.status.tempwarn = data[25]; + _this.status.fanwarn = data[26]; + _this.status.powerwarn = data[27]; + _this.status.rpm = data.readUInt16LE(28); + break; + + case TypeRecvTestResult: + _this.recvPTInfoPkt = true; + break; + + default: + Debug.IbctLogErr(COMP, _this.devPath, 'Recv Unsupported PKT Type', data[location + typeOffset]) + break; + } + return; + } + + lb1SendPkt (pkt) { + var _this = this; + var offset = 0; + var length = 0; + Object.keys(pkt).forEach(function(key) { + if(Buffer.isBuffer(pkt[key])) { + length += pkt[key].length; + } + }); + + var msg = Buffer.alloc(length); + + Object.keys(pkt).forEach(function(key) { + if(Buffer.isBuffer(pkt[key])) { + pkt[key].copy(msg, offset); + offset += pkt[key].length; + } + }); + + //Debug.IbctLogDbg(COMP, _this.devPath, 'Send pkt', msg.toString('hex')); + _this.port.write(msg, function(err) { + if (err) { + Debug.IbctLogErr(COMP, _this.devPath, 'Error on write: ', err.message) + } + _this.port.drain(function(err) { + if (err) { + Debug.IbctLogErr(COMP, _this.devPath, 'Error on Drain: ', _this.devPath, err.message) + } + }) + }) + } + + lb1GetState(_this) { + var pktQueryStatus = { + header: Buffer.from(pktHeader), + type: Buffer.from([TypeSetHWParams]), + version: Buffer.from([PV]), + pktlen: Buffer.from([0x7, 0x0 ,0x0 ,0x0]), + flag:Buffer.from([0x52]), + ender:Buffer.from(pktEnder) + }; + _this.recvQueryStatePkt = false; + _this.lb1SendPkt(pktQueryStatus); + /*TODO Wait response here*/ + return true; + } + + async lb1GetStaticInfo(modelName) { + var _this = this; + var pktQueryInfo = { + header: Buffer.from(pktHeader), + type: Buffer.from([TypeQueryInfo]), + version: Buffer.from([PV]), + pktlen: Buffer.from([0x6, 0x0 ,0x0 ,0x0]), + ender:Buffer.from(pktEnder) + }; + _this.recvInfoPkt = false; + _this.lb1SendPkt(pktQueryInfo); + return new Promise(function (resolve, reject) { + waitUntil() + .interval(50) + .times(10) + .condition(function() { + return _this.recvInfoPkt + }) + .done(function(result) { + if(result === false) { + Debug.IbctLogErr(COMP, _this.devPath, '获取矿机信息出错'); + resolve(2); + } else { + if(modelName === _this.info.modelName) { + resolve(0); + } else { + resolve(1); + } + } + }) + }); + } + + lb1SetBootMode() { + var _this = this; + var pktSetBootMode = { + header: Buffer.from(pktHeader), + type: Buffer.from([TypeSetBootMode]), + version: Buffer.from([PV]), + pktlen: Buffer.from([0x7, 0x0 ,0x0 ,0x0]), + ender:Buffer.from(pktEnder) + }; + _this.recvBootModePkt = false; + _this.lb1SendPkt(pktSetBootMode); + } + + lb1BurnFWInit() { + var _this = this; + var pktSetBootMode = { + header: Buffer.from(pktHeader), + type: Buffer.from([TypeSetBootMode]), + version: Buffer.from([PV]), + pktlen: Buffer.from([0x6, 0x0 ,0x0 ,0x0]), + ender:Buffer.from(pktEnder) + }; + + _this.recvBootModePkt = false; + _this.lb1SendPkt(pktSetBootMode); + return new Promise(function (resolve, reject) { + waitUntil() + .interval(20) + .times(50) + .condition(function() { + return _this.recvBootModePkt + }) + .done(function(result) { + if(result === false) { + resolve(1); + } else { + resolve(0); + } + }) + }) + } + + lb1SetHWParams(varity, freq, voltage) { + var _this = this; + var pktSetParam = { + header: Buffer.from(pktHeader), + type: Buffer.from([TypeSetHWParams]), + version: Buffer.from([PV]), + pktlen: Buffer.from([0x10, 0x00, 0x00, 0x00]), + flag:Buffer.from([0xA2]), + voltage: Buffer.alloc(2), + freq:Buffer.alloc(2), + varity:Buffer.alloc(4), + targettemp:Buffer.from([80]), + ender:Buffer.from(pktEnder) + }; + + pktSetParam.varity.writeUInt32LE(varity, 0); + pktSetParam.freq.writeUInt16LE(freq, 0); + pktSetParam.voltage.writeUInt16LE(voltage, 0); + _this.recvStatePkt = false; + _this.lb1SendPkt(pktSetParam); + return true; + } + + lb1SetHWParamsAndWait(varity, freq, voltage) { + var _this = this; + var pktSetParam = { + header: Buffer.from(pktHeader), + type: Buffer.from([TypeSetHWParams]), + version: Buffer.from([PV]), + pktlen: Buffer.from([0x10, 0x00, 0x00, 0x00]), + flag:Buffer.from([0xA2]), + voltage: Buffer.alloc(2), + freq:Buffer.alloc(2), + varity:Buffer.alloc(4), + targettemp:Buffer.from([80]), + ender:Buffer.from(pktEnder) + }; + pktSetParam.varity.writeUInt32LE(varity, 0); + pktSetParam.freq.writeUInt16LE(freq, 0); + pktSetParam.voltage.writeUInt16LE(voltage, 0); + _this.recvStatePkt = false; + _this.lb1SendPkt(pktSetParam); + + return new Promise(function (resolve, reject) { + waitUntil() + .interval(20) + .times(50) + .condition(function() { + return _this.recvStatePkt + }) + .done(function(result) { + if(result === false) { + Debug.IbctLogErr(COMP, _this.devPath, '设置矿机参数出错'); + resolve(1); + } else { + resolve(0); + } + }) + }) + } + + async lb1WriteJob(jobID, snonce, enonce, target, data) { + var _this = this; + _this.recvJobPkt = false; + var pktSendJob = { + header: Buffer.from(pktHeader), + type: Buffer.from([TypeSendWork]), + version: Buffer.from([PV]), + pktlen: Buffer.alloc(4), + target:Buffer.alloc(8), + startNonce: Buffer.alloc(8), + endNonce:Buffer.alloc(8), + jobNum: Buffer.from([1]), + jobID: Buffer.alloc(1), + jobData:Buffer.alloc(136), + ender:Buffer.from(pktEnder) + }; + + pktSendJob.pktlen.writeUInt32LE(168, 0); //pktlen + target.copy(pktSendJob.target, 0); + snonce.copy(pktSendJob.startNonce, 0); + enonce.copy(pktSendJob.endNonce, 0); + pktSendJob.jobID[0] = jobID; + data.copy(pktSendJob.jobData, 0); + + _this.lb1SendPkt(pktSendJob); + return new Promise(function (resolve, reject) { + waitUntil() + .interval(40) + .times(50) + .condition(function() { + return _this.recvJobPkt + }) + .done(function(result) { + if(result === false) { + Debug.IbctLogErr(COMP, _this.devPath, '发送Work失败'); + reject(new Error(__('发送Work失败')).message); + } else { + resolve(0); + } + }) + }) + } + + lb1SendFWPktAndWait(cnt, FWPKT) { + var _this = this; + _this.recvFWStatePkt = false; + _this.firmware.curState = 0; + _this.firmware.curID = 0; + _this.lb1SendPkt(FWPKT); + return new Promise(function (resolve, reject) { + waitUntil() + .interval(20) + .times(50) + .condition(function() { + return _this.recvFWStatePkt + }) + .done(function(result) { + if(result === false) { + Debug.IbctLogErr(COMP, _this.devPath, '烧录连接超时'); + reject(new Error(__('烧录连接超时')).message); + } else if(_this.firmware.curState === 0x02) { + if(cnt < _this.firmware.retryCnt){ + resolve(1); + } else { + Debug.IbctLogErr(COMP, _this.devPath, '串口出错'); + reject(new Error(__('串口出错')).message); + } + } else { + resolve(0); + } + }) + }) + } + + async stopScanWork() { + var _this = this; + Debug.IbctLogDbg(COMP, _this.devPath, 'stopScanWork ...'); + //_this.curJobid = 0; + _this.MinerShouldStop = true; + } + + async scanWork(workQueue, callback) { + var _this = this; + Debug.IbctLogInfo(COMP, _this.devPath, 'scanWork Begin ...'); + + if (!_this.inited) { + callback(null, null, null); + return; + } + + if(_this.status.scantime === 0) { + _this.status.scantime = 6000; //10s default + } + + if (_this.MinerShouldStop) { + _this.MinerShouldStop = false; + if (_this.job_id === workQueue[0].job_id) { + // new job arrived + _this.fakeJob.overScan = false; + // clean all workQueue + workQueue.splice(0, workQueue.length); + callback(null, null, _this.fakeJob); + Debug.IbctLogDbg("new job arrived, scanWork again"); + return; + } + } + _this.job_id = workQueue[0].job_id; + + for(let i = 0; i < _this.info.workDepth; i++) { + _this.Job[i] = workQueue.pop(); + + _this.work[i].target = Buffer.alloc(8); + var target = _this.Job[i].target; + if(Buffer.compare(hwTarget, target) > 0) { + hwTarget.copy(_this.work[i].target, 0); + _this.Job[i].hwTarget = hwTarget; + } else { + target.copy(_this.work[i].target, 0); + _this.Job[i].hwTarget = target; + } + + _this.work[i].target = _this.work[i].target.swap64(); + + _this.work[i].highNonce = parseInt((_this.Job[i].snonce) / 0x100000000) >>> 0; + _this.work[i].data = Buffer.from(_this.Job[i].data); + + _this.work[i].snonce = Buffer.alloc(8); + _this.work[i].snonce.writeUInt32LE(0, 0); + _this.work[i].snonce.writeUInt32LE(_this.work[i].highNonce, 4); + //Job.data.copy(_this.work[i].payload, 0); + //Debug.IbctLogDbg(COMP, _this.work[i].payload.toString('hex')); + + _this.work[i].enonce = Buffer.alloc(8); + _this.work[i].enonce.writeUInt32LE(0xffffffff, 0); + _this.work[i].enonce.writeUInt32LE(0x00000000, 4); + //_this.work[i].enonce.writeUInt32LE(_this.work[i].highNonce + 32, 4); + + _this.submitNonce = callback; + // _this.MinerShouldStop = false; + _this.jobsDone++; + + _this.work[i].jobID = _this.curJobid; + _this.curJobid = (_this.curJobid + 1) & 0xff; + if (!_this.curJobid) _this.curJobid = 1; + /* + Debug.IbctLogInfo(COMP, _this.devPath, 'Work Target:', _this.work[i].target.toString('hex'), + 'Work highNonce:', _this.work[i].highNonce.toString(16), + 'Work jobID:', _this.work[i].jobID); + */ + + await _this.lb1WriteJob(_this.work[i].jobID, _this.work[i].snonce, _this.work[i].enonce, _this.work[i].target, _this.work[i].data); + } + + var interval = 50; //ms + var times = 6000 / interval; + + waitUntil() + .interval(interval) + .times(times) + .condition(function() { + return _this.MinerShouldStop + }) + .done(function(result) { + Debug.IbctLogDbg(COMP, _this.devPath, 'ScanWork Exit', result ? "(NewJob)...": "(ScanTime Out)..."); + if (result === false) + _this.fakeJob.overScan = true; + else + _this.fakeJob.overScan = false; + callback(null, null, _this.fakeJob); + }); + } + + async setDevice(varity, freq, voltage) { + var _this = this; + _this.lb1SetHWParams(varity, freq, voltage); + } + + getInfo() { + var _this = this; + return _this.info; + } + + getPlt() { + var _this = this + return _this.plt + } + + getState() { + var _this = this; + return _this.status; + } + + async detect(modelName) { + var _this = this; + Debug.IbctLogErr(COMP, _this.devPath, 'lb1 detect...'); + return await _this.lb1GetStaticInfo(modelName); + } + + async init(params) { + var _this = this; + Debug.IbctLogErr(COMP, _this.devPath, 'lb1 init...'); + if(_this.firmware.updating === true){ + Debug.IbctLogErr(COMP, _this.devPath, 'Still Updating'); + return 1; + } + if( _this.inited === true){ + Debug.IbctLogErr(COMP, _this.devPath, 'Already Inited. return now'); + return 0; + } + + //_this.lb1SetHWParams(lb1cfg.varity, lb1cfg.targetFreq, lb1cfg.targetVoltage); + var ret = await _this.lb1SetHWParamsAndWait(_this.varity, lb1cfg.targetFreq, lb1cfg.targetVoltage); + if (ret) + return ret; + + if (_this.intervalObj) { + clearInterval(_this.intervalObj); + _this.intervalObj = null; + } + + _this.intervalObj = setInterval(function() { + Debug.IbctLogErr(COMP, _this.devPath, 'Temp', _this.status.temp, 'RPM', _this.status.rpm, 'Tempwarn', _this.status.tempwarn, 'Fanwarn', _this.status.fanwarn, 'Powerwarn', _this.status.powerwarn,'Freq', _this.status.freq, 'Jobs', _this.jobsDone); + _this.lb1GetState(_this); + waitUntil() + .interval(50) + .times(10) + .condition(function() { + return _this.recvQueryStatePkt + }) + .done(function(result) { + if(result === false) { + _this.txTimeoutCnt++; + Debug.IbctLogErr(COMP, _this.devPath, 'lb1GetState Timeout ', _this.txTimeoutCnt); + if(_this.txTimeoutCnt > 10) { + _this.emit("error", __('获得矿机状态超时')); + _this.txTimeoutCnt = 0; + } + } else { + _this.txTimeoutCnt = 0; + + if(_this.status.fanwarn) + _this.emit("error", __('矿机风扇异常')); + + if(_this.status.powerwarn) + _this.emit("error", __('矿机电源异常')); + + if((_this.status.temp > lb1cfg.offTemp) || (_this.status.tempwarn)) { + _this.emit("error", __('矿机高温关机')); + } else if(_this.status.temp > lb1cfg.warnTemp && _this.status.temp < lb1cfg.offTemp) { + _this.emit("warning", __('矿机高温警报')); + } + } + }); + }, 5000); + _this.inited = true; + return 0; + } + + async stop(enable, wait) { + var _this = this; + Debug.IbctLogErr(COMP, _this.devPath, 'lb1', enable ? 'remove...' : 'stop...'); + if (!enable) { + await _this.lb1SetHWParamsAndWait(0, 0, 0); + } + _this.inited = false; + _this.txTimeoutCnt = 0; + _this.freq = 0; + _this.voltage = 0; + _this.work.jobID = 0; + _this.jobsDone = 0; + _this.MinerShouldStop = true; + + if (_this.intervalObj) { + clearInterval(_this.intervalObj); + _this.intervalObj = null; + } + if (enable) { + if (wait) { + await _this.port.flush(function (err) { + _this.port.close(function(err) { + if(err) + Debug.IbctLogDbg(COMP, err.message); + }); + }); + } else { + await this.port.flush() + await this.port.close(function(err) { + if(err) + Debug.IbctLogDbg(COMP, err.message); + }); + return new Promise(function (resolve) { + waitUntil() + .interval(10) + .times(50) + .condition(function() { + return !_this.port.isOpen; + }) + .done(function() { + resolve(0); + }) + }) + } + } + } + + async burnFirmware(firmWare, callback) { + var _this = this; + var firmware; + if(_this.firmware.updating === true) { + callback(__('升级中,请等待')); + return; + } + + if(_this.inited === true) { + callback(__('挖矿中,请先暂停挖矿')); + return; + } + + var ret = await _this.lb1GetStaticInfo(lb1cfg.model); + if (ret) { + callback(__('无法获取当前版本号')); + return; + } else { + var info = _this.getInfo(); + if (info.mc === 'm1') { + firmware = firmWare.slice(0, 114689); + } else if (info.mc === 'm2') { + firmware = firmWare.slice(114689); + } else { + return; + } + } + + var init = await _this.lb1BurnFWInit(); + if(init === 0) { + _this.firmware.updating = true; + } else { + _this.firmware.updating = false; + callback(__('烧入固件初始化失败')); + return; + } + var fwLen = firmware.length; + var totalPktNum = Math.ceil(firmware.length / _this.firmware.FWPageSize) + var id = 0; + Debug.IbctLogErr(COMP, _this.devPath, 'BurnFW, CurVersion:', _this.info.firmwareVer, 'FW lenth' , fwLen , "PKTnum", totalPktNum); + try { + while(fwLen > 0) { + var currentLen = (fwLen > _this.firmware.FWPageSize) ? _this.firmware.FWPageSize : fwLen; + var curStart = firmware.length - fwLen; + var curEnd = curStart + currentLen; + + var pktUpdateFW = { + header: Buffer.from(pktHeader), + type: Buffer.from([TypeUpdateFW]), + version: Buffer.from([PV]), + pktLen: Buffer.alloc(4), + pktID:Buffer.alloc(4), + pageSize:Buffer.alloc(4), + curLen:Buffer.alloc(4), + flag:Buffer.alloc(1), + crc32: Buffer.alloc(4), + fwData:Buffer.alloc(currentLen), + ender:Buffer.from(pktEnder) + }; + pktUpdateFW.curLen.writeUInt32LE(currentLen, 0); + pktUpdateFW.pktID.writeUInt32LE(id, 0); + pktUpdateFW.pageSize.writeUInt32LE(_this.firmware.FWPageSize, 0); + pktUpdateFW.pktLen.writeUInt32LE(23 + currentLen); + firmware.copy(pktUpdateFW.fwData, 0, curStart, curEnd); + fwLen -= currentLen; + + if(fwLen) { + pktUpdateFW.flag[0] = 0x00; + } else { + pktUpdateFW.flag[0] = 0x01;//last + } + var msg = Buffer.alloc(23 + currentLen); + //console.log('3====>', curStart, curEnd, currentLen, fwLen); + pktUpdateFW.type.copy(msg, 0); + pktUpdateFW.version.copy(msg, 1); + pktUpdateFW.pktLen.copy(msg, 2); + pktUpdateFW.pktID.copy(msg, 6); + pktUpdateFW.pageSize.copy(msg, 10); + pktUpdateFW.curLen.copy(msg, 14); + pktUpdateFW.flag.copy(msg, 18); + pktUpdateFW.crc32.copy(msg, 19); + pktUpdateFW.fwData.copy(msg, 23); + + + var crcValue = parseInt(crc32(msg), 16); + pktUpdateFW.crc32.writeUInt32LE(crcValue, 0); + for(var i = 0; i < _this.firmware.retryCnt; i++) { + var success = await _this.lb1SendFWPktAndWait(i + 1, pktUpdateFW); + if(success === 0) + break; + } + + if(fwLen === 0) { + _this.firmware.updating = false; + } + callback(null, ((id + 1) / totalPktNum).toFixed(3)); + id++; + } + } + catch(err) { + Debug.IbctLogErr(COMP, _this.devPath, 'Updat firmware failed ' + err.message); + _this.firmware.updating = false; + callback(err.message); + } + } + + async rebootDev() { + Debug.IbctLogErr(COMP, this.devPath, 'reboot'); + var _this = this; + var pktReboot = { + header: Buffer.from(pktHeader), + type: Buffer.from([TypeReboot]), + version: Buffer.from([PV]), + pktlen: Buffer.from([0x6, 0x0 ,0x0 ,0x0]), + ender:Buffer.from(pktEnder) + }; + await _this.stop(true, true) + _this.lb1SendPkt(pktReboot); + } + + setPlt() { + var _this = this + console.log('function test') + _this.recvPltPkt = false + var pktPLT = { + header: Buffer.from(pktHeader), + type: Buffer.from([TypePlt]), + version: Buffer.from([PV]), + pktlen: Buffer.from([0x6, 0x0, 0x0, 0x0]), + ender: Buffer.from(pktEnder) + } + + _this.lb1SendPkt(pktPLT) + } + + setLed(Enable) { + Debug.IbctLogErr(COMP, 'Set', this.devPath, 'Led to', Enable ? 'ON' : 'OFF'); + var _this = this; + var ledFlag = Enable === true ? 1 : 0; + var pktSetLED = { + header: Buffer.from(pktHeader), + type: Buffer.from([TypeSetLED]), + version: Buffer.from([PV]), + pktlen: Buffer.from([0xb, 0x0 ,0x0 ,0x0]), + Flag:Buffer.from([ledFlag]), + led:Buffer.from([0xE8, 0x3, 0xC8, 0x0]), //ON 1s OFF:200ms + ender:Buffer.from(pktEnder) + }; + _this.lb1SendPkt(pktSetLED); + } + + async burnSNInfo(ptinfo) { + var _this = this + Debug.IbctLogErr(COMP, _this.devPath, 'Burn Sn Num..', ptinfo) + var pktPTInfo = { + header: Buffer.from(pktHeader), + type: Buffer.from([TypeProductTest]), + version: Buffer.from([PV]), + pktlen: Buffer.from([70, 0x0, 0x0, 0x0]), + SNInfo: Buffer.alloc(32), + HashInfo: Buffer.alloc(32), + ender: Buffer.from(pktEnder) + } + if (ptinfo.sn) { + var sn = Buffer.from(ptinfo.sn) + pktPTInfo.SNInfo[0] = sn.length + sn.copy(pktPTInfo.SNInfo, 1) + } else { + pktPTInfo.SNInfo[0] = 0 + } + _this.recvPTInfoPkt = false; + _this.lb1SendPkt(pktPTInfo) + + return new Promise(function (resolve, reject) { + waitUntil() + .interval(50) + .times(10) + .condition(function () { + return _this.recvPTInfoPkt + }) + .done(function (result) { + if (result === false) { + Debug.IbctLogErr(COMP, _this.devPath, 'Burn SN num failed..') + resolve(__('写入SN序列号失败')) + } else { + resolve(null) + } + }) + }) + } +} + +module.exports = function Getlb1(options = {}) { + return new lb1(options); +} diff --git a/src/miner/minerApi.js b/src/miner/minerApi.js new file mode 100644 index 0000000..b22c0ce --- /dev/null +++ b/src/miner/minerApi.js @@ -0,0 +1,139 @@ +const EventEmitter = require('events'); +const cpu = require('./cpu'); +const hs1 = require('./hs1'); +const hs1plus = require('./hs1plus'); +const lb1 = require('./lb1'); +const unknow = require('./unknow'); +const Debug = require('../log')(); +const COMP = '[minerApi]'; + +var miners = [ + { + name: 'cpu', + api: cpu + }, + { + name: 'Goldshell-HS1', + api: hs1 + }, + { + name: 'Goldshell-HS1-Plus', + api: hs1plus + }, + { + name: 'Goldshell-LB1', + api: lb1 + }, + { + name: 'unknow', + api: unknow + } +]; + +class miner extends EventEmitter { + constructor({ + name, + devPath, + algo, + varity, + crypto + }) { + super(); + var _this = this; + + _this.name = name; + _this.devPath = devPath; + _this.algo = algo; + _this.varity = varity; + _this.crypto = crypto; + _this.minerApi = this.GetMinerApi(_this.name)({ + devPath: _this.devPath, + algo: _this.algo, + varity: _this.varity, + crypto: _this.crypto + }); + if (!_this.minerApi) { + _this.emit('error', __('不支持此矿机:'), _this.name); + } + + _this.minerApi.on("error", function (data) { + return _this.emit("error", data); + }) + _this.minerApi.on("warning", function (data) { + return _this.emit("warning", data); + }) + return _this; + } + + GetMinerApi(name) { + var dev = null; + miners.forEach(function (miner, index) { + if (miner.name === name) { + dev = miner; + } + }) + return dev ? dev.api : null; + } + + getInfo() { + /* 返回矿机信息,包括矿机名,固件版本。*/ + return this.minerApi ? this.minerApi.getInfo() : null; + } + + async detect(modelName) { + return this.minerApi ? await this.minerApi.detect(modelName) : null; + } + + async init(params) { + /* + 初始化硬件 + + */ + return this.minerApi ? await this.minerApi.init(params) : null; + } + async setDevice() { + /* + 设置Miner参数,电压, 频率,目标温度,报警温度。 + */ + return this.minerApi ? this.minerApi.setDevice() : null; + } + async scanWork(Job, Callback) { + return this.minerApi ? this.minerApi.scanWork(Job, Callback) : null; + } + async stop(enable) { + /* + 停止硬件工作并关闭硬件 + */ + return this.minerApi ? await this.minerApi.stop(enable) : null; + } + async release() { + this.minerApi.removeAllListeners("error"); + this.minerApi.removeAllListeners("warning"); + return await this.stop(true) + } + getState() { + /* + 获取当前设备状态, 温度,电压,频率,功耗等 + */ + return this.minerApi ? this.minerApi.getState() : null; + } + async stopScanWork() { + return this.minerApi ? this.minerApi.stopScanWork() : null; + } + setLed(Enable) { + return this.minerApi ? this.minerApi.setLed(Enable) : null; + } + reboot() { + return this.minerApi ? this.minerApi.rebootDev() : null; + } + updateImage(Image, Callback) { + return this.minerApi ? this.minerApi.burnFirmware(Image, Callback) : null; + } + async burnSNInfo(ptinfo) { + return this.minerApi ? await this.minerApi.burnSNInfo(ptinfo) : null; + } +} + +module.exports = function Getminer(options = {}) { + return new miner(options); +}; diff --git a/src/miner/unknow.js b/src/miner/unknow.js new file mode 100644 index 0000000..50ce3b3 --- /dev/null +++ b/src/miner/unknow.js @@ -0,0 +1,318 @@ +const Delimiter = require('@serialport/parser-delimiter'); +const Debug = require('../log')(); +var SerialPort = require('serialport'); +const EventEmitter = require('events'); +// var waitUntil = require('wait-until'); +var waitUntil = require('../waitUntil'); +var crc32 = require('crc32'); +const COMP = '[UNKNOW]:'; + +const hwTarget = Buffer.from([0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]); + +const PV = 0x10; +const TypeSetBootMode = 0xA3; +const TypeUpdateFW = 0xAA; +const TypeQueryInfo = 0xA4; +const TypeProductTest = 0xAB; +const TypeSetLED = 0xA6; +const TypeSendWork = 0xA1; +const TypeSetHWParams = 0xA2; +const TypeReboot = 0xAC; +const TypeRecvNonce = 0x51; +const TypeRecvJob = 0x55; +const TypeRecvState = 0X52; +const TypeRecvBootMode = 0x53; +const TypeRecvInfo = 0x54; +const TypeRecvFWState = 0x5A; +const TypeRecvTestResult = 0x5B; + +const pktHeader = Buffer.from([0xA5, 0x3C, 0x96]); +const pktEnder = Buffer.from([0x69, 0xC3, 0x5A]); +const typeOffset = 0; + +class unknow extends EventEmitter { + constructor({devPath, algo, varity, crypto}) { + super(); + var _this = this; + + _this.devPath = devPath; + _this.algo = algo; + _this.crypto = crypto; + _this.MinerShouldStop = false; + _this.inited = false; + _this.txTimeoutCnt = 0; + _this.work = [{ jobID: 0 }, { jobID: 0 }, { jobID: 0 }, { jobID: 0 }]; + _this.Job = []; + _this.fakeJob = { overScan:false }; + _this.job_id = null; + + _this.info = { + firmwareVer: 'V0.0.1', + modelName: 'unknow', + sn:'unknown', + hashRation:0, + workDepth:4, + }; + + _this.firmware = { + retryCnt : 2, + FWPageSize : 256, + curState : 0, + curID : 0 + } + _this.submitNonce = null; + _this.curJobid = 4; + _this.jobsDone = 0; + + _this.status = { + chips:0, + temp:0, + voltage:0, + freq:0, + varity:0x4, + cores:0, + goodcores:0, + scanbits:0, + scantime:0, + tempwarn:0, + fanwarn:0, + powerwarn:0, + rpm:0 + }; + + _this.port = new SerialPort(_this.devPath, { + baudRate: 115200, + dataBits:8, + stopBits:1, + parity:'none', + rtscts:false + }, function (err) { + if (err) { + Debug.IbctLogErr(COMP, _this.devPath, '打开串口失败: ', err.message); + return; + } else + Debug.IbctLogErr(COMP, _this.devPath, '打开串口成功'); + + }); + + const parser = _this.port.pipe(new Delimiter({ delimiter: pktEnder})); + parser.on('data', function(data) { + _this.unknowParseNotify(_this, data); + }); + + _this.on("error", function(err) { + Debug.IbctLogDbg(COMP, err); + }) + + _this.on("warning", function(err) { + Debug.IbctLogDbg(COMP, err); + }) + } + + unknowParseNotify(_this, data) { + //Debug.IbctLogInfo(COMP, _this.devPath, 'Recv Pkt', data.toString('hex')); + var location = data.indexOf(pktHeader); + if(location === -1) { + Debug.IbctLogErr(COMP, _this.devPath, 'Recv Invalid PKT', data); + return; + } + var typeLocation = location + pktHeader.length + typeOffset; + switch(data[typeLocation]) { + + case TypeRecvNonce: + var jobID = data[9]; + //var chipID = data[10]; + //var coreID = data[11]; + var nonce_l = data.readUInt32LE(12); + var nonce_h = data.readUInt32LE(16); + var nonce = Buffer.alloc(8); + nonce.writeUInt32LE(nonce_l, 0); + nonce.writeUInt32LE(nonce_h, 4); + if(jobID !== _this.work[0].jobID && jobID !== _this.work[1].jobID && jobID !== _this.work[2].jobID && jobID !== _this.work[3].jobID) { + Debug.IbctLogDbg(COMP, _this.devPath, 'Find Stale ', jobID, _this.work[0].jobID, _this.work[1].jobID, nonce.toString('hex')); + } else { + //Debug.IbctLogDbg(COMP, _this.devPath, 'Find Nonce ', nonce.toString('hex')); + let i; + if (jobID === _this.work[0].jobID) i = 0; + else if (jobID === _this.work[1].jobID) i = 1; + else if (jobID === _this.work[2].jobID) i = 2; + else i = 3; + + if(_this.submitNonce) + _this.submitNonce(null, nonce, _this.Job[i]); + } + break; + + case TypeRecvInfo: + _this.recvInfoPkt = true; + //Debug.IbctLogDbg(COMP, _this.devPath, 'RecvInfoPkt:', data.toString('hex')); + _this.info.modelName = data.toString('utf8', 10, 10 + data[9]); + _this.info.firmwareVer = data.toString('utf8', 43, 43 + data[42]); + _this.info.sn = data.toString('utf8', 52, 52 + data[51]); + _this.info.hashRation = data.readUInt16LE(85); + //_this.info.workDepth = data[117]; + Debug.IbctLogDbg(_this.info); + break; + + case TypeRecvFWState: + _this.recvFWStatePkt = true; + _this.firmware.curID = data.readUInt32LE(9); + _this.firmware.curState = data[13]; + break; + + case TypeRecvJob: + _this.recvJobPkt = true; + break; + + case TypeRecvBootMode: + _this.recvBootModePkt = true; + break; + + case TypeRecvState: + _this.recvStatePkt = true; + _this.recvQueryStatePkt = true; + //Debug.IbctLogInfo(COMP, _this.devPath, 'RecvState pkt:', data.toString('hex')); + _this.status.chips = data[9]; + _this.status.cores = data[10]; + _this.status.goodcores = data[11]; + _this.status.scanbits = data[12]; + _this.status.scantime = data.readUInt16LE(13) * 100; //ms + _this.status.voltage = data.readUInt16LE(15); //mV + _this.status.freq = data.readUInt16LE(17); //MHz + _this.status.varity = data.readUInt32LE(19); + _this.status.temp = data[23]; + _this.status.hwreboot = data[24]; + _this.status.tempwarn = data[25]; + _this.status.fanwarn = data[26] + // _this.status.powerwarn = data[27] + _this.status.rpm = data.readUInt16LE(28) + break; + + case TypeRecvTestResult: + _this.recvPTInfoPkt = true; + break; + + default: + Debug.IbctLogErr(COMP, _this.devPath, 'Recv Unsupported PKT Type', data[location + typeOffset]) + break; + } + return; + } + + unknowSendPkt (pkt) { + var _this = this; + var offset = 0; + var length = 0; + Object.keys(pkt).forEach(function(key) { + if(Buffer.isBuffer(pkt[key])) { + length += pkt[key].length; + } + }); + + var msg = Buffer.alloc(length); + + Object.keys(pkt).forEach(function(key) { + if(Buffer.isBuffer(pkt[key])) { + pkt[key].copy(msg, offset); + offset += pkt[key].length; + } + }); + + //Debug.IbctLogDbg(COMP, _this.devPath, 'Send pkt', msg.toString('hex')); + _this.port.write(msg, function(err) { + if (err) { + Debug.IbctLogErr(COMP, _this.devPath, 'Error on write: ', err.message) + } + _this.port.drain(function(err) { + if (err) { + Debug.IbctLogErr(COMP, _this.devPath, 'Error on Drain: ', _this.devPath, err.message) + } + }) + }) + } + + async stopScanWork() { + var _this = this; + Debug.IbctLogDbg(COMP, _this.devPath, 'stopScanWork ...'); + //_this.curJobid = 0; + _this.MinerShouldStop = true; + } + + async setDevice(varity, freq, voltage) { + Debug.IbctLogDbg(COMP, 'setDevice'); + } + + getInfo() { + return this.info; + } + + getState() { + return this.status; + } + + async detect(modelName) { + Debug.IbctLogErr(COMP, this.devPath, 'unknow detect...'); + return true; + } + + async init(params) { + Debug.IbctLogErr(COMP, this.devPath, 'unknow init...'); + return 0; + } + + async stop(enable) { + var _this = this; + Debug.IbctLogErr(COMP, _this.devPath, 'unknow', enable ? 'remove...' : 'stop...'); + _this.inited = false; + _this.txTimeoutCnt = 0; + _this.freq = 0; + _this.voltage = 0; + _this.work.jobID = 0; + _this.jobsDone = 0; + _this.MinerShouldStop = true; + + if (enable) { + await this.port.flush(function(err) { + _this.port.close(function(err) { + if(err) + Debug.IbctLogDbg(COMP, err.message); + }); + }) + } + } + + async rebootDev() { + Debug.IbctLogErr(COMP, this.devPath, 'reboot'); + var _this = this; + var pktReboot = { + header: Buffer.from(pktHeader), + type: Buffer.from([TypeReboot]), + version: Buffer.from([PV]), + pktlen: Buffer.from([0x6, 0x0 ,0x0 ,0x0]), + ender:Buffer.from(pktEnder) + }; + await _this.stop(true) + _this.unknowSendPkt(pktReboot); + } + + setLed(Enable) { + Debug.IbctLogErr(COMP, 'Set', this.devPath, 'Led to', Enable ? 'ON' : 'OFF'); + var _this = this; + var ledFlag = Enable === true ? 1 : 0; + var pktSetLED = { + header: Buffer.from(pktHeader), + type: Buffer.from([TypeSetLED]), + version: Buffer.from([PV]), + pktlen: Buffer.from([0xb, 0x0 ,0x0 ,0x0]), + Flag:Buffer.from([ledFlag]), + led:Buffer.from([0xE8, 0x3, 0xC8, 0x0]), //ON 1s OFF:200ms + ender:Buffer.from(pktEnder) + }; + _this.unknowSendPkt(pktSetLED); + } +} + +module.exports = function Getunknow(options = {}) { + return new unknow(options); +} diff --git a/src/ping.js b/src/ping.js new file mode 100644 index 0000000..d655a25 --- /dev/null +++ b/src/ping.js @@ -0,0 +1,74 @@ +var net = require('net'); + +var ping = function(o, callback, sorttag) { + var optionsArry = []; + if (Object.prototype.toString.call(o) === '[object Array]') { + optionsArry = o + } else if (Object.prototype.toString.call(o) === '[object Object]') { + optionsArry.push(o); + } else { + console.warn("parameter error"); + } + var optionSize = optionsArry.length; + var outArry = []; + optionsArry.forEach(function(item) { + var options = {}; + var i = 0; + var results = []; + options.address = item.address || 'localhost'; + options.port = item.port || 80; + options.attempts = item.attempts || 10; + options.timeout = item.timeout || 5000; + connect(options); + + function check(error, options) { + if (error) { + if (i < options.attempts) { + connect(options); + return; + } + } + callback(error, item.address); + }; + + function connect(options) { + var s = new net.Socket(); + var start = process.hrtime(); + s.connect(options.port, options.address, function() { + var time_arr = process.hrtime(start); + var time = (time_arr[0] * 1e9 + time_arr[1]) / 1e6; + results.push({ + seq: i, + time: time + }); + s.destroy(); + i++; + check(null, options); + }); + s.on('error', function(e) { + results.push({ + seq: i, + time: undefined, + err: e + }); + s.destroy(); + i++; + check(e, options); + }); + s.setTimeout(options.timeout, function() { + results.push({ + seq: i, + time: undefined, + err: Error('Request timeout') + }); + s.destroy(); + i++; + check('Request timeout', options); + }); + }; + }); + + + +}; +module.exports.ping = ping; diff --git a/src/protocol/HnsStratum.js b/src/protocol/HnsStratum.js new file mode 100644 index 0000000..388d224 --- /dev/null +++ b/src/protocol/HnsStratum.js @@ -0,0 +1,275 @@ +const EventEmitter = require('events'); +const stratum = require('./stratum/lib'); +const Debug = require('../log')(); +const uuid = require("uuid"); +const COMP = '[HnsStratum]'; + +class intStratum extends EventEmitter { + constructor({}) { + super(); + var _this = this; + + _this.client = null; + _this.online = false; + _this.auth = false; + _this.subinfo = null; + _this.subinfoturn = false; + _this.diffinfo = null; + _this.socket = null; + _this.sendQueue = []; + _this.user = null; + _this.pass = null; + _this.lastRecvPacket = null; + _this.lastSendPacket = null; + _this.id = uuid.v4(); + + return _this; + } + + stratumAuthorize(id, user, pass) { + return this.socket.stratumSend({ + 'method': 'mining.authorize', + 'id': id, + 'params': [user, pass] + }, true); + } + + stratumSubmit(id, submitData) { + this.socket.setLastActivity(); + return this.socket.stratumSend({ + 'method': 'mining.submit', + 'id': id, + 'params': submitData + }); + } + + setupLoginPacket(data, job, diff, info) { + // for setup for connect.js + var packet = { + id: data.id, + error: null, + result: { + id: this.id, + job: job, + diff: diff, + subscribe: info, + } + }; + return packet; + } + + getPacketByID(id) { + var packet = null; + this.sendQueue = this.sendQueue.filter(function (pkt) { + if (pkt.id === id && !packet) { + packet = pkt; + return false; + } else { + return true; + } + }); + + return packet; + } + + getPacketByMethod(method) { + var packet = null; + this.sendQueue = this.sendQueue.filter(function (pkt) { + if (pkt.method === method && !packet) { + packet = pkt; + return false; + } else { + return true; + } + }); + + return packet; + } + + init(ssl, host, port) { + var _this = this; + + if (_this.online) { + _this.kill(); + return; + } else if (!_this.client) { + _this.client = stratum.Client.$create(); + if (!_this.client) { + _this.emit('error', __('创建Stratum客户端失败')); + } + } + + _this.client.on('error', function (socket, e) { + _this.emit("error", __('网络连接中断')); + }); + + _this.client.on('timeout', function (socket, e) { + _this.emit("error", __('Socket请求超时')); + }); + + _this.client.on('mining.error', function (msg, socket) { + Debug.IbctLogErr(COMP, msg); + _this.emit("error", __('Stratum连接中断')); + }); + + _this.client.connect({ + host: host, + port: port + }).then(function (socket) { + Debug.IbctLogDbg(COMP, 'Connected! lets ask for subscribe'); + if (!_this.online) { + _this.online = true; + } + _this.socket = socket; + _this.emit('connect'); + }) + } + + kill() { + if (this.client) { + this.client.removeAllListeners(); + if (this.client.destroy) { + this.client.destroy(); + } + this.client = null; + } + if (this.sendQueue.length) + this.sendQueue.splice(0, this.sendQueue.length); + + this.lastRecvPacket = null; + this.lastSendPacket = null; + if (this.online) { + this.online = false; + this.auth = false; + this.removeAllListeners(); + } + } + + getPacket() { + var _this = this; + var packet = null; + // TODO: have done in init + // the client is a one-way communication, it receives data from the server after issuing commands + _this.client.on('mining', function (data, socket, type) { + // type will be either 'broadcast' or 'result' + Debug.IbctLogInfo(COMP, '<===' + type + ' = ', JSON.stringify(data)); + // you can issue more commands to the socket, it's the exact same socket as "client" variable + // in this example + // the socket (client) got some fields like: + // client.name = name of the worker + // client.authorized = if the current connection is authorized or not + // client.id = an UUID ([U]niversal [U]nique [ID]entifier) that you can safely rely on it's uniqueness + // client.subscription = the subscription data from the server + switch (data.method) { + case 'set_difficulty': + case 'mining.set_difficulty': + // server sent the new difficulty + Debug.IbctLogInfo(COMP, 'mining.set_difficulty'); + if (!socket.authorized) { + _this.diffinfo = data; + } else { + _this.emit('data', _this.setupLoginPacket(data, null,data.params,null)); + } + break; + case 'notify': + case 'mining.notify': + // server sent a new block + // Debug.IbctLogInfo(COMP, 'mining.notify'); + if (!_this.auth) { + _this.lastRecvPacket = data.params; + } else { + _this.emit('data', _this.setupLoginPacket(data, data.params,null,null)); + } + break; + default: + if (!socket.authorized) { + _this.subinfo = data; + Debug.IbctLogDbg(COMP, 'Asking for authorization'); + if (_this.lastSendPacket && _this.lastSendPacket.method === 'login' && !_this.auth) { + Debug.IbctLogErr(COMP, 'Login failed: ', _this.user); + if (data.error && data.error.message) + _this.emit('error', __('矿池登陆失败') + ': ' + data.error.message); + else if (data.error && (data.error instanceof Array) && data.error.length > 2) { + _this.emit('error', __('矿池登陆失败') + ': ' + data.error[1]) + } else + _this.emit('error', __('矿池登陆失败')); + return + } + packet = _this.getPacketByMethod('login'); + if (packet) { + _this.lastSendPacket = packet; + Debug.IbctLogInfo(COMP, 'Login User: ', _this.user); + _this.stratumAuthorize(packet.id, packet.params.login, packet.params.pass); + } + } else { + if (!_this.lastSendPacket) { + return + } + /*add this step for 6 block */ + if(_this.subinfoturn === true) { + _this.subinfo = data; + _this.subinfoturn = false; + } + if (_this.lastSendPacket.method === 'login' && !_this.auth) { + Debug.IbctLogDbg(COMP, 'We are authorized', _this.lastRecvPacket); + _this.emit('data', _this.setupLoginPacket(data, _this.lastRecvPacket, null, null)); + _this.auth = true; + if(_this.subinfo.result === null){ + _this.subinfoturn = true; + _this.socket.stratumSubscribe('icbtminer'); + } + + } + + if(typeof _this.subinfo.result === 'object' && _this.subinfo.result !== null) { + _this.emit('data', _this.setupLoginPacket(_this.subinfo, null, null, _this.subinfo.result)); + _this.subinfo.result = null; + } + + if( _this.diffinfo !== null) { + _this.emit('data', _this.setupLoginPacket(_this.diffinfo, null, _this.diffinfo.params, null)); + _this.diffinfo = null; + } + if (_this.lastSendPacket.method === 'submit') { + _this.emit('data', data); + } + } + } + }); + } + + sendPacket(message) { + var _this = this; + Debug.IbctLogInfo(COMP, 'sendPacket: ', JSON.stringify(message)); + this.sendQueue.push(message); + if (message.method === 'login') { + this.user = message.params.login; + this.pass = message.params.pass; + // After the first stratumSubscribe, the data will be handled internally + // and returned deferreds to be resolved / rejected through the event 'mining' + // above + this.socket.stratumSubscribe('icbtminer').then( + // This isn't needed, it's handled automatically inside the Client class + // but if you want to deal with anything special after subscribing and such. + function (socket) { + Debug.IbctLogDbg(COMP, 'Sent!'); + }, + function (error) { + Debug.IbctLogErr(COMP, 'Error'); + } + ); + } + // have login + if (message.method === 'submit') { + _this.lastSendPacket = message; + return this.stratumSubmit(message.id, message.params.params.submit); + } + } + isWritable() { + return true; + } +}; + +module.exports = function GetIntStratum(options = {}) { + return new intStratum(options); +}; \ No newline at end of file diff --git a/src/protocol/LbcStratum.js b/src/protocol/LbcStratum.js new file mode 100644 index 0000000..866c0eb --- /dev/null +++ b/src/protocol/LbcStratum.js @@ -0,0 +1,283 @@ +const EventEmitter = require('events'); +const stratum = require('./stratum/lib'); +const Debug = require('../log')(); +const uuid = require("uuid"); +const COMP = '[LbcStratum]'; + +class intStratum extends EventEmitter { + constructor({}) { + super(); + var _this = this; + + _this.client = null; + _this.online = false; + _this.auth = false; + _this.subinfo = null; + _this.subinfoturn = false; + _this.diffinfo = null; + _this.socket = null; + _this.sendQueue = []; + _this.user = null; + _this.pass = null; + _this.lastRecvPacket = null; + _this.lastSendPacket = null; + _this.id = uuid.v4(); + + return _this; + } + + stratumAuthorize(id, user, pass) { + return this.socket.stratumSend({ + 'method': 'mining.authorize', + 'id': id, + 'params': [user, pass] + }, true); + } + + stratumSubmit(id, submitData) { + this.socket.setLastActivity(); + return this.socket.stratumSend({ + 'method': 'mining.submit', + 'id': id, + 'params': submitData + }); + } + + setupLoginPacket(data, job, diff, info) { + // for setup for connect.js + var packet = { + id: data.id, + error: null, + result: { + id: this.id, + job: job, + diff: diff, + subscribe: info, + } + }; + return packet; + } + + getPacketByID(id) { + var packet = null; + this.sendQueue = this.sendQueue.filter(function (pkt) { + if (pkt.id === id && !packet) { + packet = pkt; + return false; + } else { + return true; + } + }); + + return packet; + } + + getPacketByMethod(method) { + var packet = null; + this.sendQueue = this.sendQueue.filter(function (pkt) { + if (pkt.method === method && !packet) { + packet = pkt; + return false; + } else { + return true; + } + }); + + return packet; + } + + init(ssl, host, port) { + var _this = this; + + if (_this.online) { + _this.kill(); + return; + } else if (!_this.client) { + _this.client = stratum.Client.$create(); + if (!_this.client) { + _this.emit('error', __('创建Stratum客户端失败')); + } + } + + _this.client.on('error', function (socket, e) { + // xmr.viabtc.com donation pool + if (e.host !== 'xmr.viabtc.com') { + _this.emit("error", __('网络连接中断')); + } + }); + + _this.client.on('timeout', function (socket, e) { + // xmr.viabtc.com donation pool + if (socket.socket._host !== 'xmr.viabtc.com') { + _this.emit("error", __('Socket请求超时')); + } + }); + + _this.client.on('mining.error', function (msg, socket) { + Debug.IbctLogErr(COMP, msg); + _this.emit("error", __('Stratum连接中断')); + }); + + _this.client.connect({ + host: host, + port: port + }).then(function (socket) { + Debug.IbctLogDbg(COMP, 'Connected! lets ask for subscribe'); + if (!_this.online) { + _this.online = true; + } + _this.socket = socket; + _this.emit('connect'); + }) + } + + kill() { + if (this.client) { + this.client.removeAllListeners(); + if (this.client.destroy) { + this.client.destroy(); + } + this.client = null; + } + if (this.sendQueue.length) + this.sendQueue.splice(0, this.sendQueue.length); + + this.lastRecvPacket = null; + this.lastSendPacket = null; + if (this.online) { + this.online = false; + this.auth = false; + this.removeAllListeners(); + } + } + + getPacket() { + var _this = this; + var packet = null; + // TODO: have done in init + // the client is a one-way communication, it receives data from the server after issuing commands + _this.client.on('mining', function (data, socket, type) { + // type will be either 'broadcast' or 'result' + Debug.IbctLogInfo(COMP, '<===' + type + ' = ', JSON.stringify(data)); + // you can issue more commands to the socket, it's the exact same socket as "client" variable + // in this example + // the socket (client) got some fields like: + // client.name = name of the worker + // client.authorized = if the current connection is authorized or not + // client.id = an UUID ([U]niversal [U]nique [ID]entifier) that you can safely rely on it's uniqueness + // client.subscription = the subscription data from the server + switch (data.method) { + case 'set_difficulty': + case 'mining.set_difficulty': + // server sent the new difficulty + Debug.IbctLogInfo(COMP, 'mining.set_difficulty'); + if (!socket.authorized) { + _this.diffinfo = data; + } else { + _this.emit('data', _this.setupLoginPacket(data, null,data.params,null)); + } + break; + case 'notify': + case 'mining.notify': + // server sent a new block + // Debug.IbctLogInfo(COMP, 'mining.notify'); + if (!_this.auth) { + _this.lastRecvPacket = data.params; + } else { + _this.emit('data', _this.setupLoginPacket(data, data.params,null,null)); + } + break; + default: + if (!socket.authorized) { + _this.subinfo = data; + Debug.IbctLogDbg(COMP, 'Asking for authorization'); + if (_this.lastSendPacket && _this.lastSendPacket.method === 'login' && !_this.auth) { + Debug.IbctLogErr(COMP, 'Login failed: ', _this.user); + if (data.error && data.error.message) + _this.emit('error', __('矿池登陆失败') + ': ' + data.error.message); + else if (data.error && (data.error instanceof Array) && data.error.length > 2) { + _this.emit('error', __('矿池登陆失败') + ': ' + data.error[1]) + } else + _this.emit('error', __('矿池登陆失败')); + return + } + packet = _this.getPacketByMethod('login'); + if (packet) { + _this.lastSendPacket = packet; + Debug.IbctLogInfo(COMP, 'Login User: ', _this.user); + _this.stratumAuthorize(packet.id, packet.params.login, packet.params.pass); + } + } else { + if (!_this.lastSendPacket) { + return + } + /*add this step for 6 block */ + if(_this.subinfoturn === true) { + _this.subinfo = data; + _this.subinfoturn = false; + } + if (_this.lastSendPacket.method === 'login' && !_this.auth) { + Debug.IbctLogDbg(COMP, 'We are authorized', _this.lastRecvPacket); + _this.emit('data', _this.setupLoginPacket(data, _this.lastRecvPacket, null, null)); + _this.auth = true; + if(_this.subinfo.result === null){ + _this.subinfoturn = true; + _this.socket.stratumSubscribe('icbtminer'); + } + + } + + if(typeof _this.subinfo.result === 'object' && _this.subinfo.result !== null) { + _this.emit('data', _this.setupLoginPacket(_this.subinfo, null, null, _this.subinfo.result)); + _this.subinfo.result = null; + } + + if( _this.diffinfo !== null) { + _this.emit('data', _this.setupLoginPacket(_this.diffinfo, null, _this.diffinfo.params, null)); + _this.diffinfo = null; + } + if (_this.lastSendPacket.method === 'submit') { + _this.emit('data', data); + } + } + } + }); + } + + sendPacket(message) { + var _this = this; + //Debug.IbctLogInfo(COMP, 'sendPacket: ', JSON.stringify(message)); + this.sendQueue.push(message); + if (message.method === 'login') { + this.user = message.params.login; + this.pass = message.params.pass; + // After the first stratumSubscribe, the data will be handled internally + // and returned deferreds to be resolved / rejected through the event 'mining' + // above + this.socket.stratumSubscribe('icbtminer').then( + // This isn't needed, it's handled automatically inside the Client class + // but if you want to deal with anything special after subscribing and such. + function (socket) { + Debug.IbctLogDbg(COMP, 'Sent!'); + }, + function (error) { + Debug.IbctLogErr(COMP, 'Error'); + } + ); + } + // have login + if (message.method === 'submit') { + _this.lastSendPacket = message; + //console.log(COMP, 'sendPacket------------->: ', JSON.stringify(message)); + //console.log(COMP, 'sendPacket------------->: ', message); + return this.stratumSubmit(message.id, message.params); + } + } + isWritable() { + return true; + } +}; + +module.exports = function GetIntStratum(options = {}) { + return new intStratum(options); +}; diff --git a/src/protocol/jsonrpc.js b/src/protocol/jsonrpc.js new file mode 100644 index 0000000..c538251 --- /dev/null +++ b/src/protocol/jsonrpc.js @@ -0,0 +1,72 @@ +const EventEmitter = require('events'); +const Debug = require('../log')(); +const net = require("net"); +const tls = require("tls"); +const COMP = '[jsonrpc]'; + +class jsonrpc extends EventEmitter { + constructor({ }) { + super(); + var _this = this; + _this.socket = null; + _this.buffer = ""; + + return _this; + } + + init(ssl, host, port) { + var _this = this; + if (ssl) { + this.socket = tls.connect(+port, host, { rejectUnauthorized: false }); + } else { + this.socket = net.connect(+port, host); + } + this.socket.on("connect", function () { + _this.emit("connect"); + }); + this.socket.on("error", function (error) { + _this.emit("error", error); + }); + this.socket.on("close", function () { + _this.emit("close"); + }); + this.socket.setKeepAlive(true); + this.socket.setEncoding("utf8"); + } + kill() { + if (this.socket != null) { + try { + this.socket.end(); + this.socket.destroy(); + } + catch (e) { + Debug.IbctLogErr(COMP, "something went wrong while destroying socket (" + this.host + ":" + this.port + "):", e.message); + } + } + } + getPacket() { + var _this = this; + this.socket.on("data", function (chunk) { + _this.buffer += chunk; + while (_this.buffer.includes("\n")) { + var newLineIndex = _this.buffer.indexOf("\n"); + var stratumMessage = _this.buffer.slice(0, newLineIndex); + _this.buffer = _this.buffer.slice(newLineIndex + 1); + _this.emit("data", stratumMessage); + } + }); + } + + sendPacket(message) { + Debug.IbctLogInfo(COMP, JSON.stringify(message)); + return this.socket.write(JSON.stringify(message) + "\n"); + } + + isWritable() { + return this.socket.writable; + } +} + +module.exports = function Getjsonrpc(options = {}) { + return new jsonrpc(options); +}; \ No newline at end of file diff --git a/src/protocol/protocol.js b/src/protocol/protocol.js new file mode 100644 index 0000000..2a4fe85 --- /dev/null +++ b/src/protocol/protocol.js @@ -0,0 +1,83 @@ +const EventEmitter = require('events'); +const Debug = require('../log')(); +const jsonrpc = require('./jsonrpc'); +const HnsStratum = require('./HnsStratum'); +const LbcStratum = require('./LbcStratum'); +const COMP = '[protocol]'; + +var protocols = [ + { + name: 'stratum', + attr: 'hns', + api: HnsStratum + }, + { + name: 'stratum', + attr: 'lbc', + api: LbcStratum + } +]; + +class protocol extends EventEmitter { + constructor({ + name, + cryptoname + }) { + super(); + var _this = this; + Debug.IbctLogDbg(COMP, name); + _this.name = name; + _this.cryptoname = cryptoname; + _this.protocolApi = _this.GetProtocolApi(_this.name, _this.cryptoname); + if (!_this.protocolApi) { + _this.emit('error', __('不支持此种连接模式:'), _this.name); + } + + _this.protocolApi.on('error', function (error) { + _this.emit('error', error); + }); + + _this.protocolApi.on("connect", function () { + _this.emit("connect"); + }); + _this.protocolApi.on("close", function () { + _this.emit("close"); + }); + + this.protocolApi.on("data", function (message) { + _this.emit("data", message); + }); + + return _this; + } + + GetProtocolApi(name, cryptoname) { + var dev = null; + protocols.forEach(function (protocol, index) { + if (protocol.name === name && protocol.attr === cryptoname) { + dev = protocol; + } + }) + return dev ? dev.api() : null; + } + + init(ssl, host, port) { + return this.protocolApi ? this.protocolApi.init(ssl, host, port) : null; + } + kill() { + return this.protocolApi ? this.protocolApi.kill() : null; + } + getPacket() { + return this.protocolApi ? this.protocolApi.getPacket() : null; + } + sendPacket(message) { + return this.protocolApi ? this.protocolApi.sendPacket(message) : null; + } + isWritable() { + return this.protocolApi ? this.protocolApi.isWritable() : null; + } +} + +module.exports = function GetProtocol(options = {}) { + return new protocol(options); +}; \ No newline at end of file diff --git a/src/protocol/stratum/LICENSE b/src/protocol/stratum/LICENSE new file mode 100644 index 0000000..14b17e4 --- /dev/null +++ b/src/protocol/stratum/LICENSE @@ -0,0 +1,339 @@ +GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + Stratum protocol server using Express + Copyright (C) 2013 Paulo Cesar + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + {signature of Ty Coon}, 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/src/protocol/stratum/examples/client.js b/src/protocol/stratum/examples/client.js new file mode 100644 index 0000000..5f7f1c4 --- /dev/null +++ b/src/protocol/stratum/examples/client.js @@ -0,0 +1,82 @@ +var stratum = require('../lib'); + +var client = stratum.Client.$create(); + + //must be specified per EventEmitter requirements +client.on('error', function(socket){ + socket.destroy(); + console.log('Connection closed'); + process.exit(1); +}); + +// this usually happens when we are not authorized to send commands (the server didn't allow us) +// or share was rejected +// Stratum errors are usually an array with 3 items [int, string, null] +client.on('mining.error', function(msg, socket){ + console.log(msg); +}); + +var submitted = false; + +// the client is a one-way communication, it receives data from the server after issuing commands +client.on('mining', function(data, socket, type){ + // type will be either 'broadcast' or 'result' + console.log('Mining data: ' + type + ' = ', data); + // you can issue more commands to the socket, it's the exact same socket as "client" variable + // in this example + + // the socket (client) got some fields like: + // client.name = name of the worker + // client.authorized = if the current connection is authorized or not + // client.id = an UUID ([U]niversal [U]nique [ID]entifier) that you can safely rely on it's uniqueness + // client.subscription = the subscription data from the server + switch (data.method) { + case 'set_difficulty': + // server sent the new difficulty + break; + case 'notify': + // server sent a new block + break; + default: + if (!socket.authorized){ + console.log('Asking for authorization'); + socket.stratumAuthorize('user','pass'); + } else { + console.log('We are authorized'); + if (!submitted) { + console.log('Lets submit fake data once for test purposes, then close'); + socket.stratumSubmit('', '' ,'' ,' ', '').done(function(){ + client.destroy(); // after call to destroy, "client" doesnt exist anymore + }); + submitted = true; + } + } + } +}); + +client.connect({ + host: 'localhost', + port: 3333 +}).then(function (socket){ + // defered, this can be chained if needed, no callback hell + // "socket" refers to the current client, in this case, the 'client' + // variable + console.log('Connected! lets ask for subscribe'); + + // After the first stratumSubscribe, the data will be handled internally + // and returned deferreds to be resolved / rejected through the event 'mining' + // above + socket.stratumSubscribe('Node.js Stratum').then( + // This isn't needed, it's handled automatically inside the Client class + // but if you want to deal with anything special after subscribing and such. + function(socket){ + console.log('Sent!'); + }, + function(error){ + console.log('Error'); + } + ); + }) + // this can be chained and is the last callback to execute + //.done(function(){ }); +; \ No newline at end of file diff --git a/src/protocol/stratum/lib/base.js b/src/protocol/stratum/lib/base.js new file mode 100644 index 0000000..84b43af --- /dev/null +++ b/src/protocol/stratum/lib/base.js @@ -0,0 +1,11 @@ +'use strict'; + +var + debug = require('debug')('stratum'); + +module.exports = require('es5class').$define('Base', {}, { + debug: function(msg){ + debug(this.$className + ': ', msg); + return msg; + } +}).$implement(require('eventemitter3'), true); diff --git a/src/protocol/stratum/lib/client.js b/src/protocol/stratum/lib/client.js new file mode 100644 index 0000000..4a65a5b --- /dev/null +++ b/src/protocol/stratum/lib/client.js @@ -0,0 +1,335 @@ +module.exports = function (classes) { + 'use strict'; + + var + q = classes.q, + _ = classes.lodash, + uuid = classes.uuid; + var Debug = classes.debug; + + /** + * @param {Socket} socket + * @param {Boolean} isServer + * @constructor + */ + var Client = classes.Base.$define('Client', { + construct: function (socket, isServer) { + var self = this; + + self.$super(); + + // although BFGMiner starts at id 0, we start at 1, because it makes sense + self.currentId = 0; + // our TCP_NODELAY and KeepAlive'd socket + self.socket = self.$class.createSocket(socket); + self.authorized = false; + self.byServer = isServer === undefined ? false : isServer; + self.subscription = ''; + self.name = ''; + self.pending = {}; + // Uniquely identify this socket, regardless of clusters or inter-processes, + // it's unique. + self.id = uuid.v4(); + + // Last activity is now! + self.setLastActivity(); + + self.socket.on('end', function clientSocketEnd() { + if (self.emit) { + self.emit('end', self); + } + }); + + self.socket.on('error', function clientSocketError(e) { + if (self.emit) { + self.emit('error', self, e); + } + }); + // set 60s timeout + self.socket.setTimeout(120000, function() { + if (self.emit) { + self.emit('timeout', self, 'Socket请求超时'); + } + }); + + self.socket.on('drain', function clientSocketDrain() { + if (self.emit) { + self.emit('drain', self); + } + }); + + if (isServer !== true) { + self.socket.on('data', function (data) { + self.handleData(self.socket, data); + // classes.curry.predefine(self.handleData, [self], self) + }); + } + }, + /** + * Keep track of idle sockets, update the last activity + * + * @param {Number} [time] Unix Timestamp + * + * @return {this} + */ + setLastActivity: function (time) { + this.lastActivity = _.isNumber(time) ? time : Date.now(); + + return this; + }, + /** + * Either emit an event, or fulfill a pending request by id + */ + fullfill: function (command) { + var self = this, method; + //Debug.IbctLogDbg('command', JSON.stringify(command)); + //Debug.IbctLogDbg(JSON.stringify(self.pending)); + if (_.has(command, 'method') && command.method === 'mining.notify') { + // set command id = 0 + command['id'] = 0; + self.emit('mining', command, self, 'notify'); + } else if (_.has(command, 'id') && (_.isNull(command['id']))) { + // null id, it's a broadcast most likely, we need to check the last command + if (_.has(command, 'method')) { + method = command.method.split('mining.'); + + if (classes.Server.commands[method[1]].broadcast === true || method[1] === 'error') { + command['method'] = method[1]; + + self.emit('mining', command, self, 'broadcast'); + } else { + self.emit('mining', command, self, 'broadcast'); + //throw new Error('Server sent unknown command: ' + JSON.stringify(command)); + } + } else { + throw new Error('Broadcast without a method: ' + JSON.stringify(command)); + } + } else if (_.has(command, 'id') && _.has(self.pending, command['id'])) { + // need to resolve pending requests by id + self.$class.debug('Received pending request response: ' + command + ' ' + self.pending); + var type = self.pending[command['id']]; + switch (type) { + case 'mining.subscribe': + self.subscription = command['result']; + break; + case 'mining.authorize': + self.authorized = !!command['result'] || (command['error'] === null || command['error'] === undefined); + break; + } + + self.emit('mining', command, self, 'result', self.pending[command['id']]); + if (type === self.pending[command['id']]) { + delete self.pending[command['id']]; + } + } else if (_.has(command, 'id') && _.has(command, 'result')) { + // regular result that wasnt issued by this socket + self.emit('mining', command, self, 'result'); + } else if (_.has(command, 'error') && (!_.isNull(command['error']) && !_.isEmpty(command['error']))) { + // we have an error, we need to act on that, regardless of other members in the command received + throw new Error(command); + } else if (_.has(command, 'method') && command.method === 'mining.set_difficulty') { + self.emit('mining', command, self, 'broadcast'); + } else { + throw new Error('No suitable command was issued from the server'); + } + }, + /** + * Get the current socket IP address + * + * @returns {{port: Number, address: String, family: String}} + */ + address: function () { + return this.socket.address(); + }, + /** + * This method is exposed just for testing purposes + * + * @param {Socket} socket + * @param {Buffer} buffer + * @private + */ + handleData: function (socket, buffer) { + var + c = classes.Server.getStratumCommands(buffer), + cmds = c.cmds; + classes.Server.processCommands.call(this, this, cmds); + }, + /** + * Destroy the socket and unattach any listeners + */ + destroy: function () { + this.removeAllListeners(); + + this.socket.destroy(); + + this.$destroy(); + }, + /** + * Connect to somewhere + * + * @param {Object} opts Where to connect + * @returns {Q.promise} + */ + connect: function (opts) { + var d = q.defer(), self = this; + + this.socket.connect(opts, function clientSocketConnect() { + d.resolve(self); + }); + + return d.promise; + }, + /** + * Don't use this functions directly, they are called from the server side, + * it's not a client side command, but an answer + * + * @return {Q.promise} + * @private + */ + set_difficulty: function (args) { + return classes.Server.commands.set_difficulty.apply(this, [null].concat(args)); + }, + /** + * Don't use this functions directly, they are called from the server side + * it's not a client side command, but an answer + * + * @return {Q.promise} + * @private + */ + notify: function (args) { + return classes.Server.commands.notify.apply(this, [null].concat(args)); + }, + /** + * Send HTTP header + * + * @param {String} hostname + * @param {Number} port + * + * @return {Q.promise} + */ + stratumHttpHeader: function (hostname, port) { + var + result = '{"error": null, "result": false, "id": 0}', + d = q.defer(), + header = [ + 'HTTP/1.1 200 OK', + 'X-Stratum: stratum+tcp://' + hostname + ':' + port, + 'Connection: Close', + 'Content-Length: ' + (result.length + 1), + '', + '', + result + ]; + + this.$class.debug('Sending Stratum HTTP header'); + + this.socket.write(header.join('\n'), classes.curry.wrap(d.resolve, d)); + + return d.promise; + }, + /** + * Subscribe to the pool + * + * @param {String} [UA] Send the User-Agent + * @returns {Q.promise} + */ + stratumSubscribe: function (UA) { + this.name = UA; + + return this.stratumSend({ + 'method': 'mining.subscribe', + 'id': this.currentId++, + 'params': typeof UA !== 'undefined' ? [UA] : [] + }, true); + }, + /** + * Asks for authorization + * + * @param {String} user + * @param {String} pass + * @returns {Q.promise} + */ + stratumAuthorize: function (user, pass) { + return this.stratumSend({ + 'method': 'mining.authorize', + 'id': this.currentId++, + 'params': [user, pass] + }, true); + }, + /** + * Sends a share + * + * @param {String} worker + * @param {String} job_id + * @param {String} extranonce2 + * @param {String} ntime + * @param {String} nonce + * @returns {Q.promise} + */ + stratumSubmit: function (worker, job_id, extranonce2, ntime, nonce) { + this.setLastActivity(); + + return this.stratumSend({ + 'method': 'mining.submit', + 'id': this.currentId++, + 'params': [worker, job_id, extranonce2, ntime, nonce] + }); + }, + /** + * Send Stratum command + * + * @param {Object} data + * @param {Boolean} bypass Bypass unauthorized + * @param {String} name Call from the server + * + * @returns {Q.promise} + */ + stratumSend: function (data, bypass, name) { + if (this.authorized === true || bypass === true) { + this.pending[data.id || this.currentId++] = name || data.method; + return this.send(JSON.stringify(data) + '\n'); + } else { + var error = this.$class.debug(classes.Server.errors.UNAUTHORIZED_WORKER); + + this.emit('mining.error', error); + + return classes.Server.rejected(error); + } + }, + /** + * Send raw data to the server + * + * @param {*} data + * @returns {Q.promise} + */ + send: function (data) { + this.$class.debug('(' + this.id + ') Sent command ' + data); + //Debug.IbctLogDbg('(' + this.id + ') Sent command ' + data); + var d = q.defer(), self = this; + try { + self.socket.write(data, function clientSocketWrite(err) { + if (err) { + d.reject(err); + } else { + d.resolve(self); + } + }); + } catch (err) { + d.reject(err); + } + + return d.promise; + } + }, { + createSocket: function (socket) { + if (!socket) { + socket = new classes.net.Socket(); + } + socket.setNoDelay(true); + socket.setKeepAlive(true, 120); + return socket; + } + }); + + return Client; +}; \ No newline at end of file diff --git a/src/protocol/stratum/lib/index.js b/src/protocol/stratum/lib/index.js new file mode 100644 index 0000000..a652b94 --- /dev/null +++ b/src/protocol/stratum/lib/index.js @@ -0,0 +1,24 @@ +'use strict'; + +var + classes = { + path : require('path'), + dir : function (path){ + return this.path.join(__dirname, path); + }, + fs : require('fs'), + lodash : require('lodash'), + net : require('net'), + uuid : require('uuid'), + q : require('bluebird'), + rpc : require('json-rpc2'), + curry : require('better-curry'), + debug : require('../../../log')() + }; + +classes.Base = require('./base'); +classes.RPCServer = require('./rpc')(classes); +classes.Client = require('./client')(classes); +classes.Server = require('./server')(classes); + +module.exports = classes; \ No newline at end of file diff --git a/src/protocol/stratum/lib/rpc.js b/src/protocol/stratum/lib/rpc.js new file mode 100644 index 0000000..6fb1064 --- /dev/null +++ b/src/protocol/stratum/lib/rpc.js @@ -0,0 +1,175 @@ +module.exports = function(classes){ + 'use strict'; + + var + _ = classes.lodash, + rpc = require('json-rpc2'), + crypto = require('crypto'); + + var RPC = classes.Base.$define('RPC', { + construct: function(opts){ + this.$super(); + + opts = opts || {}; + opts = _.defaults(opts, { + 'mode': 'tcp', + 'port': false, + 'host': 'localhost', + 'password': false + }); + + if (!opts.port) { + throw new Error('Port must be set'); + } + + if (!opts.password) { + throw new Error('Password must be set'); + } + + this.opts = opts; + + this.server = new rpc.Server({type: this.opts.mode}); + this._server = null; + }, + /** + * Generate a SHA 256 hex from a base64 string + * + * @param {String} base64 The base64 string of the password + * @returns {Boolean|String} False if invalid Base64, the SHA256 hex otherwise + * @private + */ + _password: function(base64){ + var + ascii = new Buffer(base64, 'base64').toString('ascii'), + check = new Buffer(ascii).toString('base64'); + + if (base64 === check) { + return crypto + .createHash('sha256') + .update( + ascii, + 'ascii' + ).digest('hex'); + } + + return false; + }, + /** + * Returns a bound function to the current exposed name, functions and context + * + * @param {String} name + * @param {Function} func + * @param {Object} [context] + * + * @returns {Function} + * @private + */ + _authenticate: function(name, func, context){ + var self = this; + + return function rpcAuthenticate(args, connection, callback){ + var password = false; + + self.$class.debug('Received request for "' + name + '" with args "' + args + '"'); + + if (typeof args[0] === 'string') { + password = self._password(args[0]); + } else { + callback(self.$class.debug('No password provided')); + } + + if (password && password === self.opts.password) { + func.apply(context || self, [args.slice(1), connection, callback]); + } else { + callback(self.$class.debug('Unauthorized access')); + } + }; + }, + /** + * Expose a function, but first check if the password + * is valid (as the first parameter from the RPC call). + * + * This isn't standard, but forces each call to be signed, + * so there's no need to keep track of sessions, connections, + * etc. + * + * So make sure that each request it's made, the first param + * is the base64 password + * + * { + * "method":"any_method", + * "params":["base64ed_password", "param1", "param2"], + * "id": 1 + * } + * + * @param {String} method Name of the RPC method + * @param {Function} func Function that will be called with the args, connection and the callback. + * The callback must be called, so that the remote RPC endpoint receives an answer + * + * RPC.expose('name', function(args, connection, callback){ + * if (args.length === 0){ + * callback(error); + * } else { + * callback(null, result); + * } + * }); + * + * @param {Object} [context] The "this" of the previous parameter, func + * @returns {RPC} + */ + expose: function(method, func, context){ + var self = this; + + self.$class.debug('Exposing "' + method + '"'); + + self.server.expose(method, self._authenticate(method, func, context)); + + return self; + }, + /** + * Listen the RPC on the defined port + * + * @returns {RPC} + */ + listen: function(){ + if (_.isNull(this._server)) { + this.$class.debug('Listening on port ' + this.opts.port); + + var func; + + switch (this.opts.mode) { + case 'both': + func = this.server.listenHybrid; + break; + case 'http': + func = this.server.listen; + break; + case 'tcp': + func = this.server.listenRaw; + break; + } + + this._server = func.call(this.server, this.opts.port, this.opts.host); + } else { + throw new Error(this.$class.debug('Server already listening on port ' + this.opts.port)); + } + + return this; + }, + /** + * Close the RPC server + * + * @returns {RPC} + */ + close: function(){ + if (!_.isNull(this._server) && this._server.close) { + this._server.close(); + this._server = null; + } + + return this; + } + }); + + return RPC; +}; \ No newline at end of file diff --git a/src/protocol/stratum/lib/server.js b/src/protocol/stratum/lib/server.js new file mode 100644 index 0000000..eafe78e --- /dev/null +++ b/src/protocol/stratum/lib/server.js @@ -0,0 +1,630 @@ +module.exports = function (classes){ + 'use strict'; + + var + _ = classes.lodash, + q = classes.q; + + var Server = classes.Base.$define('Server', { + construct : function (opts){ + var self = this; + + this.$super(); + + self.clients = {}; + + opts = opts || {}; + + self.opts = _.defaults(_.clone(opts), Server.defaults); + + if (opts.rpc){ + self.rpc = new classes.RPCServer(self.opts.rpc); + + self.expose('mining.connections'); + self.expose('mining.block'); + self.expose('mining.wallet'); + self.expose('mining.alert'); + } + + // classes.toobusy.maxLag(self.opts.settings.toobusy); + + self.server = classes.net.createServer(); + + Server.debug('Created server'); + + self.server.on('connection', function serverConnection(socket){ + self.newConnection(socket); + }); + }, + expose : function (name){ + if (this.rpc) { + this.rpc.expose(name, Server.expose(this, name), this); + } else { + throw new Error('RPC is not enabled in the server'); + } + }, + /** + * Emits 'close' event when a connection was closed + * + * @param {Client} socket + */ + closeConnection: function (socket){ + var id = socket.id; + socket.destroy(); + + if (typeof this.clients[id] !== 'undefined') { + delete this.clients[id]; + } + + this.emit('close', id); + + Server.debug('(' + id + ') Closed connection ' + _.size(this.clients) + ' connections'); + }, + /** + * Emits 'busy' event when the server is on high load + * Emits 'connection' event when there's a new connection, passes the newly created socket as the first argument + * + * @param {Socket} socket + */ + newConnection : function (socket){ + var + self = this, + closeSocket, + handleData; + + // if (classes.toobusy()) { + if (false) { + socket.destroy(); + + Server.debug('Server is busy, ' + _.size(this.clients) + ' connections'); + + self.emit('busy'); + } else { + socket = new classes.Client(socket, true); + + closeSocket = (function (socket){ + return function closeSocket(){ + self.closeConnection(socket); + }; + })(socket); + + handleData = (function (socket){ + return function handleData(buffer){ + self.handleData(socket, buffer); + }; + })(socket); + + classes.Server.debug('(' + socket.id + ') New connection'); + + this.clients[socket.id] = socket; + + socket.on('end', closeSocket); + socket.on('error', closeSocket); + + socket.socket.on('data', handleData); + + this.emit('connection', socket); + } + }, + /** + * + * @param {Client} socket + * @param {Buffer} buffer + */ + handleData : function (socket, buffer){ + var + self = this, + c = Server.getStratumCommands(buffer), + string = c.string, + cmds = c.cmds; + + if (/ HTTP\/1\.1\n/i.test(string)) { + // getwork trying to access the stratum server, send HTTP header + socket.stratumHttpHeader( + this.opts.settings.hostname, + this.opts.settings.port + ).done(function handleDataHttp(){ + self.closeConnection(socket); + }); + } else if (cmds.length) { + // We got data + self.$class.processCommands.call(self, socket, cmds); + } + }, + /** + * Start the Stratum server, the RPC and any coind that are enabled + * + * @return {Q.promise} + */ + listen : function (){ + var self = this, d = q.defer(); + + this.server.listen(this.opts.settings.port, this.opts.settings.host, function serverListen(){ + d.resolve(Server.debug('Listening on port ' + self.opts.settings.port)); + }); + + /*istanbul ignore else */ + if (this.rpc) { + this.rpc.listen(); + } + + return d.promise; + }, + close : function (){ + Server.debug('Shutting down servers...'); + + this.server.close(); + + /*istanbul ignore else */ + if (this.rpc) { + this.rpc.close(); + } + }, + /** + * Sends a Stratum result command directly to one socket + * + * @param {String} id UUID of the socket + * @param {String} type The type of command, as defined in server.commands + * @param {Array} array Parameters to send + * + * @return {Q.promise} + */ + sendToId : function (id, type, array){ + var d = q.defer(); + + + if (type && _.isFunction(this.$class.commands[type])) { + if (id && _.has(this.clients, id)) { + + this.$class.commands[type].apply(this.clients[id], [id].concat(array)).done( + classes.curry.wrap(d.resolve, d), + classes.curry.wrap(d.reject, d) + ); + } else { + d.reject(this.$class.debug('sendToId socket id not found "' + id + '"')); + } + } else { + d.reject(this.$class.debug('sendToId command doesnt exist "' + type + '"')); + } + + return d.promise; + }, + /** + * Send a mining method or result to all connected + * sockets + * + * Returns a promise, so when it's done sending, you can + * do: + * + * server.broadcast('notify', [...params]).then(function(){ + * console.log('done'); + * }); + * + * @param {String} type + * @param {Array} data + * @returns {Q.promise} + */ + broadcast : function (type, data){ + var self = this, d = q.defer(), total = 0; + + if (typeof type === 'string' && _.isArray(data)) { + if (typeof self.$class.commands[type] === 'function' && self.$class.commands[type].broadcast === true) { + if (_.size(self.clients)) { + self.$class.debug('Brodcasting ' + type + ' with ', data); + + q.try(function serverBroadcast(){ + + _.forEach(self.clients, function serverBroadcastEach(socket){ + self.$class.commands[type].apply(socket, [null].concat(data)); + total++; + }); + + return total; + }).done( + classes.curry.wrap(d.resolve, d), + classes.curry.wrap(d.reject, d) + ); + + } else { + d.reject(self.$class.debug('No clients connected')); + } + } else { + d.reject(self.$class.debug('Invalid broadcast type "' + type + '"')); + } + } else { + d.reject(self.$class.debug('Missing type and data array parameters')); + } + + return d.promise; + } + }, { + /** + * Parse the incoming data for commands + * + * @param {Buffer} buffer + * @returns {{string: string, cmds: Array}} + */ + getStratumCommands: function (buffer){ + var + string, + cmds = []; + + if (Buffer.isBuffer(buffer) && buffer.length) { + string = buffer.toString().replace(/[\r\x00]/g, ''); + cmds = _.filter(string.split('\n'), function serverCommandsFilter(item){ return !_.isEmpty(item) && !_.isNull(item); }); + } + + // Separate cleaned up raw string and commands array + return {string: string, cmds: cmds}; + }, + /** + * Process the Stratum commands and act on them + * Emits 'mining' event + * + * @param {Client} socket + * @param {Array} cmds + */ + processCommands : function (socket, cmds){ + var + command, + method, + self = this, + onClient = self.$instanceOf(classes.Client), + onServer = !onClient && self.$instanceOf(classes.Server); + + self.$class.debug('(' + socket.id + ') Received command ' + cmds); + + _.forEach(cmds, function serverForEachCommand(cmd){ + try { + command = JSON.parse(cmd); + + // Is it a method Stratum call? + if ( + // Deal with method calls only when on Server + onServer && + typeof command['method'] !== 'undefined' && + command.method.indexOf('mining.') !== -1 + ) { + method = command.method.split('mining.'); + command['method'] = method[1]; + + + if (method.length === 2 && typeof self.$class.commands[method[1]] === 'function') { + // We don't want client sockets messing around with broadcast functions! + if (self.$class.commands[method[1]].broadcast !== true && method[1] !== 'error') { + // only set lastActivity for real mining activity + socket.setLastActivity(); + + var + d = q.defer(), + accept, reject; + + // Resolved, call the method and send data to socket + accept = self.$class.bindCommand(socket, method[1], command.id); + // Rejected, send error to socket + reject = self.$class.bindCommand(socket, 'error', command.id); + + d.promise.spread(accept, reject); + + self.emit('mining', command, d, socket); + } else { + throw new Error('(' + socket.id + ') Client trying to reach a broadcast function "' + method[1] + '"'); + } + } else { + self.$class.commands.error.call(socket, command.id, self.$class.errors.METHOD_NOT_FOUND); + + throw new Error('Method not found "' + command.method + '"'); + } + + } else if ((onClient || socket.byServer === true) && (_.has(command, 'result') || _.has(command, 'method'))) { + // Result commands ONLY when 'self' is an instance of Client + + // Since (unfortunately) stratum expects every command to be given in order + + // we need to keep track on what we asked to the server, so we can + // act accordingly. This call is either a result from a previous command or a method call (broadcast) + + socket.fullfill(command); + } else { + throw new Error('Stratum request without method or result field'); + } + } catch (e) { + if (self.emit) + self.emit('mining.error', self.$class.debug(e), socket); + } + }); + + }, + /** + * Wraps the callback and predefine the ID of the current stratum call + * + * @param {Client} socket + * @param {String} type + * @param {String} id + * + * @returns {Function} curryed function + */ + bindCommand : function (socket, type, id){ + return classes.curry.predefine(this.commands[type], [id], socket); + }, + rejected : function (msg){ + return q.reject(this.$class.debug(msg)); + }, + expose : function (base, name){ + return function serverExposedFunction(args, connection, callback){ + var d = q.defer(); + + classes.RPCServer.debug('Method "' + name + '": ' + args); + + d.promise.then(function serverExposedResolve(res){ + res = [].concat(res); + + classes.RPCServer.debug('Resolve "' + name + '": ' + res); + + callback.call(base, null, [res[0]]); + }, function serverExposedReject(err){ + classes.RPCServer.debug('Reject "' + name + '": ' + err); + + callback.call(base, ([].concat(err))[0]); + }); + + base.emit('rpc', name, args, connection, d); + }; + }, + invalidArgs: function(id, name, expected, args) { + var count = _.filter(args, function(i){ return typeof i !== 'undefined'; }).length - 1; + + if ((id === null || id === undefined) || count !== expected) { + return classes.Server.rejected( + (id === null || id === undefined) ? + 'No ID provided' : + 'Wrong number of arguments in "' + name + '", expected ' + expected + ' but got ' + count + ); + } + + return true; + }, + commands : { + /** + * Return subscription parameters to the new client + * + * @param id + * @param {String} difficulty + * @param {String} subscription + * @param {String} extranonce1 + * @param {Number} extranonce2_size + * + * @returns {Q.promise} + */ + subscribe : function (id, difficulty, subscription, extranonce1, extranonce2_size){ + var ret; + if ((ret = classes.Server.invalidArgs(id, 'subscribe', 4, arguments)) !== true){ + return ret; + } + + this.subscription = subscription; + + return this.stratumSend({ + id : id, + result: [ + [ + ['mining.set_difficulty', difficulty], + ['mining.notify', subscription] + ], + extranonce1, + extranonce2_size + ], + error : null + }, true, 'subscribe'); + }, + /** + * Send if submitted share is valid + * + * @param {Number} id ID of the call + * @param {Boolean} accepted + * @returns {Q.promise} + */ + submit : function (id, accepted){ + var ret; + if ((ret = classes.Server.invalidArgs(id, 'submit', 1, arguments)) !== true){ + return ret; + } + + return this.stratumSend({ + id : id, + result: !!accepted, + error : null + }, false, 'submit'); + }, + /** + * Send an error + * + * @param {Number} id + * @param {Array|String} error + * @returns {Q.promise} + */ + error : function (id, error){ + var ret; + if ((ret = classes.Server.invalidArgs(id, 'error', 1, arguments)) !== true){ + return ret; + } + + this.$class.debug('Stratum error: ' + error); + + return this.stratumSend({ + id : id, + error : error, + result: null + }, true, 'error'); + }, + /** + * Authorize the client (or not). Must be subscribed + * + * @param {Number} id + * @param {Boolean} authorized + * + * @returns {Q.promise} + */ + authorize : function (id, authorized){ + var ret; + if ((ret = classes.Server.invalidArgs(id, 'authorize', 1, arguments)) !== true){ + return ret; + } + + if (!this.subscription) { + return Server.commands.error.call(this, id, Server.errors.NOT_SUBSCRIBED); + } + + this.authorized = !!authorized; + + return this.stratumSend({ + id : id, + result: this.authorized, + error : null + }, true, 'authorize'); + }, + /** + * Miner is asking for pool transparency + * + * @param {String} id txlist_jobid + * @param {*} merkles + */ + get_transactions: function (id, merkles){ + var ret; + if ((ret = classes.Server.invalidArgs(id, 'get_transactions', 1, arguments)) !== true){ + return ret; + } + + return this.stratumSend({ + id : id, + result: [].concat(merkles), + error : null + }, false, 'get_transactions'); + }, + + /** + * Notify of a new job + * + * @param {Number} id + * @param {*} job_id + * @param {String} previous_hash + * @param {String} coinbase1 + * @param {String} coinbase2 + * @param {Array} branches + * @param {String} block_version + * @param {String} nbit + * @param {String} ntime + * @param {Boolean} clean + * + * @returns {Q.promise} + */ + notify : function (id, job_id, previous_hash, coinbase1, coinbase2, branches, block_version, nbit, ntime, clean){ + var ret; + if ((ret = classes.Server.invalidArgs(false, 'notify', 9, arguments)) !== true){ + return ret; + } + + return this.stratumSend({ + id : null, + method: 'mining.notify', + params: [job_id, previous_hash, coinbase1, coinbase2, branches, block_version, nbit, ntime, clean] + }, true, 'notify'); + }, + /** + * Set the difficulty + * + * @param {Number} id + * @param {Number} value + * @returns {Q.promise} + */ + set_difficulty : function (id, value){ + var ret; + if ((ret = classes.Server.invalidArgs(false, 'set_difficulty', 1, arguments)) !== true){ + return ret; + } + + return this.stratumSend({ + id : null, + method: 'mining.set_difficulty', + params: [value] + }, true, 'set_difficulty'); + } + }, + errors : { + 'FEE_REQUIRED' : [-10, 'Fee required', null], + 'SERVICE_NOT_FOUND' : [-2, 'Service not found', null], + 'METHOD_NOT_FOUND' : [-3, 'Method not found', null], + 'UNKNOWN' : [-20, 'Unknown error', null], + 'STALE_WORK' : [-21, 'Stale work', null], + 'DUPLICATE_SHARE' : [-22, 'Duplicate share', null], + 'HIGH_HASH' : [-23, 'Low difficulty share', null], + 'UNAUTHORIZED_WORKER': [-24, 'Unauthorized worker', null], + 'NOT_SUBSCRIBED' : [-25, 'Not subscribed', null] + }, + + defaults : { + /** + * RPC to listen interface for this server + */ + rpc : { + /** + * Bind to address + * + * @type {String} + */ + host: 'localhost', + /** + * RPC port + * + * @type {Number} + */ + port: 1337, + /** + * RPC password, this needs to be a SHA256 hash, defaults to 'password' + * To create a hash out of your password, launch node.js and write + * + * require('crypto').createHash('sha256').update('password').digest('hex'); + * + * @type {String} + */ + password: '5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8', + /** + * Mode to listen. By default listen only on TCP, but you may use 'http' or 'both' (deal + * with HTTP and TCP at same time) + */ + mode: 'tcp' + }, + /** + * The server settings itself + */ + settings: { + /** + * Address to set the X-Stratum header if someone connects using HTTP + * @type {String} + */ + hostname: 'localhost', + /** + * Max server lag before considering the server "too busy" and drop new connections + * @type {Number} + */ + toobusy : 70, + /** + * Bind to address, use 0.0.0.0 for external access + * @type {string} + */ + host : 'localhost', + /** + * Port for the stratum TCP server to listen on + * @type {Number} + */ + port : 3333 + } + } + }); + + Server.commands.notify.broadcast = true; + Server.commands.set_difficulty.broadcast = true; + Server.commands.error.serverOnly = true; + + return Server; +}; diff --git a/src/protocol/stratum/package.json b/src/protocol/stratum/package.json new file mode 100644 index 0000000..5707dae --- /dev/null +++ b/src/protocol/stratum/package.json @@ -0,0 +1,61 @@ +{ + "bin": { + "stratum-notify": "./bin/stratum-notify" + }, + "dependencies": { + "better-curry": "*", + "bluebird": "2.x", + "chalk": "*", + "commander": "*", + "debug": "*", + "es5class": "1.x.x", + "eventemitter3": "1.x", + "json-rpc2": "1.x", + "lodash": "3.x", + "toobusy-js": "*", + "uuid": "*" + }, + "deprecated": false, + "description": "Stratum protocol server and client for node.js", + "devDependencies": { + "coveralls": "*", + "expect.js": "*", + "istanbul": "*", + "jshint": "*", + "mocha": "*", + "mocha-lcov-reporter": "*", + "sinon": "*" + }, + "directories": { + "lib": "./lib", + "bin": "./bin" + }, + "engines": { + "node": "0.10.x" + }, + "homepage": "https://github.com/pocesar/node-stratum#readme", + "license": "GPLv2", + "main": "./lib", + "name": "stratum", + "repository": { + "type": "git", + "url": "git://github.com/pocesar/node-stratum.git" + }, + "version": "0.2.4", + "warnings": [ + { + "code": "ENOTSUP", + "required": { + "node": "0.10.x" + }, + "pkgid": "stratum@0.2.4" + }, + { + "code": "ENOTSUP", + "required": { + "node": "0.10.x" + }, + "pkgid": "stratum@0.2.4" + } + ] +} \ No newline at end of file diff --git a/src/proxy/app.json b/src/proxy/app.json new file mode 100644 index 0000000..3d8885b --- /dev/null +++ b/src/proxy/app.json @@ -0,0 +1,4 @@ +{ + "name": "CHS", + "repository": "https://github.com/cazala/coin-hive-stratum" +} diff --git a/src/proxy/bin/coin-hive-stratum b/src/proxy/bin/coin-hive-stratum new file mode 100644 index 0000000..55dc3fa --- /dev/null +++ b/src/proxy/bin/coin-hive-stratum @@ -0,0 +1,62 @@ +#!/usr/bin/env node +"use strict"; +const Proxy = require("../build"); +const fs = require("fs"); +const argv = require("minimist")(process.argv.slice(2)); +const defaults = require("../config/defaults"); + +function help() { + const text = fs.createReadStream(`${__dirname}/help`); + text.pipe(process.stderr); + text.on("close", () => process.exit(1)); +} + +if (argv.help || argv.h) { + help(); + return; +} + +const port = argv._[0]; +const key = argv.key; +const cert = argv.cert; +const isHTTPS = !!(key && cert); + +const options = { + host: argv.host || defaults.host, + pass: argv.pass || defaults.pass, + port: argv.port || defaults.port, + ssl: argv.hasOwnProperty("ssl") || defaults.ssl, + address: argv.address || defaults.address, + user: argv.user || defaults.user, + diff: argv.diff || defaults.diff, + dynamicPool: argv.hasOwnProperty("dynamic-pool") || defaults.dynamicPool, + maxMinersPerConnection: argv["max-miners-per-connection"] || defaults.maxMinersPerConnection, + path: argv.path || defaults.path +}; + +if (argv["credentials"]) { + try { + const split = argv["credentials"].split(":"); + options.credentials = { + user: split[0], + pass: split[1] + }; + } catch (e) { + console.warn(`invalid credentials: "${argv["credentials"]}", the should be like "user:pass"`); + } +} + +if (isHTTPS) { + options.key = fs.readFileSync(key); + options.cert = fs.readFileSync(cert); +} + +// proxy +const proxy = new Proxy(options); +proxy.listen(port); + +// handle errors +process.on("unhandledRejection", function(e) { + console.error("An error occured", e.message); + process.exit(1); +}); diff --git a/src/proxy/bin/help b/src/proxy/bin/help new file mode 100644 index 0000000..c62ef01 --- /dev/null +++ b/src/proxy/bin/help @@ -0,0 +1,19 @@ +Usage: 'coin-hive-stratum ' + +: The port where the server will listen to + +Options: + + --host The pool's host. + --port The pool's port. + --pass The pool's password, by default it's "x". + --ssl Use SSL/TLS to connect to the pool. + --address A fixed wallet address for all the miners. + --user A fixed user for all the miners. + --diff A fixed difficulty for all the miner. This is not supported by all the pools. + --dynamic-pool If true, the pool can be set dynamically by sending a ?pool=host:port:pass query param to the websocket endpoint. + --max-miners-per-connection Set the max amount of miners per TCP connection. When this number is exceded, a new socket is created. By default it's 100. + --path Accept connections on a specific path. + --key Path to private key file. Used for HTTPS/WSS. + --cert Path to certificate file. Used for HTTPS/WSS. + --credentials Credentials to access the /stats, /miners and /connections endponts. (usage: --credentials=username:password) \ No newline at end of file diff --git a/src/proxy/build/Connection.js b/src/proxy/build/Connection.js new file mode 100644 index 0000000..b8907c1 --- /dev/null +++ b/src/proxy/build/Connection.js @@ -0,0 +1,388 @@ +"use strict"; +var __extends = (this && this.__extends) || (function () { + var extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + return function (d, b) { + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +var EventEmitter = require("events"); +var Protocol = require('../../protocol/protocol'); +var uuid = require("uuid"); +var Queue_1 = require("./Queue"); +// var Metrics_1 = require("./Metrics"); +const COMP = '[Connect]'; +const Debug = require('../../log')(); +var Connection = /** @class */ (function (_super) { + __extends(Connection, _super); + function Connection(options) { + var _this = _super.call(this) || this; + _this.id = uuid.v4(); + _this.host = null; + _this.port = null; + _this.ssl = null; + _this.online = null; + _this.queue = null; + _this.protocolname = null; + _this.user = null; + _this.pass = null; + _this.Reconnect = true; + _this.buffer = ""; + _this.rpcId = 1; + _this.rpc = {}; + _this.auth = {}; + _this.minerId = {}; + _this.miners = []; + _this.host = options.host; + _this.port = options.port; + _this.ssl = options.ssl; + _this.user = options.user; + _this.pass = options.pass; + _this.cryptoname = options.cryptoname; + _this.protocolname = options.protocolname ? options.protocolname : 'jsonrpc'; + _this.Protocol = Protocol({ name: _this.protocolname, cryptoname: _this.cryptoname }); + return _this; + } + Connection.prototype.connect = function () { + var _this = this; + if (this.online) { + this.kill(); + } + this.queue = new Queue_1.default(); + this.Protocol.init(_this.ssl, _this.host, _this.port); + this.Protocol.on("connect", function () { + _this.ready(); + }); + + this.Protocol.on("error", function (error) { + if (_this.online) { + Debug.IbctLogErr(COMP, "Protocol error (" + _this.host + ":" + _this.port + ")", error.message); + _this.emit("error", error); + // exit protocol + _this.Protocol.kill(); + _this.Protocol.removeAllListeners("connect"); + _this.Protocol.removeAllListeners("error"); + _this.Protocol.removeAllListeners("close"); + _this.Protocol.removeAllListeners("data"); + _this.Protocol.removeAllListeners(); + } + }); + this.Protocol.on("close", function () { + if (_this.online) { + Debug.IbctLogDbg(COMP, "Protocol closed (" + _this.host + ":" + _this.port + ")"); + _this.emit("close"); + } + }); + this.online = true; + }; + Connection.prototype.setReconnect = function (flags) { + this.Reconnect = flags; + } + Connection.prototype.kill = function () { + this.Protocol.kill(); + this.Protocol.removeAllListeners(); + + if (this.queue != null) { + this.queue.stop(); + this.queue.removeAllListeners(); + } + if (this.online) { + this.online = false; + this.removeAllListeners() + } + }; + Connection.prototype.ready = function () { + var _this = this; + // message from pool + this.Protocol.getPacket(); + this.Protocol.on("data", function (message) { + if (_this.protocolname === 'jsonrpc') { + _this.receive(message); + } else { + _this.stratum_receive(message); + } + }); + // message from miner + this.queue.on("message", function (message) { + if (!_this.online) { + return false; + } + if (!_this.Protocol.isWritable()) { + if (message.method === "keepalived") { + return false; + } + var retry = message.retry ? message.retry * 2 : 1; + var ms = retry * 100; + message.retry = retry; + setTimeout(function () { + _this.queue.push({ + type: "message", + payload: message + }); + }, ms); + return false; + } + try { + if (message.retry) { + delete message.retry; + } + _this.Protocol.sendPacket(message); + } + catch (e) { + Debug.IbctLogErr(COMP, "failed to send message to pool (" + _this.host + ":" + _this.port + "): " + JSON.stringify(message)); + } + }); + // kick it + this.queue.start(); + this.emit("ready"); + }; + Connection.prototype.stratum_receive = function (message) { + // it's a response + Debug.IbctLogInfo(COMP, 'stratum_receive: ', JSON.stringify(message)); + if (message.id) { + var response = message; + if (!this.rpc[response.id]) { + if(response.result && response.result.subscribe){ + var subinfo = response.result; + var minerId = this.minerId[subinfo.id]; + if (!minerId) { + // miner is not online anymore + return; + } + this.emit(minerId + ":subscribe", subinfo.subscribe); + } + // miner is not online anymore + return; + } + var minerId = this.rpc[response.id].minerId; + var method = this.rpc[response.id].message.method; + switch (method) { + case "login": { + if (response.error && response.error.code === -1) { + this.emit(minerId + ":error", { + error: "invalid_site_key" + }); + return; + } else if (response.error) { + this.emit(minerId + ":error", { + error: response.error[1] + }); + return; + } + var result = response.result; + var auth = result.id; + this.auth[minerId] = auth; + this.minerId[auth] = minerId; + this.emit(minerId + ":authed", auth); + if (result.job) { + this.emit(minerId + ":job", result.job); + } + break; + } + case "submit": { + var job = this.rpc[response.id].message.params; + if (response.result === true || response.result === 'true') { + this.emit(minerId + ":accepted", job); + } else if (response.error) { + this.emit(minerId + ":rejected", job, response.error); + } else { + this.emit(minerId + ":rejected", job, 'Submit Error'); + } + + break; + } + default: { + if (response.error && response.error.code === -1) { + this.emit(minerId + ":error", response.error); + } + } + } + delete this.rpc[response.id]; + } + else { + // it's a request + var request = message; + if (request.error) { + this.emit(minerId + ":error", response.error); + return; + } + if (request.result && request.result.job) { + var jobParams = request.result; + var minerId = this.minerId[jobParams.id]; + if (!minerId) { + // miner is not online anymore + return; + } + this.emit(minerId + ":job", jobParams.job); + } + if(request.result && request.result.subscribe){ + var subinfo = request.result; + var minerId = this.minerId[subinfo.id]; + if (!minerId) { + // miner is not online anymore + return; + } + + this.emit(minerId + ":subscribe", subinfo.subscribe); + } + if(request.result && request.result.diff){ + var diffinfo = request.result; + var minerId = this.minerId[diffinfo.id]; + if (!minerId) { + // miner is not online anymore + Debug.IbctLogDbg(COMP, 'diff but no minerID.....'); + return; + } + this.emit(minerId + ":diff", diffinfo.diff); + } + } + }; + Connection.prototype.receive = function (message) { + var data = null; + try { + data = JSON.parse(message); + } + catch (e) { + return Debug.IbctLogErr(COMP, "invalid stratum message:", message); + } + // it's a response + Debug.IbctLogInfo(COMP, 'receive: ', JSON.stringify(data)); + if (data.id) { + var response = data; + if (!this.rpc[response.id]) { + // miner is not online anymore + return; + } + var minerId = this.rpc[response.id].minerId; + var method = this.rpc[response.id].message.method; + switch (method) { + case "login": { + if (response.error && response.error.code === -1) { + this.emit(minerId + ":error", { + error: "invalid_site_key" + }); + return; + } else if (response.error) { + this.emit(minerId + ":error", { + error: response.error.message + }); + return; + } + var result = response.result; + var auth = result.id; + this.auth[minerId] = auth; + this.minerId[auth] = minerId; + this.emit(minerId + ":authed", auth); + if (result.job) { + this.emit(minerId + ":job", result.job); + } + break; + } + case "submit": { + var job = this.rpc[response.id].message.params; + if (response.result && response.result.status === "OK") { + this.emit(minerId + ":accepted", job); + } + else if (response.error) { + this.emit(minerId + ":rejected", job, response.error); + } else { + this.emit(minerId + ":rejected", job, 'Submit Error'); + } + break; + } + default: { + if (response.error && response.error.code === -1) { + this.emit(minerId + ":error", response.error); + } + } + } + delete this.rpc[response.id]; + } + else { + // it's a request + var request = data; + switch (request.method) { + case "job": { + var jobParams = request.params; + var minerId = this.minerId[jobParams.id]; + if (!minerId) { + // miner is not online anymore + return; + } + this.emit(minerId + ":job", request.params); + break; + } + } + } + }; + Connection.prototype.send = function (id, method, params) { + if (params === void 0) { params = {}; } + var message = { + id: this.rpcId++, + method: method, + params: params + }; + switch (method) { + case "login": { + // .. + break; + } + case "keepalived": { + if (this.auth[id]) { + var keepAliveParams = message.params; + keepAliveParams.id = this.auth[id]; + } + else { + return false; + } + } + case "submit": { + if (this.auth[id]) { + var submitParams = message.params; + submitParams.id = this.auth[id]; + } + else { + Debug.IbctLogErr('Submit Err: Maybe have killed'); + return false; + } + } + } + this.rpc[message.id] = { + minerId: id, + message: message + }; + this.queue.push({ + type: "message", + payload: message + }); + }; + Connection.prototype.addMiner = function (miner) { + if (this.miners.indexOf(miner) === -1) { + this.miners.push(miner); + } + }; + Connection.prototype.removeMiner = function (minerId) { + var miner = this.miners.find(function (x) { return x.id === minerId; }); + if (miner) { + this.miners = this.miners.filter(function (x) { return x.id !== minerId; }); + this.clear(miner.id); + } + }; + Connection.prototype.clear = function (id) { + var _this = this; + var auth = this.auth[id]; + delete this.auth[id]; + delete this.minerId[auth]; + Object.keys(this.rpc).forEach(function (key) { + if (_this.rpc[key].minerId === id) { + delete _this.rpc[key]; + } + }); + }; + return Connection; +}(EventEmitter)); +exports.default = Connection; diff --git a/src/proxy/build/Metrics.js b/src/proxy/build/Metrics.js new file mode 100644 index 0000000..c75dd26 --- /dev/null +++ b/src/proxy/build/Metrics.js @@ -0,0 +1,18 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +var pmx = require("pmx"); +var probe = pmx.probe(); +const COMP = '[Metrics]'; +exports.minersCounter = probe.counter({ + name: "Miners" +}); +exports.connectionsCounter = probe.counter({ + name: "Connections" +}); +exports.sharesCounter = probe.counter({ + name: "Shares" +}); +exports.sharesMeter = probe.meter({ + name: "Shares per minute", + samples: 60 +}); diff --git a/src/proxy/build/Miner.js b/src/proxy/build/Miner.js new file mode 100644 index 0000000..7648c71 --- /dev/null +++ b/src/proxy/build/Miner.js @@ -0,0 +1,303 @@ +"use strict"; +var __extends = (this && this.__extends) || (function () { + var extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + return function (d, b) { + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function () { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; + return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function () { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (_) try { + if (f = 1, y && (t = y[op[0] & 2 ? "return" : op[0] ? "throw" : "next"]) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [0, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +Object.defineProperty(exports, "__esModule", { value: true }); +var EventEmitter = require("events"); +var uuid = require("uuid"); +var Queue_1 = require("./Queue"); +// var Metrics_1 = require("./Metrics"); +const Debug = require('../../log')(); +const COMP = '[Proxy Miner]'; +var Miner = /** @class */ (function (_super) { + __extends(Miner, _super); + function Miner(options) { + var _this = _super.call(this) || this; + _this.id = uuid.v4(); + _this.login = null; + _this.address = null; + _this.user = null; + _this.diff = null; + _this.pass = null; + _this.heartbeat = null; + _this.connection = null; + _this.minerQueue = null; + _this.protocolname = null; + _this.queue = new Queue_1.default(); + _this.online = false; + _this.jobs = []; + _this.hashes = 0; + _this.rejected = 0; + _this.connection = options.connection; + _this.address = options.address; + _this.user = options.user; + _this.diff = options.diff; + _this.pass = options.pass; + _this.minerQueue = options.minerQueue; + _this.queueId = options.queueId + _this.protocolname = options.protocolname; + return _this; + } + Miner.prototype.connect = function () { + return __awaiter(this, void 0, void 0, function () { + var _this = this; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + Debug.IbctLogDbg(COMP, "miner connected (" + this.id + ")"); + // Metrics_1.minersCounter.inc(); + this.connection.addMiner(this); + this.connection.on(this.id + ":authed", this.handleAuthed.bind(this)); + this.connection.on(this.id + ":job", this.handleJob.bind(this)); + this.connection.on(this.id + ":subscribe", this.handleSubscribe.bind(this)); + this.connection.on(this.id + ":diff", this.handleSetdiff.bind(this)); + this.connection.on(this.id + ":accepted", this.handleAccepted.bind(this)); + this.connection.on(this.id + ":error", this.handleError.bind(this)); + this.connection.on(this.id + ":rejected", this.handleRejected.bind(this)); + this.queue.on("message", function (message) { + return _this.connection.send(_this.id, message.method, message.params); + }); + if (this.protocolname === 'jsonrpc') { + this.heartbeat = setInterval(function () { return _this.connection.send(_this.id, "keepalived"); }, 30000); + } + this.online = true; + return [4 /*yield*/]; + case 1: + _a.sent(); + if (this.online) { + this.queue.start(); + this.emit("open", { + id: this.id + }); + } + return [2 /*return*/]; + } + }); + }); + }; + Miner.prototype.kill = function () { + if (this.queue) { + this.queue.stop(); + this.queue.removeAllListeners(); + } + this.connection.removeMiner(this.id); + this.connection.removeAllListeners(this.id + ":authed"); + this.connection.removeAllListeners(this.id + ":job"); + this.connection.removeAllListeners(this.id + ":accepted"); + this.connection.removeAllListeners(this.id + ":rejected"); + this.connection.removeAllListeners(this.id + ":error"); + this.jobs = []; + this.hashes = 0; + this.rejected = 0; + if (this.heartbeat) { + clearInterval(this.heartbeat); + this.heartbeat = null; + } + if (this.online) { + this.online = false; + // Metrics_1.minersCounter.dec(); + Debug.IbctLogDbg(COMP, "miner disconnected (" + this.id + ")"); + this.emit("close", { + id: this.id, + login: this.login + }); + } + this.removeAllListeners(); + }; + Miner.prototype.sendToMiner = function (payload) { + if (this.online && this.minerQueue) { + payload.type += ':' + this.queueId; + this.minerQueue.push(payload); + } + }; + Miner.prototype.sendToPool = function (method, params) { + this.queue.push({ + type: "message", + payload: { + method: method, + params: params + } + }); + }; + Miner.prototype.handleAuthed = function (auth) { + Debug.IbctLogDbg(COMP, "miner authenticated (" + this.id + "):", auth); + this.sendToMiner({ + type: "authed", + payload: { + token: "", + hashes: 0, + auth: auth + } + }); + this.emit("authed", { + id: this.id, + login: this.login, + auth: auth + }); + }; + + Miner.prototype.handleSetdiff = function (diff) { + Debug.IbctLogDbg(COMP, "miner setdiff (" + this.id + "):", diff); + this.sendToMiner({ + type: "diff", + payload:diff + }); + this.emit("diff", { + id: this.id, + login: this.login, + diff: diff + }); + }; + Miner.prototype.handleSubscribe = function (subscribe) { + Debug.IbctLogDbg(COMP, "miner subscribed (" + this.id + "):", subscribe); + this.sendToMiner({ + type: "subscribe", + payload: subscribe + + }); + this.emit("subscribe", { + id: this.id, + login: this.login, + subscribe: subscribe + }); + }; + Miner.prototype.handleJob = function (job) { + Debug.IbctLogDbg(COMP, "job arrived (" + this.id + "):", job[0]); + this.jobs.push(job); + + this.sendToMiner({ + type: "job", + payload: this.jobs.pop() + }); + + this.emit("job", { + id: this.id, + login: this.login, + job: job + }); + }; + Miner.prototype.handleAccepted = function (job) { + this.hashes++; + Debug.IbctLogDbg(COMP, "shares accepted (" + this.id + "):", this.hashes); + // Metrics_1.sharesCounter.inc(); + // Metrics_1.sharesMeter.mark(); + this.sendToMiner({ + type: "accepted", + payload: { + hashes: this.hashes, + nonce: job + } + }); + this.emit("accepted", { + id: this.id, + login: this.login, + hashes: this.hashes + }); + }; + Miner.prototype.handleError = function (error) { + Debug.IbctLogErr(COMP, "pool connection error (" + this.id + "):", error.error || (error && JSON.stringify(error)) || "unknown error"); + if (this.online) { + if (error.error === "invalid_site_key") { + this.sendToMiner({ + type: "error", + payload: error + }); + } + this.emit("error", { + id: this.id, + login: this.login, + error: error + }); + } + this.kill(); + }; + Miner.prototype.handleRejected = function (job, error) { + Debug.IbctLogDbg(COMP, "shares rejected(" + this.id + "):", this.hashes); + this.rejected++; + this.sendToMiner({ + type: "rejected", + payload: { + rejected: this.rejected, + nonce: job, + err: error + } + }); + this.emit("rejected", { + id: this.id, + login: this.login, + rejected: this.rejected + }); + }; + Miner.prototype.handleMessage = function (message) { + switch (message.type) { + case "auth": { + var params = message.params; + this.login = this.user || params.user; + if (this.diff) { + this.login += "+" + this.diff; + } + this.sendToPool("login", { + login: this.login, + pass: this.pass, + agent: "Ibctminer/1.0.0" + }); + break; + } + case "submit": { + var job = message.params; + Debug.IbctLogDbg(COMP, "job submitted (" + this.id + "):", job.job_id); + this.sendToPool("submit", job); + this.emit("found", { + id: this.id, + login: this.login, + job: job + }); + break; + } + } + }; + return Miner; +}(EventEmitter)); +exports.default = Miner; diff --git a/src/proxy/build/Proxy.js b/src/proxy/build/Proxy.js new file mode 100644 index 0000000..def998d --- /dev/null +++ b/src/proxy/build/Proxy.js @@ -0,0 +1,141 @@ +"use strict"; +var __extends = (this && this.__extends) || (function () { + var extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + return function (d, b) { + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +var EventEmitter = require("events"); +var defaults = require("../config/defaults"); +var Connection_1 = require("./Connection"); +var Miner_1 = require("./Miner"); +const Debug = require('../../log')(); +const COMP = '[Proxy]'; +var Proxy = /** @class */ (function (_super) { + __extends(Proxy, _super); + function Proxy(constructorOptions) { + if (constructorOptions === void 0) { constructorOptions = defaults; } + var _this = _super.call(this) || this; + _this.host = null; + _this.port = null; + _this.pass = null; + _this.ssl = null; + _this.address = null; + _this.user = null; + _this.maxMinersPerConnection = 100; + _this.connections = {}; + _this.online = false; + var options = Object.assign({}, defaults, constructorOptions); + _this.host = options.host; + _this.port = options.port; + _this.pass = options.pass; + _this.ssl = options.ssl; + _this.protocolname = options.protocolname; + _this.cryptoname = options.cryptoname; + _this.address = options.address; + _this.user = options.user; + _this.maxMinersPerConnection = options.maxMinersPerConnection; + _this.on("error", function (error) { + /* prevent unhandled proxy errors from stopping the proxy */ + Debug.IbctLogErr(COMP, "proxy error:", error); + }); + } + Proxy.prototype.createProxy = function (id, queue) { + var _this = this; + if (this.online) { + this.kill(); + } + var connection = _this.getConnection(_this.host, _this.port); + var miner = new Miner_1.default({ + protocolname: _this.protocolname, + connection: connection, + address: _this.address, + user: _this.user, + pass: _this.pass, + minerQueue: queue, + queueId: id + }); + miner.on("open", function (data) { return _this.emit("open", data); }); + miner.on("authed", function (data) { return _this.emit("authed", data); }); + miner.on("job", function (data) {return _this.emit("job", data); }); + miner.on("diff", function (data) { return _this.emit("diff", data); }); + miner.on("subscribe", function (data) {return _this.emit("subscribe", data); }); + miner.on("found", function (data) { return _this.emit("found", data); }); + miner.on("accepted", function (data) { return _this.emit("accepted", data); }); + miner.on("rejected", function (data) { return _this.emit("rejected", data); }); + miner.on("close", function (data) { return _this.emit("close", data); }); + miner.on("error", function (data) { return _this.emit("error", data); }); + miner.connect(); + + return miner; + }; + Proxy.prototype.getConnection = function (host, port) { + var _this = this; + var connectionId = host + ":" + port; + if (!this.connections[connectionId]) { + this.connections[connectionId] = []; + } + var connections = this.connections[connectionId]; + var availableConnections = connections.filter(function (connection) { return _this.isAvailable(connection); }); + if (availableConnections.length === 0) { + var connection = new Connection_1.default({ host: host, port: port, user: this.user, pass: this.pass, ssl: this.ssl, protocolname: this.protocolname, cryptoname: this.cryptoname }); + connection.connect(); + connection.on("close", function () { + Debug.IbctLogDbg(COMP, "connection closed (" + connectionId + ")"); + }); + connection.on("error", function (error) { + Debug.IbctLogErr(COMP, "connection error (" + connectionId + "):", error.message); + _this.emit("error", error); + }); + connections.push(connection); + return connection; + } + return availableConnections.pop(); + }; + Proxy.prototype.isAvailable = function (connection) { + return (connection.miners.length < this.maxMinersPerConnection); + }; + Proxy.prototype.isEmpty = function (connection) { + return connection.miners.length === 0; + }; + Proxy.prototype.getStats = function () { + var _this = this; + return Object.keys(this.connections).reduce(function (stats, key) { return ({ + miners: stats.miners.concat(_this.connections[key].reduce(function (miners, connection) { return miners.concat(connection.miners.map(function (miner) { return ({ + id: miner.id, + login: miner.login, + hashes: miner.hashes + }); })); }, [])), + connections: stats.connections.concat(_this.connections[key].map(function (connection) { return ({ + id: connection.id, + host: connection.host, + port: connection.port, + miners: connection.miners.length + }); })) + }); }, { + miners: [], + connections: [] + }); + }; + Proxy.prototype.kill = function () { + var _this = this; + + Object.keys(this.connections).forEach(function (connectionId) { + var connections = _this.connections[connectionId]; + connections.forEach(function (connection) { + connection.setReconnect(false); + connection.kill(); + connection.miners.forEach(function (miner) { return miner.kill(); }); + }); + }); + this.online = false; + Debug.IbctLogDbg(COMP, "\uD83D\uDC80"); + }; + return Proxy; +}(EventEmitter)); +exports.default = Proxy; diff --git a/src/proxy/build/Queue.js b/src/proxy/build/Queue.js new file mode 100644 index 0000000..60df935 --- /dev/null +++ b/src/proxy/build/Queue.js @@ -0,0 +1,63 @@ +"use strict"; +var __extends = (this && this.__extends) || (function () { + var extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + return function (d, b) { + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +var EventEmitter = require("events"); +const Debug = require('../../log')(); +const COMP = '[Queue]'; +var Queue = /** @class */ (function (_super) { + __extends(Queue, _super); + function Queue(ms) { + if (ms === void 0) { ms = 100; } + var _this = _super.call(this) || this; + _this.events = []; + _this.interval = null; + _this.bypassed = false; + _this.ms = 100; + _this.ms = ms; + return _this; + } + Queue.prototype.start = function () { + var _this = this; + if (this.interval == null) { + var that_1 = this; + this.interval = setInterval(function () { + var event = that_1.events.pop(); + if (event) { + that_1.emit(event.type, event.payload); + } + else { + _this.bypass(); + } + }, this.ms); + } + }; + Queue.prototype.stop = function () { + if (this.interval != null) { + clearInterval(this.interval); + this.interval = null; + } + }; + Queue.prototype.bypass = function () { + this.bypassed = true; + this.stop(); + }; + Queue.prototype.push = function (event) { + if (this.bypassed) { + this.emit(event.type, event.payload); + } + else { + this.events.push(event); + } + }; + return Queue; +}(EventEmitter)); +exports.default = Queue; diff --git a/src/proxy/build/index.js b/src/proxy/build/index.js new file mode 100644 index 0000000..73623d7 --- /dev/null +++ b/src/proxy/build/index.js @@ -0,0 +1,9 @@ +"use strict"; +var Proxy = require("./Proxy"); +const Debug = require('../../log')(); +const COMP = '[Proxy Index]'; +process.on("uncaughtException", function (error) { + /* prevent unhandled process errors from stopping the proxy */ + Debug.IbctLogErr(COMP, "process error:", error); +}); +module.exports = Proxy.default; diff --git a/src/proxy/build/types.js b/src/proxy/build/types.js new file mode 100644 index 0000000..d30d239 --- /dev/null +++ b/src/proxy/build/types.js @@ -0,0 +1,3 @@ +"use strict"; +// Misc +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/src/proxy/config/defaults.js b/src/proxy/config/defaults.js new file mode 100644 index 0000000..2109d8b --- /dev/null +++ b/src/proxy/config/defaults.js @@ -0,0 +1,11 @@ +module.exports = { + host: null, + port: 3333, + pass: "x", + ssl: false, + address: null, + user: null, + diff: null, + dynamicPool: false, + maxMinersPerConnection: 100, +}; diff --git a/src/proxy/package.json b/src/proxy/package.json new file mode 100644 index 0000000..43a969e --- /dev/null +++ b/src/proxy/package.json @@ -0,0 +1,218 @@ +{ + "name": "coin-hive-stratum", + "version": "2.6.7", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@types/node": { + "version": "8.0.53", + "resolved": "https://registry.npmjs.org/@types/node/-/node-8.0.53.tgz", + "integrity": "sha512-54Dm6NwYeiSQmRB1BLXKr5GELi0wFapR1npi8bnZhEcu84d/yQKqnwwXQ56hZ0RUbTG6L5nqDZaN3dgByQXQRQ==" + }, + "@types/ws": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-3.2.0.tgz", + "integrity": "sha512-XehU2SdII5wu7EUV1bAwCoTDZYZCCU7Es7gbHtJjGXq6Bs2AI4HuJ//wvPrVuuYwkkZseQzDUxsZF8Urnb3I1A==", + "requires": { + "@types/node": "8.0.53" + } + }, + "async-limiter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", + "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==" + }, + "async-listener": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/async-listener/-/async-listener-0.6.8.tgz", + "integrity": "sha512-1Sy1jDhjlgxcSd9/ICHqiAHT8VSJ9R1lzEyWwP/4Hm9p8nVTNtU0SxG/Z15XHD/aZvQraSw9BpDU3EBcFnOVrw==", + "requires": { + "semver": "5.4.1", + "shimmer": "1.2.0" + } + }, + "basic-auth": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.0.tgz", + "integrity": "sha1-AV2z81PgLlY3d1X5YnQuiYHnu7o=", + "requires": { + "safe-buffer": "5.1.1" + } + }, + "continuation-local-storage": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/continuation-local-storage/-/continuation-local-storage-3.2.1.tgz", + "integrity": "sha512-jx44cconVqkCEEyLSKWwkvUXwO561jXMa3LPjTPsm5QR22PA0/mhe33FT4Xb5y74JDvt/Cq+5lm8S8rskLv9ZA==", + "requires": { + "async-listener": "0.6.8", + "emitter-listener": "1.1.1" + } + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "emitter-listener": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/emitter-listener/-/emitter-listener-1.1.1.tgz", + "integrity": "sha1-6Lu+gkS8jg0LTvcc0UKUx/JBx+w=", + "requires": { + "shimmer": "1.2.0" + } + }, + "exec-sh": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.2.1.tgz", + "integrity": "sha512-aLt95pexaugVtQerpmE51+4QfWrNc304uez7jvj6fWnN8GeEHpttB8F36n8N7uVhUMbH/1enbxQ9HImZ4w/9qg==", + "requires": { + "merge": "1.2.0" + } + }, + "extend": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", + "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=" + }, + "is": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/is/-/is-3.2.1.tgz", + "integrity": "sha1-0Kwq1V63sL7JJqUmb2xmKqqD3KU=" + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "lodash.findindex": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.findindex/-/lodash.findindex-4.6.0.tgz", + "integrity": "sha1-oyRd7mH7m24GJLU1ElYku2nBEQY=" + }, + "lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=" + }, + "lodash.merge": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.0.tgz", + "integrity": "sha1-aYhLoUSsM/5plzemCG3v+t0PicU=" + }, + "merge": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/merge/-/merge-1.2.0.tgz", + "integrity": "sha1-dTHjnUlJwoGma4xabgJl6LBYlNo=" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + }, + "moment": { + "version": "2.19.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.19.1.tgz", + "integrity": "sha1-VtoaLRy/AdOLfhr8McELz6GSkWc=" + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "pmx": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/pmx/-/pmx-1.5.5.tgz", + "integrity": "sha1-tuC4V27c9Y1/QGlntE2z2nfjV/A=", + "requires": { + "debug": "3.1.0", + "json-stringify-safe": "5.0.1", + "vxx": "1.2.2" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + } + } + }, + "safe-buffer": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" + }, + "semver": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", + "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==" + }, + "shimmer": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.0.tgz", + "integrity": "sha512-xTCx2vohXC2EWWDqY/zb4+5Mu28D+HYNSOuFzsyRDRvI/e1ICb69afwaUwfjr+25ZXldbOLyp+iDUZHq8UnTag==" + }, + "typescript": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.6.1.tgz", + "integrity": "sha1-7znN6ierrAtQAkLWcmq5DgyEZjE=" + }, + "ultron": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.0.tgz", + "integrity": "sha1-sHoualQagV/Go0zNRTO67DB8qGQ=" + }, + "uuid": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", + "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==" + }, + "vxx": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/vxx/-/vxx-1.2.2.tgz", + "integrity": "sha1-dB+1HG8R0zg9pvm5IBil17qAdhE=", + "requires": { + "continuation-local-storage": "3.2.1", + "debug": "2.6.9", + "extend": "3.0.1", + "is": "3.2.1", + "lodash.findindex": "4.6.0", + "lodash.isequal": "4.5.0", + "lodash.merge": "4.6.0", + "methods": "1.1.2", + "semver": "5.4.1", + "shimmer": "1.2.0", + "uuid": "3.1.0" + } + }, + "watch": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/watch/-/watch-1.0.2.tgz", + "integrity": "sha1-NApxe952Vyb6CqB9ch4BR6VR3ww=", + "requires": { + "exec-sh": "0.2.1", + "minimist": "1.2.0" + } + }, + "ws": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-3.2.0.tgz", + "integrity": "sha512-hTS3mkXm/j85jTQOIcwVz3yK3up9xHgPtgEhDBOH3G18LDOZmSAG1omJeXejLKJakx+okv8vS1sopgs7rw0kVw==", + "requires": { + "async-limiter": "1.0.0", + "safe-buffer": "5.1.1", + "ultron": "1.1.0" + } + } + } +} diff --git a/src/proxy/server.js b/src/proxy/server.js new file mode 100644 index 0000000..1545af6 --- /dev/null +++ b/src/proxy/server.js @@ -0,0 +1,10 @@ +const Proxy = require("./build"); +const proxy = new Proxy({ + +}); +proxy.listen(process.env.PORT || 8892); + +setInterval(function() { + console.log(`Going to reload proxy`); + process.exit(0); +}, 14400 * 1000); diff --git a/src/proxy/src/Connection.ts b/src/proxy/src/Connection.ts new file mode 100644 index 0000000..3cf1ab6 --- /dev/null +++ b/src/proxy/src/Connection.ts @@ -0,0 +1,275 @@ +import * as EventEmitter from "events"; +import * as net from "net"; +import * as tls from "tls"; +import * as uuid from "uuid"; +import Miner from "./Miner"; +import Queue from "./Queue"; +import { connectionsCounter } from "./Metrics"; +import { + Dictionary, + Socket, + StratumRequestParams, + StratumResponse, + StratumRequest, + StratumJob, + StratumLoginResult, + RPCMessage, + StratumKeepAlive, + Job +} from "./types"; + +export type Options = { + host: string; + port: number; + ssl: boolean; +}; + +class Connection extends EventEmitter { + id: string = uuid.v4(); + host: string = null; + port: number = null; + ssl: boolean = null; + online: boolean = null; + socket: Socket = null; + queue: Queue = null; + buffer: string = ""; + rpcId: number = 1; + rpc: Dictionary = {}; + auth: Dictionary = {}; + minerId: Dictionary = {}; + miners: Miner[] = []; + + constructor(options: Options) { + super(); + this.host = options.host; + this.port = options.port; + this.ssl = options.ssl; + } + + connect() { + if (this.online) { + this.kill(); + } + this.queue = new Queue(); + if (this.ssl) { + this.socket = tls.connect(+this.port, this.host, { rejectUnauthorized: false }); + } else { + this.socket = net.connect(+this.port, this.host); + } + this.socket.on("connect", this.ready.bind(this)); + this.socket.on("error", error => { + if (this.online) { + console.warn(`socket error (${this.host}:${this.port})`, error.message); + this.emit("error", error); + this.connect(); + } + }); + this.socket.on("close", () => { + if (this.online) { + console.log(`socket closed (${this.host}:${this.port})`); + this.emit("close"); + } + }); + this.socket.setKeepAlive(true); + this.socket.setEncoding("utf8"); + this.online = true; + } + + kill() { + if (this.socket != null) { + try { + this.socket.end(); + this.socket.destroy(); + } catch (e) { + console.warn(`something went wrong while destroying socket (${this.host}:${this.port}):`, e.message); + } + } + if (this.queue != null) { + this.queue.stop(); + } + if (this.online) { + this.online = false; + } + } + + ready() { + // message from pool + this.socket.on("data", chunk => { + this.buffer += chunk; + while (this.buffer.includes("\n")) { + const newLineIndex = this.buffer.indexOf("\n"); + const stratumMessage = this.buffer.slice(0, newLineIndex); + this.buffer = this.buffer.slice(newLineIndex + 1); + this.receive(stratumMessage); + } + }); + // message from miner + this.queue.on("message", (message: StratumRequest) => { + if (!this.online) { + return false; + } + if (!this.socket.writable) { + if (message.method === "keepalived") { + return false; + } + const retry = message.retry ? message.retry * 2 : 1; + const ms = retry * 100; + message.retry = retry; + setTimeout(() => { + this.queue.push({ + type: "message", + payload: message + }); + }, ms); + return false; + } + try { + if (message.retry) { + delete message.retry; + } + this.socket.write(JSON.stringify(message) + "\n"); + } catch (e) { + console.warn(`failed to send message to pool (${this.host}:${this.port}): ${JSON.stringify(message)}`); + } + }); + // kick it + this.queue.start(); + this.emit("ready"); + } + + receive(message: string) { + let data = null; + try { + data = JSON.parse(message); + } catch (e) { + return console.warn(`invalid stratum message:`, message); + } + // it's a response + if (data.id) { + const response = data as StratumResponse; + if (!this.rpc[response.id]) { + // miner is not online anymore + return; + } + const minerId = this.rpc[response.id].minerId; + const method = this.rpc[response.id].message.method; + switch (method) { + case "login": { + if (response.error && response.error.code === -1) { + this.emit(minerId + ":error", { + error: "invalid_site_key" + }); + return; + } + const result = response.result as StratumLoginResult; + const auth = result.id; + this.auth[minerId] = auth; + this.minerId[auth] = minerId; + this.emit(minerId + ":authed", auth); + if (result.job) { + this.emit(minerId + ":job", result.job); + } + break; + } + case "submit": { + const job = this.rpc[response.id].message.params as StratumJob; + if (response.result && response.result.status === "OK") { + this.emit(minerId + ":accepted", job); + } else if (response.error) { + this.emit(minerId + ":error", response.error); + } + break; + } + default: { + if (response.error && response.error.code === -1) { + this.emit(minerId + ":error", response.error); + } + } + } + delete this.rpc[response.id]; + } else { + // it's a request + const request = data as StratumRequest; + switch (request.method) { + case "job": { + const jobParams = request.params as StratumJob; + const minerId = this.minerId[jobParams.id]; + if (!minerId) { + // miner is not online anymore + return; + } + this.emit(minerId + ":job", request.params); + break; + } + } + } + } + + send(id: string, method: string, params: StratumRequestParams = {}) { + let message: StratumRequest = { + id: this.rpcId++, + method, + params + }; + + switch (method) { + case "login": { + // .. + break; + } + case "keepalived": { + if (this.auth[id]) { + const keepAliveParams = message.params as StratumKeepAlive; + keepAliveParams.id = this.auth[id]; + } else { + return false; + } + } + case "submit": { + if (this.auth[id]) { + const submitParams = message.params as StratumJob; + submitParams.id = this.auth[id]; + } else { + return false; + } + } + } + + this.rpc[message.id] = { + minerId: id, + message + }; + + this.queue.push({ + type: "message", + payload: message + }); + } + + addMiner(miner: Miner): void { + if (this.miners.indexOf(miner) === -1) { + this.miners.push(miner); + } + } + + removeMiner(minerId: string): void { + const miner = this.miners.find(x => x.id === minerId); + if (miner) { + this.miners = this.miners.filter(x => x.id !== minerId); + this.clear(miner.id); + } + } + + clear(id: string): void { + const auth = this.auth[id]; + delete this.auth[id]; + delete this.minerId[auth]; + Object.keys(this.rpc).forEach(key => { + if (this.rpc[key].minerId === id) { + delete this.rpc[key]; + } + }); + } +} + +export default Connection; diff --git a/src/proxy/src/Metrics.ts b/src/proxy/src/Metrics.ts new file mode 100644 index 0000000..4c57243 --- /dev/null +++ b/src/proxy/src/Metrics.ts @@ -0,0 +1,19 @@ +import * as pmx from "pmx"; +const probe = pmx.probe(); + +export const minersCounter = probe.counter({ + name: "Miners" +}); + +export const connectionsCounter = probe.counter({ + name: "Connections" +}); + +export const sharesCounter = probe.counter({ + name: "Shares" +}); + +export const sharesMeter = probe.meter({ + name: "Shares per minute", + samples: 60 +}); diff --git a/src/proxy/src/Miner.ts b/src/proxy/src/Miner.ts new file mode 100644 index 0000000..288c005 --- /dev/null +++ b/src/proxy/src/Miner.ts @@ -0,0 +1,248 @@ +import * as EventEmitter from "events"; +import * as WebSocket from "ws"; +import * as uuid from "uuid"; +import Connection from "./Connection"; +import Queue from "./Queue"; +import { minersCounter, sharesCounter, sharesMeter } from "./Metrics"; +import { + Job, + CoinHiveError, + CoinHiveResponse, + CoinHiveLoginParams, + CoinHiveRequest, + StratumRequest, + StratumRequestParams, + StratumError, + StratumJob +} from "./types"; + +export type Options = { + connection: Connection | null; + ws: WebSocket | null; + address: string | null; + user: string | null; + diff: number | null; + pass: string | null; +}; + +class Miner extends EventEmitter { + id: string = uuid.v4(); + login: string = null; + address: string = null; + user: string = null; + diff: number = null; + pass: string = null; + heartbeat: NodeJS.Timer = null; + connection: Connection = null; + queue: Queue = new Queue(); + ws: WebSocket = null; + online: boolean = false; + jobs: Job[] = []; + hashes: number = 0; + + constructor(options: Options) { + super(); + this.connection = options.connection; + this.ws = options.ws; + this.address = options.address; + this.user = options.user; + this.diff = options.diff; + this.pass = options.pass; + } + + async connect() { + console.log(`miner connected (${this.id})`); + minersCounter.inc(); + this.ws.on("message", this.handleMessage.bind(this)); + this.ws.on("close", () => { + if (this.online) { + console.log(`miner connection closed (${this.id})`); + this.kill(); + } + }); + this.ws.on("error", error => { + if (this.online) { + console.log(`miner connection error (${this.id}):`, error.message); + this.kill(); + } + }); + this.connection.addMiner(this); + this.connection.on(this.id + ":authed", this.handleAuthed.bind(this)); + this.connection.on(this.id + ":job", this.handleJob.bind(this)); + this.connection.on(this.id + ":accepted", this.handleAccepted.bind(this)); + this.connection.on(this.id + ":error", this.handleError.bind(this)); + this.queue.on("message", (message: StratumRequest) => + this.connection.send(this.id, message.method, message.params) + ); + this.heartbeat = setInterval(() => this.connection.send(this.id, "keepalived"), 30000); + this.online = true; + if (this.online) { + this.queue.start(); + console.log(`miner started (${this.id})`); + this.emit("open", { + id: this.id + }); + } + } + + kill() { + this.queue.stop(); + this.connection.removeMiner(this.id); + this.connection.removeAllListeners(this.id + ":authed"); + this.connection.removeAllListeners(this.id + ":job"); + this.connection.removeAllListeners(this.id + ":accepted"); + this.connection.removeAllListeners(this.id + ":error"); + this.jobs = []; + this.hashes = 0; + this.ws.close(); + if (this.heartbeat) { + clearInterval(this.heartbeat); + this.heartbeat = null; + } + if (this.online) { + this.online = false; + minersCounter.dec(); + console.log(`miner disconnected (${this.id})`); + this.emit("close", { + id: this.id, + login: this.login + }); + } + this.removeAllListeners(); + } + + sendToMiner(payload: CoinHiveResponse) { + const coinhiveMessage = JSON.stringify(payload); + if (this.online && this.ws.readyState === WebSocket.OPEN) { + try { + this.ws.send(coinhiveMessage); + } catch (e) { + this.kill(); + } + } + } + + sendToPool(method: string, params: StratumRequestParams) { + this.queue.push({ + type: "message", + payload: { + method, + params + } + }); + } + + handleAuthed(auth: string): void { + console.log(`miner authenticated (${this.id}):`, auth); + this.sendToMiner({ + type: "authed", + params: { + token: "", + hashes: 0 + } + }); + this.emit("authed", { + id: this.id, + login: this.login, + auth + }); + } + + handleJob(job: Job): void { + console.log(`job arrived (${this.id}):`, job.job_id); + this.jobs.push(job); + + this.sendToMiner({ + type: "job", + params: this.jobs.pop() + }); + + this.emit("job", { + id: this.id, + login: this.login, + job + }); + } + + handleAccepted(job: StratumJob): void { + this.hashes++; + console.log(`shares accepted (${this.id}):`, this.hashes); + sharesCounter.inc(); + sharesMeter.mark(); + this.sendToMiner({ + type: "hash_accepted", + params: { + hashes: this.hashes + } + }); + this.emit("accepted", { + id: this.id, + login: this.login, + hashes: this.hashes + }); + } + + handleError(error: StratumError): void { + console.warn( + `pool connection error (${this.id}):`, + error.error || (error && JSON.stringify(error)) || "unknown error" + ); + if (this.online) { + if (error.error === "invalid_site_key") { + this.sendToMiner({ + type: "error", + params: error + }); + } + this.emit("error", { + id: this.id, + login: this.login, + error + }); + } + this.kill(); + } + + handleMessage(message: string) { + let data: CoinHiveRequest; + try { + data = JSON.parse(message); + } catch (e) { + console.warn(`can't parse message as JSON from miner:`, message, e.message); + return; + } + switch (data.type) { + case "auth": { + const params = data.params as CoinHiveLoginParams; + this.login = this.address || params.site_key; + const user = this.user || params.user; + if (user) { + this.login += "." + user; + } + if (this.diff) { + this.login += "+" + this.diff; + } + this.sendToPool("login", { + login: this.login, + pass: this.pass + }); + break; + } + + case "submit": { + const job = data.params as Job; + console.log(`job submitted (${this.id}):`, job.job_id); + this.sendToPool("submit", job); + + this.emit("found", { + id: this.id, + login: this.login, + job + }); + break; + } + } + } +} + +export default Miner; diff --git a/src/proxy/src/Proxy.ts b/src/proxy/src/Proxy.ts new file mode 100644 index 0000000..95a2b11 --- /dev/null +++ b/src/proxy/src/Proxy.ts @@ -0,0 +1,310 @@ +import * as EventEmitter from "events"; +import * as WebSocket from "ws"; +import * as url from "url"; +import * as http from "http"; +import * as https from "https"; +import * as defaults from "../config/defaults"; +import Connection from "./Connection"; +import Miner from "./Miner"; +import { + Dictionary, + Stats, + WebSocketQuery, + ErrorEvent, + CloseEvent, + AcceptedEvent, + FoundEvent, + JobEvent, + AuthedEvent, + OpenEvent, + Credentials +} from "./types"; +import { ServerRequest } from "http"; + +export type Options = { + host: string; + port: number; + pass: string; + ssl: false; + address: string | null; + user: string | null; + diff: number | null; + dynamicPool: boolean; + maxMinersPerConnection: number; + key: Buffer; + cert: Buffer; + path: string; + server: http.Server | https.Server; + credentials: Credentials; +}; + +class Proxy extends EventEmitter { + host: string = null; + port: number = null; + pass: string = null; + ssl: boolean = null; + address: string = null; + user: string = null; + diff: number = null; + dynamicPool: boolean = false; + maxMinersPerConnection: number = 100; + connections: Dictionary = {}; + wss: WebSocket.Server = null; + key: Buffer = null; + cert: Buffer = null; + path: string = null; + server: http.Server | https.Server = null; + credentials: Credentials = null; + online: boolean = false; + + constructor(constructorOptions: Partial = defaults) { + super(); + let options = Object.assign({}, defaults, constructorOptions) as Options; + this.host = options.host; + this.port = options.port; + this.pass = options.pass; + this.ssl = options.ssl; + this.address = options.address; + this.user = options.user; + this.diff = options.diff; + this.dynamicPool = options.dynamicPool; + this.maxMinersPerConnection = options.maxMinersPerConnection; + this.key = options.key; + this.cert = options.cert; + this.path = options.path; + this.server = options.server; + this.credentials = options.credentials; + this.on("error", error => { + /* prevent unhandled proxy errors from stopping the proxy */ + console.error("proxy error:", error.message); + }); + } + + listen(port: number, host?: string, callback?: () => void): void { + const version = require("../package").version; + console.log(`coin-hive-stratum v${version}`); + if (this.online) { + this.kill(); + } + // create server + const isHTTPS = !!(this.key && this.cert); + if (!this.server) { + const stats = (req: http.ServerRequest, res: http.ServerResponse) => { + if (this.credentials) { + const auth = require("basic-auth")(req); + if (!auth || auth.name !== this.credentials.user || auth.pass !== this.credentials.pass) { + res.statusCode = 401; + res.setHeader("WWW-Authenticate", 'Basic realm="Access to stats"'); + res.end("Access denied"); + return; + } + } + const url = require("url").parse(req.url); + + if (url.pathname === "/ping") { + res.statusCode = 200; + res.end(); + return; + } + + if (url.pathname === "/ready") { + res.statusCode = this.online ? 200 : 503; + res.end(); + return; + } + + if (url.pathname === "/version") { + const body = JSON.stringify({ version }); + res.writeHead(200, { + "Access-Control-Allow-Origin": "*", + "Content-Length": Buffer.byteLength(body), + "Content-Type": "application/json", + }); + res.end(body); + return; + } + + const proxyStats = this.getStats(); + let body = JSON.stringify({ + code: 404, + error: "Not Found" + }); + + if (url.pathname === "/stats") { + body = JSON.stringify( + { + miners: proxyStats.miners.length, + connections: proxyStats.connections.length + }, + null, + 2 + ); + } + + if (url.pathname === "/miners") { + body = JSON.stringify(proxyStats.miners, null, 2); + } + + if (url.pathname === "/connections") { + body = JSON.stringify(proxyStats.connections, null, 2); + } + + res.writeHead(200, { + "Access-Control-Allow-Origin": "*", + "Content-Length": Buffer.byteLength(body), + "Content-Type": "application/json" + }); + res.end(body); + }; + if (isHTTPS) { + const certificates = { + key: this.key, + cert: this.cert + }; + this.server = https.createServer(certificates, stats); + } else { + this.server = http.createServer(stats); + } + } + const wssOptions: WebSocket.ServerOptions = { + server: this.server + }; + if (this.path) { + wssOptions.path = this.path; + } + this.wss = new WebSocket.Server(wssOptions); + this.wss.on("connection", (ws: WebSocket, req: ServerRequest) => { + const params = url.parse(req.url, true).query as WebSocketQuery; + params.pool = params.id; + let host = this.host; + let port = this.port; + let pass = this.pass; + if (params.pool && this.dynamicPool) { + const split = params.pool.split(":"); + host = split[0] || this.host; + port = Number(split[1]) || this.port; + pass = split[2] || this.pass; + console.log(`Miner connected to pool`, host); + } + const connection = this.getConnection(host, port); + const miner = new Miner({ + connection, + ws, + address: this.address, + user: this.user, + diff: this.diff, + pass + }); + miner.on("open", (data: OpenEvent) => this.emit("open", data)); + miner.on("authed", (data: AuthedEvent) => this.emit("authed", data)); + miner.on("job", (data: JobEvent) => this.emit("job", data)); + miner.on("found", (data: FoundEvent) => this.emit("found", data)); + miner.on("accepted", (data: AcceptedEvent) => this.emit("accepted", data)); + miner.on("close", (data: CloseEvent) => this.emit("close", data)); + miner.on("error", (data: ErrorEvent) => this.emit("error", data)); + miner.connect(); + }); + if (!host && !callback) { + this.server.listen(port); + } else if (!host && callback) { + this.server.listen(port, callback); + } else if (host && !callback) { + this.server.listen(port, host); + } else { + this.server.listen(port, host, callback); + } + this.wss.on("listening", () => { + this.online = true; + console.log(`listening on port ${port}` + (isHTTPS ? ", using a secure connection" : "")); + console.log(`miners per connection:`, this.maxMinersPerConnection); + if (wssOptions.path) { + console.log(`path: ${wssOptions.path}`); + } + if (!this.dynamicPool) { + console.log(`host: ${this.host}`); + console.log(`port: ${this.port}`); + console.log(`pass: ${this.pass}`); + } + }); + } + + getConnection(host: string, port: number): Connection { + const connectionId = `${host}:${port}`; + if (!this.connections[connectionId]) { + this.connections[connectionId] = []; + } + const connections = this.connections[connectionId]; + const availableConnections = connections.filter(connection => this.isAvailable(connection)); + if (availableConnections.length === 0) { + const connection = new Connection({ host, port, ssl: this.ssl }); + connection.connect(); + connection.on("close", () => { + console.log(`connection closed (${connectionId})`); + }); + connection.on("error", error => { + console.log(`connection error (${connectionId}):`, error.message); + }); + connections.push(connection); + return connection; + } + return availableConnections.pop(); + } + + isAvailable(connection: Connection): boolean { + return ( + connection.miners.length < this.maxMinersPerConnection); + } + + isEmpty(connection: Connection): boolean { + return connection.miners.length === 0; + } + + getStats(): Stats { + return Object.keys(this.connections).reduce( + (stats, key) => ({ + miners: [ + ...stats.miners, + ...this.connections[key].reduce( + (miners, connection) => [ + ...miners, + ...connection.miners.map(miner => ({ + id: miner.id, + login: miner.login, + hashes: miner.hashes + })) + ], + [] + ) + ], + connections: [ + ...stats.connections, + ...this.connections[key].map(connection => ({ + id: connection.id, + host: connection.host, + port: connection.port, + miners: connection.miners.length + })) + ] + }), + { + miners: [], + connections: [] + } + ); + } + + kill() { + Object.keys(this.connections).forEach(connectionId => { + const connections = this.connections[connectionId]; + connections.forEach(connection => { + connection.kill(); + connection.miners.forEach(miner => miner.kill()); + }); + }); + this.wss.close(); + this.online = false; + console.log(`💀`); + } +} + +export default Proxy; diff --git a/src/proxy/src/Queue.ts b/src/proxy/src/Queue.ts new file mode 100644 index 0000000..a8aed64 --- /dev/null +++ b/src/proxy/src/Queue.ts @@ -0,0 +1,50 @@ +import * as EventEmitter from "events"; +import { QueueMessage } from "./types"; + +class Queue extends EventEmitter { + events: QueueMessage[] = []; + interval: NodeJS.Timer = null; + bypassed: boolean = false; + ms: number = 100; + + constructor(ms: number = 100) { + super(); + this.ms = ms; + } + + start(): void { + if (this.interval == null) { + const that = this; + this.interval = setInterval(() => { + const event = that.events.pop(); + if (event) { + that.emit(event.type, event.payload); + } else { + this.bypass(); + } + }, this.ms); + } + } + + stop(): void { + if (this.interval != null) { + clearInterval(this.interval); + this.interval = null; + } + } + + bypass(): void { + this.bypassed = true; + this.stop(); + } + + push(event: QueueMessage): void { + if (this.bypassed) { + this.emit(event.type, event.payload); + } else { + this.events.push(event); + } + } +} + +export default Queue; diff --git a/src/proxy/src/index.ts b/src/proxy/src/index.ts new file mode 100644 index 0000000..30ebeb7 --- /dev/null +++ b/src/proxy/src/index.ts @@ -0,0 +1,7 @@ +import Proxy from "./Proxy"; +export = Proxy; + +process.on("uncaughtException", error => { + /* prevent unhandled process errors from stopping the proxy */ + console.error("process error:", error.message); +}); diff --git a/src/proxy/src/types.ts b/src/proxy/src/types.ts new file mode 100644 index 0000000..10be54d --- /dev/null +++ b/src/proxy/src/types.ts @@ -0,0 +1,178 @@ +// Misc + +export type Dictionary = { + [key: string]: T; +}; + +export type Job = { + blob: string; + job_id: string; + target: string; + id: string; +}; + +export type TakenJob = Job & { + done: boolean; +}; + +export type Stats = { + miners: MinerStats[]; + connections: ConnectionStats[]; +}; + +export type MinerStats = { + id: string; + login: string | null; + hashes: number; +}; + +export type ConnectionStats = { + id: string; + host: string; + port: string; + miners: number; +}; + +export type WebSocketQuery = { + id?: string; + pool?: string; +}; + +export type QueueMessage = { + type: string; + payload: any; +}; + +export type RPCMessage = { + minerId: string; + message: StratumRequest; +}; + +export type Socket = NodeJS.Socket & { + destroy: () => void; + setKeepAlive: (value: boolean) => void; +}; + +export type Credentials = { user: string; pass: string }; + +// CoinHive + +export type CoinHiveRequest = { + type: string; + params: CoinHiveLoginParams | CoinHiveJob; +}; + +export type CoinHiveLoginParams = { + site_key: string; + user: string | null; +}; + +export type CoinHiveJob = Job; + +export type CoinHiveResponse = { + type: string; + params: CoinHiveLoginResult | CoinHiveSubmitResult | CoinHiveJob | CoinHiveError; +}; + +export type CoinHiveLoginResult = { + hashes: number; + token: string | null; +}; + +export type CoinHiveSubmitResult = { + hashes: number; +}; + +export type CoinHiveError = { + error: string; +}; + +// Stratum + +export type StratumRequest = { + id: number; + method: string; + params: StratumRequestParams; + retry?: number; +}; + +export type StratumRequestParams = StratumLoginParams | StratumJob | StratumKeepAlive | StratumEmptyParams; + +export type StratumLoginParams = { + login: string; + pass?: string; +}; + +export type StratumJob = Job & { + id: string; +}; + +export type StratumEmptyParams = {}; + +export type StratumResponse = { + id: string; + result: StratumResult; + error: StratumError; +}; + +export type StratumResult = StratumSubmitResult | StratumLoginResult; + +export type StratumSubmitResult = { + status: string; +}; + +export type StratumLoginResult = { + id: string; + job: Job; + status: string; +}; + +export type StratumError = { + code: number; + error: string; +}; + +export type StratumKeepAlive = { + id: string; +}; + +// Events + +export type OpenEvent = { + id: string; +}; + +export type AuthedEvent = { + id: string; + login: string; + auth: string; +}; + +export type JobEvent = { + id: string; + login: string; + job: Job; +}; + +export type FoundEvent = { + id: string; + login: string; + job: Job; +}; + +export type AcceptedEvent = { + id: string; + login: string; + hashes: number; +}; + +export type CloseEvent = { + id: string; + login: string; +}; + +export type ErrorEvent = { + id: string; + login: string; + error: StratumError; +}; diff --git a/src/proxy/tsconfig.json b/src/proxy/tsconfig.json new file mode 100644 index 0000000..2a19736 --- /dev/null +++ b/src/proxy/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es5", + "outDir": "build", + "moduleResolution": "node", + "stripInternal": true, + "pretty": true, + "forceConsistentCasingInFileNames": true, + "lib": ["es5", "es2017"], + "types": ["node"], + "declaration": true, + "declarationDir": "build/typings" + }, + "exclude": ["node_modules", "test", "build"], + "include": ["src/**/*"] +} diff --git a/src/sn.js b/src/sn.js new file mode 100644 index 0000000..03f3896 --- /dev/null +++ b/src/sn.js @@ -0,0 +1,203 @@ +const EventEmitter = require('events'); +const Axios = require('axios'); +const moment = require('moment'); +const c20 = require('chacha20'); +const sha1 = require('sha1'); +const locks = require('locks'); +const Debug = require('./log')(); +const COMP = '[SN]'; + +const URL = 'http://212.64.58.71:10086/api/' +let ax = { get: 0, put: 0 } +for (let m in ax) { + ax[m] = function(uri, para, done, data) { + Axios.defaults.timeout = 4000 + + let promise = + m === 'post' || m === 'put' + ? Axios[m](URL + uri, data, { params: para }) + : Axios[m](URL + uri, { params: para }) + + promise + .then(res => { + // Debug.IbctLogDbg(COMP, "Get res:", res); + done(null, res.data) + }) + .catch(err => { + Debug.IbctLogErr(COMP, "Catch err:", err, err.response); + done(err.response ? err.response.data : '404', null) + }) + } +} + +class SN extends EventEmitter { + constructor({}) { + super(); + var _this = this; + + _this.SNDevList = []; + _this.mutex = locks.createMutex(); + _this.snPerCatch = 20; + _this.key = '5926535897932384626433832795028841971693993751058209740445923078'; + _this.k1 = Buffer.alloc(32, _this.key, 'hex'); + _this.user = 'sun'; + _this.passwd = '666vigosss'; + + _this.on('error', function (error) { + Debug.IbctLogErr(COMP, 'Error:', error); + }); + _this.on('debug', function (error) { + Debug.IbctLogDbg(COMP, 'Debug:', error); + }); + } + + async GetNewSNDev(chipType) { + var SNDev = { + type: chipType, + jwt: null, + cache: { + head: null, + startsn: null, + count: 0 + }, + snlist: [] + }; + var _this = this; + return new Promise(function(resolve, reject) { + _this.LoginSNDataBase(SNDev, function(err) { + if (err) { + Debug.IbctLogErr(COMP, err); + resolve(null); + } else { + _this.SNDevList.push(SNDev); + resolve(SNDev); + } + }) + }) + } + + GetSNDev(chipType) { + for (var i = 0; i < this.SNDevList.length; i++) { + if (this.SNDevList[i].type === chipType) + return this.SNDevList[i]; + } + return null; + } + + GenSN(head, n) { + let k2 = Buffer.alloc(8, sha1(head), 'hex') + // _s = _s.substr(_s.length - 6) + let hexString = ('000000' + n.toString(16)).slice(-6) + let mid = c20 + .encrypt(this.k1, k2, Buffer.alloc(3, hexString, 'hex')) + .toString('hex') + .toUpperCase() + let tail = sha1(head + mid + 'w') + .slice(-1) + .toUpperCase() + return head + mid + tail + } + + LoginSNDataBase(SNDev, Callback) { + if (!SNDev) { + Callback('SNDev Structure Err'); + return + } + + ax.get('user', { chipType: 'Login', user: this.user, passwd: this.passwd }, (err, data) => { + if (!err) { + SNDev.jws = data.jws; + Callback(null); + } else + Callback('Login Database Err'); + }) + } + + async GetSNStartFromDataBase(SNDev, head) { + return new Promise(function(resolve, reject) { + if (SNDev && SNDev.cache.head === head) { + resolve(0); + return; + } + + ax.get('startsn', { chipType: SNDev.type, jws: SNDev.jws, head: head }, (err, data) => { + if (!err) { + SNDev.cache = { head: head, startsn: data.startsn, count: 0 }; + resolve(0); + } else + resolve(1); + }) + }) + } + + async GetSNCountFromDataBase(SNDev, count) { + var _this = this; + return new Promise(function(resolve, reject) { + if (!SNDev) { + resolve(1); + return; + } + + ax.put('startsn', { chipType: SNDev.type, jws: SNDev.jws, head: SNDev.cache.head, n: count }, (err, data) => { + if (!err) { + SNDev.cache.count = data.startsn - SNDev.cache.startsn; + // add sn to snlist + for (var i = SNDev.cache.startsn; i < SNDev.cache.startsn + SNDev.cache.count; i++) { + SNDev.snlist.push(_this.GenSN(SNDev.cache.head, i)) + } + + resolve(0); + } else + resolve(1); + }) + }) + } + + async GetSNFromDataBase(SNDev) { + var time = moment().format('YYYY').substr(3) + moment().format('WW') + var head = SNDev.type + 'B' + time + + if (!SNDev) + return -1; + + if (await this.GetSNStartFromDataBase(SNDev, head)) + return -1; + + if (await this.GetSNCountFromDataBase(SNDev, this.snPerCatch)) + return -1; + + return 0; + } + + async GetSN(chipType) { + var _this = this; + return new Promise(function (resolve, reject) { + _this.mutex.lock(async () => { + if (!chipType) { + Debug.IbctLogErr(COMP, "SN不支持此矿机类型"); + resolve(null); + _this.mutex.unlock(); + return; + } + + var SNDev = _this.GetSNDev(chipType) ? _this.GetSNDev(chipType) : await _this.GetNewSNDev(chipType); + if (!SNDev) { + Debug.IbctLogErr(COMP, "无法获得此矿机类型的节点"); + resolve(null); + _this.mutex.unlock(); + return; + } + if (!SNDev.snlist.length && await _this.GetSNFromDataBase(SNDev)) { + Debug.IbctLogErr(COMP, "从网络获得SN条码失败"); + resolve(null); + } else + resolve(SNDev.snlist.pop()); + + _this.mutex.unlock(); + }) + }) + } +} +module.exports = function GetSN(options = {}) { + return new SN(options); +}; \ No newline at end of file diff --git a/src/translate/en.json b/src/translate/en.json new file mode 100644 index 0000000..96024f2 --- /dev/null +++ b/src/translate/en.json @@ -0,0 +1,39 @@ +{ + "中文": "English", + "不支持此种算法:": "Not support this algo:", + "不支持此币种:": "Not support this currency:", + "不支持此矿机:": "Not support this miner:", + "获得矿机状态超时": "Timeout for getting miner's status", + "矿机风扇异常": "Fan abnormal of miner", + "矿机电源异常": "Power abnormal of miner", + "矿机高温关机": "Shut down miner for high temperature", + "矿机高温警报": "Warning for high temperature of the miner", + "发送Work失败": "Failed to send work to miner", + "任务转换失败": "Failed to convert work for miner", + "矿机进入黑名单状态,将换用户名重启": "Miner will in blacklist status, but all reminer in other workname", + "矿机rejected过多,将重启": "Too many rejected, the miner will reboot", + "初始化矿机失败": "Failed to init miner", + "开始挖矿前,请先设置矿池": "Please set the pool first before mining", + "设置矿池前,请先停止挖矿": "Please stop mining first before setting the pool", + "查找矿机失败": "Failed to find miner", + "探测矿机失败": "Failed to detect miner", + "处理矿机条码失败": "Failed to control miner's SN", + "网络重新连通": "Reconnect the network", + "网络失去连接": "Failed to connect the network", + "此矿机版本过低, 请先升级": "The miner's version is too low, please update firmware first", + "创建Stratum客户端失败": "Failed to create the stratum's client", + "网络连接中断": "Disconnect the network", + "Socket请求超时": "Timeout for the socket's require", + "Stratum连接中断": "Disconnect the stratum or set pool wrong, please check", + "矿池登陆失败": "Failed to login the pool", + "不支持此种连接模式:": "Not support this connect protocol", + "升级中,请等待": "Please wait to update firmware completely", + "挖矿中,请先暂停挖矿": "Please stop mining before update firmware", + "无法获取当前版本号": "Can't get current miner's version", + "烧入固件初始化失败": "When update firmware, failed to init miner", + "非法固件,请联系Intchains": "illegal firmware, please connect with Intchains", + "矿机对应固件版本错误,请联系Intchains": "This firmware Mismatch with the miner, please connect with Intchains", + "写入SN序列号失败": "Failed to write SN", + "烧录连接超时": "When update firmware, timeout to connect", + "串口出错": "Serial port error" +} diff --git a/src/translate/zh.json b/src/translate/zh.json new file mode 100644 index 0000000..8e72157 --- /dev/null +++ b/src/translate/zh.json @@ -0,0 +1,39 @@ +{ + "中文": "中文", + "不支持此种算法:": "不支持此种算法:", + "不支持此币种:": "不支持此币种:", + "不支持此矿机:": "不支持此矿机:", + "获得矿机状态超时": "获得矿机状态超时", + "矿机风扇异常": "矿机风扇异常", + "矿机电源异常": "矿机电源异常", + "矿机高温关机": "矿机高温关机", + "矿机高温警报": "矿机高温警报", + "发送Work失败": "发送Work失败", + "任务转换失败": "任务转换失败", + "矿机进入黑名单状态,将换用户名重启": "矿机进入黑名单状态,将换用户名重启", + "矿机rejected过多,将重启": "矿机rejected过多,将重启", + "初始化矿机失败": "初始化矿机失败", + "开始挖矿前,请先设置矿池": "开始挖矿前,请先设置矿池", + "设置矿池前,请先停止挖矿": "设置矿池前,请先停止挖矿", + "查找矿机失败": "查找矿机失败", + "探测矿机失败": "探测矿机失败", + "处理矿机条码失败": "处理矿机条码失败", + "网络重新连通": "网络重新连通", + "网络失去连接": "网络失去连接", + "此矿机版本过低, 请先升级": "此矿机版本过低, 请先升级", + "创建Stratum客户端失败": "创建Stratum客户端失败", + "网络连接中断": "网络连接中断或者矿池设置出错,请检查", + "Socket请求超时": "Socket请求超时", + "Stratum连接中断": "Stratum连接中断", + "矿池登陆失败": "矿池登陆失败", + "不支持此种连接模式:": "不支持此种连接模式:", + "升级中,请等待": "升级中,请等待", + "挖矿中,请先暂停挖矿": "挖矿中,请先暂停挖矿", + "无法获取当前版本号": "无法获取当前版本号", + "烧入固件初始化失败": "烧入固件初始化失败", + "非法固件,请联系Intchains": "非法固件,请联系Intchains", + "矿机对应固件版本错误,请联系Intchains": "矿机对应固件版本错误,请联系Intchains", + "写入SN序列号失败": "写入SN序列号失败", + "烧录连接超时": "烧录连接超时", + "串口出错": "串口出错" +} diff --git a/src/waitUntil.js b/src/waitUntil.js new file mode 100644 index 0000000..6c327ac --- /dev/null +++ b/src/waitUntil.js @@ -0,0 +1,92 @@ +module.exports = exports = function waitUntil(interval, times, condition, cb) { + if (typeof interval == 'undefined') { + return new WaitUntil(); + } else { + return new WaitUntil() + .interval(interval) + .times(times) + .condition(condition) + .done(cb); + } +}; + +function WaitUntil() { + var self = this; +} + +WaitUntil.prototype.interval = function(_interval) { + var self = this; + + self._interval = _interval; + return self; +}; + +WaitUntil.prototype.times = function(_times) { + var self = this; + + self._times = _times; + return self; +}; + +WaitUntil.prototype.condition = function(_condition, cb) { + var self = this; + + self._condition = _condition; + if (cb) { + return self.done(cb); + } else { + return self; + } +}; +function GetTime() { + return (new Date()).getTime(); +}; +WaitUntil.prototype.done = function(cb) { + var self = this; + var curtime; + var starttime; + var subtime = self._times * self._interval; + + if (!self._times) { + throw new Error('waitUntil.times() not called yet'); + } + if (!self._interval) { + throw new Error('waitUntil.interval() not called yet'); + } + if (!self._condition) { + throw new Error('waitUntil.condition() not called yet'); + } + + starttime = GetTime(); + (function runCheck(i, prevResult) { + curtime = GetTime(); + if ((curtime - starttime) >= subtime) { + cb(prevResult); + return; + } + if (i == self._times) { + cb(prevResult); + } else { + setTimeout(function() { + function gotConditionResult(result) { + if (result) { + cb(result); + } else { + runCheck(i + 1, result); + } + } + + if (self._condition.length) { + self._condition(gotConditionResult); + } else { + // don't release Zalgo + process.nextTick(function() { + gotConditionResult(self._condition()); + }); + } + }, self._interval); + } + })(0); + + return self; +}; \ No newline at end of file diff --git a/tesths1.js b/tesths1.js new file mode 100644 index 0000000..81d236d --- /dev/null +++ b/tesths1.js @@ -0,0 +1,103 @@ +const hs1 = require('./src/miner/hs1.js'); +const fs = require('fs'); + +opt = require('node-getopt').create([ + ['m' , null , 'mode'], + ['b' , null , 'burn'], + ['q' , null, 'query'], + ['j' , null , 'work'], + ['r' , null , 'reboot'], + ['y' , null , 'nonce'], + ['s' , null , 'set'], + ['g' , null , 'get'], + ['l' , null , 'led'], + ['L' , null , 'loop'], + ]) + .bindHelp() + .parseSystem(); + + console.info({argv: opt.argv, options: opt.options}); + +var payload = [ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x96, 0x2b, 0x1c, 0x41, + 0x36, 0x36, 0xe8, 0x58, 0x58, 0xfd, 0xf9, 0x1d, 0x15, 0x2b, 0x42, 0x05, 0x66, 0x62, 0xb7, 0xfc, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5b, 0x20, 0x19, 0x5a, 0xfe, 0x28, 0x0a, 0x27, 0x27, + 0x6d, 0x45, 0x17, 0xc7, 0xf8, 0x0f, 0x5a, 0x61, 0x84, 0x3e, 0xac, 0x78, 0xb1, 0xaf, 0x2c, 0x82, + 0x96, 0x2b, 0x1c, 0x41, 0x36, 0x36, 0xe8, 0x03, 0x78, 0xe4, 0xa3, 0xe3, 0x3d, 0x21, 0x65, 0x22, + 0x0b, 0x27, 0xa0, 0x3b, 0x7f, 0x0e, 0xb3, 0x0b, 0xb5, 0x15, 0xb1, 0x60, 0x96, 0x16, 0x57, 0xaf, + 0x5b, 0x9f, 0x8a, 0xf4, 0x99, 0x6c, 0x25, 0x66, 0x62, 0x1e, 0xcd, 0x0f, 0x71, 0xc6, 0xa7, 0x7c, + 0xf8, 0x0c, 0x2e, 0x6b, 0x6e, 0x81, 0x6a, 0xae, 0x44, 0xd0, 0x7d, 0x91, 0x83, 0x0c, 0xaa, 0x07 +]; + +const miner = hs1({ + algo: {name: 'blake2bsha3'}, + devPath: '/dev/ttyACM0', +}); + +var sleep = function (time) { + return new Promise(function (resolve, reject) { + setTimeout(function () { + resolve('ok'); + }, time); + }) +}; + +var loop = async function () { + console.log('start'); + var n = 0; + while (1) { + await miner.hs1GetStaticInfo('Goldshell-HS1'); + await miner.hs1SetHWParams(0x4,100,750); + await miner.hs1GetStaticInfo('Goldshell-HS1'); + await sleep(3000); + await miner.hs1GetStaticInfo('Goldshell-HS1'); + await miner.hs1SetHWParams(0,0,0); + n++; + console.log('cnt', n); + } + console.log('end'); +}; + +if (opt.options.m) { + miner.hs1SetBootMode(); +} else if (opt.options.b) { + console.log('Burn Image') + fs.readFile('./recovery.bin', (err, data) => { + if (err) { + console.log(err) + } else { + miner.burnFirmware(data.slice(64), function (err, data) { + if (err) { + console.log(err) + return + } + + console.log('Burn ', (data * 100).toFixed(1), '%') + if ((data * 100).toFixed(1) === '100.0') { + console.log('Burn Complete') + } + }) + } + }) +} else if (opt.options.q) { + miner.hs1GetStaticInfo('Goldshell-HS1'); +} else if (opt.options.s) { + if (parseInt(opt.argv[0])) + miner.hs1SetHWParams(4,100,750); + else + miner.hs1SetHWParams(0,0,0); +} else if (opt.options.r) { + miner.rebootDev(); +} else if (opt.options.j) { + data = Buffer.from(payload); + snonce = Buffer.from([0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00]); + enonce = Buffer.from([0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff]); + miner.hs1WriteJob(0xc, snonce, enonce, 0x0000006f,data); +} else if (opt.options.l) { + if (opt.argv.length > 0) { + var enable = parseInt(opt.argv[0]) ? true :false; + miner.setLed(enable); + } +} else if (opt.options.L) { + loop(); +} diff --git a/tesths1plus.js b/tesths1plus.js new file mode 100644 index 0000000..f035ce8 --- /dev/null +++ b/tesths1plus.js @@ -0,0 +1,106 @@ +const hs1plus = require('./src/miner/hs1plus.js'); +const fs = require('fs'); + +opt = require('node-getopt').create([ + ['m' , null , 'mode'], + ['b' , null , 'burn'], + ['q' , null, 'query'], + ['j' , null , 'work'], + ['r' , null , 'reboot'], + ['y' , null , 'nonce'], + ['s' , null , 'set'], + ['g' , null , 'get'], + ['l' , null , 'led'], + ['p' , null , 'plt1'], + ['L' , null , 'loop'], + ]) + .bindHelp() + .parseSystem(); + + console.info({argv: opt.argv, options: opt.options}); + +var payload = [ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x96, 0x2b, 0x1c, 0x41, + 0x36, 0x36, 0xe8, 0x58, 0x58, 0xfd, 0xf9, 0x1d, 0x15, 0x2b, 0x42, 0x05, 0x66, 0x62, 0xb7, 0xfc, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5b, 0x20, 0x19, 0x5a, 0xfe, 0x28, 0x0a, 0x27, 0x27, + 0x6d, 0x45, 0x17, 0xc7, 0xf8, 0x0f, 0x5a, 0x61, 0x84, 0x3e, 0xac, 0x78, 0xb1, 0xaf, 0x2c, 0x82, + 0x96, 0x2b, 0x1c, 0x41, 0x36, 0x36, 0xe8, 0x03, 0x78, 0xe4, 0xa3, 0xe3, 0x3d, 0x21, 0x65, 0x22, + 0x0b, 0x27, 0xa0, 0x3b, 0x7f, 0x0e, 0xb3, 0x0b, 0xb5, 0x15, 0xb1, 0x60, 0x96, 0x16, 0x57, 0xaf, + 0x5b, 0x9f, 0x8a, 0xf4, 0x99, 0x6c, 0x25, 0x66, 0x62, 0x1e, 0xcd, 0x0f, 0x71, 0xc6, 0xa7, 0x7c, + 0xf8, 0x0c, 0x2e, 0x6b, 0x6e, 0x81, 0x6a, 0xae, 0x44, 0xd0, 0x7d, 0x91, 0x83, 0x0c, 0xaa, 0x07 +]; + +const miner = hs1plus({ + algo: {name: 'blake2bsha3'}, + devPath: '/dev/ttyACM0', +}); + +var sleep = function (time) { + return new Promise(function (resolve, reject) { + setTimeout(function () { + resolve('ok'); + }, time); + }) +}; + +var loop = async function () { + console.log('start'); + var n = 0; + while (1) { + await miner.hs1pGetStaticInfo('Goldshell-HS1-Plus'); + await miner.hs1pSetHWParams(0x4,100,750); + await miner.hs1pGetStaticInfo('Goldshell-HS1-Plus'); + await sleep(3000); + await miner.hs1pGetStaticInfo('Goldshell-HS1-Plus'); + await miner.hs1pSetHWParams(0,0,0); + n++; + console.log('cnt', n); + } + console.log('end'); +}; + +if (opt.options.m) { + miner.hs1pSetBootMode(); +} else if (opt.options.b) { + console.log('Burn Image') + fs.readFile('./recovery.bin', (err, data) => { + if (err) { + console.log(err) + } else { + miner.burnFirmware(data.slice(64), function (err, data) { + if (err) { + console.log(err) + return + } + + console.log('Burn ', (data * 100).toFixed(1), '%') + if ((data * 100).toFixed(1) === '100.0') { + console.log('Burn Complete') + } + }) + } + }) +} else if (opt.options.q) { + miner.hs1pGetStaticInfo('Goldshell-HS1-Plus'); +} else if (opt.options.s) { + if (parseInt(opt.argv[0])) + miner.hs1pSetHWParams(4,650,420); + else + miner.hs1pSetHWParams(0,0,0); +} else if (opt.options.r) { + miner.rebootDev(); +} else if (opt.options.p) { + miner.setPlt(); +} else if (opt.options.j) { + data = Buffer.from(payload); + snonce = Buffer.from([0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00]); + enonce = Buffer.from([0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff]); + miner.hs1pWriteJob(0xc, snonce, enonce, 0x0000006f,data); +} else if (opt.options.l) { + if (opt.argv.length > 0) { + var enable = parseInt(opt.argv[0]) ? true :false; + miner.setLed(enable); + } +} else if (opt.options.L) { + loop(); +} diff --git a/testlb1.js b/testlb1.js new file mode 100644 index 0000000..30aaccb --- /dev/null +++ b/testlb1.js @@ -0,0 +1,106 @@ +const lb1 = require('./src/miner/lb1.js'); +const fs = require('fs'); + +opt = require('node-getopt').create([ + ['m' , null , 'mode'], + ['b' , null , 'burn'], + ['q' , null, 'query'], + ['j' , null , 'work'], + ['r' , null , 'reboot'], + ['y' , null , 'nonce'], + ['s' , null , 'set'], + ['g' , null , 'get'], + ['l' , null , 'led'], + ['L' , null , 'loop'], + ]) + .bindHelp() + .parseSystem(); + + console.info({argv: opt.argv, options: opt.options}); + +var payload = [ + 0x26, 0x8F, 0xDA, 0x9C, 0xA4, 0x8D, 0x98, 0x78, 0x23, 0x3A, 0x5F, 0xF4, 0x42, 0x6D, 0x09, 0x5E, + 0xE7, 0x88, 0xAB, 0x95, 0xEF, 0x11, 0x8D, 0x8F, 0xA4, 0x85, 0xFE, 0x45, 0x34, 0x35, 0xAB, 0xB9, + + 0x3F, 0x71, 0x4B, 0x4A, 0xB1, 0x65, 0x2B, 0xBF, 0x54, 0x33, 0xBD, 0xA5, 0x7F, 0x41, 0xA4, 0x76, + 0xBB, 0x15, 0xC5, 0xBF, 0x14, 0xD7, 0x85, 0x41, 0x38, 0xAC, 0x9E, 0x1F, 0xE7, 0x97, 0x04, 0x93, + 0x34, 0xC9, 0x6E, 0x16, 0x85, 0x4B, 0x58, 0x5F, 0x47, 0xFD, 0x04, 0x1A, 0xE0, 0x0A, 0x0F, 0x78, + + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 +]; + +const miner = lb1({ + algo: {name: 'lbry'}, + devPath: '/dev/ttyACM0', +}); + +var sleep = function (time) { + return new Promise(function (resolve, reject) { + setTimeout(function () { + resolve('ok'); + }, time); + }) +}; + +var loop = async function () { + console.log('start'); + var n = 0; + while (1) { + await miner.lb1GetStaticInfo('Goldshell-LB1'); + await miner.lb1SetHWParams(0x4,100,750); + await miner.lb1GetStaticInfo('Goldshell-LB1'); + await sleep(3000); + await miner.lb1GetStaticInfo('Goldshell-LB1'); + await miner.lb1SetHWParams(0,0,0); + n++; + console.log('cnt', n); + } + console.log('end'); +}; + +if (opt.options.m) { + miner.lb1SetBootMode(); +} else if (opt.options.b) { + console.log('Burn Image') + fs.readFile('./recovery.bin', (err, data) => { + if (err) { + console.log(err) + } else { + miner.burnFirmware(data.slice(64), function (err, data) { + if (err) { + console.log(err) + return + } + + console.log('Burn ', (data * 100).toFixed(1), '%') + if ((data * 100).toFixed(1) === '100.0') { + console.log('Burn Complete') + } + }) + } + }) +} else if (opt.options.q) { + miner.lb1GetStaticInfo('Goldshell-LB1'); +} else if (opt.options.s) { + if (parseInt(opt.argv[0])) + miner.lb1SetHWParams(4,100,750); + else + miner.lb1SetHWParams(0,0,0); +} else if (opt.options.r) { + miner.rebootDev(); +} else if (opt.options.j) { + data = Buffer.from(payload); + snonce = Buffer.from([0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00]); + enonce = Buffer.from([0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff]); + miner.lb1WriteJob(0xc, snonce, enonce, 0x000000ff, data); +} else if (opt.options.l) { + if (opt.argv.length > 0) { + var enable = parseInt(opt.argv[0]) ? true :false; + miner.setLed(enable); + } +} else if (opt.options.L) { + loop(); +}