diff --git a/src/init.cpp b/src/init.cpp index 89243cf1e..9a3e48458 100644 --- a/src/init.cpp +++ b/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 setExistingKeys; + pwalletMain->GetKeys(setExistingKeys); // keystore.h:60 / crypter.h:212 + std::set 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); diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 30eabdc43..fd2f31fed 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -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 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) { diff --git a/src/wallet/walletdb.h b/src/wallet/walletdb.h index d55072271..b3547a003 100644 --- a/src/wallet/walletdb.h +++ b/src/wallet/walletdb.h @@ -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);