wallet: repair a truncated hdchain record instead of refusing the wallet
A build predating VERSION_HD_TRANSPARENT rewrites the hdchain record with only its four base fields while leaving nVersion at whatever it read. Our version-gated reads then run off the end of the stream, ReadKeyValue turns the throw into fHDChainRead=false, and LoadWallet escalates that to DB_CORRUPT. Verified against the real v1.0.2-2b011d6ee release binary: a dev wallet, opened once by v1.0.2 and given a single new sapling address, came back to Error reading wallet database: hdchain record is corrupt Error loading wallet.dat: HD seed present but the hdchain record is missing or corrupt Error loading wallet.dat: Wallet corrupted Nothing is actually lost there -- the same test showed the seed phrase restoring the full balance, the autoshield destination, and even the address v1.0.2 had generated -- but the user is shown "Wallet corrupted" with no hint of that. Recover instead, for a v1 or v2 record. Everything derivation depends on 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 rather than reissuing them. Read the prefix from an untouched copy of the stream, default the missing tail, log it, and rewrite the record in full form so the next load is clean. A record claiming nVersion >= VERSION_HD_MNEMONIC still fails loud: that flag selects the derivation input, so guessing it wrong yields a different key tree in silence. No wallet this code has written can be in that state -- every InstallHDSeed call site passes fMnemonic=false -- so the branch is defensive only. Also say what to do about it. Both the log line and the init error now name the remedy (move wallet.dat aside, restart with -mnemonic and -rescan) rather than stopping at "Wallet corrupted". Verified on regtest against the real v1.0.2 binary, 14/14: the round-trip that previously ended in "Wallet corrupted" now loads, logs the repair and the rewrite, keeps the balance, the autoshield destination and v1.0.2's own address, still issues distinct fresh t-addresses after the counter reset, needs no repair on the second load, and remains fully recoverable from the seed phrase. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2271,7 +2271,10 @@ bool AppInit2(boost::thread_group& threadGroup, CScheduler& scheduler)
|
|||||||
if (nLoadWalletRet != DB_LOAD_OK)
|
if (nLoadWalletRet != DB_LOAD_OK)
|
||||||
{
|
{
|
||||||
if (nLoadWalletRet == DB_CORRUPT)
|
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=\"<your seed phrase>\" -rescan (see debug.log for "
|
||||||
|
"the specific record at fault).") << "\n";
|
||||||
else if (nLoadWalletRet == DB_NONCRITICAL_ERROR)
|
else if (nLoadWalletRet == DB_NONCRITICAL_ERROR)
|
||||||
{
|
{
|
||||||
string msg(_("Warning: error reading wallet.dat! All keys read correctly, but transaction data"
|
string msg(_("Warning: error reading wallet.dat! All keys read correctly, but transaction data"
|
||||||
|
|||||||
@@ -420,6 +420,9 @@ public:
|
|||||||
vector<uint256> vWalletUpgrade;
|
vector<uint256> vWalletUpgrade;
|
||||||
// True once a well-formed "hdchain" record has been loaded.
|
// True once a well-formed "hdchain" record has been loaded.
|
||||||
bool fHDChainRead;
|
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() {
|
CWalletScanState() {
|
||||||
nKeys = nCKeys = nKeyMeta = nZKeys = nCZKeys = nZKeyMeta = nSapZAddrs = 0;
|
nKeys = nCKeys = nKeyMeta = nZKeys = nCZKeys = nZKeyMeta = nSapZAddrs = 0;
|
||||||
@@ -427,6 +430,7 @@ public:
|
|||||||
fAnyUnordered = false;
|
fAnyUnordered = false;
|
||||||
nFileVersion = 0;
|
nFileVersion = 0;
|
||||||
fHDChainRead = false;
|
fHDChainRead = false;
|
||||||
|
fHDChainRepaired = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -851,14 +855,48 @@ ReadKeyValue(CWallet* pwallet, CDataStream& ssKey, CDataStream& ssValue,
|
|||||||
else if (strType == "hdchain")
|
else if (strType == "hdchain")
|
||||||
{
|
{
|
||||||
CHDChain chain;
|
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 {
|
try {
|
||||||
ssValue >> chain;
|
ssValue >> chain;
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
// Do not let this land in the "user can live with it" bucket:
|
// Downgrade repair. A build predating VERSION_HD_TRANSPARENT writes
|
||||||
// report it, and leave wss.fHDChainRead false so LoadWallet
|
// this record back with only the four base fields while leaving
|
||||||
// turns it into DB_CORRUPT when a seed is present.
|
// nVersion at whatever it read, so the version-gated reads above run
|
||||||
strErr = "Error reading wallet database: hdchain record is corrupt";
|
// off the end. Without this, one address generated under such a build
|
||||||
return false;
|
// 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;
|
wss.fHDChainRead = true;
|
||||||
pwallet->SetHDChain(chain, true);
|
pwallet->SetHDChain(chain, true);
|
||||||
@@ -1016,6 +1054,18 @@ 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;
|
||||||
|
|
||||||
|
// 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
|
// 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
|
// unreadable is NOT safe to run. hdChain would fall back to its SetNull
|
||||||
// defaults (walletdb.h:105-113), which (a) clears fMnemonicSeed, switching
|
// 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.
|
// Fail loud instead of quietly deriving into the wrong tree.
|
||||||
if (pwallet->HaveHDSeed() && !wss.fHDChainRead)
|
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=\"<your seed phrase>\" -rescan\n");
|
||||||
return DB_CORRUPT;
|
return DB_CORRUPT;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user