// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 // // DaemonUpdater background worker (libcurl download + miniz extract). The pure, no-I/O helpers it // calls (release parsing, asset/platform matching, the checksum-table parser, version core) live in // daemon_updater_core.cpp; the generic SHA-256 / ed25519 verification is reused from the miner // updater (xmrig_updater_core.cpp) via util::sha256Hex / util::verifyXmrigSignature. #include "daemon_updater.h" #include "xmrig_updater.h" // util::sha256Hex, util::verifyXmrigSignature (shared crypto) #include "logger.h" #include #include #include #include #include #include #include #include namespace fs = std::filesystem; namespace dragonx { namespace util { namespace { // Bound the archive/member sizes before they can fill memory/disk — a defense even ahead of the // checksum check (which only runs once the file is fully downloaded). The full-node archive bundles // the daemon binaries plus Sapling params, so the caps are well above the miner updater's. constexpr curl_off_t kMaxArchiveBytes = 256LL * 1024 * 1024; // 256 MiB constexpr std::size_t kMaxMemberBytes = 128u * 1024 * 1024; // 128 MiB per extracted file size_t writeStringCb(void* contents, size_t size, size_t nmemb, void* userp) { static_cast(userp)->append(static_cast(contents), size * nmemb); return size * nmemb; } size_t writeFileCb(void* contents, size_t size, size_t nmemb, void* userp) { return std::fwrite(contents, size, nmemb, static_cast(userp)); } // libcurl progress callback: publish live byte counts and abort the transfer on cancel(). int xferCb(void* clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t, curl_off_t) { auto* up = static_cast(clientp); return up->onDownloadProgress(static_cast(dlnow), static_cast(dltotal)) ? 0 : 1; } std::string baseName(const std::string& path) { const auto slash = path.find_last_of("/\\"); return slash == std::string::npos ? path : path.substr(slash + 1); } std::string toLower(std::string s) { std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); return s; } } // namespace DaemonUpdater::~DaemonUpdater() { cancel_requested_ = true; if (worker_.joinable()) worker_.join(); } void DaemonUpdater::setProgress(State state, const std::string& text, double done, double total) { std::lock_guard lk(mutex_); progress_.state = state; progress_.status_text = text; if (done > 0) progress_.downloaded_bytes = done; if (total > 0) progress_.total_bytes = total; progress_.percent = (progress_.total_bytes > 0) ? static_cast(100.0 * progress_.downloaded_bytes / progress_.total_bytes) : progress_.percent; if (state == State::Failed) progress_.error = text; } DaemonUpdater::Progress DaemonUpdater::getProgress() const { std::lock_guard lk(mutex_); return progress_; } bool DaemonUpdater::onDownloadProgress(double downloadedBytes, double totalBytes) { { std::lock_guard lk(mutex_); progress_.downloaded_bytes = downloadedBytes; if (totalBytes > 0) progress_.total_bytes = totalBytes; progress_.percent = (progress_.total_bytes > 0) ? static_cast(100.0 * progress_.downloaded_bytes / progress_.total_bytes) : progress_.percent; } return !cancel_requested_.load(); // false -> curl aborts the transfer } bool DaemonUpdater::isDone() const { std::lock_guard lk(mutex_); return progress_.state == State::Done || progress_.state == State::Failed || progress_.state == State::Unavailable; } void DaemonUpdater::cancel() { cancel_requested_ = true; } std::string DaemonUpdater::httpGet(const std::string& url) { CURL* curl = curl_easy_init(); if (!curl) return {}; std::string result; curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeStringCb); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &result); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); curl_easy_setopt(curl, CURLOPT_USERAGENT, "ObsidianDragon/1.0"); curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L); curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 15L); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L); const CURLcode res = curl_easy_perform(curl); long httpCode = 0; curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpCode); curl_easy_cleanup(curl); if (res != CURLE_OK || httpCode < 200 || httpCode >= 300) { DEBUG_LOGF("[daemon-updater] GET %s failed: %s (HTTP %ld)\n", url.c_str(), curl_easy_strerror(res), httpCode); return {}; } return result; } bool DaemonUpdater::downloadToFile(const std::string& url, const std::string& destPath) { FILE* fp = std::fopen(destPath.c_str(), "wb"); if (!fp) return false; CURL* curl = curl_easy_init(); if (!curl) { std::fclose(fp); return false; } curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeFileCb); curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); curl_easy_setopt(curl, CURLOPT_USERAGENT, "ObsidianDragon/1.0"); curl_easy_setopt(curl, CURLOPT_TIMEOUT, 0L); curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 30L); curl_easy_setopt(curl, CURLOPT_LOW_SPEED_LIMIT, 1024L); curl_easy_setopt(curl, CURLOPT_LOW_SPEED_TIME, 60L); curl_easy_setopt(curl, CURLOPT_MAXFILESIZE_LARGE, kMaxArchiveBytes); // refuse oversized bodies // Live progress + cancellation: the callback publishes byte counts and aborts on cancel(). curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0L); curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, xferCb); curl_easy_setopt(curl, CURLOPT_XFERINFODATA, this); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L); const CURLcode res = curl_easy_perform(curl); long httpCode = 0; curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpCode); curl_easy_cleanup(curl); std::fclose(fp); if (res != CURLE_OK || httpCode < 200 || httpCode >= 300) { DEBUG_LOGF("[daemon-updater] download %s failed: %s (HTTP %ld)\n", url.c_str(), curl_easy_strerror(res), httpCode); std::error_code ec; fs::remove(destPath, ec); return false; } return true; } void DaemonUpdater::startCheck(const std::string& installedVersion) { if (worker_running_.exchange(true)) return; cancel_requested_ = false; if (worker_.joinable()) worker_.join(); { std::lock_guard lk(mutex_); progress_ = Progress{}; progress_.installed_tag = installedVersion; progress_.state = State::Checking; progress_.status_text = "Checking for the latest node…"; } worker_ = std::thread([this, installedVersion] { runCheck(installedVersion); worker_running_ = false; }); } void DaemonUpdater::startInstall(const std::string& targetDir) { if (worker_running_.exchange(true)) return; cancel_requested_ = false; if (worker_.joinable()) worker_.join(); { std::lock_guard lk(mutex_); progress_.state = State::Downloading; progress_.error.clear(); progress_.status_text = "Preparing…"; progress_.downloaded_bytes = 0; // clear any prior op's progress so the bar starts at 0% progress_.total_bytes = 0; progress_.percent = 0.0f; } worker_ = std::thread([this, targetDir] { runInstall(targetDir); worker_running_ = false; }); } void DaemonUpdater::startListReleases() { if (worker_running_.exchange(true)) return; cancel_requested_ = false; if (worker_.joinable()) worker_.join(); { std::lock_guard lk(mutex_); progress_.state = State::Listing; progress_.error.clear(); progress_.status_text = "Loading releases…"; progress_.downloaded_bytes = 0; // clear any prior op's progress progress_.total_bytes = 0; progress_.percent = 0.0f; } worker_ = std::thread([this] { runListReleases(); worker_running_ = false; }); } void DaemonUpdater::startInstallRelease(const std::string& targetDir, DaemonRelease release) { if (worker_running_.exchange(true)) return; cancel_requested_ = false; if (worker_.joinable()) worker_.join(); { std::lock_guard lk(mutex_); progress_.state = State::Downloading; progress_.error.clear(); progress_.status_text = "Preparing…"; progress_.downloaded_bytes = 0; // clear any prior op's progress so the bar starts at 0% progress_.total_bytes = 0; progress_.percent = 0.0f; } worker_ = std::thread([this, targetDir, release = std::move(release)] { installResolved(targetDir, release); worker_running_ = false; }); } std::vector DaemonUpdater::getReleases() const { std::lock_guard lk(mutex_); return releases_; } void DaemonUpdater::runListReleases() { const std::string body = httpGet(kReleasesUrl); if (body.empty()) { setProgress(State::Failed, "Could not reach the update server."); return; } std::vector list = parseDaemonReleaseList(body); const bool empty = list.empty(); { std::lock_guard lk(mutex_); releases_ = std::move(list); } if (empty) { setProgress(State::Failed, "No releases found."); return; } setProgress(State::ReleaseList, "Select a version to install."); } void DaemonUpdater::runCheck(std::string installedVersion) { const std::string body = httpGet(kApiUrl); if (body.empty()) { setProgress(State::Failed, "Could not reach the update server."); return; } const DaemonRelease rel = parseDaemonRelease(body); if (!rel.ok) { setProgress(State::Failed, rel.error.empty() ? "Invalid release data." : rel.error); return; } const std::string token = currentDaemonPlatformToken(); const int idx = selectDaemonAsset(rel, token); { std::lock_guard lk(mutex_); progress_.latest_tag = rel.tag; progress_.installed_tag = installedVersion; } if (idx < 0) { setProgress(State::Unavailable, "No node build is available for this platform (" + (token.empty() ? "unknown" : token) + ")."); return; } // Compare by vN.N.N core so an installed "v1.0.2-" matches a release "v1.0.2". const std::string instCore = daemonVersionCore(installedVersion); const bool updateAvailable = instCore.empty() || instCore != daemonVersionCore(rel.tag); { std::lock_guard lk(mutex_); progress_.update_available = updateAvailable; } if (updateAvailable) setProgress(State::UpdateAvailable, "A new node version is available (" + rel.tag + ")."); else setProgress(State::UpToDate, "The node is up to date (" + rel.tag + ")."); } void DaemonUpdater::runInstall(std::string targetDir) { setProgress(State::Checking, "Checking for the latest node…"); const std::string apiBody = httpGet(kApiUrl); if (apiBody.empty()) { setProgress(State::Failed, "Could not reach the update server."); return; } const DaemonRelease rel = parseDaemonRelease(apiBody); if (!rel.ok) { setProgress(State::Failed, rel.error.empty() ? "Invalid release data." : rel.error); return; } installResolved(targetDir, rel); } void DaemonUpdater::installResolved(const std::string& targetDir, const DaemonRelease& rel) { const std::string token = currentDaemonPlatformToken(); const int idx = selectDaemonAsset(rel, token); if (idx < 0) { setProgress(State::Unavailable, "No node build is available for this platform (" + (token.empty() ? "unknown" : token) + ")."); return; } const DaemonReleaseAsset& asset = rel.assets[idx]; const auto checksums = parseDaemonChecksums(rel.body); { std::lock_guard lk(mutex_); progress_.latest_tag = rel.tag; } if (cancel_requested_) { setProgress(State::Failed, "Cancelled."); return; } std::error_code ec; fs::create_directories(targetDir, ec); // 1. Download the archive. const std::string zipPath = (fs::path(targetDir) / ".dragonx-daemon-download.zip").string(); setProgress(State::Downloading, "Downloading " + asset.name + "…", 0, static_cast(asset.size)); if (!downloadToFile(asset.downloadUrl, zipPath)) { setProgress(State::Failed, "Download failed."); return; } if (cancel_requested_) { fs::remove(zipPath, ec); setProgress(State::Failed, "Cancelled."); return; } // 2. Verify the downloaded archive. Read it once, then (a) compare its SHA-256 to the published // checksum and (b) verify a detached ed25519 signature over the archive bytes against the // pinned key, so a checksum rewritten in a tampered release body is not sufficient to install. setProgress(State::Verifying, "Verifying download…"); { std::ifstream f(zipPath, std::ios::binary); if (!f) { fs::remove(zipPath, ec); setProgress(State::Failed, "Could not read the downloaded archive."); return; } const std::string bytes((std::istreambuf_iterator(f)), std::istreambuf_iterator()); if (f.bad()) { fs::remove(zipPath, ec); setProgress(State::Failed, "Could not read the downloaded archive."); return; } // 2a. SHA-256 (integrity / transit corruption). const std::string actual = sha256Hex(bytes.data(), bytes.size()); const auto it = checksums.find(toLower(asset.name)); // keys are lowercased if (it == checksums.end()) { fs::remove(zipPath, ec); setProgress(State::Failed, "No published checksum for " + asset.name + " — refusing to install."); return; } if (actual != it->second) { fs::remove(zipPath, ec); setProgress(State::Failed, "Archive checksum mismatch — refusing to install (possible tampering)."); return; } // 2b. Detached ed25519 signature (authenticity) against the pinned key. const std::string pubKey = kDaemonSignaturePublicKeyBase64; if (!pubKey.empty()) { const int sigIdx = selectDaemonSignatureAsset(rel, asset.name); if (sigIdx < 0) { if (kDaemonRequireSignature) { fs::remove(zipPath, ec); setProgress(State::Failed, "No signature published for this release — refusing to install."); return; } DEBUG_LOGF("[daemon-updater] no signature asset for %s; proceeding on checksum only\n", asset.name.c_str()); } else { setProgress(State::Verifying, "Verifying signature…"); const std::string sigContent = httpGet(rel.assets[sigIdx].downloadUrl); if (sigContent.empty() || !verifyXmrigSignature(bytes, sigContent, pubKey)) { fs::remove(zipPath, ec); setProgress(State::Failed, "Signature verification failed — refusing to install."); return; } } } else if (kDaemonRequireSignature) { fs::remove(zipPath, ec); setProgress(State::Failed, "No signing key is pinned in this build — refusing to install."); return; } } // 3. Extract the daemon binaries (flatten the versioned subdir). No per-member hash check is // needed: the whole archive was already verified above (SHA-256 + ed25519 signature), so // every member is authentic by transitivity. The archive also carries params/asmap, which // this updater deliberately leaves to the wallet's own resource extraction. setProgress(State::Extracting, "Installing node…"); const std::vector wanted = daemonExtractBasenames(token); const std::string daemonName = wanted.front(); // "dragonxd" / "dragonxd.exe" mz_zip_archive zip{}; if (!mz_zip_reader_init_file(&zip, zipPath.c_str(), 0)) { fs::remove(zipPath, ec); setProgress(State::Failed, "Could not open the downloaded archive."); return; } bool daemonInstalled = false; bool failed = false; const int numFiles = static_cast(mz_zip_reader_get_num_files(&zip)); for (int i = 0; i < numFiles && !failed; ++i) { mz_zip_archive_file_stat st; if (!mz_zip_reader_file_stat(&zip, i, &st)) continue; if (mz_zip_reader_is_file_a_directory(&zip, i)) continue; const std::string base = baseName(st.m_filename); if (std::find(wanted.begin(), wanted.end(), base) == wanted.end()) continue; // skip params/asmap/etc. // Reject an implausibly large member before decompressing it into memory. if (st.m_uncomp_size > kMaxMemberBytes) { failed = true; break; } size_t outSize = 0; void* mem = mz_zip_reader_extract_to_heap(&zip, i, &outSize, 0); if (!mem) { failed = true; break; } const std::string finalPath = (fs::path(targetDir) / base).string(); const std::string tmpPath = finalPath + ".tmp"; // Tidy any leftover from a previous update (the old binary moved aside, freed after restart). fs::remove(finalPath + ".old", ec); { std::ofstream of(tmpPath, std::ios::binary | std::ios::trunc); if (!of) { mz_free(mem); failed = true; break; } of.write(static_cast(mem), static_cast(outSize)); } mz_free(mem); // Atomic install that also works while the daemon is running: POSIX rename() replaces the // path even if the old binary is in use (the running process keeps the unlinked inode). On // Windows a running .exe cannot be renamed-over, so move it aside to ".old" first, then put // the new file in place; the ".old" is cleaned up on a later update once the daemon restarts. fs::rename(tmpPath, finalPath, ec); if (ec) { std::error_code mec; fs::rename(finalPath, finalPath + ".old", mec); ec.clear(); fs::rename(tmpPath, finalPath, ec); } if (ec) { fs::remove(tmpPath, ec); failed = true; break; } #ifndef _WIN32 // Every wanted member is an executable in the daemon set — make them all runnable. fs::permissions(finalPath, fs::perms::owner_all | fs::perms::group_read | fs::perms::group_exec | fs::perms::others_read | fs::perms::others_exec, fs::perm_options::replace, ec); #endif if (base == daemonName) daemonInstalled = true; } mz_zip_reader_end(&zip); fs::remove(zipPath, ec); if (failed) { setProgress(State::Failed, "Could not verify/install the node binaries."); return; } if (!daemonInstalled) { setProgress(State::Failed, "Daemon binary not found in the archive."); return; } setProgress(State::Done, "Node installed (" + rel.tag + ")."); } } // namespace util } // namespace dragonx