Automates the manual recovery that fixed a wallet.dat with stale Berkeley DB extent metadata (the "main" subdb metapage records a low last_pgno while its live data spans thousands of pages beyond it). A tolerant page-walk reads every record, but the daemon's BDB verify rejects the file and auto-salvages it — finding nothing and shrinking the wallet to empty on each restart (the salvage cascade that looks like fund loss). Plain "Restore original" can't fix it (hands the same broken file back → re-salvage); a rebuild must produce a fresh, consistent DB. Pieces (Approach A from the design workflow — out-of-process helper keeps AGPL Berkeley DB out of the GPLv3 GUI): - util/wallet_file_probe.h: extractWalletBtreeRecords() — sibling to parseWalletBtree that collects raw (key,value) bytes (same bounds-checked, subdb-aware walk). Records copied verbatim → encrypted key material passes through as opaque ciphertext (no passphrase). Overflow-page values (only large tx history) are skipped + counted; a rescan rebuilds history — funds unaffected. - tools/wallet_rebuild/main.cpp: dragonx-wallet-rebuild CLI — reads via the tolerant reader, writes the records into a fresh BDB "main" btree via libdb (DB_EXCL, never overwrites), prints a JSON summary. New BDB-guarded CMake target. - App::rebuildWalletDatabase(): picks the largest readable wallet/.bak as source, stops the daemon, runs the helper, VERIFIES the output (readable BDB with keys) before swapping, moves the current wallet aside (kept, timestamped), installs the rebuilt one, clears the stale BDB env, sets -rescan, restarts. Copy/rename only — never deletes. Result surfaced via the existing pumpWalletRestore channel. - Wired as the preferred action on the existing wallet-auto-recovery dialog (shown only when the helper is present). Full-node only; lite-safe. Verified end-to-end against the real broken wallet: helper reads 3,808 t-keys + 1 z-key + HD seed and the daemon LOADS the rebuilt output with no salvage. Adds extractWalletBtreeRecords coverage. Build clean, suite green (1/1). Remaining (follow-up): release packaging — build.sh bundling the helper built against the vendored per-platform static libdb (DRAGONX_BDB_ROOT), and a macOS Berkeley DB port (no in-tree artifact). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
91 lines
4.1 KiB
C++
91 lines
4.1 KiB
C++
// 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 <source-wallet.dat> <output-wallet.dat>
|
|
// 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 <db.h>
|
|
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <string>
|
|
|
|
int main(int argc, char** argv)
|
|
{
|
|
if (argc < 3) {
|
|
std::fprintf(stderr, "usage: dragonx-wallet-rebuild <source-wallet.dat> <output-wallet.dat>\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<char*>(kv.first.data()); k.size = static_cast<u_int32_t>(kv.first.size());
|
|
v.data = const_cast<char*>(kv.second.data()); v.size = static_cast<u_int32_t>(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;
|
|
}
|