From 393f3d147e3ec2fe63a6f0f51e15dbab5cba3d6a Mon Sep 17 00:00:00 2001 From: DanS Date: Mon, 10 Aug 2026 22:10:57 -0500 Subject: [PATCH] feat(recovery): bundle & embed the offline wallet-rebuild helper in every release MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dragonx-wallet-rebuild helper is the only thing that repairs a genuinely BDB-inconsistent wallet.dat — plain "Restore" just re-triggers the daemon's salvage cascade — yet it was silently dropped from every packaged build: - Linux zip/AppImage copied a hand-picked file list that omitted it. - Windows bundled it only behind a soft `[[ -f ]]` guard (silent skip). - macOS never wired Berkeley DB, never built it, never bundled it. build.sh now HARD-REQUIRES the helper for full-node releases (fails the build if the vendored Berkeley DB depends are missing, rather than shipping recovery-less), and ships it in the Linux zip + AppImage, the Windows zip, and the macOS .app. It also compiles the helper standalone for Windows and INCBINs it, and embedded_resources gains ensureWalletRebuildHelperExtracted() to extract it on demand — so a self-contained ObsidianDragon.exe carries recovery exactly like the embedded daemon, even on a machine where first-run param extraction already ran. Co-Authored-By: Claude Opus 4.8 (1M context) --- build.sh | 82 +++++++++++++++++++++++++--- src/resources/embedded_resources.cpp | 47 ++++++++++++++++ src/resources/embedded_resources.h | 7 +++ 3 files changed, 127 insertions(+), 9 deletions(-) diff --git a/build.sh b/build.sh index 66d9f3b..e54ff98 100755 --- a/build.sh +++ b/build.sh @@ -195,6 +195,24 @@ should_bundle_full_node_assets() { ! $DO_LITE } +# The offline wallet-rebuild helper is the ONLY thing that repairs a genuinely BDB-inconsistent +# wallet.dat — plain "Restore" just re-triggers the daemon's salvage cascade. A full-node release must +# NEVER ship without it (the in-app "Repair automatically" option silently disappears otherwise), so +# treat a missing helper as a HARD build failure instead of degrading recovery to Restore-only. +# $1 = built helper path (e.g. bin/dragonx-wallet-rebuild[.exe]); $2 = the BDB depends dir for the hint. +require_wallet_rebuild_helper() { + local helper="$1" depends="$2" + should_bundle_full_node_assets || return 0 # lite builds have no BDB wallet.dat to rebuild + if [[ ! -f "$helper" ]]; then + err "wallet-rebuild helper was NOT built: $helper" + err " → the recovery 'Repair automatically' option would be MISSING from this release." + err " Cause: the vendored Berkeley DB depends are absent, so CMake skipped the dragonx-wallet-rebuild target." + err " Fix: provide ${depends}/{lib/libdb-6.2.a,include/db.h} (same static libdb the daemon links), then rebuild." + exit 1 + fi + info " wallet-rebuild helper present: $helper" +} + # ── Helper: find resource files ────────────────────────────────────────────── find_sapling_params() { local dirs=( @@ -286,13 +304,8 @@ bundle_linux_daemon() { # asmap.dat find_asmap && cp "$ASMAP_DAT" "$dest/asmap.dat" && info " Bundled asmap.dat" - # dragonx-wallet-rebuild helper (offline recovery for a BDB-inconsistent wallet.dat) - for p in "$SCRIPT_DIR/build/linux/bin/dragonx-wallet-rebuild" "$SCRIPT_DIR/../dragonx-wallet-rebuild"; do - if [[ -f "$p" ]]; then - cp "$p" "$dest/dragonx-wallet-rebuild"; chmod +x "$dest/dragonx-wallet-rebuild" - info " Bundled dragonx-wallet-rebuild"; break - fi - done + # (The dragonx-wallet-rebuild recovery helper is built into bin/ by CMake and packaged explicitly + # by each release path — required via require_wallet_rebuild_helper — so it is not copied here.) return $found } @@ -351,6 +364,10 @@ build_release_linux() { local BDB_ARGS=() if [[ -f "$lin_bdb/lib/libdb-6.2.a" && -f "$lin_bdb/include/db.h" ]]; then BDB_ARGS=( -DBDB_INCLUDE_DIR="$lin_bdb/include" -DBDB_LIBRARY="$lin_bdb/lib/libdb-6.2.a" ) + elif should_bundle_full_node_assets; then + err "Vendored Berkeley DB depends missing at $lin_bdb — the wallet-rebuild recovery helper cannot be built." + err " A full-node release must ship it; aborting rather than degrading recovery to Restore-only." + exit 1 fi info "Configuring ..." cmake "$SCRIPT_DIR" \ @@ -365,6 +382,9 @@ build_release_linux() { [[ -f "bin/${APP_BASENAME}" ]] || { err "Linux build failed"; exit 1; } + # A full-node release MUST include the recovery helper — fail loudly, never ship without it. + require_wallet_rebuild_helper "bin/dragonx-wallet-rebuild" "$lin_bdb" + info "Stripping ..." strip "bin/${APP_BASENAME}" [[ -f "bin/dragonx-wallet-rebuild" ]] && strip "bin/dragonx-wallet-rebuild" @@ -402,6 +422,9 @@ build_release_linux() { [[ -f bin/asmap.dat ]] && cp bin/asmap.dat "$dist_dir/" [[ -f bin/sapling-spend.params ]] && cp bin/sapling-spend.params "$dist_dir/" [[ -f bin/sapling-output.params ]] && cp bin/sapling-output.params "$dist_dir/" + # Offline wallet-rebuild recovery helper — required (asserted above); ships next to the app. + cp bin/dragonx-wallet-rebuild "$dist_dir/" && chmod +x "$dist_dir/dragonx-wallet-rebuild" + info " Bundled dragonx-wallet-rebuild" fi # Bundle xmrig for mining support local XMRIG_LINUX="$SCRIPT_DIR/prebuilt-binaries/drg-xmrig/xmrig" @@ -435,6 +458,8 @@ build_release_linux() { [[ -f bin/asmap.dat ]] && cp bin/asmap.dat "$APPDIR/usr/bin/" [[ -f bin/sapling-spend.params ]] && cp bin/sapling-spend.params "$APPDIR/usr/bin/" [[ -f bin/sapling-output.params ]] && cp bin/sapling-output.params "$APPDIR/usr/bin/" + # Offline wallet-rebuild recovery helper — required (asserted above); ships next to the app. + cp bin/dragonx-wallet-rebuild "$APPDIR/usr/bin/" && chmod +x "$APPDIR/usr/bin/dragonx-wallet-rebuild" fi # Bundle xmrig for mining support local XMRIG_LINUX_AI="$SCRIPT_DIR/prebuilt-binaries/drg-xmrig/xmrig" @@ -656,6 +681,30 @@ HDR info "Lite mode: skipping embedded daemon binaries" fi + # ── Wallet-rebuild recovery helper ─────────────────────────────── + # Built in-tree (not a prebuilt like the daemon), so compile it standalone HERE — before the + # main app compiles embedded_resources.cpp — and INCBIN it, so a bare, self-extracting + # ObsidianDragon.exe carries the recovery tool exactly like it does the daemon. + if should_bundle_full_node_assets; then + local WBDB="$SCRIPT_DIR/external/dragonx/depends/x86_64-w64-mingw32" + if [[ -f "$WBDB/lib/libdb-6.2.a" && -f "$WBDB/include/db.h" ]]; then + info "Compiling + embedding wallet-rebuild helper ..." + x86_64-w64-mingw32-g++ -std=c++17 -O2 -static -static-libgcc -static-libstdc++ \ + -I"$SCRIPT_DIR/src" -I"$WBDB/include" \ + "$SCRIPT_DIR/tools/wallet_rebuild/main.cpp" \ + "$WBDB/lib/libdb-6.2.a" -lws2_32 \ + -o "$RES/dragonx-wallet-rebuild.exe" \ + || { err "wallet-rebuild helper failed to compile for embedding"; exit 1; } + x86_64-w64-mingw32-strip "$RES/dragonx-wallet-rebuild.exe" 2>/dev/null || true + echo -e "\n#define HAS_EMBEDDED_WALLET_REBUILD 1" >> "$GEN/embedded_data.h" + echo "INCBIN(dragonx_wallet_rebuild_exe, \"$RES/dragonx-wallet-rebuild.exe\");" >> "$GEN/embedded_data.h" + info " Embedded dragonx-wallet-rebuild.exe ($(du -h "$RES/dragonx-wallet-rebuild.exe" | cut -f1))" + else + err "Vendored mingw Berkeley DB missing at $WBDB — cannot embed the wallet-rebuild recovery helper." + exit 1 + fi + fi + # ── xmrig binary (from prebuilt-binaries/drg-xmrig/) ──────────────── local XMRIG_DIR="$SCRIPT_DIR/prebuilt-binaries/drg-xmrig" # The published DRG-XMRig archives ship the binary inside a versioned subdir, not as a flat @@ -775,6 +824,10 @@ HDR local BDB_ARGS=() if [[ -f "$win_bdb/lib/libdb-6.2.a" && -f "$win_bdb/include/db.h" ]]; then BDB_ARGS=( -DBDB_INCLUDE_DIR="$win_bdb/include" -DBDB_LIBRARY="$win_bdb/lib/libdb-6.2.a" ) + elif should_bundle_full_node_assets; then + err "Vendored Berkeley DB depends missing at $win_bdb — the wallet-rebuild recovery helper cannot be built." + err " A full-node release must ship it; aborting rather than degrading recovery to Restore-only." + exit 1 fi info "Configuring (cross-compile) ..." cmake "$SCRIPT_DIR" \ @@ -791,6 +844,9 @@ HDR [[ -f "bin/${APP_BASENAME}.exe" ]] || { err "Windows build failed"; exit 1; } info "Binary: $(du -h "bin/${APP_BASENAME}.exe" | cut -f1)" + # A full-node release MUST include the recovery helper — fail loudly, never ship without it. + require_wallet_rebuild_helper "bin/dragonx-wallet-rebuild.exe" "$win_bdb" + # ── Package: release/windows/ ──────────────────────────────────────────── # Remove only THIS variant's prior artifacts so full-node and lite releases coexist here. mkdir -p "$out" @@ -806,8 +862,9 @@ HDR for f in dragonxd.exe dragonx-cli.exe dragonx-tx.exe; do [[ -f "$DD/$f" ]] && cp "$DD/$f" "$dist_dir/" done - # dragonx-wallet-rebuild helper (offline recovery for a BDB-inconsistent wallet.dat) - [[ -f "bin/dragonx-wallet-rebuild.exe" ]] && { cp "bin/dragonx-wallet-rebuild.exe" "$dist_dir/"; info " Bundled dragonx-wallet-rebuild.exe"; } + # dragonx-wallet-rebuild helper (offline recovery for a BDB-inconsistent wallet.dat) — required. + cp "bin/dragonx-wallet-rebuild.exe" "$dist_dir/" && info " Bundled dragonx-wallet-rebuild.exe" + [[ -f "$dist_dir/dragonx-wallet-rebuild.exe" ]] || { err "Failed to bundle dragonx-wallet-rebuild.exe"; exit 1; } # Bundle Sapling params + asmap for the zip distribution # (The single-file exe has these embedded via INCBIN, but the zip @@ -1063,6 +1120,11 @@ TOOLCHAIN [[ -f "bin/${APP_BASENAME}" ]] || { err "macOS build failed"; exit 1; } + # A full-node release MUST include the recovery helper. macOS needs a static libdb-6.2 (Homebrew + # berkeley-db for a native build, or a vendored external/dragonx/depends/) — otherwise CMake + # skips the target and this fails loudly rather than shipping a mac release with no recovery option. + require_wallet_rebuild_helper "bin/dragonx-wallet-rebuild" "$SCRIPT_DIR/external/dragonx/depends/aarch64-apple-darwin" + # Strip — use osxcross strip for cross-builds if $IS_CROSS; then local STRIP_CMD="${OSXCROSS}/target/bin/${OSXCROSS_TRIPLE}-strip" @@ -1135,6 +1197,8 @@ TOOLCHAIN else warn "prebuilt-binaries/dragonxd-mac/ not found — place macOS daemon binaries there for bundling" fi + # Offline wallet-rebuild recovery helper — required (asserted after build); next to the daemon. + cp "bin/dragonx-wallet-rebuild" "$MACOS/" && chmod +x "$MACOS/dragonx-wallet-rebuild" && info " Bundled dragonx-wallet-rebuild" else info "Lite mode: skipping macOS daemon and Sapling/asmap bundling" fi diff --git a/src/resources/embedded_resources.cpp b/src/resources/embedded_resources.cpp index f1f2c92..e144eb9 100644 --- a/src/resources/embedded_resources.cpp +++ b/src/resources/embedded_resources.cpp @@ -40,6 +40,9 @@ static const EmbeddedResource s_resources[] = { { g_dragonx_cli_exe_data, g_dragonx_cli_exe_size, RESOURCE_DRAGONX_CLI }, { g_dragonx_tx_exe_data, g_dragonx_tx_exe_size, RESOURCE_DRAGONX_TX }, #endif +#ifdef HAS_EMBEDDED_WALLET_REBUILD + { g_dragonx_wallet_rebuild_exe_data, g_dragonx_wallet_rebuild_exe_size, RESOURCE_DRAGONX_WALLET_REBUILD }, +#endif #ifdef HAS_EMBEDDED_XMRIG { g_xmrig_exe_data, g_xmrig_exe_size, RESOURCE_XMRIG }, #endif @@ -436,6 +439,24 @@ bool extractEmbeddedResources() } #endif +#ifdef HAS_EMBEDDED_WALLET_REBUILD + // Offline wallet-rebuild recovery helper — extracted next to the daemon so a bare, self-extracting + // ObsidianDragon.exe still offers "Repair automatically" (findWalletRebuildHelper() checks this dir). + const EmbeddedResource* rebuildRes = getEmbeddedResource(RESOURCE_DRAGONX_WALLET_REBUILD); + if (rebuildRes) { + std::string dest = daemonDir + pathSep + RESOURCE_DRAGONX_WALLET_REBUILD; + if (!std::filesystem::exists(dest)) { + DEBUG_LOGF("[INFO] Extracting dragonx-wallet-rebuild (%zu MB)...\n", rebuildRes->size / (1024*1024)); + if (!extractResource(rebuildRes, dest)) { + success = false; + } +#ifndef _WIN32 + else { chmod(dest.c_str(), 0755); } +#endif + } + } +#endif + // Best-effort cleanup of any ".old" binaries left behind by a previous in-use replacement. // Once the old daemon/xmrig process has exited, the file is no longer locked and removes cleanly; // if it's still running, the remove fails harmlessly and we retry on the next startup. @@ -450,6 +471,32 @@ bool extractEmbeddedResources() return success; } +std::string ensureWalletRebuildHelperExtracted() +{ +#ifdef HAS_EMBEDDED_WALLET_REBUILD + const EmbeddedResource* res = getEmbeddedResource(RESOURCE_DRAGONX_WALLET_REBUILD); + if (!res || res->size == 0) return {}; +#ifdef _WIN32 + const char sep = '\\'; +#else + const char sep = '/'; +#endif + const std::string dir = getDaemonDirectory(); + const std::string dest = dir + sep + RESOURCE_DRAGONX_WALLET_REBUILD; + std::error_code ec; + if (std::filesystem::exists(dest, ec)) return dest; // already extracted + std::filesystem::create_directories(dir, ec); + if (!extractResource(res, dest)) return {}; +#ifndef _WIN32 + chmod(dest.c_str(), 0755); +#endif + DEBUG_LOGF("[INFO] Extracted wallet-rebuild helper on demand: %s\n", dest.c_str()); + return dest; +#else + return {}; +#endif +} + std::string getDaemonDirectory() { // Daemon binaries live in %APPDATA%/ObsidianDragon/dragonx/ (Windows) or diff --git a/src/resources/embedded_resources.h b/src/resources/embedded_resources.h index a45b13d..ae8f0a7 100644 --- a/src/resources/embedded_resources.h +++ b/src/resources/embedded_resources.h @@ -55,6 +55,12 @@ BundledDaemonInfo getBundledDaemonInfo(); // caller should stop the daemon first. Returns true if all present resources were written. bool reextractBundledDaemon(); +// Ensure the embedded offline wallet-rebuild recovery helper is extracted to the daemon dir, and +// return its path ("" if not embedded in this build or extraction failed). Idempotent — extracts only +// when missing. Unlike the first-run extractEmbeddedResources() (gated on needsParamsExtraction()), +// this runs on demand so recovery works from a self-contained exe on ANY run, not just the first. +std::string ensureWalletRebuildHelperExtracted(); + // Resource names constexpr const char* RESOURCE_SAPLING_SPEND = "sapling-spend.params"; constexpr const char* RESOURCE_SAPLING_OUTPUT = "sapling-output.params"; @@ -62,6 +68,7 @@ constexpr const char* RESOURCE_ASMAP = "asmap.dat"; constexpr const char* RESOURCE_DRAGONXD = "dragonxd.exe"; constexpr const char* RESOURCE_DRAGONX_CLI = "dragonx-cli.exe"; constexpr const char* RESOURCE_DRAGONX_TX = "dragonx-tx.exe"; +constexpr const char* RESOURCE_DRAGONX_WALLET_REBUILD = "dragonx-wallet-rebuild.exe"; constexpr const char* RESOURCE_XMRIG = "xmrig.exe"; constexpr const char* RESOURCE_DARK_GRADIENT = "dark_gradient.png"; constexpr const char* RESOURCE_LOGO = "logo_ObsidianDragon_dark.png";