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:
2026-07-11 13:20:35 -05:00
parent 36efe99a0f
commit 175950a2ec
4 changed files with 229 additions and 8 deletions

View File

@@ -789,6 +789,31 @@ void testWalletFileProbe()
auto p = probeWalletFile((dir / "exact.dat").string(), 1024u * 1024u);
EXPECT_TRUE(p.isBerkeleyDB); EXPECT_TRUE(p.scanComplete); EXPECT_FALSE(p.encrypted); EXPECT_FALSE(p.hdSeed); }
// 7) Btree record counting (tier 2): hand-build a minimal single-database BDB btree — a 512-byte
// metapage (magic/pagesize/root) + one leaf page holding a "tx" record and a "key" record — and
// confirm parseWalletBtree walks it and tallies them. Also that a non-BDB file yields parsed=false.
{
std::string w(1024, '\0');
auto put16 = [&](std::size_t o, unsigned v) { w[o] = (char)(v & 0xff); w[o+1] = (char)((v >> 8) & 0xff); };
auto put32 = [&](std::size_t o, unsigned v) { for (int k=0;k<4;k++) w[o+k] = (char)((v >> (8*k)) & 0xff); };
put32(12, 0x00053162u); put32(20, 512); w[25] = (char)9; put32(88, 1); // page 0: metapage, root = page 1
const std::size_t P = 512; // page 1: a btree leaf
put16(P + 20, 4); w[P + 24] = (char)1; w[P + 25] = (char)5; // entries=4 (2 pairs), level 1, P_LBTREE
put16(P + 26, 500); put16(P + 28, 496); put16(P + 30, 488); put16(P + 32, 484); // index array → item offsets
auto keyItem = [&](std::size_t off, const std::string& type) { // BKEYDATA holding CompactSize(len)+type
std::string kd; kd += (char)type.size(); kd += type;
put16(P + off, (unsigned)kd.size()); w[P + off + 2] = (char)1;
for (std::size_t k = 0; k < kd.size(); ++k) w[P + off + 3 + k] = kd[k];
};
auto dataItem = [&](std::size_t off) { put16(P + off, 1); w[P + off + 2] = (char)1; w[P + off + 3] = 0; };
keyItem(500, "tx"); dataItem(496); keyItem(488, "key"); dataItem(484);
writef(dir / "btree.dat", w);
auto s = dragonx::util::parseWalletBtree((dir / "btree.dat").string());
EXPECT_TRUE(s.parsed); EXPECT_TRUE(s.complete);
EXPECT_EQ(s.txCount, 1); EXPECT_EQ(s.transparentKeys, 1); EXPECT_EQ(s.addresses(), 1);
EXPECT_FALSE(dragonx::util::parseWalletBtree((dir / "junk.dat").string()).parsed);
}
fs::remove_all(dir, ec);
}