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:
2026-06-27 21:27:13 -05:00
parent 2e8e214689
commit 4473e7e00a
19 changed files with 1848 additions and 108 deletions

View 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"}]}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -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();