diff --git a/src/validation.cpp b/src/validation.cpp --- a/src/validation.cpp +++ b/src/validation.cpp @@ -2224,120 +2224,130 @@ CValidationState &state, FlushStateMode mode, int nManualPruneHeight) { int64_t nMempoolUsage = mempool.DynamicMemoryUsage(); - LOCK2(cs_main, cs_LastBlockFile); + LOCK(cs_main); static int64_t nLastWrite = 0; static int64_t nLastFlush = 0; static int64_t nLastSetChain = 0; std::set setFilesToPrune; bool fFlushForPrune = false; + bool fDoFullFlush = false; + int64_t nNow = 0; try { - if (fPruneMode && (fCheckForPruning || nManualPruneHeight > 0) && - !fReindex) { - if (nManualPruneHeight > 0) { - FindFilesToPruneManual(setFilesToPrune, nManualPruneHeight); - } else { - FindFilesToPrune(setFilesToPrune, - chainparams.PruneAfterHeight()); - fCheckForPruning = false; - } - if (!setFilesToPrune.empty()) { - fFlushForPrune = true; - if (!fHavePruned) { - pblocktree->WriteFlag("prunedblockfiles", true); - fHavePruned = true; + { + LOCK(cs_LastBlockFile); + if (fPruneMode && (fCheckForPruning || nManualPruneHeight > 0) && + !fReindex) { + if (nManualPruneHeight > 0) { + FindFilesToPruneManual(setFilesToPrune, nManualPruneHeight); + } else { + FindFilesToPrune(setFilesToPrune, + chainparams.PruneAfterHeight()); + fCheckForPruning = false; } - } - } - int64_t nNow = GetTimeMicros(); - // Avoid writing/flushing immediately after startup. - if (nLastWrite == 0) { - nLastWrite = nNow; - } - if (nLastFlush == 0) { - nLastFlush = nNow; - } - if (nLastSetChain == 0) { - nLastSetChain = nNow; - } - int64_t nMempoolSizeMax = - gArgs.GetArg("-maxmempool", DEFAULT_MAX_MEMPOOL_SIZE) * 1000000; - int64_t cacheSize = pcoinsTip->DynamicMemoryUsage(); - int64_t nTotalSpace = - nCoinCacheUsage + - std::max(nMempoolSizeMax - nMempoolUsage, 0); - // The cache is large and we're within 10% and 10 MiB of the limit, but - // we have time now (not in the middle of a block processing). - bool fCacheLarge = - mode == FLUSH_STATE_PERIODIC && - cacheSize > - std::max((9 * nTotalSpace) / 10, - nTotalSpace - MAX_BLOCK_COINSDB_USAGE * 1024 * 1024); - // The cache is over the limit, we have to write now. - bool fCacheCritical = - mode == FLUSH_STATE_IF_NEEDED && cacheSize > nTotalSpace; - // It's been a while since we wrote the block index to disk. Do this - // frequently, so we don't need to redownload after a crash. - bool fPeriodicWrite = - mode == FLUSH_STATE_PERIODIC && - nNow > nLastWrite + (int64_t)DATABASE_WRITE_INTERVAL * 1000000; - // It's been very long since we flushed the cache. Do this infrequently, - // to optimize cache usage. - bool fPeriodicFlush = - mode == FLUSH_STATE_PERIODIC && - nNow > nLastFlush + (int64_t)DATABASE_FLUSH_INTERVAL * 1000000; - // Combine all conditions that result in a full cache flush. - bool fDoFullFlush = (mode == FLUSH_STATE_ALWAYS) || fCacheLarge || - fCacheCritical || fPeriodicFlush || fFlushForPrune; - // Write blocks and block index to disk. - if (fDoFullFlush || fPeriodicWrite) { - // Depend on nMinDiskSpace to ensure we can write block index - if (!CheckDiskSpace(0)) return state.Error("out of disk space"); - // First make sure all block and undo data is flushed to disk. - FlushBlockFile(); - // Then update all block file information (which may refer to block - // and undo files). - { - std::vector> vFiles; - vFiles.reserve(setDirtyFileInfo.size()); - for (std::set::iterator it = setDirtyFileInfo.begin(); - it != setDirtyFileInfo.end();) { - vFiles.push_back(std::make_pair(*it, &vinfoBlockFile[*it])); - setDirtyFileInfo.erase(it++); + if (!setFilesToPrune.empty()) { + fFlushForPrune = true; + if (!fHavePruned) { + pblocktree->WriteFlag("prunedblockfiles", true); + fHavePruned = true; + } } - std::vector vBlocks; - vBlocks.reserve(setDirtyBlockIndex.size()); - for (std::set::iterator it = - setDirtyBlockIndex.begin(); - it != setDirtyBlockIndex.end();) { - vBlocks.push_back(*it); - setDirtyBlockIndex.erase(it++); + } + nNow = GetTimeMicros(); + // Avoid writing/flushing immediately after startup. + if (nLastWrite == 0) { + nLastWrite = nNow; + } + if (nLastFlush == 0) { + nLastFlush = nNow; + } + if (nLastSetChain == 0) { + nLastSetChain = nNow; + } + int64_t nMempoolSizeMax = + gArgs.GetArg("-maxmempool", DEFAULT_MAX_MEMPOOL_SIZE) * 1000000; + int64_t cacheSize = pcoinsTip->DynamicMemoryUsage(); + int64_t nTotalSpace = + nCoinCacheUsage + + std::max(nMempoolSizeMax - nMempoolUsage, 0); + // The cache is large and we're within 10% and 10 MiB of the limit, + // but we have time now (not in the middle of a block processing). + bool fCacheLarge = + mode == FLUSH_STATE_PERIODIC && + cacheSize > std::max((9 * nTotalSpace) / 10, + nTotalSpace - + MAX_BLOCK_COINSDB_USAGE * 1024 * 1024); + // The cache is over the limit, we have to write now. + bool fCacheCritical = + mode == FLUSH_STATE_IF_NEEDED && cacheSize > nTotalSpace; + // It's been a while since we wrote the block index to disk. Do this + // frequently, so we don't need to redownload after a crash. + bool fPeriodicWrite = + mode == FLUSH_STATE_PERIODIC && + nNow > nLastWrite + (int64_t)DATABASE_WRITE_INTERVAL * 1000000; + // It's been very long since we flushed the cache. Do this + // infrequently, to optimize cache usage. + bool fPeriodicFlush = + mode == FLUSH_STATE_PERIODIC && + nNow > nLastFlush + (int64_t)DATABASE_FLUSH_INTERVAL * 1000000; + // Combine all conditions that result in a full cache flush. + fDoFullFlush = (mode == FLUSH_STATE_ALWAYS) || fCacheLarge || + fCacheCritical || fPeriodicFlush || fFlushForPrune; + // Write blocks and block index to disk. + if (fDoFullFlush || fPeriodicWrite) { + // Depend on nMinDiskSpace to ensure we can write block index + if (!CheckDiskSpace(0)) { + return state.Error("out of disk space"); } - if (!pblocktree->WriteBatchSync(vFiles, nLastBlockFile, - vBlocks)) { - return AbortNode(state, - "Failed to write to block index database"); + // First make sure all block and undo data is flushed to disk. + FlushBlockFile(); + // Then update all block file information (which may refer to + // block and undo files). + { + std::vector> vFiles; + vFiles.reserve(setDirtyFileInfo.size()); + for (std::set::iterator it = setDirtyFileInfo.begin(); + it != setDirtyFileInfo.end();) { + vFiles.push_back( + std::make_pair(*it, &vinfoBlockFile[*it])); + setDirtyFileInfo.erase(it++); + } + std::vector vBlocks; + vBlocks.reserve(setDirtyBlockIndex.size()); + for (std::set::iterator it = + setDirtyBlockIndex.begin(); + it != setDirtyBlockIndex.end();) { + vBlocks.push_back(*it); + setDirtyBlockIndex.erase(it++); + } + if (!pblocktree->WriteBatchSync(vFiles, nLastBlockFile, + vBlocks)) { + return AbortNode( + state, "Failed to write to block index database"); + } } + // Finally remove any pruned files + if (fFlushForPrune) UnlinkPrunedFiles(setFilesToPrune); + nLastWrite = nNow; } - // Finally remove any pruned files - if (fFlushForPrune) UnlinkPrunedFiles(setFilesToPrune); - nLastWrite = nNow; - } - // Flush best chain related state. This can only be done if the blocks / - // block index write was also done. - if (fDoFullFlush) { - // Typical Coin structures on disk are around 48 bytes in size. - // Pushing a new one to the database can cause it to be written - // twice (once in the log, and once in the tables). This is already - // an overestimation, as most will delete an existing entry or - // overwrite one. Still, use a conservative safety factor of 2. - if (!CheckDiskSpace(48 * 2 * 2 * pcoinsTip->GetCacheSize())) { - return state.Error("out of disk space"); - } - // Flush the chainstate (which may refer to block index entries). - if (!pcoinsTip->Flush()) { - return AbortNode(state, "Failed to write to coin database"); + // Flush best chain related state. This can only be done if the + // blocks / block index write was also done. + if (fDoFullFlush) { + // Typical Coin structures on disk are around 48 bytes in size. + // Pushing a new one to the database can cause it to be written + // twice (once in the log, and once in the tables). This is + // already an overestimation, as most will delete an existing + // entry or overwrite one. Still, use a conservative safety + // factor of 2. + if (!CheckDiskSpace(48 * 2 * 2 * pcoinsTip->GetCacheSize())) { + return state.Error("out of disk space"); + } + // Flush the chainstate (which may refer to block index + // entries). + if (!pcoinsTip->Flush()) { + return AbortNode(state, "Failed to write to coin database"); + } + nLastFlush = nNow; } - nLastFlush = nNow; } if (fDoFullFlush || ((mode == FLUSH_STATE_ALWAYS || mode == FLUSH_STATE_PERIODIC) && diff --git a/src/wallet/rpcdump.cpp b/src/wallet/rpcdump.cpp --- a/src/wallet/rpcdump.cpp +++ b/src/wallet/rpcdump.cpp @@ -692,9 +692,8 @@ } std::map mapKeyBirth; - std::set setKeyPool; + const std::map &mapKeyPool = pwallet->GetAllReserveKeys(); pwallet->GetKeyBirthTimes(mapKeyBirth); - pwallet->GetAllReserveKeys(setKeyPool); // sort time/key pairs std::vector> vKeyBirth; @@ -748,7 +747,7 @@ EncodeDumpString(pwallet->mapAddressBook[keyid].name)); } else if (keyid == masterKeyID) { file << "hdmaster=1"; - } else if (setKeyPool.count(keyid)) { + } else if (mapKeyPool.count(keyid)) { file << "reserve=1"; } else if (pwallet->mapKeyMetadata[keyid].hdKeypath == "m") { file << "inactivehdmaster=1"; diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -641,6 +641,7 @@ std::set setInternalKeyPool; std::set setExternalKeyPool; int64_t m_max_keypool_index; + std::map m_pool_key_to_index; int64_t nTimeFirstKey; @@ -685,21 +686,7 @@ } } - void LoadKeyPool(int64_t nIndex, const CKeyPool &keypool) { - if (keypool.fInternal) { - setInternalKeyPool.insert(nIndex); - } else { - setExternalKeyPool.insert(nIndex); - } - m_max_keypool_index = std::max(m_max_keypool_index, 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 (mapKeyMetadata.count(keyid) == 0) - mapKeyMetadata[keyid] = CKeyMetadata(keypool.nTime); - } + void LoadKeyPool(int64_t nIndex, const CKeyPool &keypool); // Map from Key ID (for regular keys) or Script ID (for watch-only keys) to // key metadata. @@ -757,7 +744,7 @@ //! check whether we are allowed to upgrade (or already support) to the //! named feature - bool CanSupportFeature(enum WalletFeature wf) { + bool CanSupportFeature(enum WalletFeature wf) const { AssertLockHeld(cs_wallet); return nWalletMaxVersion >= wf; } @@ -960,10 +947,18 @@ void ReserveKeyFromKeyPool(int64_t &nIndex, CKeyPool &keypool, bool fRequestedInternal); void KeepKey(int64_t nIndex); - void ReturnKey(int64_t nIndex, bool fInternal); + void ReturnKey(int64_t nIndex, bool fInternal, const CPubKey &pubkey); bool GetKeyFromPool(CPubKey &key, bool internal = false); int64_t GetOldestKeyPoolTime(); - void GetAllReserveKeys(std::set &setAddress) const; + /** + * Marks all keys in the keypool up to and including reserve_key as used. + */ + void MarkReserveKeysAsUsed(int64_t keypool_id); + const std::map &GetAllReserveKeys() const { + return m_pool_key_to_index; + } + /** Does the wallet have at least min_keys in the keypool? */ + bool HasUnusedKeys(int min_keys) const; std::set> GetAddressGroupings(); std::map GetAddressBalances(); diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -12,6 +12,7 @@ #include "consensus/validation.h" #include "dstencode.h" #include "fs.h" +#include "init.h" #include "key.h" #include "keystore.h" #include "net.h" @@ -83,6 +84,43 @@ nDepth, FormatMoney(tx->tx->vout[i].nValue)); } +class CAffectedKeysVisitor : public boost::static_visitor { +private: + const CKeyStore &keystore; + std::vector &vKeys; + +public: + CAffectedKeysVisitor(const CKeyStore &keystoreIn, + std::vector &vKeysIn) + : keystore(keystoreIn), vKeys(vKeysIn) {} + + void Process(const CScript &script) { + txnouttype type; + std::vector vDest; + int nRequired; + if (ExtractDestinations(script, type, vDest, nRequired)) { + for (const CTxDestination &dest : vDest) { + boost::apply_visitor(*this, dest); + } + } + } + + void operator()(const CKeyID &keyId) { + if (keystore.HaveKey(keyId)) { + vKeys.push_back(keyId); + } + } + + void operator()(const CScriptID &scriptId) { + CScript script; + if (keystore.GetCScript(scriptId, script)) { + Process(script); + } + } + + void operator()(const CNoDestination &none) {} +}; + const CWalletTx *CWallet::GetWalletTx(const uint256 &hash) const { LOCK(cs_wallet); std::map::const_iterator it = mapWallet.find(hash); @@ -1112,6 +1150,38 @@ return false; } if (fExisted || IsMine(tx) || IsFromMe(tx)) { + /** + * Check if any keys in the wallet keypool that were supposed to be + * unused have appeared in a new transaction. If so, remove those keys + * from the keypool. This can happen when restoring an old wallet backup + * that does not contain the mostly recently created transactions from + * newer versions of the wallet. + */ + + // loop though all outputs + for (const CTxOut &txout : tx.vout) { + // extract addresses and check if they match with an unused keypool + // key + std::vector vAffected; + CAffectedKeysVisitor(*this, vAffected).Process(txout.scriptPubKey); + for (const CKeyID &keyid : vAffected) { + std::map::const_iterator mi = + m_pool_key_to_index.find(keyid); + if (mi != m_pool_key_to_index.end()) { + LogPrintf("%s: Detected a used keypool key, mark all " + "keypool key up to this key as used\n", + __func__); + MarkReserveKeysAsUsed(mi->second); + + if (!TopUpKeyPool()) { + LogPrintf( + "%s: Topping up keypool failed (locked wallet)\n", + __func__); + } + } + } + } + CWalletTx wtx(this, ptx); // Get merkle branch if transaction was found in a block. @@ -3105,6 +3175,7 @@ LOCK(cs_wallet); setInternalKeyPool.clear(); setExternalKeyPool.clear(); + m_pool_key_to_index.clear(); // Note: can't top-up keypool here, because wallet is locked. // User will be prompted to unlock wallet the next operation // that requires a new key. @@ -3136,6 +3207,7 @@ if (dbw->Rewrite("\x04pool")) { setInternalKeyPool.clear(); setExternalKeyPool.clear(); + m_pool_key_to_index.clear(); // Note: can't top-up keypool here, because wallet is locked. // User will be prompted to unlock wallet the next operation // that requires a new key. @@ -3159,6 +3231,7 @@ LOCK(cs_wallet); setInternalKeyPool.clear(); setExternalKeyPool.clear(); + m_pool_key_to_index.clear(); // Note: can't top-up keypool here, because wallet is locked. // User will be prompted to unlock wallet the next operation // that requires a new key. @@ -3249,9 +3322,12 @@ } setExternalKeyPool.clear(); + m_pool_key_to_index.clear(); + if (!TopUpKeyPool()) { return false; } + LogPrintf("CWallet::NewKeyPool rewrote keypool\n"); return true; } @@ -3262,6 +3338,25 @@ return setExternalKeyPool.size(); } +void CWallet::LoadKeyPool(int64_t nIndex, const CKeyPool &keypool) { + AssertLockHeld(cs_wallet); + if (keypool.fInternal) { + setInternalKeyPool.insert(nIndex); + } else { + setExternalKeyPool.insert(nIndex); + } + m_max_keypool_index = std::max(m_max_keypool_index, nIndex); + m_pool_key_to_index[keypool.vchPubKey.GetID()] = 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 (mapKeyMetadata.count(keyid) == 0) { + mapKeyMetadata[keyid] = CKeyMetadata(keypool.nTime); + } +} + bool CWallet::TopUpKeyPool(unsigned int kpSize) { LOCK(cs_wallet); @@ -3301,9 +3396,8 @@ assert(m_max_keypool_index < std::numeric_limits::max()); int64_t index = ++m_max_keypool_index; - if (!walletdb.WritePool( - index, - CKeyPool(GenerateNewKey(walletdb, internal), internal))) { + CPubKey pubkey(GenerateNewKey(walletdb, internal)); + if (!walletdb.WritePool(index, CKeyPool(pubkey, internal))) { throw std::runtime_error(std::string(__func__) + ": writing generated key failed"); } @@ -3313,6 +3407,7 @@ } else { setExternalKeyPool.insert(index); } + m_pool_key_to_index[pubkey.GetID()] = index; } if (missingInternal + missingExternal > 0) { LogPrintf( @@ -3365,6 +3460,7 @@ } assert(keypool.vchPubKey.IsValid()); + m_pool_key_to_index.erase(keypool.vchPubKey.GetID()); LogPrintf("keypool reserve %d\n", nIndex); } @@ -3375,7 +3471,7 @@ LogPrintf("keypool keep %d\n", nIndex); } -void CWallet::ReturnKey(int64_t nIndex, bool fInternal) { +void CWallet::ReturnKey(int64_t nIndex, bool fInternal, const CPubKey &pubkey) { // Return to key pool { LOCK(cs_wallet); @@ -3384,6 +3480,7 @@ } else { setExternalKeyPool.insert(nIndex); } + m_pool_key_to_index[pubkey.GetID()] = nIndex; } LogPrintf("keypool return %d\n", nIndex); @@ -3667,44 +3764,44 @@ void CReserveKey::ReturnKey() { if (nIndex != -1) { - pwallet->ReturnKey(nIndex, fInternal); + pwallet->ReturnKey(nIndex, fInternal, vchPubKey); } nIndex = -1; vchPubKey = CPubKey(); } -static void LoadReserveKeysToSet(std::set &setAddress, - const std::set &setKeyPool, - CWalletDB &walletdb) { - for (const int64_t &id : setKeyPool) { - CKeyPool keypool; - if (!walletdb.ReadPool(id, keypool)) { - throw std::runtime_error(std::string(__func__) + ": read failed"); - } - - assert(keypool.vchPubKey.IsValid()); - CKeyID keyID = keypool.vchPubKey.GetID(); - setAddress.insert(keyID); - } -} - -void CWallet::GetAllReserveKeys(std::set &setAddress) const { - setAddress.clear(); +void CWallet::MarkReserveKeysAsUsed(int64_t keypool_id) { + AssertLockHeld(cs_wallet); + bool internal = setInternalKeyPool.count(keypool_id); + if (!internal) assert(setExternalKeyPool.count(keypool_id)); + std::set *setKeyPool = + internal ? &setInternalKeyPool : &setExternalKeyPool; + auto it = setKeyPool->begin(); CWalletDB walletdb(*dbw); + while (it != std::end(*setKeyPool)) { + const int64_t &index = *(it); + if (index > keypool_id) { + // set*KeyPool is ordered + break; + } - LOCK2(cs_main, cs_wallet); - LoadReserveKeysToSet(setAddress, setInternalKeyPool, walletdb); - LoadReserveKeysToSet(setAddress, setExternalKeyPool, walletdb); - - for (const CKeyID &keyID : setAddress) { - if (!HaveKey(keyID)) { - throw std::runtime_error(std::string(__func__) + - ": unknown key in key pool"); + CKeyPool keypool; + if (walletdb.ReadPool(index, keypool)) { + // TODO: This should be unnecessary + m_pool_key_to_index.erase(keypool.vchPubKey.GetID()); } + walletdb.ErasePool(index); + it = setKeyPool->erase(it); } } +bool CWallet::HasUnusedKeys(int min_keys) const { + return setExternalKeyPool.size() >= min_keys && + (setInternalKeyPool.size() >= min_keys || + !CanSupportFeature(FEATURE_HD_SPLIT)); +} + void CWallet::GetScriptForMining(std::shared_ptr &script) { std::shared_ptr rKey = std::make_shared(this); CPubKey pubkey; @@ -3754,43 +3851,6 @@ /** @} */ // end of Actions -class CAffectedKeysVisitor : public boost::static_visitor { -private: - const CKeyStore &keystore; - std::vector &vKeys; - -public: - CAffectedKeysVisitor(const CKeyStore &keystoreIn, - std::vector &vKeysIn) - : keystore(keystoreIn), vKeys(vKeysIn) {} - - void Process(const CScript &script) { - txnouttype type; - std::vector vDest; - int nRequired; - if (ExtractDestinations(script, type, vDest, nRequired)) { - for (const CTxDestination &dest : vDest) { - boost::apply_visitor(*this, dest); - } - } - } - - void operator()(const CKeyID &keyId) { - if (keystore.HaveKey(keyId)) { - vKeys.push_back(keyId); - } - } - - void operator()(const CScriptID &scriptId) { - CScript script; - if (keystore.GetCScript(scriptId, script)) { - Process(script); - } - } - - void operator()(const CNoDestination &none) {} -}; - void CWallet::GetKeyBirthTimes( std::map &mapKeyBirth) const { // mapKeyMetadata @@ -4196,6 +4256,9 @@ RegisterValidationInterface(walletInstance); + // Try to top up keypool. No-op if the wallet is locked. + walletInstance->TopUpKeyPool(); + CBlockIndex *pindexRescan = chainActive.Genesis(); if (!gArgs.GetBoolArg("-rescan", false)) { CWalletDB walletdb(*walletInstance->dbw); diff --git a/test/functional/keypool-topup.py b/test/functional/keypool-topup.py new file mode 100755 --- /dev/null +++ b/test/functional/keypool-topup.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +# Copyright (c) 2017 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test HD Wallet keypool restore function. + +Two nodes. Node1 is under test. Node0 is providing transactions and generating blocks. + +- Start node1, shutdown and backup wallet. +- Generate 110 keys (enough to drain the keypool). Store key 90 (in the initial keypool) and key 110 (beyond the initial keypool). Send funds to key 90 and key 110. +- Stop node1, clear the datadir, move wallet file back into the datadir and restart node1. +- connect node1 to node0. Verify that they sync and node1 receives its funds.""" +import shutil + +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import ( + assert_equal, + connect_nodes_bi, + sync_blocks, +) + + +class KeypoolRestoreTest(BitcoinTestFramework): + def __init__(self): + super().__init__() + self.setup_clean_chain = True + self.num_nodes = 2 + self.extra_args = [['-usehd=0'], + ['-usehd=1', '-keypool=100', '-keypoolmin=20']] + + def run_test(self): + self.tmpdir = self.options.tmpdir + self.nodes[0].generate(101) + + self.log.info("Make backup of wallet") + + self.stop_node(1) + + shutil.copyfile(self.tmpdir + "/node1/regtest/wallet.dat", + self.tmpdir + "/wallet.bak") + self.nodes[1] = self.start_node(1, self.tmpdir, self.extra_args[1]) + connect_nodes_bi(self.nodes, 0, 1) + + self.log.info("Generate keys for wallet") + + for _ in range(90): + addr_oldpool = self.nodes[1].getnewaddress() + for _ in range(20): + addr_extpool = self.nodes[1].getnewaddress() + + self.log.info("Send funds to wallet") + + self.nodes[0].sendtoaddress(addr_oldpool, 10) + self.nodes[0].generate(1) + self.nodes[0].sendtoaddress(addr_extpool, 5) + self.nodes[0].generate(1) + sync_blocks(self.nodes) + + self.log.info("Restart node with wallet backup") + + self.stop_node(1) + + shutil.copyfile(self.tmpdir + "/wallet.bak", + self.tmpdir + "/node1/regtest/wallet.dat") + + self.log.info("Verify keypool is restored and balance is correct") + + self.nodes[1] = self.start_node(1, self.tmpdir, self.extra_args[1]) + connect_nodes_bi(self.nodes, 0, 1) + self.sync_all() + + assert_equal(self.nodes[1].getbalance(), 15) + assert_equal(self.nodes[1].listtransactions() + [0]['category'], "receive") + + # Check that we have marked all keys up to the used keypool key as used + assert_equal(self.nodes[1].validateaddress( + self.nodes[1].getnewaddress())['hdkeypath'], "m/0'/0'/111'") + + +if __name__ == '__main__': + KeypoolRestoreTest().main() diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -77,6 +77,7 @@ 'rawtransactions.py', 'reindex.py', # vv Tests less than 30s vv + 'keypool-topup.py', 'zmq_test.py', 'bitcoin_cli.py', 'mempool_resurrect_test.py', diff --git a/test/functional/wallet-hd.py b/test/functional/wallet-hd.py --- a/test/functional/wallet-hd.py +++ b/test/functional/wallet-hd.py @@ -8,7 +8,6 @@ assert_equal, connect_nodes_bi, ) -import os import shutil @@ -75,12 +74,14 @@ self.log.info("Restore backup ...") self.stop_node(1) - os.remove(self.options.tmpdir + "/node1/regtest/wallet.dat") + # we need to delete the complete regtest directory + # otherwise node1 would auto-recover all funds in flag the keypool keys as used + shutil.rmtree(tmpdir + "/node1/regtest/blocks") + shutil.rmtree(tmpdir + "/node1/regtest/chainstate") shutil.copyfile(tmpdir + "/hd.bak", tmpdir + "/node1/regtest/wallet.dat") self.nodes[1] = self.start_node( 1, self.options.tmpdir, self.extra_args[1]) - #connect_nodes_bi(self.nodes, 0, 1) # Assert that derivation is deterministic hd_add_2 = None @@ -90,12 +91,13 @@ assert_equal(hd_info_2["hdkeypath"], "m/0'/0'/" + str(_ + 1) + "'") assert_equal(hd_info_2["hdmasterkeyid"], masterkeyid) assert_equal(hd_add, hd_add_2) + connect_nodes_bi(self.nodes, 0, 1) + self.sync_all() # Needs rescan self.stop_node(1) self.nodes[1] = self.start_node( 1, self.options.tmpdir, self.extra_args[1] + ['-rescan']) - #connect_nodes_bi(self.nodes, 0, 1) assert_equal(self.nodes[1].getbalance(), num_hd_adds + 1) # send a tx and make sure its using the internal chain for the changeoutput