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>
190 lines
9.4 KiB
C++
190 lines
9.4 KiB
C++
// DragonX Wallet - ImGui Edition
|
|
// Copyright 2024-2026 The Hush Developers
|
|
// Released under the GPLv3
|
|
//
|
|
// DaemonUpdater — fetch + verify + install the latest dragonxd full node from the DragonX Gitea.
|
|
//
|
|
// Sibling of util::XmrigUpdater (the miner updater): same query → download → verify → extract →
|
|
// install pipeline on a background thread, but for the full-node binaries instead of the miner.
|
|
// Flow: query the Gitea releases API for the latest dragonx release, pick the archive matching this
|
|
// platform (dragonx-<ver>-linux-amd64.zip / -win64.zip / -macos.zip), download it, verify it, then
|
|
// extract the daemon executables (dragonxd / dragonx-cli / dragonx-tx, flattening the versioned
|
|
// subdir the archive nests them in) into the daemon directory and chmod them executable.
|
|
//
|
|
// Security: download-and-execute, so verification is mandatory. TLS is verified (libcurl defaults),
|
|
// the host is the project's own Gitea over HTTPS, the archive's SHA-256 is checked against the
|
|
// checksum table published in the release body, AND a detached ed25519 signature over the archive
|
|
// bytes is verified against the key pinned below (kDaemonRequireSignature enforces it: an install
|
|
// is refused when no valid .sig is published). The inner binaries are trusted by transitivity once
|
|
// the whole archive is authenticated. The generic SHA-256 / ed25519 primitives are shared with the
|
|
// miner updater (util::sha256Hex / util::verifyXmrigSignature in xmrig_updater_core.cpp).
|
|
|
|
#pragma once
|
|
|
|
#include <atomic>
|
|
#include <cstddef>
|
|
#include <map>
|
|
#include <mutex>
|
|
#include <string>
|
|
#include <thread>
|
|
#include <vector>
|
|
|
|
namespace dragonx {
|
|
namespace util {
|
|
|
|
struct DaemonReleaseAsset {
|
|
std::string name;
|
|
std::string downloadUrl;
|
|
long long size = 0;
|
|
};
|
|
|
|
struct DaemonRelease {
|
|
bool ok = false;
|
|
std::string tag; // e.g. "v1.0.2"
|
|
std::string name; // human title (e.g. "Dragonx v1.0.2")
|
|
std::string body; // release notes markdown (holds the checksum table)
|
|
bool prerelease = false; // marked pre-release on the Gitea release
|
|
std::string publishedAt; // ISO-8601 publish timestamp (date shown in the UI)
|
|
std::vector<DaemonReleaseAsset> assets;
|
|
std::string error;
|
|
};
|
|
|
|
// ── Release signature (supply-chain hardening, mandatory for the daemon) ──────
|
|
//
|
|
// Pinned ed25519 public key (base64, 32 bytes) used to verify a detached signature over the
|
|
// downloaded archive. With kDaemonRequireSignature=true an install is refused unless the release
|
|
// publishes a "<archive-name>.sig" asset (base64 of, or raw, a 64-byte ed25519 signature over the
|
|
// archive bytes) that verifies against this key — see scripts/sign-daemon-release.sh. To rotate the
|
|
// key, regenerate with that script and replace the value below. If the key is set empty AND
|
|
// kDaemonRequireSignature=false, verification falls back to TLS + the published SHA-256 only.
|
|
inline constexpr const char* kDaemonSignaturePublicKeyBase64 =
|
|
"d59hosHzh2LtHypgGvsMBlgrRGBhOfS0Xl40D/fEjzQ=";
|
|
inline constexpr bool kDaemonRequireSignature = true; // enforced: refuse installs without a valid .sig
|
|
|
|
// ── Pure helpers (no I/O; unit-tested) ───────────────────────────────────────
|
|
|
|
// Parse the Gitea GET /releases/latest JSON into a DaemonRelease (ok=false + error on failure).
|
|
DaemonRelease parseDaemonRelease(const std::string& json);
|
|
|
|
// Parse the Gitea GET /releases (array) JSON into the list of releases, newest first, skipping
|
|
// drafts. Empty on parse failure. Lets the user browse + pin a specific (or pre-release) version.
|
|
std::vector<DaemonRelease> parseDaemonReleaseList(const std::string& json);
|
|
|
|
// The asset-name token for the host platform: "linux-amd64", "win64", "macos", or "" if
|
|
// unknown/unsupported (e.g. linux-arm64, for which no build is published -> Unavailable).
|
|
std::string currentDaemonPlatformToken();
|
|
|
|
// Index of the asset whose name matches the platform token (e.g. ends with "-linux-amd64.zip"),
|
|
// or -1 if none.
|
|
int selectDaemonAsset(const DaemonRelease& release, const std::string& platformToken);
|
|
|
|
// Parse the release-body markdown checksum table ( "| <archive>.zip | `<sha256hex>` |" rows ) into
|
|
// { archive-name -> lowercase-hex }. Header/separator/prose rows (no 64-hex token) are ignored.
|
|
std::map<std::string, std::string> parseDaemonChecksums(const std::string& body);
|
|
|
|
// The binary file basenames to extract for a platform: {"dragonxd","dragonx-cli","dragonx-tx"} on
|
|
// POSIX, the ".exe" variants on Windows. The first entry is always the daemon itself.
|
|
std::vector<std::string> daemonExtractBasenames(const std::string& platformToken);
|
|
|
|
// Index of the detached-signature asset for a given archive (name "<archive>.sig" or
|
|
// "<archive>.minisig"), or -1 if none is published.
|
|
int selectDaemonSignatureAsset(const DaemonRelease& release, const std::string& archiveName);
|
|
|
|
// Reduce a version string to its "vMAJOR.MINOR.PATCH" core, dropping any "-<commit>" suffix the
|
|
// binary scanner appends, so an installed "v1.0.2-ddd851dc1" compares equal to a release "v1.0.2".
|
|
// Returns the input unchanged if it holds no vN.N.N pattern.
|
|
std::string daemonVersionCore(const std::string& version);
|
|
|
|
// ── Background worker ────────────────────────────────────────────────────────
|
|
|
|
class DaemonUpdater {
|
|
public:
|
|
enum class State {
|
|
Idle,
|
|
Checking,
|
|
UpToDate,
|
|
UpdateAvailable,
|
|
Unavailable, // no daemon build is published for this platform (terminal, not an error)
|
|
Listing, // fetching the full release list (Browse all releases)
|
|
ReleaseList, // release list fetched; awaiting the user's pick
|
|
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 dragonx full node.
|
|
static constexpr const char* kApiUrl =
|
|
"https://git.dragonx.is/api/v1/repos/DragonX/dragonx/releases/latest";
|
|
// Full release list (newest first, includes pre-releases) for the "Browse all releases" picker.
|
|
static constexpr const char* kReleasesUrl =
|
|
"https://git.dragonx.is/api/v1/repos/DragonX/dragonx/releases?limit=50";
|
|
|
|
DaemonUpdater() = default;
|
|
~DaemonUpdater();
|
|
DaemonUpdater(const DaemonUpdater&) = delete;
|
|
DaemonUpdater& operator=(const DaemonUpdater&) = delete;
|
|
|
|
// Query the latest release on a background thread. `installedVersion` (may be empty/unknown) is
|
|
// compared (by vN.N.N core) to the API tag to set Progress.update_available. End state:
|
|
// UpToDate / UpdateAvailable / Failed.
|
|
void startCheck(const std::string& installedVersion);
|
|
|
|
// Download → verify archive → extract (flatten) → install into `targetDir` on a background
|
|
// thread. Re-fetches the latest release so it is self-contained. End state: Done / Failed. The
|
|
// new binary takes effect on the next daemon start (the caller offers a restart).
|
|
void startInstall(const std::string& targetDir);
|
|
|
|
// Fetch the full release list on a background thread (for "Browse all releases"). End state:
|
|
// ReleaseList (then getReleases() holds the list, newest first) / Failed.
|
|
void startListReleases();
|
|
|
|
// Snapshot of the release list fetched by startListReleases().
|
|
std::vector<DaemonRelease> getReleases() const;
|
|
|
|
// Install a SPECIFIC release (chosen from the browse list) into `targetDir` — same verify/extract
|
|
// path as startInstall, but pinned to `release` instead of latest. End state: Done / Failed.
|
|
void startInstallRelease(const std::string& targetDir, DaemonRelease release);
|
|
|
|
void cancel();
|
|
Progress getProgress() const;
|
|
bool isDone() const; // true once the worker reached a terminal state (Done/Failed/Unavailable)
|
|
|
|
// Internal: called by the libcurl progress callback. Publishes live download bytes and returns
|
|
// false to ask curl to abort (when cancel() was requested). Public only so the C callback in the
|
|
// .cpp can reach it without leaking curl types into this header.
|
|
bool onDownloadProgress(double downloadedBytes, double totalBytes);
|
|
|
|
private:
|
|
void runCheck(std::string installedVersion);
|
|
void runListReleases();
|
|
void runInstall(std::string targetDir);
|
|
void installResolved(const std::string& targetDir, const DaemonRelease& rel); // shared install body
|
|
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::vector<DaemonRelease> releases_;
|
|
std::atomic<bool> cancel_requested_{false};
|
|
std::atomic<bool> worker_running_{false};
|
|
std::thread worker_;
|
|
};
|
|
|
|
} // namespace util
|
|
} // namespace dragonx
|