REDACTED-rig/src/base/net/stratum/DaemonClient.cpp
2021-06-17 22:48:08 +02:00

528 lines
15 KiB
C++

/* XMRig
* Copyright 2010 Jeff Garzik <jgarzik@pobox.com>
* Copyright 2012-2014 pooler <pooler@litecoinpool.org>
* Copyright 2014 Lucas Jones <https://github.com/lucasjones>
* Copyright 2014-2016 Wolf9466 <https://github.com/OhGodAPet>
* Copyright 2016 Jay D Dee <jayddee246@gmail.com>
* Copyright 2017-2018 XMR-Stak <https://github.com/fireice-uk>, <https://github.com/psychocrypt>
* Copyright 2019 Howard Chu <https://github.com/hyc>
* Copyright 2018-2020 SChernykh <https://github.com/SChernykh>
* Copyright 2016-2020 XMRig <https://github.com/xmrig>, <support@xmrig.com>
*
* 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 3 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, see <http://www.gnu.org/licenses/>.
*/
#include "base/net/stratum/DaemonClient.h"
#include "3rdparty/rapidjson/document.h"
#include "3rdparty/rapidjson/error/en.h"
#include "base/io/json/Json.h"
#include "base/io/json/JsonRequest.h"
#include "base/io/log/Log.h"
#include "base/kernel/interfaces/IClientListener.h"
#include "base/net/http/Fetch.h"
#include "base/net/http/HttpData.h"
#include "base/net/http/HttpListener.h"
#include "base/net/stratum/SubmitResult.h"
#include "base/tools/Cvt.h"
#include "base/tools/Timer.h"
#include "base/tools/cryptonote/Signatures.h"
#include "base/tools/cryptonote/WalletAddress.h"
#include "net/JobResult.h"
#include <algorithm>
#include <cassert>
namespace xmrig {
static const char *kBlocktemplateBlob = "blocktemplate_blob";
static const char *kGetHeight = "/getheight";
static const char *kGetInfo = "/getinfo";
static const char *kHash = "hash";
static const char *kHeight = "height";
static const char *kJsonRPC = "/json_rpc";
static constexpr size_t kBlobReserveSize = 8;
}
xmrig::DaemonClient::DaemonClient(int id, IClientListener *listener) :
BaseClient(id, listener)
{
m_httpListener = std::make_shared<HttpListener>(this);
m_timer = new Timer(this);
}
xmrig::DaemonClient::~DaemonClient()
{
delete m_timer;
}
bool xmrig::DaemonClient::disconnect()
{
if (m_state != UnconnectedState) {
setState(UnconnectedState);
}
return true;
}
bool xmrig::DaemonClient::isTLS() const
{
# ifdef XMRIG_FEATURE_TLS
return m_pool.isTLS();
# else
return false;
# endif
}
int64_t xmrig::DaemonClient::submit(const JobResult &result)
{
if (result.jobId != (m_blocktemplateStr.data() + m_blocktemplateStr.size() - 32)) {
return -1;
}
char *data = (m_apiVersion == API_DERO) ? m_blockhashingblob.data() : m_blocktemplateStr.data();
const size_t sig_offset = m_job.nonceOffset() + m_job.nonceSize();
# ifdef XMRIG_PROXY_PROJECT
memcpy(data + m_job.nonceOffset() * 2, result.nonce, 8);
if (m_blocktemplate.has_miner_signature && result.sig) {
memcpy(data + sig_offset * 2, result.sig, 64 * 2);
memcpy(data + m_blocktemplate.tx_pubkey_index * 2, result.sig_data, 32 * 2);
memcpy(data + m_blocktemplate.eph_public_key_index * 2, result.sig_data + 32 * 2, 32 * 2);
}
# else
Cvt::toHex(data + m_job.nonceOffset() * 2, 8, reinterpret_cast<const uint8_t*>(&result.nonce), 4);
if (m_blocktemplate.has_miner_signature) {
Cvt::toHex(data + sig_offset * 2, 128, result.minerSignature(), 64);
}
# endif
using namespace rapidjson;
Document doc(kObjectType);
Value params(kArrayType);
if (m_apiVersion == API_DERO) {
params.PushBack(m_blocktemplateStr.toJSON(), doc.GetAllocator());
params.PushBack(m_blockhashingblob.toJSON(), doc.GetAllocator());
}
else {
params.PushBack(m_blocktemplateStr.toJSON(), doc.GetAllocator());
}
JsonRequest::create(doc, m_sequence, "submitblock", params);
# ifdef XMRIG_PROXY_PROJECT
m_results[m_sequence] = SubmitResult(m_sequence, result.diff, result.actualDiff(), result.id, 0);
# else
m_results[m_sequence] = SubmitResult(m_sequence, result.diff, result.actualDiff(), 0, result.backend);
# endif
return rpcSend(doc);
}
void xmrig::DaemonClient::connect()
{
if ((m_pool.algorithm() == Algorithm::ASTROBWT_DERO) || (m_pool.coin() == Coin::DERO)) {
m_apiVersion = API_DERO;
}
setState(ConnectingState);
getBlockTemplate();
}
void xmrig::DaemonClient::connect(const Pool &pool)
{
setPool(pool);
connect();
}
void xmrig::DaemonClient::onHttpData(const HttpData &data)
{
if (data.status != 200) {
return retry();
}
m_ip = data.ip().c_str();
# ifdef XMRIG_FEATURE_TLS
m_tlsVersion = data.tlsVersion();
m_tlsFingerprint = data.tlsFingerprint();
# endif
rapidjson::Document doc;
if (doc.Parse(data.body.c_str()).HasParseError()) {
if (!isQuiet()) {
LOG_ERR("[%s:%d] JSON decode failed: \"%s\"", m_pool.host().data(), m_pool.port(), rapidjson::GetParseError_En(doc.GetParseError()));
}
return retry();
}
if (data.method == HTTP_GET) {
if (data.url == kGetHeight) {
if (!doc.HasMember(kHash)) {
m_apiVersion = API_CRYPTONOTE_DEFAULT;
return send(kGetInfo);
}
const uint64_t height = Json::getUint64(doc, kHeight);
const String hash = Json::getString(doc, kHash);
if (isOutdated(height, hash)) {
// Multiple /getheight responses can come at once resulting in multiple getBlockTemplate() calls
if ((height != m_blocktemplateRequestHeight) || (hash != m_blocktemplateRequestHash)) {
m_blocktemplateRequestHeight = height;
m_blocktemplateRequestHash = hash;
getBlockTemplate();
}
}
}
else if (data.url == kGetInfo) {
const uint64_t height = Json::getUint64(doc, kHeight);
const String hash = Json::getString(doc, "top_block_hash");
if (isOutdated(height, hash)) {
// Multiple /getinfo responses can come at once resulting in multiple getBlockTemplate() calls
if ((height != m_blocktemplateRequestHeight) || (hash != m_blocktemplateRequestHash)) {
m_blocktemplateRequestHeight = height;
m_blocktemplateRequestHash = hash;
getBlockTemplate();
}
}
}
return;
}
if (!parseResponse(Json::getInt64(doc, "id", -1), Json::getObject(doc, "result"), Json::getObject(doc, "error"))) {
retry();
}
}
void xmrig::DaemonClient::onTimer(const Timer *)
{
if (m_state == ConnectingState) {
getBlockTemplate();
}
else if (m_state == ConnectedState) {
if (m_apiVersion == API_DERO) {
rpcSend(JsonRequest::create(m_sequence, "get_info"));
}
else {
send((m_apiVersion == API_MONERO) ? kGetHeight : kGetInfo);
}
}
}
bool xmrig::DaemonClient::isOutdated(uint64_t height, const char *hash) const
{
return m_job.height() != height || m_prevHash != hash;
}
bool xmrig::DaemonClient::parseJob(const rapidjson::Value &params, int *code)
{
Job job(false, m_pool.algorithm(), String());
String blocktemplate = Json::getString(params, kBlocktemplateBlob);
if (blocktemplate.isNull()) {
LOG_ERR("Empty block template received from daemon");
*code = 1;
return false;
}
Coin pool_coin = m_pool.coin();
if (!pool_coin.isValid() && (m_pool.algorithm() == Algorithm::RX_WOW)) {
pool_coin = Coin::WOWNERO;
}
if (!m_blocktemplate.Init(blocktemplate, pool_coin)) {
LOG_ERR("Invalid block template received from daemon");
*code = 2;
return false;
}
m_blockhashingblob = Json::getString(params, "blockhashing_blob");
if (m_blocktemplate.has_miner_signature) {
if (m_pool.spendSecretKey().isEmpty()) {
LOG_ERR("Secret spend key is not set");
*code = 4;
return false;
}
if (m_pool.spendSecretKey().size() != 64) {
LOG_ERR("Secret spend key has invalid length. It must be 64 hex characters.");
*code = 5;
return false;
}
uint8_t secret_spendkey[32];
if (!Cvt::fromHex(secret_spendkey, 32, m_pool.spendSecretKey(), 64)) {
LOG_ERR("Secret spend key is not a valid hex data.");
*code = 6;
return false;
}
uint8_t public_spendkey[32];
if (!secret_key_to_public_key(secret_spendkey, public_spendkey)) {
LOG_ERR("Secret spend key is invalid.");
*code = 7;
return false;
}
# ifdef XMRIG_PROXY_PROJECT
job.setSpendSecretKey(secret_spendkey);
job.setMinerTx(
m_blocktemplate.raw_blob.data() + m_blocktemplate.miner_tx_prefix_begin_index,
m_blocktemplate.raw_blob.data() + m_blocktemplate.miner_tx_prefix_end_index,
m_blocktemplate.eph_public_key_index - m_blocktemplate.miner_tx_prefix_begin_index,
m_blocktemplate.tx_pubkey_index - m_blocktemplate.miner_tx_prefix_begin_index,
m_blocktemplate.miner_tx_merkle_tree_branch
);
# else
uint8_t secret_viewkey[32];
derive_view_secret_key(secret_spendkey, secret_viewkey);
uint8_t public_viewkey[32];
if (!secret_key_to_public_key(secret_viewkey, public_viewkey)) {
LOG_ERR("Secret view key is invalid.");
*code = 8;
return false;
}
uint8_t derivation[32];
if (!generate_key_derivation(m_blocktemplate.raw_blob.data() + m_blocktemplate.tx_pubkey_index, secret_viewkey, derivation)) {
LOG_ERR("Failed to generate key derivation for miner signature.");
*code = 9;
return false;
}
WalletAddress user_address;
if (!user_address.Decode(m_pool.user())) {
LOG_ERR("Invalid wallet address.");
*code = 10;
return false;
}
if (memcmp(user_address.public_spend_key, public_spendkey, sizeof(public_spendkey)) != 0) {
LOG_ERR("Wallet address and spend key don't match.");
*code = 11;
return false;
}
if (memcmp(user_address.public_view_key, public_viewkey, sizeof(public_viewkey)) != 0) {
LOG_ERR("Wallet address and view key don't match.");
*code = 12;
return false;
}
uint8_t eph_secret_key[32];
derive_secret_key(derivation, 0, secret_spendkey, eph_secret_key);
job.setEphemeralKeys(m_blocktemplate.raw_blob.data() + m_blocktemplate.eph_public_key_index, eph_secret_key);
job.setTimestamp(m_blocktemplate.timestamp);
# endif
}
if (m_apiVersion == API_DERO) {
const uint64_t offset = Json::getUint64(params, "reserved_offset");
Cvt::toHex(m_blockhashingblob.data() + offset * 2, kBlobReserveSize * 2, Cvt::randomBytes(kBlobReserveSize).data(), kBlobReserveSize);
}
if (pool_coin.isValid()) {
job.setAlgorithm(pool_coin.algorithm(m_blocktemplate.major_version));
}
if (!job.setBlob(m_blockhashingblob)) {
*code = 3;
return false;
}
job.setSeedHash(Json::getString(params, "seed_hash"));
job.setHeight(Json::getUint64(params, kHeight));
job.setDiff(Json::getUint64(params, "difficulty"));
job.setId(blocktemplate.data() + blocktemplate.size() - 32);
m_job = std::move(job);
m_blocktemplateStr = std::move(blocktemplate);
m_prevHash = Json::getString(params, "prev_hash");
if (m_apiVersion == API_DERO) {
// Truncate to 32 bytes to have the same data as in get_info RPC
if (m_prevHash.size() > 64) {
m_prevHash.data()[64] = '\0';
}
}
if (m_state == ConnectingState) {
setState(ConnectedState);
}
m_listener->onJobReceived(this, m_job, params);
return true;
}
bool xmrig::DaemonClient::parseResponse(int64_t id, const rapidjson::Value &result, const rapidjson::Value &error)
{
if (id == -1) {
return false;
}
if (error.IsObject()) {
const char *message = error["message"].GetString();
if (!handleSubmitResponse(id, message) && !isQuiet()) {
LOG_ERR("[%s:%d] error: " RED_BOLD("\"%s\"") RED_S ", code: %d", m_pool.host().data(), m_pool.port(), message, error["code"].GetInt());
}
return false;
}
if (!result.IsObject()) {
return false;
}
if (result.HasMember("top_block_hash")) {
if (m_prevHash != Json::getString(result, "top_block_hash")) {
getBlockTemplate();
}
return true;
}
int code = -1;
if (result.HasMember(kBlocktemplateBlob) && parseJob(result, &code)) {
return true;
}
if (handleSubmitResponse(id)) {
getBlockTemplate();
return true;
}
return false;
}
int64_t xmrig::DaemonClient::getBlockTemplate()
{
using namespace rapidjson;
Document doc(kObjectType);
auto &allocator = doc.GetAllocator();
Value params(kObjectType);
params.AddMember("wallet_address", m_user.toJSON(), allocator);
if (m_apiVersion == API_DERO) {
params.AddMember("reserve_size", static_cast<uint64_t>(kBlobReserveSize), allocator);
}
else {
params.AddMember("extra_nonce", Cvt::toHex(Cvt::randomBytes(kBlobReserveSize)).toJSON(doc), allocator);
}
JsonRequest::create(doc, m_sequence, "getblocktemplate", params);
return rpcSend(doc);
}
int64_t xmrig::DaemonClient::rpcSend(const rapidjson::Document &doc)
{
FetchRequest req(HTTP_POST, m_pool.host(), m_pool.port(), kJsonRPC, doc, m_pool.isTLS(), isQuiet());
fetch(tag(), std::move(req), m_httpListener);
return m_sequence++;
}
void xmrig::DaemonClient::retry()
{
m_failures++;
m_listener->onClose(this, static_cast<int>(m_failures));
if (m_failures == -1) {
return;
}
if (m_state == ConnectedState) {
setState(ConnectingState);
}
m_timer->stop();
m_timer->start(m_retryPause, 0);
}
void xmrig::DaemonClient::send(const char *path)
{
FetchRequest req(HTTP_GET, m_pool.host(), m_pool.port(), path, m_pool.isTLS(), isQuiet());
fetch(tag(), std::move(req), m_httpListener);
}
void xmrig::DaemonClient::setState(SocketState state)
{
assert(m_state != state);
if (m_state == state) {
return;
}
m_state = state;
switch (state) {
case ConnectedState:
{
m_failures = 0;
m_listener->onLoginSuccess(this);
const uint64_t interval = std::max<uint64_t>(20, m_pool.pollInterval());
m_timer->start(interval, interval);
}
break;
case UnconnectedState:
m_failures = -1;
m_timer->stop();
break;
default:
break;
}
}