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

@@ -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) {