feat(updater): in-app dragonxd updater + browse-all-releases
Add a full-node daemon updater (util/DaemonUpdater + daemon_download_dialog) reachable from Settings -> NODE & SECURITY: downloads/verifies (SHA-256 + enforced ed25519 signature) and atomically installs the latest dragonxd from the project Gitea, with a "Restart daemon now" step. Add a shared "Browse all releases..." picker (release_list_view) to both the miner and daemon updaters so users can pin older/pre-release builds. Pure no-I/O cores (daemon_updater_core / xmrig_updater_core) are unit-tested; sign-daemon-release.sh signs release archives offline. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1
tests/fixtures/daemon/release_latest.json
vendored
Normal file
1
tests/fixtures/daemon/release_latest.json
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"id":15,"tag_name":"v1.0.2","target_commitish":"dragonx","name":"Dragonx v1.0.2","body":"## What is DragonX?\r\n\r\nDragonX is a privacy-focused cryptocurrency built on zero-knowledge mathematics. It uses the\r\nSapling protocol for shielded transactions and enforces mandatory z2z (shielded-to-shielded)\r\ntransactions after block 340,000, meaning funds can only be sent to shielded z-addresses at\r\nthe consensus level.\r\n\r\n### Bug Fixes\r\n * Fix sapling pool persistence — pool total no longer resets to 0 on node restart. Explorer nodes should reindex once after upgrading.\r\n\r\n### New Features\r\n * Add `subsidy` and `fees` fields to the `getblock` RPC response so explorers can display the correct 3 DRGX block reward separately from fees\r\n\r\n### Key Features\r\n\r\n * **RandomX Proof-of-Work** — CPU-mineable, ASIC-resistant mining algorithm\r\n * **Sapling zk-SNARKs** — zero-knowledge proofs for fully private transactions\r\n * **Mandatory shielded transactions** — z2z enforced at consensus after block 340,000\r\n * **Encrypted P2P** — all connections secured with TLS 1.3 via WolfSSL (AES-256-GCM and ChaCha20-Poly1305)\r\n * **Anonymous networking** — built-in Tor, i2p, and cjdns support\r\n * **Passive network spy protection** — prevents ISPs and observers from identifying transaction origins\r\n\r\n## Checksums\r\n\r\n| File | SHA-256 |\r\n|------|---------|\r\n| dragonx-1.0.2-linux-amd64.zip | `85f1dd908bfbdee6aaebabdc74848dc8963d45e7510172d84d0230c0fad6cc16` |\r\n| dragonx-1.0.2-macos.zip | `102e1f1ecab05def25465ad4867b19d46c7cc00a9fbb018ffb405bcb82cc6432` |\r\n| dragonx-1.0.2-win64.zip | `dd6a554ac05c834da9910ae796215567e97c426f3aed15b54af1f7b90d48c43a` |\r\n\r\n## License\r\n\r\nDragonX is released under the [GNU General Public License v3 (GPLv3)](https://git.dragonx.is/DragonX/dragonx/src/branch/dragonx/COPYING).\r\n\r\nCopyright © 2024-2026 The DragonX Developers\r\n","url":"https://git.dragonx.is/api/v1/repos/DragonX/dragonx/releases/15","html_url":"https://git.dragonx.is/DragonX/dragonx/releases/tag/v1.0.2","tarball_url":"https://git.dragonx.is/DragonX/dragonx/archive/v1.0.2.tar.gz","zipball_url":"https://git.dragonx.is/DragonX/dragonx/archive/v1.0.2.zip","upload_url":"https://git.dragonx.is/api/v1/repos/DragonX/dragonx/releases/15/assets","draft":false,"prerelease":false,"created_at":"2026-03-19T10:09:18-05:00","published_at":"2026-03-19T10:09:18-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":53,"name":"dragonx-1.0.2-linux-amd64.zip","size":61792264,"download_count":98,"created_at":"2026-03-19T10:21:03-05:00","uuid":"ff53b136-e128-438b-82d8-76b9f33b830a","browser_download_url":"https://git.dragonx.is/DragonX/dragonx/releases/download/v1.0.2/dragonx-1.0.2-linux-amd64.zip"},{"id":52,"name":"dragonx-1.0.2-macos.zip","size":59382241,"download_count":35,"created_at":"2026-03-19T10:21:03-05:00","uuid":"aefc7f79-ae87-4da9-a168-8b731166c080","browser_download_url":"https://git.dragonx.is/DragonX/dragonx/releases/download/v1.0.2/dragonx-1.0.2-macos.zip"},{"id":54,"name":"dragonx-1.0.2-win64.zip","size":62453466,"download_count":150,"created_at":"2026-03-19T10:21:06-05:00","uuid":"ab44576e-e354-4019-8f5a-a5536257e854","browser_download_url":"https://git.dragonx.is/DragonX/dragonx/releases/download/v1.0.2/dragonx-1.0.2-win64.zip"}]}
|
||||
1
tests/fixtures/daemon/releases_list.json
vendored
Normal file
1
tests/fixtures/daemon/releases_list.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
tests/fixtures/xmrig/releases_list.json
vendored
Normal file
1
tests/fixtures/xmrig/releases_list.json
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -23,6 +23,7 @@
|
||||
#include "util/payment_uri.h"
|
||||
#include "util/platform.h"
|
||||
#include "util/xmrig_updater.h"
|
||||
#include "util/daemon_updater.h"
|
||||
#include "util/lite_server_probe.h"
|
||||
#include "wallet/lite_connection_service.h"
|
||||
#include "wallet/lite_diagnostics.h"
|
||||
@@ -4604,6 +4605,190 @@ void testXmrigSignatureVerify()
|
||||
EXPECT_FALSE(verifyXmrigSignature(data, "", pkB64));
|
||||
}
|
||||
|
||||
// Reads any fixture file by repo-relative path under tests/fixtures/.
|
||||
static std::string readFixtureFile(const std::string& rel)
|
||||
{
|
||||
std::ifstream f(std::string(DRAGONX_TEST_FIXTURE_DIR) + "/" + rel, std::ios::binary);
|
||||
return std::string((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
|
||||
}
|
||||
|
||||
void testXmrigReleaseListParsing()
|
||||
{
|
||||
using namespace dragonx::util;
|
||||
const auto list = parseXmrigReleaseList(readFixtureFile("xmrig/releases_list.json"));
|
||||
EXPECT_TRUE(list.size() >= 2);
|
||||
EXPECT_EQ(list.front().tag, std::string("v6.25.3")); // newest first (Gitea order preserved)
|
||||
for (const auto& r : list) {
|
||||
EXPECT_TRUE(r.ok);
|
||||
EXPECT_TRUE(!r.tag.empty());
|
||||
EXPECT_TRUE(!r.assets.empty());
|
||||
}
|
||||
// Fail closed on non-array / garbage.
|
||||
EXPECT_TRUE(parseXmrigReleaseList("not json").empty());
|
||||
EXPECT_TRUE(parseXmrigReleaseList("{}").empty());
|
||||
// Drafts are skipped; the pre-release flag is captured.
|
||||
const std::string inj = R"([
|
||||
{"tag_name":"v9.9.9","prerelease":true,"assets":[{"name":"a-linux-x64.zip","browser_download_url":"https://x/a","size":1}]},
|
||||
{"tag_name":"v9.9.8","draft":true,"assets":[]}
|
||||
])";
|
||||
const auto injList = parseXmrigReleaseList(inj);
|
||||
EXPECT_EQ(injList.size(), static_cast<std::size_t>(1)); // draft dropped
|
||||
EXPECT_EQ(injList.front().tag, std::string("v9.9.9"));
|
||||
EXPECT_TRUE(injList.front().prerelease);
|
||||
}
|
||||
|
||||
// ── daemon updater pure core (util/daemon_updater.h) — driven by the real v1.0.2 release fixture ──
|
||||
static std::string readDaemonFixture()
|
||||
{
|
||||
return readFixtureFile("daemon/release_latest.json");
|
||||
}
|
||||
|
||||
void testDaemonReleaseParsing()
|
||||
{
|
||||
using namespace dragonx::util;
|
||||
const auto rel = parseDaemonRelease(readDaemonFixture());
|
||||
EXPECT_TRUE(rel.ok);
|
||||
EXPECT_EQ(rel.tag, std::string("v1.0.2"));
|
||||
EXPECT_EQ(rel.assets.size(), static_cast<std::size_t>(3)); // linux-amd64 / macos / win64
|
||||
for (const auto& a : rel.assets) {
|
||||
EXPECT_TRUE(!a.name.empty());
|
||||
EXPECT_TRUE(a.downloadUrl.rfind("https://", 0) == 0);
|
||||
EXPECT_TRUE(a.size > 0);
|
||||
}
|
||||
EXPECT_FALSE(parseDaemonRelease("not json at all").ok);
|
||||
EXPECT_FALSE(parseDaemonRelease("{}").ok); // no tag_name
|
||||
}
|
||||
|
||||
void testDaemonAssetSelection()
|
||||
{
|
||||
using namespace dragonx::util;
|
||||
const auto rel = parseDaemonRelease(readDaemonFixture());
|
||||
const int lin = selectDaemonAsset(rel, "linux-amd64");
|
||||
const int mac = selectDaemonAsset(rel, "macos");
|
||||
const int win = selectDaemonAsset(rel, "win64");
|
||||
EXPECT_TRUE(lin >= 0 && mac >= 0 && win >= 0);
|
||||
EXPECT_TRUE(lin != mac && mac != win && lin != win);
|
||||
EXPECT_TRUE(rel.assets[lin].name.find("linux-amd64.zip") != std::string::npos);
|
||||
EXPECT_TRUE(rel.assets[mac].name.find("macos.zip") != std::string::npos);
|
||||
EXPECT_TRUE(rel.assets[win].name.find("win64.zip") != std::string::npos);
|
||||
// Wrong/foreign tokens (e.g. the miner's naming) must NOT match the daemon archives.
|
||||
EXPECT_EQ(selectDaemonAsset(rel, "linux-x64"), -1);
|
||||
EXPECT_EQ(selectDaemonAsset(rel, "linux-arm64"), -1);
|
||||
EXPECT_EQ(selectDaemonAsset(rel, ""), -1);
|
||||
}
|
||||
|
||||
void testDaemonChecksumParsing()
|
||||
{
|
||||
using namespace dragonx::util;
|
||||
const auto rel = parseDaemonRelease(readDaemonFixture());
|
||||
const auto sums = parseDaemonChecksums(rel.body);
|
||||
// The markdown checksum table ( "| <archive>.zip | `<sha256>` |" ) is parsed by archive name.
|
||||
EXPECT_EQ(sums.at("dragonx-1.0.2-linux-amd64.zip"),
|
||||
std::string("85f1dd908bfbdee6aaebabdc74848dc8963d45e7510172d84d0230c0fad6cc16"));
|
||||
EXPECT_EQ(sums.at("dragonx-1.0.2-macos.zip"),
|
||||
std::string("102e1f1ecab05def25465ad4867b19d46c7cc00a9fbb018ffb405bcb82cc6432"));
|
||||
EXPECT_EQ(sums.at("dragonx-1.0.2-win64.zip"),
|
||||
std::string("dd6a554ac05c834da9910ae796215567e97c426f3aed15b54af1f7b90d48c43a"));
|
||||
// Header/separator/prose rows are ignored.
|
||||
EXPECT_TRUE(parseDaemonChecksums("| File | SHA-256 |\n|---|---|\njust prose, no hashes").empty());
|
||||
// Keys are lowercased so the (also-lowercased) lookup is case-insensitive vs the JSON names.
|
||||
const auto mixed = parseDaemonChecksums(
|
||||
"| DragonX-1.0.2-Win64.ZIP | `dd6a554ac05c834da9910ae796215567e97c426f3aed15b54af1f7b90d48c43a` |");
|
||||
EXPECT_EQ(mixed.at("dragonx-1.0.2-win64.zip"),
|
||||
std::string("dd6a554ac05c834da9910ae796215567e97c426f3aed15b54af1f7b90d48c43a"));
|
||||
}
|
||||
|
||||
void testDaemonBasenamesAndVersionCore()
|
||||
{
|
||||
using namespace dragonx::util;
|
||||
const auto posix = daemonExtractBasenames("linux-amd64");
|
||||
EXPECT_EQ(posix.size(), static_cast<std::size_t>(3));
|
||||
EXPECT_EQ(posix.front(), std::string("dragonxd")); // daemon binary is always first
|
||||
EXPECT_TRUE(std::find(posix.begin(), posix.end(), std::string("dragonx-cli")) != posix.end());
|
||||
EXPECT_TRUE(std::find(posix.begin(), posix.end(), std::string("dragonx-tx")) != posix.end());
|
||||
const auto win = daemonExtractBasenames("win64");
|
||||
EXPECT_EQ(win.front(), std::string("dragonxd.exe"));
|
||||
EXPECT_TRUE(std::find(win.begin(), win.end(), std::string("dragonx-cli.exe")) != win.end());
|
||||
|
||||
// Version-core normalization: a scanned "vX.Y.Z-<commit>" must compare equal to a release tag.
|
||||
EXPECT_EQ(daemonVersionCore("v1.0.2-ddd851dc1"), std::string("v1.0.2"));
|
||||
EXPECT_EQ(daemonVersionCore("v1.0.2"), std::string("v1.0.2"));
|
||||
EXPECT_EQ(daemonVersionCore("1.0.2"), std::string("1.0.2"));
|
||||
EXPECT_EQ(daemonVersionCore("DragonX v1.0.2 (abc)"), std::string("v1.0.2"));
|
||||
EXPECT_EQ(daemonVersionCore("v1.2"), std::string("v1.2")); // not full N.N.N -> unchanged
|
||||
EXPECT_EQ(daemonVersionCore(""), std::string(""));
|
||||
EXPECT_EQ(daemonVersionCore("nightly"), std::string("nightly"));
|
||||
}
|
||||
|
||||
void testDaemonSignatureAssetSelection()
|
||||
{
|
||||
using namespace dragonx::util;
|
||||
DaemonRelease rel; rel.ok = true; rel.tag = "v1.0.2";
|
||||
rel.assets.push_back({"dragonx-1.0.2-linux-amd64.zip", "https://x/zip", 100});
|
||||
EXPECT_EQ(selectDaemonSignatureAsset(rel, "dragonx-1.0.2-linux-amd64.zip"), -1); // none published
|
||||
rel.assets.push_back({"dragonx-1.0.2-linux-amd64.zip.sig", "https://x/sig", 64});
|
||||
EXPECT_TRUE(selectDaemonSignatureAsset(rel, "dragonx-1.0.2-linux-amd64.zip") >= 0);
|
||||
EXPECT_EQ(selectDaemonSignatureAsset(rel, "other.zip"), -1);
|
||||
}
|
||||
|
||||
void testDaemonPinnedKeyValidity()
|
||||
{
|
||||
using namespace dragonx::util;
|
||||
// The daemon updater REQUIRES signatures, so the pinned key must be a valid 32-byte ed25519 key.
|
||||
const std::string pin = kDaemonSignaturePublicKeyBase64;
|
||||
EXPECT_TRUE(kDaemonRequireSignature);
|
||||
EXPECT_TRUE(!pin.empty());
|
||||
EXPECT_TRUE(sodium_init() >= 0);
|
||||
unsigned char pk[crypto_sign_PUBLICKEYBYTES];
|
||||
std::size_t n = 0; const char* end = nullptr;
|
||||
const int rc = sodium_base642bin(pk, sizeof(pk), pin.data(), pin.size(), " \t\r\n",
|
||||
&n, &end, sodium_base64_VARIANT_ORIGINAL);
|
||||
EXPECT_EQ(rc, 0);
|
||||
EXPECT_EQ(n, static_cast<std::size_t>(crypto_sign_PUBLICKEYBYTES));
|
||||
}
|
||||
|
||||
void testDaemonSignatureInterop()
|
||||
{
|
||||
using namespace dragonx::util;
|
||||
// Known answer produced by scripts/sign-daemon-release.sh (OpenSSL ed25519) over the message
|
||||
// below with the pinned key's secret half. Proves the pinned key + the release-signing flow
|
||||
// produce signatures the wallet's libsodium verifier accepts (closes the OpenSSL<->libsodium
|
||||
// interop loop), and that the pinned public key actually matches the signing key.
|
||||
const std::string msg = "dragonx daemon archive payload bytes for signing test";
|
||||
const std::string sigB64 =
|
||||
"rmQ5qOw+W8vu56GeZrooD7Wh1N/WHRP4siD19Mxq/8WXQQuNrFY3DPCNU9C7jHB2jg/VfKrLVna57K/lkSDBDA==";
|
||||
const std::string pin = kDaemonSignaturePublicKeyBase64;
|
||||
EXPECT_TRUE(verifyXmrigSignature(msg, sigB64, pin));
|
||||
EXPECT_TRUE(verifyXmrigSignature(msg, " " + sigB64 + "\n", pin)); // trailing newline tolerated
|
||||
// Fails closed on tampered payload or wrong key.
|
||||
EXPECT_FALSE(verifyXmrigSignature(msg + "x", sigB64, pin));
|
||||
EXPECT_FALSE(verifyXmrigSignature(msg, sigB64, std::string(kXmrigSignaturePublicKeyBase64)));
|
||||
}
|
||||
|
||||
void testDaemonReleaseListParsing()
|
||||
{
|
||||
using namespace dragonx::util;
|
||||
const auto list = parseDaemonReleaseList(readFixtureFile("daemon/releases_list.json"));
|
||||
EXPECT_TRUE(list.size() >= 2);
|
||||
EXPECT_EQ(list.front().tag, std::string("v1.0.2")); // newest first
|
||||
for (const auto& r : list) {
|
||||
EXPECT_TRUE(r.ok);
|
||||
EXPECT_TRUE(!r.assets.empty());
|
||||
}
|
||||
EXPECT_TRUE(parseDaemonReleaseList("not json").empty());
|
||||
EXPECT_TRUE(parseDaemonReleaseList("{}").empty());
|
||||
// Draft skipped; pre-release + name + published_at captured.
|
||||
const std::string inj = R"([
|
||||
{"tag_name":"v2.0.0","prerelease":true,"name":"RC","published_at":"2026-09-01T00:00:00Z","assets":[{"name":"dragonx-2.0.0-linux-amd64.zip","browser_download_url":"https://x/a","size":1}]},
|
||||
{"tag_name":"v1.9.9","draft":true,"assets":[]}
|
||||
])";
|
||||
const auto injList = parseDaemonReleaseList(inj);
|
||||
EXPECT_EQ(injList.size(), static_cast<std::size_t>(1));
|
||||
EXPECT_TRUE(injList.front().prerelease);
|
||||
EXPECT_EQ(injList.front().name, std::string("RC"));
|
||||
EXPECT_EQ(injList.front().publishedAt.substr(0, 10), std::string("2026-09-01"));
|
||||
}
|
||||
|
||||
// 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.
|
||||
@@ -4707,6 +4892,15 @@ int main()
|
||||
testXmrigSignatureAssetSelection();
|
||||
testXmrigPinnedKeyValidity();
|
||||
testXmrigSignatureVerify();
|
||||
testXmrigReleaseListParsing();
|
||||
testDaemonReleaseParsing();
|
||||
testDaemonAssetSelection();
|
||||
testDaemonChecksumParsing();
|
||||
testDaemonBasenamesAndVersionCore();
|
||||
testDaemonSignatureAssetSelection();
|
||||
testDaemonPinnedKeyValidity();
|
||||
testDaemonSignatureInterop();
|
||||
testDaemonReleaseListParsing();
|
||||
testLiteServerHostParsing();
|
||||
testLiteOfficialServerDetection();
|
||||
testAtomicFileWrite();
|
||||
|
||||
Reference in New Issue
Block a user