feat(wallets): exact address/tx counts via a minimal BDB btree parser
parseWalletBtree() walks the wallet.dat Berkeley DB btree directly (no libdb, no daemon): validate the metapage magic, follow the "main" sub-database (its pgno is stored big-endian in the master map), traverse internal/leaf pages, and tally records by their length-prefixed type name — transparent + shielded spendable keys, address-book, and tx count, plus exact encryption/seed flags. Every offset is bounds-checked, pages are deduped at push time (stack stays O(npages)), and a visited-set + page/key caps make it safe on a corrupt/adversarial imported file; files using DB_CHKSUM/DB_ENCRYPT (which shift the page layout) are rejected so the byte-scan fallback runs instead. The async probe now uses this as the primary path (exact badges + counts), falling back to the byte-scan only when the btree can't be fully parsed. The wallets list shows "N keys · M txs" for probed rows (labeled "keys", not "addresses", since the count includes change keys the daemon's address list omits). Validated read-only against real wallets (counts cross-checked; enc/seed match the byte-scan) and a hand-built minimal btree fixture in the unit suite. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -227,10 +227,15 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
// Metadata line (size · N addresses · balance DRGX · last opened)
|
||||
// Metadata line (size · N addresses/keys · M txs · balance DRGX · last opened). The cached
|
||||
// wallet index gives the authoritative user-facing ADDRESS count; the btree walk gives an
|
||||
// exact tx count and a spendable-KEY count (labeled "keys", not "addresses", since it counts
|
||||
// change keys the daemon's address list omits — a different figure for the same wallet).
|
||||
std::string ms = util::Platform::formatFileSize((uint64_t)r.sizeBytes);
|
||||
char b[64];
|
||||
char b[80];
|
||||
if (meta && meta->cachedAddressCount >= 0) { snprintf(b, sizeof(b), "%s%lld %s", dot, (long long)meta->cachedAddressCount, TR("wallets_col_addresses")); ms += b; }
|
||||
else if (pres.hasCounts && pres.keyCount > 0) { snprintf(b, sizeof(b), "%s%d %s", dot, pres.keyCount, TR("wallets_col_keys")); ms += b; }
|
||||
if (pres.hasCounts) { snprintf(b, sizeof(b), "%s%d %s", dot, pres.txCount, TR("wallets_col_txs")); ms += b; }
|
||||
if (meta && meta->cachedBalance >= 0.0) { snprintf(b, sizeof(b), "%s%.4f %s", dot, meta->cachedBalance, DRAGONX_TICKER); ms += b; }
|
||||
ms += dot;
|
||||
ms += (meta && meta->lastOpenedEpoch > 0) ? util::formatTimeAgo(meta->lastOpenedEpoch) : std::string(TR("wallets_never"));
|
||||
@@ -346,10 +351,13 @@ private:
|
||||
// index-aligned batch: the probe thread fills it, render() reads it under the mutex, and a re-scan
|
||||
// supersedes the old batch (cancel + swap) so a detached in-flight probe can't touch stale rows.
|
||||
struct ProbeResult {
|
||||
bool probed = false; // validated as a BDB wallet and scanned
|
||||
bool complete = false; // scan covered the whole file → a MISSING marker is trustworthy
|
||||
bool probed = false; // validated as a BDB wallet and scanned
|
||||
bool complete = false; // scan covered the whole file → a MISSING marker is trustworthy
|
||||
bool encrypted = false; // has an mkey record
|
||||
bool hdSeed = false; // HD/seed wallet (else legacy)
|
||||
bool hdSeed = false; // HD/seed wallet (else legacy)
|
||||
bool hasCounts = false; // an exact btree walk produced the counts below
|
||||
int keyCount = 0; // transparent + shielded spendable keys (≈ addresses, incl. change)
|
||||
int txCount = 0; // wallet transaction records
|
||||
};
|
||||
struct ProbeBatch {
|
||||
std::mutex mtx;
|
||||
@@ -501,9 +509,18 @@ private:
|
||||
if (batch->cancel.load()) return;
|
||||
ProbeResult res;
|
||||
if (budget > 0) {
|
||||
const auto pr = util::probeWalletFile(t.first, std::min(budget, kPerFile));
|
||||
res = ProbeResult{ pr.isBerkeleyDB, pr.scanComplete, pr.encrypted, pr.hdSeed };
|
||||
budget -= std::min(budget, pr.bytesRead);
|
||||
// Primary: an exact btree walk (encryption/seed flags + address/tx counts). If it
|
||||
// can't fully parse (unknown BDB variant, or a >cap file), fall back to the cheap
|
||||
// byte-scan for badges only (no counts).
|
||||
const auto bt = util::parseWalletBtree(t.first, std::min(budget, kPerFile));
|
||||
if (bt.parsed && bt.complete) {
|
||||
res = ProbeResult{ true, true, bt.encrypted, bt.hdSeed, true, bt.addresses(), bt.txCount };
|
||||
budget -= std::min(budget, bt.bytesRead);
|
||||
} else {
|
||||
const auto pr = util::probeWalletFile(t.first, std::min(budget, kPerFile));
|
||||
res = ProbeResult{ pr.isBerkeleyDB, pr.scanComplete, pr.encrypted, pr.hdSeed };
|
||||
budget -= std::min(budget, std::max(bt.bytesRead, pr.bytesRead));
|
||||
}
|
||||
}
|
||||
std::lock_guard<std::mutex> lk(batch->mtx);
|
||||
if (t.second < batch->results.size()) batch->results[t.second] = res;
|
||||
|
||||
Reference in New Issue
Block a user