Sammy Libre
10 years ago
commit
7f07ef97c6
37 changed files with 2664 additions and 0 deletions
@ -0,0 +1,18 @@ |
|||||||
|
.DS_Store |
||||||
|
|
||||||
|
# CMake |
||||||
|
CMakeCache.txt |
||||||
|
CMakeFiles |
||||||
|
cmake_install.cmake |
||||||
|
install_manifest.txt |
||||||
|
*.cmake |
||||||
|
Makefile |
||||||
|
|
||||||
|
# Object files |
||||||
|
*.a |
||||||
|
*.so |
||||||
|
*.dylib |
||||||
|
|
||||||
|
# Configs |
||||||
|
config.json |
||||||
|
aeon.json |
@ -0,0 +1,13 @@ |
|||||||
|
cmake_minimum_required (VERSION 2.8.11) |
||||||
|
project (pool) |
||||||
|
|
||||||
|
if (DEFINED ENV{MONERO_DIR}) |
||||||
|
get_filename_component(MONERO_DIR $ENV{MONERO_DIR} ABSOLUTE) |
||||||
|
message("Using Monero source from env ${MONERO_DIR}") |
||||||
|
else() |
||||||
|
get_filename_component(MONERO_DIR "${CMAKE_SOURCE_DIR}/../bitmonero" ABSOLUTE) |
||||||
|
message("Monero surce directory is not defined, using default ${MONERO_DIR}") |
||||||
|
endif() |
||||||
|
|
||||||
|
add_subdirectory(hashing) |
||||||
|
add_subdirectory(cnutil) |
@ -0,0 +1,340 @@ |
|||||||
|
GNU GENERAL PUBLIC LICENSE |
||||||
|
Version 2, June 1991 |
||||||
|
|
||||||
|
Copyright (C) 1989, 1991 Free Software Foundation, Inc., <http://fsf.org/> |
||||||
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
||||||
|
Everyone is permitted to copy and distribute verbatim copies |
||||||
|
of this license document, but changing it is not allowed. |
||||||
|
|
||||||
|
Preamble |
||||||
|
|
||||||
|
The licenses for most software are designed to take away your |
||||||
|
freedom to share and change it. By contrast, the GNU General Public |
||||||
|
License is intended to guarantee your freedom to share and change free |
||||||
|
software--to make sure the software is free for all its users. This |
||||||
|
General Public License applies to most of the Free Software |
||||||
|
Foundation's software and to any other program whose authors commit to |
||||||
|
using it. (Some other Free Software Foundation software is covered by |
||||||
|
the GNU Lesser General Public License instead.) You can apply it to |
||||||
|
your programs, too. |
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, not |
||||||
|
price. Our General Public Licenses are designed to make sure that you |
||||||
|
have the freedom to distribute copies of free software (and charge for |
||||||
|
this service if you wish), that you receive source code or can get it |
||||||
|
if you want it, that you can change the software or use pieces of it |
||||||
|
in new free programs; and that you know you can do these things. |
||||||
|
|
||||||
|
To protect your rights, we need to make restrictions that forbid |
||||||
|
anyone to deny you these rights or to ask you to surrender the rights. |
||||||
|
These restrictions translate to certain responsibilities for you if you |
||||||
|
distribute copies of the software, or if you modify it. |
||||||
|
|
||||||
|
For example, if you distribute copies of such a program, whether |
||||||
|
gratis or for a fee, you must give the recipients all the rights that |
||||||
|
you have. You must make sure that they, too, receive or can get the |
||||||
|
source code. And you must show them these terms so they know their |
||||||
|
rights. |
||||||
|
|
||||||
|
We protect your rights with two steps: (1) copyright the software, and |
||||||
|
(2) offer you this license which gives you legal permission to copy, |
||||||
|
distribute and/or modify the software. |
||||||
|
|
||||||
|
Also, for each author's protection and ours, we want to make certain |
||||||
|
that everyone understands that there is no warranty for this free |
||||||
|
software. If the software is modified by someone else and passed on, we |
||||||
|
want its recipients to know that what they have is not the original, so |
||||||
|
that any problems introduced by others will not reflect on the original |
||||||
|
authors' reputations. |
||||||
|
|
||||||
|
Finally, any free program is threatened constantly by software |
||||||
|
patents. We wish to avoid the danger that redistributors of a free |
||||||
|
program will individually obtain patent licenses, in effect making the |
||||||
|
program proprietary. To prevent this, we have made it clear that any |
||||||
|
patent must be licensed for everyone's free use or not licensed at all. |
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and |
||||||
|
modification follow. |
||||||
|
|
||||||
|
GNU GENERAL PUBLIC LICENSE |
||||||
|
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION |
||||||
|
|
||||||
|
0. This License applies to any program or other work which contains |
||||||
|
a notice placed by the copyright holder saying it may be distributed |
||||||
|
under the terms of this General Public License. The "Program", below, |
||||||
|
refers to any such program or work, and a "work based on the Program" |
||||||
|
means either the Program or any derivative work under copyright law: |
||||||
|
that is to say, a work containing the Program or a portion of it, |
||||||
|
either verbatim or with modifications and/or translated into another |
||||||
|
language. (Hereinafter, translation is included without limitation in |
||||||
|
the term "modification".) Each licensee is addressed as "you". |
||||||
|
|
||||||
|
Activities other than copying, distribution and modification are not |
||||||
|
covered by this License; they are outside its scope. The act of |
||||||
|
running the Program is not restricted, and the output from the Program |
||||||
|
is covered only if its contents constitute a work based on the |
||||||
|
Program (independent of having been made by running the Program). |
||||||
|
Whether that is true depends on what the Program does. |
||||||
|
|
||||||
|
1. You may copy and distribute verbatim copies of the Program's |
||||||
|
source code as you receive it, in any medium, provided that you |
||||||
|
conspicuously and appropriately publish on each copy an appropriate |
||||||
|
copyright notice and disclaimer of warranty; keep intact all the |
||||||
|
notices that refer to this License and to the absence of any warranty; |
||||||
|
and give any other recipients of the Program a copy of this License |
||||||
|
along with the Program. |
||||||
|
|
||||||
|
You may charge a fee for the physical act of transferring a copy, and |
||||||
|
you may at your option offer warranty protection in exchange for a fee. |
||||||
|
|
||||||
|
2. You may modify your copy or copies of the Program or any portion |
||||||
|
of it, thus forming a work based on the Program, and copy and |
||||||
|
distribute such modifications or work under the terms of Section 1 |
||||||
|
above, provided that you also meet all of these conditions: |
||||||
|
|
||||||
|
a) You must cause the modified files to carry prominent notices |
||||||
|
stating that you changed the files and the date of any change. |
||||||
|
|
||||||
|
b) You must cause any work that you distribute or publish, that in |
||||||
|
whole or in part contains or is derived from the Program or any |
||||||
|
part thereof, to be licensed as a whole at no charge to all third |
||||||
|
parties under the terms of this License. |
||||||
|
|
||||||
|
c) If the modified program normally reads commands interactively |
||||||
|
when run, you must cause it, when started running for such |
||||||
|
interactive use in the most ordinary way, to print or display an |
||||||
|
announcement including an appropriate copyright notice and a |
||||||
|
notice that there is no warranty (or else, saying that you provide |
||||||
|
a warranty) and that users may redistribute the program under |
||||||
|
these conditions, and telling the user how to view a copy of this |
||||||
|
License. (Exception: if the Program itself is interactive but |
||||||
|
does not normally print such an announcement, your work based on |
||||||
|
the Program is not required to print an announcement.) |
||||||
|
|
||||||
|
These requirements apply to the modified work as a whole. If |
||||||
|
identifiable sections of that work are not derived from the Program, |
||||||
|
and can be reasonably considered independent and separate works in |
||||||
|
themselves, then this License, and its terms, do not apply to those |
||||||
|
sections when you distribute them as separate works. But when you |
||||||
|
distribute the same sections as part of a whole which is a work based |
||||||
|
on the Program, the distribution of the whole must be on the terms of |
||||||
|
this License, whose permissions for other licensees extend to the |
||||||
|
entire whole, and thus to each and every part regardless of who wrote it. |
||||||
|
|
||||||
|
Thus, it is not the intent of this section to claim rights or contest |
||||||
|
your rights to work written entirely by you; rather, the intent is to |
||||||
|
exercise the right to control the distribution of derivative or |
||||||
|
collective works based on the Program. |
||||||
|
|
||||||
|
In addition, mere aggregation of another work not based on the Program |
||||||
|
with the Program (or with a work based on the Program) on a volume of |
||||||
|
a storage or distribution medium does not bring the other work under |
||||||
|
the scope of this License. |
||||||
|
|
||||||
|
3. You may copy and distribute the Program (or a work based on it, |
||||||
|
under Section 2) in object code or executable form under the terms of |
||||||
|
Sections 1 and 2 above provided that you also do one of the following: |
||||||
|
|
||||||
|
a) Accompany it with the complete corresponding machine-readable |
||||||
|
source code, which must be distributed under the terms of Sections |
||||||
|
1 and 2 above on a medium customarily used for software interchange; or, |
||||||
|
|
||||||
|
b) Accompany it with a written offer, valid for at least three |
||||||
|
years, to give any third party, for a charge no more than your |
||||||
|
cost of physically performing source distribution, a complete |
||||||
|
machine-readable copy of the corresponding source code, to be |
||||||
|
distributed under the terms of Sections 1 and 2 above on a medium |
||||||
|
customarily used for software interchange; or, |
||||||
|
|
||||||
|
c) Accompany it with the information you received as to the offer |
||||||
|
to distribute corresponding source code. (This alternative is |
||||||
|
allowed only for noncommercial distribution and only if you |
||||||
|
received the program in object code or executable form with such |
||||||
|
an offer, in accord with Subsection b above.) |
||||||
|
|
||||||
|
The source code for a work means the preferred form of the work for |
||||||
|
making modifications to it. For an executable work, complete source |
||||||
|
code means all the source code for all modules it contains, plus any |
||||||
|
associated interface definition files, plus the scripts used to |
||||||
|
control compilation and installation of the executable. However, as a |
||||||
|
special exception, the source code distributed need not include |
||||||
|
anything that is normally distributed (in either source or binary |
||||||
|
form) with the major components (compiler, kernel, and so on) of the |
||||||
|
operating system on which the executable runs, unless that component |
||||||
|
itself accompanies the executable. |
||||||
|
|
||||||
|
If distribution of executable or object code is made by offering |
||||||
|
access to copy from a designated place, then offering equivalent |
||||||
|
access to copy the source code from the same place counts as |
||||||
|
distribution of the source code, even though third parties are not |
||||||
|
compelled to copy the source along with the object code. |
||||||
|
|
||||||
|
4. You may not copy, modify, sublicense, or distribute the Program |
||||||
|
except as expressly provided under this License. Any attempt |
||||||
|
otherwise to copy, modify, sublicense or distribute the Program is |
||||||
|
void, and will automatically terminate your rights under this License. |
||||||
|
However, parties who have received copies, or rights, from you under |
||||||
|
this License will not have their licenses terminated so long as such |
||||||
|
parties remain in full compliance. |
||||||
|
|
||||||
|
5. You are not required to accept this License, since you have not |
||||||
|
signed it. However, nothing else grants you permission to modify or |
||||||
|
distribute the Program or its derivative works. These actions are |
||||||
|
prohibited by law if you do not accept this License. Therefore, by |
||||||
|
modifying or distributing the Program (or any work based on the |
||||||
|
Program), you indicate your acceptance of this License to do so, and |
||||||
|
all its terms and conditions for copying, distributing or modifying |
||||||
|
the Program or works based on it. |
||||||
|
|
||||||
|
6. Each time you redistribute the Program (or any work based on the |
||||||
|
Program), the recipient automatically receives a license from the |
||||||
|
original licensor to copy, distribute or modify the Program subject to |
||||||
|
these terms and conditions. You may not impose any further |
||||||
|
restrictions on the recipients' exercise of the rights granted herein. |
||||||
|
You are not responsible for enforcing compliance by third parties to |
||||||
|
this License. |
||||||
|
|
||||||
|
7. If, as a consequence of a court judgment or allegation of patent |
||||||
|
infringement or for any other reason (not limited to patent issues), |
||||||
|
conditions are imposed on you (whether by court order, agreement or |
||||||
|
otherwise) that contradict the conditions of this License, they do not |
||||||
|
excuse you from the conditions of this License. If you cannot |
||||||
|
distribute so as to satisfy simultaneously your obligations under this |
||||||
|
License and any other pertinent obligations, then as a consequence you |
||||||
|
may not distribute the Program at all. For example, if a patent |
||||||
|
license would not permit royalty-free redistribution of the Program by |
||||||
|
all those who receive copies directly or indirectly through you, then |
||||||
|
the only way you could satisfy both it and this License would be to |
||||||
|
refrain entirely from distribution of the Program. |
||||||
|
|
||||||
|
If any portion of this section is held invalid or unenforceable under |
||||||
|
any particular circumstance, the balance of the section is intended to |
||||||
|
apply and the section as a whole is intended to apply in other |
||||||
|
circumstances. |
||||||
|
|
||||||
|
It is not the purpose of this section to induce you to infringe any |
||||||
|
patents or other property right claims or to contest validity of any |
||||||
|
such claims; this section has the sole purpose of protecting the |
||||||
|
integrity of the free software distribution system, which is |
||||||
|
implemented by public license practices. Many people have made |
||||||
|
generous contributions to the wide range of software distributed |
||||||
|
through that system in reliance on consistent application of that |
||||||
|
system; it is up to the author/donor to decide if he or she is willing |
||||||
|
to distribute software through any other system and a licensee cannot |
||||||
|
impose that choice. |
||||||
|
|
||||||
|
This section is intended to make thoroughly clear what is believed to |
||||||
|
be a consequence of the rest of this License. |
||||||
|
|
||||||
|
8. If the distribution and/or use of the Program is restricted in |
||||||
|
certain countries either by patents or by copyrighted interfaces, the |
||||||
|
original copyright holder who places the Program under this License |
||||||
|
may add an explicit geographical distribution limitation excluding |
||||||
|
those countries, so that distribution is permitted only in or among |
||||||
|
countries not thus excluded. In such case, this License incorporates |
||||||
|
the limitation as if written in the body of this License. |
||||||
|
|
||||||
|
9. The Free Software Foundation may publish revised and/or new versions |
||||||
|
of the General Public License from time to time. Such new versions will |
||||||
|
be similar in spirit to the present version, but may differ in detail to |
||||||
|
address new problems or concerns. |
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the Program |
||||||
|
specifies a version number of this License which applies to it and "any |
||||||
|
later version", you have the option of following the terms and conditions |
||||||
|
either of that version or of any later version published by the Free |
||||||
|
Software Foundation. If the Program does not specify a version number of |
||||||
|
this License, you may choose any version ever published by the Free Software |
||||||
|
Foundation. |
||||||
|
|
||||||
|
10. If you wish to incorporate parts of the Program into other free |
||||||
|
programs whose distribution conditions are different, write to the author |
||||||
|
to ask for permission. For software which is copyrighted by the Free |
||||||
|
Software Foundation, write to the Free Software Foundation; we sometimes |
||||||
|
make exceptions for this. Our decision will be guided by the two goals |
||||||
|
of preserving the free status of all derivatives of our free software and |
||||||
|
of promoting the sharing and reuse of software generally. |
||||||
|
|
||||||
|
NO WARRANTY |
||||||
|
|
||||||
|
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY |
||||||
|
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN |
||||||
|
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES |
||||||
|
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED |
||||||
|
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF |
||||||
|
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS |
||||||
|
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE |
||||||
|
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, |
||||||
|
REPAIR OR CORRECTION. |
||||||
|
|
||||||
|
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING |
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR |
||||||
|
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, |
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING |
||||||
|
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED |
||||||
|
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY |
||||||
|
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER |
||||||
|
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE |
||||||
|
POSSIBILITY OF SUCH DAMAGES. |
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS |
||||||
|
|
||||||
|
How to Apply These Terms to Your New Programs |
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest |
||||||
|
possible use to the public, the best way to achieve this is to make it |
||||||
|
free software which everyone can redistribute and change under these terms. |
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest |
||||||
|
to attach them to the start of each source file to most effectively |
||||||
|
convey the exclusion of warranty; and each file should have at least |
||||||
|
the "copyright" line and a pointer to where the full notice is found. |
||||||
|
|
||||||
|
{description} |
||||||
|
Copyright (C) {year} {fullname} |
||||||
|
|
||||||
|
This program is free software; you can redistribute it and/or modify |
||||||
|
it under the terms of the GNU General Public License as published by |
||||||
|
the Free Software Foundation; either version 2 of the License, or |
||||||
|
(at your option) any later version. |
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful, |
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||||
|
GNU General Public License for more details. |
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License along |
||||||
|
with this program; if not, write to the Free Software Foundation, Inc., |
||||||
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. |
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail. |
||||||
|
|
||||||
|
If the program is interactive, make it output a short notice like this |
||||||
|
when it starts in an interactive mode: |
||||||
|
|
||||||
|
Gnomovision version 69, Copyright (C) year name of author |
||||||
|
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. |
||||||
|
This is free software, and you are welcome to redistribute it |
||||||
|
under certain conditions; type `show c' for details. |
||||||
|
|
||||||
|
The hypothetical commands `show w' and `show c' should show the appropriate |
||||||
|
parts of the General Public License. Of course, the commands you use may |
||||||
|
be called something other than `show w' and `show c'; they could even be |
||||||
|
mouse-clicks or menu items--whatever suits your program. |
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or your |
||||||
|
school, if any, to sign a "copyright disclaimer" for the program, if |
||||||
|
necessary. Here is a sample; alter the names: |
||||||
|
|
||||||
|
Yoyodyne, Inc., hereby disclaims all copyright interest in the program |
||||||
|
`Gnomovision' (which makes passes at compilers) written by James Hacker. |
||||||
|
|
||||||
|
{signature of Ty Coon}, 1 April 1989 |
||||||
|
Ty Coon, President of Vice |
||||||
|
|
||||||
|
This General Public License does not permit incorporating your program into |
||||||
|
proprietary programs. If your program is a subroutine library, you may |
||||||
|
consider it more useful to permit linking proprietary applications with the |
||||||
|
library. If this is what you want to do, use the GNU Lesser General |
||||||
|
Public License instead of this License. |
||||||
|
|
@ -0,0 +1,106 @@ |
|||||||
|
# go-cryptonote-pool |
||||||
|
|
||||||
|
High performance CryptoNote mining stratum written in Golang backed by Redis. |
||||||
|
|
||||||
|
**Stratum feature list:** |
||||||
|
|
||||||
|
* Full [node-cryptonote-pool](https://github.com/zone117x/node-cryptonote-pool) database compatibility |
||||||
|
* Concurrent shares processing, each connection is handled in a lightweight thread of execution |
||||||
|
* Several configurable stratum policies to prevent basic attacks |
||||||
|
* Banning policy using [**ipset**s](http://ipset.netfilter.org/) on Linux for high performance banning |
||||||
|
* Whitelist for trusted miners and blacklist for unwelcome guests |
||||||
|
* AES-NI enabled share validation code with fallback to slow implementation provided by linking with [**Monero**](https://github.com/monero-project/bitmonero) libraries |
||||||
|
* Integrated NewRelic performance monitoring plugin |
||||||
|
|
||||||
|
### Installation |
||||||
|
|
||||||
|
Dependencies: |
||||||
|
|
||||||
|
* go-1.4 |
||||||
|
* boost-1.55+ |
||||||
|
* cmake |
||||||
|
|
||||||
|
Install required packages: |
||||||
|
|
||||||
|
go get gopkg.in/redis.v3 |
||||||
|
go get github.com/yvasiyarov/gorelic |
||||||
|
|
||||||
|
#### Mac OS X |
||||||
|
|
||||||
|
Download and compile [Monero](https://github.com/monero-project/bitmonero) daemon. |
||||||
|
|
||||||
|
Now clone stratum repo and compile it: |
||||||
|
|
||||||
|
git clone https://github.com/sammy007/go-cryptonote-pool.git |
||||||
|
cmake . |
||||||
|
make |
||||||
|
|
||||||
|
Notice that for share validation stratum requires bitmonero source tree where .a libs already compiled. By default stratum will use <code>../bitmonero</code> directory. You can override this behaviour by passing <code>MONERO_DIR</code> env variable: |
||||||
|
|
||||||
|
MONERO_DIR=/path/to/bitmonero cmake . |
||||||
|
make |
||||||
|
|
||||||
|
#### Linux |
||||||
|
|
||||||
|
Installation on linux is similar to OS X installation and currently the only dfference is that you should copy *.so* libs from *hashing* and *cnutil* directories to */usr/local/lib* or similar dir in order to make CGO happy. I would recommend you to use Ubuntu 14.04 LTS. |
||||||
|
|
||||||
|
In order to successfully link with bitmonero libs, recompile bitmonero with: |
||||||
|
|
||||||
|
CXXFLAGS="-fPIC" CFLAGS="-fPIC" make release |
||||||
|
|
||||||
|
Build stratum: |
||||||
|
|
||||||
|
MONERO_DIR=/opt/src/bitmonero cmake . |
||||||
|
make |
||||||
|
|
||||||
|
Run it: |
||||||
|
|
||||||
|
LD_LIBRARY_PATH="/usr/local/lib/" GOPATH=/path/to/go go run main.go |
||||||
|
|
||||||
|
More info on *GOPATH* you can find in a [wiki](https://github.com/golang/go/wiki/GOPATH). |
||||||
|
|
||||||
|
### Configuration |
||||||
|
|
||||||
|
Configuration is self-describing, just copy *config.example.json* to *config.json* and run stratum with path to config file as 1st argument. There is default XMR address of monero core team in config example and open monero rpc node from [moneroclub.com](https://www.moneroclub.com/node). |
||||||
|
|
||||||
|
#### Redis |
||||||
|
|
||||||
|
Leave Redis password blank if you have local setup in a trusted environment. Don't rely on Redis password, it's easily bruteforceable. Password option is only for some clouds. There is a connection pool, use some reasonable value. Remember, that each valid share submission will lease one connection from a pool due to <code>multi</code> exec and instantly release it, this is how go-redis works. |
||||||
|
|
||||||
|
#### Policies |
||||||
|
|
||||||
|
Stratum policy server collecting several stats on per IP basis. |
||||||
|
|
||||||
|
Banning enabled by default. Specify <code>ipset</code> name for banning. Timeout argument will be passed to this ipset. For ipset usage refer to [this article](https://wiki.archlinux.org/index.php/Ipset). Stratum will use os/exec command like <code>sudo ipset ...</code> for banning, so you have to configure sudo properly and make sure that your system will never ask for password: |
||||||
|
|
||||||
|
*/etc/sudoers.d/stratum* |
||||||
|
|
||||||
|
stratum ALL=NOPASSWD: /sbin/ipset |
||||||
|
|
||||||
|
Use limits to prevent connection flood to your stratum, there is initial <code>limit</code> and <code>limitJump</code>. Policy server will increase number of allowed connections on each valid share submission. Stratum will bypass this policy regarding <code>grace</code> time specified on start. |
||||||
|
|
||||||
|
#### Payouts and Block Unlocking |
||||||
|
|
||||||
|
This is just stratum yet. Use corresponding [node-cryptonote-pool](https://github.com/zone117x/node-cryptonote-pool) modules for block unlocking and payout processing. Database is 100% compatible. |
||||||
|
|
||||||
|
### Private Pool Guidelines |
||||||
|
|
||||||
|
For personal private pool you can use [DigitalOcean](https://www.digitalocean.com/?refcode=2a6767e6285f) droplet. With recent blockchain-db merged into Monero it's ok to run it even on 5 USD plan. You will receive 10 USD free credit there. |
||||||
|
|
||||||
|
### TODO |
||||||
|
|
||||||
|
Still in early stage, despite that I am using it for private setups, stratum requires a lot of stability tests. Please run it with <code>-race</code> flag with <code>GORACE="log_path=/path/to/race.log"</code> in private setup and send contents of this file to me if you are "lucky" and found race. It will make stratum ~20x slower, but it does not hit performance if you are soloing with a dozen of GPUs. Look at *-debug.fish* script for example. |
||||||
|
|
||||||
|
Cool stuff will be added after excessive testing, I always have ideas for improvement and new features. |
||||||
|
|
||||||
|
### Donations |
||||||
|
|
||||||
|
* **BTC**: [16bBz4wZPh7kV53nFMf8LmtJHE2rHsADB2](https://blockchain.info/address/16bBz4wZPh7kV53nFMf8LmtJHE2rHsADB2) |
||||||
|
* **XMR**: 4Aag5kkRHmCFHM5aRUtfB2RF3c5NDmk5CVbGdg6fefszEhhFdXhnjiTCr81YxQ9bsi73CSHT3ZN3p82qyakHwZ2GHYqeaUr |
||||||
|
* **XMR openalias**: wallet.hashinvest.net |
||||||
|
|
||||||
|
### License |
||||||
|
|
||||||
|
Released under the GNU General Public License v2. |
||||||
|
|
||||||
|
http://www.gnu.org/licenses/gpl-2.0.html |
@ -0,0 +1,7 @@ |
|||||||
|
Makefile |
||||||
|
CmakeCache.txt |
||||||
|
cmake_install.cmake |
||||||
|
CMakeFiles |
||||||
|
*.a |
||||||
|
*.so |
||||||
|
*.dylib |
@ -0,0 +1,36 @@ |
|||||||
|
set(CXXLIB "cnutilxx") |
||||||
|
|
||||||
|
find_package(Boost COMPONENTS thread system program_options date_time filesystem REQUIRED) |
||||||
|
|
||||||
|
# Flags |
||||||
|
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -D_GNU_SOURCE") |
||||||
|
|
||||||
|
include_directories(${Boost_INCLUDE_DIRS}) |
||||||
|
include_directories("${MONERO_DIR}/contrib/epee/include") |
||||||
|
include_directories("${MONERO_DIR}/src") |
||||||
|
|
||||||
|
# Build library |
||||||
|
add_library(${CXXLIB} SHARED cnutilxx/main.cpp) |
||||||
|
|
||||||
|
target_link_libraries(${CXXLIB} |
||||||
|
${MONERO_DIR}/build/release/src/cryptonote_core/libcryptonote_core.a |
||||||
|
${MONERO_DIR}/build/release/src/crypto/libcrypto.a |
||||||
|
${MONERO_DIR}/build/release/src/common/libcommon.a |
||||||
|
) |
||||||
|
|
||||||
|
target_link_libraries(${CXXLIB} |
||||||
|
${Boost_THREAD_LIBRARY} |
||||||
|
${Boost_SYSTEM_LIBRARY} |
||||||
|
${Boost_PROGRAM_OPTIONS_LIBRARY} |
||||||
|
${Boost_DATE_TIME_LIBRARY} |
||||||
|
${Boost_FILESYSTEM_LIBRARY} |
||||||
|
) |
||||||
|
|
||||||
|
set(LIB "cnutil") |
||||||
|
|
||||||
|
# Flags |
||||||
|
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -std=c11 -D_GNU_SOURCE") |
||||||
|
|
||||||
|
# Build library |
||||||
|
add_library(${LIB} SHARED cnutil.c) |
||||||
|
target_link_libraries(${LIB} ${CXXLIB}) |
@ -0,0 +1,13 @@ |
|||||||
|
#include <stdint.h> |
||||||
|
#include "stdbool.h" |
||||||
|
#include <stdio.h> |
||||||
|
#include <stdlib.h> |
||||||
|
#include "cnutilxx/main.h" |
||||||
|
|
||||||
|
uint32_t convert_blob(const char *blob, size_t len, char *out) { |
||||||
|
return cn_convert_blob(blob, len, out); |
||||||
|
} |
||||||
|
|
||||||
|
bool validate_address(const char *addr, size_t len) { |
||||||
|
return cn_validate_address(addr, len); |
||||||
|
} |
@ -0,0 +1,28 @@ |
|||||||
|
package cnutil |
||||||
|
|
||||||
|
// #cgo CFLAGS: -std=c11 -D_GNU_SOURCE
|
||||||
|
// #cgo LDFLAGS: -L. -lcnutil -lcnutilxx -lstdc++
|
||||||
|
// #include "cnutil.h"
|
||||||
|
import "C" |
||||||
|
import "unsafe" |
||||||
|
|
||||||
|
func ConvertBlob(blob []byte) []byte { |
||||||
|
output := make([]byte, 76) |
||||||
|
out := (*C.char)(unsafe.Pointer(&output[0])) |
||||||
|
|
||||||
|
input := C.CString(string(blob)) |
||||||
|
defer C.free(unsafe.Pointer(input)) |
||||||
|
|
||||||
|
size := (C.uint32_t)(len(blob)) |
||||||
|
C.convert_blob(input, size, out) |
||||||
|
return output |
||||||
|
} |
||||||
|
|
||||||
|
func ValidateAddress(addr string) bool { |
||||||
|
input := C.CString(addr) |
||||||
|
defer C.free(unsafe.Pointer(input)) |
||||||
|
|
||||||
|
size := (C.uint32_t)(len(addr)) |
||||||
|
result := C.validate_address(input, size) |
||||||
|
return (bool)(result) |
||||||
|
} |
@ -0,0 +1,7 @@ |
|||||||
|
#include <stdio.h> |
||||||
|
#include <stdint.h> |
||||||
|
#include <stdlib.h> |
||||||
|
#include "stdbool.h" |
||||||
|
|
||||||
|
uint32_t convert_blob(const char *blob, uint32_t len, char *out); |
||||||
|
bool validate_address(const char *addr, uint32_t len); |
@ -0,0 +1,58 @@ |
|||||||
|
package cnutil |
||||||
|
|
||||||
|
import ( |
||||||
|
"encoding/hex" |
||||||
|
"log" |
||||||
|
"testing" |
||||||
|
) |
||||||
|
|
||||||
|
func TestConvertBlob(t *testing.T) { |
||||||
|
hashBytes, _ := hex.DecodeString("0100a5d1fca9057dff46d140d453a672437ba0ec7d6a74bc5fa0391f8a918e41fd7ba2cf6fc1af000000000183811401ffc7801405889ec5dc2402e0fe0db63a8e532a7b988e0c32a764e5e8d64d7efac9bc9d24ce32b0984ab93980b09dc2df0102ccd38432501a9182ccc5b44cb47abdddbc9a6321cd581f5f07a7a9195795d5c68080dd9da41702f6e944ee4c6e1eeaed1fa6a3c2480a410e959c6e823b96dad54d31fe223cc0fd80c0a8ca9a3a0286d0e3411670e4c2abe8492c695c66d8262660ee88a0b14a2b03c9180fb6f0d480c0caf384a30202aec5c9b7efe841dd821476e0e06217be13a4c85a83efcf9576314d60130e02e72b0150526f7a381cec33e5827c1848dd80e6eac4b262304ea06b3a43303a4631df28020800000000018ba82000") |
||||||
|
expectedResult, _ := hex.DecodeString("0100a5d1fca9057dff46d140d453a672437ba0ec7d6a74bc5fa0391f8a918e41fd7ba2cf6fc1af00000000e81cb2bf0d2c5054a49bda094c39cb263a9565b9b81cf4c4f848292040419f4a01") |
||||||
|
output := ConvertBlob(hashBytes) |
||||||
|
|
||||||
|
if len(output) != 76 { |
||||||
|
t.Error("Invalid result length") |
||||||
|
} |
||||||
|
ok := true |
||||||
|
for i := range output { |
||||||
|
if expectedResult[i] != output[i] { |
||||||
|
ok = false |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
if !ok { |
||||||
|
log.Printf("Got: %v %v", output, len(output)) |
||||||
|
log.Printf("Expected: %v %v", expectedResult, len(expectedResult)) |
||||||
|
t.Error("Invalid result") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func TestDecodeAddress(t *testing.T) { |
||||||
|
addy := "45pyCXYn2UBVUmCFjgKr7LF8hCTeGwucWJ2xni7qrbj6GgAZBFY6tANarozZx9DaQqHyuR1AL8HJbRmqwLhUaDpKJW4hqS1" |
||||||
|
if !ValidateAddress(addy) { |
||||||
|
t.Error("Valid address") |
||||||
|
} |
||||||
|
|
||||||
|
addy = "46BeWrHpwXmHDpDEUmZBWZfoQpdc6HaERCNmx1pEYL2rAcuwufPN9rXHHtyUA4QVy66qeFQkn6sfK8aHYjA3jk3o1Bv16em" |
||||||
|
if !ValidateAddress(addy) { |
||||||
|
t.Error("Valid address") |
||||||
|
} |
||||||
|
|
||||||
|
if ValidateAddress("OMG") { |
||||||
|
t.Error("Invalid address") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func BenchmarkConvertBlob(b *testing.B) { |
||||||
|
for i := 0; i < b.N; i++ { |
||||||
|
hashBytes, _ := hex.DecodeString("0100a5d1fca9057dff46d140d453a672437ba0ec7d6a74bc5fa0391f8a918e41fd7ba2cf6fc1af000000000183811401ffc7801405889ec5dc2402e0fe0db63a8e532a7b988e0c32a764e5e8d64d7efac9bc9d24ce32b0984ab93980b09dc2df0102ccd38432501a9182ccc5b44cb47abdddbc9a6321cd581f5f07a7a9195795d5c68080dd9da41702f6e944ee4c6e1eeaed1fa6a3c2480a410e959c6e823b96dad54d31fe223cc0fd80c0a8ca9a3a0286d0e3411670e4c2abe8492c695c66d8262660ee88a0b14a2b03c9180fb6f0d480c0caf384a30202aec5c9b7efe841dd821476e0e06217be13a4c85a83efcf9576314d60130e02e72b0150526f7a381cec33e5827c1848dd80e6eac4b262304ea06b3a43303a4631df28020800000000018ba82000") |
||||||
|
ConvertBlob(hashBytes) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func BenchmarkDecodeAddress(b *testing.B) { |
||||||
|
for i := 0; i < b.N; i++ { |
||||||
|
ValidateAddress("45pyCXYn2UBVUmCFjgKr7LF8hCTeGwucWJ2xni7qrbj6GgAZBFY6tANarozZx9DaQqHyuR1AL8HJbRmqwLhUaDpKJW4hqS1") |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,30 @@ |
|||||||
|
#include <stdint.h> |
||||||
|
#include <string> |
||||||
|
#include "cryptonote_core/cryptonote_format_utils.h" |
||||||
|
#include "common/base58.h" |
||||||
|
|
||||||
|
using namespace cryptonote; |
||||||
|
|
||||||
|
// Well, it's dirty and useless, but without it I can't link to bitmonero's /build/release/src/**/*.a libs
|
||||||
|
unsigned int epee::g_test_dbg_lock_sleep = 0; |
||||||
|
|
||||||
|
extern "C" uint32_t cn_convert_blob(const char *blob, size_t len, char *out) { |
||||||
|
std::string input = std::string(blob, len); |
||||||
|
std::string output = ""; |
||||||
|
|
||||||
|
block b = AUTO_VAL_INIT(b); |
||||||
|
if (!parse_and_validate_block_from_blob(input, b)) { |
||||||
|
return 0; |
||||||
|
} |
||||||
|
|
||||||
|
output = get_block_hashing_blob(b); |
||||||
|
output.copy(out, output.length(), 0); |
||||||
|
return output.length(); |
||||||
|
} |
||||||
|
|
||||||
|
extern "C" bool cn_validate_address(const char *addr, size_t len) { |
||||||
|
std::string input = std::string(addr, len); |
||||||
|
std::string output = ""; |
||||||
|
uint64_t prefix; |
||||||
|
return tools::base58::decode_addr(addr, prefix, output); |
||||||
|
} |
@ -0,0 +1,12 @@ |
|||||||
|
#include <stdint.h> |
||||||
|
|
||||||
|
#ifdef __cplusplus |
||||||
|
extern "C" { |
||||||
|
#endif |
||||||
|
|
||||||
|
uint32_t cn_convert_blob(const char *blob, uint32_t len, char *out); |
||||||
|
bool cn_validate_address(const char *addr, uint32_t len); |
||||||
|
|
||||||
|
#ifdef __cplusplus |
||||||
|
} |
||||||
|
#endif |
@ -0,0 +1,71 @@ |
|||||||
|
{ |
||||||
|
"address": "46BeWrHpwXmHDpDEUmZBWZfoQpdc6HaERCNmx1pEYL2rAcuwufPN9rXHHtyUA4QVy66qeFQkn6sfK8aHYjA3jk3o1Bv16em", |
||||||
|
|
||||||
|
"threads": 2, |
||||||
|
"coin": "monero", |
||||||
|
|
||||||
|
"stratum": { |
||||||
|
"timeout": "15m", |
||||||
|
"blockRefreshInterval": "1s", |
||||||
|
|
||||||
|
"listen": [ |
||||||
|
{ |
||||||
|
"host": "0.0.0.0", |
||||||
|
"port": 1111, |
||||||
|
"diff": 8000, |
||||||
|
"maxConn": 32768 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"host": "0.0.0.0", |
||||||
|
"port": 3333, |
||||||
|
"diff": 16000, |
||||||
|
"maxConn": 32768 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"host": "0.0.0.0", |
||||||
|
"port": 5555, |
||||||
|
"diff": 16000, |
||||||
|
"maxConn": 32768 |
||||||
|
} |
||||||
|
] |
||||||
|
}, |
||||||
|
|
||||||
|
"daemon": { |
||||||
|
"host": "node.moneroclub.com", |
||||||
|
"port": 8880, |
||||||
|
"timeout": "1s" |
||||||
|
}, |
||||||
|
|
||||||
|
"redis": { |
||||||
|
"endpoint": "127.0.0.1:6379", |
||||||
|
"poolSize": 8, |
||||||
|
"database": 0 |
||||||
|
}, |
||||||
|
|
||||||
|
"policy": { |
||||||
|
"workers": 8, |
||||||
|
"resetInterval": "60m", |
||||||
|
"refreshInterval": "1m", |
||||||
|
|
||||||
|
"banning": { |
||||||
|
"enabled": true, |
||||||
|
"ipset": "blacklist", |
||||||
|
"timeout": 1800, |
||||||
|
"invalidPercent": 30, |
||||||
|
"checkThreshold": 30, |
||||||
|
"malformedLimit": 5 |
||||||
|
}, |
||||||
|
|
||||||
|
"limits": { |
||||||
|
"enabled": false, |
||||||
|
"limit": 30, |
||||||
|
"grace": "5m", |
||||||
|
"limitJump": 10 |
||||||
|
} |
||||||
|
}, |
||||||
|
|
||||||
|
"newrelicEnabled": false, |
||||||
|
"newrelicName": "MyStratum", |
||||||
|
"newrelicKey": "SECRET_KEY", |
||||||
|
"newrelicVerbose": false |
||||||
|
} |
@ -0,0 +1,68 @@ |
|||||||
|
package pool |
||||||
|
|
||||||
|
type Config struct { |
||||||
|
Address string `json:"address"` |
||||||
|
Stratum Stratum `json:"stratum"` |
||||||
|
Daemon Daemon `json:"daemon"` |
||||||
|
Redis Redis `json:"redis"` |
||||||
|
|
||||||
|
Threads int `json:"threads"` |
||||||
|
Coin string `json:"coin"` |
||||||
|
|
||||||
|
Policy Policy `json:"policy"` |
||||||
|
|
||||||
|
NewrelicName string `json:"newrelicName"` |
||||||
|
NewrelicKey string `json:"newrelicKey"` |
||||||
|
NewrelicVerbose bool `json:"newrelicVerbose"` |
||||||
|
NewrelicEnabled bool `json:"newrelicEnabled"` |
||||||
|
} |
||||||
|
|
||||||
|
type Stratum struct { |
||||||
|
Timeout string `json:"timeout"` |
||||||
|
BlockRefreshInterval string `json:"blockRefreshInterval"` |
||||||
|
Ports []Port `json:"listen"` |
||||||
|
} |
||||||
|
|
||||||
|
type Port struct { |
||||||
|
Host string `json:"host"` |
||||||
|
Port int `json:"port"` |
||||||
|
Difficulty int64 `json:"diff"` |
||||||
|
MaxConn int `json:"maxConn"` |
||||||
|
} |
||||||
|
|
||||||
|
type Daemon struct { |
||||||
|
Host string `json:"host"` |
||||||
|
Port int `json:"port"` |
||||||
|
Timeout string `json:"timeout"` |
||||||
|
} |
||||||
|
|
||||||
|
type Redis struct { |
||||||
|
Endpoint string `json:"endpoint"` |
||||||
|
Password string `json:"password"` |
||||||
|
Database int64 `json:"database"` |
||||||
|
PoolSize int `json:"poolSize"` |
||||||
|
} |
||||||
|
|
||||||
|
type Policy struct { |
||||||
|
Workers int `json:"workers"` |
||||||
|
Banning Banning `json:"banning"` |
||||||
|
Limits Limits `json:"limits"` |
||||||
|
ResetInterval string `json:"resetInterval"` |
||||||
|
RefreshInterval string `json:"refreshInterval"` |
||||||
|
} |
||||||
|
|
||||||
|
type Banning struct { |
||||||
|
Enabled bool `json:"enabled"` |
||||||
|
IPSet string `json:"ipset"` |
||||||
|
Timeout int64 `json:"timeout"` |
||||||
|
InvalidPercent float32 `json:"invalidPercent"` |
||||||
|
CheckThreshold uint32 `json:"checkThreshold"` |
||||||
|
MalformedLimit uint32 `json:"malformedLimit"` |
||||||
|
} |
||||||
|
|
||||||
|
type Limits struct { |
||||||
|
Enabled bool `json:"enabled"` |
||||||
|
Limit int32 `json:"limit"` |
||||||
|
Grace string `json:"grace"` |
||||||
|
LimitJump int32 `json:"limitJump"` |
||||||
|
} |
@ -0,0 +1,91 @@ |
|||||||
|
package rpc |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"encoding/json" |
||||||
|
"errors" |
||||||
|
"fmt" |
||||||
|
"io/ioutil" |
||||||
|
"net/http" |
||||||
|
"time" |
||||||
|
|
||||||
|
"../pool" |
||||||
|
) |
||||||
|
|
||||||
|
type RPCClient struct { |
||||||
|
url string |
||||||
|
client *http.Client |
||||||
|
} |
||||||
|
|
||||||
|
type GetBlockTemplateReply struct { |
||||||
|
Blob string `json:"blocktemplate_blob"` |
||||||
|
Difficulty int64 `json:"difficulty"` |
||||||
|
ReservedOffset int `json:"reserved_offset"` |
||||||
|
Height int64 `json:"height"` |
||||||
|
PrevHash string `json:"prev_hash"` |
||||||
|
} |
||||||
|
|
||||||
|
type JSONRpcResp struct { |
||||||
|
Id *json.RawMessage `json:"id"` |
||||||
|
Result *json.RawMessage `json:"result"` |
||||||
|
Error map[string]interface{} `json:"error"` |
||||||
|
} |
||||||
|
|
||||||
|
func NewRPCClient(cfg *pool.Config) *RPCClient { |
||||||
|
url := fmt.Sprintf("http://%s:%v/json_rpc", cfg.Daemon.Host, cfg.Daemon.Port) |
||||||
|
rpcClient := &RPCClient{url: url} |
||||||
|
timeout, _ := time.ParseDuration(cfg.Daemon.Timeout) |
||||||
|
rpcClient.client = &http.Client{ |
||||||
|
Timeout: timeout, |
||||||
|
} |
||||||
|
return rpcClient |
||||||
|
} |
||||||
|
|
||||||
|
func (r *RPCClient) GetBlockTemplate(reserveSize int, address string) (GetBlockTemplateReply, error) { |
||||||
|
params := map[string]interface{}{"reserve_size": reserveSize, "wallet_address": address} |
||||||
|
|
||||||
|
rpcResp, err := r.doPost(r.url, "getblocktemplate", params) |
||||||
|
var reply GetBlockTemplateReply |
||||||
|
if err != nil { |
||||||
|
return reply, err |
||||||
|
} |
||||||
|
if rpcResp.Error != nil { |
||||||
|
return reply, errors.New(string(rpcResp.Error["message"].(string))) |
||||||
|
} |
||||||
|
|
||||||
|
err = json.Unmarshal(*rpcResp.Result, &reply) |
||||||
|
return reply, err |
||||||
|
} |
||||||
|
|
||||||
|
func (r *RPCClient) SubmitBlock(hash string) (JSONRpcResp, error) { |
||||||
|
rpcResp, err := r.doPost(r.url, "submitblock", []string{hash}) |
||||||
|
if err != nil { |
||||||
|
return rpcResp, err |
||||||
|
} |
||||||
|
if rpcResp.Error != nil { |
||||||
|
return rpcResp, errors.New(string(rpcResp.Error["message"].(string))) |
||||||
|
} |
||||||
|
return rpcResp, nil |
||||||
|
} |
||||||
|
|
||||||
|
func (r *RPCClient) doPost(url string, method string, params interface{}) (JSONRpcResp, error) { |
||||||
|
jsonReq := map[string]interface{}{"id": "0", "method": method, "params": params} |
||||||
|
data, _ := json.Marshal(jsonReq) |
||||||
|
|
||||||
|
req, err := http.NewRequest("POST", url, bytes.NewBuffer(data)) |
||||||
|
req.Header.Set("Content-Length", (string)(len(data))) |
||||||
|
req.Header.Set("Content-Type", "application/json") |
||||||
|
req.Header.Set("Accept", "application/json") |
||||||
|
|
||||||
|
resp, err := r.client.Do(req) |
||||||
|
var rpcResp JSONRpcResp |
||||||
|
|
||||||
|
if err != nil { |
||||||
|
return rpcResp, err |
||||||
|
} |
||||||
|
defer resp.Body.Close() |
||||||
|
|
||||||
|
body, _ := ioutil.ReadAll(resp.Body) |
||||||
|
err = json.Unmarshal(body, &rpcResp) |
||||||
|
return rpcResp, err |
||||||
|
} |
@ -0,0 +1,135 @@ |
|||||||
|
package storage |
||||||
|
|
||||||
|
import ( |
||||||
|
"log" |
||||||
|
"strconv" |
||||||
|
"strings" |
||||||
|
"time" |
||||||
|
|
||||||
|
"gopkg.in/redis.v3" |
||||||
|
|
||||||
|
"../pool" |
||||||
|
) |
||||||
|
|
||||||
|
type RedisClient struct { |
||||||
|
client *redis.Client |
||||||
|
prefix string |
||||||
|
} |
||||||
|
|
||||||
|
func NewRedisClient(cfg *pool.Redis, prefix string) *RedisClient { |
||||||
|
client := redis.NewClient(&redis.Options{ |
||||||
|
Addr: cfg.Endpoint, |
||||||
|
Password: cfg.Password, |
||||||
|
DB: cfg.Database, |
||||||
|
PoolSize: cfg.PoolSize, |
||||||
|
}) |
||||||
|
return &RedisClient{client: client, prefix: prefix} |
||||||
|
} |
||||||
|
|
||||||
|
func (r *RedisClient) Check() { |
||||||
|
pong, err := r.client.Ping().Result() |
||||||
|
if err != nil { |
||||||
|
log.Fatalf("Can't establish Redis connection: %v", err) |
||||||
|
} |
||||||
|
log.Printf("Redis PING command reply: %v", pong) |
||||||
|
} |
||||||
|
|
||||||
|
// Always returns list of addresses. If Redis fails it will return empty list.
|
||||||
|
func (r *RedisClient) GetBlacklist() []string { |
||||||
|
cmd := r.client.SMembers(r.formatKey("blacklist")) |
||||||
|
if cmd.Err() != nil { |
||||||
|
log.Printf("Failed to get blacklist from Redis: %v", cmd.Err()) |
||||||
|
return []string{} |
||||||
|
} |
||||||
|
return cmd.Val() |
||||||
|
} |
||||||
|
|
||||||
|
// Always returns list of IPs. If Redis fails it will return empty list.
|
||||||
|
func (r *RedisClient) GetWhitelist() []string { |
||||||
|
cmd := r.client.SMembers(r.formatKey("whitelist")) |
||||||
|
if cmd.Err() != nil { |
||||||
|
log.Printf("Failed to get blacklist from Redis: %v", cmd.Err()) |
||||||
|
return []string{} |
||||||
|
} |
||||||
|
return cmd.Val() |
||||||
|
} |
||||||
|
|
||||||
|
func (r *RedisClient) WriteShare(login string, diff int64) { |
||||||
|
tx := r.client.Multi() |
||||||
|
defer tx.Close() |
||||||
|
|
||||||
|
ms := time.Now().UnixNano() / 1000000 |
||||||
|
ts := ms / 1000 |
||||||
|
|
||||||
|
_, err := tx.Exec(func() error { |
||||||
|
r.writeShare(tx, ms, ts, login, diff) |
||||||
|
return nil |
||||||
|
}) |
||||||
|
if err != nil { |
||||||
|
log.Printf("Failed to insert share data into Redis: %v", err) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (r *RedisClient) WriteBlock(login string, diff, roundDiff, height int64, hashHex string) { |
||||||
|
tx := r.client.Multi() |
||||||
|
defer tx.Close() |
||||||
|
|
||||||
|
ms := time.Now().UnixNano() / 1000000 |
||||||
|
ts := ms / 1000 |
||||||
|
|
||||||
|
cmds, err := tx.Exec(func() error { |
||||||
|
r.writeShare(tx, ms, ts, login, diff) |
||||||
|
tx.HSet(r.formatKey("stats"), "lastBlockFound", strconv.FormatInt(ms, 10)) |
||||||
|
tx.ZIncrBy(r.formatKey("finders"), 1, login) |
||||||
|
tx.Rename(r.formatKey("shares", "roundCurrent"), r.formatKey("shares", formatRound(height))) |
||||||
|
tx.HGetAllMap(r.formatKey("shares", formatRound(height))) |
||||||
|
return nil |
||||||
|
}) |
||||||
|
if err != nil { |
||||||
|
log.Printf("Failed to insert block candidate into Redis: %v", err) |
||||||
|
} else { |
||||||
|
sharesMap, _ := cmds[7].(*redis.StringStringMapCmd).Result() |
||||||
|
totalShares := int64(0) |
||||||
|
for _, v := range sharesMap { |
||||||
|
n, _ := strconv.ParseInt(v, 10, 64) |
||||||
|
totalShares += n |
||||||
|
} |
||||||
|
s := join(hashHex, ts, roundDiff, totalShares) |
||||||
|
cmd := r.client.ZAdd(r.formatKey("blocks", "candidates"), redis.Z{Score: float64(height), Member: s}) |
||||||
|
if cmd.Err() != nil { |
||||||
|
log.Printf("Failed to insert block candidate shares into Redis: %v", cmd.Err()) |
||||||
|
} else { |
||||||
|
log.Printf("Inserted block to Redis, height: %v, variance: %v/%v, %v", height, totalShares, roundDiff, cmd.Val()) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (r *RedisClient) writeShare(tx *redis.Multi, ms, ts int64, login string, diff int64) { |
||||||
|
tx.HIncrBy(r.formatKey("shares", "roundCurrent"), login, diff) |
||||||
|
tx.ZAdd(r.formatKey("hashrate"), redis.Z{Score: float64(ts), Member: join(diff, login, ms)}) |
||||||
|
tx.HIncrBy(r.formatKey("workers", login), "hashes", diff) |
||||||
|
tx.HSet(r.formatKey("workers", login), "lastShare", strconv.FormatInt(ts, 10)) |
||||||
|
} |
||||||
|
|
||||||
|
func (r *RedisClient) formatKey(args ...interface{}) string { |
||||||
|
return join(r.prefix, join(args...)) |
||||||
|
} |
||||||
|
|
||||||
|
func formatRound(height int64) string { |
||||||
|
return "round" + strconv.FormatInt(height, 10) |
||||||
|
} |
||||||
|
|
||||||
|
func join(args ...interface{}) string { |
||||||
|
s := make([]string, len(args)) |
||||||
|
for i, v := range args { |
||||||
|
switch v.(type) { |
||||||
|
case string: |
||||||
|
s[i] = v.(string) |
||||||
|
case int64: |
||||||
|
s[i] = strconv.FormatInt(v.(int64), 10) |
||||||
|
default: |
||||||
|
panic("Invalid type specified for conversion") |
||||||
|
} |
||||||
|
} |
||||||
|
return strings.Join(s, ":") |
||||||
|
} |
@ -0,0 +1,74 @@ |
|||||||
|
package storage |
||||||
|
|
||||||
|
import ( |
||||||
|
"gopkg.in/redis.v3" |
||||||
|
"os" |
||||||
|
"reflect" |
||||||
|
"strings" |
||||||
|
"testing" |
||||||
|
) |
||||||
|
|
||||||
|
var r *RedisClient |
||||||
|
|
||||||
|
func TestMain(m *testing.M) { |
||||||
|
client := redis.NewClient(&redis.Options{Addr: "127.0.0.1:6379"}) |
||||||
|
r = &RedisClient{client: client, prefix: "test"} |
||||||
|
r.client.FlushAll() |
||||||
|
|
||||||
|
os.Exit(m.Run()) |
||||||
|
} |
||||||
|
|
||||||
|
func TestWriteBlock(t *testing.T) { |
||||||
|
r.client.FlushAll() |
||||||
|
r.WriteBlock("addy", 1000, 999, 0, "abcdef") |
||||||
|
|
||||||
|
sharesRes := r.client.HGetAllMap("test:shares:round0").Val() |
||||||
|
expectedRound := map[string]string{"addy": "1000"} |
||||||
|
if !reflect.DeepEqual(sharesRes, expectedRound) { |
||||||
|
t.Errorf("Invalid round data: %v", sharesRes) |
||||||
|
} |
||||||
|
|
||||||
|
blockRes := r.client.ZRevRangeWithScores("test:blocks:candidates", 0, 99999).Val() |
||||||
|
blockRes = stripTimestampsFromZs(blockRes) |
||||||
|
expectedCandidates := []redis.Z{redis.Z{0, "abcdef:*:999:1000"}} |
||||||
|
if !reflect.DeepEqual(blockRes, expectedCandidates) { |
||||||
|
t.Errorf("Invalid candidates data: %v, expected: %v", blockRes, expectedCandidates) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func TestWriteBlockAtSameHeight(t *testing.T) { |
||||||
|
r.client.FlushAll() |
||||||
|
r.WriteBlock("addy", 1000, 999, 1, "00000000") |
||||||
|
r.WriteBlock("addy", 2000, 999, 1, "00000001") |
||||||
|
r.WriteBlock("addy", 3000, 999, 1, "00000002") |
||||||
|
|
||||||
|
sharesRes := r.client.HGetAllMap("test:shares:round1").Val() |
||||||
|
expectedRound := map[string]string{"addy": "3000"} |
||||||
|
if !reflect.DeepEqual(sharesRes, expectedRound) { |
||||||
|
t.Errorf("Invalid round data: %v", sharesRes) |
||||||
|
} |
||||||
|
|
||||||
|
blockRes := r.client.ZRevRangeWithScores("test:blocks:candidates", 0, 99999).Val() |
||||||
|
blockRes = stripTimestampsFromZs(blockRes) |
||||||
|
expectedBlocks := []redis.Z{redis.Z{1, "00000002:*:999:3000"}, redis.Z{1, "00000001:*:999:2000"}, redis.Z{1, "00000000:*:999:1000"}} |
||||||
|
if len(blockRes) != 3 { |
||||||
|
t.Errorf("Invalid number of candidates: %v, expected: %v", len(blockRes), 3) |
||||||
|
} |
||||||
|
if !reflect.DeepEqual(blockRes, expectedBlocks) { |
||||||
|
t.Errorf("Invalid candidates data: %v, expected: %v", blockRes, expectedBlocks) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func stripTimestampFromZ(z redis.Z) redis.Z { |
||||||
|
k := strings.Split(z.Member, ":") |
||||||
|
res := []string{k[0], "*", k[2], k[3]} |
||||||
|
return redis.Z{Score: z.Score, Member: strings.Join(res, ":")} |
||||||
|
} |
||||||
|
|
||||||
|
func stripTimestampsFromZs(zs []redis.Z) []redis.Z { |
||||||
|
var res []redis.Z |
||||||
|
for _, z := range zs { |
||||||
|
res = append(res, stripTimestampFromZ(z)) |
||||||
|
} |
||||||
|
return res |
||||||
|
} |
@ -0,0 +1,66 @@ |
|||||||
|
package stratum |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"encoding/binary" |
||||||
|
"encoding/hex" |
||||||
|
"log" |
||||||
|
"sync/atomic" |
||||||
|
|
||||||
|
"../../cnutil" |
||||||
|
) |
||||||
|
|
||||||
|
type BlockTemplate struct { |
||||||
|
Blob string |
||||||
|
Difficulty int64 |
||||||
|
Height int64 |
||||||
|
ReservedOffset int |
||||||
|
PrevHash string |
||||||
|
Buffer []byte |
||||||
|
ExtraNonce uint32 |
||||||
|
} |
||||||
|
|
||||||
|
func (b *BlockTemplate) nextBlob() (string, uint32) { |
||||||
|
// Preventing race by using atomic op here
|
||||||
|
// No need for using locks, because this is only one write to BT and it's atomic
|
||||||
|
extraNonce := atomic.AddUint32(&b.ExtraNonce, 1) |
||||||
|
extraBuff := new(bytes.Buffer) |
||||||
|
binary.Write(extraBuff, binary.BigEndian, extraNonce) |
||||||
|
blobBuff := make([]byte, len(b.Buffer)) |
||||||
|
copy(blobBuff, b.Buffer) // We never write to this buffer to prevent race
|
||||||
|
copy(blobBuff[b.ReservedOffset:], extraBuff.Bytes()) |
||||||
|
blob := cnutil.ConvertBlob(blobBuff) |
||||||
|
return hex.EncodeToString(blob), extraNonce |
||||||
|
} |
||||||
|
|
||||||
|
func (s *StratumServer) fetchBlockTemplate() bool { |
||||||
|
reply, err := s.rpc.GetBlockTemplate(8, s.config.Address) |
||||||
|
if err != nil { |
||||||
|
log.Printf("Error while refreshing block template: %s", err) |
||||||
|
return false |
||||||
|
} |
||||||
|
t := s.currentBlockTemplate() |
||||||
|
|
||||||
|
if t.PrevHash == reply.PrevHash { |
||||||
|
// Fallback to height comparison
|
||||||
|
if len(reply.PrevHash) == 0 && reply.Height > t.Height { |
||||||
|
log.Printf("New block to mine at height %v, diff: %v", reply.Height, reply.Difficulty) |
||||||
|
} else { |
||||||
|
return false |
||||||
|
} |
||||||
|
} else { |
||||||
|
log.Printf("New block to mine at height %v, diff: %v, prev_hash: %s", reply.Height, reply.Difficulty, reply.PrevHash) |
||||||
|
} |
||||||
|
newTemplate := BlockTemplate{ |
||||||
|
Blob: reply.Blob, |
||||||
|
Difficulty: reply.Difficulty, |
||||||
|
Height: reply.Height, |
||||||
|
PrevHash: reply.PrevHash, |
||||||
|
ReservedOffset: reply.ReservedOffset, |
||||||
|
} |
||||||
|
newTemplate.Buffer, _ = hex.DecodeString(reply.Blob) |
||||||
|
copy(newTemplate.Buffer[reply.ReservedOffset+4:reply.ReservedOffset+7], s.instanceId) |
||||||
|
newTemplate.ExtraNonce = 0 |
||||||
|
s.blockTemplate.Store(&newTemplate) |
||||||
|
return true |
||||||
|
} |
@ -0,0 +1,127 @@ |
|||||||
|
package stratum |
||||||
|
|
||||||
|
import ( |
||||||
|
"log" |
||||||
|
"regexp" |
||||||
|
"strings" |
||||||
|
|
||||||
|
"../util" |
||||||
|
) |
||||||
|
|
||||||
|
var noncePattern *regexp.Regexp |
||||||
|
|
||||||
|
func init() { |
||||||
|
noncePattern, _ = regexp.Compile("^[0-9a-f]{8}$") |
||||||
|
} |
||||||
|
|
||||||
|
func (s *StratumServer) handleLoginRPC(cs *Session, params *LoginParams) (reply *JobReply, errorReply *ErrorReply) { |
||||||
|
if !util.ValidateAddress(params.Login, s.config.Address) { |
||||||
|
errorReply = &ErrorReply{Code: -1, Message: "Invalid address used for login", Close: true} |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
if !s.policy.ApplyLoginPolicy(params.Login, cs.ip) { |
||||||
|
errorReply = &ErrorReply{Code: -1, Message: "Your address blacklisted", Close: true} |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
miner := NewMiner(params.Login, params.Pass, s.port.Difficulty, cs.ip) |
||||||
|
miner.Session = cs |
||||||
|
s.registerMiner(miner) |
||||||
|
miner.heartbeat() |
||||||
|
|
||||||
|
log.Printf("Miner connected %s@%s", params.Login, miner.IP) |
||||||
|
|
||||||
|
reply = &JobReply{} |
||||||
|
reply.Id = miner.Id |
||||||
|
reply.Job = miner.getJob(s) |
||||||
|
reply.Status = "OK" |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
func (s *StratumServer) handleGetJobRPC(cs *Session, params *GetJobParams) (reply *JobReplyData, errorReply *ErrorReply) { |
||||||
|
miner, ok := s.miners.Get(params.Id) |
||||||
|
if !ok { |
||||||
|
errorReply = &ErrorReply{Code: -1, Message: "Unauthenticated", Close: true} |
||||||
|
return |
||||||
|
} |
||||||
|
miner.heartbeat() |
||||||
|
reply = miner.getJob(s) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
func (s *StratumServer) handleSubmitRPC(cs *Session, params *SubmitParams) (reply *SubmitReply, errorReply *ErrorReply) { |
||||||
|
miner, ok := s.miners.Get(params.Id) |
||||||
|
if !ok { |
||||||
|
errorReply = &ErrorReply{Code: -1, Message: "Unauthenticated", Close: true} |
||||||
|
return |
||||||
|
} |
||||||
|
miner.heartbeat() |
||||||
|
|
||||||
|
job := miner.findJob(params.JobId) |
||||||
|
if job == nil { |
||||||
|
errorReply = &ErrorReply{Code: -1, Message: "Invalid job id", Close: true} |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
if !noncePattern.MatchString(params.Nonce) { |
||||||
|
errorReply = &ErrorReply{Code: -1, Message: "Malformed nonce", Close: true} |
||||||
|
return |
||||||
|
} |
||||||
|
nonce := strings.ToLower(params.Nonce) |
||||||
|
exist := job.submit(nonce) |
||||||
|
if exist { |
||||||
|
errorReply = &ErrorReply{Code: -1, Message: "Duplicate share", Close: true} |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
t := s.currentBlockTemplate() |
||||||
|
if job.Height != t.Height { |
||||||
|
log.Printf("Block expired for height %v %s@%s", job.Height, miner.Login, miner.IP) |
||||||
|
errorReply = &ErrorReply{Code: -1, Message: "Block expired", Close: false} |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
validShare := miner.processShare(s, job, t, nonce, params.Result) |
||||||
|
ok = s.policy.ApplySharePolicy(miner.IP, validShare) |
||||||
|
|
||||||
|
if !validShare { |
||||||
|
errorReply = &ErrorReply{Code: -1, Message: "Low difficulty share", Close: !ok} |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
reply = &SubmitReply{Status: "OK"} |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
func (s *StratumServer) handleUnknownRPC(cs *Session, req *JSONRpcReq) *ErrorReply { |
||||||
|
log.Printf("Unknown RPC method: %v", req) |
||||||
|
return &ErrorReply{Code: -1, Message: "Invalid method", Close: true} |
||||||
|
} |
||||||
|
|
||||||
|
func (s *StratumServer) broadcastNewJobs() { |
||||||
|
log.Printf("Broadcasting new jobs to %v miners", s.miners.Count()) |
||||||
|
bcast := make(chan int, 1024) |
||||||
|
n := 0 |
||||||
|
|
||||||
|
for m := range s.miners.IterBuffered() { |
||||||
|
n++ |
||||||
|
bcast <- n |
||||||
|
go func(miner *Miner) { |
||||||
|
reply := miner.getJob(s) |
||||||
|
err := miner.Session.pushMessage("job", &reply) |
||||||
|
<-bcast |
||||||
|
if err != nil { |
||||||
|
log.Printf("Job transmit error to %v@%v: %v", miner.Login, miner.IP, err) |
||||||
|
s.removeMiner(miner.Id) |
||||||
|
} |
||||||
|
}(m.Val) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (s *StratumServer) refreshBlockTemplate(bcast bool) { |
||||||
|
newBlock := s.fetchBlockTemplate() |
||||||
|
if newBlock && bcast { |
||||||
|
s.broadcastNewJobs() |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,143 @@ |
|||||||
|
package stratum |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"encoding/binary" |
||||||
|
"encoding/hex" |
||||||
|
"log" |
||||||
|
"sync" |
||||||
|
"sync/atomic" |
||||||
|
|
||||||
|
"../../cnutil" |
||||||
|
"../../hashing" |
||||||
|
"../util" |
||||||
|
) |
||||||
|
|
||||||
|
type Job struct { |
||||||
|
sync.RWMutex |
||||||
|
Id string |
||||||
|
ExtraNonce uint32 |
||||||
|
Height int64 |
||||||
|
Difficulty int64 |
||||||
|
Submissions map[string]bool |
||||||
|
} |
||||||
|
|
||||||
|
type Miner struct { |
||||||
|
sync.RWMutex |
||||||
|
Id string |
||||||
|
Login string |
||||||
|
Pass string |
||||||
|
IP string |
||||||
|
Difficulty int64 |
||||||
|
ValidJobs []*Job |
||||||
|
LastBlockHeight int64 |
||||||
|
Target uint32 |
||||||
|
TargetHex string |
||||||
|
LastBeat int64 |
||||||
|
Session *Session |
||||||
|
} |
||||||
|
|
||||||
|
func (job *Job) submit(nonce string) bool { |
||||||
|
job.Lock() |
||||||
|
defer job.Unlock() |
||||||
|
_, exist := job.Submissions[nonce] |
||||||
|
if exist { |
||||||
|
return true |
||||||
|
} |
||||||
|
job.Submissions[nonce] = true |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
func NewMiner(login, pass string, diff int64, ip string) *Miner { |
||||||
|
id := util.Random() |
||||||
|
miner := &Miner{Id: id, Login: login, Pass: pass, Difficulty: diff, IP: ip} |
||||||
|
target, targetHex := util.GetTargetHex(diff) |
||||||
|
miner.Target = target |
||||||
|
miner.TargetHex = targetHex |
||||||
|
return miner |
||||||
|
} |
||||||
|
|
||||||
|
func (m *Miner) pushJob(job *Job) { |
||||||
|
m.Lock() |
||||||
|
defer m.Unlock() |
||||||
|
m.ValidJobs = append(m.ValidJobs, job) |
||||||
|
|
||||||
|
if len(m.ValidJobs) > 4 { |
||||||
|
m.ValidJobs = m.ValidJobs[1:] |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (m *Miner) getJob(s *StratumServer) *JobReplyData { |
||||||
|
t := s.currentBlockTemplate() |
||||||
|
height := atomic.SwapInt64(&m.LastBlockHeight, t.Height) |
||||||
|
|
||||||
|
if height == t.Height { |
||||||
|
return &JobReplyData{} |
||||||
|
} |
||||||
|
|
||||||
|
blob, extraNonce := t.nextBlob() |
||||||
|
job := &Job{Id: util.Random(), ExtraNonce: extraNonce, Height: t.Height, Difficulty: m.Difficulty} |
||||||
|
job.Submissions = make(map[string]bool) |
||||||
|
m.pushJob(job) |
||||||
|
reply := &JobReplyData{JobId: job.Id, Blob: blob, Target: m.TargetHex} |
||||||
|
return reply |
||||||
|
} |
||||||
|
|
||||||
|
func (m *Miner) heartbeat() { |
||||||
|
now := util.MakeTimestamp() |
||||||
|
atomic.StoreInt64(&m.LastBeat, now) |
||||||
|
} |
||||||
|
|
||||||
|
func (m *Miner) findJob(id string) *Job { |
||||||
|
m.RLock() |
||||||
|
defer m.RUnlock() |
||||||
|
for _, job := range m.ValidJobs { |
||||||
|
if job.Id == id { |
||||||
|
return job |
||||||
|
} |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (m *Miner) processShare(s *StratumServer, job *Job, t *BlockTemplate, nonce string, result string) bool { |
||||||
|
shareBuff := make([]byte, len(t.Buffer)) |
||||||
|
copy(shareBuff, t.Buffer) |
||||||
|
|
||||||
|
extraBuff := new(bytes.Buffer) |
||||||
|
binary.Write(extraBuff, binary.BigEndian, job.ExtraNonce) |
||||||
|
copy(shareBuff[t.ReservedOffset:], extraBuff.Bytes()) |
||||||
|
|
||||||
|
nonceBuff, _ := hex.DecodeString(nonce) |
||||||
|
copy(shareBuff[39:], nonceBuff) |
||||||
|
|
||||||
|
convertedBlob := cnutil.ConvertBlob(shareBuff) |
||||||
|
hashBytes := hashing.Hash(convertedBlob, false) |
||||||
|
|
||||||
|
if hex.EncodeToString(hashBytes) != result { |
||||||
|
log.Printf("Bad hash from miner %v@%v", m.Login, m.IP) |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
hashDiff := util.GetHashDifficulty(hashBytes).Int64() // FIXME: Will return max int64 value if overflows
|
||||||
|
block := hashDiff >= t.Difficulty |
||||||
|
if block { |
||||||
|
_, err := s.rpc.SubmitBlock(hex.EncodeToString(shareBuff)) |
||||||
|
if err != nil { |
||||||
|
log.Printf("Block submission failure at height %v: %v", t.Height, err) |
||||||
|
} else { |
||||||
|
blockFastHash := hex.EncodeToString(hashing.FastHash(convertedBlob)) |
||||||
|
s.storage.WriteBlock(m.Login, job.Difficulty, t.Difficulty, t.Height, blockFastHash) |
||||||
|
// Immediately refresh current BT and send new jobs
|
||||||
|
s.refreshBlockTemplate(true) |
||||||
|
log.Printf("Block %v found at height %v by miner %v@%v", blockFastHash[0:6], t.Height, m.Login, m.IP) |
||||||
|
} |
||||||
|
} else if hashDiff < job.Difficulty { |
||||||
|
log.Printf("Rejected low difficulty share of %v from %v@%v", hashDiff, m.Login, m.IP) |
||||||
|
return false |
||||||
|
} else { |
||||||
|
s.storage.WriteShare(m.Login, job.Difficulty) |
||||||
|
} |
||||||
|
|
||||||
|
log.Printf("Valid share at difficulty %v/%v", s.port.Difficulty, hashDiff) |
||||||
|
return true |
||||||
|
} |
@ -0,0 +1,136 @@ |
|||||||
|
// Generated from https://github.com/streamrail/concurrent-map
|
||||||
|
package stratum |
||||||
|
|
||||||
|
import ( |
||||||
|
"hash/fnv" |
||||||
|
"sync" |
||||||
|
) |
||||||
|
|
||||||
|
var SHARD_COUNT = 32 |
||||||
|
|
||||||
|
// TODO: Add Keys function which returns an array of keys for the map.
|
||||||
|
|
||||||
|
// A "thread" safe map of type string:*Miner.
|
||||||
|
// To avoid lock bottlenecks this map is dived to several (SHARD_COUNT) map shards.
|
||||||
|
type MinersMap []*MinersMapShared |
||||||
|
type MinersMapShared struct { |
||||||
|
items map[string]*Miner |
||||||
|
sync.RWMutex // Read Write mutex, guards access to internal map.
|
||||||
|
} |
||||||
|
|
||||||
|
// Creates a new concurrent map.
|
||||||
|
func NewMinersMap() MinersMap { |
||||||
|
m := make(MinersMap, SHARD_COUNT) |
||||||
|
for i := 0; i < SHARD_COUNT; i++ { |
||||||
|
m[i] = &MinersMapShared{items: make(map[string]*Miner)} |
||||||
|
} |
||||||
|
return m |
||||||
|
} |
||||||
|
|
||||||
|
// Returns shard under given key
|
||||||
|
func (m MinersMap) GetShard(key string) *MinersMapShared { |
||||||
|
hasher := fnv.New32() |
||||||
|
hasher.Write([]byte(key)) |
||||||
|
return m[int(hasher.Sum32())%SHARD_COUNT] |
||||||
|
} |
||||||
|
|
||||||
|
// Sets the given value under the specified key.
|
||||||
|
func (m *MinersMap) Set(key string, value *Miner) { |
||||||
|
// Get map shard.
|
||||||
|
shard := m.GetShard(key) |
||||||
|
shard.Lock() |
||||||
|
defer shard.Unlock() |
||||||
|
shard.items[key] = value |
||||||
|
} |
||||||
|
|
||||||
|
// Retrieves an element from map under given key.
|
||||||
|
func (m MinersMap) Get(key string) (*Miner, bool) { |
||||||
|
// Get shard
|
||||||
|
shard := m.GetShard(key) |
||||||
|
shard.RLock() |
||||||
|
defer shard.RUnlock() |
||||||
|
|
||||||
|
// Get item from shard.
|
||||||
|
val, ok := shard.items[key] |
||||||
|
return val, ok |
||||||
|
} |
||||||
|
|
||||||
|
// Returns the number of elements within the map.
|
||||||
|
func (m MinersMap) Count() int { |
||||||
|
count := 0 |
||||||
|
for i := 0; i < SHARD_COUNT; i++ { |
||||||
|
shard := m[i] |
||||||
|
shard.RLock() |
||||||
|
count += len(shard.items) |
||||||
|
shard.RUnlock() |
||||||
|
} |
||||||
|
return count |
||||||
|
} |
||||||
|
|
||||||
|
// Looks up an item under specified key
|
||||||
|
func (m *MinersMap) Has(key string) bool { |
||||||
|
// Get shard
|
||||||
|
shard := m.GetShard(key) |
||||||
|
shard.RLock() |
||||||
|
defer shard.RUnlock() |
||||||
|
|
||||||
|
// See if element is within shard.
|
||||||
|
_, ok := shard.items[key] |
||||||
|
return ok |
||||||
|
} |
||||||
|
|
||||||
|
// Removes an element from the map.
|
||||||
|
func (m *MinersMap) Remove(key string) { |
||||||
|
// Try to get shard.
|
||||||
|
shard := m.GetShard(key) |
||||||
|
shard.Lock() |
||||||
|
defer shard.Unlock() |
||||||
|
delete(shard.items, key) |
||||||
|
} |
||||||
|
|
||||||
|
// Checks if map is empty.
|
||||||
|
func (m *MinersMap) IsEmpty() bool { |
||||||
|
return m.Count() == 0 |
||||||
|
} |
||||||
|
|
||||||
|
// Used by the Iter & IterBuffered functions to wrap two variables together over a channel,
|
||||||
|
type Tuple struct { |
||||||
|
Key string |
||||||
|
Val *Miner |
||||||
|
} |
||||||
|
|
||||||
|
// Returns an iterator which could be used in a for range loop.
|
||||||
|
func (m MinersMap) Iter() <-chan Tuple { |
||||||
|
ch := make(chan Tuple) |
||||||
|
go func() { |
||||||
|
// Foreach shard.
|
||||||
|
for _, shard := range m { |
||||||
|
// Foreach key, value pair.
|
||||||
|
shard.RLock() |
||||||
|
for key, val := range shard.items { |
||||||
|
ch <- Tuple{key, val} |
||||||
|
} |
||||||
|
shard.RUnlock() |
||||||
|
} |
||||||
|
close(ch) |
||||||
|
}() |
||||||
|
return ch |
||||||
|
} |
||||||
|
|
||||||
|
// Returns a buffered iterator which could be used in a for range loop.
|
||||||
|
func (m MinersMap) IterBuffered() <-chan Tuple { |
||||||
|
ch := make(chan Tuple, m.Count()) |
||||||
|
go func() { |
||||||
|
// Foreach shard.
|
||||||
|
for _, shard := range m { |
||||||
|
// Foreach key, value pair.
|
||||||
|
shard.RLock() |
||||||
|
for key, val := range shard.items { |
||||||
|
ch <- Tuple{key, val} |
||||||
|
} |
||||||
|
shard.RUnlock() |
||||||
|
} |
||||||
|
close(ch) |
||||||
|
}() |
||||||
|
return ch |
||||||
|
} |
@ -0,0 +1,271 @@ |
|||||||
|
package policy |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"log" |
||||||
|
"os/exec" |
||||||
|
"strings" |
||||||
|
"sync" |
||||||
|
"sync/atomic" |
||||||
|
"time" |
||||||
|
|
||||||
|
"../../pool" |
||||||
|
"../../storage" |
||||||
|
"../../util" |
||||||
|
) |
||||||
|
|
||||||
|
type Stats struct { |
||||||
|
sync.Mutex |
||||||
|
ValidShares uint32 |
||||||
|
InvalidShares uint32 |
||||||
|
Malformed uint32 |
||||||
|
ConnLimit int32 |
||||||
|
FailsCount uint32 |
||||||
|
LastBeat int64 |
||||||
|
Banned uint32 |
||||||
|
BannedAt int64 |
||||||
|
} |
||||||
|
|
||||||
|
type PolicyServer struct { |
||||||
|
sync.RWMutex |
||||||
|
config *pool.Policy |
||||||
|
stats StatsMap |
||||||
|
banChannel chan string |
||||||
|
startedAt int64 |
||||||
|
grace int64 |
||||||
|
timeout int64 |
||||||
|
blacklist []string |
||||||
|
whitelist []string |
||||||
|
storage *storage.RedisClient |
||||||
|
} |
||||||
|
|
||||||
|
func Start(cfg *pool.Config, storage *storage.RedisClient) *PolicyServer { |
||||||
|
s := &PolicyServer{config: &cfg.Policy, startedAt: util.MakeTimestamp()} |
||||||
|
grace, _ := time.ParseDuration(cfg.Policy.Limits.Grace) |
||||||
|
s.grace = int64(grace / time.Millisecond) |
||||||
|
s.banChannel = make(chan string, 64) |
||||||
|
s.stats = NewStatsMap() |
||||||
|
s.storage = storage |
||||||
|
s.refreshState() |
||||||
|
|
||||||
|
timeout, _ := time.ParseDuration(s.config.ResetInterval) |
||||||
|
s.timeout = int64(timeout / time.Millisecond) |
||||||
|
|
||||||
|
resetIntv, _ := time.ParseDuration(s.config.ResetInterval) |
||||||
|
resetTimer := time.NewTimer(resetIntv) |
||||||
|
log.Printf("Set policy stats reset every %v", resetIntv) |
||||||
|
|
||||||
|
refreshIntv, _ := time.ParseDuration(s.config.RefreshInterval) |
||||||
|
refreshTimer := time.NewTimer(refreshIntv) |
||||||
|
log.Printf("Set policy state refresh every %v", refreshIntv) |
||||||
|
|
||||||
|
go func() { |
||||||
|
for { |
||||||
|
select { |
||||||
|
case <-resetTimer.C: |
||||||
|
s.resetStats() |
||||||
|
resetTimer.Reset(resetIntv) |
||||||
|
case <-refreshTimer.C: |
||||||
|
s.refreshState() |
||||||
|
refreshTimer.Reset(refreshIntv) |
||||||
|
} |
||||||
|
} |
||||||
|
}() |
||||||
|
|
||||||
|
for i := 0; i < s.config.Workers; i++ { |
||||||
|
s.startPolicyWorker() |
||||||
|
} |
||||||
|
log.Printf("Running with %v policy workers", s.config.Workers) |
||||||
|
return s |
||||||
|
} |
||||||
|
|
||||||
|
func (s *PolicyServer) startPolicyWorker() { |
||||||
|
go func() { |
||||||
|
for { |
||||||
|
select { |
||||||
|
case ip := <-s.banChannel: |
||||||
|
s.doBan(ip) |
||||||
|
} |
||||||
|
} |
||||||
|
}() |
||||||
|
} |
||||||
|
|
||||||
|
func (s *PolicyServer) resetStats() { |
||||||
|
now := util.MakeTimestamp() |
||||||
|
banningTimeout := s.config.Banning.Timeout * 1000 |
||||||
|
total := 0 |
||||||
|
|
||||||
|
for m := range s.stats.IterBuffered() { |
||||||
|
lastBeat := atomic.LoadInt64(&m.Val.LastBeat) |
||||||
|
bannedAt := atomic.LoadInt64(&m.Val.BannedAt) |
||||||
|
|
||||||
|
if now-bannedAt >= banningTimeout { |
||||||
|
atomic.StoreInt64(&m.Val.BannedAt, 0) |
||||||
|
if atomic.CompareAndSwapUint32(&m.Val.Banned, 1, 0) { |
||||||
|
log.Printf("Ban dropped for %v", m.Key) |
||||||
|
} |
||||||
|
} |
||||||
|
if now-lastBeat >= s.timeout { |
||||||
|
s.stats.Remove(m.Key) |
||||||
|
total++ |
||||||
|
} |
||||||
|
} |
||||||
|
log.Printf("Flushed stats for %v IP addresses", total) |
||||||
|
} |
||||||
|
|
||||||
|
func (s *PolicyServer) refreshState() { |
||||||
|
s.Lock() |
||||||
|
defer s.Unlock() |
||||||
|
|
||||||
|
s.blacklist = s.storage.GetBlacklist() |
||||||
|
s.whitelist = s.storage.GetWhitelist() |
||||||
|
log.Println("Policy state refresh complete") |
||||||
|
} |
||||||
|
|
||||||
|
func (s *PolicyServer) NewStats() *Stats { |
||||||
|
x := &Stats{ |
||||||
|
ConnLimit: s.config.Limits.Limit, |
||||||
|
Malformed: s.config.Banning.MalformedLimit, |
||||||
|
} |
||||||
|
x.heartbeat() |
||||||
|
return x |
||||||
|
} |
||||||
|
|
||||||
|
func (s *PolicyServer) Get(ip string) *Stats { |
||||||
|
if x, ok := s.stats.Get(ip); ok { |
||||||
|
x.heartbeat() |
||||||
|
return x |
||||||
|
} |
||||||
|
x := s.NewStats() |
||||||
|
s.stats.Set(ip, x) |
||||||
|
return x |
||||||
|
} |
||||||
|
|
||||||
|
func (s *PolicyServer) ApplyLimitPolicy(ip string) bool { |
||||||
|
if !s.config.Limits.Enabled { |
||||||
|
return true |
||||||
|
} |
||||||
|
now := util.MakeTimestamp() |
||||||
|
if now-s.startedAt > s.grace { |
||||||
|
return s.Get(ip).decrLimit() > 0 |
||||||
|
} |
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
func (s *PolicyServer) ApplyLoginPolicy(addy, ip string) bool { |
||||||
|
if s.InBlackList(addy) { |
||||||
|
x := s.Get(ip) |
||||||
|
s.forceBan(x, ip) |
||||||
|
return false |
||||||
|
} |
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
func (s *PolicyServer) ApplyMalformedPolicy(ip string) { |
||||||
|
x := s.Get(ip) |
||||||
|
n := x.incrMalformed() |
||||||
|
if n >= s.config.Banning.MalformedLimit { |
||||||
|
s.forceBan(x, ip) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (s *PolicyServer) ApplySharePolicy(ip string, validShare bool) bool { |
||||||
|
x := s.Get(ip) |
||||||
|
if validShare && s.config.Limits.Enabled { |
||||||
|
s.Get(ip).incrLimit(s.config.Limits.LimitJump) |
||||||
|
} |
||||||
|
x.Lock() |
||||||
|
|
||||||
|
if validShare { |
||||||
|
x.ValidShares++ |
||||||
|
if s.config.Limits.Enabled { |
||||||
|
x.incrLimit(s.config.Limits.LimitJump) |
||||||
|
} |
||||||
|
} else { |
||||||
|
x.InvalidShares++ |
||||||
|
} |
||||||
|
|
||||||
|
totalShares := x.ValidShares + x.InvalidShares |
||||||
|
if totalShares < s.config.Banning.CheckThreshold { |
||||||
|
x.Unlock() |
||||||
|
return true |
||||||
|
} |
||||||
|
validShares := float32(x.ValidShares) |
||||||
|
invalidShares := float32(x.InvalidShares) |
||||||
|
x.resetShares() |
||||||
|
x.Unlock() |
||||||
|
|
||||||
|
if invalidShares == 0 { |
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
// Can be +Inf or value, previous check prevents NaN
|
||||||
|
ratio := invalidShares / validShares |
||||||
|
|
||||||
|
if ratio >= s.config.Banning.InvalidPercent/100.0 { |
||||||
|
s.forceBan(x, ip) |
||||||
|
return false |
||||||
|
} |
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
func (x *Stats) resetShares() { |
||||||
|
x.ValidShares = 0 |
||||||
|
x.InvalidShares = 0 |
||||||
|
} |
||||||
|
|
||||||
|
func (s *PolicyServer) forceBan(x *Stats, ip string) { |
||||||
|
if !s.config.Banning.Enabled || s.InWhiteList(ip) { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
if atomic.CompareAndSwapUint32(&x.Banned, 0, 1) { |
||||||
|
if len(s.config.Banning.IPSet) > 0 { |
||||||
|
s.banChannel <- ip |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (x *Stats) incrLimit(n int32) { |
||||||
|
atomic.AddInt32(&x.ConnLimit, n) |
||||||
|
} |
||||||
|
|
||||||
|
func (x *Stats) incrMalformed() uint32 { |
||||||
|
return atomic.AddUint32(&x.Malformed, 1) |
||||||
|
} |
||||||
|
|
||||||
|
func (x *Stats) decrLimit() int32 { |
||||||
|
return atomic.AddInt32(&x.ConnLimit, -1) |
||||||
|
} |
||||||
|
|
||||||
|
func (s *PolicyServer) InBlackList(addy string) bool { |
||||||
|
s.RLock() |
||||||
|
defer s.RUnlock() |
||||||
|
return util.StringInSlice(addy, s.blacklist) |
||||||
|
} |
||||||
|
|
||||||
|
func (s *PolicyServer) InWhiteList(ip string) bool { |
||||||
|
s.RLock() |
||||||
|
defer s.RUnlock() |
||||||
|
return util.StringInSlice(ip, s.whitelist) |
||||||
|
} |
||||||
|
|
||||||
|
func (s *PolicyServer) doBan(ip string) { |
||||||
|
set, timeout := s.config.Banning.IPSet, s.config.Banning.Timeout |
||||||
|
cmd := fmt.Sprintf("sudo ipset add %s %s timeout %v -!", set, ip, timeout) |
||||||
|
args := strings.Fields(cmd) |
||||||
|
head := args[0] |
||||||
|
args = args[1:] |
||||||
|
|
||||||
|
log.Printf("Banned %v with timeout %v", ip, timeout) |
||||||
|
|
||||||
|
_, err := exec.Command(head, args...).Output() |
||||||
|
if err != nil { |
||||||
|
log.Printf("CMD Error: %s", err) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (x *Stats) heartbeat() { |
||||||
|
now := util.MakeTimestamp() |
||||||
|
atomic.StoreInt64(&x.LastBeat, now) |
||||||
|
} |
@ -0,0 +1,136 @@ |
|||||||
|
// Generated from https://github.com/streamrail/concurrent-map
|
||||||
|
package policy |
||||||
|
|
||||||
|
import ( |
||||||
|
"hash/fnv" |
||||||
|
"sync" |
||||||
|
) |
||||||
|
|
||||||
|
var SHARD_COUNT = 32 |
||||||
|
|
||||||
|
// TODO: Add Keys function which returns an array of keys for the map.
|
||||||
|
|
||||||
|
// A "thread" safe map of type string:*Stats.
|
||||||
|
// To avoid lock bottlenecks this map is dived to several (SHARD_COUNT) map shards.
|
||||||
|
type StatsMap []*StatsMapShared |
||||||
|
type StatsMapShared struct { |
||||||
|
items map[string]*Stats |
||||||
|
sync.RWMutex // Read Write mutex, guards access to internal map.
|
||||||
|
} |
||||||
|
|
||||||
|
// Creates a new concurrent map.
|
||||||
|
func NewStatsMap() StatsMap { |
||||||
|
m := make(StatsMap, SHARD_COUNT) |
||||||
|
for i := 0; i < SHARD_COUNT; i++ { |
||||||
|
m[i] = &StatsMapShared{items: make(map[string]*Stats)} |
||||||
|
} |
||||||
|
return m |
||||||
|
} |
||||||
|
|
||||||
|
// Returns shard under given key
|
||||||
|
func (m StatsMap) GetShard(key string) *StatsMapShared { |
||||||
|
hasher := fnv.New32() |
||||||
|
hasher.Write([]byte(key)) |
||||||
|
return m[int(hasher.Sum32())%SHARD_COUNT] |
||||||
|
} |
||||||
|
|
||||||
|
// Sets the given value under the specified key.
|
||||||
|
func (m *StatsMap) Set(key string, value *Stats) { |
||||||
|
// Get map shard.
|
||||||
|
shard := m.GetShard(key) |
||||||
|
shard.Lock() |
||||||
|
defer shard.Unlock() |
||||||
|
shard.items[key] = value |
||||||
|
} |
||||||
|
|
||||||
|
// Retrieves an element from map under given key.
|
||||||
|
func (m StatsMap) Get(key string) (*Stats, bool) { |
||||||
|
// Get shard
|
||||||
|
shard := m.GetShard(key) |
||||||
|
shard.RLock() |
||||||
|
defer shard.RUnlock() |
||||||
|
|
||||||
|
// Get item from shard.
|
||||||
|
val, ok := shard.items[key] |
||||||
|
return val, ok |
||||||
|
} |
||||||
|
|
||||||
|
// Returns the number of elements within the map.
|
||||||
|
func (m StatsMap) Count() int { |
||||||
|
count := 0 |
||||||
|
for i := 0; i < SHARD_COUNT; i++ { |
||||||
|
shard := m[i] |
||||||
|
shard.RLock() |
||||||
|
count += len(shard.items) |
||||||
|
shard.RUnlock() |
||||||
|
} |
||||||
|
return count |
||||||
|
} |
||||||
|
|
||||||
|
// Looks up an item under specified key
|
||||||
|
func (m *StatsMap) Has(key string) bool { |
||||||
|
// Get shard
|
||||||
|
shard := m.GetShard(key) |
||||||
|
shard.RLock() |
||||||
|
defer shard.RUnlock() |
||||||
|
|
||||||
|
// See if element is within shard.
|
||||||
|
_, ok := shard.items[key] |
||||||
|
return ok |
||||||
|
} |
||||||
|
|
||||||
|
// Removes an element from the map.
|
||||||
|
func (m *StatsMap) Remove(key string) { |
||||||
|
// Try to get shard.
|
||||||
|
shard := m.GetShard(key) |
||||||
|
shard.Lock() |
||||||
|
defer shard.Unlock() |
||||||
|
delete(shard.items, key) |
||||||
|
} |
||||||
|
|
||||||
|
// Checks if map is empty.
|
||||||
|
func (m *StatsMap) IsEmpty() bool { |
||||||
|
return m.Count() == 0 |
||||||
|
} |
||||||
|
|
||||||
|
// Used by the Iter & IterBuffered functions to wrap two variables together over a channel,
|
||||||
|
type Tuple struct { |
||||||
|
Key string |
||||||
|
Val *Stats |
||||||
|
} |
||||||
|
|
||||||
|
// Returns an iterator which could be used in a for range loop.
|
||||||
|
func (m StatsMap) Iter() <-chan Tuple { |
||||||
|
ch := make(chan Tuple) |
||||||
|
go func() { |
||||||
|
// Foreach shard.
|
||||||
|
for _, shard := range m { |
||||||
|
// Foreach key, value pair.
|
||||||
|
shard.RLock() |
||||||
|
for key, val := range shard.items { |
||||||
|
ch <- Tuple{key, val} |
||||||
|
} |
||||||
|
shard.RUnlock() |
||||||
|
} |
||||||
|
close(ch) |
||||||
|
}() |
||||||
|
return ch |
||||||
|
} |
||||||
|
|
||||||
|
// Returns a buffered iterator which could be used in a for range loop.
|
||||||
|
func (m StatsMap) IterBuffered() <-chan Tuple { |
||||||
|
ch := make(chan Tuple, m.Count()) |
||||||
|
go func() { |
||||||
|
// Foreach shard.
|
||||||
|
for _, shard := range m { |
||||||
|
// Foreach key, value pair.
|
||||||
|
shard.RLock() |
||||||
|
for key, val := range shard.items { |
||||||
|
ch <- Tuple{key, val} |
||||||
|
} |
||||||
|
shard.RUnlock() |
||||||
|
} |
||||||
|
close(ch) |
||||||
|
}() |
||||||
|
return ch |
||||||
|
} |
@ -0,0 +1,61 @@ |
|||||||
|
package stratum |
||||||
|
|
||||||
|
import "encoding/json" |
||||||
|
|
||||||
|
type JSONRpcReq struct { |
||||||
|
Id *json.RawMessage `json:"id"` |
||||||
|
Method string `json:"method"` |
||||||
|
Params *json.RawMessage `json:"params"` |
||||||
|
} |
||||||
|
|
||||||
|
type JSONRpcResp struct { |
||||||
|
Id *json.RawMessage `json:"id"` |
||||||
|
Version string `json:"jsonrpc"` |
||||||
|
Result interface{} `json:"result"` |
||||||
|
Error interface{} `json:"error"` |
||||||
|
} |
||||||
|
|
||||||
|
type JSONPushMessage struct { |
||||||
|
Version string `json:"jsonrpc"` |
||||||
|
Method string `json:"method"` |
||||||
|
Params interface{} `json:"params"` |
||||||
|
} |
||||||
|
|
||||||
|
type LoginParams struct { |
||||||
|
Login string `json:"login"` |
||||||
|
Pass string `json:"pass"` |
||||||
|
Agent string `json:"agent"` |
||||||
|
} |
||||||
|
|
||||||
|
type GetJobParams struct { |
||||||
|
Id string `json:"id"` |
||||||
|
} |
||||||
|
|
||||||
|
type SubmitParams struct { |
||||||
|
Id string `json:"id"` |
||||||
|
JobId string `json:"job_id"` |
||||||
|
Nonce string `json:"nonce"` |
||||||
|
Result string `json:"result"` |
||||||
|
} |
||||||
|
|
||||||
|
type JobReply struct { |
||||||
|
Id string `json:"id"` |
||||||
|
Job *JobReplyData `json:"job"` |
||||||
|
Status string `json:"status"` |
||||||
|
} |
||||||
|
|
||||||
|
type JobReplyData struct { |
||||||
|
Blob string `json:"blob"` |
||||||
|
JobId string `json:"job_id"` |
||||||
|
Target string `json:"target"` |
||||||
|
} |
||||||
|
|
||||||
|
type SubmitReply struct { |
||||||
|
Status string `json:"status"` |
||||||
|
} |
||||||
|
|
||||||
|
type ErrorReply struct { |
||||||
|
Code int `json:"code"` |
||||||
|
Message string `json:"message"` |
||||||
|
Close bool `json:"-"` |
||||||
|
} |
@ -0,0 +1,262 @@ |
|||||||
|
package stratum |
||||||
|
|
||||||
|
import ( |
||||||
|
"bufio" |
||||||
|
"crypto/rand" |
||||||
|
"encoding/json" |
||||||
|
"errors" |
||||||
|
"fmt" |
||||||
|
"io" |
||||||
|
"log" |
||||||
|
"net" |
||||||
|
"sync" |
||||||
|
"sync/atomic" |
||||||
|
"time" |
||||||
|
|
||||||
|
"../pool" |
||||||
|
"../rpc" |
||||||
|
"../storage" |
||||||
|
"./policy" |
||||||
|
) |
||||||
|
|
||||||
|
type StratumServer struct { |
||||||
|
config *pool.Config |
||||||
|
port pool.Port |
||||||
|
miners MinersMap |
||||||
|
blockTemplate atomic.Value |
||||||
|
instanceId []byte |
||||||
|
rpc *rpc.RPCClient |
||||||
|
timeout time.Duration |
||||||
|
broadcastTimer *time.Timer |
||||||
|
storage *storage.RedisClient |
||||||
|
policy *policy.PolicyServer |
||||||
|
} |
||||||
|
|
||||||
|
type Session struct { |
||||||
|
sync.Mutex |
||||||
|
conn *net.TCPConn |
||||||
|
enc *json.Encoder |
||||||
|
ip string |
||||||
|
} |
||||||
|
|
||||||
|
const ( |
||||||
|
MaxReqSize = 10 * 1024 |
||||||
|
) |
||||||
|
|
||||||
|
func NewStratum(cfg *pool.Config, port pool.Port, storage *storage.RedisClient, policy *policy.PolicyServer) *StratumServer { |
||||||
|
b := make([]byte, 4) |
||||||
|
_, err := rand.Read(b) |
||||||
|
if err != nil { |
||||||
|
log.Fatalf("Can't seed with random bytes: %v", err) |
||||||
|
} |
||||||
|
|
||||||
|
stratum := &StratumServer{config: cfg, port: port, policy: policy, instanceId: b} |
||||||
|
stratum.rpc = rpc.NewRPCClient(cfg) |
||||||
|
stratum.miners = NewMinersMap() |
||||||
|
stratum.storage = storage |
||||||
|
|
||||||
|
timeout, _ := time.ParseDuration(cfg.Stratum.Timeout) |
||||||
|
stratum.timeout = timeout |
||||||
|
|
||||||
|
// Init block template
|
||||||
|
stratum.blockTemplate.Store(&BlockTemplate{}) |
||||||
|
stratum.refreshBlockTemplate(false) |
||||||
|
|
||||||
|
refreshIntv, _ := time.ParseDuration(cfg.Stratum.BlockRefreshInterval) |
||||||
|
refreshTimer := time.NewTimer(refreshIntv) |
||||||
|
log.Printf("Set block refresh every %v", refreshIntv) |
||||||
|
|
||||||
|
go func() { |
||||||
|
for { |
||||||
|
select { |
||||||
|
case <-refreshTimer.C: |
||||||
|
stratum.refreshBlockTemplate(true) |
||||||
|
refreshTimer.Reset(refreshIntv) |
||||||
|
} |
||||||
|
} |
||||||
|
}() |
||||||
|
return stratum |
||||||
|
} |
||||||
|
|
||||||
|
func (s *StratumServer) Listen() { |
||||||
|
bindAddr := fmt.Sprintf("%s:%d", s.port.Host, s.port.Port) |
||||||
|
addr, err := net.ResolveTCPAddr("tcp", bindAddr) |
||||||
|
checkError(err) |
||||||
|
server, err := net.ListenTCP("tcp", addr) |
||||||
|
checkError(err) |
||||||
|
defer server.Close() |
||||||
|
|
||||||
|
log.Printf("Stratum listening on %s", bindAddr) |
||||||
|
var accept = make(chan int, s.port.MaxConn) |
||||||
|
n := 0 |
||||||
|
|
||||||
|
for { |
||||||
|
conn, err := server.AcceptTCP() |
||||||
|
conn.SetKeepAlive(true) |
||||||
|
checkError(err) |
||||||
|
ip, _, _ := net.SplitHostPort(conn.RemoteAddr().String()) |
||||||
|
ok := s.policy.ApplyLimitPolicy(ip) |
||||||
|
if !ok { |
||||||
|
conn.Close() |
||||||
|
continue |
||||||
|
} |
||||||
|
n += 1 |
||||||
|
|
||||||
|
accept <- n |
||||||
|
go func() { |
||||||
|
err = s.handleClient(conn, ip) |
||||||
|
if err != nil { |
||||||
|
conn.Close() |
||||||
|
} |
||||||
|
<-accept |
||||||
|
}() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (s *StratumServer) handleClient(conn *net.TCPConn, ip string) error { |
||||||
|
cs := &Session{conn: conn, ip: ip} |
||||||
|
cs.enc = json.NewEncoder(conn) |
||||||
|
connbuff := bufio.NewReaderSize(conn, MaxReqSize) |
||||||
|
s.setDeadline(conn) |
||||||
|
|
||||||
|
for { |
||||||
|
data, isPrefix, err := connbuff.ReadLine() |
||||||
|
if isPrefix { |
||||||
|
log.Printf("Socket flood detected") |
||||||
|
// TODO: Ban client
|
||||||
|
return errors.New("Socket flood") |
||||||
|
} else if err == io.EOF { |
||||||
|
log.Printf("Client disconnected") |
||||||
|
break |
||||||
|
} else if err != nil { |
||||||
|
log.Printf("Error reading: %v", err) |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
// NOTICE: cpuminer-multi sends junk newlines, so we demand at least 1 byte for decode
|
||||||
|
// NOTICE: Ns*CNMiner.exe will send malformed JSON on very low diff, not sure we should handle this
|
||||||
|
if len(data) > 1 { |
||||||
|
var req JSONRpcReq |
||||||
|
err = json.Unmarshal(data, &req) |
||||||
|
if err != nil { |
||||||
|
s.policy.ApplyMalformedPolicy(ip) |
||||||
|
log.Printf("Malformed request: %v", err) |
||||||
|
return err |
||||||
|
} |
||||||
|
s.setDeadline(conn) |
||||||
|
cs.handleMessage(s, &req) |
||||||
|
} |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (cs *Session) handleMessage(s *StratumServer, req *JSONRpcReq) { |
||||||
|
if req.Id == nil { |
||||||
|
log.Println("Missing RPC id") |
||||||
|
cs.conn.Close() |
||||||
|
return |
||||||
|
} else if req.Params == nil { |
||||||
|
log.Println("Missing RPC params") |
||||||
|
cs.conn.Close() |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
var err error |
||||||
|
|
||||||
|
// Handle RPC methods
|
||||||
|
switch req.Method { |
||||||
|
case "login": |
||||||
|
var params LoginParams |
||||||
|
err = json.Unmarshal(*req.Params, ¶ms) |
||||||
|
if err != nil { |
||||||
|
log.Println("Unable to parse params") |
||||||
|
break |
||||||
|
} |
||||||
|
reply, errReply := s.handleLoginRPC(cs, ¶ms) |
||||||
|
if errReply != nil { |
||||||
|
err = cs.sendError(req.Id, errReply) |
||||||
|
break |
||||||
|
} |
||||||
|
err = cs.sendResult(req.Id, &reply) |
||||||
|
case "getjob": |
||||||
|
var params GetJobParams |
||||||
|
err = json.Unmarshal(*req.Params, ¶ms) |
||||||
|
if err != nil { |
||||||
|
log.Println("Unable to parse params") |
||||||
|
break |
||||||
|
} |
||||||
|
reply, errReply := s.handleGetJobRPC(cs, ¶ms) |
||||||
|
if errReply != nil { |
||||||
|
err = cs.sendError(req.Id, errReply) |
||||||
|
break |
||||||
|
} |
||||||
|
err = cs.sendResult(req.Id, &reply) |
||||||
|
case "submit": |
||||||
|
var params SubmitParams |
||||||
|
err := json.Unmarshal(*req.Params, ¶ms) |
||||||
|
if err != nil { |
||||||
|
log.Println("Unable to parse params") |
||||||
|
break |
||||||
|
} |
||||||
|
reply, errReply := s.handleSubmitRPC(cs, ¶ms) |
||||||
|
if errReply != nil { |
||||||
|
err = cs.sendError(req.Id, errReply) |
||||||
|
break |
||||||
|
} |
||||||
|
err = cs.sendResult(req.Id, &reply) |
||||||
|
default: |
||||||
|
errReply := s.handleUnknownRPC(cs, req) |
||||||
|
err = cs.sendError(req.Id, errReply) |
||||||
|
} |
||||||
|
|
||||||
|
if err != nil { |
||||||
|
cs.conn.Close() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (cs *Session) sendResult(id *json.RawMessage, result interface{}) error { |
||||||
|
cs.Lock() |
||||||
|
defer cs.Unlock() |
||||||
|
message := JSONRpcResp{Id: id, Version: "2.0", Error: nil, Result: result} |
||||||
|
return cs.enc.Encode(&message) |
||||||
|
} |
||||||
|
|
||||||
|
func (cs *Session) pushMessage(method string, params interface{}) error { |
||||||
|
cs.Lock() |
||||||
|
defer cs.Unlock() |
||||||
|
message := JSONPushMessage{Version: "2.0", Method: method, Params: params} |
||||||
|
return cs.enc.Encode(&message) |
||||||
|
} |
||||||
|
|
||||||
|
func (cs *Session) sendError(id *json.RawMessage, reply *ErrorReply) error { |
||||||
|
cs.Lock() |
||||||
|
defer cs.Unlock() |
||||||
|
message := JSONRpcResp{Id: id, Version: "2.0", Error: reply} |
||||||
|
err := cs.enc.Encode(&message) |
||||||
|
if reply.Close { |
||||||
|
return errors.New("Force close") |
||||||
|
} |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
func (s *StratumServer) setDeadline(conn *net.TCPConn) { |
||||||
|
conn.SetDeadline(time.Now().Add(s.timeout)) |
||||||
|
} |
||||||
|
|
||||||
|
func (s *StratumServer) registerMiner(miner *Miner) { |
||||||
|
s.miners.Set(miner.Id, miner) |
||||||
|
} |
||||||
|
|
||||||
|
func (s *StratumServer) removeMiner(id string) { |
||||||
|
s.miners.Remove(id) |
||||||
|
} |
||||||
|
|
||||||
|
func (s *StratumServer) currentBlockTemplate() *BlockTemplate { |
||||||
|
return s.blockTemplate.Load().(*BlockTemplate) |
||||||
|
} |
||||||
|
|
||||||
|
func checkError(err error) { |
||||||
|
if err != nil { |
||||||
|
log.Fatalf("Error: %v", err) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,87 @@ |
|||||||
|
package util |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"encoding/binary" |
||||||
|
"encoding/hex" |
||||||
|
"math/big" |
||||||
|
"math/rand" |
||||||
|
"strconv" |
||||||
|
"time" |
||||||
|
"unicode/utf8" |
||||||
|
|
||||||
|
"../../cnutil" |
||||||
|
) |
||||||
|
|
||||||
|
var Diff1 *big.Int |
||||||
|
|
||||||
|
func init() { |
||||||
|
Diff1 = new(big.Int) |
||||||
|
Diff1.SetString("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", 16) |
||||||
|
} |
||||||
|
|
||||||
|
func Random() string { |
||||||
|
min := int64(100000000000000) |
||||||
|
max := int64(999999999999999) |
||||||
|
n := rand.Int63n(max-min+1) + min |
||||||
|
return strconv.FormatInt(n, 10) |
||||||
|
} |
||||||
|
|
||||||
|
func MakeTimestamp() int64 { |
||||||
|
return time.Now().UnixNano() / int64(time.Millisecond) |
||||||
|
} |
||||||
|
|
||||||
|
func GetTargetHex(diff int64) (uint32, string) { |
||||||
|
padded := make([]byte, 32) |
||||||
|
|
||||||
|
diff2 := new(big.Int) |
||||||
|
diff2.SetInt64(int64(diff)) |
||||||
|
|
||||||
|
diff3 := new(big.Int) |
||||||
|
diff3 = diff3.Div(Diff1, diff2) |
||||||
|
|
||||||
|
diffBuff := diff3.Bytes() |
||||||
|
copy(padded[32-len(diffBuff):], diffBuff) |
||||||
|
buff := padded[0:4] |
||||||
|
var target uint32 |
||||||
|
targetBuff := bytes.NewReader(buff) |
||||||
|
binary.Read(targetBuff, binary.LittleEndian, &target) |
||||||
|
targetHex := hex.EncodeToString(reverse(buff)) |
||||||
|
|
||||||
|
return target, targetHex |
||||||
|
} |
||||||
|
|
||||||
|
func GetHashDifficulty(hashBytes []byte) *big.Int { |
||||||
|
diff := new(big.Int) |
||||||
|
diff.SetBytes(reverse(hashBytes)) |
||||||
|
return diff.Div(Diff1, diff) |
||||||
|
} |
||||||
|
|
||||||
|
func ValidateAddress(addy string, poolAddy string) bool { |
||||||
|
if len(addy) != len(poolAddy) { |
||||||
|
return false |
||||||
|
} |
||||||
|
prefix, _ := utf8.DecodeRuneInString(addy) |
||||||
|
poolPrefix, _ := utf8.DecodeRuneInString(poolAddy) |
||||||
|
if prefix != poolPrefix { |
||||||
|
return false |
||||||
|
} |
||||||
|
return cnutil.ValidateAddress(addy) |
||||||
|
} |
||||||
|
|
||||||
|
func StringInSlice(a string, list []string) bool { |
||||||
|
for _, b := range list { |
||||||
|
if b == a { |
||||||
|
return true |
||||||
|
} |
||||||
|
} |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
func reverse(src []byte) []byte { |
||||||
|
dst := make([]byte, len(src)) |
||||||
|
for i := len(src); i > 0; i-- { |
||||||
|
dst[len(src)-i] = src[i-1] |
||||||
|
} |
||||||
|
return dst |
||||||
|
} |
@ -0,0 +1,41 @@ |
|||||||
|
package util |
||||||
|
|
||||||
|
import ( |
||||||
|
"encoding/hex" |
||||||
|
"math/big" |
||||||
|
"testing" |
||||||
|
) |
||||||
|
|
||||||
|
func TestGetTargetHex(t *testing.T) { |
||||||
|
target, targetHex := GetTargetHex(500) |
||||||
|
expectedTarget := uint32(1846706944) |
||||||
|
expectedHex := "6e128300" |
||||||
|
if target != expectedTarget { |
||||||
|
t.Error("Invalid target") |
||||||
|
} |
||||||
|
if targetHex != expectedHex { |
||||||
|
t.Error("Invalid targetHex") |
||||||
|
} |
||||||
|
|
||||||
|
target, targetHex = GetTargetHex(15000) |
||||||
|
expectedTarget = uint32(2069758976) |
||||||
|
expectedHex = "7b5e0400" |
||||||
|
if target != expectedTarget { |
||||||
|
t.Error("Invalid target") |
||||||
|
} |
||||||
|
if targetHex != expectedHex { |
||||||
|
t.Error("Invalid targetHex") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func TestGetHashDifficulty(t *testing.T) { |
||||||
|
hash := "8e3c1865f22801dc3df0a688da80701e2390e7838e65c142604cc00eafe34000" |
||||||
|
hashBytes, _ := hex.DecodeString(hash) |
||||||
|
diff := new(big.Int) |
||||||
|
diff.SetBytes(reverse(hashBytes)) |
||||||
|
shareDiff := GetHashDifficulty(hashBytes) |
||||||
|
|
||||||
|
if shareDiff.String() != "1009" { |
||||||
|
t.Error("Invalid diff") |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,5 @@ |
|||||||
|
Makefile |
||||||
|
CmakeCache.txt |
||||||
|
cmake_install.cmake |
||||||
|
CMakeFiles |
||||||
|
libhashing.a |
@ -0,0 +1,11 @@ |
|||||||
|
set(LIB "hashing") |
||||||
|
|
||||||
|
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -std=c11 -D_GNU_SOURCE") |
||||||
|
|
||||||
|
include_directories("${MONERO_DIR}/contrib/epee/include") |
||||||
|
include_directories("${MONERO_DIR}/src") |
||||||
|
|
||||||
|
add_library(${LIB} SHARED src/hashing.c) |
||||||
|
target_link_libraries(${LIB} |
||||||
|
${MONERO_DIR}/build/release/src/crypto/libcrypto.a |
||||||
|
) |
@ -0,0 +1,23 @@ |
|||||||
|
package hashing |
||||||
|
|
||||||
|
// #cgo CFLAGS: -std=c11 -D_GNU_SOURCE
|
||||||
|
// #cgo LDFLAGS: -L. -lhashing -lstdc++
|
||||||
|
// #include <stdlib.h>
|
||||||
|
// #include <stdint.h>
|
||||||
|
// #include "src/hashing.h"
|
||||||
|
import "C" |
||||||
|
import "unsafe" |
||||||
|
|
||||||
|
func Hash(blob []byte, fast bool) []byte { |
||||||
|
output := make([]byte, 32) |
||||||
|
if fast { |
||||||
|
C.cryptonight_fast_hash((*C.char)(unsafe.Pointer(&blob[0])), (*C.char)(unsafe.Pointer(&output[0])), (C.uint32_t)(len(blob))) |
||||||
|
} else { |
||||||
|
C.cryptonight_hash((*C.char)(unsafe.Pointer(&blob[0])), (*C.char)(unsafe.Pointer(&output[0])), (C.uint32_t)(len(blob))) |
||||||
|
} |
||||||
|
return output |
||||||
|
} |
||||||
|
|
||||||
|
func FastHash(blob []byte) []byte { |
||||||
|
return Hash(append([]byte{byte(len(blob))}, blob...), true) |
||||||
|
} |
@ -0,0 +1,82 @@ |
|||||||
|
package hashing |
||||||
|
|
||||||
|
import "testing" |
||||||
|
import "log" |
||||||
|
import "encoding/hex" |
||||||
|
|
||||||
|
func TestHash(t *testing.T) { |
||||||
|
blob, _ := hex.DecodeString("01009091e4aa05ff5fe4801727ed0c1b8b339e1a0054d75568fec6ba9c4346e88b10d59edbf6858b2b00008a63b2865b65b84d28bb31feb057b16a21e2eda4bf6cc6377e3310af04debe4a01") |
||||||
|
hashBytes := Hash(blob, false) |
||||||
|
hash := hex.EncodeToString(hashBytes) |
||||||
|
log.Println(hash) |
||||||
|
|
||||||
|
expectedHash := "a70a96f64a266f0f59e4f67c4a92f24fe8237c1349f377fd2720c9e1f2970400" |
||||||
|
|
||||||
|
if hash != expectedHash { |
||||||
|
t.Error("Invalid hash") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func TestHash_fast(t *testing.T) { |
||||||
|
blob, _ := hex.DecodeString("01009091e4aa05ff5fe4801727ed0c1b8b339e1a0054d75568fec6ba9c4346e88b10d59edbf6858b2b00008a63b2865b65b84d28bb31feb057b16a21e2eda4bf6cc6377e3310af04debe4a01") |
||||||
|
hashBytes := Hash(blob, true) |
||||||
|
hash := hex.EncodeToString(hashBytes) |
||||||
|
log.Println(hash) |
||||||
|
|
||||||
|
expectedHash := "7591f4d8ff9d86ea44873e89a5fb6f380f4410be6206030010567ac9d0d4b0e1" |
||||||
|
|
||||||
|
if hash != expectedHash { |
||||||
|
t.Error("Invalid fast hash") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func TestFastHash(t *testing.T) { |
||||||
|
blob, _ := hex.DecodeString("01009091e4aa05ff5fe4801727ed0c1b8b339e1a0054d75568fec6ba9c4346e88b10d59edbf6858b2b00008a63b2865b65b84d28bb31feb057b16a21e2eda4bf6cc6377e3310af04debe4a01") |
||||||
|
hashBytes := FastHash(blob) |
||||||
|
hash := hex.EncodeToString(hashBytes) |
||||||
|
log.Println(hash) |
||||||
|
|
||||||
|
expectedHash := "8706c697d9fc8a48b14ea93a31c6f0750c48683e585ec1a534e9c57c97193fa6" |
||||||
|
|
||||||
|
if hash != expectedHash { |
||||||
|
t.Error("Invalid fast hash") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func BenchmarkHash(b *testing.B) { |
||||||
|
blob, _ := hex.DecodeString("0100b69bb3aa050a3106491f858f8646d3a8d13fd9924403bf07af95e6e7cc9e4ad105d76da27241565555866b1baa9db8f027cf57cd45d6835c11287b210d9ddb407deda565f8112e19e501") |
||||||
|
b.ResetTimer() |
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ { |
||||||
|
Hash(blob, false) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func BenchmarkHashParallel(b *testing.B) { |
||||||
|
blob, _ := hex.DecodeString("0100b69bb3aa050a3106491f858f8646d3a8d13fd9924403bf07af95e6e7cc9e4ad105d76da27241565555866b1baa9db8f027cf57cd45d6835c11287b210d9ddb407deda565f8112e19e501") |
||||||
|
b.ResetTimer() |
||||||
|
|
||||||
|
b.RunParallel(func(pb *testing.PB) { |
||||||
|
for pb.Next() { |
||||||
|
Hash(blob, false) |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
func BenchmarkHash_fast(b *testing.B) { |
||||||
|
blob, _ := hex.DecodeString("0100b69bb3aa050a3106491f858f8646d3a8d13fd9924403bf07af95e6e7cc9e4ad105d76da27241565555866b1baa9db8f027cf57cd45d6835c11287b210d9ddb407deda565f8112e19e501") |
||||||
|
b.ResetTimer() |
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ { |
||||||
|
Hash(blob, true) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func BenchmarkFastHash(b *testing.B) { |
||||||
|
blob, _ := hex.DecodeString("0100b69bb3aa050a3106491f858f8646d3a8d13fd9924403bf07af95e6e7cc9e4ad105d76da27241565555866b1baa9db8f027cf57cd45d6835c11287b210d9ddb407deda565f8112e19e501") |
||||||
|
b.ResetTimer() |
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ { |
||||||
|
FastHash(blob) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,12 @@ |
|||||||
|
#include <assert.h> |
||||||
|
#define static_assert _Static_assert |
||||||
|
|
||||||
|
#include "crypto/hash-ops.h" |
||||||
|
|
||||||
|
void cryptonight_hash(const char* input, char* output, uint32_t len) { |
||||||
|
cn_slow_hash(input, len, output); |
||||||
|
} |
||||||
|
|
||||||
|
void cryptonight_fast_hash(const char* input, char* output, uint32_t len) { |
||||||
|
cn_fast_hash(input, len, output); |
||||||
|
} |
@ -0,0 +1,2 @@ |
|||||||
|
void cryptonight_hash(const char* input, char* output, uint32_t len); |
||||||
|
void cryptonight_fast_hash(const char* input, char* output, uint32_t len); |
@ -0,0 +1,3 @@ |
|||||||
|
#!/bin/bash |
||||||
|
|
||||||
|
LD_LIBRARY_PATH="/usr/local/lib/" go run main.go $@ |
@ -0,0 +1,83 @@ |
|||||||
|
package main |
||||||
|
|
||||||
|
import ( |
||||||
|
"encoding/json" |
||||||
|
"log" |
||||||
|
"math/rand" |
||||||
|
"os" |
||||||
|
"path/filepath" |
||||||
|
"runtime" |
||||||
|
"time" |
||||||
|
|
||||||
|
"./go-pool/pool" |
||||||
|
"./go-pool/storage" |
||||||
|
"./go-pool/stratum" |
||||||
|
"./go-pool/stratum/policy" |
||||||
|
|
||||||
|
"github.com/yvasiyarov/gorelic" |
||||||
|
) |
||||||
|
|
||||||
|
var cfg pool.Config |
||||||
|
|
||||||
|
func startStratum() { |
||||||
|
if cfg.Threads > 0 { |
||||||
|
runtime.GOMAXPROCS(cfg.Threads) |
||||||
|
log.Printf("Running with %v threads", cfg.Threads) |
||||||
|
} else { |
||||||
|
n := runtime.NumCPU() |
||||||
|
runtime.GOMAXPROCS(n) |
||||||
|
log.Printf("Running with default %v threads", n) |
||||||
|
} |
||||||
|
|
||||||
|
storage := storage.NewRedisClient(&cfg.Redis, cfg.Coin) |
||||||
|
storage.Check() |
||||||
|
policy := policy.Start(&cfg, storage) |
||||||
|
|
||||||
|
quit := make(chan bool) |
||||||
|
for _, port := range cfg.Stratum.Ports { |
||||||
|
s := stratum.NewStratum(&cfg, port, storage, policy) |
||||||
|
|
||||||
|
go func() { |
||||||
|
s.Listen() |
||||||
|
quit <- true |
||||||
|
}() |
||||||
|
} |
||||||
|
<-quit |
||||||
|
} |
||||||
|
|
||||||
|
func startNewrelic() { |
||||||
|
// Run NewRelic
|
||||||
|
if cfg.NewrelicEnabled { |
||||||
|
nr := gorelic.NewAgent() |
||||||
|
nr.Verbose = cfg.NewrelicVerbose |
||||||
|
nr.NewrelicLicense = cfg.NewrelicKey |
||||||
|
nr.NewrelicName = cfg.NewrelicName |
||||||
|
nr.Run() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func readConfig(cfg *pool.Config) { |
||||||
|
configFileName := "config.json" |
||||||
|
if len(os.Args) > 1 { |
||||||
|
configFileName = os.Args[1] |
||||||
|
} |
||||||
|
configFileName, _ = filepath.Abs(configFileName) |
||||||
|
log.Printf("Loading config: %v", configFileName) |
||||||
|
|
||||||
|
configFile, err := os.Open(configFileName) |
||||||
|
if err != nil { |
||||||
|
log.Fatal("File error: ", err.Error()) |
||||||
|
} |
||||||
|
defer configFile.Close() |
||||||
|
jsonParser := json.NewDecoder(configFile) |
||||||
|
if err = jsonParser.Decode(&cfg); err != nil { |
||||||
|
log.Fatal("Config error: ", err.Error()) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func main() { |
||||||
|
rand.Seed(time.Now().UTC().UnixNano()) |
||||||
|
readConfig(&cfg) |
||||||
|
startNewrelic() |
||||||
|
startStratum() |
||||||
|
} |
@ -0,0 +1,3 @@ |
|||||||
|
#!/usr/bin/env fish |
||||||
|
|
||||||
|
env GORACE="log_path=race.log" env CGO_LDFLAGS="-L"(pwd)"/cnutil -L"(pwd)"/hashing" go run -race main.go $argv |
Loading…
Reference in new issue