From d49e9a51994bcaa5225e12d6cd04739f6330df99 Mon Sep 17 00:00:00 2001 From: dan_s Date: Tue, 27 Jan 2026 16:45:21 -0600 Subject: [PATCH] HAC changes --- src/base/base.cmake | 2 + src/base/crypto/Coin.cpp | 1 + src/base/crypto/Coin.h | 1 + src/base/net/stratum/HushClient.cpp | 854 ++++++++++++++++++++++++++++ src/base/net/stratum/HushClient.h | 135 +++++ src/base/net/stratum/Pool.cpp | 8 +- 6 files changed, 1000 insertions(+), 1 deletion(-) create mode 100644 src/base/net/stratum/HushClient.cpp create mode 100644 src/base/net/stratum/HushClient.h diff --git a/src/base/base.cmake b/src/base/base.cmake index 0f4f8725..2fb92099 100644 --- a/src/base/base.cmake +++ b/src/base/base.cmake @@ -200,6 +200,7 @@ if (WITH_HTTP) src/base/net/http/HttpData.h src/base/net/http/HttpResponse.h src/base/net/stratum/DaemonClient.h + src/base/net/stratum/HushClient.h src/base/net/stratum/SelfSelectClient.h src/base/net/tools/TcpServer.h ) @@ -220,6 +221,7 @@ if (WITH_HTTP) src/base/net/http/HttpListener.cpp src/base/net/http/HttpResponse.cpp src/base/net/stratum/DaemonClient.cpp + src/base/net/stratum/HushClient.cpp src/base/net/stratum/SelfSelectClient.cpp src/base/net/tools/TcpServer.cpp ) diff --git a/src/base/crypto/Coin.cpp b/src/base/crypto/Coin.cpp index 85cc605a..7b39fbcb 100644 --- a/src/base/crypto/Coin.cpp +++ b/src/base/crypto/Coin.cpp @@ -55,6 +55,7 @@ static const CoinInfo coinInfo[] = { { Algorithm::RX_0, "ZEPH", "Zephyr", 120, 1000000000000, BLUE_BG_BOLD( WHITE_BOLD_S " zephyr ") }, { Algorithm::RX_0, "Townforge","Townforge", 30, 100000000, MAGENTA_BG_BOLD(WHITE_BOLD_S " townforge ") }, { Algorithm::RX_YADA, "YDA", "YadaCoin", 120, 100000000, BLUE_BG_BOLD( WHITE_BOLD_S " yada ") }, + { Algorithm::RX_0, "HUSH", "Hush", 60, 100000000, GREEN_BG_BOLD( WHITE_BOLD_S " hush ") }, }; diff --git a/src/base/crypto/Coin.h b/src/base/crypto/Coin.h index c3c97fda..ac7558d1 100644 --- a/src/base/crypto/Coin.h +++ b/src/base/crypto/Coin.h @@ -41,6 +41,7 @@ public: ZEPHYR, TOWNFORGE, YADA, + HUSH, MAX }; diff --git a/src/base/net/stratum/HushClient.cpp b/src/base/net/stratum/HushClient.cpp new file mode 100644 index 00000000..e44cf39c --- /dev/null +++ b/src/base/net/stratum/HushClient.cpp @@ -0,0 +1,854 @@ +/* XMRig - Hush/DragonX HAC Support + * Copyright (c) 2024 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 +#include +#include + +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(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(m_version); + m_headerBlob.insert(m_headerBlob.end(), reinterpret_cast(&ver), + reinterpret_cast(&ver) + 4); + + // hashPrevBlock (32 bytes, internal byte order) + std::vector 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 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 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(m_curtime); + m_headerBlob.insert(m_headerBlob.end(), reinterpret_cast(&time32), + reinterpret_cast(&time32) + 4); + + // nBits (4 bytes) + uint32_t bits = 0; + for (int i = 0; i < 8 && i < static_cast(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(&bits), + reinterpret_cast(&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 + // 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(txCount)); + } else if (txCount <= 0xFFFF) { + m_headerBlob.push_back(0xFD); + const uint16_t cnt16 = static_cast(txCount); + m_headerBlob.insert(m_headerBlob.end(), reinterpret_cast(&cnt16), + reinterpret_cast(&cnt16) + 2); + } else { + m_headerBlob.push_back(0xFE); + const uint32_t cnt32 = static_cast(txCount); + m_headerBlob.insert(m_headerBlob.end(), reinterpret_cast(&cnt32), + reinterpret_cast(&cnt32) + 4); + } + + // Append coinbase transaction + std::vector 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 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(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(RANDOMX_INTERVAL + RANDOMX_LAG)) { + return 0; // Use genesis key + } + return ((height - RANDOMX_LAG) / RANDOMX_INTERVAL) * RANDOMX_INTERVAL; +} + +std::vector HushClient::serializeBlock(uint32_t nonce, uint64_t extraNonce) const +{ + std::vector blob; + blob.reserve(1024); + + // nVersion (4 bytes, little-endian) + const uint32_t ver = static_cast(m_version); + blob.insert(blob.end(), reinterpret_cast(&ver), + reinterpret_cast(&ver) + 4); + + // hashPrevBlock (32 bytes, reversed from hex display) + std::vector 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 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 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(m_curtime); + blob.insert(blob.end(), reinterpret_cast(&time32), + reinterpret_cast(&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(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(&bits), + reinterpret_cast(&bits) + 4); + + // nNonce (32 bytes for Zcash/Hush - uint256) + std::vector 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(txCount)); + } else { + blob.push_back(0xFD); + blob.push_back(txCount & 0xFF); + blob.push_back((txCount >> 8) & 0xFF); + } + + // Coinbase transaction + std::vector 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 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 blob; + blob.reserve(1024); + + // Header (same as serializeBlock but with actual nonce and solution) + const uint32_t ver = static_cast(m_version); + blob.insert(blob.end(), reinterpret_cast(&ver), + reinterpret_cast(&ver) + 4); + + std::vector 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 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 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(m_curtime); + blob.insert(blob.end(), reinterpret_cast(&time32), + reinterpret_cast(&time32) + 4); + + uint32_t bits = 0; + for (int i = 0; i < 8 && i < static_cast(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(&bits), + reinterpret_cast(&bits) + 4); + + // Nonce (32 bytes with found nonce value) + std::vector 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(txCount)); + } else { + blob.push_back(0xFD); + blob.push_back(txCount & 0xFF); + blob.push_back((txCount >> 8) & 0xFF); + } + + std::vector 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 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 HushClient::serializeHeader(uint32_t nonce, const uint8_t* solution) const +{ + std::vector header; + header.reserve(140 + 1 + 32); // header + compactsize + solution + + // nVersion (4 bytes, little-endian) + const uint32_t ver = static_cast(m_version); + header.insert(header.end(), reinterpret_cast(&ver), + reinterpret_cast(&ver) + 4); + + // hashPrevBlock (32 bytes, internal byte order) + std::vector 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 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 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(m_curtime); + header.insert(header.end(), reinterpret_cast(&time32), + reinterpret_cast(&time32) + 4); + + // nBits (4 bytes) + uint32_t bits = 0; + for (int i = 0; i < 8 && i < static_cast(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(&bits), + reinterpret_cast(&bits) + 4); + + // nNonce (32 bytes with found nonce value in first 4 bytes) + std::vector 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 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 diff --git a/src/base/net/stratum/HushClient.h b/src/base/net/stratum/HushClient.h new file mode 100644 index 00000000..90262a51 --- /dev/null +++ b/src/base/net/stratum/HushClient.h @@ -0,0 +1,135 @@ +/* XMRig - Hush/DragonX HAC Support + * Copyright (c) 2024 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 +#include +#include + +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 serializeBlock(uint32_t nonce, uint64_t extraNonce) const; + std::vector 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 m_targetBytes; // Network target as 32 bytes for comparison + String m_coinbaseTx; // coinbasetxn.data + std::vector 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 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 m_httpListener; + std::map m_results; + + char m_tlsFingerprint[64] = { 0 }; + char m_tlsVersion[32] = { 0 }; +}; + +} // namespace xmrig + +#endif // XMRIG_HUSHCLIENT_H diff --git a/src/base/net/stratum/Pool.cpp b/src/base/net/stratum/Pool.cpp index 8b4a6e03..919bab7b 100644 --- a/src/base/net/stratum/Pool.cpp +++ b/src/base/net/stratum/Pool.cpp @@ -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);