wallet: harden HD seed/chain persistence and record seed provenance

Autoshield sends mined coinbase to a seed-derived z-address, so the records that
decide how the seed derives keys become fund-safety critical. Three of them were
not treated that way.

InstallHDSeed wrote the seed record before the chain record, non-transactionally.
A crash between the two left a wallet holding a seed with no hdchain: on the next
load hdChain silently reverts to defaults, clearing fMnemonicSeed -- which
switches the derivation input from the expanded BIP39 seed to the raw entropy --
and resetting saplingAccountCounter. Write the chain first; the opposite torn
state is harmless and self-heals, because HaveHDSeed() is then false and init
installs again.

The hdchain record was read as a bare deserialise inside a catch-all with strErr
never set, and it is not a key type, so a corrupt record was downgraded to a
non-critical error and the node booted into the wrong key tree. Report it, track
whether it was read, and refuse to load a wallet that holds a seed but no
readable hdchain. Also preserve hdchain through a keys-only salvage, which would
otherwise drop it and produce exactly the state we now refuse.

GenerateNewSeed silently fell back to a random seed when BIP39 generation failed,
producing a wallet that looks mnemonic-capable but whose words can never be
exported and which no seed phrase can restore. A user who asked for -usemnemonic
now gets that or a hard failure.

Finally, record how the seed came to exist -- created on an empty wallet,
restored from -mnemonic/-hdseed, retrofitted onto a pre-existing seedless wallet,
or predating this record. A retrofitted seed is in no backup the user already
holds, so a feature that moves funds into addresses only that seed can re-derive
must not enable itself there by default. Nothing consumes this yet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-23 03:18:35 +02:00
co-authored by Claude Opus 5
parent dad162a89a
commit a494eabdce
5 changed files with 147 additions and 24 deletions
+39
View File
@@ -2307,6 +2307,23 @@ bool AppInit2(boost::thread_group& threadGroup, CScheduler& scheduler)
if (!pwalletMain->HaveHDSeed())
{
// Does this wallet predate the seed we are about to install? If so,
// that seed cannot appear in any backup the user already holds.
// Checked BEFORE installing, and before the restore path pre-derives
// its gap of keys.
bool fWalletHadContent = false;
{
LOCK(pwalletMain->cs_wallet);
std::set<CKeyID> setExistingKeys;
pwalletMain->GetKeys(setExistingKeys); // keystore.h:60 / crypter.h:212
std::set<libzcash::SaplingPaymentAddress> setExistingZAddrs;
pwalletMain->GetSaplingPaymentAddresses(setExistingZAddrs); // keystore.h:226-238
fWalletHadContent = !setExistingKeys.empty() ||
!setExistingZAddrs.empty() ||
!pwalletMain->mapWallet.empty() || // wallet.h:1041
pwalletMain->IsCrypted(); // crypter.h:174
}
std::string mnemonic = GetArg("-mnemonic", "");
std::string hdSeedHex = GetArg("-hdseed", "");
bool restoring = false;
@@ -2338,6 +2355,19 @@ bool AppInit2(boost::thread_group& threadGroup, CScheduler& scheduler)
pwalletMain->GenerateNewSeed();
}
pwalletMain->SetHDSeedOrigin(restoring
? CWallet::HDSEED_ORIGIN_RESTORED
: (fWalletHadContent ? CWallet::HDSEED_ORIGIN_RETROFIT
: CWallet::HDSEED_ORIGIN_CREATED));
if (pwalletMain->hdSeedOrigin == CWallet::HDSEED_ORIGIN_RETROFIT)
{
LogPrintf("%s: WARNING: generated a new HD seed for a wallet that already held keys or "
"transactions. This seed is in NO backup you made before now.\n", __func__);
InitWarning(_("A new HD seed was generated for this pre-existing wallet. Any backup you "
"made before now does not contain it: back the wallet up again "
"(z_exportwallet) before receiving funds to newly derived addresses."));
}
if (restoring)
{
// Pre-derive keys (birthday = genesis) so the startup rescan finds
@@ -2356,6 +2386,15 @@ bool AppInit2(boost::thread_group& threadGroup, CScheduler& scheduler)
LogPrintf("%s: pre-derived %d transparent and %d sapling keys for restore rescan\n", __func__, (int)tGap, (int)zGap);
}
}
else if (pwalletMain->hdSeedOrigin == CWallet::HDSEED_ORIGIN_UNRECORDED)
{
// The seed was installed by a build that predates this record, so
// we cannot tell whether it was minted onto a pre-existing wallet
// (and is therefore absent from the user's older backups). Assume
// the worst; the user can still opt in explicitly.
pwalletMain->SetHDSeedOrigin(CWallet::HDSEED_ORIGIN_UNKNOWN);
LogPrintf("%s: HD seed predates seed-provenance recording; recorded origin as unknown\n", __func__);
}
//Set Sapling Consolidation
pwalletMain->fSaplingConsolidationEnabled = GetBoolArg("-consolidation", false);