implement miner selected block templates
This commit is contained in:
parent
86a9900dfc
commit
b5a2dd5f5b
188
Makefile
188
Makefile
|
@ -1,65 +1,60 @@
|
||||||
define LICENSE
|
# Copyright (c) 2019, The Monero Project
|
||||||
|
#
|
||||||
Copyright (c) 2014-2019, The Monero Project
|
# All rights reserved.
|
||||||
|
#
|
||||||
All rights reserved.
|
# Redistribution and use in source and binary forms, with or without
|
||||||
|
# modification, are permitted provided that the following conditions are met:
|
||||||
Redistribution and use in source and binary forms, with or without
|
#
|
||||||
modification, are permitted provided that the following conditions are met:
|
# 1. Redistributions of source code must retain the above copyright notice,
|
||||||
|
# this list of conditions and the following disclaimer.
|
||||||
1. Redistributions of source code must retain the above copyright notice, this
|
#
|
||||||
list of conditions and the following disclaimer.
|
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
# this list of conditions and the following disclaimer in the documentation
|
||||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
# and/or other materials provided with the distribution.
|
||||||
this list of conditions and the following disclaimer in the documentation
|
#
|
||||||
and/or other materials provided with the distribution.
|
# 3. Neither the name of the copyright holder nor the names of its
|
||||||
|
# contributors may be used to endorse or promote products derived from this
|
||||||
3. Neither the name of the copyright holder nor the names of its contributors
|
# software without specific prior written permission.
|
||||||
may be used to endorse or promote products derived from this software without
|
#
|
||||||
specific prior written permission.
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
# POSSIBILITY OF SUCH DAMAGE.
|
||||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
|
|
||||||
Parts of the project are originally copyright (c) 2012-2013 The Cryptonote
|
|
||||||
developers.
|
|
||||||
|
|
||||||
endef
|
|
||||||
|
|
||||||
TARGET = monero-pool
|
TARGET = monero-pool
|
||||||
|
|
||||||
TYPE = debug
|
TYPE = debug
|
||||||
|
|
||||||
ifeq ($(MAKECMDGOALS),release)
|
ifeq ($(MAKECMDGOALS),release)
|
||||||
TYPE = release
|
TYPE = release
|
||||||
endif
|
endif
|
||||||
|
|
||||||
MONERO_BUILD_ROOT = ${MONERO_ROOT}/build/$(shell echo `uname | \
|
MONERO_BUILD_ROOT = \
|
||||||
sed -e 's|[:/\\ \(\)]|_|g'`/`\
|
${MONERO_ROOT}/build/$(shell echo `uname | \
|
||||||
git -C ${MONERO_ROOT} branch | grep '\* ' | cut -f2- -d' '| \
|
sed -e 's|[:/\\ \(\)]|_|g'`/` \
|
||||||
sed -e 's|[:/\\ \(\)]|_|g'`)/release
|
git -C ${MONERO_ROOT} branch | \
|
||||||
|
grep '\* ' | cut -f2- -d' '| \
|
||||||
|
sed -e 's|[:/\\ \(\)]|_|g'`)/release
|
||||||
|
|
||||||
MONERO_INC = ${MONERO_ROOT}/src \
|
MONERO_INC = \
|
||||||
${MONERO_ROOT}/external/easylogging++ \
|
${MONERO_ROOT}/src \
|
||||||
${MONERO_ROOT}/contrib/epee/include
|
${MONERO_ROOT}/external \
|
||||||
|
${MONERO_ROOT}/external/easylogging++ \
|
||||||
|
${MONERO_ROOT}/contrib/epee/include
|
||||||
|
|
||||||
MONERO_LIBS = \
|
MONERO_LIBS = \
|
||||||
${MONERO_BUILD_ROOT}/src/cryptonote_basic/libcryptonote_basic.a \
|
${MONERO_BUILD_ROOT}/src/cryptonote_basic/libcryptonote_basic.a \
|
||||||
${MONERO_BUILD_ROOT}/src/cryptonote_core/libcryptonote_core.a \
|
|
||||||
${MONERO_BUILD_ROOT}/src/crypto/libcncrypto.a \
|
${MONERO_BUILD_ROOT}/src/crypto/libcncrypto.a \
|
||||||
${MONERO_BUILD_ROOT}/src/common/libcommon.a \
|
${MONERO_BUILD_ROOT}/src/common/libcommon.a \
|
||||||
${MONERO_BUILD_ROOT}/src/ringct/libringct.a \
|
|
||||||
${MONERO_BUILD_ROOT}/src/ringct/libringct_basic.a \
|
${MONERO_BUILD_ROOT}/src/ringct/libringct_basic.a \
|
||||||
${MONERO_BUILD_ROOT}/src/device/libdevice.a \
|
${MONERO_BUILD_ROOT}/src/device/libdevice.a \
|
||||||
${MONERO_BUILD_ROOT}/src/blockchain_db/libblockchain_db.a \
|
|
||||||
${MONERO_BUILD_ROOT}/contrib/epee/src/libepee.a \
|
${MONERO_BUILD_ROOT}/contrib/epee/src/libepee.a \
|
||||||
${MONERO_BUILD_ROOT}/external/easylogging++/libeasylogging.a
|
${MONERO_BUILD_ROOT}/external/easylogging++/libeasylogging.a
|
||||||
|
|
||||||
|
@ -70,48 +65,52 @@ OS := $(shell uname -s)
|
||||||
CPPDEFS = _GNU_SOURCE AUTO_INITIALIZE_EASYLOGGINGPP LOG_USE_COLOR
|
CPPDEFS = _GNU_SOURCE AUTO_INITIALIZE_EASYLOGGINGPP LOG_USE_COLOR
|
||||||
|
|
||||||
CCPARAM = -Wall $(CFLAGS) -maes -fPIC
|
CCPARAM = -Wall $(CFLAGS) -maes -fPIC
|
||||||
CXXFLAGS = -std=c++11
|
CXXFLAGS = -std=c++11 -Wno-reorder
|
||||||
|
|
||||||
ifeq ($(OS), Darwin)
|
ifeq ($(OS), Darwin)
|
||||||
CXXFLAGS += -stdlib=libc++
|
CXXFLAGS += -stdlib=libc++
|
||||||
CPPDEFS += HAVE_MEMSET_S
|
CPPDEFS += HAVE_MEMSET_S
|
||||||
endif
|
endif
|
||||||
|
|
||||||
ifeq ($(OS),Darwin)
|
ifeq ($(OS),Darwin)
|
||||||
LDPARAM =
|
LDPARAM =
|
||||||
else
|
else
|
||||||
LDPARAM = -Wl,-warn-unresolved-symbols -fPIC -pie
|
LDPARAM = -Wl,-warn-unresolved-symbols -fPIC -pie
|
||||||
endif
|
endif
|
||||||
|
|
||||||
ifeq ($(TYPE),debug)
|
ifeq ($(TYPE),debug)
|
||||||
CCPARAM += -g
|
CCPARAM += -g
|
||||||
CPPDEFS += DEBUG
|
CPPDEFS += DEBUG
|
||||||
endif
|
endif
|
||||||
|
|
||||||
ifeq ($(TYPE), release)
|
ifeq ($(TYPE), release)
|
||||||
CCPARAM += -O3 -Wno-unused-variable
|
CCPARAM += -O3 -Wno-unused-variable
|
||||||
ifneq ($(OS), Darwin)
|
ifneq ($(OS), Darwin)
|
||||||
LDPARAM = -Wl,--unresolved-symbols=ignore-in-object-files
|
LDPARAM = -Wl,--unresolved-symbols=ignore-in-object-files
|
||||||
endif
|
endif
|
||||||
endif
|
endif
|
||||||
|
|
||||||
LDPARAM += $(LDFLAGS)
|
LDPARAM += $(LDFLAGS)
|
||||||
|
|
||||||
LIBS := lmdb pthread microhttpd unbound
|
LIBS := lmdb pthread microhttpd unbound
|
||||||
ifeq ($(OS), Darwin)
|
ifeq ($(OS), Darwin)
|
||||||
LIBS += c++ boost_system-mt boost_date_time-mt boost_chrono-mt \
|
LIBS += c++ \
|
||||||
boost_filesystem-mt boost_thread-mt boost_regex-mt
|
boost_system-mt boost_date_time-mt boost_chrono-mt \
|
||||||
|
boost_filesystem-mt boost_thread-mt boost_regex-mt \
|
||||||
|
boost_serialization-mt boost_program_options-mt
|
||||||
else
|
else
|
||||||
LIBS += dl boost_system boost_date_time boost_chrono boost_filesystem \
|
LIBS += dl uuid \
|
||||||
boost_thread boost_regex uuid
|
boost_system boost_date_time boost_chrono \
|
||||||
|
boost_filesystem boost_thread boost_regex \
|
||||||
|
boost_serialization boost_program_options
|
||||||
endif
|
endif
|
||||||
|
|
||||||
PKG_LIBS := $(shell pkg-config \
|
PKG_LIBS := $(shell pkg-config \
|
||||||
"libevent >= 2.1" \
|
"libevent >= 2.1" \
|
||||||
json-c \
|
json-c \
|
||||||
openssl \
|
openssl \
|
||||||
libsodium \
|
libsodium \
|
||||||
--libs)
|
--libs)
|
||||||
|
|
||||||
STATIC_LIBS =
|
STATIC_LIBS =
|
||||||
DLIBS =
|
DLIBS =
|
||||||
|
@ -119,11 +118,11 @@ DLIBS =
|
||||||
INCPATH := $(DIRS) ${MONERO_INC} /opt/local/include /usr/local/include
|
INCPATH := $(DIRS) ${MONERO_INC} /opt/local/include /usr/local/include
|
||||||
|
|
||||||
PKG_INC := $(shell pkg-config \
|
PKG_INC := $(shell pkg-config \
|
||||||
"libevent >= 2.1" \
|
"libevent >= 2.1" \
|
||||||
json-c \
|
json-c \
|
||||||
openssl \
|
openssl \
|
||||||
libsodium \
|
libsodium \
|
||||||
--cflags)
|
--cflags)
|
||||||
|
|
||||||
LIBPATH := /opt/local/lib/ /usr/local/lib
|
LIBPATH := /opt/local/lib/ /usr/local/lib
|
||||||
|
|
||||||
|
@ -150,36 +149,42 @@ SDFILES := $(addprefix $(STORE)/,$(CSOURCE:.S=.d))
|
||||||
|
|
||||||
$(TARGET): preflight dirs $(OBJECTS) $(COBJECTS) $(SOBJECTS) $(HTMLOBJECTS)
|
$(TARGET): preflight dirs $(OBJECTS) $(COBJECTS) $(SOBJECTS) $(HTMLOBJECTS)
|
||||||
@echo Linking $(OBJECTS)...
|
@echo Linking $(OBJECTS)...
|
||||||
$(C++) -o $(STORE)/$(TARGET) $(OBJECTS) $(COBJECTS) $(SOBJECTS) $(HTMLOBJECTS) \
|
$(C++) -o $(STORE)/$(TARGET) \
|
||||||
$(LDPARAM) $(MONERO_LIBS) \
|
$(OBJECTS) $(COBJECTS) $(SOBJECTS) $(HTMLOBJECTS) \
|
||||||
$(foreach LIBRARY, $(LIBS),-l$(LIBRARY)) \
|
$(LDPARAM) $(MONERO_LIBS) \
|
||||||
$(foreach LIB,$(LIBPATH),-L$(LIB)) \
|
$(foreach LIBRARY, $(LIBS),-l$(LIBRARY)) \
|
||||||
$(PKG_LIBS) $(STATIC_LIBS)
|
$(foreach LIB,$(LIBPATH),-L$(LIB)) \
|
||||||
|
$(PKG_LIBS) $(STATIC_LIBS)
|
||||||
@cp pool.conf $(STORE)/
|
@cp pool.conf $(STORE)/
|
||||||
@cp tools/* $(STORE)/
|
@cp tools/* $(STORE)/
|
||||||
|
|
||||||
$(STORE)/%.o: %.cpp
|
$(STORE)/%.o: %.cpp
|
||||||
@echo Creating object file for $*...
|
@echo Creating object file for $*...
|
||||||
$(C++) -Wp,-MMD,$(STORE)/$*.dd $(CCPARAM) $(CXXFLAGS) \
|
$(C++) -Wp,-MMD,$(STORE)/$*.dd $(CCPARAM) $(CXXFLAGS) \
|
||||||
$(foreach INC,$(INCPATH),-I$(INC)) \
|
$(foreach INC,$(INCPATH),-I$(INC)) \
|
||||||
$(PKG_INC) \
|
$(PKG_INC) \
|
||||||
$(foreach CPPDEF,$(CPPDEFS),-D$(CPPDEF)) \
|
$(foreach CPPDEF,$(CPPDEFS),-D$(CPPDEF)) \
|
||||||
-c $< -o $@
|
-c $< -o $@
|
||||||
@sed -e '1s/^\(.*\)$$/$(subst /,\/,$(dir $@))\1/' $(STORE)/$*.dd > $(STORE)/$*.d
|
@sed -e '1s/^\(.*\)$$/$(subst /,\/,$(dir $@))\1/' \
|
||||||
|
$(STORE)/$*.dd > $(STORE)/$*.d
|
||||||
@rm -f $(STORE)/$*.dd
|
@rm -f $(STORE)/$*.dd
|
||||||
|
|
||||||
$(STORE)/%.o: %.c
|
$(STORE)/%.o: %.c
|
||||||
@echo Creating object file for $*...
|
@echo Creating object file for $*...
|
||||||
$(CC) -Wp,-MMD,$(STORE)/$*.dd $(CCPARAM) $(foreach INC,$(INCPATH),-I$(INC)) $(PKG_INC)\
|
$(CC) -Wp,-MMD,$(STORE)/$*.dd $(CCPARAM) \
|
||||||
$(foreach CPPDEF,$(CPPDEFS),-D$(CPPDEF)) -c $< -o $@
|
$(foreach INC,$(INCPATH),-I$(INC)) $(PKG_INC) \
|
||||||
@sed -e '1s/^\(.*\)$$/$(subst /,\/,$(dir $@))\1/' $(STORE)/$*.dd > $(STORE)/$*.d
|
$(foreach CPPDEF,$(CPPDEFS),-D$(CPPDEF)) -c $< -o $@
|
||||||
|
@sed -e '1s/^\(.*\)$$/$(subst /,\/,$(dir $@))\1/' \
|
||||||
|
$(STORE)/$*.dd > $(STORE)/$*.d
|
||||||
@rm -f $(STORE)/$*.dd
|
@rm -f $(STORE)/$*.dd
|
||||||
|
|
||||||
$(STORE)/%.o: %.S
|
$(STORE)/%.o: %.S
|
||||||
@echo Creating object file for $*...
|
@echo Creating object file for $*...
|
||||||
$(CC) -Wp,-MMD,$(STORE)/$*.dd $(CCPARAM) $(foreach INC,$(INCPATH),-I$(INC)) $(PKG_INC)\
|
$(CC) -Wp,-MMD,$(STORE)/$*.dd $(CCPARAM) \
|
||||||
$(foreach CPPDEF,$(CPPDEFS),-D$(CPPDEF)) -c $< -o $@
|
$(foreach INC,$(INCPATH),-I$(INC)) $(PKG_INC) \
|
||||||
@sed -e '1s/^\(.*\)$$/$(subst /,\/,$(dir $@))\1/' $(STORE)/$*.dd > $(STORE)/$*.d
|
$(foreach CPPDEF,$(CPPDEFS),-D$(CPPDEF)) -c $< -o $@
|
||||||
|
@sed -e '1s/^\(.*\)$$/$(subst /,\/,$(dir $@))\1/' \
|
||||||
|
$(STORE)/$*.dd > $(STORE)/$*.d
|
||||||
@rm -f $(STORE)/$*.dd
|
@rm -f $(STORE)/$*.dd
|
||||||
|
|
||||||
$(STORE)/%.o: %.html
|
$(STORE)/%.o: %.html
|
||||||
|
@ -202,17 +207,18 @@ clean:
|
||||||
dirs:
|
dirs:
|
||||||
@-if [ ! -e $(STORE) ]; then mkdir -p $(STORE); fi;
|
@-if [ ! -e $(STORE) ]; then mkdir -p $(STORE); fi;
|
||||||
@-$(foreach DIR,$(DIRS), \
|
@-$(foreach DIR,$(DIRS), \
|
||||||
if [ ! -e $(STORE)/$(DIR) ]; then mkdir -p $(STORE)/$(DIR); fi; )
|
if [ ! -e $(STORE)/$(DIR) ]; then mkdir -p $(STORE)/$(DIR); fi; )
|
||||||
|
|
||||||
preflight:
|
preflight:
|
||||||
ifeq ($(origin MONERO_ROOT), undefined)
|
ifeq ($(origin MONERO_ROOT), undefined)
|
||||||
$(error You need to set an environment variable MONERO_ROOT to your monero repository root)
|
$(error You need to set an environment variable MONERO_ROOT \
|
||||||
|
to your monero repository root)
|
||||||
endif
|
endif
|
||||||
ifndef PKG_LIBS
|
ifndef PKG_LIBS
|
||||||
$(error Missing dependencies)
|
$(error Missing dependencies)
|
||||||
endif
|
endif
|
||||||
ifndef XXD
|
ifndef XXD
|
||||||
$(error Command xxd not found)
|
$(error Command xxd not found)
|
||||||
endif
|
endif
|
||||||
|
|
||||||
-include $(DFILES)
|
-include $(DFILES)
|
||||||
|
|
|
@ -13,6 +13,10 @@ The single payout mechanism is PPLNS, which favors loyal pool miners.
|
||||||
I have no plans to add any other payout mechanisms or other coins. Work should
|
I have no plans to add any other payout mechanisms or other coins. Work should
|
||||||
stay focussed on performance, efficiency and stability.
|
stay focussed on performance, efficiency and stability.
|
||||||
|
|
||||||
|
The pool also now supports a new, experimental and optional, method of mining
|
||||||
|
for the pool miners, whereby miners can select their *own* block template to
|
||||||
|
mine on. Further information can be found in [stratum-ss.md](./stratum-ss.md).
|
||||||
|
|
||||||
## Project status
|
## Project status
|
||||||
|
|
||||||
I have tested this quite a bit on the Monero testnet (if you plan
|
I have tested this quite a bit on the Monero testnet (if you plan
|
||||||
|
@ -134,6 +138,8 @@ pool software has), so if you use it and want to donate, XMR donations to:
|
||||||
451ytzQg1vUVkuAW73VsQ72G96FUjASi4WNQse3v8ALfjiR5vLzGQ2hMUdYhG38Fi15eJ5FJ1ZL4EV1SFVi228muGX4f3SV
|
451ytzQg1vUVkuAW73VsQ72G96FUjASi4WNQse3v8ALfjiR5vLzGQ2hMUdYhG38Fi15eJ5FJ1ZL4EV1SFVi228muGX4f3SV
|
||||||
```
|
```
|
||||||
|
|
||||||
|
![QR code](./qr-small.png)
|
||||||
|
|
||||||
would be very much appreciated.
|
would be very much appreciated.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
|
@ -11,3 +11,4 @@ pool-fee = 0.01
|
||||||
payment-threshold = 0.33
|
payment-threshold = 0.33
|
||||||
log-level = 5
|
log-level = 5
|
||||||
webui-port=4243
|
webui-port=4243
|
||||||
|
disable-self-select=0
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 552 B |
375
src/pool.c
375
src/pool.c
|
@ -87,6 +87,7 @@ developers.
|
||||||
#define ADDRESS_MAX 128
|
#define ADDRESS_MAX 128
|
||||||
#define BLOCK_TIME 120
|
#define BLOCK_TIME 120
|
||||||
#define HR_BLOCK_COUNT 5
|
#define HR_BLOCK_COUNT 5
|
||||||
|
#define TEMLATE_HEIGHT_VARIANCE 5
|
||||||
|
|
||||||
#define uint128_t unsigned __int128
|
#define uint128_t unsigned __int128
|
||||||
|
|
||||||
|
@ -117,6 +118,7 @@ developers.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
enum block_status { BLOCK_LOCKED=0, BLOCK_UNLOCKED=1, BLOCK_ORPHANED=2 };
|
enum block_status { BLOCK_LOCKED=0, BLOCK_UNLOCKED=1, BLOCK_ORPHANED=2 };
|
||||||
|
enum stratum_mode { MODE_NORMAL=0, MODE_SELF_SELECT=1 };
|
||||||
|
|
||||||
typedef struct config_t
|
typedef struct config_t
|
||||||
{
|
{
|
||||||
|
@ -135,6 +137,7 @@ typedef struct config_t
|
||||||
uint32_t webui_port;
|
uint32_t webui_port;
|
||||||
char log_file[MAX_PATH];
|
char log_file[MAX_PATH];
|
||||||
bool block_notified;
|
bool block_notified;
|
||||||
|
bool disable_self_select;
|
||||||
} config_t;
|
} config_t;
|
||||||
|
|
||||||
typedef struct block_template_t
|
typedef struct block_template_t
|
||||||
|
@ -156,6 +159,7 @@ typedef struct job_t
|
||||||
uint64_t target;
|
uint64_t target;
|
||||||
uint128_t *submissions;
|
uint128_t *submissions;
|
||||||
size_t submissions_count;
|
size_t submissions_count;
|
||||||
|
block_template_t *miner_template;
|
||||||
} job_t;
|
} job_t;
|
||||||
|
|
||||||
typedef struct client_t
|
typedef struct client_t
|
||||||
|
@ -171,6 +175,7 @@ typedef struct client_t
|
||||||
uint64_t hashes;
|
uint64_t hashes;
|
||||||
time_t connected_since;
|
time_t connected_since;
|
||||||
bool is_proxy;
|
bool is_proxy;
|
||||||
|
uint32_t mode;
|
||||||
} client_t;
|
} client_t;
|
||||||
|
|
||||||
typedef struct pool_clients_t
|
typedef struct pool_clients_t
|
||||||
|
@ -235,6 +240,8 @@ static BIGNUM *base_diff;
|
||||||
static pool_stats_t pool_stats;
|
static pool_stats_t pool_stats;
|
||||||
static pthread_mutex_t mutex_clients = PTHREAD_MUTEX_INITIALIZER;
|
static pthread_mutex_t mutex_clients = PTHREAD_MUTEX_INITIALIZER;
|
||||||
static FILE *fd_log;
|
static FILE *fd_log;
|
||||||
|
static char sec_view[32];
|
||||||
|
static char pub_spend[32];
|
||||||
|
|
||||||
#define JSON_GET_OR_ERROR(name, parent, type, client) \
|
#define JSON_GET_OR_ERROR(name, parent, type, client) \
|
||||||
json_object *name = NULL; \
|
json_object *name = NULL; \
|
||||||
|
@ -783,6 +790,17 @@ template_recycle(void *item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
retarget(client_t *client, job_t *job)
|
||||||
|
{
|
||||||
|
double duration = difftime(time(NULL), client->connected_since);
|
||||||
|
uint8_t retarget_time = client->is_proxy ? 5 : 120;
|
||||||
|
uint64_t target = fmax((double)client->hashes /
|
||||||
|
duration * retarget_time, config.pool_start_diff);
|
||||||
|
job->target = target;
|
||||||
|
log_debug("Client %.32s target now %"PRIu64, client->client_id, target);
|
||||||
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
target_to_hex(uint64_t target, char *target_hex)
|
target_to_hex(uint64_t target, char *target_hex)
|
||||||
{
|
{
|
||||||
|
@ -790,7 +808,6 @@ target_to_hex(uint64_t target, char *target_hex)
|
||||||
{
|
{
|
||||||
log_debug("High target requested: %"PRIu64, target);
|
log_debug("High target requested: %"PRIu64, target);
|
||||||
bin_to_hex((const char*)&target, 8, &target_hex[0], 16);
|
bin_to_hex((const char*)&target, 8, &target_hex[0], 16);
|
||||||
target_hex[16] = '\0';
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
BIGNUM *diff = BN_new();
|
BIGNUM *diff = BN_new();
|
||||||
|
@ -806,7 +823,6 @@ target_to_hex(uint64_t target, char *target_hex)
|
||||||
BN_rshift(diff, diff, 224);
|
BN_rshift(diff, diff, 224);
|
||||||
uint32_t w = BN_get_word(diff);
|
uint32_t w = BN_get_word(diff);
|
||||||
bin_to_hex((const char*)&w, 4, &target_hex[0], 8);
|
bin_to_hex((const char*)&w, 4, &target_hex[0], 8);
|
||||||
target_hex[8] = '\0';
|
|
||||||
BN_free(bnt);
|
BN_free(bnt);
|
||||||
BN_free(diff);
|
BN_free(diff);
|
||||||
}
|
}
|
||||||
|
@ -818,10 +834,10 @@ stratum_get_proxy_job_body(char *body, const client_t *client,
|
||||||
int json_id = client->json_id;
|
int json_id = client->json_id;
|
||||||
const char *client_id = client->client_id;
|
const char *client_id = client->client_id;
|
||||||
const job_t *job = &client->active_jobs[0];
|
const job_t *job = &client->active_jobs[0];
|
||||||
char job_id[33];
|
char job_id[33] = {0};
|
||||||
bin_to_hex((const char*)job->id, sizeof(uuid_t), job_id, 32);
|
bin_to_hex((const char*)job->id, sizeof(uuid_t), job_id, 32);
|
||||||
uint64_t target = job->target;
|
uint64_t target = job->target;
|
||||||
char target_hex[17];
|
char target_hex[17] = {0};
|
||||||
target_to_hex(target, &target_hex[0]);
|
target_to_hex(target, &target_hex[0]);
|
||||||
const block_template_t *bt = job->block_template;
|
const block_template_t *bt = job->block_template;
|
||||||
|
|
||||||
|
@ -858,18 +874,58 @@ stratum_get_proxy_job_body(char *body, const client_t *client,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
stratum_get_job_body_ss(char *body, const client_t *client, bool response)
|
||||||
|
{
|
||||||
|
/* job_id, target, pool_wallet, extra_nonce */
|
||||||
|
int json_id = client->json_id;
|
||||||
|
const char *client_id = client->client_id;
|
||||||
|
const job_t *job = &client->active_jobs[0];
|
||||||
|
char job_id[33] = {0};
|
||||||
|
bin_to_hex((const char*)job->id, sizeof(uuid_t), job_id, 32);
|
||||||
|
uint64_t target = job->target;
|
||||||
|
char target_hex[17] = {0};
|
||||||
|
target_to_hex(target, &target_hex[0]);
|
||||||
|
char extra_bin[8];
|
||||||
|
memcpy(extra_bin, &job->extra_nonce, 4);
|
||||||
|
memcpy(extra_bin+4, &instance_id, 4);
|
||||||
|
char extra_hex[17] = {0};
|
||||||
|
bin_to_hex(extra_bin, 8, extra_hex, 16);
|
||||||
|
|
||||||
|
if (response)
|
||||||
|
{
|
||||||
|
snprintf(body, JOB_BODY_MAX, "{\"id\":%d,\"jsonrpc\":\"2.0\","
|
||||||
|
"\"error\":null,\"result\""
|
||||||
|
":{\"id\":\"%.32s\",\"job\":{"
|
||||||
|
"\"job_id\":\"%.32s\",\"target\":\"%s\","
|
||||||
|
"\"extra_nonce\":\"%s\", \"pool_wallet\":\"%s\"},"
|
||||||
|
"\"status\":\"OK\"}}\n",
|
||||||
|
json_id, client_id, job_id, target_hex, extra_hex,
|
||||||
|
config.pool_wallet);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
snprintf(body, JOB_BODY_MAX, "{\"jsonrpc\":\"2.0\",\"method\":"
|
||||||
|
"\"job\",\"params\""
|
||||||
|
":{\"id\":\"%.32s\",\"job_id\":\"%.32s\","
|
||||||
|
"\"target\":\"%s\","
|
||||||
|
"\"extra_nonce\":\"%s\", \"pool_wallet\":\"%s\"}}\n",
|
||||||
|
client_id, job_id, target_hex, extra_hex, config.pool_wallet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
stratum_get_job_body(char *body, const client_t *client, bool response)
|
stratum_get_job_body(char *body, const client_t *client, bool response)
|
||||||
{
|
{
|
||||||
int json_id = client->json_id;
|
int json_id = client->json_id;
|
||||||
const char *client_id = client->client_id;
|
const char *client_id = client->client_id;
|
||||||
const job_t *job = &client->active_jobs[0];
|
const job_t *job = &client->active_jobs[0];
|
||||||
char job_id[33];
|
char job_id[33] = {0};
|
||||||
bin_to_hex((const char*)job->id, sizeof(uuid_t), job_id, 32);
|
bin_to_hex((const char*)job->id, sizeof(uuid_t), job_id, 32);
|
||||||
const char *blob = job->blob;
|
const char *blob = job->blob;
|
||||||
uint64_t target = job->target;
|
uint64_t target = job->target;
|
||||||
uint64_t height = job->block_template->height;
|
uint64_t height = job->block_template->height;
|
||||||
char target_hex[17];
|
char target_hex[17] = {0};
|
||||||
target_to_hex(target, &target_hex[0]);
|
target_to_hex(target, &target_hex[0]);
|
||||||
|
|
||||||
if (response)
|
if (response)
|
||||||
|
@ -925,17 +981,28 @@ client_clear_jobs(client_t *client)
|
||||||
for (size_t i=0; i<CLIENT_JOBS_MAX; i++)
|
for (size_t i=0; i<CLIENT_JOBS_MAX; i++)
|
||||||
{
|
{
|
||||||
job_t *job = &client->active_jobs[i];
|
job_t *job = &client->active_jobs[i];
|
||||||
if (job->blob != NULL)
|
if (job->blob)
|
||||||
{
|
{
|
||||||
free(job->blob);
|
free(job->blob);
|
||||||
job->blob = NULL;
|
job->blob = NULL;
|
||||||
}
|
}
|
||||||
if (job->submissions != NULL)
|
if (job->submissions)
|
||||||
{
|
{
|
||||||
free(job->submissions);
|
free(job->submissions);
|
||||||
job->submissions = NULL;
|
job->submissions = NULL;
|
||||||
job->submissions_count = 0;
|
job->submissions_count = 0;
|
||||||
}
|
}
|
||||||
|
if (job->miner_template)
|
||||||
|
{
|
||||||
|
block_template_t *bt = job->miner_template;
|
||||||
|
if (bt->blocktemplate_blob)
|
||||||
|
{
|
||||||
|
free(bt->blocktemplate_blob);
|
||||||
|
bt->blocktemplate_blob = NULL;
|
||||||
|
}
|
||||||
|
free(job->miner_template);
|
||||||
|
job->miner_template = NULL;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -943,7 +1010,7 @@ static job_t *
|
||||||
client_find_job(client_t *client, const char *job_id)
|
client_find_job(client_t *client, const char *job_id)
|
||||||
{
|
{
|
||||||
uuid_t jid;
|
uuid_t jid;
|
||||||
hex_to_bin(job_id, (char*)&jid, sizeof(uuid_t));
|
hex_to_bin(job_id, strlen(job_id), (char*)&jid, sizeof(uuid_t));
|
||||||
for (size_t i=0; i<CLIENT_JOBS_MAX; i++)
|
for (size_t i=0; i<CLIENT_JOBS_MAX; i++)
|
||||||
{
|
{
|
||||||
job_t *job = &client->active_jobs[i];
|
job_t *job = &client->active_jobs[i];
|
||||||
|
@ -958,17 +1025,28 @@ client_send_job(client_t *client, bool response)
|
||||||
{
|
{
|
||||||
/* First cycle jobs */
|
/* First cycle jobs */
|
||||||
job_t *last = &client->active_jobs[CLIENT_JOBS_MAX-1];
|
job_t *last = &client->active_jobs[CLIENT_JOBS_MAX-1];
|
||||||
if (last->blob != NULL)
|
if (last->blob)
|
||||||
{
|
{
|
||||||
free(last->blob);
|
free(last->blob);
|
||||||
last->blob = NULL;
|
last->blob = NULL;
|
||||||
}
|
}
|
||||||
if (last->submissions != NULL)
|
if (last->submissions)
|
||||||
{
|
{
|
||||||
free(last->submissions);
|
free(last->submissions);
|
||||||
last->submissions = NULL;
|
last->submissions = NULL;
|
||||||
last->submissions_count = 0;
|
last->submissions_count = 0;
|
||||||
}
|
}
|
||||||
|
if (last->miner_template)
|
||||||
|
{
|
||||||
|
block_template_t *bt = last->miner_template;
|
||||||
|
if (bt->blocktemplate_blob)
|
||||||
|
{
|
||||||
|
free(bt->blocktemplate_blob);
|
||||||
|
bt->blocktemplate_blob = NULL;
|
||||||
|
}
|
||||||
|
free(last->miner_template);
|
||||||
|
last->miner_template = NULL;
|
||||||
|
}
|
||||||
for (size_t i=CLIENT_JOBS_MAX-1; i>0; i--)
|
for (size_t i=CLIENT_JOBS_MAX-1; i>0; i--)
|
||||||
{
|
{
|
||||||
job_t *current = &client->active_jobs[i];
|
job_t *current = &client->active_jobs[i];
|
||||||
|
@ -978,6 +1056,20 @@ client_send_job(client_t *client, bool response)
|
||||||
job_t *job = &client->active_jobs[0];
|
job_t *job = &client->active_jobs[0];
|
||||||
memset(job, 0, sizeof(job_t));
|
memset(job, 0, sizeof(job_t));
|
||||||
|
|
||||||
|
if (client->mode == MODE_SELF_SELECT)
|
||||||
|
{
|
||||||
|
uuid_generate(job->id);
|
||||||
|
retarget(client, job);
|
||||||
|
++extra_nonce;
|
||||||
|
job->extra_nonce = extra_nonce;
|
||||||
|
char body[JOB_BODY_MAX];
|
||||||
|
stratum_get_job_body_ss(body, client, response);
|
||||||
|
log_trace("Client job: %s", body);
|
||||||
|
struct evbuffer *output = bufferevent_get_output(client->bev);
|
||||||
|
evbuffer_add(output, body, strlen(body));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
/* Quick check we actually have a block template */
|
/* Quick check we actually have a block template */
|
||||||
block_template_t *bt = bstack_peek(bst);
|
block_template_t *bt = bstack_peek(bst);
|
||||||
if (!bt)
|
if (!bt)
|
||||||
|
@ -996,7 +1088,7 @@ client_send_job(client_t *client, bool response)
|
||||||
/* Convert template to blob */
|
/* Convert template to blob */
|
||||||
size_t bin_size = strlen(bt->blocktemplate_blob) >> 1;
|
size_t bin_size = strlen(bt->blocktemplate_blob) >> 1;
|
||||||
char *block = calloc(bin_size, sizeof(char));
|
char *block = calloc(bin_size, sizeof(char));
|
||||||
hex_to_bin(bt->blocktemplate_blob, block, bin_size);
|
hex_to_bin(bt->blocktemplate_blob, bin_size << 1, block, bin_size);
|
||||||
|
|
||||||
/* Set the extra nonce in our reserved space */
|
/* Set the extra nonce in our reserved space */
|
||||||
char *p = block;
|
char *p = block;
|
||||||
|
@ -1027,16 +1119,11 @@ client_send_job(client_t *client, bool response)
|
||||||
job->block_template = bt;
|
job->block_template = bt;
|
||||||
|
|
||||||
/* Send */
|
/* Send */
|
||||||
char job_id[33];
|
char job_id[33] = {0};
|
||||||
bin_to_hex((const char*)job->id, sizeof(uuid_t), job_id, 32);
|
bin_to_hex((const char*)job->id, sizeof(uuid_t), job_id, 32);
|
||||||
|
|
||||||
/* Retarget */
|
/* Retarget */
|
||||||
double duration = difftime(time(NULL), client->connected_since);
|
retarget(client, job);
|
||||||
uint8_t retarget_time = client->is_proxy ? 5 : 120;
|
|
||||||
uint64_t target = fmax((double)client->hashes /
|
|
||||||
duration * retarget_time, config.pool_start_diff);
|
|
||||||
job->target = target;
|
|
||||||
log_debug("Client %.32s target now %"PRIu64, client->client_id, target);
|
|
||||||
|
|
||||||
char body[JOB_BODY_MAX];
|
char body[JOB_BODY_MAX];
|
||||||
if (!client->is_proxy)
|
if (!client->is_proxy)
|
||||||
|
@ -1259,7 +1346,7 @@ rpc_on_block_header_by_height(const char* data, rpc_callback_t *callback)
|
||||||
const char *ss = json_object_get_string(status);
|
const char *ss = json_object_get_string(status);
|
||||||
json_object *error = NULL;
|
json_object *error = NULL;
|
||||||
json_object_object_get_ex(root, "error", &error);
|
json_object_object_get_ex(root, "error", &error);
|
||||||
if (error != NULL)
|
if (error)
|
||||||
{
|
{
|
||||||
JSON_GET_OR_WARN(code, error, json_type_object);
|
JSON_GET_OR_WARN(code, error, json_type_object);
|
||||||
JSON_GET_OR_WARN(message, error, json_type_string);
|
JSON_GET_OR_WARN(message, error, json_type_string);
|
||||||
|
@ -1269,7 +1356,7 @@ rpc_on_block_header_by_height(const char* data, rpc_callback_t *callback)
|
||||||
json_object_put(root);
|
json_object_put(root);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (status == NULL || strcmp(ss, "OK") != 0)
|
if (!status || strcmp(ss, "OK") != 0)
|
||||||
{
|
{
|
||||||
log_error("Error getting block header by height: %s", ss);
|
log_error("Error getting block header by height: %s", ss);
|
||||||
json_object_put(root);
|
json_object_put(root);
|
||||||
|
@ -1291,7 +1378,7 @@ rpc_on_block_headers_range(const char* data, rpc_callback_t *callback)
|
||||||
const char *ss = json_object_get_string(status);
|
const char *ss = json_object_get_string(status);
|
||||||
json_object *error = NULL;
|
json_object *error = NULL;
|
||||||
json_object_object_get_ex(root, "error", &error);
|
json_object_object_get_ex(root, "error", &error);
|
||||||
if (error != NULL)
|
if (error)
|
||||||
{
|
{
|
||||||
JSON_GET_OR_WARN(code, error, json_type_object);
|
JSON_GET_OR_WARN(code, error, json_type_object);
|
||||||
JSON_GET_OR_WARN(message, error, json_type_string);
|
JSON_GET_OR_WARN(message, error, json_type_string);
|
||||||
|
@ -1301,7 +1388,7 @@ rpc_on_block_headers_range(const char* data, rpc_callback_t *callback)
|
||||||
json_object_put(root);
|
json_object_put(root);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (status == NULL || strcmp(ss, "OK") != 0)
|
if (!status || strcmp(ss, "OK") != 0)
|
||||||
{
|
{
|
||||||
log_warn("Error getting block headers by range: %s", ss);
|
log_warn("Error getting block headers by range: %s", ss);
|
||||||
json_object_put(root);
|
json_object_put(root);
|
||||||
|
@ -1331,7 +1418,7 @@ rpc_on_block_template(const char* data, rpc_callback_t *callback)
|
||||||
const char *ss = json_object_get_string(status);
|
const char *ss = json_object_get_string(status);
|
||||||
json_object *error = NULL;
|
json_object *error = NULL;
|
||||||
json_object_object_get_ex(root, "error", &error);
|
json_object_object_get_ex(root, "error", &error);
|
||||||
if (error != NULL)
|
if (error)
|
||||||
{
|
{
|
||||||
JSON_GET_OR_WARN(code, error, json_type_object);
|
JSON_GET_OR_WARN(code, error, json_type_object);
|
||||||
JSON_GET_OR_WARN(message, error, json_type_string);
|
JSON_GET_OR_WARN(message, error, json_type_string);
|
||||||
|
@ -1341,7 +1428,7 @@ rpc_on_block_template(const char* data, rpc_callback_t *callback)
|
||||||
json_object_put(root);
|
json_object_put(root);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (status == NULL || strcmp(ss, "OK") != 0)
|
if (!status || strcmp(ss, "OK") != 0)
|
||||||
{
|
{
|
||||||
log_error("Error getting block template: %s", ss);
|
log_error("Error getting block template: %s", ss);
|
||||||
json_object_put(root);
|
json_object_put(root);
|
||||||
|
@ -1419,6 +1506,32 @@ startup_pauout(uint64_t height)
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
rpc_on_view_key(const char* data, rpc_callback_t *callback)
|
||||||
|
{
|
||||||
|
json_object *root = json_tokener_parse(data);
|
||||||
|
JSON_GET_OR_WARN(result, root, json_type_object);
|
||||||
|
JSON_GET_OR_WARN(key, result, json_type_string);
|
||||||
|
json_object *error = NULL;
|
||||||
|
json_object_object_get_ex(root, "error", &error);
|
||||||
|
if (error)
|
||||||
|
{
|
||||||
|
JSON_GET_OR_WARN(code, error, json_type_object);
|
||||||
|
JSON_GET_OR_WARN(message, error, json_type_string);
|
||||||
|
int ec = json_object_get_int(code);
|
||||||
|
const char *em = json_object_get_string(message);
|
||||||
|
log_error("Error (%d) getting key: %s", ec, em);
|
||||||
|
json_object_put(root);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const char *vk = json_object_get_string(key);
|
||||||
|
hex_to_bin(vk, strlen(vk), &sec_view[0], 32);
|
||||||
|
json_object_put(root);
|
||||||
|
|
||||||
|
uint64_t prefix;
|
||||||
|
parse_address(config.pool_wallet, &prefix, &pub_spend[0]);
|
||||||
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
rpc_on_last_block_header(const char* data, rpc_callback_t *callback)
|
rpc_on_last_block_header(const char* data, rpc_callback_t *callback)
|
||||||
{
|
{
|
||||||
|
@ -1429,7 +1542,7 @@ rpc_on_last_block_header(const char* data, rpc_callback_t *callback)
|
||||||
const char *ss = json_object_get_string(status);
|
const char *ss = json_object_get_string(status);
|
||||||
json_object *error = NULL;
|
json_object *error = NULL;
|
||||||
json_object_object_get_ex(root, "error", &error);
|
json_object_object_get_ex(root, "error", &error);
|
||||||
if (error != NULL)
|
if (error)
|
||||||
{
|
{
|
||||||
JSON_GET_OR_WARN(code, error, json_type_object);
|
JSON_GET_OR_WARN(code, error, json_type_object);
|
||||||
JSON_GET_OR_WARN(message, error, json_type_string);
|
JSON_GET_OR_WARN(message, error, json_type_string);
|
||||||
|
@ -1439,7 +1552,7 @@ rpc_on_last_block_header(const char* data, rpc_callback_t *callback)
|
||||||
json_object_put(root);
|
json_object_put(root);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (status == NULL || strcmp(ss, "OK") != 0)
|
if (!status || strcmp(ss, "OK") != 0)
|
||||||
{
|
{
|
||||||
log_error("Error getting last block header: %s", ss);
|
log_error("Error getting last block header: %s", ss);
|
||||||
json_object_put(root);
|
json_object_put(root);
|
||||||
|
@ -1451,13 +1564,13 @@ rpc_on_last_block_header(const char* data, rpc_callback_t *callback)
|
||||||
uint64_t bh = json_object_get_int64(height);
|
uint64_t bh = json_object_get_int64(height);
|
||||||
bool need_new_template = false;
|
bool need_new_template = false;
|
||||||
block_t *front = bstack_peek(bsh);
|
block_t *front = bstack_peek(bsh);
|
||||||
if (front != NULL && bh > front->height)
|
if (front && bh > front->height)
|
||||||
{
|
{
|
||||||
need_new_template = true;
|
need_new_template = true;
|
||||||
block_t *block = bstack_push(bsh, NULL);
|
block_t *block = bstack_push(bsh, NULL);
|
||||||
response_to_block(block_header, block);
|
response_to_block(block_header, block);
|
||||||
}
|
}
|
||||||
else if (front == NULL)
|
else if (!front)
|
||||||
{
|
{
|
||||||
block_t *block = bstack_push(bsh, NULL);
|
block_t *block = bstack_push(bsh, NULL);
|
||||||
response_to_block(block_header, block);
|
response_to_block(block_header, block);
|
||||||
|
@ -1507,7 +1620,7 @@ rpc_on_block_submitted(const char* data, rpc_callback_t *callback)
|
||||||
an alternative block. Thus, still store it. This doesn't matter
|
an alternative block. Thus, still store it. This doesn't matter
|
||||||
as upon payout, blocks are checked whether they are orphaned or not.
|
as upon payout, blocks are checked whether they are orphaned or not.
|
||||||
*/
|
*/
|
||||||
if (error != NULL)
|
if (error)
|
||||||
{
|
{
|
||||||
JSON_GET_OR_WARN(code, error, json_type_object);
|
JSON_GET_OR_WARN(code, error, json_type_object);
|
||||||
JSON_GET_OR_WARN(message, error, json_type_string);
|
JSON_GET_OR_WARN(message, error, json_type_string);
|
||||||
|
@ -1515,7 +1628,7 @@ rpc_on_block_submitted(const char* data, rpc_callback_t *callback)
|
||||||
const char *em = json_object_get_string(message);
|
const char *em = json_object_get_string(message);
|
||||||
log_warn("Error (%d) with block submission: %s", ec, em);
|
log_warn("Error (%d) with block submission: %s", ec, em);
|
||||||
}
|
}
|
||||||
if (status == NULL || strcmp(ss, "OK") != 0)
|
if (!status || strcmp(ss, "OK") != 0)
|
||||||
{
|
{
|
||||||
log_warn("Error submitting block: %s", ss);
|
log_warn("Error submitting block: %s", ss);
|
||||||
}
|
}
|
||||||
|
@ -1537,7 +1650,7 @@ rpc_on_wallet_transferred(const char* data, rpc_callback_t *callback)
|
||||||
JSON_GET_OR_WARN(result, root, json_type_object);
|
JSON_GET_OR_WARN(result, root, json_type_object);
|
||||||
json_object *error = NULL;
|
json_object *error = NULL;
|
||||||
json_object_object_get_ex(root, "error", &error);
|
json_object_object_get_ex(root, "error", &error);
|
||||||
if (error != NULL)
|
if (error)
|
||||||
{
|
{
|
||||||
JSON_GET_OR_WARN(code, error, json_type_object);
|
JSON_GET_OR_WARN(code, error, json_type_object);
|
||||||
JSON_GET_OR_WARN(message, error, json_type_string);
|
JSON_GET_OR_WARN(message, error, json_type_string);
|
||||||
|
@ -1741,6 +1854,15 @@ send_payments(void)
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
fetch_view_key(void)
|
||||||
|
{
|
||||||
|
char body[RPC_BODY_MAX];
|
||||||
|
rpc_get_request_body(body, "query_key", "ss", "key_type", "view_key");
|
||||||
|
rpc_callback_t *cb = rpc_callback_new(rpc_on_view_key, NULL);
|
||||||
|
rpc_wallet_request(base, body, cb);
|
||||||
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
fetch_last_block_header(void)
|
fetch_last_block_header(void)
|
||||||
{
|
{
|
||||||
|
@ -1819,9 +1941,9 @@ client_find(struct bufferevent *bev, client_t **client)
|
||||||
static void
|
static void
|
||||||
client_clear(struct bufferevent *bev)
|
client_clear(struct bufferevent *bev)
|
||||||
{
|
{
|
||||||
client_t *client;
|
client_t *client = NULL;
|
||||||
client_find(bev, &client);
|
client_find(bev, &client);
|
||||||
if (client == NULL)
|
if (!client)
|
||||||
return;
|
return;
|
||||||
client_clear_jobs(client);
|
client_clear_jobs(client);
|
||||||
memset(client, 0, sizeof(client_t));
|
memset(client, 0, sizeof(client_t));
|
||||||
|
@ -1835,10 +1957,31 @@ client_on_login(json_object *message, client_t *client)
|
||||||
JSON_GET_OR_ERROR(params, message, json_type_object, client);
|
JSON_GET_OR_ERROR(params, message, json_type_object, client);
|
||||||
JSON_GET_OR_ERROR(login, params, json_type_string, client);
|
JSON_GET_OR_ERROR(login, params, json_type_string, client);
|
||||||
JSON_GET_OR_ERROR(pass, params, json_type_string, client);
|
JSON_GET_OR_ERROR(pass, params, json_type_string, client);
|
||||||
|
client->mode = MODE_NORMAL;
|
||||||
|
json_object *mode = NULL;
|
||||||
|
if (json_object_object_get_ex(params, "mode", &mode))
|
||||||
|
{
|
||||||
|
if (!json_object_is_type(mode, json_type_string))
|
||||||
|
log_warn("mode not a json_type_string");
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const char *modestr = json_object_get_string(mode);
|
||||||
|
if (strcmp(modestr, "self-select") == 0)
|
||||||
|
{
|
||||||
|
if (config.disable_self_select)
|
||||||
|
{
|
||||||
|
return send_validation_error(client,
|
||||||
|
"pool disabled self-select");
|
||||||
|
}
|
||||||
|
client->mode = MODE_SELF_SELECT;
|
||||||
|
log_trace("Client login for mode: self-select");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const char *address = json_object_get_string(login);
|
const char *address = json_object_get_string(login);
|
||||||
uint64_t prefix;
|
uint64_t prefix;
|
||||||
parse_address(address, &prefix);
|
parse_address(address, &prefix, NULL);
|
||||||
if (prefix != MAINNET_ADDRESS_PREFIX && prefix != TESTNET_ADDRESS_PREFIX)
|
if (prefix != MAINNET_ADDRESS_PREFIX && prefix != TESTNET_ADDRESS_PREFIX)
|
||||||
return send_validation_error(client,
|
return send_validation_error(client,
|
||||||
"login only main wallet addresses are supported");
|
"login only main wallet addresses are supported");
|
||||||
|
@ -1857,16 +2000,74 @@ client_on_login(json_object *message, client_t *client)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (client->is_proxy && client->mode == MODE_SELF_SELECT)
|
||||||
|
{
|
||||||
|
return send_validation_error(client,
|
||||||
|
"login mode self-select and proxy not yet supported");
|
||||||
|
}
|
||||||
|
|
||||||
strncpy(client->address, address, sizeof(client->address));
|
strncpy(client->address, address, sizeof(client->address));
|
||||||
strncpy(client->worker_id, worker_id, sizeof(client->worker_id));
|
strncpy(client->worker_id, worker_id, sizeof(client->worker_id));
|
||||||
uuid_t cid;
|
uuid_t cid;
|
||||||
uuid_generate(cid);
|
uuid_generate(cid);
|
||||||
bin_to_hex((const char*)cid, sizeof(uuid_t), client->client_id, 32);
|
bin_to_hex((const char*)cid, sizeof(uuid_t), client->client_id, 32);
|
||||||
char status[256];
|
|
||||||
snprintf(status, 256, "Logged in: %s %s\n", worker_id, address);
|
|
||||||
client_send_job(client, true);
|
client_send_job(client, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
client_on_block_template(json_object *message, client_t *client)
|
||||||
|
{
|
||||||
|
struct evbuffer *output = bufferevent_get_output(client->bev);
|
||||||
|
|
||||||
|
JSON_GET_OR_ERROR(params, message, json_type_object, client);
|
||||||
|
JSON_GET_OR_ERROR(id, params, json_type_string, client);
|
||||||
|
JSON_GET_OR_ERROR(job_id, params, json_type_string, client);
|
||||||
|
JSON_GET_OR_ERROR(blob, params, json_type_string, client);
|
||||||
|
JSON_GET_OR_ERROR(difficulty, params, json_type_int, client);
|
||||||
|
JSON_GET_OR_ERROR(height, params, json_type_int, client);
|
||||||
|
JSON_GET_OR_ERROR(prev_hash, params, json_type_string, client);
|
||||||
|
|
||||||
|
const char *jid = json_object_get_string(job_id);
|
||||||
|
if (strlen(jid) != 32)
|
||||||
|
return send_validation_error(client, "job_id invalid length");
|
||||||
|
|
||||||
|
int64_t h = json_object_get_int64(height);
|
||||||
|
int64_t dh = llabs(h - (int64_t)pool_stats.network_height);
|
||||||
|
if (dh > TEMLATE_HEIGHT_VARIANCE)
|
||||||
|
{
|
||||||
|
char m[64];
|
||||||
|
snprintf(m, 64, "Bad height. Differs to pool by %"PRIu64" blocks.", dh);
|
||||||
|
return send_validation_error(client, m);
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *btb = json_object_get_string(blob);
|
||||||
|
int rc = validate_block_from_blob(btb, &sec_view[0], &pub_spend[0]);
|
||||||
|
if (rc != 0)
|
||||||
|
{
|
||||||
|
log_warn("Bad template submitted: %d", rc);
|
||||||
|
return send_validation_error(client, "block template blob invalid");
|
||||||
|
}
|
||||||
|
|
||||||
|
job_t *job = client_find_job(client, jid);
|
||||||
|
if (!job)
|
||||||
|
return send_validation_error(client, "cannot find job with job_id");
|
||||||
|
|
||||||
|
if (job->miner_template)
|
||||||
|
return send_validation_error(client, "job already has block template");
|
||||||
|
|
||||||
|
job->miner_template = calloc(1, sizeof(block_template_t));
|
||||||
|
job->miner_template->blocktemplate_blob = strdup(btb);
|
||||||
|
job->miner_template->difficulty = json_object_get_int64(difficulty);
|
||||||
|
job->miner_template->height = json_object_get_int64(height);
|
||||||
|
memcpy(job->miner_template->prev_hash,
|
||||||
|
json_object_get_string(prev_hash), 64);
|
||||||
|
|
||||||
|
log_trace("Client set template: %s", btb);
|
||||||
|
char body[STATUS_BODY_MAX];
|
||||||
|
stratum_get_status_body(body, client->json_id, "OK");
|
||||||
|
evbuffer_add(output, body, strlen(body));
|
||||||
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
client_on_submit(json_object *message, client_t *client)
|
client_on_submit(json_object *message, client_t *client)
|
||||||
{
|
{
|
||||||
|
@ -1920,36 +2121,46 @@ client_on_submit(json_object *message, client_t *client)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* Convert template to blob */
|
/* Convert template to blob */
|
||||||
block_template_t *bt = job->block_template;
|
if (client->mode == MODE_SELF_SELECT && !job->miner_template)
|
||||||
|
return send_validation_error(client, "mode self-selct and no template");
|
||||||
|
block_template_t *bt;
|
||||||
|
if (job->miner_template)
|
||||||
|
bt = job->miner_template;
|
||||||
|
else
|
||||||
|
bt = job->block_template;
|
||||||
char *btb = bt->blocktemplate_blob;
|
char *btb = bt->blocktemplate_blob;
|
||||||
size_t bin_size = strlen(btb) >> 1;
|
size_t bin_size = strlen(btb) >> 1;
|
||||||
char *block = calloc(bin_size, sizeof(char));
|
char *block = calloc(bin_size, sizeof(char));
|
||||||
hex_to_bin(bt->blocktemplate_blob, block, bin_size);
|
hex_to_bin(bt->blocktemplate_blob, bin_size << 1, block, bin_size);
|
||||||
|
|
||||||
/* Set the extra nonce and instance_id in our reserved space */
|
|
||||||
char *p = block;
|
char *p = block;
|
||||||
p += bt->reserved_offset;
|
|
||||||
memcpy(p, &job->extra_nonce, sizeof(extra_nonce));
|
|
||||||
p += 4;
|
|
||||||
memcpy(p, &instance_id, sizeof(instance_id));
|
|
||||||
|
|
||||||
uint32_t pool_nonce = 0;
|
uint32_t pool_nonce = 0;
|
||||||
uint32_t worker_nonce = 0;
|
uint32_t worker_nonce = 0;
|
||||||
if (client->is_proxy)
|
|
||||||
|
if (client->mode != MODE_SELF_SELECT)
|
||||||
{
|
{
|
||||||
/*
|
/* Set the extra nonce and instance_id in our reserved space */
|
||||||
A proxy supplies pool_nonce and worker_nonce
|
p += bt->reserved_offset;
|
||||||
so add them in the resrved space too.
|
memcpy(p, &job->extra_nonce, sizeof(extra_nonce));
|
||||||
*/
|
|
||||||
JSON_GET_OR_WARN(poolNonce, params, json_type_int);
|
|
||||||
JSON_GET_OR_WARN(workerNonce, params, json_type_int);
|
|
||||||
pool_nonce = json_object_get_int(poolNonce);
|
|
||||||
worker_nonce = json_object_get_int(workerNonce);
|
|
||||||
p += 4;
|
p += 4;
|
||||||
memcpy(p, &pool_nonce, sizeof(pool_nonce));
|
memcpy(p, &instance_id, sizeof(instance_id));
|
||||||
p += 4;
|
if (client->is_proxy)
|
||||||
memcpy(p, &worker_nonce, sizeof(worker_nonce));
|
{
|
||||||
|
/*
|
||||||
|
A proxy supplies pool_nonce and worker_nonce
|
||||||
|
so add them in the resrved space too.
|
||||||
|
*/
|
||||||
|
JSON_GET_OR_WARN(poolNonce, params, json_type_int);
|
||||||
|
JSON_GET_OR_WARN(workerNonce, params, json_type_int);
|
||||||
|
pool_nonce = json_object_get_int(poolNonce);
|
||||||
|
worker_nonce = json_object_get_int(workerNonce);
|
||||||
|
p += 4;
|
||||||
|
memcpy(p, &pool_nonce, sizeof(pool_nonce));
|
||||||
|
p += 4;
|
||||||
|
memcpy(p, &worker_nonce, sizeof(worker_nonce));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
uint128_t sub = 0;
|
uint128_t sub = 0;
|
||||||
uint32_t *psub = (uint32_t*) ⊂
|
uint32_t *psub = (uint32_t*) ⊂
|
||||||
*psub++ = result_nonce;
|
*psub++ = result_nonce;
|
||||||
|
@ -2005,7 +2216,7 @@ client_on_submit(json_object *message, client_t *client)
|
||||||
const int cn_variant = major_version >= 7 ? major_version - 6 : 0;
|
const int cn_variant = major_version >= 7 ? major_version - 6 : 0;
|
||||||
get_hash(hashing_blob, hashing_blob_size,
|
get_hash(hashing_blob, hashing_blob_size,
|
||||||
(char**)&result_hash, cn_variant, bt->height);
|
(char**)&result_hash, cn_variant, bt->height);
|
||||||
hex_to_bin(result_hex, submitted_hash, 32);
|
hex_to_bin(result_hex, 64, submitted_hash, 32);
|
||||||
|
|
||||||
if (memcmp(submitted_hash, result_hash, 32) != 0)
|
if (memcmp(submitted_hash, result_hash, 32) != 0)
|
||||||
{
|
{
|
||||||
|
@ -2102,17 +2313,18 @@ client_on_read(struct bufferevent *bev, void *ctx)
|
||||||
struct evbuffer *input, *output;
|
struct evbuffer *input, *output;
|
||||||
char *line;
|
char *line;
|
||||||
size_t n;
|
size_t n;
|
||||||
client_t *client;
|
client_t *client = NULL;
|
||||||
|
|
||||||
client_find(bev, &client);
|
client_find(bev, &client);
|
||||||
if (client == NULL)
|
if (!client)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
input = bufferevent_get_input(bev);
|
input = bufferevent_get_input(bev);
|
||||||
output = bufferevent_get_output(bev);
|
output = bufferevent_get_output(bev);
|
||||||
|
|
||||||
size_t len = evbuffer_get_length(input);
|
size_t len = evbuffer_get_length(input);
|
||||||
if (len >= MAX_LINE)
|
if (len > MAX_LINE ||
|
||||||
|
(client->mode == MODE_SELF_SELECT && len > MAX_LINE<<2))
|
||||||
{
|
{
|
||||||
const char *too_long = "Message too long\n";
|
const char *too_long = "Message too long\n";
|
||||||
evbuffer_add(output, too_long, strlen(too_long));
|
evbuffer_add(output, too_long, strlen(too_long));
|
||||||
|
@ -2132,7 +2344,7 @@ client_on_read(struct bufferevent *bev, void *ctx)
|
||||||
|
|
||||||
bool unknown = false;
|
bool unknown = false;
|
||||||
|
|
||||||
if (method_name == NULL)
|
if (!method || !method_name)
|
||||||
{
|
{
|
||||||
unknown = true;
|
unknown = true;
|
||||||
}
|
}
|
||||||
|
@ -2140,6 +2352,10 @@ client_on_read(struct bufferevent *bev, void *ctx)
|
||||||
{
|
{
|
||||||
client_on_login(message, client);
|
client_on_login(message, client);
|
||||||
}
|
}
|
||||||
|
else if (strcmp(method_name, "block_template") == 0)
|
||||||
|
{
|
||||||
|
client_on_block_template(message, client);
|
||||||
|
}
|
||||||
else if (strcmp(method_name, "submit") == 0)
|
else if (strcmp(method_name, "submit") == 0)
|
||||||
{
|
{
|
||||||
client_on_submit(message, client);
|
client_on_submit(message, client);
|
||||||
|
@ -2238,8 +2454,9 @@ read_config(const char *config_file, const char *log_file, bool block_notified)
|
||||||
config.log_level = 5;
|
config.log_level = 5;
|
||||||
config.webui_port = 4243;
|
config.webui_port = 4243;
|
||||||
config.block_notified = block_notified;
|
config.block_notified = block_notified;
|
||||||
|
config.disable_self_select = false;
|
||||||
|
|
||||||
char path[MAX_PATH];
|
char path[MAX_PATH] = {0};
|
||||||
if (config_file)
|
if (config_file)
|
||||||
{
|
{
|
||||||
strncpy(path, config_file, MAX_PATH);
|
strncpy(path, config_file, MAX_PATH);
|
||||||
|
@ -2267,7 +2484,7 @@ read_config(const char *config_file, const char *log_file, bool block_notified)
|
||||||
log_info("Reading config at: %s", path);
|
log_info("Reading config at: %s", path);
|
||||||
|
|
||||||
FILE *fp = fopen(path, "r");
|
FILE *fp = fopen(path, "r");
|
||||||
if (fp == NULL)
|
if (!fp)
|
||||||
{
|
{
|
||||||
log_fatal("Cannot open config file. Aborting.");
|
log_fatal("Cannot open config file. Aborting.");
|
||||||
abort();
|
abort();
|
||||||
|
@ -2276,13 +2493,13 @@ read_config(const char *config_file, const char *log_file, bool block_notified)
|
||||||
char *key;
|
char *key;
|
||||||
char *val;
|
char *val;
|
||||||
const char *tok = " =";
|
const char *tok = " =";
|
||||||
while (fgets(line, sizeof(line), fp) != NULL)
|
while (fgets(line, sizeof(line), fp))
|
||||||
{
|
{
|
||||||
key = strtok(line, tok);
|
key = strtok(line, tok);
|
||||||
if (key == NULL)
|
if (!key)
|
||||||
continue;
|
continue;
|
||||||
val = strtok(NULL, tok);
|
val = strtok(NULL, tok);
|
||||||
if (val == NULL)
|
if (!val)
|
||||||
continue;
|
continue;
|
||||||
val[strcspn(val, "\r\n")] = 0;
|
val[strcspn(val, "\r\n")] = 0;
|
||||||
if (strcmp(key, "rpc-host") == 0)
|
if (strcmp(key, "rpc-host") == 0)
|
||||||
|
@ -2345,10 +2562,14 @@ read_config(const char *config_file, const char *log_file, bool block_notified)
|
||||||
{
|
{
|
||||||
config.block_notified = atoi(val);
|
config.block_notified = atoi(val);
|
||||||
}
|
}
|
||||||
|
else if (strcmp(key, "disable-self-select") == 0)
|
||||||
|
{
|
||||||
|
config.disable_self_select = atoi(val);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
fclose(fp);
|
fclose(fp);
|
||||||
|
|
||||||
if (log_file != NULL)
|
if (log_file)
|
||||||
strncpy(config.log_file, log_file, sizeof(config.log_file));
|
strncpy(config.log_file, log_file, sizeof(config.log_file));
|
||||||
|
|
||||||
if (!config.pool_wallet[0])
|
if (!config.pool_wallet[0])
|
||||||
|
@ -2368,13 +2589,14 @@ read_config(const char *config_file, const char *log_file, bool block_notified)
|
||||||
"pool_fee = %.3f\n payment_threshold = %.2f\n "
|
"pool_fee = %.3f\n payment_threshold = %.2f\n "
|
||||||
"wallet_rpc_host = %s\n wallet_rpc_port = %u\n pool_port = %u\n "
|
"wallet_rpc_host = %s\n wallet_rpc_port = %u\n pool_port = %u\n "
|
||||||
"log_level = %u\n webui_port=%u\n "
|
"log_level = %u\n webui_port=%u\n "
|
||||||
"log-file = %s\n block-notified = %u\n",
|
"log-file = %s\n block-notified = %u\n "
|
||||||
|
"disable-self-select = %u\n",
|
||||||
config.rpc_host, config.rpc_port, config.rpc_timeout,
|
config.rpc_host, config.rpc_port, config.rpc_timeout,
|
||||||
config.pool_wallet, config.pool_start_diff, config.share_mul,
|
config.pool_wallet, config.pool_start_diff, config.share_mul,
|
||||||
config.pool_fee, config.payment_threshold,
|
config.pool_fee, config.payment_threshold,
|
||||||
config.wallet_rpc_host, config.wallet_rpc_port, config.pool_port,
|
config.wallet_rpc_host, config.wallet_rpc_port, config.pool_port,
|
||||||
config.log_level, config.webui_port,
|
config.log_level, config.webui_port,
|
||||||
config.log_file, config.block_notified);
|
config.log_file, config.block_notified, config.disable_self_select);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
|
@ -2448,6 +2670,8 @@ run(void)
|
||||||
else
|
else
|
||||||
fetch_last_block_header();
|
fetch_last_block_header();
|
||||||
|
|
||||||
|
fetch_view_key();
|
||||||
|
|
||||||
timer_10m = evtimer_new(base, timer_on_10m, NULL);
|
timer_10m = evtimer_new(base, timer_on_10m, NULL);
|
||||||
timer_on_10m(-1, EV_TIMEOUT, NULL);
|
timer_on_10m(-1, EV_TIMEOUT, NULL);
|
||||||
|
|
||||||
|
@ -2476,7 +2700,7 @@ cleanup(void)
|
||||||
BN_CTX_free(bn_ctx);
|
BN_CTX_free(bn_ctx);
|
||||||
pthread_mutex_destroy(&mutex_clients);
|
pthread_mutex_destroy(&mutex_clients);
|
||||||
log_info("Pool shutdown successfully");
|
log_info("Pool shutdown successfully");
|
||||||
if (fd_log != NULL)
|
if (fd_log)
|
||||||
fclose(fd_log);
|
fclose(fd_log);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2525,19 +2749,19 @@ int main(int argc, char **argv)
|
||||||
read_config(config_file, log_file, block_notified);
|
read_config(config_file, log_file, block_notified);
|
||||||
|
|
||||||
log_set_level(LOG_FATAL - config.log_level);
|
log_set_level(LOG_FATAL - config.log_level);
|
||||||
if (config.log_file[0] != '\0')
|
if (config.log_file[0])
|
||||||
{
|
{
|
||||||
fd_log = fopen(config.log_file, "a");
|
fd_log = fopen(config.log_file, "a");
|
||||||
if (fd_log == NULL)
|
if (!fd_log)
|
||||||
log_info("Failed to open log file: %s", config.log_file);
|
log_info("Failed to open log file: %s", config.log_file);
|
||||||
else
|
else
|
||||||
log_set_fp(fd_log);
|
log_set_fp(fd_log);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config_file != NULL)
|
if (config_file)
|
||||||
free(config_file);
|
free(config_file);
|
||||||
|
|
||||||
if (log_file != NULL)
|
if (log_file)
|
||||||
free(log_file);
|
free(log_file);
|
||||||
|
|
||||||
int err = 0;
|
int err = 0;
|
||||||
|
@ -2567,6 +2791,7 @@ int main(int argc, char **argv)
|
||||||
uic.pool_stats = &pool_stats;
|
uic.pool_stats = &pool_stats;
|
||||||
uic.pool_fee = config.pool_fee;
|
uic.pool_fee = config.pool_fee;
|
||||||
uic.pool_port = config.pool_port;
|
uic.pool_port = config.pool_port;
|
||||||
|
uic.allow_self_select = !config.disable_self_select;
|
||||||
uic.payment_threshold = config.payment_threshold;
|
uic.payment_threshold = config.payment_threshold;
|
||||||
start_web_ui(&uic);
|
start_web_ui(&uic);
|
||||||
|
|
||||||
|
|
13
src/util.c
13
src/util.c
|
@ -57,11 +57,11 @@ is_hex_string(const char *str)
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
hex_to_bin(const char *hex, char *bin, size_t bin_size)
|
hex_to_bin(const char *hex, const size_t hex_len,
|
||||||
|
char *bin, const size_t bin_size)
|
||||||
{
|
{
|
||||||
size_t len = strlen(hex);
|
assert(hex_len % 2 == 0);
|
||||||
assert(len % 2 == 0);
|
assert(bin_size >= hex_len >> 1);
|
||||||
assert(bin_size >= len / 2);
|
|
||||||
const char *ph = hex;
|
const char *ph = hex;
|
||||||
char *end = bin + bin_size;
|
char *end = bin + bin_size;
|
||||||
while (*ph && bin < end)
|
while (*ph && bin < end)
|
||||||
|
@ -72,7 +72,8 @@ hex_to_bin(const char *hex, char *bin, size_t bin_size)
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
bin_to_hex(const char *bin, size_t bin_size, char *hex, size_t hex_size)
|
bin_to_hex(const char *bin, const size_t bin_size,
|
||||||
|
char *hex, const size_t hex_size)
|
||||||
{
|
{
|
||||||
assert(bin_size << 1 == hex_size);
|
assert(bin_size << 1 == hex_size);
|
||||||
const char *hex_chars = "0123456789abcdef";
|
const char *hex_chars = "0123456789abcdef";
|
||||||
|
@ -86,7 +87,7 @@ bin_to_hex(const char *bin, size_t bin_size, char *hex, size_t hex_size)
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
reverse_bin(char *bin, size_t len)
|
reverse_bin(char *bin, const size_t len)
|
||||||
{
|
{
|
||||||
size_t start = 0;
|
size_t start = 0;
|
||||||
size_t end = len-1;
|
size_t end = len-1;
|
||||||
|
|
|
@ -36,9 +36,11 @@ developers.
|
||||||
#define UTIL_H
|
#define UTIL_H
|
||||||
|
|
||||||
int is_hex_string(const char *str);
|
int is_hex_string(const char *str);
|
||||||
void hex_to_bin(const char *hex, char *bin, size_t bin_size);
|
void hex_to_bin(const char *hex, const size_t hex_len,
|
||||||
void bin_to_hex(const char *bin, size_t bin_size, char *hex, size_t hex_size);
|
char *bin, const size_t bin_size);
|
||||||
void reverse_bin(char *bin, size_t len);
|
void bin_to_hex(const char *bin, size_t bin_size, char *hex,
|
||||||
|
const size_t hex_size);
|
||||||
|
void reverse_bin(char *bin, const size_t len);
|
||||||
char *stecpy(char *dst, const char *src, const char *end);
|
char *stecpy(char *dst, const char *src, const char *end);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -39,6 +39,7 @@
|
||||||
<tr><td>Payment threshold: </td><td id="payment_threshold"></td></tr>
|
<tr><td>Payment threshold: </td><td id="payment_threshold"></td></tr>
|
||||||
<tr><td>Pool fee: </td><td id="pool_fee"></td></tr>
|
<tr><td>Pool fee: </td><td id="pool_fee"></td></tr>
|
||||||
<tr><td>Pool port: </td><td id="pool_port"></td></tr>
|
<tr><td>Pool port: </td><td id="pool_port"></td></tr>
|
||||||
|
<tr><td>Allow self-select: </td><td id="allow_self_select"></td></tr>
|
||||||
<tr><td>Miners connected: </td><td id="connected_miners"></td></tr>
|
<tr><td>Miners connected: </td><td id="connected_miners"></td></tr>
|
||||||
<tr class="wallet"><td>Your HR: </td><td id="miner_hashrate"></td></tr>
|
<tr class="wallet"><td>Your HR: </td><td id="miner_hashrate"></td></tr>
|
||||||
<tr class="wallet"><td>Balance due: </td><td id="miner_balance"></td></tr>
|
<tr class="wallet"><td>Balance due: </td><td id="miner_balance"></td></tr>
|
||||||
|
@ -113,6 +114,8 @@
|
||||||
el.innerHTML = format_hashrate(stats[e]);
|
el.innerHTML = format_hashrate(stats[e]);
|
||||||
else if (e == "pool_fee")
|
else if (e == "pool_fee")
|
||||||
el.innerHTML = (stats[e]*100) + "%";
|
el.innerHTML = (stats[e]*100) + "%";
|
||||||
|
else if (e == "allow_self_select")
|
||||||
|
el.innerHTML = stats[e] == 1 ? "Yes" : "No";
|
||||||
else
|
else
|
||||||
el.innerHTML = stats[e];
|
el.innerHTML = stats[e];
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,7 +55,7 @@ developers.
|
||||||
#include "webui.h"
|
#include "webui.h"
|
||||||
|
|
||||||
#define TAG_MAX 17
|
#define TAG_MAX 17
|
||||||
#define PAGE_MAX 4096
|
#define PAGE_MAX 8192
|
||||||
#define JSON_MAX 512
|
#define JSON_MAX 512
|
||||||
|
|
||||||
extern unsigned char webui_html[];
|
extern unsigned char webui_html[];
|
||||||
|
@ -76,6 +76,7 @@ send_json_stats (void *cls, struct MHD_Connection *connection)
|
||||||
uint64_t ltf = context->pool_stats->last_template_fetched;
|
uint64_t ltf = context->pool_stats->last_template_fetched;
|
||||||
uint64_t lbf = context->pool_stats->last_block_found;
|
uint64_t lbf = context->pool_stats->last_block_found;
|
||||||
uint32_t pbf = context->pool_stats->pool_blocks_found;
|
uint32_t pbf = context->pool_stats->pool_blocks_found;
|
||||||
|
unsigned ss = context->allow_self_select;
|
||||||
uint64_t mh = 0;
|
uint64_t mh = 0;
|
||||||
double mb = 0.0;
|
double mb = 0.0;
|
||||||
const char *wa = MHD_lookup_connection_value(connection,
|
const char *wa = MHD_lookup_connection_value(connection,
|
||||||
|
@ -96,12 +97,13 @@ send_json_stats (void *cls, struct MHD_Connection *connection)
|
||||||
"\"payment_threshold\":%.2f,"
|
"\"payment_threshold\":%.2f,"
|
||||||
"\"pool_fee\":%.3f,"
|
"\"pool_fee\":%.3f,"
|
||||||
"\"pool_port\":%d,"
|
"\"pool_port\":%d,"
|
||||||
|
"\"allow_self_select\":%u,"
|
||||||
"\"connected_miners\":%d,"
|
"\"connected_miners\":%d,"
|
||||||
"\"miner_hashrate\":%"PRIu64","
|
"\"miner_hashrate\":%"PRIu64","
|
||||||
"\"miner_balance\":%.8f"
|
"\"miner_balance\":%.8f"
|
||||||
"}", ph, nh, height, ltf, lbf, pbf,
|
"}", ph, nh, height, ltf, lbf, pbf,
|
||||||
context->payment_threshold, context->pool_fee,
|
context->payment_threshold, context->pool_fee,
|
||||||
context->pool_port, context->pool_stats->connected_miners,
|
context->pool_port, ss, context->pool_stats->connected_miners,
|
||||||
mh, mb);
|
mh, mb);
|
||||||
response = MHD_create_response_from_buffer(strlen(json),
|
response = MHD_create_response_from_buffer(strlen(json),
|
||||||
(void*) json, MHD_RESPMEM_MUST_COPY);
|
(void*) json, MHD_RESPMEM_MUST_COPY);
|
||||||
|
|
|
@ -54,6 +54,7 @@ typedef struct wui_context_t
|
||||||
double pool_fee;
|
double pool_fee;
|
||||||
double payment_threshold;
|
double payment_threshold;
|
||||||
uint32_t pool_port;
|
uint32_t pool_port;
|
||||||
|
unsigned allow_self_select;
|
||||||
} wui_context_t;
|
} wui_context_t;
|
||||||
|
|
||||||
int start_web_ui(wui_context_t *context);
|
int start_web_ui(wui_context_t *context);
|
||||||
|
|
85
src/xmr.cpp
85
src/xmr.cpp
|
@ -45,12 +45,16 @@ developers.
|
||||||
#include "cryptonote_basic/difficulty.h"
|
#include "cryptonote_basic/difficulty.h"
|
||||||
#include "crypto/crypto.h"
|
#include "crypto/crypto.h"
|
||||||
#include "crypto/hash.h"
|
#include "crypto/hash.h"
|
||||||
#include "common/base58.h"
|
|
||||||
#include "serialization/binary_utils.h"
|
#include "serialization/binary_utils.h"
|
||||||
|
#include "ringct/rctSigs.h"
|
||||||
|
#include "common/base58.h"
|
||||||
|
#include "string_tools.h"
|
||||||
|
|
||||||
#include "xmr.h"
|
#include "xmr.h"
|
||||||
|
|
||||||
|
using namespace epee::string_tools;
|
||||||
using namespace cryptonote;
|
using namespace cryptonote;
|
||||||
|
using namespace crypto;
|
||||||
|
|
||||||
int get_hashing_blob(const char *input, const size_t in_size,
|
int get_hashing_blob(const char *input, const size_t in_size,
|
||||||
char **output, size_t *out_size)
|
char **output, size_t *out_size)
|
||||||
|
@ -59,18 +63,17 @@ int get_hashing_blob(const char *input, const size_t in_size,
|
||||||
blobdata bd = std::string(input, in_size);
|
blobdata bd = std::string(input, in_size);
|
||||||
if (!parse_and_validate_block_from_blob(bd, b))
|
if (!parse_and_validate_block_from_blob(bd, b))
|
||||||
{
|
{
|
||||||
printf("Failed to parse block\n");
|
return XMR_PARSE_ERROR;
|
||||||
return -1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
blobdata blob = get_block_hashing_blob(b);
|
blobdata blob = get_block_hashing_blob(b);
|
||||||
*out_size = blob.length();
|
*out_size = blob.length();
|
||||||
*output = (char*) malloc(*out_size);
|
*output = (char*) malloc(*out_size);
|
||||||
memcpy(*output, blob.data(), *out_size);
|
memcpy(*output, blob.data(), *out_size);
|
||||||
return 0;
|
return XMR_NO_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
int parse_address(const char *input, uint64_t *prefix)
|
int parse_address(const char *input, uint64_t *prefix, char *pub_spend)
|
||||||
{
|
{
|
||||||
uint64_t tag;
|
uint64_t tag;
|
||||||
std::string decoded;
|
std::string decoded;
|
||||||
|
@ -78,14 +81,80 @@ int parse_address(const char *input, uint64_t *prefix)
|
||||||
if (rv)
|
if (rv)
|
||||||
{
|
{
|
||||||
*prefix = tag;
|
*prefix = tag;
|
||||||
|
if (pub_spend != NULL)
|
||||||
|
{
|
||||||
|
account_public_address address;
|
||||||
|
::serialization::parse_binary(decoded, address);
|
||||||
|
public_key S = address.m_spend_public_key;
|
||||||
|
memcpy(pub_spend, &S, 32);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return rv ? 0 : -1;
|
return rv ? XMR_NO_ERROR : XMR_PARSE_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
void get_hash(const char *input, const size_t in_size,
|
void get_hash(const char *input, const size_t in_size,
|
||||||
char **output, int variant, uint64_t height)
|
char **output, int variant, uint64_t height)
|
||||||
{
|
{
|
||||||
crypto::cn_slow_hash(input, in_size,
|
cn_slow_hash(input, in_size,
|
||||||
reinterpret_cast<crypto::hash&>(*output), variant, height);
|
reinterpret_cast<hash&>(*output), variant, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
int validate_block_from_blob(const char *blob_hex, const char *sec_view,
|
||||||
|
const char *pub_spend)
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
The only validation needed is that the data parses to a block and the
|
||||||
|
miner tx only pays out to the pool.
|
||||||
|
*/
|
||||||
|
block b = AUTO_VAL_INIT(b);
|
||||||
|
blobdata bd;
|
||||||
|
secret_key v;
|
||||||
|
public_key S;
|
||||||
|
memcpy(&unwrap(v), sec_view, 32);
|
||||||
|
memcpy(&S, pub_spend, 32);
|
||||||
|
|
||||||
|
if (!parse_hexstr_to_binbuff(blob_hex, bd))
|
||||||
|
return XMR_PARSE_ERROR;
|
||||||
|
|
||||||
|
if (!parse_and_validate_block_from_blob(bd, b))
|
||||||
|
return XMR_PARSE_ERROR;
|
||||||
|
|
||||||
|
transaction tx = b.miner_tx;
|
||||||
|
|
||||||
|
/*
|
||||||
|
Ensure we have only one in, one out and in is gen.
|
||||||
|
*/
|
||||||
|
if (tx.vin.size() != 1)
|
||||||
|
return XMR_VIN_COUNT_ERROR;
|
||||||
|
|
||||||
|
if (tx.vout.size() != 1)
|
||||||
|
return XMR_VOUT_COUNT_ERROR;
|
||||||
|
|
||||||
|
if (tx.vin[0].type() != typeid(txin_gen))
|
||||||
|
return XMR_VIN_TYPE_ERROR;
|
||||||
|
|
||||||
|
/*
|
||||||
|
Ensure that the miner tx single output key is destined for the pool
|
||||||
|
wallet.
|
||||||
|
|
||||||
|
Don't bother checking any additional pub keys in tx extra. The daemon
|
||||||
|
created miner tx only has one public key in extra. If we can't derive
|
||||||
|
from the first (which should be only) found, reject.
|
||||||
|
*/
|
||||||
|
std::vector<tx_extra_field> tx_extra_fields;
|
||||||
|
parse_tx_extra(tx.extra, tx_extra_fields);
|
||||||
|
tx_extra_pub_key pub_key_field;
|
||||||
|
if (!find_tx_extra_field_by_type(tx_extra_fields, pub_key_field, 0))
|
||||||
|
return XMR_TX_EXTRA_ERROR;
|
||||||
|
public_key R = pub_key_field.pub_key;
|
||||||
|
public_key P = boost::get<txout_to_key>(tx.vout[0].target).key;
|
||||||
|
key_derivation derivation;
|
||||||
|
generate_key_derivation(R, v, derivation);
|
||||||
|
public_key derived;
|
||||||
|
derive_subaddress_public_key(P, derivation, 0, derived);
|
||||||
|
if (derived != S)
|
||||||
|
return XMR_MISMATCH_ERROR;
|
||||||
|
|
||||||
|
return XMR_NO_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
15
src/xmr.h
15
src/xmr.h
|
@ -39,11 +39,24 @@ developers.
|
||||||
extern "C" {
|
extern "C" {
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
enum xmr_error
|
||||||
|
{
|
||||||
|
XMR_NO_ERROR = 0,
|
||||||
|
XMR_PARSE_ERROR = -1,
|
||||||
|
XMR_VIN_COUNT_ERROR = -2,
|
||||||
|
XMR_VOUT_COUNT_ERROR = -3,
|
||||||
|
XMR_VIN_TYPE_ERROR = -4,
|
||||||
|
XMR_TX_EXTRA_ERROR = -5,
|
||||||
|
XMR_MISMATCH_ERROR = -6
|
||||||
|
};
|
||||||
|
|
||||||
int get_hashing_blob(const char *input, const size_t in_size,
|
int get_hashing_blob(const char *input, const size_t in_size,
|
||||||
char **output, size_t *out_size);
|
char **output, size_t *out_size);
|
||||||
int parse_address(const char *input, uint64_t *prefix);
|
int parse_address(const char *input, uint64_t *prefix, char *pub_spend);
|
||||||
void get_hash(const char *input, const size_t in_size,
|
void get_hash(const char *input, const size_t in_size,
|
||||||
char **output, int variant, uint64_t height);
|
char **output, int variant, uint64_t height);
|
||||||
|
int validate_block_from_blob(const char *blob_hex, const char *sec_view,
|
||||||
|
const char *pub_spend);
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,174 @@
|
||||||
|
# Stratum mode self-select
|
||||||
|
|
||||||
|
One major concern of pool mining has been that of pool centralization. A
|
||||||
|
malicious pool operator, controlling a pool with a significant portion of the
|
||||||
|
total network hash-rate (e.g. 51% or more), has the ability to perform various
|
||||||
|
attacks. This is made possible due to the fact miners have no visibility into
|
||||||
|
what block template they are actually mining on. This leads to another concern -
|
||||||
|
censorship of transactions. Again, as miners have no visibility of the block
|
||||||
|
template they are mining, they also have no visibility of the transactions
|
||||||
|
included in the block template. This enables a malicious pool be selective as to
|
||||||
|
which transactions get included (or not) into a block.
|
||||||
|
|
||||||
|
To address these concerns, I've implemented a new, experimental and optional
|
||||||
|
*mode* to this pool, which enables miners to select their *own* block template
|
||||||
|
to mine on.
|
||||||
|
|
||||||
|
What follows are the instructions to test this new mode and the changes made to
|
||||||
|
the stratum messages. For a miner to test against the pool, there is a very
|
||||||
|
simple miner, [monero-powpy](https://github.com/jtgrassie/monero-powpy)
|
||||||
|
(`stratum-ss-miner.py`), and also a hastily cobbled together branch of
|
||||||
|
[XMRig](https://github.com/jtgrassie/xmrig/tree/stratum-ss).
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
First you'll need to compile the Monero master branch with pull request
|
||||||
|
[#5728](https://github.com/monero-project/monero/pull/5728). For example:
|
||||||
|
|
||||||
|
cd "$MONERO_ROOT"
|
||||||
|
git fetch origin pull/5728/head:pr-5728
|
||||||
|
git checkout -b stratum-ss
|
||||||
|
git rebase pr-5728
|
||||||
|
make release
|
||||||
|
|
||||||
|
Now proceed to building the pool as per the instructions in the
|
||||||
|
[README](./README.md#compiling-from-source).
|
||||||
|
|
||||||
|
## Running
|
||||||
|
|
||||||
|
Start your newly patched and compiled `monerod` and `monero-wallet-rpc`. For
|
||||||
|
example, in one shell:
|
||||||
|
|
||||||
|
cd "$MONERO_ROOT"/build/Linux/stratum-ss/release/bin
|
||||||
|
./monerod --testnet
|
||||||
|
|
||||||
|
And in another shell:
|
||||||
|
|
||||||
|
cd "$MONERO_ROOT"/build/Linux/stratum-ss/release/bin
|
||||||
|
./monero-wallet-rpc --testnet --rpc-bind-port 28084 --disable-rpc-login \
|
||||||
|
--password "" --wallet-file ~/testnet-pool-wallet
|
||||||
|
|
||||||
|
Next, in a third shell, run `monero-pool`. Instructions per the
|
||||||
|
[README](./README.md#running).
|
||||||
|
|
||||||
|
Lastly you'll need to run a miner that supports this new stratum mode (see
|
||||||
|
above). If using [monero-powpy](https://github.com/jtgrassie/monero-powpy), just
|
||||||
|
run `stratum-ss-miner.py`, optionally editing the parameters first. If using
|
||||||
|
[XMRig](https://github.com/jtgrassie/xmrig/tree/stratum-ss), edit your
|
||||||
|
`config.json` file by setting the parameter `self-select` in your `pools` array
|
||||||
|
(e.g. `"self-select": "localhost:28081"`).
|
||||||
|
|
||||||
|
## Specification
|
||||||
|
|
||||||
|
What follows are the stratum message and flow changes required to enable pool
|
||||||
|
miners to mine on miner created block templates.
|
||||||
|
|
||||||
|
(1) The miner logs into the pool with an additional *mode* parameter:
|
||||||
|
|
||||||
|
{
|
||||||
|
"method": "login",
|
||||||
|
"params": {
|
||||||
|
"login": "wallet address",
|
||||||
|
"pass": "password",
|
||||||
|
"agent": "user-agent/0.1",
|
||||||
|
"mode": "self-select" /* new field */
|
||||||
|
},
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id":1
|
||||||
|
}
|
||||||
|
|
||||||
|
(2) The pool responds with a result job which includes the pool wallet address
|
||||||
|
and an extra nonce:
|
||||||
|
|
||||||
|
{
|
||||||
|
"result": {
|
||||||
|
"job": {
|
||||||
|
"pool_wallet": "pool wallet address", /* new field */
|
||||||
|
"extra_nonce": "extra nonce hex", /* new field */
|
||||||
|
"target": "target hex",
|
||||||
|
"job_id": "job id"
|
||||||
|
},
|
||||||
|
"id": "client id",
|
||||||
|
"status": "OK"
|
||||||
|
},
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id":1
|
||||||
|
}
|
||||||
|
|
||||||
|
(3) The miner can now call the daemons RPC method
|
||||||
|
[get_block_template](https://ww.getmonero.org/resources/developer-guides/daemon-rpc.html#get_block_template)
|
||||||
|
with parameters `extra_nonce: extra_nonce` (implemented in pull request
|
||||||
|
[#5728](https://github.com/monero-project/monero/pull/5728)), and
|
||||||
|
`wallet_address: pool_wallet`.
|
||||||
|
|
||||||
|
The miner now informs the pool of the resulting block template it will use for
|
||||||
|
the job:
|
||||||
|
|
||||||
|
{
|
||||||
|
"method":"block_template", /* new method */
|
||||||
|
"params": {
|
||||||
|
"id": "client id",
|
||||||
|
"job_id": "job id",
|
||||||
|
"blob": "block template hex",
|
||||||
|
"height": N,
|
||||||
|
"difficulty": N,
|
||||||
|
"prev_hash": "prev hash hex"
|
||||||
|
},
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id":1
|
||||||
|
}
|
||||||
|
|
||||||
|
(4) The pool validates and caches the supplied block template and responds with
|
||||||
|
a status:
|
||||||
|
|
||||||
|
{
|
||||||
|
"result": {
|
||||||
|
"status": "OK",
|
||||||
|
"error", null
|
||||||
|
},
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id":1
|
||||||
|
}
|
||||||
|
|
||||||
|
(5) The miner submits results. No changes here:
|
||||||
|
|
||||||
|
{
|
||||||
|
"method":"submit",
|
||||||
|
"params": {
|
||||||
|
"id": "client id",
|
||||||
|
"job_id": "job id",
|
||||||
|
"nonce": "deadbeef",
|
||||||
|
"result": "hash hex"
|
||||||
|
},
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id":1
|
||||||
|
}
|
||||||
|
|
||||||
|
(6) The pool responds to job submissions. No changes here:
|
||||||
|
|
||||||
|
{
|
||||||
|
"result": {
|
||||||
|
"status": "OK",
|
||||||
|
"error", null
|
||||||
|
},
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id":1
|
||||||
|
}
|
||||||
|
|
||||||
|
(7) The pool asks the miner to start a new job:
|
||||||
|
|
||||||
|
{
|
||||||
|
"method": "job",
|
||||||
|
"params": {
|
||||||
|
"pool_wallet": "pool wallet address", /* new field */
|
||||||
|
"extra_nonce": "extra nonce hex", /* new field */
|
||||||
|
"target": "target hex",
|
||||||
|
"job_id": "job id"
|
||||||
|
},
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id":1
|
||||||
|
}
|
||||||
|
|
||||||
|
The miner now repeats from step #3.
|
||||||
|
|
||||||
|
[//]: # ( vim: set tw=80: )
|
Loading…
Reference in New Issue