A wallet.dat repaired by v1.0.3-or-earlier -salvagewallet could not be opened by
this build at all. Those builds' IsKeyType has no "hdchain" case, so salvage
dropped the record; LoadWallet then returned DB_CORRUPT and told the user to
restore from a seed phrase. There is no such recovery path: the node aborts
before the RPC server exists, -usemnemonic defaulted to 0 in v1.0.3 so many of
these wallets never had a phrase, and SetHDSeedFromMnemonic refuses a non-empty
wallet, so "move wallet.dat aside" discards every non-HD key salvage preserved.
The guard itself was right to refuse to DERIVE -- a missing hdchain falls back to
SetNull defaults, clearing fMnemonicSeed and switching derivation from the
64-byte BIP39 seed to the raw 32-byte entropy, an entirely different key tree.
It was wrong to refuse to OPEN. Split the two:
- "hdchain" record ABSENT (the salvage signature, tracked by a new
wss.fHDChainSeen set before any parse attempt) -> open DEGRADED.
- "hdchain" record PRESENT but unreadable -> still DB_CORRUPT; that signals
wider file damage.
Degraded means the wallet spends, receives and rescans normally but refuses to
derive any new HD key. Gated at the single choke point GetHDSeedForDerivation
rather than at the two generators, so z_getnewaddress/sendmany/shieldcoinbase
raise a clean error and autoshield skips. hdChain.seedFp is deliberately left
null, which keeps IsHDTransparentEnabled false so getnewaddress falls back to the
legacy random-key path -- gating it instead would break TopUpKeyPool, change
addresses and block templates. -rescan is soft-set because an old salvage also
dropped defaultkey and bestblock, which would otherwise look like a first run and
skip the rescan, leaving a permanently zero balance.
NEW DETECTOR, covering strictly more damage than the guard beside it. A wallet
salvaged by v1.0.3 and then USED on v1.0.3 persists a SetNull-derived hdchain
carrying seedFp=null. On this build fHDChainRead is then true, the guard never
fires, the node starts perfectly clean -- and derives into the wrong key tree
forever, silently. That state is unforgeable by any legitimate writer, since
InstallHDSeed always stores seedFp = seed.Fingerprint(), so compare the loaded
chain's seedFp against the fingerprint taken from the hdseed/chdseed record KEY
(which works while an encrypted wallet is locked) and degrade on mismatch.
Also:
- init.cpp: abort in the DB_CORRUPT branch itself, as DB_NEED_REWRITE already
does. Falling through ran several hundred more lines against a wallet just
declared corrupt, including SetHDSeedOrigin(), which WRITES to it. Error text
no longer advises -mnemonic (wrong twice over, see above).
- rpcdump.cpp: z_exportwallet discarded GetHDSeedForDerivation's return and
emitted the line regardless, so a failure wrote a blank seed next to a
legitimate-looking BLAKE2b-of-empty fingerprint -- a backup that looks valid
and restores nothing.
- Corrected the guard's comment: the saplingAccountCounter half was overstated.
Both generators loop while(Have...Key(...)), so a low counter walks forward
past existing accounts rather than colliding with them.
qa/r4-salvaged-wallet-harness.sh builds the victim wallets with the on-disk
v1.0.1 release binary and asserts the behaviour end to end. It REFUSES to run
outside a network namespace and re-checks getconnectioncount==0, because those
old binaries predate the regtest seed-injection fix and would otherwise dial the
live network. 11/11 pass:
C healthy wallet opens and is NOT flagged (no false positives)
A salvaged wallet opens, flagged, persists no hdchain, z_getnewaddress refused,
getnewaddress still works, z_exportwallet emits no bogus HDSeed line
B poisoned wallet opens and the seedFp detector fires
Note the harness asserts "no hdchain synthesised", NOT "wallet.dat unchanged":
a control run showed normal startup rewrites wallet.dat for healthy wallets too.
This is the access/detection half. Proving fMnemonicSeed by re-deriving against
held keys, and repairing the record, is deliberately left out: it requires a
write, and the affected population is unmeasured. dev's own salvage already
preserves hdchain, so the class is closed going forward.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FU87LdsJZiZkfq1eXubpeo
1504 lines
53 KiB
C++
1504 lines
53 KiB
C++
// Copyright (c) 2009-2010 Satoshi Nakamoto
|
|
// Copyright (c) 2009-2014 The Bitcoin Core developers
|
|
// Copyright (c) 2016-2024 The Hush developers
|
|
// Distributed under the GPLv3 software license, see the accompanying
|
|
// file COPYING or https://www.gnu.org/licenses/gpl-3.0.en.html
|
|
/******************************************************************************
|
|
* Copyright © 2014-2019 The SuperNET Developers. *
|
|
* *
|
|
* See the AUTHORS, DEVELOPER-AGREEMENT and LICENSE files at *
|
|
* the top-level directory of this distribution for the individual copyright *
|
|
* holder information and the developer policies on copyright and licensing. *
|
|
* *
|
|
* Unless otherwise agreed in a custom licensing agreement, no part of the *
|
|
* SuperNET software, including this file may be copied, modified, propagated *
|
|
* or distributed except according to the terms contained in the LICENSE file *
|
|
* *
|
|
* Removal or modification of this copyright notice is prohibited. *
|
|
* *
|
|
******************************************************************************/
|
|
#include "wallet/walletdb.h"
|
|
#include "consensus/validation.h"
|
|
#include "key_io.h"
|
|
#include "main.h"
|
|
#include "protocol.h"
|
|
#include "serialize.h"
|
|
#include "sync.h"
|
|
#include "util.h"
|
|
#include "utiltime.h"
|
|
#include "wallet/wallet.h"
|
|
#include "zcash/Proof.hpp"
|
|
#include "hush_defs.h"
|
|
#include <boost/filesystem.hpp>
|
|
#include <boost/foreach.hpp>
|
|
#include <boost/scoped_ptr.hpp>
|
|
#include <boost/thread.hpp>
|
|
|
|
// Out-of-line definitions for CHDChain's in-class static constants. These are
|
|
// only initialised in the class body, so any ODR use -- binding one to a const
|
|
// reference, which is exactly what gtest's EXPECT_*/ASSERT_* macros do -- needs
|
|
// a definition or the link fails. hush-gtest hit this on VERSION_HD_MNEMONIC.
|
|
const int CHDChain::VERSION_HD_BASE;
|
|
const int CHDChain::VERSION_HD_TRANSPARENT;
|
|
const int CHDChain::VERSION_HD_MNEMONIC;
|
|
const int CHDChain::CURRENT_VERSION;
|
|
|
|
using namespace std;
|
|
|
|
static uint64_t nAccountingEntryNumber = 0;
|
|
|
|
//
|
|
// CWalletDB
|
|
//
|
|
|
|
bool CWalletDB::WriteName(const string& strAddress, const string& strName)
|
|
{
|
|
nWalletDBUpdated++;
|
|
return Write(make_pair(string("name"), strAddress), strName);
|
|
}
|
|
|
|
bool CWalletDB::EraseName(const string& strAddress)
|
|
{
|
|
// This should only be used for sending addresses, never for receiving addresses,
|
|
// receiving addresses must always have an address book entry if they're not change return.
|
|
nWalletDBUpdated++;
|
|
return Erase(make_pair(string("name"), strAddress));
|
|
}
|
|
|
|
bool CWalletDB::WritePurpose(const string& strAddress, const string& strPurpose)
|
|
{
|
|
nWalletDBUpdated++;
|
|
return Write(make_pair(string("purpose"), strAddress), strPurpose);
|
|
}
|
|
|
|
bool CWalletDB::ErasePurpose(const string& strPurpose)
|
|
{
|
|
nWalletDBUpdated++;
|
|
return Erase(make_pair(string("purpose"), strPurpose));
|
|
}
|
|
|
|
bool CWalletDB::WriteTx(uint256 hash, const CWalletTx& wtx)
|
|
{
|
|
nWalletDBUpdated++;
|
|
return Write(std::make_pair(std::string("tx"), hash), wtx);
|
|
}
|
|
|
|
bool CWalletDB::EraseTx(uint256 hash)
|
|
{
|
|
nWalletDBUpdated++;
|
|
return Erase(std::make_pair(std::string("tx"), hash));
|
|
}
|
|
|
|
bool CWalletDB::WriteKey(const CPubKey& vchPubKey, const CPrivKey& vchPrivKey, const CKeyMetadata& keyMeta)
|
|
{
|
|
nWalletDBUpdated++;
|
|
|
|
if (!Write(std::make_pair(std::string("keymeta"), vchPubKey),
|
|
keyMeta, false))
|
|
return false;
|
|
|
|
// hash pubkey/privkey to accelerate wallet load
|
|
std::vector<unsigned char> vchKey;
|
|
vchKey.reserve(vchPubKey.size() + vchPrivKey.size());
|
|
vchKey.insert(vchKey.end(), vchPubKey.begin(), vchPubKey.end());
|
|
vchKey.insert(vchKey.end(), vchPrivKey.begin(), vchPrivKey.end());
|
|
|
|
return Write(std::make_pair(std::string("key"), vchPubKey), std::make_pair(vchPrivKey, Hash(vchKey.begin(), vchKey.end())), false);
|
|
}
|
|
|
|
bool CWalletDB::WriteCryptedKey(const CPubKey& vchPubKey,
|
|
const std::vector<unsigned char>& vchCryptedSecret,
|
|
const CKeyMetadata &keyMeta)
|
|
{
|
|
const bool fEraseUnencryptedKey = true;
|
|
nWalletDBUpdated++;
|
|
|
|
if (!Write(std::make_pair(std::string("keymeta"), vchPubKey),
|
|
keyMeta))
|
|
return false;
|
|
|
|
if (!Write(std::make_pair(std::string("ckey"), vchPubKey), vchCryptedSecret, false))
|
|
return false;
|
|
if (fEraseUnencryptedKey)
|
|
{
|
|
Erase(std::make_pair(std::string("key"), vchPubKey));
|
|
Erase(std::make_pair(std::string("wkey"), vchPubKey));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool CWalletDB::WriteCryptedSaplingZKey(
|
|
const libzcash::SaplingExtendedFullViewingKey &extfvk,
|
|
const std::vector<unsigned char>& vchCryptedSecret,
|
|
const CKeyMetadata &keyMeta)
|
|
{
|
|
const bool fEraseUnencryptedKey = true;
|
|
nWalletDBUpdated++;
|
|
auto ivk = extfvk.fvk.in_viewing_key();
|
|
|
|
if (!Write(std::make_pair(std::string("sapzkeymeta"), ivk), keyMeta))
|
|
return false;
|
|
|
|
if (!Write(std::make_pair(std::string("csapzkey"), ivk), std::make_pair(extfvk, vchCryptedSecret), false))
|
|
return false;
|
|
|
|
if (fEraseUnencryptedKey)
|
|
{
|
|
Erase(std::make_pair(std::string("sapzkey"), ivk));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool CWalletDB::WriteMasterKey(unsigned int nID, const CMasterKey& kMasterKey)
|
|
{
|
|
nWalletDBUpdated++;
|
|
return Write(std::make_pair(std::string("mkey"), nID), kMasterKey, true);
|
|
}
|
|
|
|
bool CWalletDB::WriteSaplingZKey(const libzcash::SaplingIncomingViewingKey &ivk,
|
|
const libzcash::SaplingExtendedSpendingKey &key,
|
|
const CKeyMetadata &keyMeta)
|
|
{
|
|
nWalletDBUpdated++;
|
|
|
|
if (!Write(std::make_pair(std::string("sapzkeymeta"), ivk), keyMeta))
|
|
return false;
|
|
|
|
return Write(std::make_pair(std::string("sapzkey"), ivk), key, false);
|
|
}
|
|
|
|
bool CWalletDB::WriteSaplingPaymentAddress(
|
|
const libzcash::SaplingPaymentAddress &addr,
|
|
const libzcash::SaplingIncomingViewingKey &ivk)
|
|
{
|
|
nWalletDBUpdated++;
|
|
|
|
return Write(std::make_pair(std::string("sapzaddr"), addr), ivk, false);
|
|
}
|
|
|
|
bool CWalletDB::WriteCScript(const uint160& hash, const CScript& redeemScript)
|
|
{
|
|
nWalletDBUpdated++;
|
|
return Write(std::make_pair(std::string("cscript"), hash), *(const CScriptBase*)(&redeemScript), false);
|
|
}
|
|
|
|
bool CWalletDB::WriteWatchOnly(const CScript &dest)
|
|
{
|
|
nWalletDBUpdated++;
|
|
return Write(std::make_pair(std::string("watchs"), *(const CScriptBase*)(&dest)), '1');
|
|
}
|
|
|
|
bool CWalletDB::EraseWatchOnly(const CScript &dest)
|
|
{
|
|
nWalletDBUpdated++;
|
|
return Erase(std::make_pair(std::string("watchs"), *(const CScriptBase*)(&dest)));
|
|
}
|
|
|
|
bool CWalletDB::WriteBestBlock(const CBlockLocator& locator)
|
|
{
|
|
nWalletDBUpdated++;
|
|
return Write(std::string("bestblock"), locator);
|
|
}
|
|
|
|
bool CWalletDB::ReadBestBlock(CBlockLocator& locator)
|
|
{
|
|
return Read(std::string("bestblock"), locator);
|
|
}
|
|
|
|
bool CWalletDB::WriteOrderPosNext(int64_t nOrderPosNext)
|
|
{
|
|
nWalletDBUpdated++;
|
|
return Write(std::string("orderposnext"), nOrderPosNext);
|
|
}
|
|
|
|
bool CWalletDB::WriteDefaultKey(const CPubKey& vchPubKey)
|
|
{
|
|
nWalletDBUpdated++;
|
|
return Write(std::string("defaultkey"), vchPubKey);
|
|
}
|
|
|
|
bool CWalletDB::WriteWitnessCacheSize(int64_t nWitnessCacheSize)
|
|
{
|
|
nWalletDBUpdated++;
|
|
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)
|
|
{
|
|
return Read(std::make_pair(std::string("pool"), nPool), keypool);
|
|
}
|
|
|
|
bool CWalletDB::WritePool(int64_t nPool, const CKeyPool& keypool)
|
|
{
|
|
nWalletDBUpdated++;
|
|
return Write(std::make_pair(std::string("pool"), nPool), keypool);
|
|
}
|
|
|
|
bool CWalletDB::ErasePool(int64_t nPool)
|
|
{
|
|
nWalletDBUpdated++;
|
|
return Erase(std::make_pair(std::string("pool"), nPool));
|
|
}
|
|
|
|
bool CWalletDB::WriteMinVersion(int nVersion)
|
|
{
|
|
return Write(std::string("minversion"), nVersion);
|
|
}
|
|
|
|
bool CWalletDB::ReadAccount(const string& strAccount, CAccount& account)
|
|
{
|
|
account.SetNull();
|
|
return Read(make_pair(string("acc"), strAccount), account);
|
|
}
|
|
|
|
bool CWalletDB::WriteAccount(const string& strAccount, const CAccount& account)
|
|
{
|
|
return Write(make_pair(string("acc"), strAccount), account);
|
|
}
|
|
|
|
bool CWalletDB::WriteAccountingEntry(const uint64_t nAccEntryNum, const CAccountingEntry& acentry)
|
|
{
|
|
return Write(std::make_pair(std::string("acentry"), std::make_pair(acentry.strAccount, nAccEntryNum)), acentry);
|
|
}
|
|
|
|
bool CWalletDB::WriteAccountingEntry(const CAccountingEntry& acentry)
|
|
{
|
|
return WriteAccountingEntry(++nAccountingEntryNumber, acentry);
|
|
}
|
|
|
|
CAmount CWalletDB::GetAccountCreditDebit(const string& strAccount)
|
|
{
|
|
list<CAccountingEntry> entries;
|
|
ListAccountCreditDebit(strAccount, entries);
|
|
|
|
CAmount nCreditDebit = 0;
|
|
BOOST_FOREACH (const CAccountingEntry& entry, entries)
|
|
nCreditDebit += entry.nCreditDebit;
|
|
|
|
return nCreditDebit;
|
|
}
|
|
|
|
void CWalletDB::ListAccountCreditDebit(const string& strAccount, list<CAccountingEntry>& entries)
|
|
{
|
|
bool fAllAccounts = (strAccount == "*");
|
|
|
|
Dbc* pcursor = GetCursor();
|
|
if (!pcursor)
|
|
throw runtime_error("CWalletDB::ListAccountCreditDebit(): cannot create DB cursor");
|
|
unsigned int fFlags = DB_SET_RANGE;
|
|
while (true)
|
|
{
|
|
// Read next record
|
|
CDataStream ssKey(SER_DISK, CLIENT_VERSION);
|
|
if (fFlags == DB_SET_RANGE)
|
|
ssKey << std::make_pair(std::string("acentry"), std::make_pair((fAllAccounts ? string("") : strAccount), uint64_t(0)));
|
|
CDataStream ssValue(SER_DISK, CLIENT_VERSION);
|
|
int ret = ReadAtCursor(pcursor, ssKey, ssValue, fFlags);
|
|
fFlags = DB_NEXT;
|
|
if (ret == DB_NOTFOUND)
|
|
break;
|
|
else if (ret != 0)
|
|
{
|
|
pcursor->close();
|
|
throw runtime_error("CWalletDB::ListAccountCreditDebit(): error scanning DB");
|
|
}
|
|
|
|
// Unserialize
|
|
string strType;
|
|
ssKey >> strType;
|
|
if (strType != "acentry")
|
|
break;
|
|
CAccountingEntry acentry;
|
|
ssKey >> acentry.strAccount;
|
|
if (!fAllAccounts && acentry.strAccount != strAccount)
|
|
break;
|
|
|
|
ssValue >> acentry;
|
|
ssKey >> acentry.nEntryNo;
|
|
entries.push_back(acentry);
|
|
}
|
|
|
|
pcursor->close();
|
|
}
|
|
|
|
DBErrors CWalletDB::ReorderTransactions(CWallet* pwallet)
|
|
{
|
|
LOCK(pwallet->cs_wallet);
|
|
// Old wallets didn't have any defined order for transactions
|
|
// Probably a bad idea to change the output of this
|
|
|
|
// First: get all CWalletTx and CAccountingEntry into a sorted-by-time multimap.
|
|
typedef pair<CWalletTx*, CAccountingEntry*> TxPair;
|
|
typedef multimap<int64_t, TxPair > TxItems;
|
|
TxItems txByTime;
|
|
|
|
for (map<uint256, CWalletTx>::iterator it = pwallet->mapWallet.begin(); it != pwallet->mapWallet.end(); ++it)
|
|
{
|
|
CWalletTx* wtx = &((*it).second);
|
|
txByTime.insert(make_pair(wtx->nTimeReceived, TxPair(wtx, (CAccountingEntry*)0)));
|
|
}
|
|
list<CAccountingEntry> acentries;
|
|
ListAccountCreditDebit("", acentries);
|
|
BOOST_FOREACH(CAccountingEntry& entry, acentries)
|
|
{
|
|
txByTime.insert(make_pair(entry.nTime, TxPair((CWalletTx*)0, &entry)));
|
|
}
|
|
|
|
int64_t& nOrderPosNext = pwallet->nOrderPosNext;
|
|
nOrderPosNext = 0;
|
|
std::vector<int64_t> nOrderPosOffsets;
|
|
for (TxItems::iterator it = txByTime.begin(); it != txByTime.end(); ++it)
|
|
{
|
|
CWalletTx *const pwtx = (*it).second.first;
|
|
CAccountingEntry *const pacentry = (*it).second.second;
|
|
int64_t& nOrderPos = (pwtx != 0) ? pwtx->nOrderPos : pacentry->nOrderPos;
|
|
|
|
if (nOrderPos == -1)
|
|
{
|
|
nOrderPos = nOrderPosNext++;
|
|
nOrderPosOffsets.push_back(nOrderPos);
|
|
|
|
if (pwtx)
|
|
{
|
|
if (!WriteTx(pwtx->GetHash(), *pwtx))
|
|
return DB_LOAD_FAIL;
|
|
}
|
|
else
|
|
if (!WriteAccountingEntry(pacentry->nEntryNo, *pacentry))
|
|
return DB_LOAD_FAIL;
|
|
}
|
|
else
|
|
{
|
|
int64_t nOrderPosOff = 0;
|
|
BOOST_FOREACH(const int64_t& nOffsetStart, nOrderPosOffsets)
|
|
{
|
|
if (nOrderPos >= nOffsetStart)
|
|
++nOrderPosOff;
|
|
}
|
|
nOrderPos += nOrderPosOff;
|
|
nOrderPosNext = std::max(nOrderPosNext, nOrderPos + 1);
|
|
|
|
if (!nOrderPosOff)
|
|
continue;
|
|
|
|
// Since we're changing the order, write it back
|
|
if (pwtx)
|
|
{
|
|
if (!WriteTx(pwtx->GetHash(), *pwtx))
|
|
return DB_LOAD_FAIL;
|
|
}
|
|
else
|
|
if (!WriteAccountingEntry(pacentry->nEntryNo, *pacentry))
|
|
return DB_LOAD_FAIL;
|
|
}
|
|
}
|
|
WriteOrderPosNext(nOrderPosNext);
|
|
|
|
return DB_LOAD_OK;
|
|
}
|
|
|
|
class CWalletScanState {
|
|
public:
|
|
unsigned int nKeys;
|
|
unsigned int nCKeys;
|
|
unsigned int nKeyMeta;
|
|
unsigned int nZKeys;
|
|
unsigned int nCZKeys;
|
|
unsigned int nZKeyMeta;
|
|
unsigned int nSapZAddrs;
|
|
bool fIsEncrypted;
|
|
bool fAnyUnordered;
|
|
int nFileVersion;
|
|
vector<uint256> 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;
|
|
// True once an "hdchain" record was ENCOUNTERED, whether or not it parsed. This is what
|
|
// separates the two failure shapes: a v1.0.3-or-earlier -salvagewallet drops the record
|
|
// entirely (its IsKeyType has no "hdchain" case), so absent == salvaged and recoverable,
|
|
// while present-but-unreadable means wider file damage.
|
|
bool fHDChainSeen;
|
|
// Fingerprint taken from the KEY of the hdseed/chdseed record. Available even for an
|
|
// encrypted wallet, where the seed itself cannot be read at load time.
|
|
bool fHDSeedSeen;
|
|
uint256 hdSeedFpSeen;
|
|
|
|
CWalletScanState() {
|
|
nKeys = nCKeys = nKeyMeta = nZKeys = nCZKeys = nZKeyMeta = nSapZAddrs = 0;
|
|
fIsEncrypted = false;
|
|
fAnyUnordered = false;
|
|
nFileVersion = 0;
|
|
fHDChainRead = false;
|
|
fHDChainRepaired = false;
|
|
fHDChainSeen = false;
|
|
fHDSeedSeen = false;
|
|
}
|
|
};
|
|
|
|
bool
|
|
ReadKeyValue(CWallet* pwallet, CDataStream& ssKey, CDataStream& ssValue,
|
|
CWalletScanState &wss, 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[DecodeDestination(strAddress)].name;
|
|
}
|
|
else if (strType == "purpose")
|
|
{
|
|
string strAddress;
|
|
ssKey >> strAddress;
|
|
ssValue >> pwallet->mapAddressBook[DecodeDestination(strAddress)].purpose;
|
|
}
|
|
else if (strType == "tx")
|
|
{
|
|
uint256 hash;
|
|
ssKey >> hash;
|
|
CWalletTx wtx;
|
|
ssValue >> wtx;
|
|
CValidationState state;
|
|
auto verifier = libzcash::ProofVerifier::Strict();
|
|
// ac_public chains which enable at a certain height instead of from genesis, this will force a rescan if we dont ignore this error: bad-txns-acpublic-chain
|
|
// there cannot be any ztx in the wallet on ac_public chains that started from block 1, so this wont affect those.
|
|
// ac_private chains fail this check for notary nodes, need exception. Triggers full rescan without it.
|
|
if ( !(CheckTransaction(0,wtx, state, verifier, 0, 0) && (wtx.GetHash() == hash) && state.IsValid()) && (state.GetRejectReason() != "bad-txns-acpublic-chain" && state.GetRejectReason() != "bad-txns-acprivacy-chain" && state.GetRejectReason() != "bad-txns-stakingtx") )
|
|
{
|
|
//fprintf(stderr, "tx failed: %s rejectreason.%s\n", wtx.GetHash().GetHex().c_str(), state.GetRejectReason().c_str());
|
|
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, hash.ToString());
|
|
wtx.fTimeReceivedIsTxTime = fTmp;
|
|
}
|
|
else
|
|
{
|
|
strErr = strprintf("LoadWallet() repairing tx ver=%d %s", wtx.fTimeReceivedIsTxTime, hash.ToString());
|
|
wtx.fTimeReceivedIsTxTime = 0;
|
|
}
|
|
wss.vWalletUpgrade.push_back(hash);
|
|
}
|
|
|
|
if (wtx.nOrderPos == -1)
|
|
wss.fAnyUnordered = true;
|
|
|
|
pwallet->AddToWallet(wtx, true, NULL);
|
|
}
|
|
else if (strType == "acentry")
|
|
{
|
|
string strAccount;
|
|
ssKey >> strAccount;
|
|
uint64_t nNumber;
|
|
ssKey >> nNumber;
|
|
if (nNumber > nAccountingEntryNumber)
|
|
nAccountingEntryNumber = nNumber;
|
|
|
|
if (!wss.fAnyUnordered)
|
|
{
|
|
CAccountingEntry acentry;
|
|
ssValue >> acentry;
|
|
if (acentry.nOrderPos == -1)
|
|
wss.fAnyUnordered = true;
|
|
}
|
|
}
|
|
else if (strType == "watchs")
|
|
{
|
|
CScript script;
|
|
ssKey >> *(CScriptBase*)(&script);
|
|
char fYes;
|
|
ssValue >> fYes;
|
|
if (fYes == '1')
|
|
pwallet->LoadWatchOnly(script);
|
|
|
|
// Watch-only addresses have no birthday information for now,
|
|
// so set the wallet birthday to the beginning of time.
|
|
pwallet->nTimeFirstKey = 1;
|
|
}
|
|
else if (strType == "vkey")
|
|
{
|
|
/*
|
|
libzcash::SproutViewingKey vk;
|
|
ssKey >> vk;
|
|
char fYes;
|
|
ssValue >> fYes;
|
|
if (fYes == '1')
|
|
pwallet->LoadSproutViewingKey(vk);
|
|
|
|
Viewing keys have no birthday information for now,
|
|
so set the wallet birthday to the beginning of time.
|
|
pwallet->nTimeFirstKey = 1;
|
|
*/
|
|
}
|
|
else if (strType == "zkey")
|
|
{
|
|
/*
|
|
libzcash::SproutPaymentAddress addr;
|
|
ssKey >> addr;
|
|
libzcash::SproutSpendingKey key;
|
|
ssValue >> key;
|
|
|
|
//if (!pwallet->LoadZKey(key))
|
|
//{
|
|
// strErr = "Error reading wallet database: LoadZKey failed";
|
|
// return false;
|
|
//}
|
|
|
|
wss.nZKeys++;
|
|
*/
|
|
}
|
|
else if (strType == "sapzkey")
|
|
{
|
|
libzcash::SaplingIncomingViewingKey ivk;
|
|
ssKey >> ivk;
|
|
libzcash::SaplingExtendedSpendingKey key;
|
|
ssValue >> key;
|
|
|
|
if (!pwallet->LoadSaplingZKey(key))
|
|
{
|
|
strErr = "Error reading wallet database: LoadSaplingZKey failed";
|
|
return false;
|
|
}
|
|
|
|
//add checks for integrity
|
|
wss.nZKeys++;
|
|
}
|
|
|
|
else if (strType == "key" || strType == "wkey")
|
|
{
|
|
CPubKey vchPubKey;
|
|
ssKey >> vchPubKey;
|
|
if (!vchPubKey.IsValid())
|
|
{
|
|
strErr = "Error reading wallet database: CPubKey corrupt";
|
|
return false;
|
|
}
|
|
CKey key;
|
|
CPrivKey pkey;
|
|
uint256 hash;
|
|
|
|
if (strType == "key")
|
|
{
|
|
wss.nKeys++;
|
|
ssValue >> pkey;
|
|
} else {
|
|
CWalletKey wkey;
|
|
ssValue >> wkey;
|
|
pkey = wkey.vchPrivKey;
|
|
}
|
|
|
|
// Old wallets store keys as "key" [pubkey] => [privkey]
|
|
// ... which was slow for wallets with lots of keys, because the public key is re-derived from the private key
|
|
// using EC operations as a checksum.
|
|
// Newer wallets store keys as "key"[pubkey] => [privkey][hash(pubkey,privkey)], which is much faster while
|
|
// remaining backwards-compatible.
|
|
try
|
|
{
|
|
ssValue >> hash;
|
|
}
|
|
catch (...) {}
|
|
|
|
bool fSkipCheck = false;
|
|
|
|
if (!hash.IsNull())
|
|
{
|
|
// hash pubkey/privkey to accelerate wallet load
|
|
std::vector<unsigned char> vchKey;
|
|
vchKey.reserve(vchPubKey.size() + pkey.size());
|
|
vchKey.insert(vchKey.end(), vchPubKey.begin(), vchPubKey.end());
|
|
vchKey.insert(vchKey.end(), pkey.begin(), pkey.end());
|
|
|
|
if (Hash(vchKey.begin(), vchKey.end()) != hash)
|
|
{
|
|
strErr = "Error reading wallet database: CPubKey/CPrivKey corrupt";
|
|
return false;
|
|
}
|
|
|
|
fSkipCheck = true;
|
|
}
|
|
|
|
if (!key.Load(pkey, vchPubKey, fSkipCheck))
|
|
{
|
|
strErr = "Error reading wallet database: CPrivKey corrupt";
|
|
return false;
|
|
}
|
|
if (!pwallet->LoadKey(key, vchPubKey))
|
|
{
|
|
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<unsigned char> vchPubKey;
|
|
ssKey >> vchPubKey;
|
|
vector<unsigned char> vchPrivKey;
|
|
ssValue >> vchPrivKey;
|
|
wss.nCKeys++;
|
|
|
|
if (!pwallet->LoadCryptedKey(vchPubKey, vchPrivKey))
|
|
{
|
|
strErr = "Error reading wallet database: LoadCryptedKey failed";
|
|
return false;
|
|
}
|
|
wss.fIsEncrypted = true;
|
|
}
|
|
else if (strType == "czkey")
|
|
{
|
|
/*
|
|
libzcash::SproutPaymentAddress addr;
|
|
ssKey >> addr;
|
|
// Deserialization of a pair is just one item after another
|
|
uint256 rkValue;
|
|
ssValue >> rkValue;
|
|
libzcash::ReceivingKey rk(rkValue);
|
|
vector<unsigned char> vchCryptedSecret;
|
|
ssValue >> vchCryptedSecret;
|
|
wss.nCKeys++;
|
|
|
|
if (!pwallet->LoadCryptedZKey(addr, rk, vchCryptedSecret))
|
|
{
|
|
strErr = "Error reading wallet database: LoadCryptedZKey failed";
|
|
return false;
|
|
}
|
|
wss.fIsEncrypted = true;
|
|
*/
|
|
}
|
|
else if (strType == "csapzkey")
|
|
{
|
|
libzcash::SaplingIncomingViewingKey ivk;
|
|
ssKey >> ivk;
|
|
libzcash::SaplingExtendedFullViewingKey extfvk;
|
|
ssValue >> extfvk;
|
|
vector<unsigned char> vchCryptedSecret;
|
|
ssValue >> vchCryptedSecret;
|
|
wss.nCKeys++;
|
|
|
|
if (!pwallet->LoadCryptedSaplingZKey(extfvk, vchCryptedSecret))
|
|
{
|
|
strErr = "Error reading wallet database: LoadCryptedSaplingZKey failed";
|
|
return false;
|
|
}
|
|
wss.fIsEncrypted = true;
|
|
}
|
|
else if (strType == "keymeta")
|
|
{
|
|
CPubKey vchPubKey;
|
|
ssKey >> vchPubKey;
|
|
CKeyMetadata keyMeta;
|
|
ssValue >> keyMeta;
|
|
wss.nKeyMeta++;
|
|
|
|
pwallet->LoadKeyMetadata(vchPubKey, keyMeta);
|
|
|
|
// find earliest key creation time, as wallet birthday
|
|
if (!pwallet->nTimeFirstKey ||
|
|
(keyMeta.nCreateTime < pwallet->nTimeFirstKey))
|
|
pwallet->nTimeFirstKey = keyMeta.nCreateTime;
|
|
}
|
|
else if (strType == "zkeymeta")
|
|
{
|
|
/*
|
|
libzcash::SproutPaymentAddress addr;
|
|
ssKey >> addr;
|
|
CKeyMetadata keyMeta;
|
|
ssValue >> keyMeta;
|
|
wss.nZKeyMeta++;
|
|
*/
|
|
|
|
// pwallet->LoadZKeyMetadata(addr, keyMeta);
|
|
|
|
// ignore earliest key creation time as taddr will exist before any zaddr
|
|
}
|
|
else if (strType == "sapzkeymeta")
|
|
{
|
|
libzcash::SaplingIncomingViewingKey ivk;
|
|
ssKey >> ivk;
|
|
CKeyMetadata keyMeta;
|
|
ssValue >> keyMeta;
|
|
|
|
wss.nZKeyMeta++;
|
|
|
|
pwallet->LoadSaplingZKeyMetadata(ivk, keyMeta);
|
|
}
|
|
else if (strType == "sapzaddr")
|
|
{
|
|
libzcash::SaplingPaymentAddress addr;
|
|
ssKey >> addr;
|
|
libzcash::SaplingIncomingViewingKey ivk;
|
|
ssValue >> ivk;
|
|
|
|
wss.nSapZAddrs++;
|
|
|
|
if (!pwallet->LoadSaplingPaymentAddress(addr, ivk))
|
|
{
|
|
strErr = "Error reading wallet database: LoadSaplingPaymentAddress failed";
|
|
return false;
|
|
}
|
|
}
|
|
else if (strType == "defaultkey")
|
|
{
|
|
ssValue >> pwallet->vchDefaultKey;
|
|
}
|
|
else if (strType == "pool")
|
|
{
|
|
int64_t nIndex;
|
|
ssKey >> nIndex;
|
|
CKeyPool keypool;
|
|
ssValue >> keypool;
|
|
pwallet->setKeyPool.insert(nIndex);
|
|
|
|
// If no metadata exists yet, create a default with the pool key's
|
|
// creation time. Note that this may be overwritten by actually
|
|
// stored metadata for that key later, which is fine.
|
|
CKeyID keyid = keypool.vchPubKey.GetID();
|
|
if (pwallet->mapKeyMetadata.count(keyid) == 0)
|
|
pwallet->mapKeyMetadata[keyid] = CKeyMetadata(keypool.nTime);
|
|
}
|
|
else if (strType == "version")
|
|
{
|
|
ssValue >> wss.nFileVersion;
|
|
if (wss.nFileVersion == 10300)
|
|
wss.nFileVersion = 300;
|
|
}
|
|
else if (strType == "cscript")
|
|
{
|
|
uint160 hash;
|
|
ssKey >> hash;
|
|
CScript script;
|
|
ssValue >> *(CScriptBase*)(&script);
|
|
if (!pwallet->LoadCScript(script))
|
|
{
|
|
strErr = "Error reading wallet database: LoadCScript failed";
|
|
return false;
|
|
}
|
|
}
|
|
else if (strType == "orderposnext")
|
|
{
|
|
ssValue >> pwallet->nOrderPosNext;
|
|
}
|
|
else if (strType == "destdata")
|
|
{
|
|
std::string strAddress, strKey, strValue;
|
|
ssKey >> strAddress;
|
|
ssKey >> strKey;
|
|
ssValue >> strValue;
|
|
if (!pwallet->LoadDestData(DecodeDestination(strAddress), strKey, strValue))
|
|
{
|
|
strErr = "Error reading wallet database: LoadDestData failed";
|
|
return false;
|
|
}
|
|
}
|
|
else if (strType == "witnesscachesize")
|
|
{
|
|
ssValue >> pwallet->nWitnessCacheSize;
|
|
}
|
|
else if (strType == "hdseed")
|
|
{
|
|
uint256 seedFp;
|
|
RawHDSeed rawSeed;
|
|
ssKey >> seedFp;
|
|
ssValue >> rawSeed;
|
|
HDSeed seed(rawSeed);
|
|
|
|
if (seed.Fingerprint() != seedFp)
|
|
{
|
|
strErr = "Error reading wallet database: HDSeed corrupt";
|
|
return false;
|
|
}
|
|
|
|
if (!pwallet->LoadHDSeed(seed))
|
|
{
|
|
strErr = "Error reading wallet database: LoadHDSeed failed";
|
|
return false;
|
|
}
|
|
wss.fHDSeedSeen = true;
|
|
wss.hdSeedFpSeen = seedFp;
|
|
}
|
|
else if (strType == "chdseed")
|
|
{
|
|
uint256 seedFp;
|
|
vector<unsigned char> vchCryptedSecret;
|
|
ssKey >> seedFp;
|
|
ssValue >> vchCryptedSecret;
|
|
if (!pwallet->LoadCryptedHDSeed(seedFp, vchCryptedSecret))
|
|
{
|
|
strErr = "Error reading wallet database: LoadCryptedHDSeed failed";
|
|
return false;
|
|
}
|
|
wss.fIsEncrypted = true;
|
|
// The fingerprint is the plaintext KEY of the chdseed record, so this works while
|
|
// the wallet is locked and the seed itself is unreadable.
|
|
wss.fHDSeedSeen = true;
|
|
wss.hdSeedFpSeen = seedFp;
|
|
}
|
|
else if (strType == "hdchain")
|
|
{
|
|
// Record the ENCOUNTER before any parsing can fail.
|
|
wss.fHDChainSeen = true;
|
|
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 (...) {
|
|
// 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);
|
|
}
|
|
else if (strType == "hdseedorigin")
|
|
{
|
|
int64_t nOrigin = 0;
|
|
ssValue >> nOrigin;
|
|
// Clamp an out-of-range value (corrupt or hand-edited wallet.dat) to
|
|
// UNKNOWN — the conservative origin that leaves autoshield OFF — rather
|
|
// than trusting it to claim a known-recoverable (CREATED/RESTORED) seed.
|
|
if (nOrigin < CWallet::HDSEED_ORIGIN_UNRECORDED || nOrigin > CWallet::HDSEED_ORIGIN_UNKNOWN) {
|
|
nOrigin = CWallet::HDSEED_ORIGIN_UNKNOWN;
|
|
}
|
|
pwallet->hdSeedOrigin = (int)nOrigin;
|
|
}
|
|
else if (strType == "mnementropy")
|
|
{
|
|
uint256 entropyFp;
|
|
RawHDSeed entropy;
|
|
ssKey >> entropyFp;
|
|
ssValue >> entropy;
|
|
|
|
if (MnemonicEntropyFingerprint(entropy) != entropyFp)
|
|
{
|
|
strErr = "Error reading wallet database: mnemonic entropy corrupt";
|
|
return false;
|
|
}
|
|
|
|
if (!pwallet->LoadMnemonicEntropy(entropy))
|
|
{
|
|
strErr = "Error reading wallet database: LoadMnemonicEntropy failed";
|
|
return false;
|
|
}
|
|
}
|
|
else if (strType == "cmnementropy")
|
|
{
|
|
uint256 entropyFp;
|
|
vector<unsigned char> vchCryptedSecret;
|
|
ssKey >> entropyFp;
|
|
ssValue >> vchCryptedSecret;
|
|
if (!pwallet->LoadCryptedMnemonicEntropy(entropyFp, vchCryptedSecret))
|
|
{
|
|
strErr = "Error reading wallet database: LoadCryptedMnemonicEntropy failed";
|
|
return false;
|
|
}
|
|
wss.fIsEncrypted = true;
|
|
}
|
|
} catch (...)
|
|
{
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool IsKeyType(string strType)
|
|
{
|
|
return (strType == "key" || strType == "wkey" ||
|
|
strType == "hdseed" || strType == "chdseed" ||
|
|
// The mnemonic entropy must survive a keys-only salvage: without it
|
|
// a recovered wallet keeps its seed (and stays fully spendable) but
|
|
// silently loses the ability to reprint its seed phrase.
|
|
strType == "mnementropy" || strType == "cmnementropy" ||
|
|
strType == "zkey" || strType == "czkey" ||
|
|
strType == "sapzkey" || strType == "csapzkey" ||
|
|
strType == "vkey" ||
|
|
strType == "mkey" || strType == "ckey");
|
|
}
|
|
|
|
DBErrors CWalletDB::LoadWallet(CWallet* pwallet)
|
|
{
|
|
pwallet->vchDefaultKey = CPubKey();
|
|
CWalletScanState wss;
|
|
bool fNoncriticalErrors = false;
|
|
DBErrors result = DB_LOAD_OK;
|
|
|
|
try {
|
|
LOCK(pwallet->cs_wallet);
|
|
int nMinVersion = 0;
|
|
if (Read((string)"minversion", nMinVersion))
|
|
{
|
|
if (nMinVersion > CLIENT_VERSION)
|
|
return DB_TOO_NEW;
|
|
pwallet->LoadMinVersion(nMinVersion);
|
|
}
|
|
|
|
// Get cursor
|
|
Dbc* pcursor = GetCursor();
|
|
if (!pcursor)
|
|
{
|
|
LogPrintf("Error getting wallet database cursor\n");
|
|
return DB_CORRUPT;
|
|
}
|
|
|
|
while (true)
|
|
{
|
|
// Read next record
|
|
CDataStream ssKey(SER_DISK, CLIENT_VERSION);
|
|
CDataStream ssValue(SER_DISK, CLIENT_VERSION);
|
|
int ret = ReadAtCursor(pcursor, ssKey, ssValue);
|
|
if (ret == DB_NOTFOUND)
|
|
break;
|
|
else if (ret != 0)
|
|
{
|
|
LogPrintf("Error reading next record from wallet database\n");
|
|
return DB_CORRUPT;
|
|
}
|
|
|
|
// Try to be tolerant of single corrupt records:
|
|
string strType, strErr;
|
|
if (!ReadKeyValue(pwallet, ssKey, ssValue, wss, strType, strErr))
|
|
{
|
|
// losing keys is considered a catastrophic error, anything else
|
|
// we assume the user can live with:
|
|
if (IsKeyType(strType))
|
|
result = DB_CORRUPT;
|
|
else
|
|
{
|
|
// 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")
|
|
SoftSetBoolArg("-rescan", true);
|
|
}
|
|
}
|
|
if (!strErr.empty())
|
|
LogPrintf("%s\n", strErr);
|
|
}
|
|
pcursor->close();
|
|
}
|
|
catch (const boost::thread_interrupted&) {
|
|
throw;
|
|
}
|
|
catch (...) {
|
|
result = DB_CORRUPT;
|
|
}
|
|
|
|
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 no usable hdchain record cannot safely DERIVE: hdChain
|
|
// falls back to its SetNull defaults, clearing fMnemonicSeed and so switching derivation from
|
|
// the 64-byte BIP39 seed to the raw 32-byte entropy -- an entirely different key tree.
|
|
//
|
|
// saplingAccountCounter was previously listed here as a second hazard. It is not one:
|
|
// GenerateNewSaplingZKey loops `do {...} while (HaveSaplingSpendingKey(...))` and
|
|
// DeriveNewChildKey loops `while (HaveKey(...))`, so a counter that starts low walks forward
|
|
// past accounts that already exist rather than colliding with them.
|
|
//
|
|
// Two shapes reach here and only one is real corruption:
|
|
// (a) the record is ABSENT -- the signature of a -salvagewallet run by v1.0.3 or earlier,
|
|
// whose IsKeyType has no "hdchain" case, so salvage dropped it. The keys are intact.
|
|
// Refusing strands the wallet with no way back: the node aborts before the RPC server
|
|
// exists, and -mnemonic refuses a non-empty wallet, so there is no user-executable
|
|
// recovery path at all.
|
|
// (b) the record is PRESENT but unreadable -- wider file damage. Keep refusing.
|
|
if (pwallet->HaveHDSeed() && !wss.fHDChainRead && wss.fHDChainSeen)
|
|
{
|
|
LogPrintf("Error loading wallet.dat: the hdchain record is present but unreadable. Your keys "
|
|
"are intact -- do NOT delete or replace wallet.dat. Back it up and see debug.log.\n");
|
|
return DB_CORRUPT;
|
|
}
|
|
|
|
{
|
|
const char* pszDegraded = NULL;
|
|
if (pwallet->HaveHDSeed() && !wss.fHDChainRead)
|
|
{
|
|
pszDegraded = "the hdchain record is missing (an older -salvagewallet drops it)";
|
|
}
|
|
// A chain WAS read, but it does not belong to the seed we loaded. No legitimate writer can
|
|
// produce that -- InstallHDSeed always stores seedFp = seed.Fingerprint(). It is the mark
|
|
// of a wallet that lost its hdchain to an old salvage and was then USED on that old build,
|
|
// which persists a SetNull-derived chain carrying a null seedFp. Such a wallet otherwise
|
|
// starts up perfectly clean and derives into the WRONG TREE forever, silently -- strictly
|
|
// worse than failing to open, which is why it is worth detecting here.
|
|
else if (pwallet->HaveHDSeed() && wss.fHDSeedSeen &&
|
|
pwallet->GetHDChain().seedFp != wss.hdSeedFpSeen)
|
|
{
|
|
pszDegraded = "the hdchain record does not belong to this wallet's HD seed";
|
|
}
|
|
|
|
if (pszDegraded != NULL)
|
|
{
|
|
pwallet->SetHDChainUnproven();
|
|
LogPrintf("Wallet opened in DEGRADED mode: %s. Existing keys are intact, spendable and "
|
|
"receivable, but no NEW HD-derived key can be generated and new transparent "
|
|
"addresses will not be recoverable from a seed phrase. Back up wallet.dat now "
|
|
"and do NOT delete or replace it.\n", pszDegraded);
|
|
// An old salvage also dropped defaultkey and bestblock, which would make this look like
|
|
// a first run and skip the rescan, leaving a permanently zero balance.
|
|
SoftSetBoolArg("-rescan", true);
|
|
}
|
|
}
|
|
|
|
// 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;
|
|
|
|
LogPrintf("nFileVersion = %d\n", wss.nFileVersion);
|
|
|
|
LogPrintf("Keys: %u plaintext, %u encrypted, %u w/ metadata, %u total\n",
|
|
wss.nKeys, wss.nCKeys, wss.nKeyMeta, wss.nKeys + wss.nCKeys);
|
|
|
|
LogPrintf("ZKeys: %u plaintext, %u encrypted, %u w/metadata, %u total\n",
|
|
wss.nZKeys, wss.nCZKeys, wss.nZKeyMeta, wss.nZKeys + wss.nCZKeys);
|
|
|
|
// nTimeFirstKey is only reliable if all keys have metadata
|
|
if ((wss.nKeys + wss.nCKeys) != wss.nKeyMeta)
|
|
pwallet->nTimeFirstKey = 1; // 0 would be considered 'no value'
|
|
|
|
BOOST_FOREACH(uint256 hash, wss.vWalletUpgrade)
|
|
WriteTx(hash, pwallet->mapWallet[hash]);
|
|
|
|
// Rewrite encrypted wallets of versions 0.4.0 and 0.5.0rc:
|
|
if (wss.fIsEncrypted && (wss.nFileVersion == 40000 || wss.nFileVersion == 50000))
|
|
return DB_NEED_REWRITE;
|
|
|
|
if (wss.nFileVersion < CLIENT_VERSION) // Update
|
|
WriteVersion(CLIENT_VERSION);
|
|
|
|
if (wss.fAnyUnordered)
|
|
result = ReorderTransactions(pwallet);
|
|
|
|
return result;
|
|
}
|
|
|
|
DBErrors CWalletDB::FindWalletTx(CWallet* pwallet, vector<uint256>& vTxHash, vector<CWalletTx>& vWtx)
|
|
{
|
|
pwallet->vchDefaultKey = CPubKey();
|
|
bool fNoncriticalErrors = false;
|
|
DBErrors result = DB_LOAD_OK;
|
|
|
|
try {
|
|
LOCK(pwallet->cs_wallet);
|
|
int nMinVersion = 0;
|
|
if (Read((string)"minversion", nMinVersion))
|
|
{
|
|
if (nMinVersion > CLIENT_VERSION)
|
|
return DB_TOO_NEW;
|
|
pwallet->LoadMinVersion(nMinVersion);
|
|
}
|
|
|
|
// Get cursor
|
|
Dbc* pcursor = GetCursor();
|
|
if (!pcursor)
|
|
{
|
|
LogPrintf("Error getting wallet database cursor\n");
|
|
return DB_CORRUPT;
|
|
}
|
|
|
|
while (true)
|
|
{
|
|
// Read next record
|
|
CDataStream ssKey(SER_DISK, CLIENT_VERSION);
|
|
CDataStream ssValue(SER_DISK, CLIENT_VERSION);
|
|
int ret = ReadAtCursor(pcursor, ssKey, ssValue);
|
|
if (ret == DB_NOTFOUND)
|
|
break;
|
|
else if (ret != 0)
|
|
{
|
|
LogPrintf("Error reading next record from wallet database\n");
|
|
return DB_CORRUPT;
|
|
}
|
|
|
|
string strType;
|
|
ssKey >> strType;
|
|
if (strType == "tx") {
|
|
uint256 hash;
|
|
ssKey >> hash;
|
|
|
|
std::vector<unsigned char> txData(ssValue.begin(), ssValue.end());
|
|
try {
|
|
CWalletTx wtx;
|
|
ssValue >> wtx;
|
|
vWtx.push_back(wtx);
|
|
} catch (...) {
|
|
// Decode failure likely due to Sapling v4 transaction format change
|
|
// between 2.0.0 and 2.0.1. As user is requesting deletion, log the
|
|
// transaction entry and then mark it for deletion anyway.
|
|
LogPrintf("Failed to decode wallet transaction; logging it here before deletion:\n");
|
|
LogPrintf("txid: %s\n%s\n", hash.GetHex(), HexStr(txData));
|
|
}
|
|
|
|
vTxHash.push_back(hash);
|
|
}
|
|
}
|
|
pcursor->close();
|
|
}
|
|
catch (const boost::thread_interrupted&) {
|
|
throw;
|
|
}
|
|
catch (...) {
|
|
result = DB_CORRUPT;
|
|
}
|
|
|
|
if (fNoncriticalErrors && result == DB_LOAD_OK)
|
|
result = DB_NONCRITICAL_ERROR;
|
|
|
|
return result;
|
|
}
|
|
|
|
DBErrors CWalletDB::ZapWalletTx(CWallet* pwallet, vector<CWalletTx>& vWtx)
|
|
{
|
|
// build list of wallet TXs
|
|
vector<uint256> vTxHash;
|
|
DBErrors err = FindWalletTx(pwallet, vTxHash, vWtx);
|
|
if (err != DB_LOAD_OK)
|
|
return err;
|
|
|
|
// erase each wallet TX
|
|
BOOST_FOREACH (uint256& hash, vTxHash) {
|
|
if (!EraseTx(hash))
|
|
return DB_CORRUPT;
|
|
}
|
|
|
|
return DB_LOAD_OK;
|
|
}
|
|
|
|
void ThreadFlushWalletDB(const string& strFile)
|
|
{
|
|
// Make this thread recognisable as the wallet flushing thread
|
|
RenameThread("hush-wallet");
|
|
|
|
static bool fOneThread;
|
|
if (fOneThread)
|
|
return;
|
|
fOneThread = true;
|
|
if (!GetBoolArg("-flushwallet", true))
|
|
return;
|
|
|
|
unsigned int nLastSeen = nWalletDBUpdated;
|
|
unsigned int nLastFlushed = nWalletDBUpdated;
|
|
int64_t nLastWalletUpdate = GetTime();
|
|
while (true)
|
|
{
|
|
MilliSleep(500);
|
|
|
|
if (nLastSeen != nWalletDBUpdated)
|
|
{
|
|
nLastSeen = nWalletDBUpdated;
|
|
nLastWalletUpdate = GetTime();
|
|
}
|
|
|
|
if (nLastFlushed != nWalletDBUpdated && GetTime() - nLastWalletUpdate >= 2)
|
|
{
|
|
TRY_LOCK(bitdb.cs_db,lockDb);
|
|
if (lockDb)
|
|
{
|
|
// Don't do this if any databases are in use
|
|
int nRefCount = 0;
|
|
map<string, int>::iterator mi = bitdb.mapFileUseCount.begin();
|
|
while (mi != bitdb.mapFileUseCount.end())
|
|
{
|
|
nRefCount += (*mi).second;
|
|
mi++;
|
|
}
|
|
|
|
if (nRefCount == 0)
|
|
{
|
|
boost::this_thread::interruption_point();
|
|
map<string, int>::iterator mi = bitdb.mapFileUseCount.find(strFile);
|
|
if (mi != bitdb.mapFileUseCount.end())
|
|
{
|
|
LogPrint("db", "Flushing wallet.dat\n");
|
|
nLastFlushed = nWalletDBUpdated;
|
|
int64_t nStart = GetTimeMillis();
|
|
|
|
// Flush wallet.dat so it's self contained
|
|
bitdb.CloseDb(strFile);
|
|
bitdb.CheckpointLSN(strFile);
|
|
|
|
bitdb.mapFileUseCount.erase(mi++);
|
|
LogPrint("db", "Flushed wallet.dat %dms\n", GetTimeMillis() - nStart);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
bool BackupWallet(const CWallet& wallet, const string& strDest)
|
|
{
|
|
if (!wallet.fFileBacked)
|
|
return false;
|
|
while (true)
|
|
{
|
|
{
|
|
LOCK(bitdb.cs_db);
|
|
if (!bitdb.mapFileUseCount.count(wallet.strWalletFile) || bitdb.mapFileUseCount[wallet.strWalletFile] == 0)
|
|
{
|
|
// Flush log data to the dat file
|
|
bitdb.CloseDb(wallet.strWalletFile);
|
|
bitdb.CheckpointLSN(wallet.strWalletFile);
|
|
bitdb.mapFileUseCount.erase(wallet.strWalletFile);
|
|
|
|
// Copy wallet.dat
|
|
boost::filesystem::path pathSrc = GetDataDir() / wallet.strWalletFile;
|
|
boost::filesystem::path pathDest(strDest);
|
|
if (boost::filesystem::is_directory(pathDest))
|
|
pathDest /= wallet.strWalletFile;
|
|
|
|
try {
|
|
boost::filesystem::copy_file(pathSrc, pathDest, boost::filesystem::copy_option::overwrite_if_exists);
|
|
LogPrintf("copied wallet.dat to %s\n", pathDest.string());
|
|
return true;
|
|
} catch (const boost::filesystem::filesystem_error& e) {
|
|
LogPrintf("error copying wallet.dat to %s - %s\n", pathDest.string(), e.what());
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
MilliSleep(100);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool CWalletDB::Compact(CDBEnv& dbenv, const std::string& strFile)
|
|
{
|
|
bool fSuccess = dbenv.Compact(strFile);
|
|
return fSuccess;
|
|
}
|
|
|
|
//
|
|
// Try to (very carefully!) recover wallet.dat if there is a problem.
|
|
//
|
|
bool CWalletDB::Recover(CDBEnv& dbenv, const 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_t now = GetTime();
|
|
std::string newFilename = strprintf("wallet.%d.bak", now);
|
|
|
|
int result = dbenv.dbenv->dbrename(NULL, filename.c_str(), NULL,
|
|
newFilename.c_str(), DB_AUTO_COMMIT);
|
|
if (result == 0)
|
|
LogPrintf("Renamed %s to %s\n", filename, newFilename);
|
|
else
|
|
{
|
|
LogPrintf("Failed to rename %s to %s\n", filename, newFilename);
|
|
return false;
|
|
}
|
|
|
|
std::vector<CDBEnv::KeyValPair> salvagedData;
|
|
bool fSuccess = dbenv.Salvage(newFilename, true, salvagedData);
|
|
if (salvagedData.empty())
|
|
{
|
|
LogPrintf("Salvage(aggressive) found no records in %s.\n", newFilename);
|
|
return false;
|
|
}
|
|
LogPrintf("Salvage(aggressive) found %u records\n", salvagedData.size());
|
|
|
|
boost::scoped_ptr<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)
|
|
{
|
|
LogPrintf("Cannot create database file %s\n", filename);
|
|
return false;
|
|
}
|
|
CWallet dummyWallet;
|
|
CWalletScanState wss;
|
|
|
|
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;
|
|
{
|
|
// Required in LoadKeyMetadata():
|
|
LOCK(dummyWallet.cs_wallet);
|
|
fReadOK = ReadKeyValue(&dummyWallet, ssKey, ssValue,
|
|
wss, strType, strErr);
|
|
}
|
|
// "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)
|
|
{
|
|
LogPrintf("WARNING: CWalletDB::Recover skipping %s: %s\n", strType, strErr);
|
|
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);
|
|
|
|
return fSuccess;
|
|
}
|
|
|
|
bool CWalletDB::Recover(CDBEnv& dbenv, const std::string& filename)
|
|
{
|
|
return CWalletDB::Recover(dbenv, filename, false);
|
|
}
|
|
|
|
bool CWalletDB::WriteDestData(const std::string &address, const std::string &key, const std::string &value)
|
|
{
|
|
nWalletDBUpdated++;
|
|
return Write(std::make_pair(std::string("destdata"), std::make_pair(address, key)), value);
|
|
}
|
|
|
|
bool CWalletDB::EraseDestData(const std::string &address, const std::string &key)
|
|
{
|
|
nWalletDBUpdated++;
|
|
return Erase(std::make_pair(std::string("destdata"), std::make_pair(address, key)));
|
|
}
|
|
|
|
|
|
bool CWalletDB::WriteHDSeed(const HDSeed& seed)
|
|
{
|
|
nWalletDBUpdated++;
|
|
return Write(std::make_pair(std::string("hdseed"), seed.Fingerprint()), seed.RawSeed());
|
|
}
|
|
|
|
bool CWalletDB::WriteCryptedHDSeed(const uint256& seedFp, const std::vector<unsigned char>& vchCryptedSecret)
|
|
{
|
|
nWalletDBUpdated++;
|
|
return Write(std::make_pair(std::string("chdseed"), seedFp), vchCryptedSecret);
|
|
}
|
|
|
|
bool CWalletDB::EraseHDSeed(const uint256& seedFp)
|
|
{
|
|
nWalletDBUpdated++;
|
|
// CDB::Erase honours activeTxn, so when this runs inside EncryptWallet's
|
|
// transaction the erase commits or aborts atomically with the chdseed write.
|
|
// It also returns true for DB_NOTFOUND, so erasing a record that was never
|
|
// written (e.g. a wallet encrypted at creation time) is not a failure.
|
|
return Erase(std::make_pair(std::string("hdseed"), seedFp));
|
|
}
|
|
|
|
bool CWalletDB::WriteMnemonicEntropy(const uint256& entropyFp, const RawHDSeed& entropy)
|
|
{
|
|
nWalletDBUpdated++;
|
|
return Write(std::make_pair(std::string("mnementropy"), entropyFp), entropy);
|
|
}
|
|
|
|
bool CWalletDB::WriteCryptedMnemonicEntropy(const uint256& entropyFp, const std::vector<unsigned char>& vchCryptedSecret)
|
|
{
|
|
nWalletDBUpdated++;
|
|
return Write(std::make_pair(std::string("cmnementropy"), entropyFp), vchCryptedSecret);
|
|
}
|
|
|
|
bool CWalletDB::EraseMnemonicEntropy(const uint256& entropyFp)
|
|
{
|
|
nWalletDBUpdated++;
|
|
return Erase(std::make_pair(std::string("mnementropy"), entropyFp));
|
|
}
|
|
|
|
bool CWalletDB::WriteHDChain(const CHDChain& chain)
|
|
{
|
|
nWalletDBUpdated++;
|
|
return Write(std::string("hdchain"), chain);
|
|
}
|