fix(wallets): detect seed-phrase wallets offline via hdchain fMnemonicSeed
The wallet switcher couldn't tell a BIP39 seed-phrase wallet from a legacy / raw-entropy HD wallet without loading it, so non-active rows fell back to bare HD-record presence — which mislabeled every HD wallet as "Seed phrase". The distinction is actually on disk: the daemon serializes CHDChain with an fMnemonicSeed bool (VERSION_HD_MNEMONIC=3, byte offset 52), and the hdchain record stays plaintext even in an encrypted wallet. Read it directly: - wallet_file_probe.h: hdChainMnemonicFlag() decodes the flag from the hdchain value (1 mnemonic / 2 no-phrase / 0 undecidable); WalletBtreeStats.mnemonicSeed surfaces it from the tier-2 btree walk. - wallets_dialog.h: the badge prefers runtime z_exportmnemonic for the active wallet, then the on-disk flag, then HD-record presence — so every row is classified correctly (or honestly shows "?" when the flag can't be read, e.g. the tier-1 byte-scan fallback). - tests: decode-level cases (v3 set/clear, v1/v2, truncated, garbage version) plus an end-to-end btree walk asserting mnemonicSeed. Offset math + badge logic adversarially verified against the daemon source. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -828,6 +828,63 @@ void testWalletFileProbe()
|
||||
EXPECT_FALSE(dragonx::util::parseWalletBtree((dir / "junk.dat").string()).parsed);
|
||||
}
|
||||
|
||||
// 8) Mnemonic-flag decode (hdChainMnemonicFlag): the fMnemonicSeed byte lives at offset 52 of the
|
||||
// CHDChain value once nVersion>=3; v1/v2 have no such field (→ "no phrase"), and a short/garbage
|
||||
// value is undecidable (0), never mislabeled "legacy".
|
||||
{
|
||||
using dragonx::util::hdChainMnemonicFlag;
|
||||
auto chdchain = [](int32_t ver, int flag /* -1 = omit the trailing byte */) {
|
||||
std::string v;
|
||||
for (int k = 0; k < 4; ++k) v += (char)(((uint32_t)ver >> (8 * k)) & 0xff); // nVersion, LE
|
||||
v.append(32 + 8 + 4 + 4, '\x07'); // seedFp+times+counters (48B)
|
||||
if (flag >= 0) v += (char)(flag ? 1 : 0); // fMnemonicSeed @ offset 52
|
||||
return v;
|
||||
};
|
||||
auto flagOf = [&](const std::string& v) {
|
||||
return hdChainMnemonicFlag(reinterpret_cast<const unsigned char*>(v.data()), (uint32_t)v.size());
|
||||
};
|
||||
EXPECT_EQ(flagOf(chdchain(3, 1)), 1); // v3, fMnemonicSeed=1 → seed phrase
|
||||
EXPECT_EQ(flagOf(chdchain(3, 0)), 2); // v3, fMnemonicSeed=0 → HD, no phrase
|
||||
EXPECT_EQ(flagOf(chdchain(2, -1)), 2); // v2 (field not serialized) → no phrase
|
||||
EXPECT_EQ(flagOf(chdchain(1, -1)), 2); // v1 → no phrase
|
||||
EXPECT_EQ(flagOf(chdchain(3, -1)), 0); // v3 but value ends before offset 52 → undecidable
|
||||
EXPECT_EQ(flagOf(chdchain(999, 1)), 0); // implausible version → undecidable, NOT legacy
|
||||
EXPECT_EQ(hdChainMnemonicFlag(nullptr, 0), 0);
|
||||
EXPECT_EQ(flagOf(std::string(3, '\0')), 0); // < 4 bytes → undecidable
|
||||
}
|
||||
|
||||
// 9) End-to-end: a minimal btree whose single record is an hdchain with a v3 CHDChain value →
|
||||
// parseWalletBtree surfaces hdSeed AND the decoded mnemonicSeed (1 for a seed-phrase wallet, 2 when
|
||||
// the flag is clear). Exercises the value-reading path inside the real walk, not just the decoder.
|
||||
{
|
||||
auto buildHdchainWallet = [](const std::string& chdValue) {
|
||||
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); // metapage → root = page 1
|
||||
const std::size_t P = 512; // page 1: one (key,data) pair
|
||||
put16(P+20, 2); w[P+24]=(char)1; w[P+25]=(char)5; // entries=2, level 1, P_LBTREE
|
||||
put16(P+26, 490); put16(P+28, 430); // item offsets: key@490, data@430
|
||||
std::string kd; kd += (char)7; kd += "hdchain"; // key BKEYDATA: CompactSize(7)+name
|
||||
put16(P+490, (unsigned)kd.size()); w[P+490+2]=(char)1;
|
||||
for (std::size_t k=0;k<kd.size();++k) w[P+490+3+k]=kd[k];
|
||||
put16(P+430, (unsigned)chdValue.size()); w[P+430+2]=(char)1; // data BKEYDATA: the CHDChain value
|
||||
for (std::size_t k=0;k<chdValue.size();++k) w[P+430+3+k]=chdValue[k];
|
||||
return w;
|
||||
};
|
||||
std::string v3seed; // v3, fMnemonicSeed=1
|
||||
for (int k=0;k<4;k++) v3seed += (char)((3u>>(8*k))&0xff);
|
||||
v3seed.append(48, '\0'); v3seed += (char)1;
|
||||
writef(dir / "hdchain_seed.dat", buildHdchainWallet(v3seed));
|
||||
auto s1 = dragonx::util::parseWalletBtree((dir / "hdchain_seed.dat").string());
|
||||
EXPECT_TRUE(s1.parsed); EXPECT_TRUE(s1.hdSeed); EXPECT_EQ(s1.mnemonicSeed, 1);
|
||||
|
||||
std::string v3nophrase = v3seed; v3nophrase.back() = (char)0; // fMnemonicSeed=0
|
||||
writef(dir / "hdchain_legacy.dat", buildHdchainWallet(v3nophrase));
|
||||
auto s2 = dragonx::util::parseWalletBtree((dir / "hdchain_legacy.dat").string());
|
||||
EXPECT_TRUE(s2.parsed); EXPECT_TRUE(s2.hdSeed); EXPECT_EQ(s2.mnemonicSeed, 2);
|
||||
}
|
||||
|
||||
fs::remove_all(dir, ec);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user