Files
xmrig-hac/src/base/net/stratum/HushClient.cpp
dan-s 9c9b7680e4 Fixing miner issues
- Switch from full-block to header-only (140-byte) RandomX input
- Add 32-byte SoloNonce system for solo mining mode
- Compute proper difficulty target from compact bits field
- Add SHA256D dual-hash PoW check in CpuWorker for solo mining
- Raise RandomX dataset/scratchpad limits to 4GB/4MB
- Use standard RandomX share filtering in pool (stratum) mode
2026-03-05 06:00:55 -06:00

865 lines
28 KiB
C++

/* 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 <cinttypes>
#include <cstring>
#ifdef XMRIG_FEATURE_TLS
# include <openssl/sha.h>
#else
// Standalone SHA-256 for non-TLS builds (Windows cross-compile)
# ifdef _WIN32
# include <windows.h>
# include <bcrypt.h>
static void SHA256(const uint8_t* data, size_t len, uint8_t* out) {
BCRYPT_ALG_HANDLE hAlg = nullptr;
BCRYPT_HASH_HANDLE hHash = nullptr;
BCryptOpenAlgorithmProvider(&hAlg, BCRYPT_SHA256_ALGORITHM, nullptr, 0);
BCryptCreateHash(hAlg, &hHash, nullptr, 0, nullptr, 0, 0);
BCryptHashData(hHash, const_cast<PUCHAR>(data), static_cast<ULONG>(len), 0);
BCryptFinishHash(hHash, out, 32, 0);
BCryptDestroyHash(hHash);
BCryptCloseAlgorithmProvider(hAlg, 0);
}
# else
# include "crypto/ghostrider/sph_sha2.h"
static void SHA256(const uint8_t* data, size_t len, uint8_t* out) {
sph_sha256_context ctx;
sph_sha256_init(&ctx);
sph_sha256(&ctx, data, len);
sph_sha256_close(&ctx, out);
}
# endif
#endif
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 &params)
{
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;
}
// Get the 32-byte nonce from the solo mining result
const uint8_t *nonce32 = result.isSoloResult() ? result.soloNonce() : nullptr;
if (!nonce32) {
LOG_ERR("%s " RED("submit called without solo nonce"), tag());
return -1;
}
// HAC PoW: RandomX hash becomes nSolution, then SHA256D(header + solution) must be < target
if (!checkPow(nonce32, result.result())) {
return -1;
}
LOG_INFO("%s " CYAN_BOLD("submitting block at height %u"), tag(), m_height);
// Build the full block with header + nonce + solution + transactions
const std::string blockHex = serializeBlockHex(nonce32, 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 chain-specific initial RandomX key.
// Daemon computes: sprintf("%08x%s%08x", ASSETCHAINS_MAGIC, SYMBOL, RPCPORT)
// and passes the raw ASCII bytes to randomx_init_cache().
// We hex-encode those bytes (zero-padded to 32) for setSeedHash().
char initialKey[81];
const uint32_t magic = 2387029918u; // DRAGONX ASSETCHAINS_MAGIC
const char* symbol = "DRAGONX";
const uint16_t rpcPort = 21769;
snprintf(initialKey, sizeof(initialKey), "%08x%s%08x", magic, symbol, (uint32_t)rpcPort);
const size_t keyLen = strlen(initialKey);
// Hex-encode only the actual key bytes (no zero-padding).
// The daemon passes strlen(initialKey) bytes to randomx_init_cache(),
// so we must match that exact length.
char hexBuf[163]; // max 81 bytes * 2 + 1
for (size_t i = 0; i < keyLen; i++) snprintf(hexBuf + i*2, 3, "%02x", (uint8_t)initialKey[i]);
hexBuf[keyLen * 2] = '\0';
m_keyBlockHash = hexBuf;
m_keyHeight = 0;
LOG_INFO("%s " CYAN("using initial RandomX key \"%s\" for height %u"), tag(), initialKey, 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);
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 108-byte header for RandomX hashing (nonce bytes 108-139 filled by worker):
// version(4) + prevHash(32) + merkleRoot(32) + saplingRoot(32) + time(4) + bits(4) = 108
uint8_t header[108];
size_t offset = 0;
// nVersion (4 bytes, little-endian)
const uint32_t ver = static_cast<uint32_t>(m_version);
memcpy(header + offset, &ver, 4);
offset += 4;
// hashPrevBlock (32 bytes, internal byte order = reversed from display)
{
std::vector<uint8_t> tmp(32);
Cvt::fromHex(tmp.data(), 32, m_prevHash.data(), 64);
std::reverse(tmp.begin(), tmp.end());
memcpy(header + offset, tmp.data(), 32);
// Also store for block submission
memcpy(m_headerPrevHash, tmp.data(), 32);
}
offset += 32;
// hashMerkleRoot (32 bytes, reversed)
{
std::vector<uint8_t> tmp(32);
Cvt::fromHex(tmp.data(), 32, m_merkleRoot.data(), 64);
std::reverse(tmp.begin(), tmp.end());
memcpy(header + offset, tmp.data(), 32);
memcpy(m_headerMerkleRoot, tmp.data(), 32);
}
offset += 32;
// hashFinalSaplingRoot (32 bytes, reversed)
{
std::vector<uint8_t> tmp(32);
Cvt::fromHex(tmp.data(), 32, m_saplingRoot.data(), 64);
std::reverse(tmp.begin(), tmp.end());
memcpy(header + offset, tmp.data(), 32);
memcpy(m_headerSaplingRoot, tmp.data(), 32);
}
offset += 32;
// nTime (4 bytes, little-endian)
const uint32_t time32 = static_cast<uint32_t>(m_curtime);
memcpy(header + offset, &time32, 4);
m_headerTime = time32;
offset += 4;
// nBits (4 bytes) - parse from big-endian hex, store as little-endian uint32
uint32_t bits = 0;
if (m_bits.size() == 8) {
bits = static_cast<uint32_t>(strtoul(m_bits.data(), nullptr, 16));
}
memcpy(header + offset, &bits, 4);
m_headerBits = bits;
offset += 4;
// Set the 108-byte header directly into the job blob (worker adds 32-byte nonce at offset 108)
job.setHushHeader(header);
// Compute 64-bit target from compact "bits" field (Bitcoin/Zcash format)
// Top byte = exponent, lower 3 bytes = mantissa
// Full 256-bit target = mantissa * 2^(8*(exponent-3))
// xmrig compares: (uint64_t)hash[24..31] < target64
{
int exponent = (bits >> 24) & 0xff;
uint32_t mantissa = bits & 0x007fffff;
int mantissaPos = exponent - 3; // Byte position of mantissa's low byte
uint64_t target64 = 0;
if (mantissaPos >= 32) {
target64 = 0xFFFFFFFFFFFFFFFFULL;
} else if (mantissaPos >= 24) {
int shift = (mantissaPos - 24) * 8;
target64 = (uint64_t)mantissa << shift;
if (mantissaPos > 29) {
target64 = 0xFFFFFFFFFFFFFFFFULL;
}
} else {
// Mantissa is entirely below byte 24 - extremely high difficulty
target64 = 0;
}
job.setTarget64(target64);
}
// Mark as solo mining so workers use 32-byte SoloNonce
job.setSoloMining(true);
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") " bits: " CYAN_BOLD("0x%08x") " diff: " CYAN_BOLD("%" PRIu64),
tag(), m_height, m_headerBits, m_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()) {
// getblockhash returns display-order (big-endian) hex, but the daemon
// passes uint256 internal bytes (little-endian) to randomx_init_cache().
// Reverse the bytes so setSeedHash() produces the correct LE byte order.
const char* displayHash = result.GetString();
std::vector<uint8_t> hashBytes(32);
Cvt::fromHex(hashBytes.data(), 32, displayHash, 64);
std::reverse(hashBytes.begin(), hashBytes.end());
char reversedHex[65];
for (size_t i = 0; i < 32; i++) snprintf(reversedHex + i*2, 3, "%02x", hashBytes[i]);
reversedHex[64] = '\0';
m_keyBlockHash = reversedHex;
m_keyHeight = m_pendingKeyHeight;
LOG_INFO("%s " GREEN("RandomX key block %u: %.16s... (reversed to LE)"),
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(const uint8_t* nonce32, const uint8_t* solution) const
{
std::vector<uint8_t> blob;
blob.reserve(1024);
// Header (140 bytes) using cached binary fields
// Version (4 bytes LE)
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);
// Previous hash (32 bytes - already in internal order)
blob.insert(blob.end(), m_headerPrevHash, m_headerPrevHash + 32);
// Merkle root (32 bytes)
blob.insert(blob.end(), m_headerMerkleRoot, m_headerMerkleRoot + 32);
// Sapling root (32 bytes)
blob.insert(blob.end(), m_headerSaplingRoot, m_headerSaplingRoot + 32);
// Time (4 bytes LE)
blob.insert(blob.end(), reinterpret_cast<const uint8_t*>(&m_headerTime),
reinterpret_cast<const uint8_t*>(&m_headerTime) + 4);
// Bits (4 bytes LE)
blob.insert(blob.end(), reinterpret_cast<const uint8_t*>(&m_headerBits),
reinterpret_cast<const uint8_t*>(&m_headerBits) + 4);
// Nonce (32 bytes from the mining result)
blob.insert(blob.end(), nonce32, nonce32 + 32);
// Solution (the RandomX hash - 32 bytes with CompactSize prefix)
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(const uint8_t* nonce32, const uint8_t* solution) const
{
std::vector<uint8_t> header;
header.reserve(140 + 1 + 32); // header + compactsize + solution
// Version (4 bytes LE)
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);
// Use cached binary header fields (already in internal byte order)
header.insert(header.end(), m_headerPrevHash, m_headerPrevHash + 32);
header.insert(header.end(), m_headerMerkleRoot, m_headerMerkleRoot + 32);
header.insert(header.end(), m_headerSaplingRoot, m_headerSaplingRoot + 32);
// Time (4 bytes LE)
header.insert(header.end(), reinterpret_cast<const uint8_t*>(&m_headerTime),
reinterpret_cast<const uint8_t*>(&m_headerTime) + 4);
// Bits (4 bytes LE)
header.insert(header.end(), reinterpret_cast<const uint8_t*>(&m_headerBits),
reinterpret_cast<const uint8_t*>(&m_headerBits) + 4);
// Nonce (32 bytes)
header.insert(header.end(), nonce32, nonce32 + 32);
// nSolution (compactsize + 32 bytes)
header.push_back(32); // CompactSize for 32 bytes
header.insert(header.end(), solution, solution + 32);
return header;
}
bool HushClient::checkPow(const uint8_t* nonce32, const uint8_t* solution) const
{
// Build header with solution
const std::vector<uint8_t> header = serializeHeader(nonce32, 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