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:
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user