Sammy Libre
10 years ago
commit
7f07ef97c6
37 changed files with 2664 additions and 0 deletions
@ -0,0 +1,18 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -0,0 +1,7 @@
|
||||
Makefile |
||||
CmakeCache.txt |
||||
cmake_install.cmake |
||||
CMakeFiles |
||||
*.a |
||||
*.so |
||||
*.dylib |
@ -0,0 +1,36 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -0,0 +1,5 @@
|
||||
Makefile |
||||
CmakeCache.txt |
||||
cmake_install.cmake |
||||
CMakeFiles |
||||
libhashing.a |
@ -0,0 +1,11 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -0,0 +1,3 @@
|
||||
#!/bin/bash |
||||
|
||||
LD_LIBRARY_PATH="/usr/local/lib/" go run main.go $@ |
@ -0,0 +1,83 @@
@@ -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 @@
@@ -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