From eed1785f701be93ac2464e854c2a7de1f748ef84 Mon Sep 17 00:00:00 2001 From: Gavin Andresen Date: Tue, 18 Sep 2012 14:30:47 -0400 Subject: Handle corrupt wallets gracefully. Corrupt wallets used to cause a DB_RUNRECOVERY uncaught exception and a crash. This commit does three things: 1) Runs a BDB verify early in the startup process, and if there is a low-level problem with the database: + Moves the bad wallet.dat to wallet.timestamp.bak + Runs a 'salvage' operation to get key/value pairs, and writes them to a new wallet.dat + Continues with startup. 2) Much more tolerant of serialization errors. All errors in deserialization are reported by tolerated EXCEPT for errors related to reading keypairs or master key records-- those are reported and then shut down, so the user can get help (or recover from a backup). 3) Adds a new -salvagewallet option, which: + Moves the wallet.dat to wallet.timestamp.bak + extracts ONLY keypairs and master keys into a new wallet.dat + soft-sets -rescan, to recreate transaction history This was tested by randomly corrupting testnet wallets using a little python script I wrote (https://gist.github.com/3812689) --- src/walletdb.cpp | 508 ++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 330 insertions(+), 178 deletions(-) (limited to 'src/walletdb.cpp') diff --git a/src/walletdb.cpp b/src/walletdb.cpp index 0fac0109c8..e102df9720 100644 --- a/src/walletdb.cpp +++ b/src/walletdb.cpp @@ -108,7 +108,7 @@ void CWalletDB::ListAccountCreditDebit(const string& strAccount, listcs_wallet); @@ -181,16 +181,221 @@ CWalletDB::ReorderTransactions(CWallet* pwallet) } -int CWalletDB::LoadWallet(CWallet* pwallet) +bool +ReadKeyValue(CWallet* pwallet, CDataStream& ssKey, CDataStream& ssValue, + int& nFileVersion, vector& vWalletUpgrade, + bool& fIsEncrypted, bool& fAnyUnordered, string& strType, string& strErr) +{ + try { + // Unserialize + // Taking advantage of the fact that pair serialization + // is just the two items serialized one after the other + ssKey >> strType; + if (strType == "name") + { + string strAddress; + ssKey >> strAddress; + ssValue >> pwallet->mapAddressBook[CBitcoinAddress(strAddress).Get()]; + } + else if (strType == "tx") + { + uint256 hash; + ssKey >> hash; + CWalletTx& wtx = pwallet->mapWallet[hash]; + ssValue >> wtx; + if (wtx.CheckTransaction() && (wtx.GetHash() == hash)) + wtx.BindWallet(pwallet); + else + { + pwallet->mapWallet.erase(hash); + return false; + } + + // Undo serialize changes in 31600 + if (31404 <= wtx.fTimeReceivedIsTxTime && wtx.fTimeReceivedIsTxTime <= 31703) + { + if (!ssValue.empty()) + { + char fTmp; + char fUnused; + ssValue >> fTmp >> fUnused >> wtx.strFromAccount; + strErr = strprintf("LoadWallet() upgrading tx ver=%d %d '%s' %s", + wtx.fTimeReceivedIsTxTime, fTmp, wtx.strFromAccount.c_str(), hash.ToString().c_str()); + wtx.fTimeReceivedIsTxTime = fTmp; + } + else + { + strErr = strprintf("LoadWallet() repairing tx ver=%d %s", wtx.fTimeReceivedIsTxTime, hash.ToString().c_str()); + wtx.fTimeReceivedIsTxTime = 0; + } + vWalletUpgrade.push_back(hash); + } + + if (wtx.nOrderPos == -1) + fAnyUnordered = true; + + //// debug print + //printf("LoadWallet %s\n", wtx.GetHash().ToString().c_str()); + //printf(" %12"PRI64d" %s %s %s\n", + // wtx.vout[0].nValue, + // DateTimeStrFormat("%x %H:%M:%S", wtx.GetBlockTime()).c_str(), + // wtx.hashBlock.ToString().substr(0,20).c_str(), + // wtx.mapValue["message"].c_str()); + } + else if (strType == "acentry") + { + string strAccount; + ssKey >> strAccount; + uint64 nNumber; + ssKey >> nNumber; + if (nNumber > nAccountingEntryNumber) + nAccountingEntryNumber = nNumber; + + if (!fAnyUnordered) + { + CAccountingEntry acentry; + ssValue >> acentry; + if (acentry.nOrderPos == -1) + fAnyUnordered = true; + } + } + else if (strType == "key" || strType == "wkey") + { + vector vchPubKey; + ssKey >> vchPubKey; + CKey key; + if (strType == "key") + { + CPrivKey pkey; + ssValue >> pkey; + key.SetPubKey(vchPubKey); + if (!key.SetPrivKey(pkey)) + { + strErr = "Error reading wallet database: CPrivKey corrupt"; + return false; + } + if (key.GetPubKey() != vchPubKey) + { + strErr = "Error reading wallet database: CPrivKey pubkey inconsistency"; + return false; + } + if (!key.IsValid()) + { + strErr = "Error reading wallet database: invalid CPrivKey"; + return false; + } + } + else + { + CWalletKey wkey; + ssValue >> wkey; + key.SetPubKey(vchPubKey); + if (!key.SetPrivKey(wkey.vchPrivKey)) + { + strErr = "Error reading wallet database: CPrivKey corrupt"; + return false; + } + if (key.GetPubKey() != vchPubKey) + { + strErr = "Error reading wallet database: CWalletKey pubkey inconsistency"; + return false; + } + if (!key.IsValid()) + { + strErr = "Error reading wallet database: invalid CWalletKey"; + return false; + } + } + if (!pwallet->LoadKey(key)) + { + strErr = "Error reading wallet database: LoadKey failed"; + return false; + } + } + else if (strType == "mkey") + { + unsigned int nID; + ssKey >> nID; + CMasterKey kMasterKey; + ssValue >> kMasterKey; + if(pwallet->mapMasterKeys.count(nID) != 0) + { + strErr = strprintf("Error reading wallet database: duplicate CMasterKey id %u", nID); + return false; + } + pwallet->mapMasterKeys[nID] = kMasterKey; + if (pwallet->nMasterKeyMaxID < nID) + pwallet->nMasterKeyMaxID = nID; + } + else if (strType == "ckey") + { + vector vchPubKey; + ssKey >> vchPubKey; + vector vchPrivKey; + ssValue >> vchPrivKey; + if (!pwallet->LoadCryptedKey(vchPubKey, vchPrivKey)) + { + strErr = "Error reading wallet database: LoadCryptedKey failed"; + return false; + } + fIsEncrypted = true; + } + else if (strType == "defaultkey") + { + ssValue >> pwallet->vchDefaultKey; + } + else if (strType == "pool") + { + int64 nIndex; + ssKey >> nIndex; + pwallet->setKeyPool.insert(nIndex); + } + else if (strType == "version") + { + ssValue >> nFileVersion; + if (nFileVersion == 10300) + nFileVersion = 300; + } + else if (strType == "cscript") + { + uint160 hash; + ssKey >> hash; + CScript script; + ssValue >> script; + if (!pwallet->LoadCScript(script)) + { + strErr = "Error reading wallet database: LoadCScript failed"; + return false; + } + } + else if (strType == "orderposnext") + { + ssValue >> pwallet->nOrderPosNext; + } + } catch (...) + { + return false; + } + return true; +} + +static bool IsKeyType(string strType) +{ + return (strType== "key" || strType == "wkey" || + strType == "mkey" || strType == "ckey"); +} + +DBErrors CWalletDB::LoadWallet(CWallet* pwallet) { pwallet->vchDefaultKey = CPubKey(); int nFileVersion = 0; vector vWalletUpgrade; bool fIsEncrypted = false; bool fAnyUnordered = false; + bool fNoncriticalErrors = false; + DBErrors result = DB_LOAD_OK; - //// todo: shouldn't we catch exceptions and try to recover and continue? - { + try { LOCK(pwallet->cs_wallet); int nMinVersion = 0; if (Read((string)"minversion", nMinVersion)) @@ -222,189 +427,46 @@ int CWalletDB::LoadWallet(CWallet* pwallet) return DB_CORRUPT; } - // Unserialize - // Taking advantage of the fact that pair serialization - // is just the two items serialized one after the other - string strType; - ssKey >> strType; - if (strType == "name") - { - string strAddress; - ssKey >> strAddress; - ssValue >> pwallet->mapAddressBook[CBitcoinAddress(strAddress).Get()]; - } - else if (strType == "tx") - { - uint256 hash; - ssKey >> hash; - CWalletTx& wtx = pwallet->mapWallet[hash]; - ssValue >> wtx; - wtx.BindWallet(pwallet); - - if (wtx.GetHash() != hash) - printf("Error in wallet.dat, hash mismatch\n"); - - // Undo serialize changes in 31600 - if (31404 <= wtx.fTimeReceivedIsTxTime && wtx.fTimeReceivedIsTxTime <= 31703) - { - if (!ssValue.empty()) - { - char fTmp; - char fUnused; - ssValue >> fTmp >> fUnused >> wtx.strFromAccount; - printf("LoadWallet() upgrading tx ver=%d %d '%s' %s\n", wtx.fTimeReceivedIsTxTime, fTmp, wtx.strFromAccount.c_str(), hash.ToString().c_str()); - wtx.fTimeReceivedIsTxTime = fTmp; - } - else - { - printf("LoadWallet() repairing tx ver=%d %s\n", wtx.fTimeReceivedIsTxTime, hash.ToString().c_str()); - wtx.fTimeReceivedIsTxTime = 0; - } - vWalletUpgrade.push_back(hash); - } - - if (wtx.nOrderPos == -1) - fAnyUnordered = true; - - //// debug print - //printf("LoadWallet %s\n", wtx.GetHash().ToString().c_str()); - //printf(" %12"PRI64d" %s %s %s\n", - // wtx.vout[0].nValue, - // DateTimeStrFormat("%x %H:%M:%S", wtx.GetBlockTime()).c_str(), - // wtx.hashBlock.ToString().substr(0,20).c_str(), - // wtx.mapValue["message"].c_str()); - } - else if (strType == "acentry") + // Try to be tolerant of single corrupt records: + string strType, strErr; + if (!ReadKeyValue(pwallet, ssKey, ssValue, nFileVersion, + vWalletUpgrade, fIsEncrypted, fAnyUnordered, strType, strErr)) { - string strAccount; - ssKey >> strAccount; - uint64 nNumber; - ssKey >> nNumber; - if (nNumber > nAccountingEntryNumber) - nAccountingEntryNumber = nNumber; - - if (!fAnyUnordered) - { - CAccountingEntry acentry; - ssValue >> acentry; - if (acentry.nOrderPos == -1) - fAnyUnordered = true; - } - } - else if (strType == "key" || strType == "wkey") - { - vector vchPubKey; - ssKey >> vchPubKey; - CKey key; - if (strType == "key") - { - CPrivKey pkey; - ssValue >> pkey; - key.SetPubKey(vchPubKey); - key.SetPrivKey(pkey); - if (key.GetPubKey() != vchPubKey) - { - printf("Error reading wallet database: CPrivKey pubkey inconsistency\n"); - return DB_CORRUPT; - } - if (!key.IsValid()) - { - printf("Error reading wallet database: invalid CPrivKey\n"); - return DB_CORRUPT; - } - } + // losing keys is considered a catastrophic error, anything else + // we assume the user can live with: + if (IsKeyType(strType)) + result = DB_CORRUPT; else { - CWalletKey wkey; - ssValue >> wkey; - key.SetPubKey(vchPubKey); - key.SetPrivKey(wkey.vchPrivKey); - if (key.GetPubKey() != vchPubKey) - { - printf("Error reading wallet database: CWalletKey pubkey inconsistency\n"); - return DB_CORRUPT; - } - if (!key.IsValid()) - { - printf("Error reading wallet database: invalid CWalletKey\n"); - return DB_CORRUPT; - } - } - if (!pwallet->LoadKey(key)) - { - printf("Error reading wallet database: LoadKey failed\n"); - return DB_CORRUPT; - } - } - else if (strType == "mkey") - { - unsigned int nID; - ssKey >> nID; - CMasterKey kMasterKey; - ssValue >> kMasterKey; - if(pwallet->mapMasterKeys.count(nID) != 0) - { - printf("Error reading wallet database: duplicate CMasterKey id %u\n", nID); - return DB_CORRUPT; - } - pwallet->mapMasterKeys[nID] = kMasterKey; - if (pwallet->nMasterKeyMaxID < nID) - pwallet->nMasterKeyMaxID = nID; - } - else if (strType == "ckey") - { - vector vchPubKey; - ssKey >> vchPubKey; - vector vchPrivKey; - ssValue >> vchPrivKey; - if (!pwallet->LoadCryptedKey(vchPubKey, vchPrivKey)) - { - printf("Error reading wallet database: LoadCryptedKey failed\n"); - return DB_CORRUPT; + // Leave other errors alone, if we try to fix them we might make things worse. + fNoncriticalErrors = true; // ... but do warn the user there is something wrong. + if (strType == "tx") + // Rescan if there is a bad transaction record: + SoftSetBoolArg("-rescan", true); } - fIsEncrypted = true; - } - else if (strType == "defaultkey") - { - ssValue >> pwallet->vchDefaultKey; - } - else if (strType == "pool") - { - int64 nIndex; - ssKey >> nIndex; - pwallet->setKeyPool.insert(nIndex); - } - else if (strType == "version") - { - ssValue >> nFileVersion; - if (nFileVersion == 10300) - nFileVersion = 300; - } - else if (strType == "cscript") - { - uint160 hash; - ssKey >> hash; - CScript script; - ssValue >> script; - if (!pwallet->LoadCScript(script)) - { - printf("Error reading wallet database: LoadCScript failed\n"); - return DB_CORRUPT; - } - } - else if (strType == "orderposnext") - { - ssValue >> pwallet->nOrderPosNext; } + if (!strErr.empty()) + printf("%s\n", strErr.c_str()); } pcursor->close(); } + catch (...) + { + result = DB_CORRUPT; + } - BOOST_FOREACH(uint256 hash, vWalletUpgrade) - WriteTx(hash, pwallet->mapWallet[hash]); + if (fNoncriticalErrors && result == DB_LOAD_OK) + result = DB_NONCRITICAL_ERROR; + + // Any wallet corruption at all: skip any rewriting or + // upgrading, we don't want to make it worse. + if (result != DB_LOAD_OK) + return result; printf("nFileVersion = %d\n", nFileVersion); + BOOST_FOREACH(uint256 hash, vWalletUpgrade) + WriteTx(hash, pwallet->mapWallet[hash]); // Rewrite encrypted wallets of versions 0.4.0 and 0.5.0rc: if (fIsEncrypted && (nFileVersion == 40000 || nFileVersion == 50000)) @@ -414,10 +476,9 @@ int CWalletDB::LoadWallet(CWallet* pwallet) WriteVersion(CLIENT_VERSION); if (fAnyUnordered) - return ReorderTransactions(pwallet); + result = ReorderTransactions(pwallet); - // If you add anything else here... be sure to do it if ReorderTransactions returns DB_LOAD_OK too! - return DB_LOAD_OK; + return result; } void ThreadFlushWalletDB(void* parg) @@ -521,3 +582,94 @@ bool BackupWallet(const CWallet& wallet, const string& strDest) } return false; } + +// +// Try to (very carefully!) recover wallet.dat if there is a problem. +// +bool CWalletDB::Recover(CDBEnv& dbenv, std::string filename, bool fOnlyKeys) +{ + // Recovery procedure: + // move wallet.dat to wallet.timestamp.bak + // Call Salvage with fAggressive=true to + // get as much data as possible. + // Rewrite salvaged data to wallet.dat + // Set -rescan so any missing transactions will be + // found. + int64 now = GetTime(); + std::string newFilename = strprintf("wallet.%"PRI64d".bak", now); + + int result = dbenv.dbenv.dbrename(NULL, filename.c_str(), NULL, + newFilename.c_str(), DB_AUTO_COMMIT); + if (result == 0) + printf("Renamed %s to %s\n", filename.c_str(), newFilename.c_str()); + else + { + printf("Failed to rename %s to %s\n", filename.c_str(), newFilename.c_str()); + return false; + } + + std::vector salvagedData; + bool allOK = dbenv.Salvage(newFilename, true, salvagedData); + if (salvagedData.empty()) + { + printf("Salvage(aggressive) found no records in %s.\n", newFilename.c_str()); + return false; + } + printf("Salvage(aggressive) found %"PRIszu" records\n", salvagedData.size()); + + bool fSuccess = allOK; + Db* pdbCopy = new Db(&dbenv.dbenv, 0); + int ret = pdbCopy->open(NULL, // Txn pointer + filename.c_str(), // Filename + "main", // Logical db name + DB_BTREE, // Database type + DB_CREATE, // Flags + 0); + if (ret > 0) + { + printf("Cannot create database file %s\n", filename.c_str()); + return false; + } + CWallet dummyWallet; + int nFileVersion = 0; + vector vWalletUpgrade; + bool fIsEncrypted = false; + bool fAnyUnordered = false; + + DbTxn* ptxn = dbenv.TxnBegin(); + BOOST_FOREACH(CDBEnv::KeyValPair& row, salvagedData) + { + if (fOnlyKeys) + { + CDataStream ssKey(row.first, SER_DISK, CLIENT_VERSION); + CDataStream ssValue(row.second, SER_DISK, CLIENT_VERSION); + string strType, strErr; + bool fReadOK = ReadKeyValue(&dummyWallet, ssKey, ssValue, + nFileVersion, vWalletUpgrade, + fIsEncrypted, fAnyUnordered, + strType, strErr); + if (!IsKeyType(strType)) + continue; + if (!fReadOK) + { + printf("WARNING: CWalletDB::Recover skipping %s: %s\n", strType.c_str(), strErr.c_str()); + continue; + } + } + Dbt datKey(&row.first[0], row.first.size()); + Dbt datValue(&row.second[0], row.second.size()); + int ret2 = pdbCopy->put(ptxn, &datKey, &datValue, DB_NOOVERWRITE); + if (ret2 > 0) + fSuccess = false; + } + ptxn->commit(0); + pdbCopy->close(0); + delete pdbCopy; + + return fSuccess; +} + +bool CWalletDB::Recover(CDBEnv& dbenv, std::string filename) +{ + return CWalletDB::Recover(dbenv, filename, false); +} -- cgit v1.2.3