diff --git a/src/init.cpp b/src/init.cpp index 7e1519769..0766abe03 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -2271,7 +2271,10 @@ bool AppInit2(boost::thread_group& threadGroup, CScheduler& scheduler) if (nLoadWalletRet != DB_LOAD_OK) { if (nLoadWalletRet == DB_CORRUPT) - strErrors << _("Error loading wallet.dat: Wallet corrupted") << "\n"; + strErrors << _("Error loading wallet.dat: Wallet corrupted. If this wallet was last opened " + "by an older version, move wallet.dat aside and restore from your seed " + "phrase with -mnemonic=\"\" -rescan (see debug.log for " + "the specific record at fault).") << "\n"; else if (nLoadWalletRet == DB_NONCRITICAL_ERROR) { string msg(_("Warning: error reading wallet.dat! All keys read correctly, but transaction data" diff --git a/src/wallet/walletdb.cpp b/src/wallet/walletdb.cpp index 31e83e4b5..47bc658a4 100644 --- a/src/wallet/walletdb.cpp +++ b/src/wallet/walletdb.cpp @@ -420,6 +420,9 @@ public: vector vWalletUpgrade; // True once a well-formed "hdchain" record has been loaded. bool fHDChainRead; + // True when that record had to be repaired on read (see the "hdchain" case + // in ReadKeyValue); LoadWallet rewrites it in full form afterwards. + bool fHDChainRepaired; CWalletScanState() { nKeys = nCKeys = nKeyMeta = nZKeys = nCZKeys = nZKeyMeta = nSapZAddrs = 0; @@ -427,6 +430,7 @@ public: fAnyUnordered = false; nFileVersion = 0; fHDChainRead = false; + fHDChainRepaired = false; } }; @@ -851,14 +855,48 @@ ReadKeyValue(CWallet* pwallet, CDataStream& ssKey, CDataStream& ssValue, else if (strType == "hdchain") { CHDChain chain; + // Keep an untouched copy: a failed >> has already consumed part of ssValue. + CDataStream ssRetry(ssValue.begin(), ssValue.end(), ssValue.GetType(), ssValue.GetVersion()); 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; + // Downgrade repair. A build predating VERSION_HD_TRANSPARENT writes + // this record back with only the four base fields while leaving + // nVersion at whatever it read, so the version-gated reads above run + // off the end. Without this, one address generated under such a build + // makes the wallet unopenable here ("Wallet corrupted") even though + // nothing is actually lost. + // + // Recovering is safe for a v1/v2 record: everything derivation needs + // is either in the four-field prefix or in the separate hdseed record, + // and the trailing counters are self-healing -- DeriveNewChildKey and + // GenerateNewSaplingZKey both skip indices whose key the wallet + // already holds, so restarting a counter at 0 re-walks past existing + // keys instead of reissuing them. + chain = CHDChain(); + try { + ssRetry >> chain.nVersion; + ssRetry >> chain.seedFp; + ssRetry >> chain.nCreateTime; + ssRetry >> chain.saplingAccountCounter; + } catch (...) { + // Short even in the base fields: genuinely corrupt. + strErr = "Error reading wallet database: hdchain record is corrupt"; + return false; + } + if (chain.nVersion >= CHDChain::VERSION_HD_MNEMONIC) { + // A record claiming to carry fMnemonicSeed must not have it + // guessed: that flag selects the derivation input, so defaulting + // it wrong yields a different key tree in silence. Fail loud, as + // this branch always did. + strErr = "Error reading wallet database: hdchain record is corrupt"; + return false; + } + chain.transparentChildCounter = 0; + chain.fMnemonicSeed = false; + wss.fHDChainRepaired = true; + LogPrintf("Repairing a truncated hdchain record (nVersion=%d): it was last written by " + "a wallet build that predates the transparent HD counter\n", chain.nVersion); } wss.fHDChainRead = true; pwallet->SetHDChain(chain, true); @@ -1016,6 +1054,18 @@ DBErrors CWalletDB::LoadWallet(CWallet* pwallet) if (fNoncriticalErrors && result == DB_LOAD_OK) result = DB_NONCRITICAL_ERROR; + // Rewrite a repaired record in full form so the next load is clean and the + // transparent counter starts being persisted again. + if (wss.fHDChainRepaired && pwallet->HaveHDSeed()) + { + try { + pwallet->SetHDChain(pwallet->GetHDChain(), false); + LogPrintf("Rewrote the repaired hdchain record in full form\n"); + } catch (const std::exception& e) { + LogPrintf("Could not rewrite the repaired hdchain record: %s\n", e.what()); + } + } + // 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 @@ -1027,7 +1077,9 @@ DBErrors CWalletDB::LoadWallet(CWallet* pwallet) // 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"); + LogPrintf("Error loading wallet.dat: HD seed present but the hdchain record is missing or corrupt. " + "Recover by restoring from the seed phrase: move wallet.dat aside and start with " + "-mnemonic=\"\" -rescan\n"); return DB_CORRUPT; }