diff --git a/CMakeLists.txt b/CMakeLists.txt index 31fd4fb..6774e0e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1075,6 +1075,26 @@ install(DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/res OPTIONAL ) +# ----------------------------------------------------------------------------- +# dragonx-wallet-rebuild — offline recovery helper for a BDB-inconsistent wallet.dat. +# Bundled next to the daemon; the app spawns it out-of-process. It is the ONLY thing that links +# Berkeley DB, so the AGPLv3 BDB never contaminates the GPLv3 GUI (same boundary as the daemon). +# Release builds should point DRAGONX_BDB_ROOT at the vendored static libdb (external/dragonx/depends); +# a dev build falls back to the system Berkeley DB. Skipped (with a note) if no BDB is found. +# ----------------------------------------------------------------------------- +find_path(BDB_INCLUDE_DIR db.h HINTS ${DRAGONX_BDB_ROOT}/include /usr/include /usr/local/include) +find_library(BDB_LIBRARY NAMES db-6.2 db-6.0 db-5.3 db libdb + HINTS ${DRAGONX_BDB_ROOT}/lib /usr/lib /usr/local/lib /usr/lib/x86_64-linux-gnu) +if(BDB_INCLUDE_DIR AND BDB_LIBRARY) + add_executable(dragonx-wallet-rebuild tools/wallet_rebuild/main.cpp) + target_include_directories(dragonx-wallet-rebuild PRIVATE ${CMAKE_SOURCE_DIR}/src ${BDB_INCLUDE_DIR}) + target_link_libraries(dragonx-wallet-rebuild PRIVATE ${BDB_LIBRARY}) + set_target_properties(dragonx-wallet-rebuild PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) + message(STATUS "wallet-rebuild helper: ON (Berkeley DB ${BDB_LIBRARY})") +else() + message(STATUS "wallet-rebuild helper: OFF (no Berkeley DB found; set DRAGONX_BDB_ROOT for release builds)") +endif() + # ----------------------------------------------------------------------------- # Tests # ----------------------------------------------------------------------------- diff --git a/src/app.cpp b/src/app.cpp index eed5389..a504d23 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -4303,8 +4303,17 @@ void App::renderWalletRecoveredDialog() ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm())); ImGui::TextWrapped("%s", TR("wallet_recovered_body")); ImGui::Dummy(ImVec2(0, ui::Layout::spacingMd())); - // Primary: one-click restore of the untouched original (stops the node, swaps the .bak back over the - // salvaged copy, clears the stale BDB env, restarts). Copy/rename-only — nothing is deleted. + // Preferred fix when available: REBUILD the wallet database. Plain "Restore original" hands the same + // BDB-inconsistent file back and the daemon just re-salvages it (the cascade); the rebuild produces a + // fresh, consistent copy of every key that the daemon loads cleanly. Copy/rename-only — never deletes. + if (walletRebuildAvailable()) { + if (ui::material::TactileButton(TR("wallet_recovered_rebuild"), ImVec2(280.0f * dp, 0))) { + rebuildWalletDatabase(); // clears show_wallet_recovered_dialog_ + } + ImGui::Dummy(ImVec2(0, ui::Layout::spacingSm())); + } + // Restore of the untouched original (stops the node, swaps the .bak back over the salvaged copy, + // clears the stale BDB env, restarts). if (ui::material::TactileButton(TR("wallet_recovered_restore"), ImVec2(260.0f * dp, 0))) { restoreOriginalWallet(); // clears show_wallet_recovered_dialog_ } diff --git a/src/app.h b/src/app.h index dbb50a5..9f66b7b 100644 --- a/src/app.h +++ b/src/app.h @@ -1345,7 +1345,9 @@ private: void reindexBlockDatabase(); // restart the daemon with -reindex to rebuild the block DB void renderWalletRecoveredDialog(); // warn that the node auto-recovered/salvaged wallet.dat void restoreOriginalWallet(); // swap the wallet..bak back over the salvaged copy + restart - void pumpWalletRestore(); // main-thread: surface the restore op's result + void pumpWalletRestore(); // main-thread: surface the restore/rebuild op's result + void rebuildWalletDatabase(); // rebuild a BDB-inconsistent wallet into a loadable one (helper) + bool walletRebuildAvailable() const; // the dragonx-wallet-rebuild helper is present void processDeferredEncryption(); // Private methods - connection diff --git a/src/app_network.cpp b/src/app_network.cpp index 04007ff..e7bd35a 100644 --- a/src/app_network.cpp +++ b/src/app_network.cpp @@ -59,6 +59,8 @@ #include "data/seed_migration_resume.h" #include "util/platform.h" #include "util/wallet_file_probe.h" // verify a salvage-backup is a real BDB before restoring it +#include "resources/embedded_resources.h" // getDaemonDirectory() — locate the wallet-rebuild helper +#include // popen the rebuild helper #include "util/perf_log.h" #include "util/i18n.h" #include "util/secure_vault.h" @@ -4670,7 +4672,159 @@ void App::pumpWalletRestore() if (!done) return; if (sev == 2) ui::Notifications::instance().error(msg, 25.0f); else if (sev == 1) ui::Notifications::instance().warning(msg, 20.0f); - else ui::Notifications::instance().success(TR("wallet_restore_ok"), 12.0f); + else ui::Notifications::instance().success(msg.empty() ? TR("wallet_restore_ok") : msg, 12.0f); +} + +// Locate the bundled dragonx-wallet-rebuild helper (exe dir → daemon dir). "" if not present. +static std::string findWalletRebuildHelper() +{ + namespace fs = std::filesystem; +#ifdef _WIN32 + const char* exe = "dragonx-wallet-rebuild.exe"; +#else + const char* exe = "dragonx-wallet-rebuild"; +#endif + for (const std::string& d : { util::Platform::getExecutableDirectory(), + dragonx::resources::getDaemonDirectory() }) { + if (d.empty()) continue; + std::error_code ec; + const std::string p = d + "/" + exe; + if (fs::exists(p, ec)) return p; + } + return {}; +} + +bool App::walletRebuildAvailable() const { return !findWalletRebuildHelper().empty(); } + +// Rebuild a BDB-inconsistent wallet into a fresh, daemon-loadable one via the offline helper (see +// tools/wallet_rebuild). This is the real fix for the salvage cascade: plain "Restore original" just +// hands the same broken file back and the daemon re-salvages it. Modeled on restoreOriginalWallet: +// stop daemon → run helper → verify → safe swap (copy/rename only, never delete) → rescan → restart. +void App::rebuildWalletDatabase() +{ + if (!supportsFullNodeLifecycleActions()) { + ui::Notifications::instance().warning("Full-node lifecycle actions are unavailable in lite build"); + return; + } + if (daemon_restarting_) { ui::Notifications::instance().warning(TR("wallet_restore_busy")); return; } + const std::string helper = findWalletRebuildHelper(); + if (helper.empty()) { ui::Notifications::instance().error(TR("wallet_rebuild_no_helper"), 15.0f); return; } + + show_wallet_recovered_dialog_ = false; + { std::lock_guard lk(wallet_restore_mutex_); wallet_restore_done_ = false; } + daemon_restarting_ = true; + connection_status_ = TR("sb_restarting_daemon"); + if (rpc_ && rpc_->isConnected()) rpc_->disconnect(); + onDisconnected("Rebuilding wallet database"); + ui::Notifications::instance().info(TR("wallet_rebuild_started"), 15.0f); + + const std::string activeWalletName = (settings_ && !settings_->getActiveWalletFile().empty()) + ? settings_->getActiveWalletFile() : std::string("wallet.dat"); + + async_tasks_.submit("Rebuild wallet database", [this, helper, activeWalletName](const util::AsyncTaskManager::Token&) { + namespace fs = std::filesystem; + std::string err, warn; + try { + const std::string datadir = util::Platform::getDragonXDataDir(); + const std::string active = datadir + "/" + activeWalletName; + // 1. Rebuild SOURCE = the largest readable wallet file (the active wallet or any salvage .bak). + // Largest = most records = the original / least-salvaged (a salvaged copy is tiny). + std::string src; unsigned long long best = 0; + { + std::error_code ec; + for (const auto& e : fs::directory_iterator(datadir, ec)) { + if (ec) break; + const std::string n = e.path().filename().string(); + if (n != activeWalletName && daemon::parseWalletSalvageBakTs(n) < 0) continue; + std::error_code se; + const auto sz = fs::is_regular_file(e, se) ? fs::file_size(e, se) : 0; + const auto usz = se ? 0ull : static_cast(sz); + if (usz > best && util::probeWalletFile(e.path().string()).isBerkeleyDB) { + best = usz; src = e.path().string(); + } + } + } + if (src.empty()) { + err = TR("wallet_rebuild_no_source"); + } else if (!stopDaemonForWalletSwitch()) { // 2. release wallet.dat + the port + err = TR("wallet_restore_stop_failed"); + } else { + std::time_t t = std::time(nullptr); + std::tm tmv{}; +#ifdef _WIN32 + localtime_s(&tmv, &t); +#else + localtime_r(&t, &tmv); +#endif + char ts[32]; std::strftime(ts, sizeof(ts), "%Y%m%d-%H%M%S", &tmv); + const std::string tmpOut = datadir + "/wallet.rebuilt-" + std::string(ts) + ".tmp"; + { std::error_code ec; fs::remove(tmpOut, ec); } // helper uses DB_EXCL — path must be fresh + + // 3. Run the helper (src -> tmpOut). Quote both paths; capture its JSON line. + std::string cmd = "\"" + helper + "\" \"" + src + "\" \"" + tmpOut + "\""; +#ifdef _WIN32 + cmd = "\"" + cmd + "\""; // cmd.exe strips the outermost quotes + FILE* fp = _popen(cmd.c_str(), "r"); +#else + FILE* fp = popen(cmd.c_str(), "r"); +#endif + std::string jout; + if (fp) { char b[512]; while (std::fgets(b, sizeof b, fp)) jout += b; } +#ifdef _WIN32 + const int rc = fp ? _pclose(fp) : -1; +#else + const int rc = fp ? pclose(fp) : -1; +#endif + DEBUG_LOGF("[App] wallet-rebuild helper rc=%d out=%s\n", rc, jout.c_str()); + + // 4. Verify-before-swap: the output must be a readable BDB with the fund-critical keys. + const auto probe = util::parseWalletBtree(tmpOut); + if (rc != 0 || !probe.parsed || probe.addresses() == 0) { + std::error_code ec; fs::remove(tmpOut, ec); + err = TR("wallet_rebuild_failed"); + } else { + // 5. Swap: set the current wallet aside (kept), install the rebuilt one, clear stale env. + std::error_code ec; + const std::string aside = active + ".prerebuild-" + std::string(ts) + ".dat"; + bool moved = false; + if (fs::exists(active)) { + fs::rename(active, aside, ec); + if (ec) err = TR("wallet_restore_move_failed"); else moved = true; + } + if (err.empty()) { + fs::rename(tmpOut, active, ec); + if (ec) { + if (moved) { std::error_code e2; fs::rename(aside, active, e2); } + err = TR("wallet_rebuild_install_failed"); + } + } + if (err.empty()) { + std::error_code e2; + if (fs::exists(datadir + "/database")) + fs::rename(datadir + "/database", datadir + "/database.prerebuild-" + std::string(ts) + ".bak", e2); + for (const auto& e : fs::directory_iterator(datadir, e2)) + if (e.path().filename().string().rfind("__db.", 0) == 0) { std::error_code e3; fs::remove(e.path(), e3); } + if (daemon_controller_) daemon_controller_->setRescanOnNextStart(true); + } + } + } + + if (!shutting_down_) { + if (daemon_controller_) daemon_controller_->clearExternalDaemonDetected(); + if (!startEmbeddedDaemon() && err.empty()) warn = TR("wallet_restore_no_restart"); + } + } catch (const std::exception& e) { + err = std::string("Rebuild failed: ") + e.what(); + } catch (...) { + err = "Rebuild failed due to an unexpected error."; + } + daemon_restarting_ = false; + + std::lock_guard lk(wallet_restore_mutex_); + wallet_restore_severity_ = !err.empty() ? 2 : (!warn.empty() ? 1 : 0); + wallet_restore_msg_ = !err.empty() ? err : (!warn.empty() ? warn : std::string(TR("wallet_rebuild_ok"))); + wallet_restore_done_ = true; + }); } void App::pumpSeedMigration() diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index 2d0e1fe..87b5de3 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -1211,6 +1211,14 @@ void I18n::loadBuiltinEnglish() strings_["wallet_restore_move_failed"] = "Couldn't set the current wallet aside — nothing was changed."; strings_["wallet_restore_copy_failed"] = "Couldn't install the backup wallet; your current wallet was left in place."; strings_["wallet_restore_no_restart"] = "Your original wallet was restored, but the node didn't restart — start it from Settings."; + // One-click "Rebuild wallet database" flow (fixes a BDB-inconsistent wallet that keeps getting salvaged). + strings_["wallet_recovered_rebuild"] = "Rebuild wallet database (recommended)"; + strings_["wallet_rebuild_started"] = "Rebuilding your wallet database and restarting the node…"; + strings_["wallet_rebuild_ok"] = "Wallet database rebuilt — the node is loading it and rescanning for your balance."; + strings_["wallet_rebuild_no_helper"] = "The wallet-rebuild helper isn't available in this build. Use Restore, or rebuild manually."; + strings_["wallet_rebuild_no_source"] = "Couldn't find a readable wallet to rebuild. Nothing was changed."; + strings_["wallet_rebuild_failed"] = "The rebuild didn't produce a valid wallet, so nothing was changed. Your wallet is untouched."; + strings_["wallet_rebuild_install_failed"] = "Couldn't install the rebuilt wallet; your current wallet was left in place."; // Receive Tab strings_["receiving_addresses"] = "Your Receiving Addresses"; diff --git a/src/util/wallet_file_probe.h b/src/util/wallet_file_probe.h index 9f3b100..fa22130 100644 --- a/src/util/wallet_file_probe.h +++ b/src/util/wallet_file_probe.h @@ -339,5 +339,155 @@ inline WalletBtreeStats parseWalletBtree(const std::string& path, return st; } +// ───────────────────────────────────────────────────────────────────────────────────────────────── +// Tier 3: collect the raw (key,value) record BYTES — the read half of the offline wallet REBUILD that +// recovers a BDB-inconsistent wallet.dat (stale extent metadata: our tolerant walk reads records the +// daemon's Berkeley DB verify rejects and auto-salvages). A helper then writes these verbatim into a +// fresh, consistent BDB so the daemon loads it cleanly. Records are copied byte-for-byte — encrypted +// key material (ckey/csapzkey/mkey) passes through as opaque ciphertext, so no passphrase is needed. +// Values that live in BDB OVERFLOW pages (only large `tx` history records) are NOT captured (skipped + +// counted); they are irrelevant to funds — a rescan rebuilds transaction history. Same bounds-checked, +// subdb-aware, visited-set-capped walk as parseWalletBtree. +struct WalletRawRecords { + bool parsed = false; ///< the btree walked cleanly + bool complete = false; ///< the whole file was read (not cap-truncated) + std::vector> records; ///< inline (key,value) bytes, verbatim + int keyRecords = 0; ///< fund-critical key-type records captured (key/wkey/ckey/z*/sap*/hdseed) + int skippedOverflow = 0; ///< records whose value spilled to overflow pages (tx history) — not captured + std::size_t bytesRead = 0; +}; + +inline WalletRawRecords extractWalletBtreeRecords(const std::string& path, + std::size_t maxBytes = 512u * 1024u * 1024u) { + WalletRawRecords out; + std::ifstream f(path, std::ios::binary); + if (!f) return out; + std::string buf; + { + f.seekg(0, std::ios::end); + std::streamoff sz = f.tellg(); + if (sz < 512) return out; + const std::size_t want = std::min(static_cast(sz), maxBytes); + f.seekg(0, std::ios::beg); + buf.resize(want); + f.read(&buf[0], static_cast(want)); + buf.resize(static_cast(std::max(0, f.gcount()))); + if (buf.size() < 512) return out; + out.bytesRead = buf.size(); + out.complete = (buf.size() == static_cast(sz)); + } + const unsigned char* B = reinterpret_cast(buf.data()); + const std::size_t N = buf.size(); + + auto rd32at = [&](std::size_t o, bool le) -> uint32_t { + return le ? (uint32_t)B[o] | ((uint32_t)B[o+1]<<8) | ((uint32_t)B[o+2]<<16) | ((uint32_t)B[o+3]<<24) + : (uint32_t)B[o+3] | ((uint32_t)B[o+2]<<8) | ((uint32_t)B[o+1]<<16) | ((uint32_t)B[o]<<24); + }; + constexpr uint32_t kBtreeMagic = 0x00053162u; + bool le; + if (rd32at(12, true) == kBtreeMagic) le = true; + else if (rd32at(12, false) == kBtreeMagic) le = false; + else return out; + auto r32 = [&](std::size_t o) { return o + 4 <= N ? rd32at(o, le) : 0u; }; + auto r16 = [&](std::size_t o) -> uint32_t { + if (o + 2 > N) return 0; + return le ? (uint32_t)B[o] | ((uint32_t)B[o+1]<<8) : (uint32_t)B[o+1] | ((uint32_t)B[o]<<8); + }; + const uint32_t pagesize = r32(20); + if (pagesize < 512 || pagesize > 65536 || (pagesize & (pagesize - 1)) != 0) return out; + const uint32_t npages = static_cast(N / pagesize); + const uint32_t root = r32(88); + if (npages == 0 || root == 0 || root >= npages) return out; + if (B[24] != 0 || (B[26] & 0x01)) return out; // page checksum/encryption — offsets shift; bail + + constexpr uint8_t P_IBTREE = 3, P_LBTREE = 5, P_BTREEMETA = 9, B_KEYDATA = 1; + constexpr std::size_t kMaxPagesVisited = 600000; + constexpr int kMaxKeys = 4000000; + std::vector visited(npages, false); + std::size_t pagesVisited = 0; + int keys = 0; + bool aborted = false; + auto rdpgno = [&](const unsigned char* p) -> uint32_t { + return le ? (uint32_t)p[0] | ((uint32_t)p[1]<<8) | ((uint32_t)p[2]<<16) | ((uint32_t)p[3]<<24) + : (uint32_t)p[3] | ((uint32_t)p[2]<<8) | ((uint32_t)p[1]<<16) | ((uint32_t)p[0]<<24); + }; + auto traverse = [&](uint32_t rootPg, auto&& fn) { + std::fill(visited.begin(), visited.end(), false); + std::vector stack; + if (rootPg < npages && !visited[rootPg]) { visited[rootPg] = true; stack.push_back(rootPg); } + while (!stack.empty()) { + const uint32_t pg = stack.back(); stack.pop_back(); + if (pg >= npages) continue; + if (++pagesVisited > kMaxPagesVisited) { aborted = true; return; } + const std::size_t base = static_cast(pg) * pagesize; + if (base + 26 > N) continue; + const uint8_t type = B[base + 25]; + const uint32_t entries = r16(base + 20); + if (26 + static_cast(entries) * 2 > pagesize) continue; + if (type == P_IBTREE) { + for (uint32_t i = 0; i < entries; ++i) { + const uint32_t off = r16(base + 26 + i * 2); + if (off + 8 > pagesize) continue; + const uint32_t child = r32(base + off + 4); + if (child > 0 && child < npages && !visited[child]) { visited[child] = true; stack.push_back(child); } + } + } else if (type == P_LBTREE) { + for (uint32_t i = 0; i + 1 < entries; i += 2) { + if (++keys > kMaxKeys) { aborted = true; return; } + const uint32_t ko = r16(base + 26 + i * 2); + const uint32_t dO = r16(base + 26 + (i + 1) * 2); + if (ko + 3 > pagesize || dO + 3 > pagesize) continue; + if (B[base + ko + 2] != B_KEYDATA) continue; // overflow/dup key — never a record name + const uint32_t kl = r16(base + ko); + if (kl < 1 || ko + 3 + kl > pagesize) continue; + const uint8_t dtype = B[base + dO + 2]; + const uint32_t dl = r16(base + dO); + const unsigned char* dp = (dO + 3 + dl <= pagesize) ? B + base + dO + 3 : nullptr; + fn(B + base + ko + 3, kl, dp, dl, dtype); + } + } + } + }; + + // Master DB: subdb-name → subdb meta/root pgno (big-endian value; native fallback), same as parseWalletBtree. + std::vector subRoots; + traverse(root, [&](const unsigned char*, uint32_t, const unsigned char* dp, uint32_t dl, uint8_t dtype) { + if (dtype != B_KEYDATA || dl != 4 || !dp) return; + const uint32_t cand[2] = { + (uint32_t)dp[3] | ((uint32_t)dp[2]<<8) | ((uint32_t)dp[1]<<16) | ((uint32_t)dp[0]<<24), + rdpgno(dp), + }; + for (const uint32_t pgno : cand) { + if (pgno == 0 || pgno >= npages) continue; + const uint8_t pt = B[static_cast(pgno) * pagesize + 25]; + if (pt == P_BTREEMETA) { + const uint32_t sr = r32(static_cast(pgno) * pagesize + 88); + if (sr > 0 && sr < npages) { subRoots.push_back(sr); break; } + } else if (pt == P_LBTREE || pt == P_IBTREE) { subRoots.push_back(pgno); break; } + } + }); + if (aborted) return out; + if (subRoots.empty()) subRoots.push_back(root); + + auto isKeyType = [](const char* nm, uint32_t nl) { + auto is = [&](const char* s) { return std::strlen(s) == nl && std::memcmp(nm, s, nl) == 0; }; + return is("key") || is("wkey") || is("ckey") || is("zkey") || is("czkey") + || is("sapzkey") || is("csapzkey") || is("hdseed") || is("chdseed"); + }; + for (const uint32_t sr : subRoots) { + traverse(sr, [&](const unsigned char* kp, uint32_t kl, const unsigned char* dp, uint32_t dl, uint8_t dt) { + if (dt != B_KEYDATA || !dp) { out.skippedOverflow++; return; } // overflow value (tx history) — skip + out.records.emplace_back(std::string(reinterpret_cast(kp), kl), + std::string(reinterpret_cast(dp), dl)); + const uint32_t nlen = kp[0]; + if (nlen >= 2 && nlen <= 20 && 1u + nlen <= kl && isKeyType(reinterpret_cast(kp + 1), nlen)) + out.keyRecords++; + }); + if (aborted) return out; + } + out.parsed = true; + return out; +} + } // namespace util } // namespace dragonx diff --git a/tests/test_phase4.cpp b/tests/test_phase4.cpp index d065651..321c783 100644 --- a/tests/test_phase4.cpp +++ b/tests/test_phase4.cpp @@ -845,6 +845,18 @@ void testWalletFileProbe() EXPECT_EQ(s.txCount, 1); EXPECT_EQ(s.transparentKeys, 1); EXPECT_EQ(s.addresses(), 1); EXPECT_EQ(s.createdEpoch, kCreated); EXPECT_FALSE(dragonx::util::parseWalletBtree((dir / "junk.dat").string()).parsed); + + // Byte-collecting reader (the wallet-REBUILD read half): same walk, but collects (key,value) bytes. + auto ex = dragonx::util::extractWalletBtreeRecords((dir / "btree.dat").string()); + EXPECT_TRUE(ex.parsed); + EXPECT_TRUE(ex.records.size() >= static_cast(3)); // tx + key + keymeta captured verbatim + EXPECT_EQ(ex.keyRecords, 1); // the "key" record is fund-critical + bool foundKeyRec = false; // value copied byte-for-byte, name intact + for (const auto& kv : ex.records) + if (kv.first.size() >= 4 && (unsigned char)kv.first[0] == 3 && kv.first.compare(1, 3, "key") == 0 + && !kv.second.empty()) foundKeyRec = true; + EXPECT_TRUE(foundKeyRec); + EXPECT_FALSE(dragonx::util::extractWalletBtreeRecords((dir / "junk.dat").string()).parsed); } // 8) Mnemonic-flag decode (hdChainMnemonicFlag): the fMnemonicSeed byte lives at offset 52 of the diff --git a/tools/wallet_rebuild/main.cpp b/tools/wallet_rebuild/main.cpp new file mode 100644 index 0000000..6586f6b --- /dev/null +++ b/tools/wallet_rebuild/main.cpp @@ -0,0 +1,90 @@ +// dragonx-wallet-rebuild — offline recovery helper for a BDB-inconsistent wallet.dat. +// +// Some wallet.dat files are valid Berkeley DB btrees whose EXTENT metadata is stale (the "main" +// subdatabase metapage records a low last_pgno while its live data spans thousands of pages further +// in the file). A tolerant page-walk reads every record, but the daemon's Berkeley DB `verify` +// rejects the file and auto-salvages it — which finds nothing and, on each restart, shrinks the +// wallet to empty (the "salvage cascade" that looks like fund loss). The keys are intact; only the +// DB envelope is broken. +// +// This tool fixes it the way it must be fixed — offline, on the file, before any daemon touches it: +// 1) read all (key,value) records with the tolerant walker (util/wallet_file_probe.h), then +// 2) write them VERBATIM into a fresh, consistent Berkeley DB "main" btree via real libdb put()s, +// so libdb computes correct extent/metapage bookkeeping itself (sidestepping the whole defect). +// Records are copied byte-for-byte: encrypted key material (ckey/csapzkey/mkey) passes through as +// opaque ciphertext — no passphrase, no decryption, no key material ever interpreted. Only large `tx` +// history values (which live in BDB overflow pages) are skipped; a wallet rescan rebuilds those. +// +// Usage: dragonx-wallet-rebuild +// Output: a single JSON line on stdout; exit 0 on success, non-zero on failure. Never touches the +// source (opens it read-only); refuses to overwrite an existing output (DB_EXCL). + +#include "util/wallet_file_probe.h" + +#include + +#include +#include +#include + +int main(int argc, char** argv) +{ + if (argc < 3) { + std::fprintf(stderr, "usage: dragonx-wallet-rebuild \n"); + return 2; + } + const char* src = argv[1]; + const char* dst = argv[2]; + + // --- 1) tolerant read (no libdb; reads records the daemon's BDB can't) --- + const auto rec = dragonx::util::extractWalletBtreeRecords(src); + if (!rec.parsed) { + std::printf("{\"ok\":false,\"error\":\"source is not a readable Berkeley DB btree wallet\"}\n"); + return 3; + } + if (rec.keyRecords == 0) { + // Refuse to produce a keyless wallet — nothing to recover, and installing it would look like loss. + std::printf("{\"ok\":false,\"error\":\"no key records found in source\",\"read\":%zu}\n", + rec.records.size()); + return 4; + } + + // --- 2) write a fresh, consistent BDB "main" btree (what CWalletDB expects) --- + DB* db = nullptr; + int r = db_create(&db, nullptr, 0); + if (r != 0) { + std::printf("{\"ok\":false,\"error\":\"db_create: %s\"}\n", db_strerror(r)); + return 5; + } + // DB_EXCL: never clobber an existing file — the caller passes a fresh path. + r = db->open(db, nullptr, dst, "main", DB_BTREE, DB_CREATE | DB_EXCL, 0600); + if (r != 0) { + std::printf("{\"ok\":false,\"error\":\"open output: %s\"}\n", db_strerror(r)); + db->close(db, 0); + return 6; + } + long wrote = 0; + for (const auto& kv : rec.records) { + DBT k, v; + std::memset(&k, 0, sizeof k); + std::memset(&v, 0, sizeof v); + k.data = const_cast(kv.first.data()); k.size = static_cast(kv.first.size()); + v.data = const_cast(kv.second.data()); v.size = static_cast(kv.second.size()); + r = db->put(db, nullptr, &k, &v, 0); + if (r != 0) { + std::printf("{\"ok\":false,\"error\":\"put failed: %s\",\"wrote\":%ld}\n", db_strerror(r), wrote); + db->close(db, 0); + return 7; + } + ++wrote; + } + r = db->close(db, 0); // close flushes correct metadata + if (r != 0) { + std::printf("{\"ok\":false,\"error\":\"close: %s\",\"wrote\":%ld}\n", db_strerror(r), wrote); + return 8; + } + + std::printf("{\"ok\":true,\"read\":%zu,\"keyRecords\":%d,\"skippedOverflow\":%d,\"wrote\":%ld,\"complete\":%s}\n", + rec.records.size(), rec.keyRecords, rec.skippedOverflow, wrote, rec.complete ? "true" : "false"); + return 0; +}