├── test └── .gitignore ├── .gitignore ├── .npmignore ├── package.json ├── LICENSE ├── README.md ├── .eslintrc.json └── lib └── stratum.js /test/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | node_modules/ 3 | npm-debug.log 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .babel* 2 | .bmocharc* 3 | .bpkgignore 4 | .editorconfig 5 | .eslint* 6 | .git* 7 | .mocharc* 8 | .yarnignore 9 | bench/ 10 | build/ 11 | docs/ 12 | node_modules/ 13 | npm-debug.log 14 | package-lock.json 15 | test/ 16 | webpack.*.js 17 | yarn.lock 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bstratum", 3 | "version": "0.9.0", 4 | "description": "Bitcoin mining bike-shed", 5 | "keywords": [ 6 | "bcoin", 7 | "bitcoin", 8 | "blockchain", 9 | "mining", 10 | "miner", 11 | "stratum" 12 | ], 13 | "license": "MIT", 14 | "repository": "git://github.com/bcoin-org/bstratum.git", 15 | "homepage": "https://github.com/bcoin-org/bstratum", 16 | "bugs": { 17 | "url": "https://github.com/bcoin-org/bstratum/issues" 18 | }, 19 | "author": "Christopher Jeffrey ", 20 | "main": "./lib/stratum.js", 21 | "scripts": { 22 | "lint": "eslint lib/ || exit 0", 23 | "test": "bmocha --reporter spec test/*-test.js" 24 | }, 25 | "dependencies": { 26 | "bcrypto": "~5.0.0", 27 | "bfile": "~0.2.1", 28 | "binet": "~0.3.5", 29 | "blst": "~0.1.5", 30 | "bmutex": "~0.1.6", 31 | "bsert": "~0.0.10", 32 | "btcp": "~0.1.5", 33 | "buffer-map": "~0.0.7" 34 | }, 35 | "peerDependencies": { 36 | "bcoin": "~1.0.2" 37 | }, 38 | "devDependencies": { 39 | "bmocha": "^2.1.3" 40 | }, 41 | "engines": { 42 | "node": ">=8.0.0" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This software is licensed under the MIT License. 2 | 3 | Copyright (c) 2017, Christopher Jeffrey (https://github.com/chjj) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bstratum 2 | 3 | A segwit-capable stratum server on top of [bcoin][bcoin]. This is a bcoin 4 | plugin which will run a stratum server in the same process as a bcoin fullnode. 5 | 6 | ## Usage 7 | 8 | bstratum can be used as a bcoin plugin. 9 | 10 | ``` bash 11 | $ bcoin --plugins bstratum \ 12 | --stratum-host :: \ 13 | --stratum-port 3008 \ 14 | --stratum-public-host pool.example.com \ 15 | --stratum-public-port 3008 \ 16 | --stratum-max-inbound 1000 \ 17 | --stratum-difficulty 8 \ 18 | --stratum-dynamic \ 19 | --stratum-password=admin-pass 20 | ``` 21 | 22 | ## Cutting out the middleman 23 | 24 | While having a stratum+fullnode marriage violates separation of concerns, it 25 | provides a benefit to large competitive miners: because it sits in the same 26 | process, there is no overhead of hitting/longpolling a JSON-rpc api to submit 27 | or be notified of new blocks. It has direct in-memory access to all of the data 28 | it needs. No getwork or getblocktemplate required. 29 | 30 | It can also broadcast submitted blocks before verifying and saving them to disk 31 | (since we created the block and know it's going to be valid ahead of time). 32 | 33 | ### Single point of failure? 34 | 35 | There's nothing to say you can't have multiple bcoin-nodes/stratum-servers 36 | behind a reverse/failover proxy still. It's only a single point of failure if 37 | you treat it that way. 38 | 39 | ## Payouts 40 | 41 | Shares are currently tracked by username and will be dumped to 42 | `~/.bcoin/stratum/shares/[height]-[hash].json` when a block is found. A script 43 | can parse through these later and either add the user's balance to a webserver 44 | or pay directly to an address. Users are stored in a line-separated json file 45 | in `~/.bcoin/stratum/users.json`. 46 | 47 | ## Administration 48 | 49 | bcoin-stratum exposes some custom stratum calls: 50 | `mining.authorize_admin('password')` to auth as an admin and 51 | `mining.add_user('username', 'password')` to create a user during runtime. 52 | 53 | ## Todo 54 | 55 | - Reverse/failover proxy for `HASH(sid)->bcoin-stratum-ip`. 56 | 57 | ## Contribution and License Agreement 58 | 59 | If you contribute code to this project, you are implicitly allowing your code 60 | to be distributed under the MIT license. You are also implicitly verifying that 61 | all code is your original work. `` 62 | 63 | ## License 64 | 65 | Copyright (c) 2017, Christopher Jeffrey (MIT License). 66 | 67 | See LICENSE for more info. 68 | 69 | --- 70 | 71 | # bstratum 72 | 73 | Bcoin-stratum是一个插件,是[bcoin][bcoin]之上的一个能支持隔离见证(segwit)的stratum server。这是bcoin的一个插件,将可以和bcoin作为一个完整节点在同一个进程中运行stratum。正在开发中。 74 | 75 | ## 用法 76 | 77 | ``` bash 78 | $ bcoin --plugins bcoin-stratum \ 79 | --stratum-host :: \ 80 | --stratum-port 3008 \ 81 | --stratum-public-host pool.example.com \ 82 | --stratum-public-port 3008 \ 83 | --stratum-max-inbound 1000 \ 84 | --stratum-difficulty 8 \ 85 | --stratum-dynamic \ 86 | --stratum-password=admin-pass 87 | ``` 88 | 89 | ## 避开中间环节 90 | 91 | 尽管使用这个“stratum”+“全节点”的联姻违背了关注度分离(的软件设计准则),但它为竞争性的大矿工提供了优势: 92 | 93 | 由于它们在同一个进程中运行,所以在需要提交block和通知新block的时候,能够消除不断访问和轮询JSON-rpc api所带来的开销。 94 | 95 | 它拥有着所有所需数据的直接内存访问能力(权限)。故而不需要getwork或者getblocktemplate。 96 | 97 | 它也可以在将区块验证和将存盘之前广播提交区块(因为我们可以创建区块,并且可以提前知道它是有效的)。 98 | 99 | ### 单点故障? 100 | 101 | 因为无法使用反向/故障转移代理背后来运行多个bcoin-nodes/stratum-servers,所以即使是单个节点出现故障就没什么可说的了,这只是单个节点故障。 102 | 103 | ## 付款(Payouts) 104 | 105 | 在找一个区块后,根据用户名区分的哈希计算结果(Shares)将会存储在:(dumped)~/.bcoin/stratum/shares/[height]-[hash].json。之后用一个脚本来解析,并将用户的余额添加到网络服务器,或者直接支付到一个地址。不同的用户的json是分别分行储存中文件: ~/.bcoin/stratum/users.json。 106 | 107 | ## 管理 108 | 109 | bcoin-stratum暴露了一些自定义层接口调用,如在运行时,通过mining.authorize_admin('password') 可授权一个管理员,通过mining.add_user('username', 'password') 可建立一个用户 110 | 111 | ## 接下来要开发的功能 112 | 113 | - 为HASH(sid)->bcoin-stratum-ip做反向/故障转移代理。 114 | 115 | ## 贡献与许可协议 116 | 117 | 如果您为这个项目贡献代码,就默认你允许你的代码在MIT许可证下分发。你应该保证你的所有代码都是原创工作。 118 | 119 | ## 许可 120 | 121 | 版权所有(C) 2017, Christopher Jeffrey (MIT 许可证)。 122 | 123 | 更多的许可信息请查阅LICENSE。 124 | 125 | [bcoin]: https://github.com/bcoin-org/bcoin 126 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "node": true 5 | }, 6 | "extends": "eslint:recommended", 7 | "globals": { 8 | "Atomics": "readable", 9 | "BigInt": "readable", 10 | "BigInt64Array": "readable", 11 | "BigUint64Array": "readable", 12 | "queueMicrotask": "readable", 13 | "SharedArrayBuffer": "readable", 14 | "TextEncoder": "readable", 15 | "TextDecoder": "readable" 16 | }, 17 | "overrides": [ 18 | { 19 | "files": ["*.mjs"], 20 | "parserOptions": { 21 | "sourceType": "module" 22 | } 23 | }, 24 | { 25 | "files": ["*.cjs"], 26 | "parserOptions": { 27 | "sourceType": "script" 28 | } 29 | }, 30 | { 31 | "files": [ 32 | "test/{,**/}*.{mjs,cjs,js}" 33 | ], 34 | "env": { 35 | "mocha": true 36 | }, 37 | "globals": { 38 | "register": "readable" 39 | }, 40 | "rules": { 41 | "max-len": "off", 42 | "prefer-arrow-callback": "off" 43 | } 44 | } 45 | ], 46 | "parser": "babel-eslint", 47 | "parserOptions": { 48 | "ecmaVersion": 10, 49 | "ecmaFeatures": { 50 | "globalReturn": true 51 | }, 52 | "requireConfigFile": false, 53 | "sourceType": "script" 54 | }, 55 | "root": true, 56 | "rules": { 57 | "array-bracket-spacing": ["error", "never"], 58 | "arrow-parens": ["error", "as-needed", { 59 | "requireForBlockBody": true 60 | }], 61 | "arrow-spacing": "error", 62 | "block-spacing": ["error", "always"], 63 | "brace-style": ["error", "1tbs"], 64 | "camelcase": ["error", { 65 | "properties": "never" 66 | }], 67 | "comma-dangle": ["error", "never"], 68 | "consistent-return": "error", 69 | "eol-last": ["error", "always"], 70 | "eqeqeq": ["error", "always", { 71 | "null": "ignore" 72 | }], 73 | "func-name-matching": "error", 74 | "indent": ["off", 2, { 75 | "ArrayExpression": "off", 76 | "SwitchCase": 1, 77 | "CallExpression": { 78 | "arguments": "off" 79 | }, 80 | "FunctionDeclaration": { 81 | "parameters": "off" 82 | }, 83 | "FunctionExpression": { 84 | "parameters": "off" 85 | }, 86 | "MemberExpression": "off", 87 | "ObjectExpression": "off", 88 | "ImportDeclaration": "off" 89 | }], 90 | "handle-callback-err": "off", 91 | "linebreak-style": ["error", "unix"], 92 | "max-len": ["error", { 93 | "code": 80, 94 | "ignorePattern": "function \\w+\\(", 95 | "ignoreUrls": true 96 | }], 97 | "max-statements-per-line": ["error", { 98 | "max": 1 99 | }], 100 | "new-cap": ["error", { 101 | "newIsCap": true, 102 | "capIsNew": false 103 | }], 104 | "new-parens": "error", 105 | "no-buffer-constructor": "error", 106 | "no-console": "off", 107 | "no-extra-semi": "off", 108 | "no-fallthrough": "off", 109 | "no-func-assign": "off", 110 | "no-implicit-coercion": "error", 111 | "no-multi-assign": "error", 112 | "no-multiple-empty-lines": ["error", { 113 | "max": 1 114 | }], 115 | "no-nested-ternary": "error", 116 | "no-param-reassign": "off", 117 | "no-return-assign": "error", 118 | "no-return-await": "off", 119 | "no-shadow-restricted-names": "error", 120 | "no-tabs": "error", 121 | "no-trailing-spaces": "error", 122 | "no-unused-vars": ["error", { 123 | "vars": "all", 124 | "args": "none", 125 | "ignoreRestSiblings": false 126 | }], 127 | "no-use-before-define": ["error", { 128 | "functions": false, 129 | "classes": false 130 | }], 131 | "no-useless-escape": "off", 132 | "no-var": "error", 133 | "nonblock-statement-body-position": ["error", "below"], 134 | "padded-blocks": ["error", "never"], 135 | "prefer-arrow-callback": "error", 136 | "prefer-const": ["error", { 137 | "destructuring": "all", 138 | "ignoreReadBeforeAssign": true 139 | }], 140 | "prefer-template": "off", 141 | "quotes": ["error", "single"], 142 | "semi": ["error", "always"], 143 | "spaced-comment": ["error", "always", { 144 | "exceptions": ["!"] 145 | }], 146 | "space-before-blocks": "error", 147 | "strict": "error", 148 | "unicode-bom": ["error", "never"], 149 | "valid-jsdoc": "error", 150 | "wrap-iife": ["error", "inside"] 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /lib/stratum.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * stratum.js - stratum server for bcoin 3 | * Copyright (c) 2017, Christopher Jeffrey (MIT License). 4 | * https://github.com/bcoin-org/bcoin 5 | */ 6 | 7 | 'use strict'; 8 | 9 | const assert = require('bsert'); 10 | const path = require('path'); 11 | const os = require('os'); 12 | const {StringDecoder} = require('string_decoder'); 13 | const EventEmitter = require('events'); 14 | const {format} = require('util'); 15 | const {BufferSet} = require('buffer-map'); 16 | const {Lock} = require('bmutex'); 17 | const tcp = require('btcp'); 18 | const IP = require('binet'); 19 | const Logger = require('blgr'); 20 | const fs = require('bfile'); 21 | const List = require('blst'); 22 | const hash256 = require('bcrypto/lib/hash256'); 23 | const {safeEqual} = require('bcrypto/lib/safe'); 24 | const util = require('bcoin/lib/utils/util'); 25 | const consensus = require('bcoin/lib/protocol/consensus'); 26 | const Network = require('bcoin/lib/protocol/network'); 27 | const common = require('bcoin/lib/mining/common'); 28 | 29 | /* 30 | * Constants 31 | */ 32 | 33 | const NONCE_SIZE = 4; 34 | 35 | /** 36 | * Stratum Server 37 | * @extends {EventEmitter} 38 | */ 39 | 40 | class Stratum extends EventEmitter { 41 | /** 42 | * Create a stratum server. 43 | * @constructor 44 | * @param {Object} options 45 | */ 46 | 47 | constructor(options) { 48 | super(); 49 | 50 | this.options = new StratumOptions(options); 51 | 52 | this.node = this.options.node; 53 | this.chain = this.options.chain; 54 | this.network = this.options.network; 55 | this.logger = this.options.logger.context('stratum'); 56 | this.difficulty = this.options.difficulty; 57 | 58 | this.server = tcp.createServer(); 59 | this.sharedb = new ShareDB(this.options); 60 | this.userdb = new UserDB(this.options); 61 | this.locker = new Lock(); 62 | this.jobMap = new Map(); 63 | this.banned = new Map(); 64 | this.jobs = new List(); 65 | this.current = null; 66 | this.inbound = new List(); 67 | this.lastActive = 0; 68 | this.subscribed = false; 69 | this.uid = 0; 70 | this.suid = 0; 71 | 72 | this._init(); 73 | } 74 | 75 | static init(node) { 76 | const config = node.config; 77 | return new Stratum({ 78 | node: node, 79 | prefix: config.prefix, 80 | logger: node.logger, 81 | host: config.str('stratum-host'), 82 | port: config.uint('stratum-port'), 83 | publicHost: config.str('stratum-public-host'), 84 | publicPort: config.uint('stratum-public-port'), 85 | maxInbound: config.uint('stratum-max-inbound'), 86 | difficulty: config.uint('stratum-difficulty'), 87 | dynamic: config.bool('stratum-dynamic'), 88 | password: config.str('stratum-password') 89 | }); 90 | } 91 | 92 | sid() { 93 | const sid = this.suid; 94 | this.suid += 1; 95 | this.suid >>>= 0; 96 | return sid; 97 | } 98 | 99 | jid() { 100 | const now = util.now(); 101 | const id = this.uid; 102 | this.uid += 1; 103 | this.uid >>>= 0; 104 | return `${now}:${id}`; 105 | } 106 | 107 | _init() { 108 | this.server.on('connection', (socket) => { 109 | this.handleSocket(socket); 110 | }); 111 | 112 | this.node.on('connect', async () => { 113 | try { 114 | await this.handleBlock(); 115 | } catch (e) { 116 | this.emit('error', e); 117 | } 118 | }); 119 | 120 | this.node.on('tx', async () => { 121 | try { 122 | await this.handleTX(); 123 | } catch (e) { 124 | this.emit('error', e); 125 | } 126 | }); 127 | } 128 | 129 | async handleBlock() { 130 | const unlock = await this.locker.lock(); 131 | try { 132 | return await this._handleBlock(); 133 | } finally { 134 | unlock(); 135 | } 136 | } 137 | 138 | async _handleBlock() { 139 | const now = util.now(); 140 | 141 | if (!this.subscribed) { 142 | this.lastActive = now; 143 | return; 144 | } 145 | 146 | this.current = null; 147 | this.lastActive = now; 148 | 149 | await this.notifyAll(); 150 | } 151 | 152 | async handleTX() { 153 | const unlock = await this.locker.lock(); 154 | try { 155 | return await this._handleTX(); 156 | } finally { 157 | unlock(); 158 | } 159 | } 160 | 161 | async _handleTX() { 162 | const now = util.now(); 163 | 164 | if (!this.subscribed) { 165 | this.lastActive = now; 166 | return; 167 | } 168 | 169 | if (now > this.lastActive + Stratum.ACTIVE_TIME) { 170 | this.current = null; 171 | this.lastActive = now; 172 | 173 | await this.notifyAll(); 174 | } 175 | } 176 | 177 | async handleSocket(socket) { 178 | if (!socket.remoteAddress) { 179 | this.logger.debug('Ignoring disconnected client.'); 180 | socket.destroy(); 181 | return; 182 | } 183 | 184 | const host = IP.normalize(socket.remoteAddress); 185 | 186 | if (this.inbound.size >= this.options.maxInbound) { 187 | this.logger.debug('Ignoring client: too many inbound (%s).', host); 188 | socket.destroy(); 189 | return; 190 | } 191 | 192 | if (this.isBanned(host)) { 193 | this.logger.debug('Ignoring banned client (%s).', host); 194 | socket.destroy(); 195 | return; 196 | } 197 | 198 | socket.setKeepAlive(true); 199 | socket.setNoDelay(true); 200 | 201 | this.addClient(socket); 202 | } 203 | 204 | addClient(socket) { 205 | const conn = new Connection(this, socket); 206 | 207 | conn.on('error', (err) => { 208 | this.emit('error', err); 209 | }); 210 | 211 | conn.on('close', () => { 212 | assert(this.inbound.remove(conn)); 213 | }); 214 | 215 | conn.on('ban', () => { 216 | this.handleBan(conn); 217 | }); 218 | 219 | this.inbound.push(conn); 220 | } 221 | 222 | handleBan(conn) { 223 | this.logger.warning('Banning client (%s).', conn.id()); 224 | this.banned.set(conn.host, util.now()); 225 | conn.destroy(); 226 | } 227 | 228 | isBanned(host) { 229 | const time = this.banned.get(host); 230 | 231 | if (time == null) 232 | return false; 233 | 234 | if (util.now() - time > Stratum.BAN_TIME) { 235 | this.banned.delete(host); 236 | return false; 237 | } 238 | 239 | return true; 240 | } 241 | 242 | async listen() { 243 | this.server.maxConnections = this.options.maxInbound; 244 | 245 | await this.server.listen(this.options.port, this.options.host); 246 | 247 | this.logger.info('Server listening on %d.', this.options.port); 248 | } 249 | 250 | async open() { 251 | if (this.node.miner.addresses.length === 0) 252 | throw new Error('No addresses available for coinbase.'); 253 | 254 | await this.userdb.open(); 255 | await this.sharedb.open(); 256 | await this.listen(); 257 | 258 | if (this.options.password) { 259 | if (!this.userdb.get('admin')) { 260 | this.userdb.add({ 261 | username: 'admin', 262 | hash: this.options.password 263 | }); 264 | } 265 | } 266 | 267 | this.lastActive = util.now(); 268 | } 269 | 270 | async close() { 271 | let conn, next; 272 | 273 | for (conn = this.inbound.head; conn; conn = next) { 274 | next = conn.next; 275 | conn.destroy(); 276 | } 277 | 278 | await this.server.close(); 279 | await this.userdb.close(); 280 | await this.sharedb.close(); 281 | } 282 | 283 | async notifyAll() { 284 | const job = await this.getJob(); 285 | let conn; 286 | 287 | this.logger.debug('Notifying all clients of new job: %s.', job.id); 288 | 289 | for (conn = this.inbound.head; conn; conn = conn.next) { 290 | if (conn.sid === -1) 291 | continue; 292 | 293 | conn.sendJob(job); 294 | } 295 | } 296 | 297 | createBlock() { 298 | if (this.node.miner.addresses.length === 0) 299 | throw new Error('No addresses available for coinbase.'); 300 | 301 | return this.node.miner.createBlock(); 302 | } 303 | 304 | addJob(job) { 305 | if (this.jobs.size >= Stratum.MAX_JOBS) 306 | this.removeJob(this.jobs.head); 307 | 308 | assert(this.jobs.push(job)); 309 | 310 | assert(!this.jobMap.has(job.id)); 311 | this.jobMap.set(job.id, job); 312 | 313 | this.current = job; 314 | } 315 | 316 | removeJob(job) { 317 | assert(this.jobs.remove(job)); 318 | 319 | assert(this.jobMap.has(job.id)); 320 | this.jobMap.delete(job.id); 321 | 322 | if (job === this.current) 323 | this.current = null; 324 | } 325 | 326 | async getJob() { 327 | if (!this.current) { 328 | const attempt = await this.createBlock(); 329 | const job = Job.fromTemplate(this.jid(), attempt); 330 | 331 | this.addJob(job); 332 | 333 | this.logger.debug( 334 | 'New job (id=%s, prev=%s).', 335 | job.id, util.revHex(job.attempt.prevBlock)); 336 | } 337 | 338 | return this.current; 339 | } 340 | 341 | async tryCommit(entry, block) { 342 | try { 343 | await this.sharedb.commit(entry, block); 344 | } catch (e) { 345 | this.emit('error', e); 346 | } 347 | } 348 | 349 | auth(username, password) { 350 | const user = this.userdb.get(username); 351 | 352 | if (!user) 353 | return false; 354 | 355 | const passwd = Buffer.from(password, 'utf8'); 356 | const hash = hash256.digest(passwd); 357 | 358 | if (!safeEqual(hash, user.password)) 359 | return false; 360 | 361 | return true; 362 | } 363 | 364 | authAdmin(password) { 365 | if (!this.options.password) 366 | return false; 367 | 368 | const data = Buffer.from(password, 'utf8'); 369 | const hash = hash256.digest(data); 370 | 371 | if (!safeEqual(hash, this.options.password)) 372 | return false; 373 | 374 | return true; 375 | } 376 | 377 | async addBlock(conn, block) { 378 | // Broadcast immediately. 379 | this.node.broadcast(block); 380 | 381 | let entry; 382 | try { 383 | entry = await this.chain.add(block); 384 | } catch (e) { 385 | if (e.type === 'VerifyError') { 386 | switch (e.reason) { 387 | case 'high-hash': 388 | return new StratumError(23, 'high-hash'); 389 | case 'duplicate': 390 | return new StratumError(22, 'duplicate'); 391 | } 392 | return new StratumError(20, e.reason); 393 | } 394 | throw e; 395 | } 396 | 397 | if (!entry) 398 | return new StratumError(21, 'stale-prevblk'); 399 | 400 | if (entry.hash !== this.chain.tip.hash) 401 | return new StratumError(21, 'stale-work'); 402 | 403 | this.tryCommit(entry, block); 404 | 405 | this.logger.info('Client found block %s (%d) (%s).', 406 | entry.rhash(), 407 | entry.height, 408 | conn.id()); 409 | 410 | return null; 411 | } 412 | 413 | async handlePacket(conn, msg) { 414 | const unlock = await this.locker.lock(); 415 | try { 416 | return await this._handlePacket(conn, msg); 417 | } finally { 418 | unlock(); 419 | } 420 | } 421 | 422 | async _handlePacket(conn, msg) { 423 | switch (msg.method) { 424 | case 'mining.authorize': 425 | return this.handleAuthorize(conn, msg); 426 | case 'mining.subscribe': 427 | return this.handleSubscribe(conn, msg); 428 | case 'mining.submit': 429 | return this.handleSubmit(conn, msg); 430 | case 'mining.get_transactions': 431 | return this.handleTransactions(conn, msg); 432 | case 'mining.authorize_admin': 433 | return this.handleAuthAdmin(conn, msg); 434 | case 'mining.add_user': 435 | return this.handleAddUser(conn, msg); 436 | default: 437 | return this.handleUnknown(conn, msg); 438 | } 439 | } 440 | 441 | async handleAuthorize(conn, msg) { 442 | if (typeof msg.params.length < 2) { 443 | conn.sendError(msg, 0, 'invalid params'); 444 | return; 445 | } 446 | 447 | const user = msg.params[0]; 448 | const pass = msg.params[1]; 449 | 450 | if (!isUsername(user) || !isPassword(pass)) { 451 | conn.sendError(msg, 0, 'invalid params'); 452 | return; 453 | } 454 | 455 | if (!this.auth(user, pass)) { 456 | this.logger.debug( 457 | 'Client failed auth for user %s (%s).', 458 | user, conn.id()); 459 | conn.sendResponse(msg, false); 460 | return; 461 | } 462 | 463 | this.logger.debug( 464 | 'Client successfully authd for %s (%s).', 465 | user, conn.id()); 466 | 467 | conn.addUser(user); 468 | conn.sendResponse(msg, true); 469 | } 470 | 471 | async handleSubscribe(conn, msg) { 472 | if (!this.chain.synced) { 473 | conn.sendError(msg, 0, 'not up to date'); 474 | return; 475 | } 476 | 477 | if (!conn.agent && msg.params.length > 0) { 478 | if (!isAgent(msg.params[0])) { 479 | conn.sendError(msg, 0, 'invalid params'); 480 | return; 481 | } 482 | conn.agent = msg.params[0]; 483 | } 484 | 485 | if (msg.params.length > 1) { 486 | if (!isSID(msg.params[1])) { 487 | conn.sendError(msg, 0, 'invalid params'); 488 | return; 489 | } 490 | conn.sid = this.sid(); 491 | } else { 492 | conn.sid = this.sid(); 493 | } 494 | 495 | if (!this.subscribed) { 496 | this.logger.debug('First subscriber (%s).', conn.id()); 497 | this.subscribed = true; 498 | } 499 | 500 | const sid = hex32(conn.sid); 501 | const job = await this.getJob(); 502 | 503 | this.logger.debug( 504 | 'Client is subscribing with sid=%s (%s).', 505 | sid, conn.id()); 506 | 507 | conn.sendResponse(msg, [ 508 | [ 509 | ['mining.notify', sid], 510 | ['mining.set_difficulty', sid] 511 | ], 512 | sid, 513 | NONCE_SIZE 514 | ]); 515 | 516 | conn.setDifficulty(this.difficulty); 517 | conn.sendJob(job); 518 | } 519 | 520 | async handleSubmit(conn, msg) { 521 | const now = this.network.now(); 522 | 523 | let subm; 524 | try { 525 | subm = Submission.fromPacket(msg); 526 | } catch (e) { 527 | conn.sendError(msg, 0, 'invalid params'); 528 | return; 529 | } 530 | 531 | this.logger.spam( 532 | 'Client submitted job %s (%s).', 533 | subm.job, conn.id()); 534 | 535 | if (!conn.hasUser(subm.username)) { 536 | conn.sendError(msg, 24, 'unauthorized user'); 537 | return; 538 | } 539 | 540 | if (conn.sid === -1) { 541 | conn.sendError(msg, 25, 'not subscribed'); 542 | return; 543 | } 544 | 545 | const job = this.jobMap.get(subm.job); 546 | 547 | if (!job || job.committed) { 548 | conn.sendError(msg, 21, 'job not found'); 549 | return; 550 | } 551 | 552 | if (job !== this.current) { 553 | this.logger.warning( 554 | 'Client is submitting a stale job %s (%s).', 555 | job.id, conn.id()); 556 | } 557 | 558 | // Non-consensus sanity check. 559 | // 2 hours should be less than MTP in 99% of cases. 560 | if (subm.ts < now - 7200) { 561 | conn.sendError(msg, 20, 'time too old'); 562 | return; 563 | } 564 | 565 | if (subm.ts > now + 7200) { 566 | conn.sendError(msg, 20, 'time too new'); 567 | return; 568 | } 569 | 570 | const share = job.check(conn.sid, subm); 571 | const difficulty = share.getDifficulty(); 572 | 573 | if (difficulty < conn.difficulty - 1) { 574 | this.logger.debug( 575 | 'Client submitted a low share of %d, hash=%s, ban=%d (%s).', 576 | difficulty, share.rhash(), conn.banScore, conn.id()); 577 | 578 | conn.increaseBan(1); 579 | conn.sendError(msg, 23, 'high-hash'); 580 | conn.sendDifficulty(conn.difficulty); 581 | 582 | return; 583 | } 584 | 585 | if (!job.insert(share.hash)) { 586 | this.logger.debug( 587 | 'Client submitted a duplicate share: %s (%s).', 588 | share.rhash(), conn.id()); 589 | conn.increaseBan(10); 590 | conn.sendError(msg, 22, 'duplicate'); 591 | return; 592 | } 593 | 594 | this.sharedb.add(subm.username, difficulty); 595 | 596 | this.logger.debug( 597 | 'Client submitted share of %d, hash=%s (%s).', 598 | difficulty, share.rhash(), conn.id()); 599 | 600 | let error; 601 | if (share.verify(job.target)) { 602 | const block = job.commit(share); 603 | error = await this.addBlock(conn, block); 604 | } 605 | 606 | if (error) { 607 | this.logger.warning( 608 | 'Client found an invalid block: %s (%s).', 609 | error.reason, conn.id()); 610 | conn.sendError(msg, error.code, error.reason); 611 | } else { 612 | conn.sendResponse(msg, true); 613 | } 614 | 615 | if (this.options.dynamic) { 616 | if (conn.retarget(job.difficulty)) { 617 | this.logger.debug( 618 | 'Retargeted client to %d (%s).', 619 | conn.nextDifficulty, conn.id()); 620 | } 621 | } 622 | } 623 | 624 | async handleTransactions(conn, msg) { 625 | if (conn.sid === -1) { 626 | conn.sendError(msg, 25, 'not subscribed'); 627 | return; 628 | } 629 | 630 | if (msg.params.length < 1) { 631 | conn.sendError(msg, 21, 'job not found'); 632 | return; 633 | } 634 | 635 | const id = msg.params[0]; 636 | 637 | if (!isJob(id)) { 638 | conn.sendError(msg, 21, 'job not found'); 639 | return; 640 | } 641 | 642 | const job = this.jobMap.get(id); 643 | 644 | if (!job || job.committed) { 645 | conn.sendError(msg, 21, 'job not found'); 646 | return; 647 | } 648 | 649 | this.logger.debug( 650 | 'Sending tx list (%s).', 651 | conn.id()); 652 | 653 | const attempt = job.attempt; 654 | const result = []; 655 | 656 | for (const item of attempt.items) 657 | result.push(item.tx.hash().toString('hex')); 658 | 659 | conn.sendResponse(msg, result); 660 | } 661 | 662 | async handleAuthAdmin(conn, msg) { 663 | if (typeof msg.params.length < 1) { 664 | conn.sendError(msg, 0, 'invalid params'); 665 | return; 666 | } 667 | 668 | const password = msg.params[0]; 669 | 670 | if (!isPassword(password)) { 671 | conn.sendError(msg, 0, 'invalid params'); 672 | return; 673 | } 674 | 675 | if (!this.authAdmin(password)) { 676 | this.logger.debug( 677 | 'Client sent bad admin password (%s).', 678 | conn.id()); 679 | conn.increaseBan(10); 680 | conn.sendError(msg, 0, 'invalid password'); 681 | return; 682 | } 683 | 684 | conn.admin = true; 685 | conn.sendResponse(msg, true); 686 | } 687 | 688 | async handleAddUser(conn, msg) { 689 | if (typeof msg.params.length < 3) { 690 | conn.sendError(msg, 0, 'invalid params'); 691 | return; 692 | } 693 | 694 | const user = msg.params[0]; 695 | const pass = msg.params[1]; 696 | 697 | if (!isUsername(user) || !isPassword(pass)) { 698 | conn.sendError(msg, 0, 'invalid params'); 699 | return; 700 | } 701 | 702 | if (!conn.admin) { 703 | this.logger.debug( 704 | 'Client is not an admin (%s).', 705 | conn.id()); 706 | conn.sendError(msg, 0, 'invalid password'); 707 | return; 708 | } 709 | 710 | try { 711 | this.userdb.add({ 712 | username: user, 713 | password: pass 714 | }); 715 | } catch (e) { 716 | conn.sendError(msg, 0, e.message); 717 | return; 718 | } 719 | 720 | conn.sendResponse(msg, true); 721 | } 722 | 723 | async handleUnknown(conn, msg) { 724 | this.logger.debug( 725 | 'Client sent an unknown message (%s):', 726 | conn.id()); 727 | 728 | this.logger.debug(msg); 729 | 730 | conn.send({ 731 | id: msg.id, 732 | result: null, 733 | error: true 734 | }); 735 | } 736 | } 737 | 738 | Stratum.id = 'stratum'; 739 | 740 | Stratum.ACTIVE_TIME = 60; 741 | Stratum.MAX_JOBS = 6; 742 | Stratum.SHARES_PER_MINUTE = 8; 743 | Stratum.BAN_SCORE = 1000; 744 | Stratum.BAN_TIME = 10 * 60; 745 | 746 | /** 747 | * Stratum Options 748 | */ 749 | 750 | class StratumOptions { 751 | /** 752 | * Create stratum options. 753 | * @constructor 754 | * @param {Object} options 755 | */ 756 | 757 | constructor(options) { 758 | this.node = null; 759 | this.chain = null; 760 | this.logger = Logger.global; 761 | this.network = Network.primary; 762 | this.host = '0.0.0.0'; 763 | this.port = 3008; 764 | this.publicHost = '127.0.0.1'; 765 | this.publicPort = 3008; 766 | this.maxInbound = 50; 767 | this.difficulty = 1000; 768 | this.dynamic = true; 769 | this.prefix = path.resolve(os.homedir(), '.bcoin', 'stratum'); 770 | this.password = null; 771 | 772 | this.fromOptions(options); 773 | } 774 | 775 | fromOptions(options) { 776 | assert(options, 'Options are required.'); 777 | assert(options.node && typeof options.node === 'object', 778 | 'Node is required.'); 779 | 780 | this.node = options.node; 781 | this.chain = this.node.chain; 782 | this.network = this.node.network; 783 | this.logger = this.node.logger; 784 | this.prefix = this.node.location('stratum'); 785 | 786 | if (options.host != null) { 787 | assert(typeof options.host === 'string'); 788 | this.host = options.host; 789 | } 790 | 791 | if (options.port != null) { 792 | assert(typeof options.port === 'number'); 793 | this.port = options.port; 794 | } 795 | 796 | if (options.publicHost != null) { 797 | assert(typeof options.publicHost === 'string'); 798 | this.publicHost = options.publicHost; 799 | } 800 | 801 | if (options.publicPort != null) { 802 | assert(typeof options.publicPort === 'number'); 803 | this.publicPort = options.publicPort; 804 | } 805 | 806 | if (options.maxInbound != null) { 807 | assert(typeof options.maxInbound === 'number'); 808 | this.maxInbound = options.maxInbound; 809 | } 810 | 811 | if (options.difficulty != null) { 812 | assert(typeof options.difficulty === 'number'); 813 | this.difficulty = options.difficulty; 814 | } 815 | 816 | if (options.dynamic != null) { 817 | assert(typeof options.dynamic === 'boolean'); 818 | this.dynamic = options.dynamic; 819 | } 820 | 821 | if (options.password != null) { 822 | assert(isPassword(options.password)); 823 | this.password = hash256.digest(Buffer.from(options.password, 'utf8')); 824 | } 825 | 826 | return this; 827 | } 828 | 829 | static fromOptions(options) { 830 | return new this().fromOptions(options); 831 | } 832 | } 833 | 834 | /** 835 | * Stratum Connection 836 | */ 837 | 838 | class Connection extends EventEmitter { 839 | /** 840 | * Create a stratum connection. 841 | * @constructor 842 | * @param {Stratum} stratum 843 | * @param {net.Socket} socket 844 | */ 845 | 846 | constructor(stratum, socket) { 847 | super(); 848 | 849 | this.locker = new Lock(); 850 | this.stratum = stratum; 851 | this.logger = stratum.logger; 852 | this.socket = socket; 853 | this.host = IP.normalize(socket.remoteAddress); 854 | this.port = socket.remotePort; 855 | this.hostname = IP.toHostname(this.host, this.port); 856 | this.decoder = new StringDecoder('utf8'); 857 | this.agent = ''; 858 | this.recv = ''; 859 | this.admin = false; 860 | this.users = new Set(); 861 | this.sid = -1; 862 | this.difficulty = -1; 863 | this.nextDifficulty = -1; 864 | this.banScore = 0; 865 | this.lastBan = 0; 866 | this.drainSize = 0; 867 | this.destroyed = false; 868 | this.lastRetarget = -1; 869 | this.submissions = 0; 870 | this.prev = null; 871 | this.next = null; 872 | 873 | this._init(); 874 | } 875 | 876 | _init() { 877 | this.on('packet', async (msg) => { 878 | try { 879 | await this.readPacket(msg); 880 | } catch (e) { 881 | this.error(e); 882 | } 883 | }); 884 | 885 | this.socket.on('data', (data) => { 886 | this.feed(data); 887 | }); 888 | 889 | this.socket.on('error', (err) => { 890 | this.emit('error', err); 891 | }); 892 | 893 | this.socket.on('close', () => { 894 | this.error('Socket hangup.'); 895 | this.destroy(); 896 | }); 897 | 898 | this.socket.on('drain', () => { 899 | this.drainSize = 0; 900 | }); 901 | } 902 | 903 | destroy() { 904 | if (this.destroyed) 905 | return; 906 | 907 | this.destroyed = true; 908 | 909 | this.locker.destroy(); 910 | this.socket.destroy(); 911 | this.socket = null; 912 | 913 | this.emit('close'); 914 | } 915 | 916 | send(json) { 917 | if (this.destroyed) 918 | return; 919 | 920 | json = JSON.stringify(json); 921 | json += '\n'; 922 | 923 | this.write(json); 924 | } 925 | 926 | write(text) { 927 | if (this.destroyed) 928 | return; 929 | 930 | if (this.socket.write(text, 'utf8') === false) { 931 | this.drainSize += Buffer.byteLength(text, 'utf8'); 932 | if (this.drainSize > (5 << 20)) { 933 | this.logger.warning( 934 | 'Client is not reading (%s).', 935 | this.id()); 936 | this.destroy(); 937 | } 938 | } 939 | } 940 | 941 | error(err) { 942 | if (this.destroyed) 943 | return; 944 | 945 | if (err instanceof Error) { 946 | err.message += ` (${this.id()})`; 947 | this.emit('error', err); 948 | return; 949 | } 950 | 951 | let msg = format.apply(null, arguments); 952 | 953 | msg += ` (${this.id()})`; 954 | 955 | this.emit('error', new Error(msg)); 956 | } 957 | 958 | redirect() { 959 | const host = this.stratum.options.publicHost; 960 | const port = this.stratum.options.publicPort; 961 | 962 | const res = [ 963 | 'HTTP/1.1 200 OK', 964 | `X-Stratum: stratum+tcp://${host}:${port}`, 965 | 'Connection: Close', 966 | 'Content-Type: application/json; charset=utf-8', 967 | 'Content-Length: 38', 968 | '', 969 | '', 970 | '{"error":null,"result":false,"id":0}' 971 | ]; 972 | 973 | this.write(res.join('\r\n')); 974 | 975 | this.logger.debug('Redirecting client (%s).', this.id()); 976 | 977 | this.destroy(); 978 | } 979 | 980 | feed(data) { 981 | this.recv += this.decoder.write(data); 982 | 983 | if (this.recv.length >= 100000) { 984 | this.error('Too much data buffered (%s).', this.id()); 985 | this.destroy(); 986 | return; 987 | } 988 | 989 | if (/HTTP\/1\.1/i.test(this.recv)) { 990 | this.redirect(); 991 | return; 992 | } 993 | 994 | const lines = this.recv.replace(/\r+/g, '').split(/\n+/); 995 | 996 | this.recv = lines.pop(); 997 | 998 | for (const line of lines) { 999 | if (line.length === 0) 1000 | continue; 1001 | 1002 | let msg; 1003 | try { 1004 | msg = ClientPacket.fromRaw(line); 1005 | } catch (e) { 1006 | this.error(e); 1007 | continue; 1008 | } 1009 | 1010 | this.emit('packet', msg); 1011 | } 1012 | } 1013 | 1014 | async readPacket(msg) { 1015 | const unlock = await this.locker.lock(); 1016 | try { 1017 | this.socket.pause(); 1018 | await this.handlePacket(msg); 1019 | } finally { 1020 | if (!this.destroyed) 1021 | this.socket.resume(); 1022 | unlock(); 1023 | } 1024 | } 1025 | 1026 | async handlePacket(msg) { 1027 | return await this.stratum.handlePacket(this, msg); 1028 | } 1029 | 1030 | addUser(username) { 1031 | if (this.users.has(username)) 1032 | return false; 1033 | 1034 | this.users.add(username); 1035 | 1036 | return true; 1037 | } 1038 | 1039 | hasUser(username) { 1040 | return this.users.has(username); 1041 | } 1042 | 1043 | increaseBan(score) { 1044 | const now = Date.now(); 1045 | 1046 | this.banScore *= Math.pow(1 - 1 / 60000, now - this.lastBan); 1047 | this.banScore += score; 1048 | this.lastBan = now; 1049 | 1050 | if (this.banScore >= Stratum.BAN_SCORE) { 1051 | this.logger.debug( 1052 | 'Ban score exceeds threshold %d (%s).', 1053 | this.banScore, this.id()); 1054 | this.ban(); 1055 | } 1056 | } 1057 | 1058 | ban() { 1059 | this.emit('ban'); 1060 | } 1061 | 1062 | sendError(msg, code, reason) { 1063 | this.logger.spam( 1064 | 'Sending error %s (%s).', 1065 | reason, this.id()); 1066 | 1067 | this.send({ 1068 | id: msg.id, 1069 | result: null, 1070 | error: [code, reason, false] 1071 | }); 1072 | } 1073 | 1074 | sendResponse(msg, result) { 1075 | this.logger.spam( 1076 | 'Sending response %s (%s).', 1077 | msg.id, this.id()); 1078 | 1079 | this.send({ 1080 | id: msg.id, 1081 | result: result, 1082 | error: null 1083 | }); 1084 | } 1085 | 1086 | sendMethod(method, params) { 1087 | this.logger.spam( 1088 | 'Sending method %s (%s).', 1089 | method, this.id()); 1090 | 1091 | this.send({ 1092 | id: null, 1093 | method: method, 1094 | params: params 1095 | }); 1096 | } 1097 | 1098 | sendDifficulty(difficulty) { 1099 | assert(difficulty > 0, 'Difficulty must be at least 1.'); 1100 | 1101 | this.logger.debug( 1102 | 'Setting difficulty=%d for client (%s).', 1103 | difficulty, this.id()); 1104 | 1105 | this.sendMethod('mining.set_difficulty', [difficulty]); 1106 | } 1107 | 1108 | setDifficulty(difficulty) { 1109 | this.nextDifficulty = difficulty; 1110 | } 1111 | 1112 | sendJob(job) { 1113 | this.logger.debug( 1114 | 'Sending job %s to client (%s).', 1115 | job.id, this.id()); 1116 | 1117 | if (this.nextDifficulty !== -1) { 1118 | this.submissions = 0; 1119 | this.lastRetarget = Date.now(); 1120 | this.sendDifficulty(this.nextDifficulty); 1121 | this.difficulty = this.nextDifficulty; 1122 | this.nextDifficulty = -1; 1123 | } 1124 | 1125 | this.sendMethod('mining.notify', job.toJSON()); 1126 | } 1127 | 1128 | retarget(max) { 1129 | const now = Date.now(); 1130 | const pm = Stratum.SHARES_PER_MINUTE; 1131 | 1132 | assert(this.difficulty > 0); 1133 | assert(this.lastRetarget !== -1); 1134 | 1135 | this.submissions += 1; 1136 | 1137 | if (this.submissions % pm === 0) { 1138 | const target = (this.submissions / pm) * 60000; 1139 | let actual = now - this.lastRetarget; 1140 | let difficulty = 0x100000000 / this.difficulty; 1141 | 1142 | if (max > (-1 >>> 0)) 1143 | max = -1 >>> 0; 1144 | 1145 | if (Math.abs(target - actual) <= 5000) 1146 | return false; 1147 | 1148 | if (actual < target / 4) 1149 | actual = target / 4; 1150 | 1151 | if (actual > target * 4) 1152 | actual = target * 4; 1153 | 1154 | difficulty *= actual; 1155 | difficulty /= target; 1156 | difficulty = 0x100000000 / difficulty; 1157 | difficulty >>>= 0; 1158 | difficulty = Math.min(max, difficulty); 1159 | difficulty = Math.max(1, difficulty); 1160 | 1161 | this.setDifficulty(difficulty); 1162 | 1163 | return true; 1164 | } 1165 | 1166 | return false; 1167 | } 1168 | 1169 | id() { 1170 | let id = this.host; 1171 | 1172 | if (this.agent) 1173 | id += '/' + this.agent; 1174 | 1175 | return id; 1176 | } 1177 | } 1178 | 1179 | /** 1180 | * User 1181 | */ 1182 | 1183 | class User { 1184 | /** 1185 | * Create a user. 1186 | * @constructor 1187 | * @param {Object} options 1188 | */ 1189 | 1190 | constructor(options) { 1191 | this.username = ''; 1192 | this.password = consensus.ZERO_HASH; 1193 | 1194 | if (options) 1195 | this.fromOptions(options); 1196 | } 1197 | 1198 | fromOptions(options) { 1199 | assert(options, 'Options required.'); 1200 | assert(isUsername(options.username), 'Username required.'); 1201 | assert(options.hash || options.password, 'Password required.'); 1202 | 1203 | this.setUsername(options.username); 1204 | 1205 | if (options.hash != null) 1206 | this.setHash(options.hash); 1207 | 1208 | if (options.password != null) 1209 | this.setPassword(options.password); 1210 | 1211 | return this; 1212 | } 1213 | 1214 | static fromOptions(options) { 1215 | return new this().fromOptions(options); 1216 | } 1217 | 1218 | setUsername(username) { 1219 | assert(isUsername(username), 'Username must be a string.'); 1220 | this.username = username; 1221 | } 1222 | 1223 | setHash(hash) { 1224 | if (typeof hash === 'string') { 1225 | assert(isHex(hash), 'Hash must be a hex string.'); 1226 | assert(hash.length === 64, 'Hash must be 32 bytes.'); 1227 | this.password = Buffer.from(hash, 'hex'); 1228 | } else { 1229 | assert(Buffer.isBuffer(hash), 'Hash must be a buffer.'); 1230 | assert(hash.length === 32, 'Hash must be 32 bytes.'); 1231 | this.password = hash; 1232 | } 1233 | } 1234 | 1235 | setPassword(password) { 1236 | assert(isPassword(password), 'Password must be a string.'); 1237 | password = Buffer.from(password, 'utf8'); 1238 | this.password = hash256.digest(password); 1239 | } 1240 | 1241 | toJSON() { 1242 | return { 1243 | username: this.username, 1244 | password: this.password.toString('hex') 1245 | }; 1246 | } 1247 | 1248 | fromJSON(json) { 1249 | assert(json); 1250 | assert(typeof json.username === 'string'); 1251 | this.username = json.username; 1252 | this.setHash(json.password); 1253 | return this; 1254 | } 1255 | 1256 | static fromJSON(json) { 1257 | return new this().fromJSON(json); 1258 | } 1259 | } 1260 | 1261 | /** 1262 | * ClientPacket 1263 | */ 1264 | 1265 | class ClientPacket { 1266 | /** 1267 | * Create a packet. 1268 | */ 1269 | 1270 | constructor() { 1271 | this.id = null; 1272 | this.method = 'unknown'; 1273 | this.params = []; 1274 | } 1275 | 1276 | static fromRaw(json) { 1277 | const packet = new ClientPacket(); 1278 | const msg = JSON.parse(json); 1279 | 1280 | if (msg.id != null) { 1281 | assert(typeof msg.id === 'string' 1282 | || typeof msg.id === 'number'); 1283 | packet.id = msg.id; 1284 | } 1285 | 1286 | assert(typeof msg.method === 'string'); 1287 | assert(msg.method.length <= 50); 1288 | packet.method = msg.method; 1289 | 1290 | if (msg.params) { 1291 | assert(Array.isArray(msg.params)); 1292 | packet.params = msg.params; 1293 | } 1294 | 1295 | return packet; 1296 | } 1297 | } 1298 | 1299 | /** 1300 | * Submission Packet 1301 | */ 1302 | 1303 | class Submission { 1304 | /** 1305 | * Create a submission packet. 1306 | */ 1307 | 1308 | constructor() { 1309 | this.username = ''; 1310 | this.job = ''; 1311 | this.nonce2 = 0; 1312 | this.ts = 0; 1313 | this.nonce = 0; 1314 | } 1315 | 1316 | static fromPacket(msg) { 1317 | const subm = new Submission(); 1318 | 1319 | assert(msg.params.length >= 5, 'Invalid parameters.'); 1320 | 1321 | assert(isUsername(msg.params[0]), 'Name must be a string.'); 1322 | assert(isJob(msg.params[1]), 'Job ID must be a string.'); 1323 | 1324 | assert(typeof msg.params[2] === 'string', 'Nonce2 must be a string.'); 1325 | assert(msg.params[2].length === NONCE_SIZE * 2, 'Nonce2 must be a string.'); 1326 | assert(isHex(msg.params[2]), 'Nonce2 must be a string.'); 1327 | 1328 | assert(typeof msg.params[3] === 'string', 'Time must be a string.'); 1329 | assert(msg.params[3].length === 8, 'Time must be a string.'); 1330 | assert(isHex(msg.params[3]), 'Time must be a string.'); 1331 | 1332 | assert(typeof msg.params[4] === 'string', 'Nonce must be a string.'); 1333 | assert(msg.params[4].length === 8, 'Nonce must be a string.'); 1334 | assert(isHex(msg.params[4]), 'Nonce must be a string.'); 1335 | 1336 | subm.username = msg.params[0]; 1337 | subm.job = msg.params[1]; 1338 | subm.nonce2 = parseInt(msg.params[2], 16); 1339 | subm.ts = parseInt(msg.params[3], 16); 1340 | subm.nonce = parseInt(msg.params[4], 16); 1341 | 1342 | return subm; 1343 | } 1344 | } 1345 | 1346 | /** 1347 | * Job 1348 | */ 1349 | 1350 | class Job { 1351 | /** 1352 | * Create a job. 1353 | * @constructor 1354 | */ 1355 | 1356 | constructor(id) { 1357 | assert(typeof id === 'string'); 1358 | 1359 | this.id = id; 1360 | this.attempt = null; 1361 | this.target = consensus.ZERO_HASH; 1362 | this.difficulty = 0; 1363 | this.submissions = new BufferSet(); 1364 | this.committed = false; 1365 | this.prev = null; 1366 | this.next = null; 1367 | } 1368 | 1369 | fromTemplate(attempt) { 1370 | this.attempt = attempt; 1371 | this.attempt.refresh(); 1372 | this.target = attempt.target; 1373 | this.difficulty = attempt.getDifficulty(); 1374 | return this; 1375 | } 1376 | 1377 | static fromTemplate(id, attempt) { 1378 | return new this(id).fromTemplate(attempt); 1379 | } 1380 | 1381 | insert(hash) { 1382 | if (this.submissions.has(hash)) 1383 | return false; 1384 | 1385 | this.submissions.add(hash); 1386 | 1387 | return true; 1388 | } 1389 | 1390 | check(nonce1, subm) { 1391 | const nonce2 = subm.nonce2; 1392 | const ts = subm.ts; 1393 | const nonce = subm.nonce; 1394 | return this.attempt.getProof(nonce1, nonce2, ts, nonce); 1395 | } 1396 | 1397 | commit(share) { 1398 | assert(!this.committed, 'Already committed.'); 1399 | this.committed = true; 1400 | return this.attempt.commit(share); 1401 | } 1402 | 1403 | toJSON() { 1404 | return [ 1405 | this.id, 1406 | common.swap32(this.attempt.prevBlock).toString('hex'), 1407 | this.attempt.left.toString('hex'), 1408 | this.attempt.right.toString('hex'), 1409 | this.attempt.tree.toJSON(), 1410 | hex32(this.attempt.version), 1411 | hex32(this.attempt.bits), 1412 | hex32(this.attempt.ts), 1413 | false 1414 | ]; 1415 | } 1416 | } 1417 | 1418 | /** 1419 | * Stratum Error 1420 | */ 1421 | 1422 | class StratumError { 1423 | /** 1424 | * Create a stratum error. 1425 | * @constructor 1426 | * @param {Number} code 1427 | * @param {String} reason 1428 | */ 1429 | 1430 | constructor(code, reason) { 1431 | this.code = code; 1432 | this.reason = reason; 1433 | } 1434 | } 1435 | 1436 | /** 1437 | * Share DB 1438 | */ 1439 | 1440 | class ShareDB { 1441 | /** 1442 | * Create a Share DB 1443 | * @constructor 1444 | * @param {Object} options 1445 | */ 1446 | 1447 | constructor(options) { 1448 | this.network = options.network; 1449 | this.logger = options.logger; 1450 | this.location = path.resolve(options.prefix, 'shares'); 1451 | 1452 | this.map = Object.create(null); 1453 | this.total = 0; 1454 | this.size = 0; 1455 | } 1456 | 1457 | async open() { 1458 | await fs.mkdirp(this.location); 1459 | } 1460 | 1461 | async close() { 1462 | ; 1463 | } 1464 | 1465 | file(entry) { 1466 | const name = entry.height + '-' + entry.rhash(); 1467 | return path.resolve(this.location, name + '.json'); 1468 | } 1469 | 1470 | add(username, difficulty) { 1471 | if (!this.map[username]) { 1472 | this.map[username] = 0; 1473 | this.size++; 1474 | } 1475 | 1476 | this.map[username] += difficulty; 1477 | this.total += difficulty; 1478 | } 1479 | 1480 | clear() { 1481 | this.map = Object.create(null); 1482 | this.size = 0; 1483 | this.total = 0; 1484 | } 1485 | 1486 | async commit(entry, block) { 1487 | const cb = block.txs[0]; 1488 | const addr = cb.outputs[0].getAddress(); 1489 | 1490 | assert(addr); 1491 | 1492 | const data = { 1493 | network: this.network.type, 1494 | height: entry.height, 1495 | block: block.rhash(), 1496 | ts: block.ts, 1497 | time: util.now(), 1498 | txid: cb.txid(), 1499 | address: addr.toBase58(this.network), 1500 | reward: cb.getOutputValue(), 1501 | size: this.size, 1502 | total: this.total, 1503 | shares: this.map 1504 | }; 1505 | 1506 | this.clear(); 1507 | 1508 | const file = this.file(entry); 1509 | const json = JSON.stringify(data, null, 2); 1510 | 1511 | this.logger.info( 1512 | 'Committing %d payouts to disk for block %d (file=%s).', 1513 | data.size, entry.height, file); 1514 | 1515 | await fs.writeFile(file, json); 1516 | } 1517 | } 1518 | 1519 | /** 1520 | * User DB 1521 | */ 1522 | 1523 | class UserDB { 1524 | /** 1525 | * Create a user DB. 1526 | * @constructor 1527 | * @param {Object} options 1528 | */ 1529 | 1530 | constructor(options) { 1531 | this.network = options.network; 1532 | this.logger = options.logger; 1533 | this.location = path.resolve(options.prefix, 'users.json'); 1534 | this.locker = new Lock(); 1535 | this.lastFail = 0; 1536 | this.stream = null; 1537 | 1538 | this.map = new Map(); 1539 | this.size = 0; 1540 | } 1541 | 1542 | async open() { 1543 | const unlock = await this.locker.lock(); 1544 | try { 1545 | return await this._open(); 1546 | } finally { 1547 | unlock(); 1548 | } 1549 | } 1550 | 1551 | async _open() { 1552 | await this.load(); 1553 | } 1554 | 1555 | async close() { 1556 | const unlock = await this.locker.lock(); 1557 | try { 1558 | return await this._close(); 1559 | } finally { 1560 | unlock(); 1561 | } 1562 | } 1563 | 1564 | async _close() { 1565 | if (!this.stream) 1566 | return; 1567 | 1568 | try { 1569 | this.stream.close(); 1570 | } catch (e) { 1571 | ; 1572 | } 1573 | 1574 | this.stream = null; 1575 | } 1576 | 1577 | load() { 1578 | return new Promise((resolve, reject) => { 1579 | this._load(resolve, reject); 1580 | }); 1581 | } 1582 | 1583 | _load(resolve, reject) { 1584 | let buf = ''; 1585 | let lineno = 0; 1586 | 1587 | let stream = fs.createReadStream(this.location, { 1588 | flags: 'r', 1589 | encoding: 'utf8', 1590 | autoClose: true 1591 | }); 1592 | 1593 | const close = () => { 1594 | if (!stream) 1595 | return; 1596 | 1597 | try { 1598 | stream.close(); 1599 | } catch (e) { 1600 | ; 1601 | } 1602 | 1603 | stream = null; 1604 | }; 1605 | 1606 | stream.on('error', (err) => { 1607 | if (!stream) 1608 | return; 1609 | 1610 | if (err.code === 'ENOENT') { 1611 | close(); 1612 | resolve(); 1613 | return; 1614 | } 1615 | 1616 | close(); 1617 | reject(err); 1618 | }); 1619 | 1620 | stream.on('data', (data) => { 1621 | if (!stream) 1622 | return; 1623 | 1624 | buf += data; 1625 | 1626 | if (buf.length >= 10000) { 1627 | close(); 1628 | reject(new Error(`UserDB parse error. Line: ${lineno}.`)); 1629 | return; 1630 | } 1631 | 1632 | const lines = buf.split(/\n+/); 1633 | 1634 | buf = lines.pop(); 1635 | 1636 | for (const line of lines) { 1637 | lineno += 1; 1638 | 1639 | if (line.length === 0) 1640 | continue; 1641 | 1642 | let json, user; 1643 | try { 1644 | json = JSON.parse(line); 1645 | user = User.fromJSON(json); 1646 | } catch (e) { 1647 | close(); 1648 | reject(new Error(`UserDB parse error. Line: ${lineno}.`)); 1649 | return; 1650 | } 1651 | 1652 | if (!this.map.has(user.username)) 1653 | this.size += 1; 1654 | 1655 | this.map.set(user.username, user); 1656 | } 1657 | }); 1658 | 1659 | stream.on('end', () => { 1660 | if (!stream) 1661 | return; 1662 | 1663 | this.logger.debug( 1664 | 'Loaded %d users into memory.', 1665 | this.size); 1666 | 1667 | stream = null; 1668 | resolve(); 1669 | }); 1670 | } 1671 | 1672 | get(username) { 1673 | return this.map.get(username); 1674 | } 1675 | 1676 | has(username) { 1677 | return this.map.has(username); 1678 | } 1679 | 1680 | add(options) { 1681 | const user = new User(options); 1682 | 1683 | assert(!this.map.has(user.username), 'User already exists.'); 1684 | 1685 | this.logger.debug( 1686 | 'Adding new user (%s).', 1687 | user.username); 1688 | 1689 | this.map.set(user.username, user); 1690 | this.size += 1; 1691 | 1692 | this.write(user.toJSON()); 1693 | } 1694 | 1695 | setPassword(username, password) { 1696 | const user = this.map.get(username); 1697 | assert(user, 'User does not exist.'); 1698 | user.setPassword(password); 1699 | this.write(user.toJSON()); 1700 | } 1701 | 1702 | write(data) { 1703 | const stream = this.getStream(); 1704 | 1705 | if (!stream) 1706 | return; 1707 | 1708 | const json = JSON.stringify(data) + '\n'; 1709 | stream.write(json, 'utf8'); 1710 | } 1711 | 1712 | getStream() { 1713 | if (this.stream) 1714 | return this.stream; 1715 | 1716 | if (this.lastFail > util.now() - 10) 1717 | return null; 1718 | 1719 | this.lastFail = 0; 1720 | 1721 | this.stream = fs.createWriteStream(this.location, { flags: 'a' }); 1722 | 1723 | this.stream.on('error', (err) => { 1724 | this.logger.warning('UserDB file stream died!'); 1725 | this.logger.error(err); 1726 | 1727 | try { 1728 | this.stream.close(); 1729 | } catch (e) { 1730 | ; 1731 | } 1732 | 1733 | // Retry in ten seconds. 1734 | this.stream = null; 1735 | this.lastFail = util.now(); 1736 | }); 1737 | 1738 | return this.stream; 1739 | } 1740 | } 1741 | 1742 | /* 1743 | * Helpers 1744 | */ 1745 | 1746 | function isJob(id) { 1747 | if (typeof id !== 'string') 1748 | return false; 1749 | 1750 | return id.length >= 12 && id.length <= 21; 1751 | } 1752 | 1753 | function isSID(sid) { 1754 | if (typeof sid !== 'string') 1755 | return false; 1756 | 1757 | return sid.length === 8 && isHex(sid); 1758 | } 1759 | 1760 | function isUsername(username) { 1761 | if (typeof username !== 'string') 1762 | return false; 1763 | 1764 | return username.length > 0 && username.length <= 100; 1765 | } 1766 | 1767 | function isPassword(password) { 1768 | if (typeof password !== 'string') 1769 | return false; 1770 | 1771 | return password.length > 0 && password.length <= 255; 1772 | } 1773 | 1774 | function isAgent(agent) { 1775 | if (typeof agent !== 'string') 1776 | return false; 1777 | 1778 | return agent.length > 0 && agent.length <= 255; 1779 | } 1780 | 1781 | function isHex(str) { 1782 | return typeof str === 'string' 1783 | && str.length % 2 === 0 1784 | && /^[0-9a-f]$/i.test(str); 1785 | } 1786 | 1787 | function hex32(num) { 1788 | assert((num >>> 0) === num); 1789 | num = num.toString(16); 1790 | switch (num.length) { 1791 | case 1: 1792 | return `0000000${num}`; 1793 | case 2: 1794 | return `000000${num}`; 1795 | case 3: 1796 | return `00000${num}`; 1797 | case 4: 1798 | return `0000${num}`; 1799 | case 5: 1800 | return `000${num}`; 1801 | case 6: 1802 | return `00${num}`; 1803 | case 7: 1804 | return `0${num}`; 1805 | case 8: 1806 | return `${num}`; 1807 | default: 1808 | throw new Error(); 1809 | } 1810 | } 1811 | 1812 | /* 1813 | * Expose 1814 | */ 1815 | 1816 | module.exports = Stratum; 1817 | --------------------------------------------------------------------------------