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())
|
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 mnemonic = GetArg("-mnemonic", "");
|
||||||
std::string hdSeedHex = GetArg("-hdseed", "");
|
std::string hdSeedHex = GetArg("-hdseed", "");
|
||||||
bool restoring = false;
|
bool restoring = false;
|
||||||
@@ -2338,6 +2355,19 @@ bool AppInit2(boost::thread_group& threadGroup, CScheduler& scheduler)
|
|||||||
pwalletMain->GenerateNewSeed();
|
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)
|
if (restoring)
|
||||||
{
|
{
|
||||||
// Pre-derive keys (birthday = genesis) so the startup rescan finds
|
// 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);
|
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
|
//Set Sapling Consolidation
|
||||||
pwalletMain->fSaplingConsolidationEnabled = GetBoolArg("-consolidation", false);
|
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
|
// Opt-in: create the wallet from a fresh BIP39 mnemonic so its 24 words can
|
||||||
// be exported (z_exportmnemonic) and used in SilentDragonXLite.
|
// 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)) {
|
if (GetBoolArg("-usemnemonic", false)) {
|
||||||
RawHDSeed entropy;
|
RawHDSeed entropy;
|
||||||
if (GenerateMnemonicEntropy(256, entropy)) {
|
if (!GenerateMnemonicEntropy(256, entropy))
|
||||||
|
throw std::runtime_error(std::string(__func__) + ": -usemnemonic entropy generation failed");
|
||||||
HDSeed seed(entropy);
|
HDSeed seed(entropy);
|
||||||
if (InstallHDSeed(seed, true, nCreationTime))
|
if (!InstallHDSeed(seed, true, nCreationTime))
|
||||||
|
throw std::runtime_error(std::string(__func__) + ": installing the mnemonic HD seed failed");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
LogPrintf("%s: -usemnemonic seed generation failed, falling back to a random seed\n", __func__);
|
|
||||||
}
|
|
||||||
|
|
||||||
auto seed = HDSeed::Random(HD_WALLET_SEED_LENGTH);
|
|
||||||
|
|
||||||
// If the wallet is encrypted and locked, this will fail.
|
// 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");
|
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)
|
bool CWallet::SetHDSeed(const HDSeed& seed)
|
||||||
@@ -2535,6 +2531,7 @@ bool CWallet::SetCryptedHDSeed(const uint256& seedFp, const std::vector<unsigned
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void CWallet::SetHDChain(const CHDChain& chain, bool memonly)
|
void CWallet::SetHDChain(const CHDChain& chain, bool memonly)
|
||||||
{
|
{
|
||||||
LOCK(cs_wallet);
|
LOCK(cs_wallet);
|
||||||
@@ -2544,6 +2541,20 @@ void CWallet::SetHDChain(const CHDChain& chain, bool memonly)
|
|||||||
hdChain = chain;
|
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)
|
bool CWallet::LoadHDSeed(const HDSeed& seed)
|
||||||
{
|
{
|
||||||
return CBasicKeyStore::SetHDSeed(seed);
|
return CBasicKeyStore::SetHDSeed(seed);
|
||||||
@@ -2558,16 +2569,23 @@ bool CWallet::InstallHDSeed(const HDSeed& seed, bool fMnemonic, int64_t nCreateT
|
|||||||
{
|
{
|
||||||
AssertLockHeld(cs_wallet);
|
AssertLockHeld(cs_wallet);
|
||||||
|
|
||||||
if (!SetHDSeed(seed))
|
// Chain BEFORE seed. A crash between the two records must never leave a
|
||||||
return false;
|
// 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;
|
CHDChain newHdChain;
|
||||||
newHdChain.nVersion = fMnemonic ? CHDChain::VERSION_HD_MNEMONIC
|
newHdChain.nVersion = fMnemonic ? CHDChain::VERSION_HD_MNEMONIC
|
||||||
: CHDChain::VERSION_HD_TRANSPARENT;
|
: CHDChain::VERSION_HD_TRANSPARENT;
|
||||||
newHdChain.seedFp = seed.Fingerprint();
|
newHdChain.seedFp = seed.Fingerprint();
|
||||||
newHdChain.nCreateTime = nCreateTime;
|
newHdChain.nCreateTime = nCreateTime;
|
||||||
newHdChain.fMnemonicSeed = fMnemonic;
|
newHdChain.fMnemonicSeed = fMnemonic;
|
||||||
SetHDChain(newHdChain, false);
|
SetHDChain(newHdChain, false); // throws if the write fails
|
||||||
|
|
||||||
|
if (!SetHDSeed(seed))
|
||||||
|
return false;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -855,6 +855,18 @@ public:
|
|||||||
// Id of the in-flight autoshield op; read by the op to confirm it is still
|
// Id of the in-flight autoshield op; read by the op to confirm it is still
|
||||||
// the current one before mutating scheduler state.
|
// the current one before mutating scheduler state.
|
||||||
AsyncRPCOperationId saplingAutoShieldOperationId;
|
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();
|
void ClearNoteWitnessCache();
|
||||||
|
|
||||||
@@ -1373,6 +1385,12 @@ public:
|
|||||||
void SetHDChain(const CHDChain& chain, bool memonly);
|
void SetHDChain(const CHDChain& chain, bool memonly);
|
||||||
const CHDChain& GetHDChain() const { return hdChain; }
|
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) */
|
/* Set the current HD seed, without saving it to disk (used by LoadWallet) */
|
||||||
bool LoadHDSeed(const HDSeed& key);
|
bool LoadHDSeed(const HDSeed& key);
|
||||||
|
|
||||||
|
|||||||
@@ -216,6 +216,12 @@ bool CWalletDB::WriteWitnessCacheSize(int64_t nWitnessCacheSize)
|
|||||||
return Write(std::string("witnesscachesize"), 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)
|
bool CWalletDB::ReadPool(int64_t nPool, CKeyPool& keypool)
|
||||||
{
|
{
|
||||||
return Read(std::make_pair(std::string("pool"), nPool), keypool);
|
return Read(std::make_pair(std::string("pool"), nPool), keypool);
|
||||||
@@ -403,12 +409,15 @@ public:
|
|||||||
bool fAnyUnordered;
|
bool fAnyUnordered;
|
||||||
int nFileVersion;
|
int nFileVersion;
|
||||||
vector<uint256> vWalletUpgrade;
|
vector<uint256> vWalletUpgrade;
|
||||||
|
// True once a well-formed "hdchain" record has been loaded.
|
||||||
|
bool fHDChainRead;
|
||||||
|
|
||||||
CWalletScanState() {
|
CWalletScanState() {
|
||||||
nKeys = nCKeys = nKeyMeta = nZKeys = nCZKeys = nZKeyMeta = nSapZAddrs = 0;
|
nKeys = nCKeys = nKeyMeta = nZKeys = nCZKeys = nZKeyMeta = nSapZAddrs = 0;
|
||||||
fIsEncrypted = false;
|
fIsEncrypted = false;
|
||||||
fAnyUnordered = false;
|
fAnyUnordered = false;
|
||||||
nFileVersion = 0;
|
nFileVersion = 0;
|
||||||
|
fHDChainRead = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -833,9 +842,24 @@ ReadKeyValue(CWallet* pwallet, CDataStream& ssKey, CDataStream& ssValue,
|
|||||||
else if (strType == "hdchain")
|
else if (strType == "hdchain")
|
||||||
{
|
{
|
||||||
CHDChain chain;
|
CHDChain chain;
|
||||||
|
try {
|
||||||
ssValue >> chain;
|
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);
|
pwallet->SetHDChain(chain, true);
|
||||||
}
|
}
|
||||||
|
else if (strType == "hdseedorigin")
|
||||||
|
{
|
||||||
|
int64_t nOrigin = 0;
|
||||||
|
ssValue >> nOrigin;
|
||||||
|
pwallet->hdSeedOrigin = (int)nOrigin;
|
||||||
|
}
|
||||||
} catch (...)
|
} catch (...)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
@@ -947,6 +971,21 @@ DBErrors CWalletDB::LoadWallet(CWallet* pwallet)
|
|||||||
if (fNoncriticalErrors && result == DB_LOAD_OK)
|
if (fNoncriticalErrors && result == DB_LOAD_OK)
|
||||||
result = DB_NONCRITICAL_ERROR;
|
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
|
// Any wallet corruption at all: skip any rewriting or
|
||||||
// upgrading, we don't want to make it worse.
|
// upgrading, we don't want to make it worse.
|
||||||
if (result != DB_LOAD_OK)
|
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,
|
fReadOK = ReadKeyValue(&dummyWallet, ssKey, ssValue,
|
||||||
wss, strType, strErr);
|
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;
|
continue;
|
||||||
if (!fReadOK)
|
if (!fReadOK)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -191,6 +191,9 @@ public:
|
|||||||
|
|
||||||
bool WriteWitnessCacheSize(int64_t nWitnessCacheSize);
|
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 ReadPool(int64_t nPool, CKeyPool& keypool);
|
||||||
bool WritePool(int64_t nPool, const CKeyPool& keypool);
|
bool WritePool(int64_t nPool, const CKeyPool& keypool);
|
||||||
bool ErasePool(int64_t nPool);
|
bool ErasePool(int64_t nPool);
|
||||||
|
|||||||
Reference in New Issue
Block a user