fix(wallets): stop a legacy wallet showing as a seed-phrase wallet when linked

The wallets list badged the active/linked row from the runtime seed status
(activeWalletSeedBadge → wallet_seed_status_, reset only on disconnect) in
preference to the offline on-disk probe. Two issues let a genuinely legacy
wallet render as "seed phrase":

- The offline probe's budget-fallback branch dropped the fMnemonicSeed flag:
  res.mnemonic was set only in the (parsed && complete) branch. The probe shares
  a 768 MB budget across all wallet files, so a large wallet (e.g. a 194 MB one)
  probed after the budget is spent falls into the fallback, loses its seed/legacy
  classification (mnemonic → 0), and the row defers to the runtime badge.
- With mnemonic == 0, the code used the runtime badge, which can still carry a
  HasMnemonic from a previously-active mnemonic wallet — mislabelling the legacy
  wallet.

Fix:
- Carry the definitive positives (fMnemonicSeed/hdSeed/mkey) from a cap-truncated
  btree walk — a found marker is authoritative even when the scan didn't finish.
- Make the on-disk fMnemonicSeed read take precedence: it's the SAME flag the
  daemon's IsMnemonicSeed()/z_exportmnemonic consult, so a definitive read wins;
  the runtime badge is used only when the probe genuinely couldn't decide, and
  never overrides a definitive on-disk classification.

Verified the wallet in question is truly legacy (fMnemonicSeed=false on disk,
matching the daemon's CHDChain serialization + IsMnemonicSeed). The flag reader
(hdChainMnemonicFlag) is already unit-tested; suite green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-09 15:29:23 -05:00
parent 5296dd7ae5
commit f88304fed2

View File

@@ -288,17 +288,21 @@ public:
// encryption — so absence of a lock never falsely reads as "unencrypted" on a huge wallet.
const ProbeResult pres = probeAt(i); // from the frame-consistent snapshot above
const bool bLock = pres.probed && pres.encrypted;
// Seed-phrase vs legacy. Runtime status (z_exportmnemonic → activeWalletSeedBadge) is
// authoritative for the ACTIVE wallet; otherwise the offline probe reads the hdchain
// record's fMnemonicSeed flag directly (pres.mnemonic: 1 = BIP39 seed phrase, 2 = HD/legacy
// with no phrase, 0 = couldn't tell) — which, unlike bare HD-record presence, actually
// distinguishes the two. seed uses the same 1/2/0 encoding.
const int activeBadge = rowActive[i] ? app->activeWalletSeedBadge() : 0;
int seed = activeBadge;
if (seed == 0 && pres.probed) {
if (pres.mnemonic != 0) seed = pres.mnemonic; // read the flag off disk
else if (pres.complete && !pres.hdSeed) seed = 2; // no HD records at all → no phrase
}
// Seed-phrase vs legacy. The offline probe reads the hdchain record's fMnemonicSeed flag
// straight off disk (pres.mnemonic: 1 = BIP39 seed phrase, 2 = HD/legacy with no phrase,
// 0 = couldn't tell). That is the SAME flag the daemon's IsMnemonicSeed()/z_exportmnemonic
// consult, so a definitive read is authoritative and takes precedence. The runtime badge
// (activeWalletSeedBadge, reset only on disconnect) is used ONLY when the offline probe
// couldn't decide — it must NEVER override a definitive on-disk read, or a stale
// HasMnemonic carried from a previously-active mnemonic wallet mislabels a legacy wallet as
// a seed-phrase wallet. seed uses the same 1/2/0 encoding.
int seed = 0;
if (pres.probed && pres.mnemonic != 0)
seed = pres.mnemonic; // definitive on-disk flag wins
else if (rowActive[i] && app->activeWalletSeedBadge() != 0)
seed = app->activeWalletSeedBadge(); // runtime fallback (active row only)
else if (pres.probed && pres.complete && !pres.hdSeed)
seed = 2; // no HD records at all → no phrase
const bool bSeed = (seed == 1);
const bool bLegacy = (seed == 2);
// seed==0 splits by what the probe DID learn: if it saw HD records we know it's an HD
@@ -792,6 +796,14 @@ private:
} else {
const auto pr = util::probeWalletFile(t.first, std::min(budget, kPerFile));
res = ProbeResult{ pr.isBerkeleyDB, pr.scanComplete, pr.encrypted, pr.hdSeed };
// A cap-truncated btree walk still yields DEFINITIVE positives (a found marker is
// authoritative even when the scan didn't finish), so carry what it read — notably
// the fMnemonicSeed flag. Otherwise a large wallet probed after the shared budget is
// spent loses its seed/legacy classification and the row falls back to the (possibly
// stale) runtime badge, mislabelling a legacy wallet as a seed-phrase wallet.
if (bt.mnemonicSeed != 0) res.mnemonic = bt.mnemonicSeed;
if (bt.hdSeed) res.hdSeed = true;
if (bt.encrypted) res.encrypted = true;
budget -= std::min(budget, std::max(bt.bytesRead, pr.bytesRead));
}
}