feat(mining): xmrig updater service — fetch/verify/install the latest miner from Gitea

Adds util/XmrigUpdater: a background-thread service (mirrors util/Bootstrap) that pulls
the latest DRG-XMRig release from the project's Gitea, verifies it, and installs the miner
binary into the daemon directory. Service layer only; the mining-tab UI hook comes next.

Flow: GET /api/v1/repos/DragonX/drg-xmrig/releases/latest -> pick the asset matching this
platform (…-linux-x64.zip / …-win-x64.zip; no macOS build -> graceful "unavailable") ->
download (libcurl, TLS verified) -> verify the archive SHA-256 -> extract with miniz,
flattening the versioned subdir the archive nests the binary in -> verify the extracted
binary's SHA-256 in memory before writing it -> atomic install (+chmod +x on POSIX). On
Windows also extracts WinRing0x64.sys; config.json/README.md are skipped.

Security (download-and-execute): TLS is verified, and BOTH the archive and the inner binary
are checked against the SHA-256 checksums published in the release body (parsed as
"<hex>  <name>" lines) — install is refused on a missing or mismatched checksum.

Split into a pure core (xmrig_updater_core.cpp: release parse, asset/platform match, checksum
parse, SHA-256) and the curl/miniz worker (xmrig_updater.cpp). The core is unit-tested against
a real captured release fixture (tests/fixtures/xmrig/release_latest.json); an env-gated
(DRAGONX_TEST_NETWORK=1) integration test exercises the worker live and was verified end-to-end
on linux-x64 (inner binary SHA-256 matches the published value). Both variants build; suite passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-06 18:07:46 -05:00
parent f5561c0dac
commit 946958b591
6 changed files with 745 additions and 0 deletions

View File

@@ -0,0 +1,148 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
//
// Pure (no-I/O) core of the xmrig updater: release-JSON parsing, platform/asset matching, checksum
// parsing, and SHA-256 hashing. Split from xmrig_updater.cpp (the libcurl/miniz worker) so it can
// be unit-tested in the test binary, which links libsodium + nlohmann/json but not curl/miniz.
#include "xmrig_updater.h"
#include <nlohmann/json.hpp>
#include <sodium.h>
#include <algorithm>
#include <cctype>
#include <sstream>
using json = nlohmann::json;
namespace dragonx {
namespace util {
namespace {
std::string toLower(std::string s)
{
std::transform(s.begin(), s.end(), s.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
return s;
}
bool isHex64(const std::string& s)
{
if (s.size() != 64) return false;
for (unsigned char c : s)
if (!std::isxdigit(c)) return false;
return true;
}
} // namespace
XmrigRelease parseXmrigRelease(const std::string& jsonStr)
{
XmrigRelease r;
try {
const json j = json::parse(jsonStr);
if (j.contains("tag_name") && j["tag_name"].is_string())
r.tag = j["tag_name"].get<std::string>();
if (j.contains("body") && j["body"].is_string())
r.body = j["body"].get<std::string>();
if (j.contains("assets") && j["assets"].is_array()) {
for (const auto& a : j["assets"]) {
if (!a.is_object()) continue;
XmrigReleaseAsset asset;
if (a.contains("name") && a["name"].is_string())
asset.name = a["name"].get<std::string>();
if (a.contains("browser_download_url") && a["browser_download_url"].is_string())
asset.downloadUrl = a["browser_download_url"].get<std::string>();
if (a.contains("size") && a["size"].is_number_integer())
asset.size = a["size"].get<long long>();
if (!asset.name.empty() && !asset.downloadUrl.empty())
r.assets.push_back(std::move(asset));
}
}
if (r.tag.empty()) { r.error = "release JSON has no tag_name"; return r; }
r.ok = true;
} catch (const std::exception& e) {
r.error = std::string("failed to parse release JSON: ") + e.what();
}
return r;
}
std::string currentXmrigPlatformToken()
{
#if defined(_WIN32)
return "win-x64";
#elif defined(__APPLE__)
#if defined(__aarch64__) || defined(__arm64__)
return "macos-arm64";
#else
return "macos-x64";
#endif
#elif defined(__linux__)
#if defined(__aarch64__)
return "linux-arm64";
#else
return "linux-x64";
#endif
#else
return "";
#endif
}
int selectXmrigAsset(const XmrigRelease& release, const std::string& platformToken)
{
if (platformToken.empty()) return -1;
const std::string needle = "-" + toLower(platformToken) + ".zip";
for (std::size_t i = 0; i < release.assets.size(); ++i) {
const std::string n = toLower(release.assets[i].name);
if (n.size() >= needle.size() &&
n.compare(n.size() - needle.size(), needle.size(), needle) == 0)
return static_cast<int>(i);
}
return -1;
}
std::map<std::string, std::string> parseXmrigChecksums(const std::string& body)
{
std::map<std::string, std::string> out;
std::istringstream in(body);
std::string line;
while (std::getline(in, line)) {
std::istringstream ls(line);
std::string hash, name;
if (!(ls >> hash)) continue;
if (!isHex64(hash)) continue;
if (!(ls >> name)) continue;
// First whitespace-delimited token after the hash is the key (zip name, or "xmrig"/
// "xmrig.exe" for the inner-binary block; any trailing "(linux-x64)" annotation is ignored).
out[name] = toLower(hash);
}
return out;
}
std::string sha256Hex(const void* data, std::size_t len)
{
unsigned char hash[crypto_hash_sha256_BYTES];
if (crypto_hash_sha256(hash, static_cast<const unsigned char*>(data), len) != 0)
return {};
static const char* kHex = "0123456789abcdef";
std::string out;
out.reserve(crypto_hash_sha256_BYTES * 2);
for (unsigned char c : hash) {
out.push_back(kHex[c >> 4]);
out.push_back(kHex[c & 0x0F]);
}
return out;
}
std::vector<std::string> xmrigExtractBasenames(const std::string& platformToken)
{
if (platformToken.rfind("win", 0) == 0)
return {"xmrig.exe", "WinRing0x64.sys"};
return {"xmrig"};
}
} // namespace util
} // namespace dragonx