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:
39
src/init.cpp
39
src/init.cpp
@@ -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);
|
||||
|
||||
@@ -2470,30 +2470,26 @@ void CWallet::GenerateNewSeed()
|
||||
|
||||
// Opt-in: create the wallet from a fresh BIP39 mnemonic so its 24 words can
|
||||
// be exported (z_exportmnemonic) and used in SilentDragonXLite.
|
||||
//
|
||||
// NO SILENT FALLBACK. Falling back to a random seed here produced a wallet
|
||||
// that looks mnemonic-capable but whose words can never be exported
|
||||
// (z_exportmnemonic refuses non-mnemonic wallets, rpcdump.cpp:1031+) and
|
||||
// that no seed phrase can restore. A user who asked for -usemnemonic must
|
||||
// get that or a hard failure.
|
||||
if (GetBoolArg("-usemnemonic", false)) {
|
||||
RawHDSeed entropy;
|
||||
if (GenerateMnemonicEntropy(256, entropy)) {
|
||||
HDSeed seed(entropy);
|
||||
if (InstallHDSeed(seed, true, nCreationTime))
|
||||
return;
|
||||
}
|
||||
LogPrintf("%s: -usemnemonic seed generation failed, falling back to a random seed\n", __func__);
|
||||
if (!GenerateMnemonicEntropy(256, entropy))
|
||||
throw std::runtime_error(std::string(__func__) + ": -usemnemonic entropy generation failed");
|
||||
HDSeed seed(entropy);
|
||||
if (!InstallHDSeed(seed, true, nCreationTime))
|
||||
throw std::runtime_error(std::string(__func__) + ": installing the mnemonic HD seed failed");
|
||||
return;
|
||||
}
|
||||
|
||||
auto seed = HDSeed::Random(HD_WALLET_SEED_LENGTH);
|
||||
|
||||
// If the wallet is encrypted and locked, this will fail.
|
||||
if (!SetHDSeed(seed))
|
||||
auto seed = HDSeed::Random(HD_WALLET_SEED_LENGTH);
|
||||
if (!InstallHDSeed(seed, false, nCreationTime))
|
||||
throw std::runtime_error(std::string(__func__) + ": SetHDSeed failed");
|
||||
|
||||
// store the key creation time together with
|
||||
// the child index counter in the database
|
||||
// as a hdchain object
|
||||
CHDChain newHdChain;
|
||||
newHdChain.nVersion = CHDChain::VERSION_HD_TRANSPARENT;
|
||||
newHdChain.seedFp = seed.Fingerprint();
|
||||
newHdChain.nCreateTime = nCreationTime;
|
||||
SetHDChain(newHdChain, false);
|
||||
}
|
||||
|
||||
bool CWallet::SetHDSeed(const HDSeed& seed)
|
||||
@@ -2535,6 +2531,7 @@ bool CWallet::SetCryptedHDSeed(const uint256& seedFp, const std::vector<unsigned
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
void CWallet::SetHDChain(const CHDChain& chain, bool memonly)
|
||||
{
|
||||
LOCK(cs_wallet);
|
||||
@@ -2544,6 +2541,20 @@ void CWallet::SetHDChain(const CHDChain& chain, bool memonly)
|
||||
hdChain = chain;
|
||||
}
|
||||
|
||||
void CWallet::SetHDSeedOrigin(int origin)
|
||||
{
|
||||
LOCK(cs_wallet);
|
||||
|
||||
hdSeedOrigin = origin;
|
||||
|
||||
// Deliberately non-fatal, unlike SetHDChain: losing this record must never
|
||||
// stop a node from starting. The cost of a failed write is that the next
|
||||
// start re-classifies the wallet, and re-classification of an already-seeded
|
||||
// wallet yields HDSEED_ORIGIN_UNKNOWN, i.e. the safe answer.
|
||||
if (fFileBacked && !CWalletDB(strWalletFile).WriteHDSeedOrigin((int64_t)origin))
|
||||
LogPrintf("%s: WARNING: could not record HD seed origin %d in wallet.dat\n", __func__, origin);
|
||||
}
|
||||
|
||||
bool CWallet::LoadHDSeed(const HDSeed& seed)
|
||||
{
|
||||
return CBasicKeyStore::SetHDSeed(seed);
|
||||
@@ -2558,16 +2569,23 @@ bool CWallet::InstallHDSeed(const HDSeed& seed, bool fMnemonic, int64_t nCreateT
|
||||
{
|
||||
AssertLockHeld(cs_wallet);
|
||||
|
||||
if (!SetHDSeed(seed))
|
||||
return false;
|
||||
|
||||
// Chain BEFORE seed. A crash between the two records must never leave a
|
||||
// wallet that holds a seed with no hdchain: on the next load hdChain would
|
||||
// silently revert to its defaults, clearing fMnemonicSeed (which switches
|
||||
// the derivation input, wallet.cpp:2615-2633) and resetting
|
||||
// saplingAccountCounter. The opposite torn state — chain without seed — is
|
||||
// harmless and self-healing: HaveHDSeed() is false, so init installs a seed
|
||||
// again and overwrites the chain.
|
||||
CHDChain newHdChain;
|
||||
newHdChain.nVersion = fMnemonic ? CHDChain::VERSION_HD_MNEMONIC
|
||||
: CHDChain::VERSION_HD_TRANSPARENT;
|
||||
newHdChain.seedFp = seed.Fingerprint();
|
||||
newHdChain.nCreateTime = nCreateTime;
|
||||
newHdChain.fMnemonicSeed = fMnemonic;
|
||||
SetHDChain(newHdChain, false);
|
||||
SetHDChain(newHdChain, false); // throws if the write fails
|
||||
|
||||
if (!SetHDSeed(seed))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -855,6 +855,18 @@ public:
|
||||
// Id of the in-flight autoshield op; read by the op to confirm it is still
|
||||
// the current one before mutating scheduler state.
|
||||
AsyncRPCOperationId saplingAutoShieldOperationId;
|
||||
// Provenance of this wallet's HD seed, recorded once in wallet.dat the
|
||||
// first time a build that knows about it opens the wallet. Features that
|
||||
// move funds into addresses only the seed can re-derive must not turn
|
||||
// themselves ON by default unless the user can actually restore that seed.
|
||||
enum HDSeedOrigin {
|
||||
HDSEED_ORIGIN_UNRECORDED = 0, // no record in wallet.dat yet
|
||||
HDSEED_ORIGIN_CREATED = 1, // minted onto a brand-new empty wallet
|
||||
HDSEED_ORIGIN_RESTORED = 2, // user supplied -mnemonic / -hdseed
|
||||
HDSEED_ORIGIN_RETROFIT = 3, // minted onto a pre-existing seedless wallet
|
||||
HDSEED_ORIGIN_UNKNOWN = 4, // seed predates this record
|
||||
};
|
||||
int hdSeedOrigin = HDSEED_ORIGIN_UNRECORDED;
|
||||
|
||||
void ClearNoteWitnessCache();
|
||||
|
||||
@@ -1373,6 +1385,12 @@ public:
|
||||
void SetHDChain(const CHDChain& chain, bool memonly);
|
||||
const CHDChain& GetHDChain() const { return hdChain; }
|
||||
|
||||
/* Record (in memory and in wallet.dat) how this wallet's HD seed came to
|
||||
exist. Best-effort: a failed write is logged, not fatal — the next start
|
||||
simply re-classifies, and re-classification always errs toward
|
||||
HDSEED_ORIGIN_UNKNOWN, which is the conservative answer. */
|
||||
void SetHDSeedOrigin(int origin);
|
||||
|
||||
/* Set the current HD seed, without saving it to disk (used by LoadWallet) */
|
||||
bool LoadHDSeed(const HDSeed& key);
|
||||
|
||||
|
||||
@@ -216,6 +216,12 @@ bool CWalletDB::WriteWitnessCacheSize(int64_t nWitnessCacheSize)
|
||||
return Write(std::string("witnesscachesize"), nWitnessCacheSize);
|
||||
}
|
||||
|
||||
bool CWalletDB::WriteHDSeedOrigin(int64_t nOrigin)
|
||||
{
|
||||
nWalletDBUpdated++;
|
||||
return Write(std::string("hdseedorigin"), nOrigin);
|
||||
}
|
||||
|
||||
bool CWalletDB::ReadPool(int64_t nPool, CKeyPool& keypool)
|
||||
{
|
||||
return Read(std::make_pair(std::string("pool"), nPool), keypool);
|
||||
@@ -403,12 +409,15 @@ public:
|
||||
bool fAnyUnordered;
|
||||
int nFileVersion;
|
||||
vector<uint256> vWalletUpgrade;
|
||||
// True once a well-formed "hdchain" record has been loaded.
|
||||
bool fHDChainRead;
|
||||
|
||||
CWalletScanState() {
|
||||
nKeys = nCKeys = nKeyMeta = nZKeys = nCZKeys = nZKeyMeta = nSapZAddrs = 0;
|
||||
fIsEncrypted = false;
|
||||
fAnyUnordered = false;
|
||||
nFileVersion = 0;
|
||||
fHDChainRead = false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -833,9 +842,24 @@ ReadKeyValue(CWallet* pwallet, CDataStream& ssKey, CDataStream& ssValue,
|
||||
else if (strType == "hdchain")
|
||||
{
|
||||
CHDChain chain;
|
||||
ssValue >> chain;
|
||||
try {
|
||||
ssValue >> chain;
|
||||
} catch (...) {
|
||||
// Do not let this land in the "user can live with it" bucket:
|
||||
// report it, and leave wss.fHDChainRead false so LoadWallet
|
||||
// turns it into DB_CORRUPT when a seed is present.
|
||||
strErr = "Error reading wallet database: hdchain record is corrupt";
|
||||
return false;
|
||||
}
|
||||
wss.fHDChainRead = true;
|
||||
pwallet->SetHDChain(chain, true);
|
||||
}
|
||||
else if (strType == "hdseedorigin")
|
||||
{
|
||||
int64_t nOrigin = 0;
|
||||
ssValue >> nOrigin;
|
||||
pwallet->hdSeedOrigin = (int)nOrigin;
|
||||
}
|
||||
} catch (...)
|
||||
{
|
||||
return false;
|
||||
@@ -947,6 +971,21 @@ DBErrors CWalletDB::LoadWallet(CWallet* pwallet)
|
||||
if (fNoncriticalErrors && result == DB_LOAD_OK)
|
||||
result = DB_NONCRITICAL_ERROR;
|
||||
|
||||
// A wallet that holds an HD seed but whose hdchain record is missing or
|
||||
// unreadable is NOT safe to run. hdChain would fall back to its SetNull
|
||||
// defaults (walletdb.h:105-113), which (a) clears fMnemonicSeed, switching
|
||||
// HD derivation from the 64-byte BIP39 seed to the raw 32-byte entropy
|
||||
// (CWallet::GetHDSeedForDerivation, wallet.cpp:2615-2633) -> an entirely
|
||||
// different key tree, and (b) resets saplingAccountCounter to 0, so the
|
||||
// next GenerateNewSaplingZKey walks back over accounts that already exist.
|
||||
// Both are silent today (a bad hdchain read is only DB_NONCRITICAL_ERROR).
|
||||
// Fail loud instead of quietly deriving into the wrong tree.
|
||||
if (pwallet->HaveHDSeed() && !wss.fHDChainRead)
|
||||
{
|
||||
LogPrintf("Error loading wallet.dat: HD seed present but the hdchain record is missing or corrupt\n");
|
||||
return DB_CORRUPT;
|
||||
}
|
||||
|
||||
// Any wallet corruption at all: skip any rewriting or
|
||||
// upgrading, we don't want to make it worse.
|
||||
if (result != DB_LOAD_OK)
|
||||
@@ -1240,7 +1279,13 @@ bool CWalletDB::Recover(CDBEnv& dbenv, const std::string& filename, bool fOnlyKe
|
||||
fReadOK = ReadKeyValue(&dummyWallet, ssKey, ssValue,
|
||||
wss, strType, strErr);
|
||||
}
|
||||
if (!IsKeyType(strType))
|
||||
// "hdchain" is not a key type, but it must survive a keys-only
|
||||
// salvage: a recovered wallet that keeps its seed while losing its
|
||||
// hdchain silently derives from a different key tree (fMnemonicSeed
|
||||
// cleared -> raw entropy instead of the 64-byte BIP39 seed) and
|
||||
// re-issues sapling accounts from 0. CWalletDB::LoadWallet now
|
||||
// refuses such a wallet outright, so preserve the record here.
|
||||
if (!IsKeyType(strType) && strType != "hdchain")
|
||||
continue;
|
||||
if (!fReadOK)
|
||||
{
|
||||
|
||||
@@ -191,6 +191,9 @@ public:
|
||||
|
||||
bool WriteWitnessCacheSize(int64_t nWitnessCacheSize);
|
||||
|
||||
//! Record how this wallet's HD seed came to exist (CWallet::HDSeedOrigin).
|
||||
bool WriteHDSeedOrigin(int64_t nOrigin);
|
||||
|
||||
bool ReadPool(int64_t nPool, CKeyPool& keypool);
|
||||
bool WritePool(int64_t nPool, const CKeyPool& keypool);
|
||||
bool ErasePool(int64_t nPool);
|
||||
|
||||
Reference in New Issue
Block a user