fix(node): restore the LARGEST salvage backup, not the newest (salvage cascade)
The "Restore original wallet" action picked the newest wallet.<ts>.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.<digits>.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) <noreply@anthropic.com>
This commit is contained in:
@@ -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<std::string> 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<std::pair<std::string, unsigned long long>> 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<unsigned long long>(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) {
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
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.<unixtime>.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.<unixtime>.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<std::string>& 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<std::pair<std::string, unsigned long long>>& 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
|
||||
|
||||
@@ -6156,6 +6156,18 @@ void testBlockDbOutputDiagnosis()
|
||||
EXPECT_EQ(newestWalletSalvageBak({"wallet.dat", "wallet.dat.encrypted.bak", "notes.txt"}),
|
||||
std::string("")); // no wallet.<digits>.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.
|
||||
|
||||
Reference in New Issue
Block a user