From 975650f11b3d872789c4d5853c4af3dbe0412a08 Mon Sep 17 00:00:00 2001 From: DanS Date: Mon, 10 Aug 2026 00:52:41 -0500 Subject: [PATCH] fix(node): restore the LARGEST salvage backup, not the newest (salvage cascade) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "Restore original wallet" action picked the newest wallet..bak — but the daemon auto-salvages on every failed BDB verify, and each round SHRINKS the wallet (salvage keeps only readable records + drops the dead-page bloat). In a cascade the newest .bak is the most-degraded (seen in the wild as "Salvage found no records") while the original is the oldest and by far the largest. Pick by file SIZE instead: add largestWalletSalvageBak((name,size) pairs) — the largest wallet..bak is the least-salvaged, i.e. the pristine original (an emptied salvage is tiny; a real wallet is large); ties break to the newest ts. Factor the shared parse into parseWalletSalvageBakTs(). restoreOriginalWallet() now gathers file sizes and uses it (still verifies the pick is a valid BDB before swapping). newestWalletSalvageBak kept for reference. Adds a cascade regression test (a 40KB emptied newest .bak must NOT win over the 194MB original). Suite green (1/1). Co-Authored-By: Claude Opus 4.8 (1M context) --- src/app_network.cpp | 17 +++++++--- src/daemon/daemon_startup_diagnosis.h | 46 ++++++++++++++++++++------- tests/test_phase4.cpp | 12 +++++++ 3 files changed, 59 insertions(+), 16 deletions(-) diff --git a/src/app_network.cpp b/src/app_network.cpp index 2b0e088..04007ff 100644 --- a/src/app_network.cpp +++ b/src/app_network.cpp @@ -4575,14 +4575,21 @@ void App::restoreOriginalWallet() std::string err, warn; try { const std::string datadir = util::Platform::getDragonXDataDir(); - // 1. Find the newest salvage backup (offline — no daemon needed). - std::vector files; + // 1. Find the LARGEST salvage backup (offline — no daemon needed). Largest = least-salvaged = + // the original: a salvage cascade shrinks the wallet each round, so the newest .bak can be + // empty ("Salvage found no records") while the original is untouched and huge. + std::vector> files; { std::error_code lec; - for (const auto& e : fs::directory_iterator(datadir, lec)) - if (!lec) files.push_back(e.path().filename().string()); + for (const auto& e : fs::directory_iterator(datadir, lec)) { + if (lec) break; + std::error_code se; + const auto sz = fs::is_regular_file(e, se) ? fs::file_size(e, se) : 0; + files.emplace_back(e.path().filename().string(), + se ? 0ull : static_cast(sz)); + } } - const std::string bak = daemon::newestWalletSalvageBak(files); + const std::string bak = daemon::largestWalletSalvageBak(files); if (bak.empty()) { err = TR("wallet_restore_no_backup"); } else if (!util::probeWalletFile(datadir + "/" + bak).isBerkeleyDB) { diff --git a/src/daemon/daemon_startup_diagnosis.h b/src/daemon/daemon_startup_diagnosis.h index 38a1883..5307865 100644 --- a/src/daemon/daemon_startup_diagnosis.h +++ b/src/daemon/daemon_startup_diagnosis.h @@ -8,6 +8,7 @@ #pragma once #include +#include #include namespace dragonx { @@ -39,26 +40,49 @@ inline bool walletAutoRecovered(const std::string& out) || out.find("wallet.dat corrupt, salvage failed") != std::string::npos; // RECOVER_FAIL } -// From a list of datadir filenames, pick the most recent daemon salvage backup — the "wallet..bak" -// the auto-recovery just created (highest timestamp). Returns "" if none present. Pure, so it's testable. +// If `name` is a daemon salvage backup "wallet..bak", return its timestamp; else -1. +inline long long parseWalletSalvageBakTs(const std::string& name) +{ + if (name.rfind("wallet.", 0) != 0) return -1; // must start "wallet." + if (name.size() < 12 || name.compare(name.size() - 4, 4, ".bak") != 0) return -1; // ...and end ".bak" + const std::string mid = name.substr(7, name.size() - 7 - 4); // digits between the dots + if (mid.empty() || mid.size() > 18) return -1; + for (char c : mid) if (c < '0' || c > '9') return -1; + long long ts = 0; + for (char c : mid) ts = ts * 10 + (c - '0'); + return ts; +} + +// Most RECENT salvage backup (highest timestamp). Pure, testable. inline std::string newestWalletSalvageBak(const std::vector& filenames) { long long best = -1; std::string bestName; for (const auto& f : filenames) { - if (f.rfind("wallet.", 0) != 0) continue; // must start "wallet." - if (f.size() < 12 || f.compare(f.size() - 4, 4, ".bak") != 0) continue; // ...and end ".bak" - const std::string mid = f.substr(7, f.size() - 7 - 4); // digits between the dots - if (mid.empty() || mid.size() > 18) continue; - bool allDigits = true; - for (char c : mid) if (c < '0' || c > '9') { allDigits = false; break; } - if (!allDigits) continue; - long long ts = 0; - for (char c : mid) ts = ts * 10 + (c - '0'); + const long long ts = parseWalletSalvageBakTs(f); if (ts > best) { best = ts; bestName = f; } } return bestName; } +// LARGEST salvage backup, from (filename, fileSize) pairs — the least-salvaged one, i.e. the original. +// This is what "Restore original wallet" should use: a salvage CASCADE shrinks the wallet each round, so +// the newest .bak is the WORST and the largest is the pristine pre-salvage original (an emptied salvage +// is tiny; a real wallet is large). Ties break toward the newest timestamp. Returns "" if none present. +inline std::string largestWalletSalvageBak(const std::vector>& files) +{ + std::string bestName; + unsigned long long bestSize = 0; + long long bestTs = -1; + for (const auto& fp : files) { + const long long ts = parseWalletSalvageBakTs(fp.first); + if (ts < 0) continue; + if (fp.second > bestSize || (fp.second == bestSize && ts > bestTs)) { + bestSize = fp.second; bestTs = ts; bestName = fp.first; + } + } + return bestName; +} + } // namespace daemon } // namespace dragonx diff --git a/tests/test_phase4.cpp b/tests/test_phase4.cpp index b890c01..d065651 100644 --- a/tests/test_phase4.cpp +++ b/tests/test_phase4.cpp @@ -6156,6 +6156,18 @@ void testBlockDbOutputDiagnosis() EXPECT_EQ(newestWalletSalvageBak({"wallet.dat", "wallet.dat.encrypted.bak", "notes.txt"}), std::string("")); // no wallet..bak present EXPECT_EQ(newestWalletSalvageBak({}), std::string("")); + + // Restore must pick the LARGEST (least-salvaged) backup, NOT the newest — a salvage cascade shrinks + // the wallet each round, so the newest .bak can be an emptied 40KB copy while the original is huge. + using dragonx::daemon::largestWalletSalvageBak; + EXPECT_EQ(largestWalletSalvageBak({ + {"wallet.dat", 40000ull}, // current (salvaged, tiny) — not a .bak + {"wallet.1786200000.bak", 194174976ull}, // ORIGINAL — oldest ts, biggest + {"wallet.1786340620.bak", 40960ull}, // latest salvage — newest ts, empty + {"peers.dat", 999ull}}), + std::string("wallet.1786200000.bak")); // largest wins over newest + EXPECT_EQ(largestWalletSalvageBak({{"wallet.dat", 100ull}, {"notes.txt", 5ull}}), std::string("")); + EXPECT_EQ(largestWalletSalvageBak({}), std::string("")); } // Live probe of a real lite server (env-gated). Validates CONNECT_ONLY latency + IP capture.