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;
|
std::string err, warn;
|
||||||
try {
|
try {
|
||||||
const std::string datadir = util::Platform::getDragonXDataDir();
|
const std::string datadir = util::Platform::getDragonXDataDir();
|
||||||
// 1. Find the newest salvage backup (offline — no daemon needed).
|
// 1. Find the LARGEST salvage backup (offline — no daemon needed). Largest = least-salvaged =
|
||||||
std::vector<std::string> files;
|
// 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;
|
std::error_code lec;
|
||||||
for (const auto& e : fs::directory_iterator(datadir, lec))
|
for (const auto& e : fs::directory_iterator(datadir, lec)) {
|
||||||
if (!lec) files.push_back(e.path().filename().string());
|
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()) {
|
if (bak.empty()) {
|
||||||
err = TR("wallet_restore_no_backup");
|
err = TR("wallet_restore_no_backup");
|
||||||
} else if (!util::probeWalletFile(datadir + "/" + bak).isBerkeleyDB) {
|
} else if (!util::probeWalletFile(datadir + "/" + bak).isBerkeleyDB) {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace dragonx {
|
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
|
|| 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"
|
// If `name` is a daemon salvage backup "wallet.<unixtime>.bak", return its timestamp; else -1.
|
||||||
// the auto-recovery just created (highest timestamp). Returns "" if none present. Pure, so it's testable.
|
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)
|
inline std::string newestWalletSalvageBak(const std::vector<std::string>& filenames)
|
||||||
{
|
{
|
||||||
long long best = -1;
|
long long best = -1;
|
||||||
std::string bestName;
|
std::string bestName;
|
||||||
for (const auto& f : filenames) {
|
for (const auto& f : filenames) {
|
||||||
if (f.rfind("wallet.", 0) != 0) continue; // must start "wallet."
|
const long long ts = parseWalletSalvageBakTs(f);
|
||||||
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');
|
|
||||||
if (ts > best) { best = ts; bestName = f; }
|
if (ts > best) { best = ts; bestName = f; }
|
||||||
}
|
}
|
||||||
return bestName;
|
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 daemon
|
||||||
} // namespace dragonx
|
} // namespace dragonx
|
||||||
|
|||||||
@@ -6156,6 +6156,18 @@ void testBlockDbOutputDiagnosis()
|
|||||||
EXPECT_EQ(newestWalletSalvageBak({"wallet.dat", "wallet.dat.encrypted.bak", "notes.txt"}),
|
EXPECT_EQ(newestWalletSalvageBak({"wallet.dat", "wallet.dat.encrypted.bak", "notes.txt"}),
|
||||||
std::string("")); // no wallet.<digits>.bak present
|
std::string("")); // no wallet.<digits>.bak present
|
||||||
EXPECT_EQ(newestWalletSalvageBak({}), std::string(""));
|
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.
|
// Live probe of a real lite server (env-gated). Validates CONNECT_ONLY latency + IP capture.
|
||||||
|
|||||||
Reference in New Issue
Block a user