/* 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 #ifdef XMRIG_FEATURE_TLS # include #else // Standalone SHA-256 for non-TLS builds (Windows cross-compile) # ifdef _WIN32 # include # include 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(data), static_cast(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 ¶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; } // 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(m_version); memcpy(header + offset, &ver, 4); offset += 4; // hashPrevBlock (32 bytes, internal byte order = reversed from display) { std::vector 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 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 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(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(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 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(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(const uint8_t* nonce32, const uint8_t* solution) const { std::vector blob; blob.reserve(1024); // Header (140 bytes) using cached binary fields // Version (4 bytes LE) const uint32_t ver = static_cast(m_version); blob.insert(blob.end(), reinterpret_cast(&ver), reinterpret_cast(&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(&m_headerTime), reinterpret_cast(&m_headerTime) + 4); // Bits (4 bytes LE) blob.insert(blob.end(), reinterpret_cast(&m_headerBits), reinterpret_cast(&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(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(const uint8_t* nonce32, const uint8_t* solution) const { std::vector header; header.reserve(140 + 1 + 32); // header + compactsize + solution // Version (4 bytes LE) const uint32_t ver = static_cast(m_version); header.insert(header.end(), reinterpret_cast(&ver), reinterpret_cast(&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(&m_headerTime), reinterpret_cast(&m_headerTime) + 4); // Bits (4 bytes LE) header.insert(header.end(), reinterpret_cast(&m_headerBits), reinterpret_cast(&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 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