mirror of
https://github.com/kvazar-network/keva-stratum.git
synced 2025-02-04 11:14:17 +00:00
Initial import
This commit is contained in:
commit
7f07ef97c6
18
.gitignore
vendored
Normal file
18
.gitignore
vendored
Normal file
@ -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
|
13
CMakeLists.txt
Normal file
13
CMakeLists.txt
Normal file
@ -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)
|
340
LICENSE
Normal file
340
LICENSE
Normal file
@ -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.
|
||||
|
106
README.md
Normal file
106
README.md
Normal file
@ -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
|
7
cnutil/.gitignore
vendored
Normal file
7
cnutil/.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
Makefile
|
||||
CmakeCache.txt
|
||||
cmake_install.cmake
|
||||
CMakeFiles
|
||||
*.a
|
||||
*.so
|
||||
*.dylib
|
36
cnutil/CMakeLists.txt
Normal file
36
cnutil/CMakeLists.txt
Normal file
@ -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})
|
13
cnutil/cnutil.c
Normal file
13
cnutil/cnutil.c
Normal file
@ -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);
|
||||
}
|
28
cnutil/cnutil.go
Normal file
28
cnutil/cnutil.go
Normal file
@ -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)
|
||||
}
|
7
cnutil/cnutil.h
Normal file
7
cnutil/cnutil.h
Normal file
@ -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);
|
58
cnutil/cnutil_test.go
Normal file
58
cnutil/cnutil_test.go
Normal file
@ -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")
|
||||
}
|
||||
}
|
30
cnutil/cnutilxx/main.cpp
Normal file
30
cnutil/cnutilxx/main.cpp
Normal file
@ -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);
|
||||
}
|
12
cnutil/cnutilxx/main.h
Normal file
12
cnutil/cnutilxx/main.h
Normal file
@ -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
|
71
config.example.json
Normal file
71
config.example.json
Normal file
@ -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
|
||||
}
|
68
go-pool/pool/pool.go
Normal file
68
go-pool/pool/pool.go
Normal file
@ -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"`
|
||||
}
|
91
go-pool/rpc/rpc.go
Normal file
91
go-pool/rpc/rpc.go
Normal file
@ -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
|
||||
}
|
135
go-pool/storage/redis.go
Normal file
135
go-pool/storage/redis.go
Normal file
@ -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, ":")
|
||||
}
|
74
go-pool/storage/redis_test.go
Normal file
74
go-pool/storage/redis_test.go
Normal file
@ -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
|
||||
}
|
66
go-pool/stratum/blocks.go
Normal file
66
go-pool/stratum/blocks.go
Normal file
@ -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
|
||||
}
|
127
go-pool/stratum/handlers.go
Normal file
127
go-pool/stratum/handlers.go
Normal file
@ -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()
|
||||
}
|
||||
}
|
143
go-pool/stratum/miner.go
Normal file
143
go-pool/stratum/miner.go
Normal file
@ -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
|
||||
}
|
136
go-pool/stratum/mmap.go
Normal file
136
go-pool/stratum/mmap.go
Normal file
@ -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
|
||||
}
|
271
go-pool/stratum/policy/policy.go
Normal file
271
go-pool/stratum/policy/policy.go
Normal file
@ -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)
|
||||
}
|
136
go-pool/stratum/policy/smap.go
Normal file
136
go-pool/stratum/policy/smap.go
Normal file
@ -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
|
||||
}
|
61
go-pool/stratum/proto.go
Normal file
61
go-pool/stratum/proto.go
Normal file
@ -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:"-"`
|
||||
}
|
262
go-pool/stratum/stratum.go
Normal file
262
go-pool/stratum/stratum.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
87
go-pool/util/util.go
Normal file
87
go-pool/util/util.go
Normal file
@ -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
|
||||
}
|
41
go-pool/util/util_test.go
Normal file
41
go-pool/util/util_test.go
Normal file
@ -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")
|
||||
}
|
||||
}
|
5
hashing/.gitignore
vendored
Normal file
5
hashing/.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
Makefile
|
||||
CmakeCache.txt
|
||||
cmake_install.cmake
|
||||
CMakeFiles
|
||||
libhashing.a
|
11
hashing/CMakeLists.txt
Normal file
11
hashing/CMakeLists.txt
Normal file
@ -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
|
||||
)
|
23
hashing/hashing.go
Normal file
23
hashing/hashing.go
Normal file
@ -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)
|
||||
}
|
82
hashing/hashing_test.go
Normal file
82
hashing/hashing_test.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
12
hashing/src/hashing.c
Normal file
12
hashing/src/hashing.c
Normal file
@ -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);
|
||||
}
|
2
hashing/src/hashing.h
Normal file
2
hashing/src/hashing.h
Normal file
@ -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);
|
3
linux-run.sh
Executable file
3
linux-run.sh
Executable file
@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
|
||||
LD_LIBRARY_PATH="/usr/local/lib/" go run main.go $@
|
83
main.go
Normal file
83
main.go
Normal file
@ -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()
|
||||
}
|
3
osx-debug.fish
Executable file
3
osx-debug.fish
Executable file
@ -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
|
3
osx-run.fish
Executable file
3
osx-run.fish
Executable file
@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env fish
|
||||
|
||||
env CGO_LDFLAGS="-L"(pwd)"/cnutil -L"(pwd)"/hashing" go run main.go $argv
|
Loading…
x
Reference in New Issue
Block a user