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:
@@ -138,10 +138,33 @@ struct WalletBtreeStats {
|
||||
long long createdEpoch = 0; ///< earliest keymeta nCreateTime (wallet birthday); 0 = unknown
|
||||
bool encrypted = false; ///< saw an mkey record
|
||||
bool hdSeed = false; ///< saw hdseed/chdseed/hdchain
|
||||
// BIP39 seed-phrase detection read straight off the hdchain record's fMnemonicSeed flag (which the
|
||||
// daemon keeps PLAINTEXT even in an encrypted wallet — only the seed itself is crypted):
|
||||
// 0 = undecidable, 1 = mnemonic seed-phrase wallet (z_exportmnemonic works), 2 = HD/legacy, no phrase.
|
||||
int mnemonicSeed = 0;
|
||||
std::size_t bytesRead = 0; ///< bytes actually read (for budgeting)
|
||||
int addresses() const { return transparentKeys + shieldedKeys; }
|
||||
};
|
||||
|
||||
// Recover the BIP39-mnemonic flag from a serialized CHDChain record value (the plaintext "hdchain"
|
||||
// record). The daemon serializes CHDChain in Bitcoin/Zcash little-endian byte order as:
|
||||
// nVersion:int32 | seedFp:32B | nCreateTime:int64 | saplingAccountCounter:uint32
|
||||
// | [nVersion>=2 (VERSION_HD_TRANSPARENT)] transparentChildCounter:uint32
|
||||
// | [nVersion>=3 (VERSION_HD_MNEMONIC)] fMnemonicSeed:bool(1B)
|
||||
// so fMnemonicSeed sits at byte offset 52 (= 4+32+8+4+4) once nVersion>=3. Returns 1 = mnemonic,
|
||||
// 2 = HD but not mnemonic (raw-entropy seed, or a pre-mnemonic v1/v2 wallet), 0 = undecidable.
|
||||
inline int hdChainMnemonicFlag(const unsigned char* val, uint32_t len) {
|
||||
if (!val || len < 4) return 0;
|
||||
const uint32_t v = (uint32_t)val[0] | ((uint32_t)val[1] << 8) |
|
||||
((uint32_t)val[2] << 16) | ((uint32_t)val[3] << 24);
|
||||
const int32_t nVersion = (int32_t)v;
|
||||
if (nVersion < 1 || nVersion > 100) return 0; // implausible version → treat as unreadable, not "legacy"
|
||||
if (nVersion < 3) return 2; // VERSION_HD_MNEMONIC not reached → field absent → no phrase
|
||||
constexpr uint32_t kMnemonicOff = 52;
|
||||
if (len <= kMnemonicOff) return 0; // truncated/unexpected value → can't read the flag
|
||||
return val[kMnemonicOff] ? 1 : 2;
|
||||
}
|
||||
|
||||
inline WalletBtreeStats parseWalletBtree(const std::string& path,
|
||||
std::size_t maxBytes = 256u * 1024u * 1024u) {
|
||||
WalletBtreeStats st;
|
||||
@@ -287,7 +310,14 @@ inline WalletBtreeStats parseWalletBtree(const std::string& path,
|
||||
else if (is("name")) st.addressBook++;
|
||||
else if (is("tx")) st.txCount++;
|
||||
else if (is("mkey")) st.encrypted = true;
|
||||
else if (is("hdseed") || is("chdseed") || is("hdchain")) st.hdSeed = true;
|
||||
else if (is("hdseed") || is("chdseed")) st.hdSeed = true;
|
||||
else if (is("hdchain")) {
|
||||
st.hdSeed = true;
|
||||
// The hdchain VALUE carries fMnemonicSeed — read it to tell a BIP39 seed-phrase wallet
|
||||
// apart from a legacy/raw-entropy HD wallet (bare record presence can't). First one wins.
|
||||
if (st.mnemonicSeed == 0 && dtype == B_KEYDATA && dp)
|
||||
st.mnemonicSeed = hdChainMnemonicFlag(dp, dl);
|
||||
}
|
||||
else if (is("keymeta")) {
|
||||
// CKeyMetadata data = nVersion(int32) + nCreateTime(int64) + …, all little-endian (Bitcoin
|
||||
// serialization). The earliest non-zero nCreateTime is the wallet birthday (daemon's
|
||||
|
||||
Reference in New Issue
Block a user