HAC changes
This commit is contained in:
854
src/base/net/stratum/HushClient.cpp
Normal file
854
src/base/net/stratum/HushClient.cpp
Normal file
@@ -0,0 +1,854 @@
|
||||
/* XMRig - Hush/DragonX HAC Support
|
||||
* Copyright (c) 2024 XMRig <https://github.com/xmrig>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#include "base/net/stratum/HushClient.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/tools/Chrono.h"
|
||||
#include "base/tools/Cvt.h"
|
||||
#include "base/tools/Timer.h"
|
||||
#include "net/JobResult.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <openssl/sha.h>
|
||||
|
||||
namespace xmrig {
|
||||
|
||||
static const char *kJsonRPC = "/";
|
||||
|
||||
// Simple Base64 encoder for HTTP Basic Auth
|
||||
static std::string toBase64(const std::string &input)
|
||||
{
|
||||
static const char table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
std::string encoded;
|
||||
encoded.reserve(((input.size() + 2) / 3) * 4);
|
||||
|
||||
unsigned int val = 0;
|
||||
int bits = -6;
|
||||
for (unsigned char c : input) {
|
||||
val = (val << 8) + c;
|
||||
bits += 8;
|
||||
while (bits >= 0) {
|
||||
encoded.push_back(table[(val >> bits) & 0x3F]);
|
||||
bits -= 6;
|
||||
}
|
||||
}
|
||||
if (bits > -6) {
|
||||
encoded.push_back(table[((val << 8) >> (bits + 8)) & 0x3F]);
|
||||
}
|
||||
while (encoded.size() % 4) {
|
||||
encoded.push_back('=');
|
||||
}
|
||||
return encoded;
|
||||
}
|
||||
|
||||
// Create JSON-RPC 1.0 request (HAC daemons use 1.0, not 2.0)
|
||||
static void createRpc10(rapidjson::Document &doc, int64_t id, const char *method, rapidjson::Value ¶ms)
|
||||
{
|
||||
using namespace rapidjson;
|
||||
auto &allocator = doc.GetAllocator();
|
||||
|
||||
doc.AddMember("jsonrpc", "1.0", allocator);
|
||||
doc.AddMember("id", id, allocator);
|
||||
doc.AddMember("method", StringRef(method), allocator);
|
||||
doc.AddMember("params", params, allocator);
|
||||
}
|
||||
|
||||
HushClient::HushClient(int id, IClientListener *listener) :
|
||||
BaseClient(id, listener)
|
||||
{
|
||||
m_httpListener = std::make_shared<HttpListener>(this);
|
||||
m_timer = new Timer(this);
|
||||
}
|
||||
|
||||
HushClient::~HushClient()
|
||||
{
|
||||
delete m_timer;
|
||||
}
|
||||
|
||||
void HushClient::deleteLater()
|
||||
{
|
||||
delete this;
|
||||
}
|
||||
|
||||
bool HushClient::disconnect()
|
||||
{
|
||||
if (m_state != UnconnectedState) {
|
||||
setState(UnconnectedState);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool HushClient::isTLS() const
|
||||
{
|
||||
#ifdef XMRIG_FEATURE_TLS
|
||||
return m_pool.isTLS();
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void HushClient::connect()
|
||||
{
|
||||
if (m_pool.algorithm().family() != Algorithm::RANDOM_X) {
|
||||
LOG_ERR("%s " RED("HAC coins require RandomX algorithm"), tag());
|
||||
return;
|
||||
}
|
||||
|
||||
setState(ConnectingState);
|
||||
getBlockTemplate();
|
||||
}
|
||||
|
||||
void HushClient::connect(const Pool &pool)
|
||||
{
|
||||
setPool(pool);
|
||||
connect();
|
||||
}
|
||||
|
||||
void HushClient::setPool(const Pool &pool)
|
||||
{
|
||||
BaseClient::setPool(pool);
|
||||
|
||||
// Default to RandomX if not specified
|
||||
if (!m_pool.algorithm().isValid()) {
|
||||
m_pool.setAlgo(Algorithm::RX_0);
|
||||
}
|
||||
}
|
||||
|
||||
int64_t HushClient::submit(const JobResult &result)
|
||||
{
|
||||
if (result.jobId != m_currentJobId) {
|
||||
LOG_DEBUG("%s " RED("job ID mismatch"), tag());
|
||||
return -1;
|
||||
}
|
||||
|
||||
// HAC PoW: RandomX hash becomes nSolution, then SHA256D(header) must be < target
|
||||
// Check if SHA256D(header + solution) meets network target before submitting
|
||||
if (!checkPow(result.nonce, result.result())) {
|
||||
// Hash doesn't meet network target, don't submit
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Log submission details
|
||||
const String solutionHex = Cvt::toHex(result.result(), 32);
|
||||
LOG_INFO("%s " CYAN_BOLD("submitting block") " nonce=0x%08x", tag(), result.nonce);
|
||||
|
||||
// Build the full block with header + solution + transactions
|
||||
const std::string blockHex = serializeBlockHex(result.nonce, result.result());
|
||||
|
||||
return submitBlock(blockHex);
|
||||
}
|
||||
|
||||
void HushClient::onTimer(const Timer *)
|
||||
{
|
||||
if (m_state == ConnectingState) {
|
||||
connect();
|
||||
}
|
||||
else if (m_state == ConnectedState) {
|
||||
// Poll for new block template
|
||||
getBlockTemplate();
|
||||
}
|
||||
}
|
||||
|
||||
void HushClient::onHttpData(const HttpData &data)
|
||||
{
|
||||
if (data.status != 200) {
|
||||
LOG_ERR("%s " RED("HTTP error %d"), tag(), data.status);
|
||||
return retry();
|
||||
}
|
||||
|
||||
m_ip = data.ip().c_str();
|
||||
|
||||
#ifdef XMRIG_FEATURE_TLS
|
||||
if (data.tlsVersion()) {
|
||||
strncpy(m_tlsVersion, data.tlsVersion(), sizeof(m_tlsVersion) - 1);
|
||||
}
|
||||
if (data.tlsFingerprint()) {
|
||||
strncpy(m_tlsFingerprint, data.tlsFingerprint(), sizeof(m_tlsFingerprint) - 1);
|
||||
}
|
||||
#endif
|
||||
|
||||
rapidjson::Document doc;
|
||||
if (doc.Parse(data.body.c_str()).HasParseError()) {
|
||||
LOG_ERR("%s " RED("JSON parse error: %s"), tag(), rapidjson::GetParseError_En(doc.GetParseError()));
|
||||
return retry();
|
||||
}
|
||||
|
||||
// Debug: log the raw response for troubleshooting
|
||||
LOG_DEBUG("%s RPC response: %.200s", tag(), data.body.c_str());
|
||||
|
||||
const int64_t id = Json::getInt64(doc, "id", -1);
|
||||
|
||||
// Handle "result" field which can be object, string, or null depending on RPC method
|
||||
const rapidjson::Value *resultPtr = nullptr;
|
||||
if (doc.HasMember("result")) {
|
||||
resultPtr = &doc["result"];
|
||||
}
|
||||
static const rapidjson::Value nullValue;
|
||||
const auto &result = resultPtr ? *resultPtr : nullValue;
|
||||
const auto &error = Json::getObject(doc, "error");
|
||||
|
||||
if (!parseResponse(id, result, error)) {
|
||||
retry();
|
||||
}
|
||||
}
|
||||
|
||||
int64_t HushClient::getBlockTemplate()
|
||||
{
|
||||
using namespace rapidjson;
|
||||
Document doc(kObjectType);
|
||||
auto &allocator = doc.GetAllocator();
|
||||
|
||||
// JSON-RPC 1.0 style: params is an array with optional object
|
||||
Value params(kArrayType);
|
||||
// Empty params array for basic getblocktemplate
|
||||
// HAC will return coinbasetxn by default
|
||||
|
||||
createRpc10(doc, m_sequence, "getblocktemplate", params);
|
||||
m_pendingRequest = REQ_TEMPLATE;
|
||||
|
||||
return rpcSend(doc);
|
||||
}
|
||||
|
||||
int64_t HushClient::getBlockHash(uint64_t height)
|
||||
{
|
||||
using namespace rapidjson;
|
||||
Document doc(kObjectType);
|
||||
auto &allocator = doc.GetAllocator();
|
||||
|
||||
Value params(kArrayType);
|
||||
params.PushBack(Value(height), allocator);
|
||||
|
||||
createRpc10(doc, m_sequence, "getblockhash", params);
|
||||
m_pendingRequest = REQ_KEYHASH;
|
||||
m_pendingKeyHeight = height;
|
||||
|
||||
return rpcSend(doc);
|
||||
}
|
||||
|
||||
int64_t HushClient::submitBlock(const std::string &blockHex)
|
||||
{
|
||||
using namespace rapidjson;
|
||||
Document doc(kObjectType);
|
||||
auto &allocator = doc.GetAllocator();
|
||||
|
||||
Value params(kArrayType);
|
||||
params.PushBack(Value(blockHex.c_str(), allocator), allocator);
|
||||
|
||||
createRpc10(doc, m_sequence, "submitblock", params);
|
||||
m_pendingRequest = REQ_SUBMIT;
|
||||
|
||||
LOG_INFO("%s " MAGENTA_BOLD("submitting block at height %u"), tag(), m_height);
|
||||
|
||||
return rpcSend(doc);
|
||||
}
|
||||
|
||||
int64_t HushClient::rpcSend(const rapidjson::Document &doc)
|
||||
{
|
||||
FetchRequest req(HTTP_POST, m_pool.host(), m_pool.port(), kJsonRPC, doc, m_pool.isTLS(), isQuiet());
|
||||
|
||||
// HAC daemons require text/plain content-type (override application/json)
|
||||
req.headers.erase("Content-Type");
|
||||
req.headers.insert({"Content-Type", "text/plain"});
|
||||
|
||||
// Add RPC authentication if configured
|
||||
if (!m_pool.user().isEmpty()) {
|
||||
std::string auth = m_pool.user().data();
|
||||
if (!m_pool.password().isEmpty()) {
|
||||
auth += ":";
|
||||
auth += m_pool.password().data();
|
||||
}
|
||||
const std::string encoded = toBase64(auth);
|
||||
req.headers.insert({"Authorization", "Basic " + encoded});
|
||||
}
|
||||
|
||||
fetch(tag(), std::move(req), m_httpListener);
|
||||
|
||||
return m_sequence++;
|
||||
}
|
||||
|
||||
bool HushClient::parseBlockTemplate(const rapidjson::Value &result)
|
||||
{
|
||||
if (!result.IsObject()) {
|
||||
LOG_ERR("%s " RED("invalid block template response"), tag());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if this is the same job (same previous block hash)
|
||||
const char *prevHash = Json::getString(result, "previousblockhash");
|
||||
if (prevHash && m_prevJobHash == prevHash && m_state == ConnectedState) {
|
||||
// Same block, skip duplicate job
|
||||
return true;
|
||||
}
|
||||
|
||||
// Parse BIP22 block template fields
|
||||
m_height = Json::getUint64(result, "height");
|
||||
m_curtime = Json::getUint64(result, "curtime");
|
||||
m_version = Json::getInt(result, "version", 4);
|
||||
m_prevHash = Json::getString(result, "previousblockhash");
|
||||
m_saplingRoot = Json::getString(result, "finalsaplingroothash");
|
||||
m_target = Json::getString(result, "target");
|
||||
m_bits = Json::getString(result, "bits");
|
||||
|
||||
// Parse target into bytes for PoW comparison (32 bytes, big-endian from hex)
|
||||
m_targetBytes.resize(32);
|
||||
if (m_target.size() == 64) {
|
||||
Cvt::fromHex(m_targetBytes.data(), 32, m_target.data(), 64);
|
||||
}
|
||||
|
||||
// Coinbase transaction
|
||||
const auto &coinbase = result["coinbasetxn"];
|
||||
if (!coinbase.IsObject()) {
|
||||
LOG_ERR("%s " RED("missing coinbasetxn in template"), tag());
|
||||
return false;
|
||||
}
|
||||
m_coinbaseTx = Json::getString(coinbase, "data");
|
||||
m_merkleRoot = Json::getString(coinbase, "hash");
|
||||
|
||||
LOG_DEBUG("%s template: height=%lu bits=%s target=%s", tag(), m_height, m_bits.data(), m_target.data());
|
||||
LOG_DEBUG("%s prevHash=%s", tag(), m_prevHash.data());
|
||||
LOG_DEBUG("%s merkleRoot=%s", tag(), m_merkleRoot.data());
|
||||
|
||||
// Other transactions (may be empty for low traffic chains)
|
||||
m_transactions.clear();
|
||||
const auto &txs = result["transactions"];
|
||||
if (txs.IsArray()) {
|
||||
for (const auto &tx : txs.GetArray()) {
|
||||
if (tx.IsObject()) {
|
||||
m_transactions.push_back(Json::getString(tx, "data"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if (m_height == 0 || m_prevHash.isEmpty() || m_coinbaseTx.isEmpty()) {
|
||||
LOG_ERR("%s " RED("incomplete block template"), tag());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if we need to fetch new RandomX key
|
||||
const uint64_t keyHeight = getKeyHeight(m_height);
|
||||
if (keyHeight != m_keyHeight || m_keyBlockHash.isEmpty()) {
|
||||
if (keyHeight == 0) {
|
||||
// Use genesis key - chain-specific initial seed
|
||||
// For HACs this is typically: hash(magic + symbol + rpcport)
|
||||
m_keyBlockHash = "0000000000000000000000000000000000000000000000000000000000000000";
|
||||
m_keyHeight = 0;
|
||||
LOG_INFO("%s " CYAN("using genesis RandomX key for height %u"), tag(), m_height);
|
||||
} else {
|
||||
LOG_INFO("%s " CYAN("fetching RandomX key block hash at height %u"), tag(), keyHeight);
|
||||
getBlockHash(keyHeight);
|
||||
return true; // Will continue after receiving key hash
|
||||
}
|
||||
}
|
||||
|
||||
// Create the mining job
|
||||
Job job(false, m_pool.algorithm(), String());
|
||||
job.setHeight(m_height);
|
||||
|
||||
// Set diff=1 so ALL RandomX results get submitted to us for PoW checking
|
||||
// We filter in submit() by checking SHA256D(header+solution) < target
|
||||
job.setDiff(1);
|
||||
|
||||
if (!job.setSeedHash(m_keyBlockHash.data())) {
|
||||
LOG_ERR("%s " RED("failed to set seed hash: %s (len=%zu)"), tag(), m_keyBlockHash.data(), m_keyBlockHash.size());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Build full block blob for RandomX hashing:
|
||||
// HAC computes RandomX(full_serialized_block) where block = header + nSolution(empty) + txcount + txs
|
||||
// Header: version(4) + prevHash(32) + merkleRoot(32) + saplingRoot(32) + time(4) + bits(4) + nonce(32) + nSolution(varint_len + data)
|
||||
m_headerBlob.clear();
|
||||
m_headerBlob.reserve(1024); // Will grow if many transactions
|
||||
|
||||
// nVersion (4 bytes, little-endian)
|
||||
const uint32_t ver = static_cast<uint32_t>(m_version);
|
||||
m_headerBlob.insert(m_headerBlob.end(), reinterpret_cast<const uint8_t*>(&ver),
|
||||
reinterpret_cast<const uint8_t*>(&ver) + 4);
|
||||
|
||||
// hashPrevBlock (32 bytes, internal byte order)
|
||||
std::vector<uint8_t> prevHashBytes(32);
|
||||
Cvt::fromHex(prevHashBytes.data(), 32, m_prevHash.data(), 64);
|
||||
std::reverse(prevHashBytes.begin(), prevHashBytes.end());
|
||||
m_headerBlob.insert(m_headerBlob.end(), prevHashBytes.begin(), prevHashBytes.end());
|
||||
|
||||
// hashMerkleRoot (32 bytes)
|
||||
std::vector<uint8_t> merkleRoot(32);
|
||||
Cvt::fromHex(merkleRoot.data(), 32, m_merkleRoot.data(), 64);
|
||||
std::reverse(merkleRoot.begin(), merkleRoot.end());
|
||||
m_headerBlob.insert(m_headerBlob.end(), merkleRoot.begin(), merkleRoot.end());
|
||||
|
||||
// hashFinalSaplingRoot (32 bytes)
|
||||
std::vector<uint8_t> saplingRoot(32);
|
||||
Cvt::fromHex(saplingRoot.data(), 32, m_saplingRoot.data(), 64);
|
||||
std::reverse(saplingRoot.begin(), saplingRoot.end());
|
||||
m_headerBlob.insert(m_headerBlob.end(), saplingRoot.begin(), saplingRoot.end());
|
||||
|
||||
// nTime (4 bytes)
|
||||
const uint32_t time32 = static_cast<uint32_t>(m_curtime);
|
||||
m_headerBlob.insert(m_headerBlob.end(), reinterpret_cast<const uint8_t*>(&time32),
|
||||
reinterpret_cast<const uint8_t*>(&time32) + 4);
|
||||
|
||||
// nBits (4 bytes)
|
||||
uint32_t bits = 0;
|
||||
for (int i = 0; i < 8 && i < static_cast<int>(m_bits.size()); i += 2) {
|
||||
uint8_t byte;
|
||||
Cvt::fromHex(&byte, 1, m_bits.data() + i, 2);
|
||||
bits = (bits << 8) | byte;
|
||||
}
|
||||
m_headerBlob.insert(m_headerBlob.end(), reinterpret_cast<const uint8_t*>(&bits),
|
||||
reinterpret_cast<const uint8_t*>(&bits) + 4);
|
||||
|
||||
// nNonce placeholder (32 bytes of zeros - miner fills first 4 bytes)
|
||||
// Remember position for later extraction
|
||||
m_nonceOffset = m_headerBlob.size();
|
||||
m_headerBlob.insert(m_headerBlob.end(), 32, 0);
|
||||
|
||||
// nSolution - empty vector serialized as compact size 0
|
||||
// For CBlock serialization, nSolution is std::vector<unsigned char>
|
||||
// Empty vector = compactsize(0) = 0x00
|
||||
m_headerBlob.push_back(0x00);
|
||||
|
||||
// Transactions: compactsize(count) + coinbase + other txs
|
||||
const size_t txCount = 1 + m_transactions.size(); // coinbase + others
|
||||
|
||||
// Write compact size for transaction count
|
||||
if (txCount < 0xFD) {
|
||||
m_headerBlob.push_back(static_cast<uint8_t>(txCount));
|
||||
} else if (txCount <= 0xFFFF) {
|
||||
m_headerBlob.push_back(0xFD);
|
||||
const uint16_t cnt16 = static_cast<uint16_t>(txCount);
|
||||
m_headerBlob.insert(m_headerBlob.end(), reinterpret_cast<const uint8_t*>(&cnt16),
|
||||
reinterpret_cast<const uint8_t*>(&cnt16) + 2);
|
||||
} else {
|
||||
m_headerBlob.push_back(0xFE);
|
||||
const uint32_t cnt32 = static_cast<uint32_t>(txCount);
|
||||
m_headerBlob.insert(m_headerBlob.end(), reinterpret_cast<const uint8_t*>(&cnt32),
|
||||
reinterpret_cast<const uint8_t*>(&cnt32) + 4);
|
||||
}
|
||||
|
||||
// Append coinbase transaction
|
||||
std::vector<uint8_t> cbTx(m_coinbaseTx.size() / 2);
|
||||
Cvt::fromHex(cbTx.data(), cbTx.size(), m_coinbaseTx.data(), m_coinbaseTx.size());
|
||||
m_headerBlob.insert(m_headerBlob.end(), cbTx.begin(), cbTx.end());
|
||||
|
||||
// Append other transactions
|
||||
for (const auto &txHex : m_transactions) {
|
||||
std::vector<uint8_t> tx(txHex.size() / 2);
|
||||
Cvt::fromHex(tx.data(), tx.size(), txHex.data(), txHex.size());
|
||||
m_headerBlob.insert(m_headerBlob.end(), tx.begin(), tx.end());
|
||||
}
|
||||
|
||||
// Set the full block blob for mining
|
||||
LOG_DEBUG("%s block blob size=%zu (header=141 + %zu txs), seed=%s",
|
||||
tag(), m_headerBlob.size(), txCount, m_keyBlockHash.data());
|
||||
{
|
||||
const String hdrHex = Cvt::toHex(m_headerBlob.data(), std::min(m_headerBlob.size(), size_t(200)));
|
||||
LOG_DEBUG("%s block: %s...", tag(), hdrHex.data());
|
||||
}
|
||||
if (!job.setBlob(Cvt::toHex(m_headerBlob.data(), m_headerBlob.size()))) {
|
||||
LOG_ERR("%s " RED("failed to set job blob (size=%zu)"), tag(), m_headerBlob.size());
|
||||
return false;
|
||||
}
|
||||
|
||||
m_currentJobId = Cvt::toHex(Cvt::randomBytes(4));
|
||||
job.setId(m_currentJobId);
|
||||
|
||||
m_job = std::move(job);
|
||||
m_prevJobHash = m_prevHash;
|
||||
m_jobSteadyMs = Chrono::steadyMSecs();
|
||||
|
||||
if (m_state == ConnectingState) {
|
||||
LOG_INFO("%s " GREEN("connected to %s:%d"), tag(), m_pool.host().data(), m_pool.port());
|
||||
setState(ConnectedState);
|
||||
}
|
||||
|
||||
LOG_INFO("%s " MAGENTA_BOLD("new job") " height: " CYAN_BOLD("%u") " diff: " CYAN_BOLD("%u"),
|
||||
tag(), m_height, job.diff());
|
||||
|
||||
m_listener->onJobReceived(this, m_job, rapidjson::Value());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool HushClient::parseResponse(int64_t id, const rapidjson::Value &result, const rapidjson::Value &error)
|
||||
{
|
||||
// Handle RPC errors
|
||||
if (error.IsObject()) {
|
||||
const char *message = Json::getString(error, "message", "unknown error");
|
||||
const int code = Json::getInt(error, "code", -1);
|
||||
LOG_ERR("%s " RED("RPC error %d: \"%s\""), tag(), code, message);
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (m_pendingRequest) {
|
||||
case REQ_TEMPLATE:
|
||||
if (parseBlockTemplate(result)) {
|
||||
m_timer->start(m_pool.pollInterval(), m_pool.pollInterval());
|
||||
return true;
|
||||
}
|
||||
return m_pendingRequest == REQ_KEYHASH; // Waiting for key is ok
|
||||
|
||||
case REQ_KEYHASH:
|
||||
if (result.IsString()) {
|
||||
m_keyBlockHash = result.GetString();
|
||||
m_keyHeight = m_pendingKeyHeight;
|
||||
LOG_INFO("%s " GREEN("RandomX key block %u: %.16s..."),
|
||||
tag(), m_keyHeight, m_keyBlockHash.data());
|
||||
// Now get the block template again with the key
|
||||
m_pendingRequest = REQ_NONE;
|
||||
getBlockTemplate();
|
||||
return true;
|
||||
}
|
||||
LOG_ERR("%s " RED("invalid getblockhash response - expected string, got type %d"),
|
||||
tag(), result.GetType());
|
||||
return false;
|
||||
|
||||
case REQ_SUBMIT:
|
||||
// submitblock returns null on success
|
||||
if (result.IsNull()) {
|
||||
LOG_INFO("%s " GREEN_BOLD("BLOCK ACCEPTED!"), tag());
|
||||
if (m_results.count(id)) {
|
||||
m_listener->onResultAccepted(this, m_results[id], nullptr);
|
||||
m_results.erase(id);
|
||||
}
|
||||
} else if (result.IsString()) {
|
||||
const char *msg = result.GetString();
|
||||
if (strlen(msg) > 0) {
|
||||
LOG_ERR("%s " RED("block rejected: %s"), tag(), msg);
|
||||
} else {
|
||||
LOG_INFO("%s " GREEN_BOLD("BLOCK ACCEPTED!"), tag());
|
||||
}
|
||||
}
|
||||
// Get new work after submit
|
||||
getBlockTemplate();
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void HushClient::retry()
|
||||
{
|
||||
m_failures++;
|
||||
m_listener->onClose(this, static_cast<int>(m_failures));
|
||||
|
||||
if (m_state == ConnectedState) {
|
||||
setState(ConnectingState);
|
||||
}
|
||||
|
||||
m_timer->stop();
|
||||
m_timer->start(m_retryPause, 0);
|
||||
}
|
||||
|
||||
void HushClient::setState(SocketState state)
|
||||
{
|
||||
if (m_state == state) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_state = state;
|
||||
|
||||
switch (state) {
|
||||
case ConnectedState:
|
||||
m_failures = 0;
|
||||
m_listener->onLoginSuccess(this);
|
||||
break;
|
||||
|
||||
case UnconnectedState:
|
||||
m_timer->stop();
|
||||
m_failures = -1;
|
||||
m_listener->onClose(this, -1);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
uint64_t HushClient::getKeyHeight(uint64_t height) const
|
||||
{
|
||||
// RandomX key changes every RANDOMX_INTERVAL blocks with RANDOMX_LAG delay
|
||||
if (height < static_cast<uint64_t>(RANDOMX_INTERVAL + RANDOMX_LAG)) {
|
||||
return 0; // Use genesis key
|
||||
}
|
||||
return ((height - RANDOMX_LAG) / RANDOMX_INTERVAL) * RANDOMX_INTERVAL;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> HushClient::serializeBlock(uint32_t nonce, uint64_t extraNonce) const
|
||||
{
|
||||
std::vector<uint8_t> blob;
|
||||
blob.reserve(1024);
|
||||
|
||||
// nVersion (4 bytes, little-endian)
|
||||
const uint32_t ver = static_cast<uint32_t>(m_version);
|
||||
blob.insert(blob.end(), reinterpret_cast<const uint8_t*>(&ver),
|
||||
reinterpret_cast<const uint8_t*>(&ver) + 4);
|
||||
|
||||
// hashPrevBlock (32 bytes, reversed from hex display)
|
||||
std::vector<uint8_t> prevHash(32);
|
||||
Cvt::fromHex(prevHash.data(), 32, m_prevHash.data(), 64);
|
||||
std::reverse(prevHash.begin(), prevHash.end());
|
||||
blob.insert(blob.end(), prevHash.begin(), prevHash.end());
|
||||
|
||||
// hashMerkleRoot (32 bytes, reversed)
|
||||
std::vector<uint8_t> merkleRoot(32);
|
||||
Cvt::fromHex(merkleRoot.data(), 32, m_merkleRoot.data(), 64);
|
||||
std::reverse(merkleRoot.begin(), merkleRoot.end());
|
||||
blob.insert(blob.end(), merkleRoot.begin(), merkleRoot.end());
|
||||
|
||||
// hashFinalSaplingRoot (32 bytes, reversed)
|
||||
std::vector<uint8_t> saplingRoot(32);
|
||||
Cvt::fromHex(saplingRoot.data(), 32, m_saplingRoot.data(), 64);
|
||||
std::reverse(saplingRoot.begin(), saplingRoot.end());
|
||||
blob.insert(blob.end(), saplingRoot.begin(), saplingRoot.end());
|
||||
|
||||
// nTime (4 bytes, little-endian)
|
||||
const uint32_t time32 = static_cast<uint32_t>(m_curtime);
|
||||
blob.insert(blob.end(), reinterpret_cast<const uint8_t*>(&time32),
|
||||
reinterpret_cast<const uint8_t*>(&time32) + 4);
|
||||
|
||||
// nBits (4 bytes) - convert from big-endian hex to little-endian binary
|
||||
uint32_t bits = 0;
|
||||
for (int i = 0; i < 8 && i < static_cast<int>(m_bits.size()); i += 2) {
|
||||
uint8_t byte;
|
||||
Cvt::fromHex(&byte, 1, m_bits.data() + i, 2);
|
||||
bits = (bits << 8) | byte;
|
||||
}
|
||||
blob.insert(blob.end(), reinterpret_cast<const uint8_t*>(&bits),
|
||||
reinterpret_cast<const uint8_t*>(&bits) + 4);
|
||||
|
||||
// nNonce (32 bytes for Zcash/Hush - uint256)
|
||||
std::vector<uint8_t> nonce256(32, 0);
|
||||
memcpy(nonce256.data(), &nonce, 4);
|
||||
memcpy(nonce256.data() + 4, &extraNonce, sizeof(extraNonce));
|
||||
blob.insert(blob.end(), nonce256.begin(), nonce256.end());
|
||||
|
||||
// nSolution placeholder (CompactSize + 32 zeros)
|
||||
blob.push_back(32); // CompactSize for 32 bytes
|
||||
blob.insert(blob.end(), 32, 0);
|
||||
|
||||
// Transaction count (CompactSize)
|
||||
const size_t txCount = 1 + m_transactions.size();
|
||||
if (txCount < 0xFD) {
|
||||
blob.push_back(static_cast<uint8_t>(txCount));
|
||||
} else {
|
||||
blob.push_back(0xFD);
|
||||
blob.push_back(txCount & 0xFF);
|
||||
blob.push_back((txCount >> 8) & 0xFF);
|
||||
}
|
||||
|
||||
// Coinbase transaction
|
||||
std::vector<uint8_t> coinbase(m_coinbaseTx.size() / 2);
|
||||
Cvt::fromHex(coinbase.data(), coinbase.size(), m_coinbaseTx.data(), m_coinbaseTx.size());
|
||||
blob.insert(blob.end(), coinbase.begin(), coinbase.end());
|
||||
|
||||
// Other transactions
|
||||
for (const auto &txHex : m_transactions) {
|
||||
std::vector<uint8_t> tx(txHex.size() / 2);
|
||||
Cvt::fromHex(tx.data(), tx.size(), txHex.data(), txHex.size());
|
||||
blob.insert(blob.end(), tx.begin(), tx.end());
|
||||
}
|
||||
|
||||
return blob;
|
||||
}
|
||||
|
||||
std::string HushClient::serializeBlockHex(uint32_t nonce, const uint8_t* solution) const
|
||||
{
|
||||
std::vector<uint8_t> blob;
|
||||
blob.reserve(1024);
|
||||
|
||||
// Header (same as serializeBlock but with actual nonce and solution)
|
||||
const uint32_t ver = static_cast<uint32_t>(m_version);
|
||||
blob.insert(blob.end(), reinterpret_cast<const uint8_t*>(&ver),
|
||||
reinterpret_cast<const uint8_t*>(&ver) + 4);
|
||||
|
||||
std::vector<uint8_t> prevHash(32);
|
||||
Cvt::fromHex(prevHash.data(), 32, m_prevHash.data(), 64);
|
||||
std::reverse(prevHash.begin(), prevHash.end());
|
||||
blob.insert(blob.end(), prevHash.begin(), prevHash.end());
|
||||
|
||||
std::vector<uint8_t> merkleRoot(32);
|
||||
Cvt::fromHex(merkleRoot.data(), 32, m_merkleRoot.data(), 64);
|
||||
std::reverse(merkleRoot.begin(), merkleRoot.end());
|
||||
blob.insert(blob.end(), merkleRoot.begin(), merkleRoot.end());
|
||||
|
||||
std::vector<uint8_t> saplingRoot(32);
|
||||
Cvt::fromHex(saplingRoot.data(), 32, m_saplingRoot.data(), 64);
|
||||
std::reverse(saplingRoot.begin(), saplingRoot.end());
|
||||
blob.insert(blob.end(), saplingRoot.begin(), saplingRoot.end());
|
||||
|
||||
const uint32_t time32 = static_cast<uint32_t>(m_curtime);
|
||||
blob.insert(blob.end(), reinterpret_cast<const uint8_t*>(&time32),
|
||||
reinterpret_cast<const uint8_t*>(&time32) + 4);
|
||||
|
||||
uint32_t bits = 0;
|
||||
for (int i = 0; i < 8 && i < static_cast<int>(m_bits.size()); i += 2) {
|
||||
uint8_t byte;
|
||||
Cvt::fromHex(&byte, 1, m_bits.data() + i, 2);
|
||||
bits = (bits << 8) | byte;
|
||||
}
|
||||
blob.insert(blob.end(), reinterpret_cast<const uint8_t*>(&bits),
|
||||
reinterpret_cast<const uint8_t*>(&bits) + 4);
|
||||
|
||||
// Nonce (32 bytes with found nonce value)
|
||||
std::vector<uint8_t> nonce256(32, 0);
|
||||
memcpy(nonce256.data(), &nonce, 4);
|
||||
blob.insert(blob.end(), nonce256.begin(), nonce256.end());
|
||||
|
||||
// Solution (the RandomX hash - 32 bytes)
|
||||
blob.push_back(32); // CompactSize
|
||||
blob.insert(blob.end(), solution, solution + 32);
|
||||
|
||||
// Transactions
|
||||
const size_t txCount = 1 + m_transactions.size();
|
||||
if (txCount < 0xFD) {
|
||||
blob.push_back(static_cast<uint8_t>(txCount));
|
||||
} else {
|
||||
blob.push_back(0xFD);
|
||||
blob.push_back(txCount & 0xFF);
|
||||
blob.push_back((txCount >> 8) & 0xFF);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> coinbase(m_coinbaseTx.size() / 2);
|
||||
Cvt::fromHex(coinbase.data(), coinbase.size(), m_coinbaseTx.data(), m_coinbaseTx.size());
|
||||
blob.insert(blob.end(), coinbase.begin(), coinbase.end());
|
||||
|
||||
for (const auto &txHex : m_transactions) {
|
||||
std::vector<uint8_t> tx(txHex.size() / 2);
|
||||
Cvt::fromHex(tx.data(), tx.size(), txHex.data(), txHex.size());
|
||||
blob.insert(blob.end(), tx.begin(), tx.end());
|
||||
}
|
||||
|
||||
const String hex = Cvt::toHex(blob.data(), blob.size());
|
||||
return std::string(hex.data(), hex.size());
|
||||
}
|
||||
|
||||
void HushClient::sha256d(const uint8_t* data, size_t len, uint8_t* out)
|
||||
{
|
||||
uint8_t hash1[32];
|
||||
SHA256(data, len, hash1);
|
||||
SHA256(hash1, 32, out);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> HushClient::serializeHeader(uint32_t nonce, const uint8_t* solution) const
|
||||
{
|
||||
std::vector<uint8_t> header;
|
||||
header.reserve(140 + 1 + 32); // header + compactsize + solution
|
||||
|
||||
// nVersion (4 bytes, little-endian)
|
||||
const uint32_t ver = static_cast<uint32_t>(m_version);
|
||||
header.insert(header.end(), reinterpret_cast<const uint8_t*>(&ver),
|
||||
reinterpret_cast<const uint8_t*>(&ver) + 4);
|
||||
|
||||
// hashPrevBlock (32 bytes, internal byte order)
|
||||
std::vector<uint8_t> prevHashBytes(32);
|
||||
Cvt::fromHex(prevHashBytes.data(), 32, m_prevHash.data(), 64);
|
||||
std::reverse(prevHashBytes.begin(), prevHashBytes.end());
|
||||
header.insert(header.end(), prevHashBytes.begin(), prevHashBytes.end());
|
||||
|
||||
// hashMerkleRoot (32 bytes)
|
||||
std::vector<uint8_t> merkleRoot(32);
|
||||
Cvt::fromHex(merkleRoot.data(), 32, m_merkleRoot.data(), 64);
|
||||
std::reverse(merkleRoot.begin(), merkleRoot.end());
|
||||
header.insert(header.end(), merkleRoot.begin(), merkleRoot.end());
|
||||
|
||||
// hashFinalSaplingRoot (32 bytes)
|
||||
std::vector<uint8_t> saplingRoot(32);
|
||||
Cvt::fromHex(saplingRoot.data(), 32, m_saplingRoot.data(), 64);
|
||||
std::reverse(saplingRoot.begin(), saplingRoot.end());
|
||||
header.insert(header.end(), saplingRoot.begin(), saplingRoot.end());
|
||||
|
||||
// nTime (4 bytes)
|
||||
const uint32_t time32 = static_cast<uint32_t>(m_curtime);
|
||||
header.insert(header.end(), reinterpret_cast<const uint8_t*>(&time32),
|
||||
reinterpret_cast<const uint8_t*>(&time32) + 4);
|
||||
|
||||
// nBits (4 bytes)
|
||||
uint32_t bits = 0;
|
||||
for (int i = 0; i < 8 && i < static_cast<int>(m_bits.size()); i += 2) {
|
||||
uint8_t byte;
|
||||
Cvt::fromHex(&byte, 1, m_bits.data() + i, 2);
|
||||
bits = (bits << 8) | byte;
|
||||
}
|
||||
header.insert(header.end(), reinterpret_cast<const uint8_t*>(&bits),
|
||||
reinterpret_cast<const uint8_t*>(&bits) + 4);
|
||||
|
||||
// nNonce (32 bytes with found nonce value in first 4 bytes)
|
||||
std::vector<uint8_t> nonce256(32, 0);
|
||||
memcpy(nonce256.data(), &nonce, 4);
|
||||
header.insert(header.end(), nonce256.begin(), nonce256.end());
|
||||
|
||||
// nSolution (compactsize + 32 bytes)
|
||||
header.push_back(32); // CompactSize for 32 bytes
|
||||
header.insert(header.end(), solution, solution + 32);
|
||||
|
||||
return header;
|
||||
}
|
||||
|
||||
bool HushClient::checkPow(uint32_t nonce, const uint8_t* solution) const
|
||||
{
|
||||
// Build header with solution
|
||||
const std::vector<uint8_t> header = serializeHeader(nonce, solution);
|
||||
|
||||
// Compute SHA256D(header)
|
||||
uint8_t blockHash[32];
|
||||
sha256d(header.data(), header.size(), blockHash);
|
||||
|
||||
// Compare blockHash < target (both are 32 bytes, big-endian in target, little-endian in hash)
|
||||
// Block hash from SHA256D is little-endian, target from RPC is big-endian
|
||||
// We need to compare them byte by byte, reversing the hash for comparison
|
||||
for (int i = 0; i < 32; i++) {
|
||||
uint8_t hashByte = blockHash[31 - i]; // Reverse hash to big-endian
|
||||
uint8_t targetByte = m_targetBytes[i];
|
||||
|
||||
if (hashByte < targetByte) {
|
||||
LOG_INFO("%s " GREEN_BOLD("PoW check PASSED") " - hash meets target", tag());
|
||||
return true;
|
||||
}
|
||||
if (hashByte > targetByte) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true; // Equal means it passes
|
||||
}
|
||||
|
||||
uint64_t HushClient::targetToDiff(const char *target) const
|
||||
{
|
||||
// Target is 64 hex chars (256-bit big-endian)
|
||||
// Find the first non-zero bytes and compute difficulty
|
||||
if (!target || strlen(target) < 16) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Parse first 8 bytes as big-endian uint64 for approximation
|
||||
uint64_t t = 0;
|
||||
for (int i = 0; i < 16; i += 2) {
|
||||
uint8_t byte;
|
||||
Cvt::fromHex(&byte, 1, target + i, 2);
|
||||
t = (t << 8) | byte;
|
||||
}
|
||||
|
||||
if (t == 0) {
|
||||
return 0xFFFFFFFFFFFFFFFFULL;
|
||||
}
|
||||
|
||||
// Rough difficulty approximation
|
||||
return 0xFFFFFFFFFFFFFFFFULL / t;
|
||||
}
|
||||
|
||||
} // namespace xmrig
|
||||
135
src/base/net/stratum/HushClient.h
Normal file
135
src/base/net/stratum/HushClient.h
Normal file
@@ -0,0 +1,135 @@
|
||||
/* XMRig - Hush/DragonX HAC Support
|
||||
* Copyright (c) 2024 XMRig <https://github.com/xmrig>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#ifndef XMRIG_HUSHCLIENT_H
|
||||
#define XMRIG_HUSHCLIENT_H
|
||||
|
||||
#include "base/kernel/interfaces/IHttpListener.h"
|
||||
#include "base/kernel/interfaces/ITimerListener.h"
|
||||
#include "base/net/stratum/BaseClient.h"
|
||||
#include "base/net/stratum/SubmitResult.h"
|
||||
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
namespace xmrig {
|
||||
|
||||
class Timer;
|
||||
|
||||
/**
|
||||
* Client for Hush Asset Chains (HAC) that use RandomX PoW.
|
||||
* Handles BIP22-style getblocktemplate RPC format used by hushd/dragonxd.
|
||||
*/
|
||||
class HushClient : public BaseClient, public ITimerListener, public IHttpListener
|
||||
{
|
||||
public:
|
||||
XMRIG_DISABLE_COPY_MOVE_DEFAULT(HushClient)
|
||||
|
||||
HushClient(int id, IClientListener *listener);
|
||||
~HushClient() override;
|
||||
|
||||
protected:
|
||||
// BaseClient interface
|
||||
bool disconnect() override;
|
||||
bool isTLS() const override;
|
||||
int64_t submit(const JobResult &result) override;
|
||||
void connect() override;
|
||||
void connect(const Pool &pool) override;
|
||||
void setPool(const Pool &pool) override;
|
||||
void deleteLater() override;
|
||||
|
||||
// IHttpListener
|
||||
void onHttpData(const HttpData &data) override;
|
||||
|
||||
// ITimerListener
|
||||
void onTimer(const Timer *timer) override;
|
||||
|
||||
inline bool hasExtension(Extension) const noexcept override { return false; }
|
||||
inline const char *mode() const override { return "hush-daemon"; }
|
||||
inline const char *tlsFingerprint() const override { return m_tlsFingerprint; }
|
||||
inline const char *tlsVersion() const override { return m_tlsVersion; }
|
||||
inline int64_t send(const rapidjson::Value &, Callback) override { return -1; }
|
||||
inline int64_t send(const rapidjson::Value &) override { return -1; }
|
||||
inline void tick(uint64_t) override {}
|
||||
|
||||
private:
|
||||
// RPC methods
|
||||
int64_t getBlockTemplate();
|
||||
int64_t getBlockHash(uint64_t height);
|
||||
int64_t submitBlock(const std::string &blockHex);
|
||||
int64_t rpcSend(const rapidjson::Document &doc);
|
||||
|
||||
// Job handling
|
||||
bool parseBlockTemplate(const rapidjson::Value &result);
|
||||
bool parseResponse(int64_t id, const rapidjson::Value &result, const rapidjson::Value &error);
|
||||
void retry();
|
||||
void setState(SocketState state);
|
||||
|
||||
// RandomX key management
|
||||
uint64_t getKeyHeight(uint64_t height) const;
|
||||
|
||||
// Block serialization
|
||||
std::vector<uint8_t> serializeBlock(uint32_t nonce, uint64_t extraNonce) const;
|
||||
std::vector<uint8_t> serializeHeader(uint32_t nonce, const uint8_t* solution) const;
|
||||
std::string serializeBlockHex(uint32_t nonce, const uint8_t* solution) const;
|
||||
|
||||
// PoW verification (SHA256D of header with solution)
|
||||
bool checkPow(uint32_t nonce, const uint8_t* solution) const;
|
||||
static void sha256d(const uint8_t* data, size_t len, uint8_t* out);
|
||||
|
||||
// Target/difficulty conversion
|
||||
uint64_t targetToDiff(const char *target) const;
|
||||
|
||||
// Constants for RandomX key derivation
|
||||
static constexpr int RANDOMX_INTERVAL = 1024;
|
||||
static constexpr int RANDOMX_LAG = 64;
|
||||
|
||||
// Block template data
|
||||
int32_t m_version = 4;
|
||||
uint64_t m_height = 0;
|
||||
uint64_t m_curtime = 0;
|
||||
String m_prevHash;
|
||||
String m_merkleRoot; // coinbasetxn.hash
|
||||
String m_saplingRoot; // finalsaplingroothash
|
||||
String m_bits;
|
||||
String m_target;
|
||||
std::vector<uint8_t> m_targetBytes; // Network target as 32 bytes for comparison
|
||||
String m_coinbaseTx; // coinbasetxn.data
|
||||
std::vector<String> m_transactions;
|
||||
|
||||
// RandomX key tracking
|
||||
uint64_t m_keyHeight = 0;
|
||||
String m_keyBlockHash;
|
||||
|
||||
// Job state
|
||||
String m_currentJobId;
|
||||
String m_prevJobHash;
|
||||
uint64_t m_jobSteadyMs = 0;
|
||||
|
||||
// Cached block blob for submission (full block with header + txs)
|
||||
std::vector<uint8_t> m_headerBlob;
|
||||
size_t m_nonceOffset = 0; // Offset of nNonce field in the block blob
|
||||
|
||||
// Request tracking
|
||||
enum RequestType { REQ_NONE, REQ_TEMPLATE, REQ_KEYHASH, REQ_SUBMIT };
|
||||
RequestType m_pendingRequest = REQ_NONE;
|
||||
uint64_t m_pendingKeyHeight = 0;
|
||||
|
||||
Timer *m_timer;
|
||||
std::shared_ptr<IHttpListener> m_httpListener;
|
||||
std::map<int64_t, SubmitResult> m_results;
|
||||
|
||||
char m_tlsFingerprint[64] = { 0 };
|
||||
char m_tlsVersion[32] = { 0 };
|
||||
};
|
||||
|
||||
} // namespace xmrig
|
||||
|
||||
#endif // XMRIG_HUSHCLIENT_H
|
||||
@@ -39,6 +39,7 @@
|
||||
|
||||
#ifdef XMRIG_FEATURE_HTTP
|
||||
# include "base/net/stratum/DaemonClient.h"
|
||||
# include "base/net/stratum/HushClient.h"
|
||||
# include "base/net/stratum/SelfSelectClient.h"
|
||||
#endif
|
||||
|
||||
@@ -239,7 +240,12 @@ xmrig::IClient *xmrig::Pool::createClient(int id, IClientListener *listener) con
|
||||
}
|
||||
# ifdef XMRIG_FEATURE_HTTP
|
||||
else if (m_mode == MODE_DAEMON) {
|
||||
client = new DaemonClient(id, listener);
|
||||
// Use HushClient for Hush Asset Chains (HAC) with RandomX
|
||||
if (m_coin == Coin::HUSH) {
|
||||
client = new HushClient(id, listener);
|
||||
} else {
|
||||
client = new DaemonClient(id, listener);
|
||||
}
|
||||
}
|
||||
else if (m_mode == MODE_SELF_SELECT) {
|
||||
client = new SelfSelectClient(id, Platform::userAgent(), listener, m_submitToOrigin);
|
||||
|
||||
Reference in New Issue
Block a user