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:
+47
-2
@@ -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)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user