// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 #pragma once #include #include #include #include #include #include #include #include #include #include namespace dragonx { namespace util { /** * @brief Bootstrap downloader + extractor for DRAGONX blockchain data. * * Downloads a blockchain snapshot zip from bootstrap.dragonx.is, verifies * SHA-256 / MD5 checksums, extracts it into the DRAGONX data directory * (skipping wallet.dat), and cleans old chain data before extraction. * All work runs on a background thread; progress is queried thread-safely * from the UI thread. * * The old chain data is removed only after a preflight passes (every archive entry is safe and the * disk has room for the extracted data). From that point the run is not cancellable by the user * (Progress::cancellable), and a failed or aborted extraction removes the partial chain again before * the result is published, so dragonxd never starts on a half-written blocks/ + chainstate/. */ class Bootstrap { public: enum class State { Idle, Downloading, Verifying, Extracting, Completed, Failed }; struct Progress { State state = State::Idle; double downloaded_bytes = 0; double total_bytes = 0; // 0 if server doesn't send Content-Length float percent = 0.0f; // 0..100 std::string status_text; // human-readable status std::string error; // non-empty on failure bool cancellable = true; // false from the removal of the old chain data onwards }; /// What extract() would write: the preflight result for an archive. struct ArchivePlan { bool ok = false; std::uint64_t bytes = 0; // uncompressed bytes of the members that would be extracted std::string error; // set when !ok }; using DaemonUpFn = std::function; using SpaceFn = std::function; Bootstrap() = default; ~Bootstrap(); // Non-copyable Bootstrap(const Bootstrap&) = delete; Bootstrap& operator=(const Bootstrap&) = delete; /// Base URL for bootstrap downloads (zip + checksum files). static constexpr const char* kBaseUrl = "https://bootstrap.dragonx.is"; static constexpr const char* kMirrorUrl = "https://bootstrap2.dragonx.is"; static constexpr const char* kZipName = "DRAGONX.zip"; /// Start the bootstrap process on a background thread. /// @param dataDir Path to DRAGONX data dir (e.g. ~/.hush/DRAGONX/) /// @param url Bootstrap zip URL void start(const std::string& dataDir, const std::string& url = "https://bootstrap.dragonx.is/DRAGONX.zip"); /// Test seam: run only the install half (daemon-exit wait, preflight, clean, extract) on a local zip, /// skipping the download and checksum steps. daemonUp defaults to the real probe, spaceFn to /// std::filesystem::space. void startFromZip(const std::string& dataDir, const std::string& zipPath, DaemonUpFn daemonUp = {}, SpaceFn spaceFn = {}); /// Cancel an in-progress download/extract. Unconditional: for quit and the destructor, which may abort /// the destructive window too (the partial chain is then removed again). void cancel(); /// The user's Cancel: refused (returns false, nothing changes) once the destructive window has started /// (Progress::cancellable false). The window's start re-checks the request under the same lock, so a /// click either lands before the old chain data is touched or not at all. bool requestCancel(); /// Quit: true once leaving the worker to the process exit can no longer leave a part-replaced chain: it /// has finished, or it is cancelled and has not opened the destructive window (the window's start /// re-checks the cancel under the same lock, so it never will). bool safeToAbandon() const; /// Quit: cancel() and wait for the worker until `deadline`. True = finished (the object can be destroyed); /// false = still running, and the caller must leave it to the process exit (destroying it would join). bool cancelAndWait(std::chrono::steady_clock::time_point deadline); /// Thread-safe read of current progress. Progress getProgress() const; /// Whether the operation is finished (success or failure). True only once the worker has made its /// last filesystem change. bool isDone() const; /// Format a byte count as human-readable string (e.g. "1.24 GB") static std::string formatSize(double bytes); /// Preflight: open the zip, refuse it if any entry is unsafe (zip-slip; also any ':', an 8.3 "~N" /// alias, or a component of only dots/spaces other than ".") or unreadable, and sum the /// uncompressed size of the members extract() would write (directories and protected names excluded). static ArchivePlan planArchive(const std::string& zipPath); /// Bytes that cleanChainData() would free on the data dir's filesystem: regular files under /// blocks/, chainstate/ and notarizations/. A symlinked directory (or file) counts 0, since only the /// link is removed. static std::uint64_t reclaimableBytes(const std::string& dataDir); /// False = refuse (with *msg set). Needs avail + reclaimable >= needed + max(256 MiB, needed / 20). /// "Unknown" (capacity 0 or avail == UINT64_MAX; the caller also treats an error as unknown) returns /// true. avail 0 with a known capacity is a full disk, which is a refusal. static bool spaceSufficient(std::uint64_t needed, std::uint64_t avail, std::uint64_t capacity, std::uint64_t reclaimable, std::string* msg); /// Snapshot members that are never extracted: any wallet* name, database/, *.conf, .cookie, and /// top-level *.dat other than fee_estimates/peers/banlist. Judged case-insensitively on the name the /// member lands on: "." components dropped, trailing dots/spaces and an NTFS stream suffix (":...") /// stripped from each component, so "./wallet.dat" and "DRAGONX.conf." are protected too. static bool isProtectedEntry(const std::string& name); /// First whitespace-delimited token of a checksum file, lowercased, iff it is exactly /// expectedHexLen hex chars; "" otherwise (an HTTP error page is never a checksum). static std::string parseChecksumFile(const std::string& content, std::size_t expectedHexLen); enum class ResumeAction { Fresh, TruncateAndFresh, ReuseComplete, RangedGet }; /// Pure, no-I/O. authoritative = HEAD was 2xx with a positive Content-Length. static ResumeAction decideResume(long long existingBytes, bool authoritative, long long remoteSize); private: mutable std::mutex mutex_; Progress progress_; std::atomic cancel_requested_{false}; std::thread worker_; std::atomic worker_running_{false}; // Worker bodies. The caller sets worker_running_ = false after they return; each one publishes its // terminal state (Failed/Completed) as its last action. void runDownloadAndInstall(const std::string& dataDir, const std::string& url); void installFromZip(const std::string& dataDir, const std::string& zipPath, const DaemonUpFn& daemonUp, const SpaceFn& spaceFn); void beginRun(); bool download(const std::string& url, const std::string& destZip); /// Never publishes a state: returns false with *err set. bool extract(const std::string& zipPath, const std::string& dataDir, std::string* err); /// True iff blocks/, chainstate/ and notarizations/ are absent afterwards. bool cleanChainData(const std::string& dataDir); void setProgress(State state, const std::string& text, double downloaded = 0, double total = 0); /// Compute SHA-256 of a file, return lowercase hex digest. /// pctBase/pctRange map file progress onto a portion of the overall percent. std::string computeSHA256(const std::string& filePath, float pctBase = 0.0f, float pctRange = 100.0f); /// Compute MD5 of a file, return lowercase hex digest. /// pctBase/pctRange map file progress onto a portion of the overall percent. std::string computeMD5(const std::string& filePath, float pctBase = 0.0f, float pctRange = 100.0f); /// Verify downloaded zip against remote checksums. Returns true if valid. bool verifyChecksums(const std::string& zipPath, const std::string& baseUrl); struct RemoteInfo { bool transportOk = false; long httpCode = 0; long long size = -1; }; /// HEAD the remote file. transportOk=false on any curl error; size=-1 when absent. RemoteInfo getRemoteFileSize(const std::string& url); /// Byte offset of the partial file when resuming a download. /// Added to dlnow in progress reports so the bar reflects true total. double resume_offset_{0}; /// Set by download() on failure (worker thread only): the CURLcode of the transfer and, for a /// failure that is not a transfer error, the reason. int last_curl_{0}; std::string download_error_; /// Set by verifyChecksums (worker thread only): true only on a real digest MISMATCH, so the caller /// deletes the archive only then - never for a missing/unusable sidecar or a transient fetch error. bool last_verify_mismatch_{false}; // libcurl progress callback (static → calls into instance via clientp) static int progressCallback(void* clientp, long long dltotal, long long dlnow, long long ultotal, long long ulnow); }; } // namespace util } // namespace dragonx