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

@@ -467,6 +467,8 @@ set(APP_SOURCES
src/daemon/lifecycle_adapters.cpp
src/daemon/xmrig_manager.cpp
src/util/bootstrap.cpp
src/util/xmrig_updater.cpp
src/util/xmrig_updater_core.cpp
src/util/secure_vault.cpp
src/ui/effects/framebuffer.cpp
src/ui/effects/blur_shader.cpp
@@ -969,6 +971,8 @@ if(BUILD_TESTING)
src/util/secure_vault.cpp
src/util/platform.cpp
src/util/logger.cpp
src/util/xmrig_updater.cpp
src/util/xmrig_updater_core.cpp
${MINIZ_SOURCES}
)
@@ -979,12 +983,15 @@ if(BUILD_TESTING)
${CMAKE_BINARY_DIR}/generated
${IMGUI_DIR}
${SODIUM_INCLUDE_DIR}
${CURL_INCLUDE_DIRS}
${MINIZ_DIR}
)
target_link_libraries(ObsidianDragonTests PRIVATE
nlohmann_json::nlohmann_json
sqlite3_amalgamation
${SODIUM_LIBRARY}
${CURL_LIBRARIES}
)
target_compile_definitions(ObsidianDragonTests PRIVATE

325
src/util/xmrig_updater.cpp Normal file
View File

@@ -0,0 +1,325 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
//
// XmrigUpdater background worker (libcurl download + miniz extract). The pure, no-I/O helpers it
// calls (release parsing, asset/platform matching, checksum parsing, SHA-256) live in
// xmrig_updater_core.cpp so they can be unit-tested without curl/miniz.
#include "xmrig_updater.h"
#include "logger.h"
#include <curl/curl.h>
#include <miniz.h>
#include <algorithm>
#include <cstdio>
#include <filesystem>
#include <fstream>
#include <iterator>
namespace fs = std::filesystem;
namespace dragonx {
namespace util {
namespace {
size_t writeStringCb(void* contents, size_t size, size_t nmemb, void* userp)
{
static_cast<std::string*>(userp)->append(static_cast<char*>(contents), size * nmemb);
return size * nmemb;
}
size_t writeFileCb(void* contents, size_t size, size_t nmemb, void* userp)
{
return std::fwrite(contents, size, nmemb, static_cast<FILE*>(userp));
}
std::string baseName(const std::string& path)
{
const auto slash = path.find_last_of("/\\");
return slash == std::string::npos ? path : path.substr(slash + 1);
}
} // namespace
XmrigUpdater::~XmrigUpdater()
{
cancel_requested_ = true;
if (worker_.joinable()) worker_.join();
}
void XmrigUpdater::setProgress(State state, const std::string& text, double done, double total)
{
std::lock_guard<std::mutex> lk(mutex_);
progress_.state = state;
progress_.status_text = text;
if (done > 0) progress_.downloaded_bytes = done;
if (total > 0) progress_.total_bytes = total;
progress_.percent = (progress_.total_bytes > 0)
? static_cast<float>(100.0 * progress_.downloaded_bytes / progress_.total_bytes)
: progress_.percent;
if (state == State::Failed) progress_.error = text;
}
XmrigUpdater::Progress XmrigUpdater::getProgress() const
{
std::lock_guard<std::mutex> lk(mutex_);
return progress_;
}
bool XmrigUpdater::isDone() const
{
std::lock_guard<std::mutex> lk(mutex_);
return progress_.state == State::Done || progress_.state == State::Failed;
}
void XmrigUpdater::cancel()
{
cancel_requested_ = true;
}
std::string XmrigUpdater::httpGet(const std::string& url)
{
CURL* curl = curl_easy_init();
if (!curl) return {};
std::string result;
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeStringCb);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &result);
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(curl, CURLOPT_USERAGENT, "ObsidianDragon/1.0");
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L);
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 15L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);
const CURLcode res = curl_easy_perform(curl);
long httpCode = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpCode);
curl_easy_cleanup(curl);
if (res != CURLE_OK || httpCode < 200 || httpCode >= 300) {
DEBUG_LOGF("[xmrig-updater] GET %s failed: %s (HTTP %ld)\n",
url.c_str(), curl_easy_strerror(res), httpCode);
return {};
}
return result;
}
bool XmrigUpdater::downloadToFile(const std::string& url, const std::string& destPath)
{
FILE* fp = std::fopen(destPath.c_str(), "wb");
if (!fp) return false;
CURL* curl = curl_easy_init();
if (!curl) { std::fclose(fp); return false; }
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeFileCb);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp);
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(curl, CURLOPT_USERAGENT, "ObsidianDragon/1.0");
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 0L);
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 30L);
curl_easy_setopt(curl, CURLOPT_LOW_SPEED_LIMIT, 1024L);
curl_easy_setopt(curl, CURLOPT_LOW_SPEED_TIME, 60L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);
const CURLcode res = curl_easy_perform(curl);
long httpCode = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpCode);
curl_easy_cleanup(curl);
std::fclose(fp);
if (res != CURLE_OK || httpCode < 200 || httpCode >= 300) {
DEBUG_LOGF("[xmrig-updater] download %s failed: %s (HTTP %ld)\n",
url.c_str(), curl_easy_strerror(res), httpCode);
std::error_code ec; fs::remove(destPath, ec);
return false;
}
return true;
}
void XmrigUpdater::startCheck(const std::string& installedTag)
{
if (worker_running_.exchange(true)) return;
cancel_requested_ = false;
if (worker_.joinable()) worker_.join();
{
std::lock_guard<std::mutex> lk(mutex_);
progress_ = Progress{};
progress_.installed_tag = installedTag;
progress_.state = State::Checking;
progress_.status_text = "Checking for the latest miner…";
}
worker_ = std::thread([this, installedTag] { runCheck(installedTag); worker_running_ = false; });
}
void XmrigUpdater::startInstall(const std::string& targetDir)
{
if (worker_running_.exchange(true)) return;
cancel_requested_ = false;
if (worker_.joinable()) worker_.join();
{
std::lock_guard<std::mutex> lk(mutex_);
progress_.state = State::Downloading;
progress_.error.clear();
progress_.status_text = "Preparing…";
}
worker_ = std::thread([this, targetDir] { runInstall(targetDir); worker_running_ = false; });
}
void XmrigUpdater::runCheck(std::string installedTag)
{
const std::string body = httpGet(kApiUrl);
if (body.empty()) { setProgress(State::Failed, "Could not reach the update server."); return; }
const XmrigRelease rel = parseXmrigRelease(body);
if (!rel.ok) { setProgress(State::Failed, rel.error.empty() ? "Invalid release data." : rel.error); return; }
const std::string token = currentXmrigPlatformToken();
const int idx = selectXmrigAsset(rel, token);
{
std::lock_guard<std::mutex> lk(mutex_);
progress_.latest_tag = rel.tag;
progress_.installed_tag = installedTag;
}
if (idx < 0) {
setProgress(State::Failed, "No miner build is available for this platform (" +
(token.empty() ? "unknown" : token) + ").");
return;
}
const bool updateAvailable = installedTag.empty() || installedTag != rel.tag;
{
std::lock_guard<std::mutex> lk(mutex_);
progress_.update_available = updateAvailable;
}
if (updateAvailable)
setProgress(State::UpdateAvailable, "A new miner is available (" + rel.tag + ").");
else
setProgress(State::UpToDate, "The miner is up to date (" + rel.tag + ").");
}
void XmrigUpdater::runInstall(std::string targetDir)
{
setProgress(State::Checking, "Checking for the latest miner…");
const std::string apiBody = httpGet(kApiUrl);
if (apiBody.empty()) { setProgress(State::Failed, "Could not reach the update server."); return; }
const XmrigRelease rel = parseXmrigRelease(apiBody);
if (!rel.ok) { setProgress(State::Failed, rel.error.empty() ? "Invalid release data." : rel.error); return; }
const std::string token = currentXmrigPlatformToken();
const int idx = selectXmrigAsset(rel, token);
if (idx < 0) {
setProgress(State::Failed, "No miner build is available for this platform (" +
(token.empty() ? "unknown" : token) + ").");
return;
}
const XmrigReleaseAsset& asset = rel.assets[idx];
const auto checksums = parseXmrigChecksums(rel.body);
{
std::lock_guard<std::mutex> lk(mutex_);
progress_.latest_tag = rel.tag;
}
if (cancel_requested_) { setProgress(State::Failed, "Cancelled."); return; }
std::error_code ec;
fs::create_directories(targetDir, ec);
// 1. Download the archive.
const std::string zipPath = (fs::path(targetDir) / ".drg-xmrig-download.zip").string();
setProgress(State::Downloading, "Downloading " + asset.name + "", 0,
static_cast<double>(asset.size));
if (!downloadToFile(asset.downloadUrl, zipPath)) {
setProgress(State::Failed, "Download failed.");
return;
}
if (cancel_requested_) { fs::remove(zipPath, ec); setProgress(State::Failed, "Cancelled."); return; }
// 2. Verify the archive SHA-256 against the published checksum (refuse on missing/mismatch).
setProgress(State::Verifying, "Verifying download…");
{
std::ifstream f(zipPath, std::ios::binary);
const std::string bytes((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
const std::string actual = sha256Hex(bytes.data(), bytes.size());
const auto it = checksums.find(asset.name);
if (it == checksums.end()) {
fs::remove(zipPath, ec);
setProgress(State::Failed, "No published checksum for " + asset.name + " — refusing to install.");
return;
}
if (actual != it->second) {
fs::remove(zipPath, ec);
setProgress(State::Failed, "Archive checksum mismatch — refusing to install (possible tampering).");
return;
}
}
// 3. Extract the wanted binaries (flatten the versioned subdir), verifying the miner binary's
// SHA-256 in memory before writing the executable to disk.
setProgress(State::Extracting, "Installing miner…");
const std::vector<std::string> wanted = xmrigExtractBasenames(token);
const std::string minerName = wanted.front(); // "xmrig" / "xmrig.exe"
mz_zip_archive zip{};
if (!mz_zip_reader_init_file(&zip, zipPath.c_str(), 0)) {
fs::remove(zipPath, ec);
setProgress(State::Failed, "Could not open the downloaded archive.");
return;
}
bool minerInstalled = false;
bool failed = false;
const int numFiles = static_cast<int>(mz_zip_reader_get_num_files(&zip));
for (int i = 0; i < numFiles && !failed; ++i) {
mz_zip_archive_file_stat st;
if (!mz_zip_reader_file_stat(&zip, i, &st)) continue;
if (mz_zip_reader_is_file_a_directory(&zip, i)) continue;
const std::string base = baseName(st.m_filename);
if (std::find(wanted.begin(), wanted.end(), base) == wanted.end()) continue; // skip config/readme
size_t outSize = 0;
void* mem = mz_zip_reader_extract_to_heap(&zip, i, &outSize, 0);
if (!mem) { failed = true; break; }
// For the miner binary, verify the inner-binary SHA-256 before writing the executable.
if (base == minerName) {
const std::string actual = sha256Hex(mem, outSize);
const auto it = checksums.find(minerName);
if (it == checksums.end() || actual != it->second) {
mz_free(mem);
failed = true;
break;
}
}
// Atomic write: temp file in target dir, then rename over the destination.
const std::string finalPath = (fs::path(targetDir) / base).string();
const std::string tmpPath = finalPath + ".tmp";
{
std::ofstream of(tmpPath, std::ios::binary | std::ios::trunc);
if (!of) { mz_free(mem); failed = true; break; }
of.write(static_cast<const char*>(mem), static_cast<std::streamsize>(outSize));
}
mz_free(mem);
fs::remove(finalPath, ec); // Windows rename won't clobber an existing file
fs::rename(tmpPath, finalPath, ec);
if (ec) { fs::remove(tmpPath, ec); failed = true; break; }
#ifndef _WIN32
if (base == minerName) {
fs::permissions(finalPath,
fs::perms::owner_all | fs::perms::group_read | fs::perms::group_exec |
fs::perms::others_read | fs::perms::others_exec,
fs::perm_options::replace, ec);
}
#endif
if (base == minerName) minerInstalled = true;
}
mz_zip_reader_end(&zip);
fs::remove(zipPath, ec);
if (failed) { setProgress(State::Failed, "Could not verify/install the miner binary."); return; }
if (!minerInstalled) { setProgress(State::Failed, "Miner binary not found in the archive."); return; }
setProgress(State::Done, "Miner installed (" + rel.tag + ").");
}
} // namespace util
} // namespace dragonx

137
src/util/xmrig_updater.h Normal file
View File

@@ -0,0 +1,137 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
//
// XmrigUpdater — fetch + verify + install the latest DRG-XMRig miner from the DragonX Gitea.
//
// Flow (mirrors util/Bootstrap): query the Gitea releases API for the latest release, pick the
// asset matching this platform (…-linux-x64.zip / …-win-x64.zip), download it, verify its
// published SHA-256, then extract the miner binary (flattening the versioned subdir the archive
// nests it in) into the target directory and verify the extracted binary's SHA-256 before it is
// made executable. All network/disk work runs on a background thread; progress is polled
// thread-safely from the UI thread.
//
// Security: download-and-execute, so verification is mandatory — TLS is verified (libcurl
// defaults), the host is the project's own Gitea over HTTPS, and BOTH the archive and the inner
// binary are checked against the SHA-256 checksums published in the release body. The checksums
// live in the release body markdown (no SHA256SUMS asset), parsed as "<hex> <name>" lines.
#pragma once
#include <atomic>
#include <cstddef>
#include <map>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
namespace dragonx {
namespace util {
struct XmrigReleaseAsset {
std::string name;
std::string downloadUrl;
long long size = 0;
};
struct XmrigRelease {
bool ok = false;
std::string tag; // e.g. "v1.0.0"
std::string body; // release notes markdown (holds the checksum blocks)
std::vector<XmrigReleaseAsset> assets;
std::string error;
};
// ── Pure helpers (no I/O; unit-tested) ───────────────────────────────────────
// Parse the Gitea GET /releases/latest JSON into an XmrigRelease (ok=false + error on failure).
XmrigRelease parseXmrigRelease(const std::string& json);
// The asset-name token for the host platform: "linux-x64", "win-x64", "macos-x64",
// "macos-arm64", or "" if unknown/unsupported.
std::string currentXmrigPlatformToken();
// Index of the asset whose name matches the platform token (e.g. ends with "-linux-x64.zip"),
// or -1 if none (e.g. no macOS build is published).
int selectXmrigAsset(const XmrigRelease& release, const std::string& platformToken);
// Parse "<sha256hex> <name> …" lines from the release body into { name -> lowercase-hex }.
// Keys are the first whitespace-delimited token after the hash, so this captures both the archive
// checksums (keyed by zip filename) and the inner-binary checksums (keyed by "xmrig"/"xmrig.exe").
std::map<std::string, std::string> parseXmrigChecksums(const std::string& body);
// Lowercase-hex SHA-256 of a buffer (libsodium). Empty string on failure.
std::string sha256Hex(const void* data, std::size_t len);
// The binary file basenames to extract for a platform: {"xmrig"} on POSIX,
// {"xmrig.exe", "WinRing0x64.sys"} on Windows. First entry is always the miner binary.
std::vector<std::string> xmrigExtractBasenames(const std::string& platformToken);
// ── Background worker ────────────────────────────────────────────────────────
class XmrigUpdater {
public:
enum class State {
Idle,
Checking,
UpToDate,
UpdateAvailable,
Downloading,
Verifying,
Extracting,
Done,
Failed
};
struct Progress {
State state = State::Idle;
double downloaded_bytes = 0;
double total_bytes = 0;
float percent = 0.0f;
std::string status_text;
std::string error; // non-empty on Failed
std::string latest_tag; // tag reported by the API (once checked)
std::string installed_tag; // caller-supplied current install (for update detection)
bool update_available = false;
};
// Gitea releases API for the DRG-XMRig fork.
static constexpr const char* kApiUrl =
"https://git.dragonx.is/api/v1/repos/DragonX/drg-xmrig/releases/latest";
XmrigUpdater() = default;
~XmrigUpdater();
XmrigUpdater(const XmrigUpdater&) = delete;
XmrigUpdater& operator=(const XmrigUpdater&) = delete;
// Query the latest release on a background thread. `installedTag` (may be empty/unknown) is
// compared to the API tag to set Progress.update_available. End state: UpToDate / UpdateAvailable
// / Failed.
void startCheck(const std::string& installedTag);
// Download → verify archive → extract (flatten) → verify binary → install into `targetDir` on a
// background thread. Re-fetches the release so it is self-contained. End state: Done / Failed.
// On Done, getProgress().latest_tag is the version that should be persisted as the installed tag.
void startInstall(const std::string& targetDir);
void cancel();
Progress getProgress() const;
bool isDone() const; // true when state is Done or Failed (worker finished)
private:
void runCheck(std::string installedTag);
void runInstall(std::string targetDir);
void setProgress(State state, const std::string& text, double done = 0, double total = 0);
bool downloadToFile(const std::string& url, const std::string& destPath);
std::string httpGet(const std::string& url);
mutable std::mutex mutex_;
Progress progress_;
std::atomic<bool> cancel_requested_{false};
std::atomic<bool> worker_running_{false};
std::thread worker_;
};
} // namespace util
} // namespace dragonx

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

View File

@@ -0,0 +1 @@
{"id":26,"tag_name":"v1.0.0","target_commitish":"master","name":"DRG-XMRig v1.0.0","body":"# DRG-XMRig 6.25.1-drg1 — Release Notes\r\n\r\n**Release date:** 2026-06-06\r\n**Source commit:** `8f711d7c`\r\n**Base:** XMRig 6.25.1 (drg fork — unified pow-hash share model, DragonX/`rx/dragonx` primary algo)\r\n\r\n## About this build\r\n\r\nDRG-XMRig is a fork of xmrig-hac with a unified pow-hash share model. This\r\nrelease makes `rx/dragonx` the primary algorithm name (with `rx/hush` retained\r\nas an alias).\r\n\r\n## Artifacts\r\n\r\n| Platform | File | Size |\r\n|----------|------|------|\r\n| Linux x86_64 | `drg-xmrig-6.25.1-drg1-linux-x64.zip` | 3.9 MiB |\r\n| Windows x86_64 | `drg-xmrig-6.25.1-drg1-win-x64.zip` | 2.7 MiB |\r\n\r\nEach archive contains the miner binary, a starter `config.json`, and `README.md`.\r\nThe Windows archive additionally includes `WinRing0x64.sys` (MSR driver required\r\nfor MSR mods and large-page tweaks).\r\n\r\n## Build configuration\r\n\r\nBoth binaries are **statically linked** release builds (`-DBUILD_STATIC=ON`,\r\n`CMAKE_BUILD_TYPE=Release`) with GPU backends disabled (CPU-only):\r\n\r\n- `WITH_OPENCL=OFF`, `WITH_CUDA=OFF`\r\n- Linux: built with GCC 11.4.0; `WITH_HWLOC=ON`\r\n- Windows: cross-compiled with MinGW-w64; `WITH_HWLOC=OFF`\r\n\r\nBundled dependencies:\r\n\r\n| Dependency | Linux | Windows |\r\n|------------|-------|---------|\r\n| libuv | 1.51.0 | 1.51.0 |\r\n| OpenSSL | 3.0.16 | 1.1.1w |\r\n| hwloc | 2.12.1 | — (disabled) |\r\n\r\n## Checksums (SHA-256)\r\n\r\n### Release archives\r\n\r\n```\r\nc121a078ee46943584aa6148cac26583409c832d28668cf38ba908e10214c9a6 drg-xmrig-6.25.1-drg1-linux-x64.zip\r\n225bd33f5d40af706800fe8e2242222beead65aed14126ed776c32e82119fa1e drg-xmrig-6.25.1-drg1-win-x64.zip\r\n```\r\n\r\n### Binaries (inside the archives)\r\n\r\n```\r\n37c178f743c269c1d9e18302cead0ed117ded2b5fe30910e836896b4abc20e57 xmrig (linux-x64)\r\n01223711eddea347eee394c4b6d265b9a3e5c13fe93204bc041c4399c9c758f8 xmrig.exe (win-x64)\r\n```\r\n\r\n### Verify\r\n\r\n```bash\r\n# Linux\r\nsha256sum -c \u003c\u003c'EOF'\r\nc121a078ee46943584aa6148cac26583409c832d28668cf38ba908e10214c9a6 drg-xmrig-6.25.1-drg1-linux-x64.zip\r\n225bd33f5d40af706800fe8e2242222beead65aed14126ed776c32e82119fa1e drg-xmrig-6.25.1-drg1-win-x64.zip\r\nEOF\r\n```\r\n\r\n```powershell\r\n# Windows (PowerShell)\r\nGet-FileHash .\\drg-xmrig-6.25.1-drg1-win-x64.zip -Algorithm SHA256\r\n```\r\n","url":"https://git.dragonx.is/api/v1/repos/DragonX/drg-xmrig/releases/26","html_url":"https://git.dragonx.is/DragonX/drg-xmrig/releases/tag/v1.0.0","tarball_url":"https://git.dragonx.is/DragonX/drg-xmrig/archive/v1.0.0.tar.gz","zipball_url":"https://git.dragonx.is/DragonX/drg-xmrig/archive/v1.0.0.zip","upload_url":"https://git.dragonx.is/api/v1/repos/DragonX/drg-xmrig/releases/26/assets","draft":false,"prerelease":false,"created_at":"2026-06-06T17:32:05-05:00","published_at":"2026-06-06T17:32:05-05:00","author":{"id":1,"login":"DanS","login_name":"","source_id":0,"full_name":"","email":"dans@noreply.localhost","avatar_url":"https://git.dragonx.is/avatars/ac3f843e96162174082a0b0e2b02b4d894ea793139c2e181e5971483e233a946","html_url":"https://git.dragonx.is/DanS","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2026-02-27T13:12:08-06:00","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":1,"username":"DanS"},"assets":[{"id":99,"name":"drg-xmrig-6.25.1-drg1-linux-x64.zip","size":4016958,"download_count":0,"created_at":"2026-06-06T17:49:10-05:00","uuid":"33e950e4-82e2-442f-a6a0-99fcdfad9f9f","browser_download_url":"https://git.dragonx.is/DragonX/drg-xmrig/releases/download/v1.0.0/drg-xmrig-6.25.1-drg1-linux-x64.zip"},{"id":98,"name":"drg-xmrig-6.25.1-drg1-win-x64.zip","size":2843361,"download_count":0,"created_at":"2026-06-06T17:49:09-05:00","uuid":"82f5bd5a-81d5-45b1-8e72-981eab5e9516","browser_download_url":"https://git.dragonx.is/DragonX/drg-xmrig/releases/download/v1.0.0/drg-xmrig-6.25.1-drg1-win-x64.zip"}]}

View File

@@ -20,6 +20,7 @@
#include "ui/windows/mining_tab_helpers.h"
#include "util/amount_format.h"
#include "util/payment_uri.h"
#include "util/xmrig_updater.h"
#include "wallet/lite_owned_string.h"
#include "wallet/lite_rollout_policy.h"
#include "wallet/lite_wallet_controller.h"
@@ -3959,6 +3960,127 @@ void testLiteWalletControllerRolloutGate()
}
}
// ── xmrig updater pure core (util/xmrig_updater.h) — driven by the real release fixture ──
static std::string readXmrigFixture()
{
const std::string path = std::string(DRAGONX_TEST_FIXTURE_DIR) + "/xmrig/release_latest.json";
std::ifstream f(path, std::ios::binary);
return std::string((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
}
void testXmrigReleaseParsing()
{
using namespace dragonx::util;
const auto rel = parseXmrigRelease(readXmrigFixture());
EXPECT_TRUE(rel.ok);
EXPECT_EQ(rel.tag, std::string("v1.0.0"));
EXPECT_EQ(rel.assets.size(), static_cast<std::size_t>(2));
// Both assets carry a name + a usable download URL.
for (const auto& a : rel.assets) {
EXPECT_TRUE(!a.name.empty());
EXPECT_TRUE(a.downloadUrl.rfind("https://", 0) == 0);
EXPECT_TRUE(a.size > 0);
}
// Garbage input fails closed.
EXPECT_FALSE(parseXmrigRelease("not json at all").ok);
EXPECT_FALSE(parseXmrigRelease("{}").ok); // no tag_name
}
void testXmrigAssetSelection()
{
using namespace dragonx::util;
const auto rel = parseXmrigRelease(readXmrigFixture());
const int linux = selectXmrigAsset(rel, "linux-x64");
const int win = selectXmrigAsset(rel, "win-x64");
EXPECT_TRUE(linux >= 0);
EXPECT_TRUE(win >= 0);
EXPECT_TRUE(linux != win);
EXPECT_TRUE(rel.assets[linux].name.find("linux-x64.zip") != std::string::npos);
EXPECT_TRUE(rel.assets[win].name.find("win-x64.zip") != std::string::npos);
// No macOS build is published -> graceful "not found".
EXPECT_EQ(selectXmrigAsset(rel, "macos-x64"), -1);
EXPECT_EQ(selectXmrigAsset(rel, "macos-arm64"), -1);
EXPECT_EQ(selectXmrigAsset(rel, ""), -1);
}
void testXmrigChecksumParsing()
{
using namespace dragonx::util;
const auto rel = parseXmrigRelease(readXmrigFixture());
const auto sums = parseXmrigChecksums(rel.body);
// Inner-binary checksums (what we verify before making the binary executable).
EXPECT_EQ(sums.at("xmrig"),
std::string("37c178f743c269c1d9e18302cead0ed117ded2b5fe30910e836896b4abc20e57"));
EXPECT_EQ(sums.at("xmrig.exe"),
std::string("01223711eddea347eee394c4b6d265b9a3e5c13fe93204bc041c4399c9c758f8"));
// Archive checksum, keyed by the asset filename.
EXPECT_EQ(sums.at("drg-xmrig-6.25.1-drg1-linux-x64.zip"),
std::string("c121a078ee46943584aa6148cac26583409c832d28668cf38ba908e10214c9a6"));
// Non-checksum prose lines are ignored.
EXPECT_TRUE(parseXmrigChecksums("just some text\nno hashes here").empty());
}
void testXmrigSha256AndBasenames()
{
using namespace dragonx::util;
// Known-answer: SHA-256("abc").
const std::string abc = "abc";
EXPECT_EQ(sha256Hex(abc.data(), abc.size()),
std::string("ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"));
// The selected asset's published archive checksum matches what the verifier will compute over
// the bytes (closes the loop between parse + hash). Empty input is the SHA-256 of "".
EXPECT_EQ(sha256Hex("", 0),
std::string("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"));
const auto linuxNames = xmrigExtractBasenames("linux-x64");
EXPECT_EQ(linuxNames.size(), static_cast<std::size_t>(1));
EXPECT_EQ(linuxNames.front(), std::string("xmrig"));
const auto winNames = xmrigExtractBasenames("win-x64");
EXPECT_EQ(winNames.size(), static_cast<std::size_t>(2));
EXPECT_EQ(winNames.front(), std::string("xmrig.exe")); // miner binary is always first
EXPECT_TRUE(std::find(winNames.begin(), winNames.end(), std::string("WinRing0x64.sys"))
!= winNames.end());
}
// Live end-to-end exercise of the XmrigUpdater WORKER (real network + curl + miniz). Env-gated so
// CI / offline runs skip it; run with DRAGONX_TEST_NETWORK=1 to hit git.dragonx.is. Verifies the
// full download -> archive-checksum -> extract/flatten -> inner-binary-checksum -> install path.
void testXmrigLiveInstall()
{
if (!std::getenv("DRAGONX_TEST_NETWORK")) {
std::printf("[skip] testXmrigLiveInstall (set DRAGONX_TEST_NETWORK=1 to run)\n");
return;
}
using namespace dragonx::util;
const std::string dir = "/tmp/obsidian-xmrig-live-test";
std::error_code ec; std::filesystem::remove_all(dir, ec);
XmrigUpdater up;
up.startInstall(dir);
for (int i = 0; i < 1200 && !up.isDone(); ++i) // up to ~120s for a ~4 MiB download
std::this_thread::sleep_for(std::chrono::milliseconds(100));
const auto p = up.getProgress();
EXPECT_TRUE(up.isDone());
EXPECT_TRUE(p.state == XmrigUpdater::State::Done);
if (p.state != XmrigUpdater::State::Done) {
std::printf("[testXmrigLiveInstall] failed: %s\n", p.error.c_str());
return;
}
// The miner binary was flattened out of the versioned subdir into the target dir, verified.
const std::string bin = dir + "/" + xmrigExtractBasenames(currentXmrigPlatformToken()).front();
EXPECT_TRUE(std::filesystem::exists(bin));
if (std::filesystem::exists(bin)) {
std::ifstream f(bin, std::ios::binary);
const std::string bytes((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
// On this (linux-x64) box the installed binary must match the published inner checksum.
if (currentXmrigPlatformToken() == "linux-x64")
EXPECT_EQ(sha256Hex(bytes.data(), bytes.size()),
std::string("37c178f743c269c1d9e18302cead0ed117ded2b5fe30910e836896b4abc20e57"));
}
std::filesystem::remove_all(dir, ec);
}
} // namespace
int main()
@@ -4012,6 +4134,11 @@ int main()
testLiteRolloutPolicyStagedRollout();
testLiteRolloutManifestLoader();
testLiteWalletControllerRolloutGate();
testXmrigReleaseParsing();
testXmrigAssetSelection();
testXmrigChecksumParsing();
testXmrigSha256AndBasenames();
testXmrigLiveInstall();
testGeneratedResourceBehavior();
if (g_failures != 0) {