diff --git a/CMakeLists.txt b/CMakeLists.txt index c28bf20..653159d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/src/util/xmrig_updater.cpp b/src/util/xmrig_updater.cpp new file mode 100644 index 0000000..31d76a9 --- /dev/null +++ b/src/util/xmrig_updater.cpp @@ -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 +#include + +#include +#include +#include +#include +#include + +namespace fs = std::filesystem; + +namespace dragonx { +namespace util { + +namespace { + +size_t writeStringCb(void* contents, size_t size, size_t nmemb, void* userp) +{ + static_cast(userp)->append(static_cast(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(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 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(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 lk(mutex_); + return progress_; +} + +bool XmrigUpdater::isDone() const +{ + std::lock_guard 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 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 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 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 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 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(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(f)), std::istreambuf_iterator()); + 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 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(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(mem), static_cast(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 diff --git a/src/util/xmrig_updater.h b/src/util/xmrig_updater.h new file mode 100644 index 0000000..99d9668 --- /dev/null +++ b/src/util/xmrig_updater.h @@ -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 " " lines. + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +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 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 " …" 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 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 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 cancel_requested_{false}; + std::atomic worker_running_{false}; + std::thread worker_; +}; + +} // namespace util +} // namespace dragonx diff --git a/src/util/xmrig_updater_core.cpp b/src/util/xmrig_updater_core.cpp new file mode 100644 index 0000000..24f4813 --- /dev/null +++ b/src/util/xmrig_updater_core.cpp @@ -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 +#include + +#include +#include +#include + +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(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(); + if (j.contains("body") && j["body"].is_string()) + r.body = j["body"].get(); + 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(); + if (a.contains("browser_download_url") && a["browser_download_url"].is_string()) + asset.downloadUrl = a["browser_download_url"].get(); + if (a.contains("size") && a["size"].is_number_integer()) + asset.size = a["size"].get(); + 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(i); + } + return -1; +} + +std::map parseXmrigChecksums(const std::string& body) +{ + std::map 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(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 xmrigExtractBasenames(const std::string& platformToken) +{ + if (platformToken.rfind("win", 0) == 0) + return {"xmrig.exe", "WinRing0x64.sys"}; + return {"xmrig"}; +} + +} // namespace util +} // namespace dragonx diff --git a/tests/fixtures/xmrig/release_latest.json b/tests/fixtures/xmrig/release_latest.json new file mode 100644 index 0000000..9b6ef41 --- /dev/null +++ b/tests/fixtures/xmrig/release_latest.json @@ -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"}]} diff --git a/tests/test_phase4.cpp b/tests/test_phase4.cpp index 17cfcb0..c8293a5 100644 --- a/tests/test_phase4.cpp +++ b/tests/test_phase4.cpp @@ -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(f)), std::istreambuf_iterator()); +} + +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(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(1)); + EXPECT_EQ(linuxNames.front(), std::string("xmrig")); + const auto winNames = xmrigExtractBasenames("win-x64"); + EXPECT_EQ(winNames.size(), static_cast(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(f)), std::istreambuf_iterator()); + // 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) {