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

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