├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── lib ├── algoProperties.js ├── blockTemplate.js ├── daemon.js ├── index.js ├── jobManager.js ├── merkleTree.js ├── peer.js ├── pool.js ├── stratum.js ├── transactions.js ├── util.js └── varDiff.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .idea/ -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dashpay/node-stratum-pool/d8861211fd1fbea4164d6d82e4ebe3287db8d068/.npmignore -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.10' 4 | deploy: 5 | provider: npm 6 | email: zone117x@gmail.com 7 | api_key: 8 | secure: D9lpUDAx1OudPBji3mapnAhOug3wcEBqFrNgWaFh5XiYesa/f/X0gMOJggLlvzyhLHKs8VdTHShdu3XzlC3EDwr5wCdgYO1JSOyDo93FG7Y/qhPDVFnzdtsKmr813Qtj2UDKIh2ZP+JnjKaITrvUwRmdi/8+B9Enr5o9ulFb/a0= -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | {description} 294 | Copyright (C) {year} {fullname} 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | High performance Stratum poolserver in Node.js. One instance of this software can startup and manage multiple coin 2 | pools, each with their own daemon and stratum port :) 3 | 4 | #### Notice 5 | This is a module for Node.js that will do nothing on its own. Unless you're a Node.js developer who would like to 6 | handle stratum authentication and raw share data then this module will not be of use to you. For a full featured portal 7 | that uses this module, see [NOMP (Node Open Mining Portal)](https://github.com/zone117x/node-open-mining-portal). It 8 | handles payments, website front-end, database layer, mutli-coin/pool support, auto-switching miners between coins/pools, 9 | etc.. The portal also has an [MPOS](https://github.com/MPOS/php-mpos) compatibility mode so that the it can function as 10 | a drop-in-replacement for [python-stratum-mining](https://github.com/Crypto-Expert/stratum-mining). 11 | 12 | 13 | [![Build Status](https://travis-ci.org/zone117x/node-stratum-pool.png?branch=master)](https://travis-ci.org/zone117x/node-stratum-pool) 14 | 15 | [![NPM](https://nodei.co/npm/stratum-pool.png?downloads=true&stars=true)](https://nodei.co/npm/stratum-pool/) 16 | 17 | #### Why 18 | This server was built to be more efficient and easier to setup, maintain and scale than existing stratum poolservers 19 | which are written in python. Compared to the spaghetti state of the latest 20 | [stratum-mining python server](https://github.com/Crypto-Expert/stratum-mining/), this software should also have a 21 | lower barrier to entry for other developers to fork and add features or fix bugs. 22 | 23 | 24 | Features 25 | ---------------------------------- 26 | * Daemon RPC interface 27 | * Stratum TCP socket server 28 | * Block template / job manager 29 | * P2P to get block notifications as peer node 30 | * Optimized generation transaction building 31 | * Connecting to multiple daemons for redundancy 32 | * Process share submissions 33 | * Session managing for purging DDoS/flood initiated zombie workers 34 | * Auto ban IPs that are flooding with invalid shares 35 | * __POW__ (proof-of-work) & __POS__ (proof-of-stake) support 36 | * Transaction messages support 37 | * Vardiff (variable difficulty / share limiter) 38 | * When started with a coin deamon that hasn't finished syncing to the network it shows the blockchain download progress and initializes once synced 39 | 40 | #### Hashing algorithms supported: 41 | * ✓ __SHA256__ (Bitcoin, Freicoin, Peercoin/PPCoin, Terracoin, etc..) 42 | * ✓ __Scrypt__ (Litecoin, Dogecoin, Feathercoin, etc..) 43 | * ✓ __Scrypt-Jane__ (YaCoin, CopperBars, Pennies, Tickets, etc..) 44 | * ✓ __Scrypt-N__ (Vertcoin [VTC]) 45 | * ✓ __Quark__ (Quarkcoin [QRK]) 46 | * ✓ __X11__ (Darkcoin [DRK], Hirocoin, Limecoin) 47 | * ✓ __X13__ (MaruCoin, BoostCoin) 48 | * ✓ __Keccak__ (Maxcoin [MAX], HelixCoin, CryptoMeth, Galleon, 365coin, Slothcoin, BitcointalkCoin) 49 | * ✓ __Skein__ (Skeincoin [SKC]) 50 | * ✓ __Groestl__ (Groestlcoin [GRS]) 51 | 52 | May be working (needs additional testing): 53 | * ? *Blake* (Blakecoin [BLC]) 54 | * ? *Fugue* (Fuguecoin [FC]) 55 | * ? *Qubit* (Qubitcoin [Q2C], Myriadcoin [MYR]) 56 | * ? *Hefty1* (Heavycoin [HVC]) 57 | * ? *SHAvite-3* (INKcoin [INK]) 58 | 59 | Not working currently: 60 | * *Groestl* - for Myriadcoin 61 | * *Keccak* - for eCoin & Copperlark 62 | 63 | 64 | 65 | Requirements 66 | ------------ 67 | * node v0.10+ 68 | * coin daemon (preferably one with a relatively updated API and not some crapcoin :p) 69 | 70 | 71 | Example Usage 72 | ------------- 73 | 74 | #### Install as a node module by cloning repository 75 | 76 | ```bash 77 | git clone https://github.com/zone117x/node-stratum-pool node_modules/stratum-pool 78 | npm update 79 | ``` 80 | 81 | #### Module usage 82 | 83 | Create the configuration for your coin: 84 | 85 | Possible options for `algorithm`: *sha256, scrypt, scrypt-jane, scrypt-n, quark, x11, keccak, blake, 86 | skein, groestl, fugue, shavite3, hefty1, or qubit*. 87 | 88 | ```javascript 89 | var myCoin = { 90 | "name": "Dogecoin", 91 | "symbol": "DOGE", 92 | "algorithm": "scrypt", 93 | "nValue": 1024, //optional - defaults to 1024 94 | "rValue": 1, //optional - defaults to 1 95 | "txMessages": false, //optional - defaults to false, 96 | 97 | /* Magic value only required for setting up p2p block notifications. It is found in the daemon 98 | source code as the pchMessageStart variable. 99 | For example, litecoin mainnet magic: http://git.io/Bi8YFw 100 | And for litecoin testnet magic: http://git.io/NXBYJA */ 101 | "peerMagic": "fbc0b6db" //optional 102 | "peerMagicTestnet": "fcc1b7dc" //optional 103 | }; 104 | ``` 105 | 106 | If you are using the `scrypt-jane` algorithm there are additional configurations: 107 | 108 | ```javascript 109 | var myCoin = { 110 | "name": "Freecoin", 111 | "symbol": "FEC", 112 | "algorithm": "scrypt-jane", 113 | "chainStartTime": 1375801200, //defaults to 1367991200 (YACoin) if not used 114 | "nMin": 6, //defaults to 4 if not used 115 | "nMax": 32 //defaults to 30 if not used 116 | }; 117 | ``` 118 | 119 | If you are using the `scrypt-n` algorithm there is an additional configuration: 120 | ```javascript 121 | var myCoin = { 122 | "name": "Execoin", 123 | "symbol": "EXE", 124 | "algorithm": "scrypt-n", 125 | /* This defaults to Vertcoin's timetable if not used. It is required for scrypt-n coins that 126 | have modified their N-factor timetable to be different than Vertcoin's. */ 127 | "timeTable": { 128 | "2048": 1390959880, 129 | "4096": 1438295269, 130 | "8192": 1485630658, 131 | "16384": 1532966047, 132 | "32768": 1580301436, 133 | "65536": 1627636825, 134 | "131072": 1674972214, 135 | "262144": 1722307603 136 | } 137 | }; 138 | ``` 139 | 140 | If you are using the `keccak` algorithm there are additional configurations *(The rare `normalHashing` keccak coins 141 | such as Copperlark and eCoin don't appear to work yet - only the popular ones like Maxcoin are)*: 142 | ```javascript 143 | var myCoin = { 144 | "name": "eCoin", 145 | "symbol": "ECN", 146 | "algorithm": "keccak", 147 | 148 | /* This is not required and set to false by default. Some coins such as Copperlark and eCoin 149 | require it to be set to true. Maxcoin and most others are false. */ 150 | "normalHashing": true 151 | }; 152 | ``` 153 | 154 | 155 | Create and start new pool with configuration options and authentication function 156 | 157 | ```javascript 158 | var Stratum = require('stratum-pool'); 159 | 160 | var pool = Stratum.createPool({ 161 | 162 | "coin": myCoin, 163 | 164 | "address": "mi4iBXbBsydtcc5yFmsff2zCFVX4XG7qJc", //Address to where block rewards are given 165 | 166 | /* Block rewards go to the configured pool wallet address to later be paid out to miners, 167 | except for a percentage that can go to, for examples, pool operator(s) as pool fees or 168 | or to donations address. Addresses or hashed public keys can be used. Here is an example 169 | of rewards going to the main pool op, a pool co-owner, and NOMP donation. */ 170 | "rewardRecipients": { 171 | "n37vuNFkXfk15uFnGoVyHZ6PYQxppD3QqK": 1.5, //1.5% goes to pool op 172 | "mirj3LtZxbSTharhtXvotqtJXUY7ki5qfx": 0.5, //0.5% goes to a pool co-owner 173 | 174 | /* 0.1% donation to NOMP. This pubkey can accept any type of coin, please leave this in 175 | your config to help support NOMP development. */ 176 | "22851477d63a085dbc2398c8430af1c09e7343f6": 0.1 177 | }, 178 | 179 | "blockRefreshInterval": 1000, //How often to poll RPC daemons for new blocks, in milliseconds 180 | 181 | 182 | /* Some miner apps will consider the pool dead/offline if it doesn't receive anything new jobs 183 | for around a minute, so every time we broadcast jobs, set a timeout to rebroadcast 184 | in this many seconds unless we find a new job. Set to zero or remove to disable this. */ 185 | "jobRebroadcastTimeout": 55, 186 | 187 | //instanceId: 37, //Recommend not using this because a crypto-random one will be generated 188 | 189 | /* Some attackers will create thousands of workers that use up all available socket connections, 190 | usually the workers are zombies and don't submit shares after connecting. This features 191 | detects those and disconnects them. */ 192 | "connectionTimeout": 600, //Remove workers that haven't been in contact for this many seconds 193 | 194 | /* Sometimes you want the block hashes even for shares that aren't block candidates. */ 195 | "emitInvalidBlockHashes": false, 196 | 197 | /* Enable for client IP addresses to be detected when using a load balancer with TCP proxy 198 | protocol enabled, such as HAProxy with 'send-proxy' param: 199 | http://haproxy.1wt.eu/download/1.5/doc/configuration.txt */ 200 | "tcpProxyProtocol": false, 201 | 202 | /* If a worker is submitting a high threshold of invalid shares we can temporarily ban their IP 203 | to reduce system/network load. Also useful to fight against flooding attacks. If running 204 | behind something like HAProxy be sure to enable 'tcpProxyProtocol', otherwise you'll end up 205 | banning your own IP address (and therefore all workers). */ 206 | "banning": { 207 | "enabled": true, 208 | "time": 600, //How many seconds to ban worker for 209 | "invalidPercent": 50, //What percent of invalid shares triggers ban 210 | "checkThreshold": 500, //Check invalid percent when this many shares have been submitted 211 | "purgeInterval": 300 //Every this many seconds clear out the list of old bans 212 | }, 213 | 214 | /* Each pool can have as many ports for your miners to connect to as you wish. Each port can 215 | be configured to use its own pool difficulty and variable difficulty settings. varDiff is 216 | optional and will only be used for the ports you configure it for. */ 217 | "ports": { 218 | "3032": { //A port for your miners to connect to 219 | "diff": 32, //the pool difficulty for this port 220 | 221 | /* Variable difficulty is a feature that will automatically adjust difficulty for 222 | individual miners based on their hashrate in order to lower networking overhead */ 223 | "varDiff": { 224 | "minDiff": 8, //Minimum difficulty 225 | "maxDiff": 512, //Network difficulty will be used if it is lower than this 226 | "targetTime": 15, //Try to get 1 share per this many seconds 227 | "retargetTime": 90, //Check to see if we should retarget every this many seconds 228 | "variancePercent": 30 //Allow time to very this % from target without retargeting 229 | } 230 | }, 231 | "3256": { //Another port for your miners to connect to, this port does not use varDiff 232 | "diff": 256 //The pool difficulty 233 | } 234 | }, 235 | 236 | /* Recommended to have at least two daemon instances running in case one drops out-of-sync 237 | or offline. For redundancy, all instances will be polled for block/transaction updates 238 | and be used for submitting blocks. Creating a backup daemon involves spawning a daemon 239 | using the "-datadir=/backup" argument which creates a new daemon instance with it's own 240 | RPC config. For more info on this see: 241 | - https://en.bitcoin.it/wiki/Data_directory 242 | - https://en.bitcoin.it/wiki/Running_bitcoind */ 243 | "daemons": [ 244 | { //Main daemon instance 245 | "host": "127.0.0.1", 246 | "port": 19332, 247 | "user": "litecoinrpc", 248 | "password": "testnet" 249 | }, 250 | { //Backup daemon instance 251 | "host": "127.0.0.1", 252 | "port": 19344, 253 | "user": "litecoinrpc", 254 | "password": "testnet" 255 | } 256 | ], 257 | 258 | 259 | /* This allows the pool to connect to the daemon as a node peer to receive block updates. 260 | It may be the most efficient way to get block updates (faster than polling, less 261 | intensive than blocknotify script). It requires the additional field "peerMagic" in 262 | the coin config. */ 263 | "p2p": { 264 | "enabled": false, 265 | 266 | /* Host for daemon */ 267 | "host": "127.0.0.1", 268 | 269 | /* Port configured for daemon (this is the actual peer port not RPC port) */ 270 | "port": 19333, 271 | 272 | /* If your coin daemon is new enough (i.e. not a shitcoin) then it will support a p2p 273 | feature that prevents the daemon from spamming our peer node with unnecessary 274 | transaction data. Assume its supported but if you have problems try disabling it. */ 275 | "disableTransactions": true 276 | 277 | } 278 | 279 | }, function(ip, workerName, password, callback){ //stratum authorization function 280 | console.log("Authorize " + workerName + ":" + password + "@" + ip); 281 | callback({ 282 | error: null, 283 | authorized: true, 284 | disconnect: false 285 | }); 286 | }); 287 | ``` 288 | 289 | 290 | Listen to pool events 291 | ```javascript 292 | /* 293 | 294 | 'data' object contains: 295 | job: 4, //stratum work job ID 296 | ip: '71.33.19.37', //ip address of client 297 | port: 3333, //port of the client 298 | worker: 'matt.worker1', //stratum worker name 299 | height: 443795, //block height 300 | blockReward: 5000000000, //the number of satoshis received as payment for solving this block 301 | difficulty: 64, //stratum worker difficulty 302 | shareDiff: 78, //actual difficulty of the share 303 | blockDiff: 3349, //block difficulty adjusted for share padding 304 | blockDiffActual: 3349 //actual difficulty for this block 305 | 306 | 307 | //AKA the block solution - set if block was found 308 | blockHash: '110c0447171ad819dd181216d5d80f41e9218e25d833a2789cb8ba289a52eee4', 309 | 310 | //Exists if "emitInvalidBlockHashes" is set to true 311 | blockHashInvalid: '110c0447171ad819dd181216d5d80f41e9218e25d833a2789cb8ba289a52eee4' 312 | 313 | //txHash is the coinbase transaction hash from the block 314 | txHash: '41bb22d6cc409f9c0bae2c39cecd2b3e3e1be213754f23d12c5d6d2003d59b1d, 315 | 316 | error: 'low share difficulty' //set if share is rejected for some reason 317 | */ 318 | pool.on('share', function(isValidShare, isValidBlock, data){ 319 | 320 | if (isValidBlock) 321 | console.log('Block found'); 322 | else if (isValidShare) 323 | console.log('Valid share submitted'); 324 | else if (data.blockHash) 325 | console.log('We thought a block was found but it was rejected by the daemon'); 326 | else 327 | console.log('Invalid share submitted') 328 | 329 | console.log('share data: ' + JSON.stringify(data)); 330 | }); 331 | 332 | 333 | 334 | /* 335 | 'severity': can be 'debug', 'warning', 'error' 336 | 'logKey': can be 'system' or 'client' indicating if the error 337 | was caused by our system or a stratum client 338 | */ 339 | pool.on('log', function(severity, logKey, logText){ 340 | console.log(severity + ': ' + '[' + logKey + '] ' + logText); 341 | }); 342 | ``` 343 | 344 | Start pool 345 | ```javascript 346 | pool.start(); 347 | ``` 348 | 349 | 350 | Credits 351 | ------- 352 | * [vekexasia](//github.com/vekexasia) - co-developer & great tester 353 | * [LucasJones](//github.com/LucasJones) - got p2p block notify working and implemented additional hashing algos 354 | * [TheSeven](//github.com/TheSeven) - answering an absurd amount of my questions, found the block 1-16 problem, provided example code for peer node functionality 355 | * [pronooob](https://dogehouse.org) - knowledgeable & helpful 356 | * [Slush0](//github.com/slush0/stratum-mining) - stratum protocol, documentation and original python code 357 | * [viperaus](//github.com/viperaus/stratum-mining) - scrypt adaptions to python code 358 | * [ahmedbodi](//github.com/ahmedbodi/stratum-mining) - more algo adaptions to python code 359 | * [steveshit](//github.com/steveshit) - ported X11 hashing algo from python to node module 360 | 361 | 362 | Donations 363 | --------- 364 | To support development of this project feel free to donate :) 365 | 366 | * BTC: `1KRotMnQpxu3sePQnsVLRy3EraRFYfJQFR` 367 | * LTC: `LKfavSDJmwiFdcgaP1bbu46hhyiWw5oFhE` 368 | * VTC: `VgW4uFTZcimMSvcnE4cwS3bjJ6P8bcTykN` 369 | * MAX: `mWexUXRCX5PWBmfh34p11wzS5WX2VWvTRT` 370 | * QRK: `QehPDAhzVQWPwDPQvmn7iT3PoFUGT7o8bC` 371 | * DRK: `XcQmhp8ANR7okWAuArcNFZ2bHSB81jpapQ` 372 | * DOGE: `DBGGVtwAAit1NPZpRm5Nz9VUFErcvVvHYW` 373 | * Cryptsy Trade Key: `254ca13444be14937b36c44ba29160bd8f02ff76` 374 | 375 | License 376 | ------- 377 | Released under the GNU General Public License v2 378 | 379 | http://www.gnu.org/licenses/gpl-2.0.html 380 | -------------------------------------------------------------------------------- /lib/algoProperties.js: -------------------------------------------------------------------------------- 1 | var bignum = require('bignum'); 2 | var multiHashing = require('multi-hashing'); 3 | var util = require('./util.js'); 4 | 5 | var diff1 = global.diff1 = 0x00000000ffff0000000000000000000000000000000000000000000000000000; 6 | 7 | var algos = module.exports = global.algos = { 8 | sha256: { 9 | //Uncomment diff if you want to use hardcoded truncated diff 10 | //diff: '00000000ffff0000000000000000000000000000000000000000000000000000', 11 | hash: function(){ 12 | return function(){ 13 | return util.sha256d.apply(this, arguments); 14 | } 15 | } 16 | }, 17 | 'scrypt': { 18 | //Uncomment diff if you want to use hardcoded truncated diff 19 | //diff: '0000ffff00000000000000000000000000000000000000000000000000000000', 20 | multiplier: Math.pow(2, 16), 21 | hash: function(coinConfig){ 22 | var nValue = coinConfig.nValue || 1024; 23 | var rValue = coinConfig.rValue || 1; 24 | return function(data){ 25 | return multiHashing.scrypt(data,nValue,rValue); 26 | } 27 | } 28 | }, 29 | 'scrypt-jane': { 30 | multiplier: Math.pow(2, 16), 31 | hash: function(coinConfig){ 32 | var nTimestamp = coinConfig.chainStartTime || 1367991200; 33 | var nMin = coinConfig.nMin || 4; 34 | var nMax = coinConfig.nMax || 30; 35 | return function(data, nTime){ 36 | return multiHashing.scryptjane(data, nTime, nTimestamp, nMin, nMax); 37 | } 38 | } 39 | }, 40 | 'scrypt-n': { 41 | multiplier: Math.pow(2, 16), 42 | hash: function(coinConfig){ 43 | 44 | var timeTable = coinConfig.timeTable || { 45 | "2048": 1389306217, "4096": 1456415081, "8192": 1506746729, "16384": 1557078377, "32768": 1657741673, 46 | "65536": 1859068265, "131072": 2060394857, "262144": 1722307603, "524288": 1769642992 47 | }; 48 | 49 | var nFactor = (function(){ 50 | var n = Object.keys(timeTable).sort().reverse().filter(function(nKey){ 51 | return Date.now() / 1000 > timeTable[nKey]; 52 | })[0]; 53 | 54 | var nInt = parseInt(n); 55 | return Math.log(nInt) / Math.log(2); 56 | })(); 57 | 58 | return function(data) { 59 | return multiHashing.scryptn(data, nFactor); 60 | } 61 | } 62 | }, 63 | x11: { 64 | hash: function(){ 65 | return function(){ 66 | return multiHashing.x11.apply(this, arguments); 67 | } 68 | } 69 | }, 70 | x13: { 71 | hash: function(){ 72 | return function(){ 73 | return multiHashing.x13.apply(this, arguments); 74 | } 75 | } 76 | }, 77 | quark: { 78 | hash: function(){ 79 | return function(){ 80 | return multiHashing.quark.apply(this, arguments); 81 | } 82 | } 83 | }, 84 | keccak: { 85 | multiplier: Math.pow(2, 8), 86 | hash: function(coinConfig){ 87 | if (coinConfig.normalHashing === true) { 88 | return function (data, nTimeInt) { 89 | return multiHashing.keccak(multiHashing.keccak(Buffer.concat([data, new Buffer(nTimeInt.toString(16), 'hex')]))); 90 | }; 91 | } 92 | else { 93 | return function () { 94 | return multiHashing.keccak.apply(this, arguments); 95 | } 96 | } 97 | } 98 | }, 99 | blake: { 100 | multiplier: Math.pow(2, 8), 101 | hash: function(){ 102 | return function(){ 103 | return multiHashing.blake.apply(this, arguments); 104 | } 105 | } 106 | }, 107 | skein: { 108 | hash: function(){ 109 | return function(){ 110 | return multiHashing.skein.apply(this, arguments); 111 | } 112 | } 113 | }, 114 | groestl: { 115 | multiplier: Math.pow(2, 8), 116 | hash: function(){ 117 | return function(){ 118 | return multiHashing.groestl.apply(this, arguments); 119 | } 120 | } 121 | }, 122 | fugue: { 123 | multiplier: Math.pow(2, 8), 124 | hash: function(){ 125 | return function(){ 126 | return multiHashing.fugue.apply(this, arguments); 127 | } 128 | } 129 | }, 130 | shavite3: { 131 | hash: function(){ 132 | return function(){ 133 | return multiHashing.shavite3.apply(this, arguments); 134 | } 135 | } 136 | }, 137 | hefty1: { 138 | hash: function(){ 139 | return function(){ 140 | return multiHashing.hefty1.apply(this, arguments); 141 | } 142 | } 143 | }, 144 | qubit: { 145 | hash: function(){ 146 | return function(){ 147 | return multiHashing.qubit.apply(this, arguments); 148 | } 149 | } 150 | } 151 | }; 152 | 153 | 154 | for (var algo in algos){ 155 | if (!algos[algo].multiplier) 156 | algos[algo].multiplier = 1; 157 | 158 | /*if (algos[algo].diff){ 159 | algos[algo].maxDiff = bignum(algos[algo].diff, 16); 160 | } 161 | else if (algos[algo].shift){ 162 | algos[algo].nonTruncatedDiff = util.shiftMax256Right(algos[algo].shift); 163 | algos[algo].bits = util.bufferToCompactBits(algos[algo].nonTruncatedDiff); 164 | algos[algo].maxDiff = bignum.fromBuffer(util.convertBitsToBuff(algos[algo].bits)); 165 | } 166 | else if (algos[algo].multiplier){ 167 | algos[algo].maxDiff = diff1.mul(Math.pow(2, 32) / algos[algo].multiplier); 168 | } 169 | else{ 170 | algos[algo].maxDiff = diff1; 171 | }*/ 172 | } 173 | -------------------------------------------------------------------------------- /lib/blockTemplate.js: -------------------------------------------------------------------------------- 1 | var bignum = require('bignum'); 2 | 3 | var merkleTree = require('./merkleTree.js'); 4 | var transactions = require('./transactions.js'); 5 | var util = require('./util.js'); 6 | 7 | 8 | /** 9 | * The BlockTemplate class holds a single job. 10 | * and provides several methods to validate and submit it to the daemon coin 11 | **/ 12 | var BlockTemplate = module.exports = function BlockTemplate(jobId, rpcData, poolAddressScript, extraNoncePlaceholder, reward, txMessages, recipients){ 13 | 14 | //private members 15 | 16 | var submits = []; 17 | 18 | function getMerkleHashes(steps){ 19 | return steps.map(function(step){ 20 | return step.toString('hex'); 21 | }); 22 | } 23 | 24 | function getTransactionBuffers(txs){ 25 | var txHashes = txs.map(function(tx){ 26 | return util.uint256BufferFromHash(tx.hash); 27 | }); 28 | return [null].concat(txHashes); 29 | } 30 | 31 | function getVoteData(){ 32 | if (!rpcData.masternode_payments) return new Buffer([]); 33 | 34 | return Buffer.concat( 35 | [util.varIntBuffer(rpcData.votes.length)].concat( 36 | rpcData.votes.map(function (vt) { 37 | return new Buffer(vt, 'hex'); 38 | }) 39 | ) 40 | ); 41 | } 42 | 43 | //public members 44 | 45 | this.rpcData = rpcData; 46 | this.jobId = jobId; 47 | 48 | 49 | this.target = rpcData.target ? 50 | bignum(rpcData.target, 16) : 51 | util.bignumFromBitsHex(rpcData.bits); 52 | 53 | this.difficulty = parseFloat((diff1 / this.target.toNumber()).toFixed(9)); 54 | 55 | 56 | 57 | 58 | 59 | this.prevHashReversed = util.reverseByteOrder(new Buffer(rpcData.previousblockhash, 'hex')).toString('hex'); 60 | this.transactionData = Buffer.concat(rpcData.transactions.map(function(tx){ 61 | return new Buffer(tx.data, 'hex'); 62 | })); 63 | this.merkleTree = new merkleTree(getTransactionBuffers(rpcData.transactions)); 64 | this.merkleBranch = getMerkleHashes(this.merkleTree.steps); 65 | this.generationTransaction = transactions.CreateGeneration( 66 | rpcData, 67 | poolAddressScript, 68 | extraNoncePlaceholder, 69 | reward, 70 | txMessages, 71 | recipients 72 | ); 73 | 74 | this.serializeCoinbase = function(extraNonce1, extraNonce2){ 75 | return Buffer.concat([ 76 | this.generationTransaction[0], 77 | extraNonce1, 78 | extraNonce2, 79 | this.generationTransaction[1] 80 | ]); 81 | }; 82 | 83 | 84 | //https://en.bitcoin.it/wiki/Protocol_specification#Block_Headers 85 | this.serializeHeader = function(merkleRoot, nTime, nonce){ 86 | 87 | var header = new Buffer(80); 88 | var position = 0; 89 | header.write(nonce, position, 4, 'hex'); 90 | header.write(rpcData.bits, position += 4, 4, 'hex'); 91 | header.write(nTime, position += 4, 4, 'hex'); 92 | header.write(merkleRoot, position += 4, 32, 'hex'); 93 | header.write(rpcData.previousblockhash, position += 32, 32, 'hex'); 94 | header.writeUInt32BE(rpcData.version, position + 32); 95 | var header = util.reverseBuffer(header); 96 | return header; 97 | }; 98 | 99 | this.serializeBlock = function(header, coinbase){ 100 | return Buffer.concat([ 101 | header, 102 | 103 | util.varIntBuffer(this.rpcData.transactions.length + 1), 104 | coinbase, 105 | this.transactionData, 106 | 107 | getVoteData(), 108 | 109 | //POS coins require a zero byte appended to block which the daemon replaces with the signature 110 | new Buffer(reward === 'POS' ? [0] : []) 111 | ]); 112 | }; 113 | 114 | this.registerSubmit = function(extraNonce1, extraNonce2, nTime, nonce){ 115 | var submission = extraNonce1 + extraNonce2 + nTime + nonce; 116 | if (submits.indexOf(submission) === -1){ 117 | submits.push(submission); 118 | return true; 119 | } 120 | return false; 121 | }; 122 | 123 | this.getJobParams = function(){ 124 | if (!this.jobParams){ 125 | this.jobParams = [ 126 | this.jobId, 127 | this.prevHashReversed, 128 | this.generationTransaction[0].toString('hex'), 129 | this.generationTransaction[1].toString('hex'), 130 | this.merkleBranch, 131 | util.packInt32BE(this.rpcData.version).toString('hex'), 132 | this.rpcData.bits, 133 | util.packUInt32BE(this.rpcData.curtime).toString('hex'), 134 | true 135 | ]; 136 | } 137 | return this.jobParams; 138 | }; 139 | }; -------------------------------------------------------------------------------- /lib/daemon.js: -------------------------------------------------------------------------------- 1 | var http = require('http'); 2 | var cp = require('child_process'); 3 | var events = require('events'); 4 | 5 | var async = require('async'); 6 | 7 | /** 8 | * The daemon interface interacts with the coin daemon by using the rpc interface. 9 | * in order to make it work it needs, as constructor, an array of objects containing 10 | * - 'host' : hostname where the coin lives 11 | * - 'port' : port where the coin accepts rpc connections 12 | * - 'user' : username of the coin for the rpc interface 13 | * - 'password': password for the rpc interface of the coin 14 | **/ 15 | 16 | function DaemonInterface(daemons, logger){ 17 | 18 | //private members 19 | var _this = this; 20 | logger = logger || function(severity, message){ 21 | console.log(severity + ': ' + message); 22 | }; 23 | 24 | 25 | var instances = (function(){ 26 | for (var i = 0; i < daemons.length; i++) 27 | daemons[i]['index'] = i; 28 | return daemons; 29 | })(); 30 | 31 | 32 | function init(){ 33 | isOnline(function(online){ 34 | if (online) 35 | _this.emit('online'); 36 | }); 37 | } 38 | 39 | function isOnline(callback){ 40 | cmd('getinfo', [], function(results){ 41 | var allOnline = results.every(function(result){ 42 | return !results.error; 43 | }); 44 | callback(allOnline); 45 | if (!allOnline) 46 | _this.emit('connectionFailed', results); 47 | }); 48 | } 49 | 50 | 51 | function performHttpRequest(instance, jsonData, callback){ 52 | var options = { 53 | hostname: (typeof(instance.host) === 'undefined' ? '127.0.0.1' : instance.host), 54 | port : instance.port, 55 | method : 'POST', 56 | auth : instance.user + ':' + instance.password, 57 | headers : { 58 | 'Content-Length': jsonData.length 59 | } 60 | }; 61 | 62 | var parseJson = function(res, data){ 63 | var dataJson; 64 | 65 | if (res.statusCode === 401){ 66 | logger('error', 'Unauthorized RPC access - invalid RPC username or password'); 67 | return; 68 | } 69 | 70 | try{ 71 | dataJson = JSON.parse(data); 72 | } 73 | catch(e){ 74 | if (data.indexOf(':-nan') !== -1){ 75 | data = data.replace(/:-nan,/g, ":0"); 76 | parseJson(res, data); 77 | return; 78 | } 79 | logger('error', 'Could not parse rpc data from daemon instance ' + instance.index 80 | + '\nRequest Data: ' + jsonData 81 | + '\nReponse Data: ' + data); 82 | 83 | } 84 | if (dataJson) 85 | callback(dataJson.error, dataJson, data); 86 | }; 87 | 88 | var req = http.request(options, function(res) { 89 | var data = ''; 90 | res.setEncoding('utf8'); 91 | res.on('data', function (chunk) { 92 | data += chunk; 93 | }); 94 | res.on('end', function(){ 95 | parseJson(res, data); 96 | }); 97 | }); 98 | 99 | req.on('error', function(e) { 100 | if (e.code === 'ECONNREFUSED') 101 | callback({type: 'offline', message: e.message}, null); 102 | else 103 | callback({type: 'request error', message: e.message}, null); 104 | }); 105 | 106 | req.end(jsonData); 107 | } 108 | 109 | 110 | 111 | //Performs a batch JSON-RPC command - only uses the first configured rpc daemon 112 | /* First argument must have: 113 | [ 114 | [ methodName, [params] ], 115 | [ methodName, [params] ] 116 | ] 117 | */ 118 | 119 | function batchCmd(cmdArray, callback){ 120 | 121 | var requestJson = []; 122 | 123 | for (var i = 0; i < cmdArray.length; i++){ 124 | requestJson.push({ 125 | method: cmdArray[i][0], 126 | params: cmdArray[i][1], 127 | id: Date.now() + Math.floor(Math.random() * 10) + i 128 | }); 129 | } 130 | 131 | var serializedRequest = JSON.stringify(requestJson); 132 | 133 | performHttpRequest(instances[0], serializedRequest, function(error, result){ 134 | callback(error, result); 135 | }); 136 | 137 | } 138 | 139 | /* Sends a JSON RPC (http://json-rpc.org/wiki/specification) command to every configured daemon. 140 | The callback function is fired once with the result from each daemon unless streamResults is 141 | set to true. */ 142 | function cmd(method, params, callback, streamResults, returnRawData){ 143 | 144 | var results = []; 145 | 146 | async.each(instances, function(instance, eachCallback){ 147 | 148 | var itemFinished = function(error, result, data){ 149 | 150 | var returnObj = { 151 | error: error, 152 | response: (result || {}).result, 153 | instance: instance 154 | }; 155 | if (returnRawData) returnObj.data = data; 156 | if (streamResults) callback(returnObj); 157 | else results.push(returnObj); 158 | eachCallback(); 159 | itemFinished = function(){}; 160 | }; 161 | 162 | var requestJson = JSON.stringify({ 163 | method: method, 164 | params: params, 165 | id: Date.now() + Math.floor(Math.random() * 10) 166 | }); 167 | 168 | performHttpRequest(instance, requestJson, function(error, result, data){ 169 | itemFinished(error, result, data); 170 | }); 171 | 172 | 173 | }, function(){ 174 | if (!streamResults){ 175 | callback(results); 176 | } 177 | }); 178 | 179 | } 180 | 181 | 182 | //public members 183 | 184 | this.init = init; 185 | this.isOnline = isOnline; 186 | this.cmd = cmd; 187 | this.batchCmd = batchCmd; 188 | } 189 | 190 | DaemonInterface.prototype.__proto__ = events.EventEmitter.prototype; 191 | 192 | exports.interface = DaemonInterface; -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | var net = require('net'); 2 | var events = require('events'); 3 | 4 | //Gives us global access to everything we need for each hashing algorithm 5 | require('./algoProperties.js'); 6 | 7 | var pool = require('./pool.js'); 8 | 9 | exports.daemon = require('./daemon.js'); 10 | exports.varDiff = require('./varDiff.js'); 11 | 12 | 13 | exports.createPool = function(poolOptions, authorizeFn){ 14 | var newPool = new pool(poolOptions, authorizeFn); 15 | return newPool; 16 | }; 17 | -------------------------------------------------------------------------------- /lib/jobManager.js: -------------------------------------------------------------------------------- 1 | var events = require('events'); 2 | var crypto = require('crypto'); 3 | 4 | var bignum = require('bignum'); 5 | 6 | 7 | 8 | var util = require('./util.js'); 9 | var blockTemplate = require('./blockTemplate.js'); 10 | 11 | 12 | 13 | //Unique extranonce per subscriber 14 | var ExtraNonceCounter = function(configInstanceId){ 15 | 16 | var instanceId = configInstanceId || crypto.randomBytes(4).readUInt32LE(0); 17 | var counter = instanceId << 27; 18 | 19 | this.next = function(){ 20 | var extraNonce = util.packUInt32BE(Math.abs(counter++)); 21 | return extraNonce.toString('hex'); 22 | }; 23 | 24 | this.size = 4; //bytes 25 | }; 26 | 27 | //Unique job per new block template 28 | var JobCounter = function(){ 29 | var counter = 0; 30 | 31 | this.next = function(){ 32 | counter++; 33 | if (counter % 0xffff === 0) 34 | counter = 1; 35 | return this.cur(); 36 | }; 37 | 38 | this.cur = function () { 39 | return counter.toString(16); 40 | }; 41 | }; 42 | 43 | /** 44 | * Emits: 45 | * - newBlock(blockTemplate) - When a new block (previously unknown to the JobManager) is added, use this event to broadcast new jobs 46 | * - share(shareData, blockHex) - When a worker submits a share. It will have blockHex if a block was found 47 | **/ 48 | var JobManager = module.exports = function JobManager(options){ 49 | 50 | 51 | //private members 52 | 53 | var _this = this; 54 | var jobCounter = new JobCounter(); 55 | 56 | var shareMultiplier = algos[options.coin.algorithm].multiplier; 57 | 58 | //public members 59 | 60 | this.extraNonceCounter = new ExtraNonceCounter(options.instanceId); 61 | this.extraNoncePlaceholder = new Buffer('f000000ff111111f', 'hex'); 62 | this.extraNonce2Size = this.extraNoncePlaceholder.length - this.extraNonceCounter.size; 63 | 64 | this.currentJob; 65 | this.validJobs = {}; 66 | 67 | var hashDigest = algos[options.coin.algorithm].hash(options.coin); 68 | 69 | var coinbaseHasher = (function(){ 70 | switch(options.coin.algorithm){ 71 | case 'keccak': 72 | case 'blake': 73 | case 'fugue': 74 | case 'groestl': 75 | if (options.coin.normalHashing === true) 76 | return util.sha256d; 77 | else 78 | return util.sha256; 79 | default: 80 | return util.sha256d; 81 | } 82 | })(); 83 | 84 | 85 | var blockHasher = (function () { 86 | switch (options.coin.algorithm) { 87 | case 'scrypt': 88 | if (options.coin.reward === 'POS') { 89 | return function (d) { 90 | return util.reverseBuffer(hashDigest.apply(this, arguments)); 91 | }; 92 | } 93 | case 'scrypt-jane': 94 | if (options.coin.reward === 'POS') { 95 | return function (d) { 96 | return util.reverseBuffer(hashDigest.apply(this, arguments)); 97 | }; 98 | } 99 | case 'scrypt-n': 100 | return function (d) { 101 | return util.reverseBuffer(util.sha256d(d)); 102 | }; 103 | default: 104 | return function () { 105 | return util.reverseBuffer(hashDigest.apply(this, arguments)); 106 | }; 107 | } 108 | })(); 109 | 110 | this.updateCurrentJob = function(rpcData){ 111 | 112 | var tmpBlockTemplate = new blockTemplate( 113 | jobCounter.next(), 114 | rpcData, 115 | options.poolAddressScript, 116 | _this.extraNoncePlaceholder, 117 | options.coin.reward, 118 | options.coin.txMessages, 119 | options.recipients 120 | ); 121 | 122 | _this.currentJob = tmpBlockTemplate; 123 | 124 | _this.emit('updatedBlock', tmpBlockTemplate, true); 125 | 126 | _this.validJobs[tmpBlockTemplate.jobId] = tmpBlockTemplate; 127 | 128 | }; 129 | 130 | //returns true if processed a new block 131 | this.processTemplate = function(rpcData){ 132 | 133 | /* Block is new if A) its the first block we have seen so far or B) the blockhash is different and the 134 | block height is greater than the one we have */ 135 | var isNewBlock = typeof(_this.currentJob) === 'undefined'; 136 | if (!isNewBlock && _this.currentJob.rpcData.previousblockhash !== rpcData.previousblockhash){ 137 | isNewBlock = true; 138 | 139 | //If new block is outdated/out-of-sync than return 140 | if (rpcData.height < _this.currentJob.rpcData.height) 141 | return false; 142 | } 143 | 144 | if (!isNewBlock) return false; 145 | 146 | 147 | var tmpBlockTemplate = new blockTemplate( 148 | jobCounter.next(), 149 | rpcData, 150 | options.poolAddressScript, 151 | _this.extraNoncePlaceholder, 152 | options.coin.reward, 153 | options.coin.txMessages, 154 | options.recipients 155 | ); 156 | 157 | this.currentJob = tmpBlockTemplate; 158 | 159 | this.validJobs = {}; 160 | _this.emit('newBlock', tmpBlockTemplate); 161 | 162 | this.validJobs[tmpBlockTemplate.jobId] = tmpBlockTemplate; 163 | 164 | return true; 165 | 166 | }; 167 | 168 | this.processShare = function(jobId, previousDifficulty, difficulty, extraNonce1, extraNonce2, nTime, nonce, ipAddress, port, workerName){ 169 | var shareError = function(error){ 170 | _this.emit('share', { 171 | job: jobId, 172 | ip: ipAddress, 173 | worker: workerName, 174 | difficulty: difficulty, 175 | error: error[1] 176 | }); 177 | return {error: error, result: null}; 178 | }; 179 | 180 | var submitTime = Date.now() / 1000 | 0; 181 | 182 | if (extraNonce2.length / 2 !== _this.extraNonce2Size) 183 | return shareError([20, 'incorrect size of extranonce2']); 184 | 185 | var job = this.validJobs[jobId]; 186 | 187 | if (typeof job === 'undefined' || job.jobId != jobId ) { 188 | return shareError([21, 'job not found']); 189 | } 190 | 191 | if (nTime.length !== 8) { 192 | return shareError([20, 'incorrect size of ntime']); 193 | } 194 | 195 | var nTimeInt = parseInt(nTime, 16); 196 | if (nTimeInt < job.rpcData.curtime || nTimeInt > submitTime + 7200) { 197 | return shareError([20, 'ntime out of range']); 198 | } 199 | 200 | if (nonce.length !== 8) { 201 | return shareError([20, 'incorrect size of nonce']); 202 | } 203 | 204 | if (!job.registerSubmit(extraNonce1, extraNonce2, nTime, nonce)) { 205 | return shareError([22, 'duplicate share']); 206 | } 207 | 208 | 209 | var extraNonce1Buffer = new Buffer(extraNonce1, 'hex'); 210 | var extraNonce2Buffer = new Buffer(extraNonce2, 'hex'); 211 | 212 | var coinbaseBuffer = job.serializeCoinbase(extraNonce1Buffer, extraNonce2Buffer); 213 | var coinbaseHash = coinbaseHasher(coinbaseBuffer); 214 | 215 | var merkleRoot = util.reverseBuffer(job.merkleTree.withFirst(coinbaseHash)).toString('hex'); 216 | 217 | var headerBuffer = job.serializeHeader(merkleRoot, nTime, nonce); 218 | var headerHash = hashDigest(headerBuffer, nTimeInt); 219 | var headerBigNum = bignum.fromBuffer(headerHash, {endian: 'little', size: 32}); 220 | 221 | var blockHashInvalid; 222 | var blockHash; 223 | var blockHex; 224 | 225 | var shareDiff = diff1 / headerBigNum.toNumber() * shareMultiplier; 226 | 227 | var blockDiffAdjusted = job.difficulty * shareMultiplier; 228 | 229 | //Check if share is a block candidate (matched network difficulty) 230 | if (job.target.ge(headerBigNum)){ 231 | blockHex = job.serializeBlock(headerBuffer, coinbaseBuffer).toString('hex'); 232 | blockHash = blockHasher(headerBuffer, nTime).toString('hex'); 233 | } 234 | else { 235 | if (options.emitInvalidBlockHashes) 236 | blockHashInvalid = util.reverseBuffer(util.sha256d(headerBuffer)).toString('hex'); 237 | 238 | //Check if share didn't reached the miner's difficulty) 239 | if (shareDiff / difficulty < 0.99){ 240 | 241 | //Check if share matched a previous difficulty from before a vardiff retarget 242 | if (previousDifficulty && shareDiff >= previousDifficulty){ 243 | difficulty = previousDifficulty; 244 | } 245 | else{ 246 | return shareError([23, 'low difficulty share of ' + shareDiff]); 247 | } 248 | 249 | } 250 | } 251 | 252 | 253 | _this.emit('share', { 254 | job: jobId, 255 | ip: ipAddress, 256 | port: port, 257 | worker: workerName, 258 | height: job.rpcData.height, 259 | blockReward: job.rpcData.coinbasevalue, 260 | difficulty: difficulty, 261 | shareDiff: shareDiff.toFixed(8), 262 | blockDiff : blockDiffAdjusted, 263 | blockDiffActual: job.difficulty, 264 | blockHash: blockHash, 265 | blockHashInvalid: blockHashInvalid 266 | }, blockHex); 267 | 268 | return {result: true, error: null, blockHash: blockHash}; 269 | }; 270 | }; 271 | JobManager.prototype.__proto__ = events.EventEmitter.prototype; 272 | -------------------------------------------------------------------------------- /lib/merkleTree.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Ported from https://github.com/slush0/stratum-mining/blob/master/lib/merkletree.py 4 | 5 | */ 6 | 7 | var util = require('./util.js'); 8 | 9 | var MerkleTree = module.exports = function MerkleTree(data){ 10 | 11 | function merkleJoin(h1, h2){ 12 | var joined = Buffer.concat([h1, h2]); 13 | var dhashed = util.sha256d(joined); 14 | return dhashed; 15 | } 16 | 17 | function calculateSteps(data){ 18 | var L = data; 19 | var steps = []; 20 | var PreL = [null]; 21 | var StartL = 2; 22 | var Ll = L.length; 23 | 24 | if (Ll > 1){ 25 | while (true){ 26 | 27 | if (Ll === 1) 28 | break; 29 | 30 | steps.push(L[1]); 31 | 32 | if (Ll % 2) 33 | L.push(L[L.length - 1]); 34 | 35 | var Ld = []; 36 | var r = util.range(StartL, Ll, 2); 37 | r.forEach(function(i){ 38 | Ld.push(merkleJoin(L[i], L[i + 1])); 39 | }); 40 | L = PreL.concat(Ld); 41 | Ll = L.length; 42 | } 43 | } 44 | return steps; 45 | } 46 | 47 | this.data = data; 48 | this.steps = calculateSteps(data); 49 | 50 | } 51 | MerkleTree.prototype = { 52 | withFirst: function(f){ 53 | this.steps.forEach(function(s){ 54 | f = util.sha256d(Buffer.concat([f, s])); 55 | }); 56 | return f; 57 | } 58 | }; -------------------------------------------------------------------------------- /lib/peer.js: -------------------------------------------------------------------------------- 1 | var net = require('net'); 2 | var crypto = require('crypto'); 3 | var events = require('events'); 4 | 5 | var util = require('./util.js'); 6 | 7 | 8 | //Example of p2p in node from TheSeven: http://paste.pm/e54.js 9 | 10 | 11 | var fixedLenStringBuffer = function(s, len) { 12 | var buff = new Buffer(len); 13 | buff.fill(0); 14 | buff.write(s); 15 | return buff; 16 | }; 17 | 18 | var commandStringBuffer = function (s) { 19 | return fixedLenStringBuffer(s, 12); 20 | }; 21 | 22 | /* Reads a set amount of bytes from a flowing stream, argument descriptions: 23 | - stream to read from, must have data emitter 24 | - amount of bytes to read 25 | - preRead argument can be used to set start with an existing data buffer 26 | - callback returns 1) data buffer and 2) lopped/over-read data */ 27 | var readFlowingBytes = function (stream, amount, preRead, callback) { 28 | 29 | var buff = preRead ? preRead : new Buffer([]); 30 | 31 | var readData = function (data) { 32 | buff = Buffer.concat([buff, data]); 33 | if (buff.length >= amount) { 34 | var returnData = buff.slice(0, amount); 35 | var lopped = buff.length > amount ? buff.slice(amount) : null; 36 | callback(returnData, lopped); 37 | } 38 | else 39 | stream.once('data', readData); 40 | }; 41 | 42 | readData(new Buffer([])); 43 | }; 44 | 45 | var Peer = module.exports = function (options) { 46 | 47 | var _this = this; 48 | var client; 49 | var magic = new Buffer(options.testnet ? options.coin.peerMagicTestnet : options.coin.peerMagic, 'hex'); 50 | var magicInt = magic.readUInt32LE(0); 51 | var verack = false; 52 | var validConnectionConfig = true; 53 | 54 | //https://en.bitcoin.it/wiki/Protocol_specification#Inventory_Vectors 55 | var invCodes = { 56 | error: 0, 57 | tx: 1, 58 | block: 2 59 | }; 60 | 61 | var networkServices = new Buffer('0100000000000000', 'hex'); //NODE_NETWORK services (value 1 packed as uint64) 62 | var emptyNetAddress = new Buffer('010000000000000000000000000000000000ffff000000000000', 'hex'); 63 | var userAgent = util.varStringBuffer('/node-stratum/'); 64 | var blockStartHeight = new Buffer('00000000', 'hex'); //block start_height, can be empty 65 | 66 | //If protocol version is new enough, add do not relay transactions flag byte, outlined in BIP37 67 | //https://github.com/bitcoin/bips/blob/master/bip-0037.mediawiki#extensions-to-existing-messages 68 | var relayTransactions = options.p2p.disableTransactions === true ? new Buffer([false]) : new Buffer([]); 69 | 70 | var commands = { 71 | version: commandStringBuffer('version'), 72 | inv: commandStringBuffer('inv'), 73 | verack: commandStringBuffer('verack'), 74 | addr: commandStringBuffer('addr'), 75 | getblocks: commandStringBuffer('getblocks') 76 | }; 77 | 78 | 79 | (function init() { 80 | Connect(); 81 | })(); 82 | 83 | 84 | function Connect() { 85 | 86 | client = net.connect({ 87 | host: options.p2p.host, 88 | port: options.p2p.port 89 | }, function () { 90 | SendVersion(); 91 | }); 92 | client.on('close', function () { 93 | if (verack) { 94 | _this.emit('disconnected'); 95 | verack = false; 96 | Connect(); 97 | } 98 | else if (validConnectionConfig) 99 | _this.emit('connectionRejected'); 100 | 101 | }); 102 | client.on('error', function (e) { 103 | if (e.code === 'ECONNREFUSED') { 104 | validConnectionConfig = false; 105 | _this.emit('connectionFailed'); 106 | } 107 | else 108 | _this.emit('socketError', e); 109 | }); 110 | 111 | 112 | SetupMessageParser(client); 113 | 114 | } 115 | 116 | function SetupMessageParser(client) { 117 | 118 | var beginReadingMessage = function (preRead) { 119 | 120 | readFlowingBytes(client, 24, preRead, function (header, lopped) { 121 | var msgMagic = header.readUInt32LE(0); 122 | if (msgMagic !== magicInt) { 123 | _this.emit('error', 'bad magic number from peer'); 124 | while (header.readUInt32LE(0) !== magicInt && header.length >= 4) { 125 | header = header.slice(1); 126 | } 127 | if (header.readUInt32LE(0) === magicInt) { 128 | beginReadingMessage(header); 129 | } else { 130 | beginReadingMessage(new Buffer([])); 131 | } 132 | return; 133 | } 134 | var msgCommand = header.slice(4, 16).toString(); 135 | var msgLength = header.readUInt32LE(16); 136 | var msgChecksum = header.readUInt32LE(20); 137 | readFlowingBytes(client, msgLength, lopped, function (payload, lopped) { 138 | if (util.sha256d(payload).readUInt32LE(0) !== msgChecksum) { 139 | _this.emit('error', 'bad payload - failed checksum'); 140 | beginReadingMessage(null); 141 | return; 142 | } 143 | HandleMessage(msgCommand, payload); 144 | beginReadingMessage(lopped); 145 | }); 146 | }); 147 | }; 148 | 149 | beginReadingMessage(null); 150 | } 151 | 152 | 153 | //Parsing inv message https://en.bitcoin.it/wiki/Protocol_specification#inv 154 | function HandleInv(payload) { 155 | //sloppy varint decoding 156 | var count = payload.readUInt8(0); 157 | payload = payload.slice(1); 158 | if (count >= 0xfd) 159 | { 160 | count = payload.readUInt16LE(0); 161 | payload = payload.slice(2); 162 | } 163 | while (count--) { 164 | switch(payload.readUInt32LE(0)) { 165 | case invCodes.error: 166 | break; 167 | case invCodes.tx: 168 | var tx = payload.slice(4, 36).toString('hex'); 169 | break; 170 | case invCodes.block: 171 | var block = payload.slice(4, 36).toString('hex'); 172 | _this.emit('blockFound', block); 173 | break; 174 | } 175 | payload = payload.slice(36); 176 | } 177 | } 178 | 179 | function HandleMessage(command, payload) { 180 | _this.emit('peerMessage', {command: command, payload: payload}); 181 | switch (command) { 182 | case commands.inv.toString(): 183 | HandleInv(payload); 184 | break; 185 | case commands.verack.toString(): 186 | if(!verack) { 187 | verack = true; 188 | _this.emit('connected'); 189 | } 190 | break; 191 | default: 192 | break; 193 | } 194 | 195 | } 196 | 197 | //Message structure defined at: https://en.bitcoin.it/wiki/Protocol_specification#Message_structure 198 | function SendMessage(command, payload) { 199 | var message = Buffer.concat([ 200 | magic, 201 | command, 202 | util.packUInt32LE(payload.length), 203 | util.sha256d(payload).slice(0, 4), 204 | payload 205 | ]); 206 | client.write(message); 207 | _this.emit('sentMessage', message); 208 | } 209 | 210 | function SendVersion() { 211 | var payload = Buffer.concat([ 212 | util.packUInt32LE(options.protocolVersion), 213 | networkServices, 214 | util.packInt64LE(Date.now() / 1000 | 0), 215 | emptyNetAddress, //addr_recv, can be empty 216 | emptyNetAddress, //addr_from, can be empty 217 | crypto.pseudoRandomBytes(8), //nonce, random unique ID 218 | userAgent, 219 | blockStartHeight, 220 | relayTransactions 221 | ]); 222 | SendMessage(commands.version, payload); 223 | } 224 | 225 | }; 226 | 227 | Peer.prototype.__proto__ = events.EventEmitter.prototype; 228 | -------------------------------------------------------------------------------- /lib/pool.js: -------------------------------------------------------------------------------- 1 | var events = require('events'); 2 | var async = require('async'); 3 | 4 | var varDiff = require('./varDiff.js'); 5 | var daemon = require('./daemon.js'); 6 | var peer = require('./peer.js'); 7 | var stratum = require('./stratum.js'); 8 | var jobManager = require('./jobManager.js'); 9 | var util = require('./util.js'); 10 | 11 | /*process.on('uncaughtException', function(err) { 12 | console.log(err.stack); 13 | throw err; 14 | });*/ 15 | 16 | var pool = module.exports = function pool(options, authorizeFn){ 17 | 18 | this.options = options; 19 | 20 | var _this = this; 21 | var blockPollingIntervalId; 22 | 23 | 24 | var emitLog = function(text) { _this.emit('log', 'debug' , text); }; 25 | var emitWarningLog = function(text) { _this.emit('log', 'warning', text); }; 26 | var emitErrorLog = function(text) { _this.emit('log', 'error' , text); }; 27 | var emitSpecialLog = function(text) { _this.emit('log', 'special', text); }; 28 | 29 | 30 | 31 | if (!(options.coin.algorithm in algos)){ 32 | emitErrorLog('The ' + options.coin.algorithm + ' hashing algorithm is not supported.'); 33 | throw new Error(); 34 | } 35 | 36 | 37 | 38 | this.start = function(){ 39 | SetupVarDiff(); 40 | SetupApi(); 41 | SetupDaemonInterface(function(){ 42 | DetectCoinData(function(){ 43 | SetupRecipients(); 44 | SetupJobManager(); 45 | OnBlockchainSynced(function(){ 46 | GetFirstJob(function(){ 47 | SetupBlockPolling(); 48 | SetupPeer(); 49 | StartStratumServer(function(){ 50 | OutputPoolInfo(); 51 | _this.emit('started'); 52 | }); 53 | }); 54 | }); 55 | }); 56 | }); 57 | }; 58 | 59 | 60 | 61 | function GetFirstJob(finishedCallback){ 62 | 63 | GetBlockTemplate(function(error, result){ 64 | if (error) { 65 | emitErrorLog('Error with getblocktemplate on creating first job, server cannot start'); 66 | return; 67 | } 68 | 69 | var portWarnings = []; 70 | 71 | var networkDiffAdjusted = options.initStats.difficulty; 72 | 73 | Object.keys(options.ports).forEach(function(port){ 74 | var portDiff = options.ports[port].diff; 75 | if (networkDiffAdjusted < portDiff) 76 | portWarnings.push('port ' + port + ' w/ diff ' + portDiff); 77 | }); 78 | 79 | //Only let the first fork show synced status or the log wil look flooded with it 80 | if (portWarnings.length > 0 && (!process.env.forkId || process.env.forkId === '0')) { 81 | var warnMessage = 'Network diff of ' + networkDiffAdjusted + ' is lower than ' 82 | + portWarnings.join(' and '); 83 | emitWarningLog(warnMessage); 84 | } 85 | 86 | finishedCallback(); 87 | 88 | }); 89 | } 90 | 91 | 92 | function OutputPoolInfo(){ 93 | 94 | var startMessage = 'Stratum Pool Server Started for ' + options.coin.name + 95 | ' [' + options.coin.symbol.toUpperCase() + '] {' + options.coin.algorithm + '}'; 96 | if (process.env.forkId && process.env.forkId !== '0'){ 97 | emitLog(startMessage); 98 | return; 99 | } 100 | var infoLines = [startMessage, 101 | 'Network Connected:\t' + (options.testnet ? 'Testnet' : 'Mainnet'), 102 | 'Detected Reward Type:\t' + options.coin.reward, 103 | 'Current Block Height:\t' + _this.jobManager.currentJob.rpcData.height, 104 | 'Current Connect Peers:\t' + options.initStats.connections, 105 | 'Current Block Diff:\t' + _this.jobManager.currentJob.difficulty * algos[options.coin.algorithm].multiplier, 106 | 'Network Difficulty:\t' + options.initStats.difficulty, 107 | 'Network Hash Rate:\t' + util.getReadableHashRateString(options.initStats.networkHashRate), 108 | 'Stratum Port(s):\t' + _this.options.initStats.stratumPorts.join(', '), 109 | 'Pool Fee Percent:\t' + _this.options.feePercent + '%' 110 | ]; 111 | 112 | if (typeof options.blockRefreshInterval === "number" && options.blockRefreshInterval > 0) 113 | infoLines.push('Block polling every:\t' + options.blockRefreshInterval + ' ms'); 114 | 115 | emitSpecialLog(infoLines.join('\n\t\t\t\t\t\t')); 116 | } 117 | 118 | 119 | function OnBlockchainSynced(syncedCallback){ 120 | 121 | var checkSynced = function(displayNotSynced){ 122 | _this.daemon.cmd('getblocktemplate', [], function(results){ 123 | var synced = results.every(function(r){ 124 | return !r.error || r.error.code !== -10; 125 | }); 126 | if (synced){ 127 | syncedCallback(); 128 | } 129 | else{ 130 | if (displayNotSynced) displayNotSynced(); 131 | setTimeout(checkSynced, 5000); 132 | 133 | //Only let the first fork show synced status or the log wil look flooded with it 134 | if (!process.env.forkId || process.env.forkId === '0') 135 | generateProgress(); 136 | } 137 | 138 | }); 139 | }; 140 | checkSynced(function(){ 141 | //Only let the first fork show synced status or the log wil look flooded with it 142 | if (!process.env.forkId || process.env.forkId === '0') 143 | emitErrorLog('Daemon is still syncing with network (download blockchain) - server will be started once synced'); 144 | }); 145 | 146 | 147 | var generateProgress = function(){ 148 | 149 | _this.daemon.cmd('getinfo', [], function(results) { 150 | var blockCount = results.sort(function (a, b) { 151 | return b.response.blocks - a.response.blocks; 152 | })[0].response.blocks; 153 | 154 | //get list of peers and their highest block height to compare to ours 155 | _this.daemon.cmd('getpeerinfo', [], function(results){ 156 | 157 | var peers = results[0].response; 158 | var totalBlocks = peers.sort(function(a, b){ 159 | return b.startingheight - a.startingheight; 160 | })[0].startingheight; 161 | 162 | var percent = (blockCount / totalBlocks * 100).toFixed(2); 163 | emitWarningLog('Downloaded ' + percent + '% of blockchain from ' + peers.length + ' peers'); 164 | }); 165 | 166 | }); 167 | }; 168 | 169 | } 170 | 171 | 172 | function SetupApi() { 173 | if (typeof(options.api) !== 'object' || typeof(options.api.start) !== 'function') { 174 | return; 175 | } else { 176 | options.api.start(_this); 177 | } 178 | } 179 | 180 | 181 | function SetupPeer(){ 182 | if (!options.p2p || !options.p2p.enabled) 183 | return; 184 | 185 | if (options.testnet && !options.coin.peerMagicTestnet){ 186 | emitErrorLog('p2p cannot be enabled in testnet without peerMagicTestnet set in coin configuration'); 187 | return; 188 | } 189 | else if (!options.coin.peerMagic){ 190 | emitErrorLog('p2p cannot be enabled without peerMagic set in coin configuration'); 191 | return; 192 | } 193 | 194 | _this.peer = new peer(options); 195 | _this.peer.on('connected', function() { 196 | emitLog('p2p connection successful'); 197 | }).on('connectionRejected', function(){ 198 | emitErrorLog('p2p connection failed - likely incorrect p2p magic value'); 199 | }).on('disconnected', function(){ 200 | emitWarningLog('p2p peer node disconnected - attempting reconnection...'); 201 | }).on('connectionFailed', function(e){ 202 | emitErrorLog('p2p connection failed - likely incorrect host or port'); 203 | }).on('socketError', function(e){ 204 | emitErrorLog('p2p had a socket error ' + JSON.stringify(e)); 205 | }).on('error', function(msg){ 206 | emitWarningLog('p2p had an error ' + msg); 207 | }).on('blockFound', function(hash){ 208 | _this.processBlockNotify(hash, 'p2p'); 209 | }); 210 | } 211 | 212 | 213 | function SetupVarDiff(){ 214 | _this.varDiff = {}; 215 | Object.keys(options.ports).forEach(function(port) { 216 | if (options.ports[port].varDiff) 217 | _this.setVarDiff(port, options.ports[port].varDiff); 218 | }); 219 | } 220 | 221 | 222 | /* 223 | Coin daemons either use submitblock or getblocktemplate for submitting new blocks 224 | */ 225 | function SubmitBlock(blockHex, callback){ 226 | 227 | var rpcCommand, rpcArgs; 228 | if (options.hasSubmitMethod){ 229 | rpcCommand = 'submitblock'; 230 | rpcArgs = [blockHex]; 231 | } 232 | else{ 233 | rpcCommand = 'getblocktemplate'; 234 | rpcArgs = [{'mode': 'submit', 'data': blockHex}]; 235 | } 236 | 237 | 238 | _this.daemon.cmd(rpcCommand, 239 | rpcArgs, 240 | function(results){ 241 | for (var i = 0; i < results.length; i++){ 242 | var result = results[i]; 243 | if (result.error) { 244 | emitErrorLog('rpc error with daemon instance ' + 245 | result.instance.index + ' when submitting block with ' + rpcCommand + ' ' + 246 | JSON.stringify(result.error) 247 | ); 248 | return; 249 | } 250 | else if (result.response === 'rejected') { 251 | emitErrorLog('Daemon instance ' + result.instance.index + ' rejected a supposedly valid block'); 252 | return; 253 | } 254 | } 255 | emitLog('Submitted Block using ' + rpcCommand + ' successfully to daemon instance(s)'); 256 | callback(); 257 | } 258 | ); 259 | 260 | } 261 | 262 | 263 | function SetupRecipients(){ 264 | var recipients = []; 265 | options.feePercent = 0; 266 | options.rewardRecipients = options.rewardRecipients || {}; 267 | for (var r in options.rewardRecipients){ 268 | var percent = options.rewardRecipients[r]; 269 | var rObj = { 270 | percent: percent / 100 271 | }; 272 | try { 273 | if (r.length === 40) 274 | rObj.script = util.miningKeyToScript(r); 275 | else 276 | rObj.script = util.addressToScript(r); 277 | recipients.push(rObj); 278 | options.feePercent += percent; 279 | } 280 | catch(e){ 281 | emitErrorLog('Error generating transaction output script for ' + r + ' in rewardRecipients'); 282 | } 283 | } 284 | if (recipients.length === 0){ 285 | emitErrorLog('No rewardRecipients have been setup which means no fees will be taken'); 286 | } 287 | options.recipients = recipients; 288 | } 289 | 290 | function SetupJobManager(){ 291 | 292 | _this.jobManager = new jobManager(options); 293 | 294 | _this.jobManager.on('newBlock', function(blockTemplate){ 295 | //Check if stratumServer has been initialized yet 296 | if (_this.stratumServer) { 297 | _this.stratumServer.broadcastMiningJobs(blockTemplate.getJobParams()); 298 | } 299 | }).on('updatedBlock', function(blockTemplate){ 300 | //Check if stratumServer has been initialized yet 301 | if (_this.stratumServer) { 302 | var job = blockTemplate.getJobParams(); 303 | job[8] = false; 304 | _this.stratumServer.broadcastMiningJobs(job); 305 | } 306 | }).on('share', function(shareData, blockHex){ 307 | var isValidShare = !shareData.error; 308 | var isValidBlock = !!blockHex; 309 | var emitShare = function(){ 310 | _this.emit('share', isValidShare, isValidBlock, shareData); 311 | }; 312 | 313 | /* 314 | If we calculated that the block solution was found, 315 | before we emit the share, lets submit the block, 316 | then check if it was accepted using RPC getblock 317 | */ 318 | if (!isValidBlock) 319 | emitShare(); 320 | else{ 321 | SubmitBlock(blockHex, function(){ 322 | CheckBlockAccepted(shareData.blockHash, function(isAccepted, tx){ 323 | isValidBlock = isAccepted; 324 | shareData.txHash = tx; 325 | emitShare(); 326 | 327 | GetBlockTemplate(function(error, result, foundNewBlock){ 328 | if (foundNewBlock) 329 | emitLog('Block notification via RPC after block submission'); 330 | }); 331 | 332 | }); 333 | }); 334 | } 335 | }).on('log', function(severity, message){ 336 | _this.emit('log', severity, message); 337 | }); 338 | } 339 | 340 | 341 | function SetupDaemonInterface(finishedCallback){ 342 | 343 | if (!Array.isArray(options.daemons) || options.daemons.length < 1){ 344 | emitErrorLog('No daemons have been configured - pool cannot start'); 345 | return; 346 | } 347 | 348 | _this.daemon = new daemon.interface(options.daemons, function(severity, message){ 349 | _this.emit('log', severity , message); 350 | }); 351 | 352 | _this.daemon.once('online', function(){ 353 | finishedCallback(); 354 | 355 | }).on('connectionFailed', function(error){ 356 | emitErrorLog('Failed to connect daemon(s): ' + JSON.stringify(error)); 357 | 358 | }).on('error', function(message){ 359 | emitErrorLog(message); 360 | 361 | }); 362 | 363 | _this.daemon.init(); 364 | } 365 | 366 | 367 | function DetectCoinData(finishedCallback){ 368 | 369 | var batchRpcCalls = [ 370 | ['validateaddress', [options.address]], 371 | ['getdifficulty', []], 372 | ['getinfo', []], 373 | ['getmininginfo', []], 374 | ['submitblock', []] 375 | ]; 376 | 377 | _this.daemon.batchCmd(batchRpcCalls, function(error, results){ 378 | if (error || !results){ 379 | emitErrorLog('Could not start pool, error with init batch RPC call: ' + JSON.stringify(error)); 380 | return; 381 | } 382 | 383 | var rpcResults = {}; 384 | 385 | for (var i = 0; i < results.length; i++){ 386 | var rpcCall = batchRpcCalls[i][0]; 387 | var r = results[i]; 388 | rpcResults[rpcCall] = r.result || r.error; 389 | 390 | if (rpcCall !== 'submitblock' && (r.error || !r.result)){ 391 | emitErrorLog('Could not start pool, error with init RPC ' + rpcCall + ' - ' + JSON.stringify(r.error)); 392 | return; 393 | } 394 | } 395 | 396 | if (!rpcResults.validateaddress.isvalid){ 397 | emitErrorLog('Daemon reports address is not valid'); 398 | return; 399 | } 400 | 401 | if (isNaN(rpcResults.getdifficulty) && 'proof-of-stake' in rpcResults.getdifficulty) 402 | options.coin.reward = 'POS'; 403 | else 404 | options.coin.reward = 'POW'; 405 | 406 | 407 | /* POS coins must use the pubkey in coinbase transaction, and pubkey is 408 | only given if address is owned by wallet.*/ 409 | if (options.coin.reward === 'POS' && typeof(rpcResults.validateaddress.pubkey) == 'undefined') { 410 | emitErrorLog('The address provided is not from the daemon wallet - this is required for POS coins.'); 411 | return; 412 | } 413 | 414 | options.poolAddressScript = (function(){ 415 | switch(options.coin.reward){ 416 | case 'POS': 417 | return util.pubkeyToScript(rpcResults.validateaddress.pubkey); 418 | case 'POW': 419 | return util.addressToScript(rpcResults.validateaddress.address); 420 | } 421 | })(); 422 | 423 | options.testnet = rpcResults.getinfo.testnet; 424 | options.protocolVersion = rpcResults.getinfo.protocolversion; 425 | 426 | options.initStats = { 427 | connections: rpcResults.getinfo.connections, 428 | difficulty: rpcResults.getinfo.difficulty * algos[options.coin.algorithm].multiplier, 429 | networkHashRate: rpcResults.getmininginfo.networkhashps 430 | }; 431 | 432 | 433 | if (rpcResults.submitblock.message === 'Method not found'){ 434 | options.hasSubmitMethod = false; 435 | } 436 | else if (rpcResults.submitblock.code === -1){ 437 | options.hasSubmitMethod = true; 438 | } 439 | else { 440 | emitErrorLog('Could not detect block submission RPC method, ' + JSON.stringify(results)); 441 | return; 442 | } 443 | 444 | finishedCallback(); 445 | 446 | }); 447 | } 448 | 449 | 450 | 451 | function StartStratumServer(finishedCallback){ 452 | _this.stratumServer = new stratum.Server(options, authorizeFn); 453 | 454 | _this.stratumServer.on('started', function(){ 455 | options.initStats.stratumPorts = Object.keys(options.ports); 456 | _this.stratumServer.broadcastMiningJobs(_this.jobManager.currentJob.getJobParams()); 457 | finishedCallback(); 458 | 459 | }).on('broadcastTimeout', function(){ 460 | emitLog('No new blocks for ' + options.jobRebroadcastTimeout + ' seconds - updating transactions & rebroadcasting work'); 461 | 462 | GetBlockTemplate(function(error, rpcData, processedBlock){ 463 | if (error || processedBlock) return; 464 | _this.jobManager.updateCurrentJob(rpcData); 465 | }); 466 | 467 | }).on('client.connected', function(client){ 468 | if (typeof(_this.varDiff[client.socket.localPort]) !== 'undefined') { 469 | _this.varDiff[client.socket.localPort].manageClient(client); 470 | } 471 | 472 | client.on('difficultyChanged', function(diff){ 473 | _this.emit('difficultyUpdate', client.workerName, diff); 474 | 475 | }).on('subscription', function(params, resultCallback){ 476 | 477 | var extraNonce = _this.jobManager.extraNonceCounter.next(); 478 | var extraNonce2Size = _this.jobManager.extraNonce2Size; 479 | resultCallback(null, 480 | extraNonce, 481 | extraNonce2Size 482 | ); 483 | 484 | if (typeof(options.ports[client.socket.localPort]) !== 'undefined' && options.ports[client.socket.localPort].diff) { 485 | this.sendDifficulty(options.ports[client.socket.localPort].diff); 486 | } else { 487 | this.sendDifficulty(8); 488 | } 489 | 490 | this.sendMiningJob(_this.jobManager.currentJob.getJobParams()); 491 | 492 | }).on('submit', function(params, resultCallback){ 493 | var result =_this.jobManager.processShare( 494 | params.jobId, 495 | client.previousDifficulty, 496 | client.difficulty, 497 | client.extraNonce1, 498 | params.extraNonce2, 499 | params.nTime, 500 | params.nonce, 501 | client.remoteAddress, 502 | client.socket.localPort, 503 | params.name 504 | ); 505 | 506 | resultCallback(result.error, result.result ? true : null); 507 | 508 | }).on('malformedMessage', function (message) { 509 | emitWarningLog('Malformed message from ' + client.getLabel() + ': ' + message); 510 | 511 | }).on('socketError', function(err) { 512 | emitWarningLog('Socket error from ' + client.getLabel() + ': ' + JSON.stringify(err)); 513 | 514 | }).on('socketTimeout', function(reason){ 515 | emitWarningLog('Connected timed out for ' + client.getLabel() + ': ' + reason) 516 | 517 | }).on('socketDisconnect', function() { 518 | //emitLog('Socket disconnected from ' + client.getLabel()); 519 | 520 | }).on('kickedBannedIP', function(remainingBanTime){ 521 | emitLog('Rejected incoming connection from ' + client.remoteAddress + ' banned for ' + remainingBanTime + ' more seconds'); 522 | 523 | }).on('forgaveBannedIP', function(){ 524 | emitLog('Forgave banned IP ' + client.remoteAddress); 525 | 526 | }).on('unknownStratumMethod', function(fullMessage) { 527 | emitLog('Unknown stratum method from ' + client.getLabel() + ': ' + fullMessage.method); 528 | 529 | }).on('socketFlooded', function() { 530 | emitWarningLog('Detected socket flooding from ' + client.getLabel()); 531 | 532 | }).on('tcpProxyError', function(data) { 533 | emitErrorLog('Client IP detection failed, tcpProxyProtocol is enabled yet did not receive proxy protocol message, instead got data: ' + data); 534 | 535 | }).on('bootedBannedWorker', function(){ 536 | emitWarningLog('Booted worker ' + client.getLabel() + ' who was connected from an IP address that was just banned'); 537 | 538 | }).on('triggerBan', function(reason){ 539 | emitWarningLog('Banned triggered for ' + client.getLabel() + ': ' + reason); 540 | _this.emit('banIP', client.remoteAddress, client.workerName); 541 | }); 542 | }); 543 | } 544 | 545 | 546 | 547 | function SetupBlockPolling(){ 548 | if (typeof options.blockRefreshInterval !== "number" || options.blockRefreshInterval <= 0){ 549 | emitLog('Block template polling has been disabled'); 550 | return; 551 | } 552 | 553 | var pollingInterval = options.blockRefreshInterval; 554 | 555 | blockPollingIntervalId = setInterval(function () { 556 | GetBlockTemplate(function(error, result, foundNewBlock){ 557 | if (foundNewBlock) 558 | emitLog('Block notification via RPC polling'); 559 | }); 560 | }, pollingInterval); 561 | } 562 | 563 | 564 | 565 | function GetBlockTemplate(callback){ 566 | _this.daemon.cmd('getblocktemplate', 567 | [{"capabilities": [ "coinbasetxn", "workid", "coinbase/append" ]}], 568 | function(result){ 569 | if (result.error){ 570 | emitErrorLog('getblocktemplate call failed for daemon instance ' + 571 | result.instance.index + ' with error ' + JSON.stringify(result.error)); 572 | callback(result.error); 573 | } else { 574 | var processedNewBlock = _this.jobManager.processTemplate(result.response); 575 | callback(null, result.response, processedNewBlock); 576 | callback = function(){}; 577 | } 578 | }, true 579 | ); 580 | } 581 | 582 | 583 | 584 | function CheckBlockAccepted(blockHash, callback){ 585 | //setTimeout(function(){ 586 | _this.daemon.cmd('getblock', 587 | [blockHash], 588 | function(results){ 589 | var validResults = results.filter(function(result){ 590 | return result.response && (result.response.hash === blockHash) 591 | }); 592 | 593 | if (validResults.length >= 1){ 594 | callback(true, validResults[0].response.tx[0]); 595 | } 596 | else{ 597 | callback(false); 598 | } 599 | } 600 | ); 601 | //}, 500); 602 | } 603 | 604 | 605 | 606 | /** 607 | * This method is being called from the blockNotify so that when a new block is discovered by the daemon 608 | * We can inform our miners about the newly found block 609 | **/ 610 | this.processBlockNotify = function(blockHash, sourceTrigger) { 611 | emitLog('Block notification via ' + sourceTrigger); 612 | if (typeof(_this.jobManager.currentJob) !== 'undefined' && blockHash !== _this.jobManager.currentJob.rpcData.previousblockhash){ 613 | GetBlockTemplate(function(error, result){ 614 | if (error) 615 | emitErrorLog('Block notify error getting block template for ' + options.coin.name); 616 | }) 617 | } 618 | }; 619 | 620 | 621 | this.relinquishMiners = function(filterFn, resultCback) { 622 | var origStratumClients = this.stratumServer.getStratumClients(); 623 | 624 | var stratumClients = []; 625 | Object.keys(origStratumClients).forEach(function (subId) { 626 | stratumClients.push({subId: subId, client: origStratumClients[subId]}); 627 | }); 628 | async.filter( 629 | stratumClients, 630 | filterFn, 631 | function (clientsToRelinquish) { 632 | clientsToRelinquish.forEach(function(cObj) { 633 | cObj.client.removeAllListeners(); 634 | _this.stratumServer.removeStratumClientBySubId(cObj.subId); 635 | }); 636 | 637 | process.nextTick(function () { 638 | resultCback( 639 | clientsToRelinquish.map( 640 | function (item) { 641 | return item.client; 642 | } 643 | ) 644 | ); 645 | }); 646 | } 647 | ) 648 | }; 649 | 650 | 651 | this.attachMiners = function(miners) { 652 | miners.forEach(function (clientObj) { 653 | _this.stratumServer.manuallyAddStratumClient(clientObj); 654 | }); 655 | _this.stratumServer.broadcastMiningJobs(_this.jobManager.currentJob.getJobParams()); 656 | 657 | }; 658 | 659 | 660 | this.getStratumServer = function() { 661 | return _this.stratumServer; 662 | }; 663 | 664 | 665 | this.setVarDiff = function(port, varDiffConfig) { 666 | if (typeof(_this.varDiff[port]) != 'undefined' ) { 667 | _this.varDiff[port].removeAllListeners(); 668 | } 669 | var varDiffInstance = new varDiff(port, varDiffConfig); 670 | _this.varDiff[port] = varDiffInstance; 671 | _this.varDiff[port].on('newDifficulty', function(client, newDiff) { 672 | 673 | /* We request to set the newDiff @ the next difficulty retarget 674 | (which should happen when a new job comes in - AKA BLOCK) */ 675 | client.enqueueNextDifficulty(newDiff); 676 | 677 | /*if (options.varDiff.mode === 'fast'){ 678 | //Send new difficulty, then force miner to use new diff by resending the 679 | //current job parameters but with the "clean jobs" flag set to false 680 | //so the miner doesn't restart work and submit duplicate shares 681 | client.sendDifficulty(newDiff); 682 | var job = _this.jobManager.currentJob.getJobParams(); 683 | job[8] = false; 684 | client.sendMiningJob(job); 685 | }*/ 686 | 687 | }); 688 | }; 689 | 690 | }; 691 | pool.prototype.__proto__ = events.EventEmitter.prototype; 692 | -------------------------------------------------------------------------------- /lib/stratum.js: -------------------------------------------------------------------------------- 1 | var net = require('net'); 2 | var events = require('events'); 3 | 4 | var util = require('./util.js'); 5 | 6 | 7 | var SubscriptionCounter = function(){ 8 | var count = 0; 9 | var padding = 'deadbeefcafebabe'; 10 | return { 11 | next: function(){ 12 | count++; 13 | if (Number.MAX_VALUE === count) count = 0; 14 | return padding + util.packInt64LE(count).toString('hex'); 15 | } 16 | }; 17 | }; 18 | 19 | 20 | /** 21 | * Defining each client that connects to the stratum server. 22 | * Emits: 23 | * - subscription(obj, cback(error, extraNonce1, extraNonce2Size)) 24 | * - submit(data(name, jobID, extraNonce2, ntime, nonce)) 25 | **/ 26 | var StratumClient = function(options){ 27 | var pendingDifficulty = null; 28 | //private members 29 | this.socket = options.socket; 30 | 31 | this.remoteAddress = options.socket.remoteAddress; 32 | 33 | var banning = options.banning; 34 | 35 | var _this = this; 36 | 37 | this.lastActivity = Date.now(); 38 | 39 | this.shares = {valid: 0, invalid: 0}; 40 | 41 | var considerBan = (!banning || !banning.enabled) ? function(){ return false } : function(shareValid){ 42 | if (shareValid === true) _this.shares.valid++; 43 | else _this.shares.invalid++; 44 | var totalShares = _this.shares.valid + _this.shares.invalid; 45 | if (totalShares >= banning.checkThreshold){ 46 | var percentBad = (_this.shares.invalid / totalShares) * 100; 47 | if (percentBad < banning.invalidPercent) //reset shares 48 | this.shares = {valid: 0, invalid: 0}; 49 | else { 50 | _this.emit('triggerBan', _this.shares.invalid + ' out of the last ' + totalShares + ' shares were invalid'); 51 | _this.socket.destroy(); 52 | return true; 53 | } 54 | } 55 | return false; 56 | }; 57 | 58 | this.init = function init(){ 59 | setupSocket(); 60 | }; 61 | 62 | function handleMessage(message){ 63 | switch(message.method){ 64 | case 'mining.subscribe': 65 | handleSubscribe(message); 66 | break; 67 | case 'mining.authorize': 68 | handleAuthorize(message, true /*reply to socket*/); 69 | break; 70 | case 'mining.submit': 71 | _this.lastActivity = Date.now(); 72 | handleSubmit(message); 73 | break; 74 | case 'mining.get_transactions': 75 | sendJson({ 76 | id : null, 77 | result : [], 78 | error : true 79 | }); 80 | break; 81 | default: 82 | _this.emit('unknownStratumMethod', message); 83 | break; 84 | } 85 | } 86 | 87 | function handleSubscribe(message){ 88 | if (! _this._authorized ) { 89 | _this.requestedSubscriptionBeforeAuth = true; 90 | } 91 | _this.emit('subscription', 92 | {}, 93 | function(error, extraNonce1, extraNonce2Size){ 94 | if (error){ 95 | sendJson({ 96 | id: message.id, 97 | result: null, 98 | error: error 99 | }); 100 | return; 101 | } 102 | _this.extraNonce1 = extraNonce1; 103 | sendJson({ 104 | id: message.id, 105 | result: [ 106 | [ 107 | ["mining.set_difficulty", options.subscriptionId], 108 | ["mining.notify", options.subscriptionId] 109 | ], 110 | extraNonce1, 111 | extraNonce2Size 112 | ], 113 | error: null 114 | }); 115 | } 116 | ); 117 | } 118 | 119 | function handleAuthorize(message, replyToSocket){ 120 | _this.workerName = message.params[0]; 121 | _this.workerPass = message.params[1]; 122 | options.authorizeFn(_this.remoteAddress, options.socket.localPort, _this.workerName, _this.workerPass, function(result) { 123 | _this.authorized = (!result.error && result.authorized); 124 | 125 | if (replyToSocket) { 126 | sendJson({ 127 | id : message.id, 128 | result : _this.authorized, 129 | error : result.error 130 | }); 131 | } 132 | 133 | // If the authorizer wants us to close the socket lets do it. 134 | if (result.disconnect === true) { 135 | options.socket.destroy(); 136 | } 137 | }); 138 | } 139 | 140 | function handleSubmit(message){ 141 | if (!_this.authorized){ 142 | sendJson({ 143 | id : message.id, 144 | result: null, 145 | error : [24, "unauthorized worker", null] 146 | }); 147 | considerBan(false); 148 | return; 149 | } 150 | if (!_this.extraNonce1){ 151 | sendJson({ 152 | id : message.id, 153 | result: null, 154 | error : [25, "not subscribed", null] 155 | }); 156 | considerBan(false); 157 | return; 158 | } 159 | _this.emit('submit', 160 | { 161 | name : message.params[0], 162 | jobId : message.params[1], 163 | extraNonce2 : message.params[2], 164 | nTime : message.params[3], 165 | nonce : message.params[4] 166 | }, 167 | function(error, result){ 168 | if (!considerBan(result)){ 169 | sendJson({ 170 | id: message.id, 171 | result: result, 172 | error: error 173 | }); 174 | } 175 | } 176 | ); 177 | 178 | } 179 | 180 | function sendJson(){ 181 | var response = ''; 182 | for (var i = 0; i < arguments.length; i++){ 183 | response += JSON.stringify(arguments[i]) + '\n'; 184 | } 185 | options.socket.write(response); 186 | } 187 | 188 | function setupSocket(){ 189 | var socket = options.socket; 190 | var dataBuffer = ''; 191 | socket.setEncoding('utf8'); 192 | 193 | if (options.tcpProxyProtocol === true) { 194 | socket.once('data', function (d) { 195 | if (d.indexOf('PROXY') === 0) { 196 | _this.remoteAddress = d.split(' ')[2]; 197 | } 198 | else{ 199 | _this.emit('tcpProxyError', d); 200 | } 201 | _this.emit('checkBan'); 202 | }); 203 | } 204 | else{ 205 | _this.emit('checkBan'); 206 | } 207 | socket.on('data', function(d){ 208 | dataBuffer += d; 209 | if (Buffer.byteLength(dataBuffer, 'utf8') > 10240){ //10KB 210 | dataBuffer = ''; 211 | _this.emit('socketFlooded'); 212 | socket.destroy(); 213 | return; 214 | } 215 | if (dataBuffer.indexOf('\n') !== -1){ 216 | var messages = dataBuffer.split('\n'); 217 | var incomplete = dataBuffer.slice(-1) === '\n' ? '' : messages.pop(); 218 | messages.forEach(function(message){ 219 | if (message === '') return; 220 | var messageJson; 221 | try { 222 | messageJson = JSON.parse(message); 223 | } catch(e) { 224 | if (options.tcpProxyProtocol !== true || d.indexOf('PROXY') !== 0){ 225 | _this.emit('malformedMessage', message); 226 | socket.destroy(); 227 | } 228 | return; 229 | } 230 | 231 | if (messageJson) { 232 | handleMessage(messageJson); 233 | } 234 | }); 235 | dataBuffer = incomplete; 236 | } 237 | }); 238 | socket.on('close', function() { 239 | _this.emit('socketDisconnect'); 240 | }); 241 | socket.on('error', function(err){ 242 | if (err.code !== 'ECONNRESET') 243 | _this.emit('socketError', err); 244 | }); 245 | } 246 | 247 | 248 | this.getLabel = function(){ 249 | return (_this.workerName || '(unauthorized)') + ' [' + _this.remoteAddress + ']'; 250 | }; 251 | 252 | this.enqueueNextDifficulty = function(requestedNewDifficulty) { 253 | pendingDifficulty = requestedNewDifficulty; 254 | return true; 255 | }; 256 | 257 | //public members 258 | 259 | /** 260 | * IF the given difficulty is valid and new it'll send it to the client. 261 | * returns boolean 262 | **/ 263 | this.sendDifficulty = function(difficulty){ 264 | if (difficulty === this.difficulty) 265 | return false; 266 | 267 | _this.previousDifficulty = _this.difficulty; 268 | _this.difficulty = difficulty; 269 | sendJson({ 270 | id : null, 271 | method: "mining.set_difficulty", 272 | params: [difficulty]//[512], 273 | }); 274 | return true; 275 | }; 276 | 277 | this.sendMiningJob = function(jobParams){ 278 | 279 | var lastActivityAgo = Date.now() - _this.lastActivity; 280 | if (lastActivityAgo > options.connectionTimeout * 1000){ 281 | _this.emit('socketTimeout', 'last submitted a share was ' + (lastActivityAgo / 1000 | 0) + ' seconds ago'); 282 | _this.socket.destroy(); 283 | return; 284 | } 285 | 286 | if (pendingDifficulty !== null){ 287 | var result = _this.sendDifficulty(pendingDifficulty); 288 | pendingDifficulty = null; 289 | if (result) { 290 | _this.emit('difficultyChanged', _this.difficulty); 291 | } 292 | } 293 | sendJson({ 294 | id : null, 295 | method: "mining.notify", 296 | params: jobParams 297 | }); 298 | 299 | }; 300 | 301 | this.manuallyAuthClient = function (username, password) { 302 | handleAuthorize({id: 1, params: [username, password]}, false /*do not reply to miner*/); 303 | }; 304 | 305 | this.manuallySetValues = function (otherClient) { 306 | _this.extraNonce1 = otherClient.extraNonce1; 307 | _this.previousDifficulty = otherClient.previousDifficulty; 308 | _this.difficulty = otherClient.difficulty; 309 | }; 310 | }; 311 | StratumClient.prototype.__proto__ = events.EventEmitter.prototype; 312 | 313 | 314 | 315 | 316 | /** 317 | * The actual stratum server. 318 | * It emits the following Events: 319 | * - 'client.connected'(StratumClientInstance) - when a new miner connects 320 | * - 'client.disconnected'(StratumClientInstance) - when a miner disconnects. Be aware that the socket cannot be used anymore. 321 | * - 'started' - when the server is up and running 322 | **/ 323 | var StratumServer = exports.Server = function StratumServer(options, authorizeFn){ 324 | 325 | //private members 326 | 327 | //ports, connectionTimeout, jobRebroadcastTimeout, banning, haproxy, authorizeFn 328 | 329 | var bannedMS = options.banning ? options.banning.time * 1000 : null; 330 | 331 | var _this = this; 332 | var stratumClients = {}; 333 | var subscriptionCounter = SubscriptionCounter(); 334 | var rebroadcastTimeout; 335 | var bannedIPs = {}; 336 | 337 | 338 | function checkBan(client){ 339 | if (options.banning && options.banning.enabled && client.remoteAddress in bannedIPs){ 340 | var bannedTime = bannedIPs[client.remoteAddress]; 341 | var bannedTimeAgo = Date.now() - bannedTime; 342 | var timeLeft = bannedMS - bannedTimeAgo; 343 | if (timeLeft > 0){ 344 | client.socket.destroy(); 345 | client.emit('kickedBannedIP', timeLeft / 1000 | 0); 346 | } 347 | else { 348 | delete bannedIPs[client.remoteAddress]; 349 | client.emit('forgaveBannedIP'); 350 | } 351 | } 352 | } 353 | 354 | this.handleNewClient = function (socket){ 355 | 356 | socket.setKeepAlive(true); 357 | var subscriptionId = subscriptionCounter.next(); 358 | var client = new StratumClient( 359 | { 360 | subscriptionId: subscriptionId, 361 | authorizeFn: authorizeFn, 362 | socket: socket, 363 | banning: options.banning, 364 | connectionTimeout: options.connectionTimeout, 365 | tcpProxyProtocol: options.tcpProxyProtocol 366 | } 367 | ); 368 | 369 | stratumClients[subscriptionId] = client; 370 | _this.emit('client.connected', client); 371 | client.on('socketDisconnect', function() { 372 | _this.removeStratumClientBySubId(subscriptionId); 373 | _this.emit('client.disconnected', client); 374 | }).on('checkBan', function(){ 375 | checkBan(client); 376 | }).on('triggerBan', function(){ 377 | _this.addBannedIP(client.remoteAddress); 378 | }).init(); 379 | return subscriptionId; 380 | }; 381 | 382 | 383 | this.broadcastMiningJobs = function(jobParams){ 384 | for (var clientId in stratumClients) { 385 | var client = stratumClients[clientId]; 386 | client.sendMiningJob(jobParams); 387 | } 388 | /* Some miners will consider the pool dead if it doesn't receive a job for around a minute. 389 | So every time we broadcast jobs, set a timeout to rebroadcast in X seconds unless cleared. */ 390 | clearTimeout(rebroadcastTimeout); 391 | rebroadcastTimeout = setTimeout(function(){ 392 | _this.emit('broadcastTimeout'); 393 | }, options.jobRebroadcastTimeout * 1000); 394 | }; 395 | 396 | 397 | 398 | (function init(){ 399 | 400 | //Interval to look through bannedIPs for old bans and remove them in order to prevent a memory leak 401 | if (options.banning && options.banning.enabled){ 402 | setInterval(function(){ 403 | for (ip in bannedIPs){ 404 | var banTime = bannedIPs[ip]; 405 | if (Date.now() - banTime > options.banning.time) 406 | delete bannedIPs[ip]; 407 | } 408 | }, 1000 * options.banning.purgeInterval); 409 | } 410 | 411 | 412 | //SetupBroadcasting(); 413 | 414 | 415 | var serversStarted = 0; 416 | Object.keys(options.ports).forEach(function(port){ 417 | net.createServer({allowHalfOpen: false}, function(socket) { 418 | _this.handleNewClient(socket); 419 | }).listen(parseInt(port), function() { 420 | serversStarted++; 421 | if (serversStarted == Object.keys(options.ports).length) 422 | _this.emit('started'); 423 | }); 424 | }); 425 | })(); 426 | 427 | 428 | //public members 429 | 430 | this.addBannedIP = function(ipAddress){ 431 | bannedIPs[ipAddress] = Date.now(); 432 | /*for (var c in stratumClients){ 433 | var client = stratumClients[c]; 434 | if (client.remoteAddress === ipAddress){ 435 | _this.emit('bootedBannedWorker'); 436 | } 437 | }*/ 438 | }; 439 | 440 | this.getStratumClients = function () { 441 | return stratumClients; 442 | }; 443 | 444 | this.removeStratumClientBySubId = function (subscriptionId) { 445 | delete stratumClients[subscriptionId]; 446 | }; 447 | 448 | this.manuallyAddStratumClient = function(clientObj) { 449 | var subId = _this.handleNewClient(clientObj.socket); 450 | if (subId != null) { // not banned! 451 | stratumClients[subId].manuallyAuthClient(clientObj.workerName, clientObj.workerPass); 452 | stratumClients[subId].manuallySetValues(clientObj); 453 | } 454 | }; 455 | 456 | }; 457 | StratumServer.prototype.__proto__ = events.EventEmitter.prototype; 458 | -------------------------------------------------------------------------------- /lib/transactions.js: -------------------------------------------------------------------------------- 1 | var util = require('./util.js'); 2 | 3 | 4 | /* 5 | function Transaction(params){ 6 | 7 | var version = params.version || 1, 8 | inputs = params.inputs || [], 9 | outputs = params.outputs || [], 10 | lockTime = params.lockTime || 0; 11 | 12 | 13 | this.toBuffer = function(){ 14 | return Buffer.concat([ 15 | binpack.packUInt32(version, 'little'), 16 | util.varIntBuffer(inputs.length), 17 | Buffer.concat(inputs.map(function(i){ return i.toBuffer() })), 18 | util.varIntBuffer(outputs.length), 19 | Buffer.concat(outputs.map(function(o){ return o.toBuffer() })), 20 | binpack.packUInt32(lockTime, 'little') 21 | ]); 22 | }; 23 | 24 | this.inputs = inputs; 25 | this.outputs = outputs; 26 | 27 | } 28 | 29 | function TransactionInput(params){ 30 | 31 | var prevOutHash = params.prevOutHash || 0, 32 | prevOutIndex = params.prevOutIndex, 33 | sigScript = params.sigScript, 34 | sequence = params.sequence || 0; 35 | 36 | 37 | this.toBuffer = function(){ 38 | sigScriptBuffer = sigScript.toBuffer(); 39 | console.log('scriptSig length ' + sigScriptBuffer.length); 40 | return Buffer.concat([ 41 | util.uint256BufferFromHash(prevOutHash), 42 | binpack.packUInt32(prevOutIndex, 'little'), 43 | util.varIntBuffer(sigScriptBuffer.length), 44 | sigScriptBuffer, 45 | binpack.packUInt32(sequence) 46 | ]); 47 | }; 48 | } 49 | 50 | function TransactionOutput(params){ 51 | 52 | var value = params.value, 53 | pkScriptBuffer = params.pkScriptBuffer; 54 | 55 | this.toBuffer = function(){ 56 | return Buffer.concat([ 57 | binpack.packInt64(value, 'little'), 58 | util.varIntBuffer(pkScriptBuffer.length), 59 | pkScriptBuffer 60 | ]); 61 | }; 62 | } 63 | 64 | function ScriptSig(params){ 65 | 66 | var height = params.height, 67 | flags = params.flags, 68 | extraNoncePlaceholder = params.extraNoncePlaceholder; 69 | 70 | this.toBuffer = function(){ 71 | 72 | return Buffer.concat([ 73 | util.serializeNumber(height), 74 | new Buffer(flags, 'hex'), 75 | util.serializeNumber(Date.now() / 1000 | 0), 76 | new Buffer([extraNoncePlaceholder.length]), 77 | extraNoncePlaceholder, 78 | util.serializeString('/nodeStratum/') 79 | ]); 80 | } 81 | }; 82 | 83 | 84 | var Generation = exports.Generation = function Generation(rpcData, publicKey, extraNoncePlaceholder){ 85 | 86 | var tx = new Transaction({ 87 | inputs: [new TransactionInput({ 88 | prevOutIndex : Math.pow(2, 32) - 1, 89 | sigScript : new ScriptSig({ 90 | height : rpcData.height, 91 | flags : rpcData.coinbaseaux.flags, 92 | extraNoncePlaceholder : extraNoncePlaceholder 93 | }) 94 | })], 95 | outputs: [new TransactionOutput({ 96 | value : rpcData.coinbasevalue, 97 | pkScriptBuffer : publicKey 98 | })] 99 | }); 100 | 101 | var txBuffer = tx.toBuffer(); 102 | var epIndex = buffertools.indexOf(txBuffer, extraNoncePlaceholder); 103 | var p1 = txBuffer.slice(0, epIndex); 104 | var p2 = txBuffer.slice(epIndex + extraNoncePlaceholder.length); 105 | 106 | this.transaction = tx; 107 | this.coinbase = [p1, p2]; 108 | 109 | }; 110 | */ 111 | 112 | 113 | /* 114 | ^^^^ The above code was a bit slow. The below code is uglier but optimized. 115 | */ 116 | 117 | 118 | 119 | /* 120 | This function creates the generation transaction that accepts the reward for 121 | successfully mining a new block. 122 | For some (probably outdated and incorrect) documentation about whats kinda going on here, 123 | see: https://en.bitcoin.it/wiki/Protocol_specification#tx 124 | */ 125 | 126 | var generateOutputTransactions = function(poolRecipient, recipients, rpcData){ 127 | 128 | var reward = rpcData.coinbasevalue; 129 | var rewardToPool = reward; 130 | 131 | var txOutputBuffers = []; 132 | 133 | 134 | /* Dash 12.1 */ 135 | if (rpcData.masternode && rpcData.superblock) { 136 | if (rpcData.masternode.payee) { 137 | var payeeReward = 0; 138 | 139 | payeeReward = rpcData.masternode.amount; 140 | reward -= payeeReward; 141 | rewardToPool -= payeeReward; 142 | 143 | var payeeScript = util.addressToScript(rpcData.masternode.payee); 144 | txOutputBuffers.push(Buffer.concat([ 145 | util.packInt64LE(payeeReward), 146 | util.varIntBuffer(payeeScript.length), 147 | payeeScript 148 | ])); 149 | } else if (rpcData.superblock.length > 0) { 150 | for(var i in rpcData.superblock){ 151 | var payeeReward = 0; 152 | 153 | payeeReward = rpcData.superblock[i].amount; 154 | reward -= payeeReward; 155 | rewardToPool -= payeeReward; 156 | 157 | var payeeScript = util.addressToScript(rpcData.superblock[i].payee); 158 | txOutputBuffers.push(Buffer.concat([ 159 | util.packInt64LE(payeeReward), 160 | util.varIntBuffer(payeeScript.length), 161 | payeeScript 162 | ])); 163 | } 164 | } 165 | } 166 | 167 | if (rpcData.payee) { 168 | var payeeReward = 0; 169 | 170 | if (rpcData.payee_amount) { 171 | payeeReward = rpcData.payee_amount; 172 | } else { 173 | payeeReward = Math.ceil(reward / 5); 174 | } 175 | 176 | reward -= payeeReward; 177 | rewardToPool -= payeeReward; 178 | 179 | var payeeScript = util.addressToScript(rpcData.payee); 180 | txOutputBuffers.push(Buffer.concat([ 181 | util.packInt64LE(payeeReward), 182 | util.varIntBuffer(payeeScript.length), 183 | payeeScript 184 | ])); 185 | } 186 | 187 | 188 | 189 | for (var i = 0; i < recipients.length; i++){ 190 | var recipientReward = Math.floor(recipients[i].percent * reward); 191 | rewardToPool -= recipientReward; 192 | 193 | txOutputBuffers.push(Buffer.concat([ 194 | util.packInt64LE(recipientReward), 195 | util.varIntBuffer(recipients[i].script.length), 196 | recipients[i].script 197 | ])); 198 | } 199 | 200 | 201 | txOutputBuffers.unshift(Buffer.concat([ 202 | util.packInt64LE(rewardToPool), 203 | util.varIntBuffer(poolRecipient.length), 204 | poolRecipient 205 | ])); 206 | 207 | 208 | return Buffer.concat([ 209 | util.varIntBuffer(txOutputBuffers.length), 210 | Buffer.concat(txOutputBuffers) 211 | ]); 212 | 213 | }; 214 | 215 | 216 | exports.CreateGeneration = function(rpcData, publicKey, extraNoncePlaceholder, reward, txMessages, recipients){ 217 | 218 | var txInputsCount = 1; 219 | var txOutputsCount = 1; 220 | var txVersion = txMessages === true ? 2 : 1; 221 | var txLockTime = 0; 222 | 223 | var txInPrevOutHash = 0; 224 | var txInPrevOutIndex = Math.pow(2, 32) - 1; 225 | var txInSequence = 0; 226 | 227 | //Only required for POS coins 228 | var txTimestamp = reward === 'POS' ? 229 | util.packUInt32LE(rpcData.curtime) : new Buffer([]); 230 | 231 | //For coins that support/require transaction comments 232 | var txComment = txMessages === true ? 233 | util.serializeString('https://github.com/zone117x/node-stratum') : 234 | new Buffer([]); 235 | 236 | 237 | var scriptSigPart1 = Buffer.concat([ 238 | util.serializeNumber(rpcData.height), 239 | new Buffer(rpcData.coinbaseaux.flags, 'hex'), 240 | util.serializeNumber(Date.now() / 1000 | 0), 241 | new Buffer([extraNoncePlaceholder.length]) 242 | ]); 243 | 244 | var scriptSigPart2 = util.serializeString('/nodeStratum/'); 245 | 246 | var p1 = Buffer.concat([ 247 | util.packUInt32LE(txVersion), 248 | txTimestamp, 249 | 250 | //transaction input 251 | util.varIntBuffer(txInputsCount), 252 | util.uint256BufferFromHash(txInPrevOutHash), 253 | util.packUInt32LE(txInPrevOutIndex), 254 | util.varIntBuffer(scriptSigPart1.length + extraNoncePlaceholder.length + scriptSigPart2.length), 255 | scriptSigPart1 256 | ]); 257 | 258 | 259 | /* 260 | The generation transaction must be split at the extranonce (which located in the transaction input 261 | scriptSig). Miners send us unique extranonces that we use to join the two parts in attempt to create 262 | a valid share and/or block. 263 | */ 264 | 265 | 266 | var outputTransactions = generateOutputTransactions(publicKey, recipients, rpcData); 267 | 268 | var p2 = Buffer.concat([ 269 | scriptSigPart2, 270 | util.packUInt32LE(txInSequence), 271 | //end transaction input 272 | 273 | //transaction output 274 | outputTransactions, 275 | //end transaction ouput 276 | 277 | util.packUInt32LE(txLockTime), 278 | txComment 279 | ]); 280 | 281 | return [p1, p2]; 282 | 283 | }; -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | var crypto = require('crypto'); 2 | 3 | var base58 = require('base58-native'); 4 | var bignum = require('bignum'); 5 | 6 | 7 | exports.addressFromEx = function(exAddress, ripdm160Key){ 8 | try { 9 | var versionByte = exports.getVersionByte(exAddress); 10 | var addrBase = Buffer.concat([versionByte, new Buffer(ripdm160Key, 'hex')]); 11 | var checksum = exports.sha256d(addrBase).slice(0, 4); 12 | var address = Buffer.concat([addrBase, checksum]); 13 | return base58.encode(address); 14 | } 15 | catch(e){ 16 | return null; 17 | } 18 | }; 19 | 20 | 21 | exports.getVersionByte = function(addr){ 22 | var versionByte = base58.decode(addr).slice(0, 1); 23 | return versionByte; 24 | }; 25 | 26 | exports.sha256 = function(buffer){ 27 | var hash1 = crypto.createHash('sha256'); 28 | hash1.update(buffer); 29 | return hash1.digest(); 30 | }; 31 | 32 | exports.sha256d = function(buffer){ 33 | return exports.sha256(exports.sha256(buffer)); 34 | }; 35 | 36 | exports.reverseBuffer = function(buff){ 37 | var reversed = new Buffer(buff.length); 38 | for (var i = buff.length - 1; i >= 0; i--) 39 | reversed[buff.length - i - 1] = buff[i]; 40 | return reversed; 41 | }; 42 | 43 | exports.reverseHex = function(hex){ 44 | return exports.reverseBuffer(new Buffer(hex, 'hex')).toString('hex'); 45 | }; 46 | 47 | exports.reverseByteOrder = function(buff){ 48 | for (var i = 0; i < 8; i++) buff.writeUInt32LE(buff.readUInt32BE(i * 4), i * 4); 49 | return exports.reverseBuffer(buff); 50 | }; 51 | 52 | exports.uint256BufferFromHash = function(hex){ 53 | 54 | var fromHex = new Buffer(hex, 'hex'); 55 | 56 | if (fromHex.length != 32){ 57 | var empty = new Buffer(32); 58 | empty.fill(0); 59 | fromHex.copy(empty); 60 | fromHex = empty; 61 | } 62 | 63 | return exports.reverseBuffer(fromHex); 64 | }; 65 | 66 | exports.hexFromReversedBuffer = function(buffer){ 67 | return exports.reverseBuffer(buffer).toString('hex'); 68 | }; 69 | 70 | 71 | /* 72 | Defined in bitcoin protocol here: 73 | https://en.bitcoin.it/wiki/Protocol_specification#Variable_length_integer 74 | */ 75 | exports.varIntBuffer = function(n){ 76 | if (n < 0xfd) 77 | return new Buffer([n]); 78 | else if (n < 0xffff){ 79 | var buff = new Buffer(3); 80 | buff[0] = 0xfd; 81 | buff.writeUInt16LE(n, 1); 82 | return buff; 83 | } 84 | else if (n < 0xffffffff){ 85 | var buff = new Buffer(5); 86 | buff[0] = 0xfe; 87 | buff.writeUInt32LE(n, 1); 88 | return buff; 89 | } 90 | else{ 91 | var buff = new Buffer(9); 92 | buff[0] = 0xff; 93 | exports.packUInt16LE(n).copy(buff, 1); 94 | return buff; 95 | } 96 | }; 97 | 98 | exports.varStringBuffer = function(string){ 99 | var strBuff = new Buffer(string); 100 | return Buffer.concat([exports.varIntBuffer(strBuff.length), strBuff]); 101 | }; 102 | 103 | /* 104 | "serialized CScript" formatting as defined here: 105 | https://github.com/bitcoin/bips/blob/master/bip-0034.mediawiki#specification 106 | Used to format height and date when putting into script signature: 107 | https://en.bitcoin.it/wiki/Script 108 | */ 109 | exports.serializeNumber = function(n){ 110 | 111 | /* Old version that is bugged 112 | if (n < 0xfd){ 113 | var buff = new Buffer(2); 114 | buff[0] = 0x1; 115 | buff.writeUInt8(n, 1); 116 | return buff; 117 | } 118 | else if (n <= 0xffff){ 119 | var buff = new Buffer(4); 120 | buff[0] = 0x3; 121 | buff.writeUInt16LE(n, 1); 122 | return buff; 123 | } 124 | else if (n <= 0xffffffff){ 125 | var buff = new Buffer(5); 126 | buff[0] = 0x4; 127 | buff.writeUInt32LE(n, 1); 128 | return buff; 129 | } 130 | else{ 131 | return Buffer.concat([new Buffer([0x9]), binpack.packUInt64(n, 'little')]); 132 | }*/ 133 | 134 | //New version from TheSeven 135 | if (n >= 1 && n <= 16) return new Buffer([0x50 + n]); 136 | var l = 1; 137 | var buff = new Buffer(9); 138 | while (n > 0x7f) 139 | { 140 | buff.writeUInt8(n & 0xff, l++); 141 | n >>= 8; 142 | } 143 | buff.writeUInt8(l, 0); 144 | buff.writeUInt8(n, l++); 145 | return buff.slice(0, l); 146 | 147 | }; 148 | 149 | 150 | /* 151 | Used for serializing strings used in script signature 152 | */ 153 | exports.serializeString = function(s){ 154 | 155 | if (s.length < 253) 156 | return Buffer.concat([ 157 | new Buffer([s.length]), 158 | new Buffer(s) 159 | ]); 160 | else if (s.length < 0x10000) 161 | return Buffer.concat([ 162 | new Buffer([253]), 163 | exports.packUInt16LE(s.length), 164 | new Buffer(s) 165 | ]); 166 | else if (s.length < 0x100000000) 167 | return Buffer.concat([ 168 | new Buffer([254]), 169 | exports.packUInt32LE(s.length), 170 | new Buffer(s) 171 | ]); 172 | else 173 | return Buffer.concat([ 174 | new Buffer([255]), 175 | exports.packUInt16LE(s.length), 176 | new Buffer(s) 177 | ]); 178 | }; 179 | 180 | 181 | 182 | exports.packUInt16LE = function(num){ 183 | var buff = new Buffer(2); 184 | buff.writeUInt16LE(num, 0); 185 | return buff; 186 | }; 187 | exports.packInt32LE = function(num){ 188 | var buff = new Buffer(4); 189 | buff.writeInt32LE(num, 0); 190 | return buff; 191 | }; 192 | exports.packInt32BE = function(num){ 193 | var buff = new Buffer(4); 194 | buff.writeInt32BE(num, 0); 195 | return buff; 196 | }; 197 | exports.packUInt32LE = function(num){ 198 | var buff = new Buffer(4); 199 | buff.writeUInt32LE(num, 0); 200 | return buff; 201 | }; 202 | exports.packUInt32BE = function(num){ 203 | var buff = new Buffer(4); 204 | buff.writeUInt32BE(num, 0); 205 | return buff; 206 | }; 207 | exports.packInt64LE = function(num){ 208 | var buff = new Buffer(8); 209 | buff.writeUInt32LE(num % Math.pow(2, 32), 0); 210 | buff.writeUInt32LE(Math.floor(num / Math.pow(2, 32)), 4); 211 | return buff; 212 | }; 213 | 214 | 215 | /* 216 | An exact copy of python's range feature. Written by Tadeck: 217 | http://stackoverflow.com/a/8273091 218 | */ 219 | exports.range = function(start, stop, step){ 220 | if (typeof stop === 'undefined'){ 221 | stop = start; 222 | start = 0; 223 | } 224 | if (typeof step === 'undefined'){ 225 | step = 1; 226 | } 227 | if ((step > 0 && start >= stop) || (step < 0 && start <= stop)){ 228 | return []; 229 | } 230 | var result = []; 231 | for (var i = start; step > 0 ? i < stop : i > stop; i += step){ 232 | result.push(i); 233 | } 234 | return result; 235 | }; 236 | 237 | 238 | 239 | 240 | /* 241 | For POS coins - used to format wallet address for use in generation transaction's output 242 | */ 243 | exports.pubkeyToScript = function(key){ 244 | if (key.length !== 66) { 245 | console.error('Invalid pubkey: ' + key); 246 | throw new Error(); 247 | } 248 | var pubkey = new Buffer(35); 249 | pubkey[0] = 0x21; 250 | pubkey[34] = 0xac; 251 | new Buffer(key, 'hex').copy(pubkey, 1); 252 | return pubkey; 253 | }; 254 | 255 | 256 | exports.miningKeyToScript = function(key){ 257 | var keyBuffer = new Buffer(key, 'hex'); 258 | return Buffer.concat([new Buffer([0x76, 0xa9, 0x14]), keyBuffer, new Buffer([0x88, 0xac])]); 259 | }; 260 | 261 | /* 262 | For POW coins - used to format wallet address for use in generation transaction's output 263 | */ 264 | exports.addressToScript = function(addr){ 265 | 266 | var decoded = base58.decode(addr); 267 | 268 | if (decoded.length != 25){ 269 | console.error('invalid address length for ' + addr); 270 | throw new Error(); 271 | } 272 | 273 | if (!decoded){ 274 | console.error('base58 decode failed for ' + addr); 275 | throw new Error(); 276 | } 277 | 278 | var pubkey = decoded.slice(1,-4); 279 | 280 | return Buffer.concat([new Buffer([0x76, 0xa9, 0x14]), pubkey, new Buffer([0x88, 0xac])]); 281 | }; 282 | 283 | 284 | exports.getReadableHashRateString = function(hashrate){ 285 | var i = -1; 286 | var byteUnits = [ ' KH', ' MH', ' GH', ' TH', ' PH' ]; 287 | do { 288 | hashrate = hashrate / 1024; 289 | i++; 290 | } while (hashrate > 1024); 291 | return hashrate.toFixed(2) + byteUnits[i]; 292 | }; 293 | 294 | 295 | 296 | 297 | //Creates a non-truncated max difficulty (diff1) by bitwise right-shifting the max value of a uint256 298 | exports.shiftMax256Right = function(shiftRight){ 299 | 300 | //Max value uint256 (an array of ones representing 256 enabled bits) 301 | var arr256 = Array.apply(null, new Array(256)).map(Number.prototype.valueOf, 1); 302 | 303 | //An array of zero bits for how far the max uint256 is shifted right 304 | var arrLeft = Array.apply(null, new Array(shiftRight)).map(Number.prototype.valueOf, 0); 305 | 306 | //Add zero bits to uint256 and remove the bits shifted out 307 | arr256 = arrLeft.concat(arr256).slice(0, 256); 308 | 309 | //An array of bytes to convert the bits to, 8 bits in a byte so length will be 32 310 | var octets = []; 311 | 312 | for (var i = 0; i < 32; i++){ 313 | 314 | octets[i] = 0; 315 | 316 | //The 8 bits for this byte 317 | var bits = arr256.slice(i * 8, i * 8 + 8); 318 | 319 | //Bit math to add the bits into a byte 320 | for (var f = 0; f < bits.length; f++){ 321 | var multiplier = Math.pow(2, f); 322 | octets[i] += bits[f] * multiplier; 323 | } 324 | 325 | } 326 | 327 | return new Buffer(octets); 328 | }; 329 | 330 | 331 | exports.bufferToCompactBits = function(startingBuff){ 332 | var bigNum = bignum.fromBuffer(startingBuff); 333 | var buff = bigNum.toBuffer(); 334 | 335 | buff = buff.readUInt8(0) > 0x7f ? Buffer.concat([new Buffer([0x00]), buff]) : buff; 336 | 337 | buff = Buffer.concat([new Buffer([buff.length]), buff]); 338 | var compact = buff.slice(0, 4); 339 | return compact; 340 | }; 341 | 342 | /* 343 | Used to convert getblocktemplate bits field into target if target is not included. 344 | More info: https://en.bitcoin.it/wiki/Target 345 | */ 346 | 347 | exports.bignumFromBitsBuffer = function(bitsBuff){ 348 | var numBytes = bitsBuff.readUInt8(0); 349 | var bigBits = bignum.fromBuffer(bitsBuff.slice(1)); 350 | var target = bigBits.mul( 351 | bignum(2).pow( 352 | bignum(8).mul( 353 | numBytes - 3 354 | ) 355 | ) 356 | ); 357 | return target; 358 | }; 359 | 360 | exports.bignumFromBitsHex = function(bitsString){ 361 | var bitsBuff = new Buffer(bitsString, 'hex'); 362 | return exports.bignumFromBitsBuffer(bitsBuff); 363 | }; 364 | 365 | exports.convertBitsToBuff = function(bitsBuff){ 366 | var target = exports.bignumFromBitsBuffer(bitsBuff); 367 | var resultBuff = target.toBuffer(); 368 | var buff256 = new Buffer(32); 369 | buff256.fill(0); 370 | resultBuff.copy(buff256, buff256.length - resultBuff.length); 371 | return buff256; 372 | }; 373 | 374 | exports.getTruncatedDiff = function(shift){ 375 | return exports.convertBitsToBuff(exports.bufferToCompactBits(exports.shiftMax256Right(shift))); 376 | }; -------------------------------------------------------------------------------- /lib/varDiff.js: -------------------------------------------------------------------------------- 1 | var events = require('events'); 2 | 3 | /* 4 | 5 | Vardiff ported from stratum-mining share-limiter 6 | https://github.com/ahmedbodi/stratum-mining/blob/master/mining/basic_share_limiter.py 7 | 8 | */ 9 | 10 | 11 | function RingBuffer(maxSize){ 12 | var data = []; 13 | var cursor = 0; 14 | var isFull = false; 15 | this.append = function(x){ 16 | if (isFull){ 17 | data[cursor] = x; 18 | cursor = (cursor + 1) % maxSize; 19 | } 20 | else{ 21 | data.push(x); 22 | cursor++; 23 | if (data.length === maxSize){ 24 | cursor = 0; 25 | isFull = true; 26 | } 27 | } 28 | }; 29 | this.avg = function(){ 30 | var sum = data.reduce(function(a, b){ return a + b }); 31 | return sum / (isFull ? maxSize : cursor); 32 | }; 33 | this.size = function(){ 34 | return isFull ? maxSize : cursor; 35 | }; 36 | this.clear = function(){ 37 | data = []; 38 | cursor = 0; 39 | isFull = false; 40 | }; 41 | } 42 | 43 | // Truncate a number to a fixed amount of decimal places 44 | function toFixed(num, len) { 45 | return parseFloat(num.toFixed(len)); 46 | } 47 | 48 | var varDiff = module.exports = function varDiff(port, varDiffOptions){ 49 | var _this = this; 50 | 51 | var bufferSize, tMin, tMax; 52 | 53 | //if (!varDiffOptions) return; 54 | 55 | var variance = varDiffOptions.targetTime * (varDiffOptions.variancePercent / 100); 56 | 57 | 58 | bufferSize = varDiffOptions.retargetTime / varDiffOptions.targetTime * 4; 59 | tMin = varDiffOptions.targetTime - variance; 60 | tMax = varDiffOptions.targetTime + variance; 61 | 62 | 63 | 64 | this.manageClient = function(client){ 65 | 66 | var stratumPort = client.socket.localPort; 67 | 68 | if (stratumPort != port) { 69 | console.error("Handling a client which is not of this vardiff?"); 70 | } 71 | var options = varDiffOptions; 72 | 73 | var lastTs; 74 | var lastRtc; 75 | var timeBuffer; 76 | 77 | client.on('submit', function(){ 78 | 79 | var ts = (Date.now() / 1000) | 0; 80 | 81 | if (!lastRtc){ 82 | lastRtc = ts - options.retargetTime / 2; 83 | lastTs = ts; 84 | timeBuffer = new RingBuffer(bufferSize); 85 | return; 86 | } 87 | 88 | var sinceLast = ts - lastTs; 89 | 90 | timeBuffer.append(sinceLast); 91 | lastTs = ts; 92 | 93 | if ((ts - lastRtc) < options.retargetTime && timeBuffer.size() > 0) 94 | return; 95 | 96 | lastRtc = ts; 97 | var avg = timeBuffer.avg(); 98 | var ddiff = options.targetTime / avg; 99 | 100 | if (avg > tMax && client.difficulty > options.minDiff) { 101 | if (options.x2mode) { 102 | ddiff = 0.5; 103 | } 104 | if (ddiff * client.difficulty < options.minDiff) { 105 | ddiff = options.minDiff / client.difficulty; 106 | } 107 | } else if (avg < tMin) { 108 | if (options.x2mode) { 109 | ddiff = 2; 110 | } 111 | var diffMax = options.maxDiff; 112 | if (ddiff * client.difficulty > diffMax) { 113 | ddiff = diffMax / client.difficulty; 114 | } 115 | } 116 | else{ 117 | return; 118 | } 119 | 120 | var newDiff = toFixed(client.difficulty * ddiff, 8); 121 | timeBuffer.clear(); 122 | _this.emit('newDifficulty', client, newDiff); 123 | }); 124 | }; 125 | }; 126 | varDiff.prototype.__proto__ = events.EventEmitter.prototype; 127 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stratum-pool", 3 | "version": "0.1.6", 4 | "description": "High performance Stratum poolserver in Node.js", 5 | "keywords": [ 6 | "stratum", 7 | "mining", 8 | "pool", 9 | "server", 10 | "poolserver", 11 | "bitcoin", 12 | "litecoin", 13 | "scrypt" 14 | ], 15 | "homepage": "https://github.com/zone117x/node-stratum-pool", 16 | "bugs": { 17 | "url": "https://github.com/zone117x/node-stratum-pool/issues" 18 | }, 19 | "license": "GPL-2.0", 20 | "author": "Matthew Little", 21 | "contributors": [ 22 | "vekexasia", 23 | "TheSeven" 24 | ], 25 | "main": "lib/index.js", 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/zone117x/node-stratum-pool.git" 29 | }, 30 | "dependencies": { 31 | "multi-hashing": "git://github.com/zone117x/node-multi-hashing.git", 32 | "bignum": "*", 33 | "base58-native": "*", 34 | "async": "*" 35 | }, 36 | "engines": { 37 | "node": ">=0.10" 38 | } 39 | } 40 | --------------------------------------------------------------------------------