diff --git a/src/sync.cpp b/src/sync.cpp index 3474f9f9a..f544d06bf 100644 --- a/src/sync.cpp +++ b/src/sync.cpp @@ -1,293 +1,302 @@ // Copyright (c) 2011-2016 The Bitcoin Core developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef DEBUG_LOCKCONTENTION void PrintLockContention(const char *pszName, const char *pszFile, int nLine) { LogPrintf("LOCKCONTENTION: %s\n", pszName); LogPrintf("Locker: %s:%d\n", pszFile, nLine); } #endif /* DEBUG_LOCKCONTENTION */ #ifdef DEBUG_LOCKORDER // // Early deadlock detection. // Problem being solved: // Thread 1 locks A, then B, then C // Thread 2 locks D, then C, then A // --> may result in deadlock between the two threads, depending on when // they run. // Solution implemented here: // Keep track of pairs of locks: (A before B), (A before C), etc. // Complain if any thread tries to lock in a different order. // struct CLockLocation { CLockLocation(const char *pszName, const char *pszFile, int nLine, bool fTryIn, const std::string &thread_name) : fTry(fTryIn), mutexName(pszName), sourceFile(pszFile), m_thread_name(thread_name), sourceLine(nLine) {} std::string ToString() const { return strprintf("'%s' in %s:%s%s (in thread '%s')", mutexName, sourceFile, sourceLine, (fTry ? " (TRY)" : ""), m_thread_name); } std::string Name() const { return mutexName; } private: bool fTry; std::string mutexName; std::string sourceFile; const std::string &m_thread_name; int sourceLine; }; using LockStackItem = std::pair; using LockStack = std::vector; using LockStacks = std::unordered_map; using LockPair = std::pair; using LockOrders = std::map; using InvLockOrders = std::set; struct LockData { LockStacks m_lock_stacks; LockOrders lockorders; InvLockOrders invlockorders; std::mutex dd_mutex; }; LockData &GetLockData() { // This approach guarantees that the object is not destroyed until after its // last use. The operating system automatically reclaims all the memory in a // program's heap when that program exits. // Since the ~LockData() destructor is never called, the LockData class and // all its subclasses must have implicitly-defined destructors. static LockData &lock_data = *new LockData(); return lock_data; } static void potential_deadlock_detected(const LockPair &mismatch, const LockStack &s1, const LockStack &s2) { LogPrintf("POTENTIAL DEADLOCK DETECTED\n"); LogPrintf("Previous lock order was:\n"); for (const LockStackItem &i : s1) { if (i.first == mismatch.first) { LogPrintfToBeContinued(" (1)"); } if (i.first == mismatch.second) { LogPrintfToBeContinued(" (2)"); } LogPrintf(" %s\n", i.second.ToString()); } std::string mutex_a, mutex_b; LogPrintf("Current lock order is:\n"); for (const LockStackItem &i : s2) { if (i.first == mismatch.first) { LogPrintfToBeContinued(" (1)"); mutex_a = i.second.Name(); } if (i.first == mismatch.second) { LogPrintfToBeContinued(" (2)"); mutex_b = i.second.Name(); } LogPrintf(" %s\n", i.second.ToString()); } if (g_debug_lockorder_abort) { tfm::format( std::cerr, "Assertion failed: detected inconsistent lock order for %s, " "details in debug log.\n", s2.back().second.ToString()); abort(); } throw std::logic_error( strprintf("potential deadlock detected: %s -> %s -> %s", mutex_b, mutex_a, mutex_b)); } static void push_lock(void *c, const CLockLocation &locklocation) { LockData &lockdata = GetLockData(); std::lock_guard lock(lockdata.dd_mutex); LockStack &lock_stack = lockdata.m_lock_stacks[std::this_thread::get_id()]; lock_stack.emplace_back(c, locklocation); for (const LockStackItem &i : lock_stack) { if (i.first == c) { break; } const LockPair p1 = std::make_pair(i.first, c); if (lockdata.lockorders.count(p1)) { continue; } const LockPair p2 = std::make_pair(c, i.first); if (lockdata.lockorders.count(p2)) { auto lock_stack_copy = lock_stack; lock_stack.pop_back(); potential_deadlock_detected(p1, lockdata.lockorders[p2], lock_stack_copy); // potential_deadlock_detected() does not return. } lockdata.lockorders.emplace(p1, lock_stack); lockdata.invlockorders.insert(p2); } } static void pop_lock() { LockData &lockdata = GetLockData(); std::lock_guard lock(lockdata.dd_mutex); LockStack &lock_stack = lockdata.m_lock_stacks[std::this_thread::get_id()]; lock_stack.pop_back(); if (lock_stack.empty()) { lockdata.m_lock_stacks.erase(std::this_thread::get_id()); } } void EnterCritical(const char *pszName, const char *pszFile, int nLine, void *cs, bool fTry) { push_lock(cs, CLockLocation(pszName, pszFile, nLine, fTry, util::ThreadGetInternalName())); } void CheckLastCritical(void *cs, std::string &lockname, const char *guardname, const char *file, int line) { - { - LockData &lockdata = GetLockData(); - std::lock_guard lock(lockdata.dd_mutex); - - const LockStack &lock_stack = - lockdata.m_lock_stacks[std::this_thread::get_id()]; - if (!lock_stack.empty()) { - const auto &lastlock = lock_stack.back(); - if (lastlock.first == cs) { - lockname = lastlock.second.Name(); - return; - } + LockData &lockdata = GetLockData(); + std::lock_guard lock(lockdata.dd_mutex); + + const LockStack &lock_stack = + lockdata.m_lock_stacks[std::this_thread::get_id()]; + if (!lock_stack.empty()) { + const auto &lastlock = lock_stack.back(); + if (lastlock.first == cs) { + lockname = lastlock.second.Name(); + return; } } - throw std::system_error( - EPERM, std::generic_category(), - strprintf("%s:%s %s was not most recent critical section locked", file, - line, guardname)); + + LogPrintf("INCONSISTENT LOCK ORDER DETECTED\n"); + LogPrintf("Current lock order (least recent first) is:\n"); + for (const LockStackItem &i : lock_stack) { + LogPrintf(" %s\n", i.second.ToString()); + } + if (g_debug_lockorder_abort) { + tfm::format(std::cerr, + "%s:%s %s was not most recent critical section locked, " + "details in debug log.\n", + file, line, guardname); + abort(); + } + throw std::logic_error( + strprintf("%s was not most recent critical section locked", guardname)); } void LeaveCritical() { pop_lock(); } std::string LocksHeld() { LockData &lockdata = GetLockData(); std::lock_guard lock(lockdata.dd_mutex); const LockStack &lock_stack = lockdata.m_lock_stacks[std::this_thread::get_id()]; std::string result; for (const LockStackItem &i : lock_stack) { result += i.second.ToString() + std::string("\n"); } return result; } static bool LockHeld(void *mutex) { LockData &lockdata = GetLockData(); std::lock_guard lock(lockdata.dd_mutex); const LockStack &lock_stack = lockdata.m_lock_stacks[std::this_thread::get_id()]; for (const LockStackItem &i : lock_stack) { if (i.first == mutex) { return true; } } return false; } template void AssertLockHeldInternal(const char *pszName, const char *pszFile, int nLine, MutexType *cs) { if (LockHeld(cs)) { return; } tfm::format(std::cerr, "Assertion failed: lock %s not held in %s:%i; locks held:\n%s", pszName, pszFile, nLine, LocksHeld()); abort(); } template void AssertLockHeldInternal(const char *, const char *, int, Mutex *); template void AssertLockHeldInternal(const char *, const char *, int, RecursiveMutex *); template void AssertLockNotHeldInternal(const char *pszName, const char *pszFile, int nLine, MutexType *cs) { if (!LockHeld(cs)) { return; } tfm::format(std::cerr, "Assertion failed: lock %s held in %s:%i; locks held:\n%s", pszName, pszFile, nLine, LocksHeld()); abort(); } template void AssertLockNotHeldInternal(const char *, const char *, int, Mutex *); template void AssertLockNotHeldInternal(const char *, const char *, int, RecursiveMutex *); void DeleteLock(void *cs) { LockData &lockdata = GetLockData(); std::lock_guard lock(lockdata.dd_mutex); const LockPair item = std::make_pair(cs, nullptr); LockOrders::iterator it = lockdata.lockorders.lower_bound(item); while (it != lockdata.lockorders.end() && it->first.first == cs) { const LockPair invitem = std::make_pair(it->first.second, it->first.first); lockdata.invlockorders.erase(invitem); lockdata.lockorders.erase(it++); } InvLockOrders::iterator invit = lockdata.invlockorders.lower_bound(item); while (invit != lockdata.invlockorders.end() && invit->first == cs) { const LockPair invinvitem = std::make_pair(invit->second, invit->first); lockdata.lockorders.erase(invinvitem); lockdata.invlockorders.erase(invit++); } } bool LockStackEmpty() { LockData &lockdata = GetLockData(); std::lock_guard lock(lockdata.dd_mutex); const auto it = lockdata.m_lock_stacks.find(std::this_thread::get_id()); if (it == lockdata.m_lock_stacks.end()) { return true; } return it->second.empty(); } bool g_debug_lockorder_abort = true; #endif /* DEBUG_LOCKORDER */ diff --git a/src/sync.h b/src/sync.h index 57a54bee8..9788bd837 100644 --- a/src/sync.h +++ b/src/sync.h @@ -1,364 +1,366 @@ // Copyright (c) 2009-2010 Satoshi Nakamoto // Copyright (c) 2009-2016 The Bitcoin Core developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. #ifndef BITCOIN_SYNC_H #define BITCOIN_SYNC_H #include #include #include #include #include #include ///////////////////////////////////////////////// // // // THE SIMPLE DEFINITION, EXCLUDING DEBUG CODE // // // ///////////////////////////////////////////////// /* RecursiveMutex mutex; std::recursive_mutex mutex; LOCK(mutex); std::unique_lock criticalblock(mutex); LOCK2(mutex1, mutex2); std::unique_lock criticalblock1(mutex1); std::unique_lock criticalblock2(mutex2); TRY_LOCK(mutex, name); std::unique_lock name(mutex, std::try_to_lock_t); ENTER_CRITICAL_SECTION(mutex); // no RAII mutex.lock(); LEAVE_CRITICAL_SECTION(mutex); // no RAII mutex.unlock(); */ /////////////////////////////// // // // THE ACTUAL IMPLEMENTATION // // // /////////////////////////////// #ifdef DEBUG_LOCKORDER void EnterCritical(const char *pszName, const char *pszFile, int nLine, void *cs, bool fTry = false); void LeaveCritical(); void CheckLastCritical(void *cs, std::string &lockname, const char *guardname, const char *file, int line); std::string LocksHeld(); template void AssertLockHeldInternal(const char *pszName, const char *pszFile, int nLine, MutexType *cs) EXCLUSIVE_LOCKS_REQUIRED(cs); template void AssertLockNotHeldInternal(const char *pszName, const char *pszFile, int nLine, MutexType *cs) LOCKS_EXCLUDED(cs); void DeleteLock(void *cs); bool LockStackEmpty(); /** * Call abort() if a potential lock order deadlock bug is detected, instead of * just logging information and throwing a logic_error. Defaults to true, and * set to false in DEBUG_LOCKORDER unit tests. */ extern bool g_debug_lockorder_abort; #else inline void EnterCritical(const char *pszName, const char *pszFile, int nLine, void *cs, bool fTry = false) {} inline void LeaveCritical() {} inline void CheckLastCritical(void *cs, std::string &lockname, const char *guardname, const char *file, int line) {} template inline void AssertLockHeldInternal(const char *pszName, const char *pszFile, int nLine, MutexType *cs) EXCLUSIVE_LOCKS_REQUIRED(cs) {} template void AssertLockNotHeldInternal(const char *pszName, const char *pszFile, int nLine, MutexType *cs) LOCKS_EXCLUDED(cs) {} inline void DeleteLock(void *cs) {} inline bool LockStackEmpty() { return true; } #endif #define AssertLockHeld(cs) AssertLockHeldInternal(#cs, __FILE__, __LINE__, &cs) #define AssertLockNotHeld(cs) \ AssertLockNotHeldInternal(#cs, __FILE__, __LINE__, &cs) /** * Template mixin that adds -Wthread-safety locking annotations and lock order * checking to a subset of the mutex API. */ template class LOCKABLE AnnotatedMixin : public PARENT { public: ~AnnotatedMixin() { DeleteLock((void *)this); } void lock() EXCLUSIVE_LOCK_FUNCTION() { PARENT::lock(); } void unlock() UNLOCK_FUNCTION() { PARENT::unlock(); } bool try_lock() EXCLUSIVE_TRYLOCK_FUNCTION(true) { return PARENT::try_lock(); } using UniqueLock = std::unique_lock; #ifdef __clang__ //! For negative capabilities in the Clang Thread Safety Analysis. //! A negative requirement uses the EXCLUSIVE_LOCKS_REQUIRED attribute, in //! conjunction with the ! operator, to indicate that a mutex should not be //! held. const AnnotatedMixin &operator!() const { return *this; } #endif // __clang__ }; /** * Wrapped mutex: supports recursive locking, but no waiting * TODO: We should move away from using the recursive lock by default. */ using RecursiveMutex = AnnotatedMixin; /** Wrapped mutex: supports waiting but not recursive locking */ typedef AnnotatedMixin Mutex; #ifdef DEBUG_LOCKCONTENTION void PrintLockContention(const char *pszName, const char *pszFile, int nLine); #endif /** Wrapper around std::unique_lock style lock for Mutex. */ template class SCOPED_LOCKABLE UniqueLock : public Base { private: void Enter(const char *pszName, const char *pszFile, int nLine) { EnterCritical(pszName, pszFile, nLine, (void *)(Base::mutex())); #ifdef DEBUG_LOCKCONTENTION if (!Base::try_lock()) { PrintLockContention(pszName, pszFile, nLine); #endif Base::lock(); #ifdef DEBUG_LOCKCONTENTION } #endif } bool TryEnter(const char *pszName, const char *pszFile, int nLine) { EnterCritical(pszName, pszFile, nLine, (void *)(Base::mutex()), true); Base::try_lock(); if (!Base::owns_lock()) { LeaveCritical(); } return Base::owns_lock(); } public: UniqueLock(Mutex &mutexIn, const char *pszName, const char *pszFile, int nLine, bool fTry = false) EXCLUSIVE_LOCK_FUNCTION(mutexIn) : Base(mutexIn, std::defer_lock) { if (fTry) { TryEnter(pszName, pszFile, nLine); } else { Enter(pszName, pszFile, nLine); } } UniqueLock(Mutex *pmutexIn, const char *pszName, const char *pszFile, int nLine, bool fTry = false) EXCLUSIVE_LOCK_FUNCTION(pmutexIn) { if (!pmutexIn) { return; } *static_cast(this) = Base(*pmutexIn, std::defer_lock); if (fTry) { TryEnter(pszName, pszFile, nLine); } else { Enter(pszName, pszFile, nLine); } } ~UniqueLock() UNLOCK_FUNCTION() { if (Base::owns_lock()) { LeaveCritical(); } } operator bool() { return Base::owns_lock(); } protected: // needed for reverse_lock UniqueLock() {} public: /** * An RAII-style reverse lock. Unlocks on construction and locks on * destruction. */ class reverse_lock { public: explicit reverse_lock(UniqueLock &_lock, const char *_guardname, const char *_file, int _line) : lock(_lock), file(_file), line(_line) { CheckLastCritical((void *)lock.mutex(), lockname, _guardname, _file, _line); lock.unlock(); LeaveCritical(); lock.swap(templock); } ~reverse_lock() { templock.swap(lock); EnterCritical(lockname.c_str(), file.c_str(), line, (void *)lock.mutex()); lock.lock(); } private: reverse_lock(reverse_lock const &); reverse_lock &operator=(reverse_lock const &); UniqueLock &lock; UniqueLock templock; std::string lockname; const std::string file; const int line; }; friend class reverse_lock; }; #define REVERSE_LOCK(g) \ typename std::decay::type::reverse_lock PASTE2( \ revlock, __COUNTER__)(g, #g, __FILE__, __LINE__) template using DebugLock = UniqueLock::type>::type>; #define LOCK(cs) \ DebugLock PASTE2(criticalblock, \ __COUNTER__)(cs, #cs, __FILE__, __LINE__) #define LOCK2(cs1, cs2) \ DebugLock criticalblock1(cs1, #cs1, __FILE__, __LINE__); \ DebugLock criticalblock2(cs2, #cs2, __FILE__, __LINE__); #define TRY_LOCK(cs, name) \ DebugLock name(cs, #cs, __FILE__, __LINE__, true) #define WAIT_LOCK(cs, name) \ DebugLock name(cs, #cs, __FILE__, __LINE__) #define ENTER_CRITICAL_SECTION(cs) \ { \ EnterCritical(#cs, __FILE__, __LINE__, (void *)(&cs)); \ (cs).lock(); \ } #define LEAVE_CRITICAL_SECTION(cs) \ { \ + std::string lockname; \ + CheckLastCritical((void *)(&cs), lockname, #cs, __FILE__, __LINE__); \ (cs).unlock(); \ LeaveCritical(); \ } //! Run code while locking a mutex. //! //! Examples: //! //! WITH_LOCK(cs, shared_val = shared_val + 1); //! //! int val = WITH_LOCK(cs, return shared_val); //! #define WITH_LOCK(cs, code) \ [&] { \ LOCK(cs); \ code; \ }() class CSemaphore { private: std::condition_variable condition; std::mutex mutex; int value; public: explicit CSemaphore(int init) : value(init) {} void wait() { std::unique_lock lock(mutex); condition.wait(lock, [&]() { return value >= 1; }); value--; } bool try_wait() { std::lock_guard lock(mutex); if (value < 1) { return false; } value--; return true; } void post() { { std::lock_guard lock(mutex); value++; } condition.notify_one(); } }; /** RAII-style semaphore lock */ class CSemaphoreGrant { private: CSemaphore *sem; bool fHaveGrant; public: void Acquire() { if (fHaveGrant) { return; } sem->wait(); fHaveGrant = true; } void Release() { if (!fHaveGrant) { return; } sem->post(); fHaveGrant = false; } bool TryAcquire() { if (!fHaveGrant && sem->try_wait()) { fHaveGrant = true; } return fHaveGrant; } void MoveTo(CSemaphoreGrant &grant) { grant.Release(); grant.sem = sem; grant.fHaveGrant = fHaveGrant; fHaveGrant = false; } CSemaphoreGrant() : sem(nullptr), fHaveGrant(false) {} explicit CSemaphoreGrant(CSemaphore &sema, bool fTry = false) : sem(&sema), fHaveGrant(false) { if (fTry) { TryAcquire(); } else { Acquire(); } } ~CSemaphoreGrant() { Release(); } operator bool() const { return fHaveGrant; } }; #endif // BITCOIN_SYNC_H diff --git a/src/test/reverselock_tests.cpp b/src/test/reverselock_tests.cpp index a555ccaf2..35bcfb695 100644 --- a/src/test/reverselock_tests.cpp +++ b/src/test/reverselock_tests.cpp @@ -1,88 +1,91 @@ // Copyright (c) 2015-2019 The Bitcoin Core developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include #include #include BOOST_FIXTURE_TEST_SUITE(reverselock_tests, BasicTestingSetup) BOOST_AUTO_TEST_CASE(reverselock_basics) { Mutex mutex; WAIT_LOCK(mutex, lock); BOOST_CHECK(lock.owns_lock()); { REVERSE_LOCK(lock); BOOST_CHECK(!lock.owns_lock()); } BOOST_CHECK(lock.owns_lock()); } BOOST_AUTO_TEST_CASE(reverselock_multiple) { Mutex mutex2; Mutex mutex; WAIT_LOCK(mutex2, lock2); WAIT_LOCK(mutex, lock); // Make sure undoing two locks succeeds { REVERSE_LOCK(lock); BOOST_CHECK(!lock.owns_lock()); REVERSE_LOCK(lock2); BOOST_CHECK(!lock2.owns_lock()); } BOOST_CHECK(lock.owns_lock()); BOOST_CHECK(lock2.owns_lock()); } BOOST_AUTO_TEST_CASE(reverselock_errors) { Mutex mutex2; Mutex mutex; WAIT_LOCK(mutex2, lock2); WAIT_LOCK(mutex, lock); #ifdef DEBUG_LOCKORDER + bool prev = g_debug_lockorder_abort; + g_debug_lockorder_abort = false; + // Make sure trying to reverse lock a previous lock fails - try { - REVERSE_LOCK(lock2); - BOOST_CHECK(false); // REVERSE_LOCK(lock2) succeeded - } catch (...) { - } + BOOST_CHECK_EXCEPTION( + REVERSE_LOCK(lock2), std::logic_error, + HasReason("lock2 was not most recent critical section locked")); BOOST_CHECK(lock2.owns_lock()); + + g_debug_lockorder_abort = prev; #endif // Make sure trying to reverse lock an unlocked lock fails lock.unlock(); BOOST_CHECK(!lock.owns_lock()); bool failed = false; try { REVERSE_LOCK(lock); } catch (...) { failed = true; } BOOST_CHECK(failed); BOOST_CHECK(!lock.owns_lock()); // Locking the original lock after it has been taken by a reverse lock // makes no sense. Ensure that the original lock no longer owns the lock // after giving it to a reverse one. lock.lock(); BOOST_CHECK(lock.owns_lock()); { REVERSE_LOCK(lock); BOOST_CHECK(!lock.owns_lock()); } BOOST_CHECK(failed); BOOST_CHECK(lock.owns_lock()); } BOOST_AUTO_TEST_SUITE_END() diff --git a/src/test/sync_tests.cpp b/src/test/sync_tests.cpp index b12cb0c7c..5615ed231 100644 --- a/src/test/sync_tests.cpp +++ b/src/test/sync_tests.cpp @@ -1,58 +1,98 @@ // Copyright (c) 2012-2019 The Bitcoin Core developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include #include #include namespace { template void TestPotentialDeadLockDetected(MutexType &mutex1, MutexType &mutex2) { { LOCK2(mutex1, mutex2); } BOOST_CHECK(LockStackEmpty()); bool error_thrown = false; try { LOCK2(mutex2, mutex1); } catch (const std::logic_error &e) { BOOST_CHECK_EQUAL( e.what(), "potential deadlock detected: mutex1 -> mutex2 -> mutex1"); error_thrown = true; } BOOST_CHECK(LockStackEmpty()); #ifdef DEBUG_LOCKORDER BOOST_CHECK(error_thrown); #else BOOST_CHECK(!error_thrown); #endif } + +template +void TestInconsistentLockOrderDetected(MutexType &mutex1, MutexType &mutex2) + NO_THREAD_SAFETY_ANALYSIS { + ENTER_CRITICAL_SECTION(mutex1); + ENTER_CRITICAL_SECTION(mutex2); +#ifdef DEBUG_LOCKORDER + BOOST_CHECK_EXCEPTION( + LEAVE_CRITICAL_SECTION(mutex1), std::logic_error, + HasReason("mutex1 was not most recent critical section locked")); +#endif // DEBUG_LOCKORDER + LEAVE_CRITICAL_SECTION(mutex2); + LEAVE_CRITICAL_SECTION(mutex1); + BOOST_CHECK(LockStackEmpty()); +} } // namespace BOOST_FIXTURE_TEST_SUITE(sync_tests, BasicTestingSetup) BOOST_AUTO_TEST_CASE(potential_deadlock_detected) { #ifdef DEBUG_LOCKORDER bool prev = g_debug_lockorder_abort; g_debug_lockorder_abort = false; #endif RecursiveMutex rmutex1, rmutex2; TestPotentialDeadLockDetected(rmutex1, rmutex2); // The second test ensures that lock tracking data have not been broken by // exception. TestPotentialDeadLockDetected(rmutex1, rmutex2); Mutex mutex1, mutex2; TestPotentialDeadLockDetected(mutex1, mutex2); // The second test ensures that lock tracking data have not been broken by // exception. TestPotentialDeadLockDetected(mutex1, mutex2); #ifdef DEBUG_LOCKORDER g_debug_lockorder_abort = prev; #endif } +BOOST_AUTO_TEST_CASE(inconsistent_lock_order_detected) { +#ifdef DEBUG_LOCKORDER + bool prev = g_debug_lockorder_abort; + g_debug_lockorder_abort = false; +#endif // DEBUG_LOCKORDER + + RecursiveMutex rmutex1, rmutex2; + TestInconsistentLockOrderDetected(rmutex1, rmutex2); + // By checking lock order consistency (CheckLastCritical) before any + // unlocking (LeaveCritical) the lock tracking data must not have been + // broken by exception. + TestInconsistentLockOrderDetected(rmutex1, rmutex2); + + Mutex mutex1, mutex2; + TestInconsistentLockOrderDetected(mutex1, mutex2); + // By checking lock order consistency (CheckLastCritical) before any + // unlocking (LeaveCritical) the lock tracking data must not have been + // broken by exception. + TestInconsistentLockOrderDetected(mutex1, mutex2); + +#ifdef DEBUG_LOCKORDER + g_debug_lockorder_abort = prev; +#endif // DEBUG_LOCKORDER +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/src/wallet/test/wallet_tests.cpp b/src/wallet/test/wallet_tests.cpp index 8b4e60c6f..21e30c6fb 100644 --- a/src/wallet/test/wallet_tests.cpp +++ b/src/wallet/test/wallet_tests.cpp @@ -1,906 +1,911 @@ // Copyright (c) 2012-2019 The Bitcoin Core developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include +extern RecursiveMutex cs_wallets; + BOOST_FIXTURE_TEST_SUITE(wallet_tests, WalletTestingSetup) static std::shared_ptr TestLoadWallet(interfaces::Chain &chain) { DatabaseOptions options; DatabaseStatus status; bilingual_str error; std::vector warnings; auto database = MakeWalletDatabase("", options, status, error); auto wallet = CWallet::Create(chain, "", std::move(database), options.create_flags, error, warnings); wallet->postInitProcess(); return wallet; } static void TestUnloadWallet(std::shared_ptr &&wallet) { SyncWithValidationInterfaceQueue(); wallet->m_chain_notifications_handler.reset(); UnloadWallet(std::move(wallet)); } static CMutableTransaction TestSimpleSpend(const CTransaction &from, uint32_t index, const CKey &key, const CScript &pubkey) { CMutableTransaction mtx; mtx.vout.push_back( {from.vout[index].nValue - DEFAULT_TRANSACTION_MAXFEE, pubkey}); mtx.vin.push_back({CTxIn{from.GetId(), index}}); FillableSigningProvider keystore; keystore.AddKey(key); std::map coins; coins[mtx.vin[0].prevout].GetTxOut() = from.vout[index]; std::map input_errors; BOOST_CHECK(SignTransaction(mtx, &keystore, coins, SigHashType().withForkId(), input_errors)); return mtx; } static void AddKey(CWallet &wallet, const CKey &key) { auto spk_man = wallet.GetOrCreateLegacyScriptPubKeyMan(); LOCK2(wallet.cs_wallet, spk_man->cs_KeyStore); spk_man->AddKeyPubKey(key, key.GetPubKey()); } BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions, TestChain100Setup) { // Cap last block file size, and mine new block in a new block file. CBlockIndex *oldTip = ::ChainActive().Tip(); GetBlockFileInfo(oldTip->GetBlockPos().nFile)->nSize = MAX_BLOCKFILE_SIZE; CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())); CBlockIndex *newTip = ::ChainActive().Tip(); NodeContext node; auto chain = interfaces::MakeChain(node, Params()); // Verify ScanForWalletTransactions fails to read an unknown start block. { CWallet wallet(chain.get(), "", CreateDummyWalletDatabase()); { LOCK(wallet.cs_wallet); wallet.SetLastBlockProcessed(::ChainActive().Height(), ::ChainActive().Tip()->GetBlockHash()); } AddKey(wallet, coinbaseKey); WalletRescanReserver reserver(wallet); reserver.reserve(); CWallet::ScanResult result = wallet.ScanForWalletTransactions( BlockHash() /* start_block */, 0 /* start_height */, {} /* max_height */, reserver, false /* update */); BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::FAILURE); BOOST_CHECK(result.last_failed_block.IsNull()); BOOST_CHECK(result.last_scanned_block.IsNull()); BOOST_CHECK(!result.last_scanned_height); BOOST_CHECK_EQUAL(wallet.GetBalance().m_mine_immature, Amount::zero()); } // Verify ScanForWalletTransactions picks up transactions in both the old // and new block files. { CWallet wallet(chain.get(), "", CreateDummyWalletDatabase()); { LOCK(wallet.cs_wallet); wallet.SetLastBlockProcessed(::ChainActive().Height(), ::ChainActive().Tip()->GetBlockHash()); } AddKey(wallet, coinbaseKey); WalletRescanReserver reserver(wallet); reserver.reserve(); CWallet::ScanResult result = wallet.ScanForWalletTransactions( oldTip->GetBlockHash(), oldTip->nHeight, {} /* max_height */, reserver, false /* update */); BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::SUCCESS); BOOST_CHECK(result.last_failed_block.IsNull()); BOOST_CHECK_EQUAL(result.last_scanned_block, newTip->GetBlockHash()); BOOST_CHECK_EQUAL(*result.last_scanned_height, newTip->nHeight); BOOST_CHECK_EQUAL(wallet.GetBalance().m_mine_immature, 100 * COIN); } // Prune the older block file. { LOCK(cs_main); Assert(m_node.chainman) ->m_blockman.PruneOneBlockFile(oldTip->GetBlockPos().nFile); } UnlinkPrunedFiles({oldTip->GetBlockPos().nFile}); // Verify ScanForWalletTransactions only picks transactions in the new block // file. { CWallet wallet(chain.get(), "", CreateDummyWalletDatabase()); { LOCK(wallet.cs_wallet); wallet.SetLastBlockProcessed(::ChainActive().Height(), ::ChainActive().Tip()->GetBlockHash()); } AddKey(wallet, coinbaseKey); WalletRescanReserver reserver(wallet); reserver.reserve(); CWallet::ScanResult result = wallet.ScanForWalletTransactions( oldTip->GetBlockHash(), oldTip->nHeight, {} /* max_height */, reserver, false /* update */); BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::FAILURE); BOOST_CHECK_EQUAL(result.last_failed_block, oldTip->GetBlockHash()); BOOST_CHECK_EQUAL(result.last_scanned_block, newTip->GetBlockHash()); BOOST_CHECK_EQUAL(*result.last_scanned_height, newTip->nHeight); BOOST_CHECK_EQUAL(wallet.GetBalance().m_mine_immature, 50 * COIN); } // Prune the remaining block file. { LOCK(cs_main); Assert(m_node.chainman) ->m_blockman.PruneOneBlockFile(newTip->GetBlockPos().nFile); } UnlinkPrunedFiles({newTip->GetBlockPos().nFile}); // Verify ScanForWalletTransactions scans no blocks. { CWallet wallet(chain.get(), "", CreateDummyWalletDatabase()); { LOCK(wallet.cs_wallet); wallet.SetLastBlockProcessed(::ChainActive().Height(), ::ChainActive().Tip()->GetBlockHash()); } AddKey(wallet, coinbaseKey); WalletRescanReserver reserver(wallet); reserver.reserve(); CWallet::ScanResult result = wallet.ScanForWalletTransactions( oldTip->GetBlockHash(), oldTip->nHeight, {} /* max_height */, reserver, false /* update */); BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::FAILURE); BOOST_CHECK_EQUAL(result.last_failed_block, newTip->GetBlockHash()); BOOST_CHECK(result.last_scanned_block.IsNull()); BOOST_CHECK(!result.last_scanned_height); BOOST_CHECK_EQUAL(wallet.GetBalance().m_mine_immature, Amount::zero()); } } BOOST_FIXTURE_TEST_CASE(importmulti_rescan, TestChain100Setup) { // Cap last block file size, and mine new block in a new block file. CBlockIndex *oldTip = ::ChainActive().Tip(); GetBlockFileInfo(oldTip->GetBlockPos().nFile)->nSize = MAX_BLOCKFILE_SIZE; CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())); CBlockIndex *newTip = ::ChainActive().Tip(); NodeContext node; auto chain = interfaces::MakeChain(node, Params()); // Prune the older block file. { LOCK(cs_main); Assert(m_node.chainman) ->m_blockman.PruneOneBlockFile(oldTip->GetBlockPos().nFile); } UnlinkPrunedFiles({oldTip->GetBlockPos().nFile}); // Verify importmulti RPC returns failure for a key whose creation time is // before the missing block, and success for a key whose creation time is // after. { std::shared_ptr wallet = std::make_shared( chain.get(), "", CreateDummyWalletDatabase()); wallet->SetupLegacyScriptPubKeyMan(); WITH_LOCK(wallet->cs_wallet, wallet->SetLastBlockProcessed(newTip->nHeight, newTip->GetBlockHash())); AddWallet(wallet); UniValue keys; keys.setArray(); UniValue key; key.setObject(); key.pushKV("scriptPubKey", HexStr(GetScriptForRawPubKey(coinbaseKey.GetPubKey()))); key.pushKV("timestamp", 0); key.pushKV("internal", UniValue(true)); keys.push_back(key); key.clear(); key.setObject(); CKey futureKey; futureKey.MakeNewKey(true); key.pushKV("scriptPubKey", HexStr(GetScriptForRawPubKey(futureKey.GetPubKey()))); key.pushKV("timestamp", newTip->GetBlockTimeMax() + TIMESTAMP_WINDOW + 1); key.pushKV("internal", UniValue(true)); keys.push_back(key); util::Ref context; JSONRPCRequest request(context); request.params.setArray(); request.params.push_back(keys); UniValue response = importmulti().HandleRequest(GetConfig(), request); BOOST_CHECK_EQUAL( response.write(), strprintf("[{\"success\":false,\"error\":{\"code\":-1,\"message\":" "\"Rescan failed for key with creation timestamp %d. " "There was an error reading a block from time %d, which " "is after or within %d seconds of key creation, and " "could contain transactions pertaining to the key. As a " "result, transactions and coins using this key may not " "appear in the wallet. This error could be caused by " "pruning or data corruption (see bitcoind log for " "details) and could be dealt with by downloading and " "rescanning the relevant blocks (see -reindex and " "-rescan options).\"}},{\"success\":true}]", 0, oldTip->GetBlockTimeMax(), TIMESTAMP_WINDOW)); RemoveWallet(wallet, std::nullopt); } } // Verify importwallet RPC starts rescan at earliest block with timestamp // greater or equal than key birthday. Previously there was a bug where // importwallet RPC would start the scan at the latest block with timestamp less // than or equal to key birthday. BOOST_FIXTURE_TEST_CASE(importwallet_rescan, TestChain100Setup) { // Create two blocks with same timestamp to verify that importwallet rescan // will pick up both blocks, not just the first. const int64_t BLOCK_TIME = ::ChainActive().Tip()->GetBlockTimeMax() + 5; SetMockTime(BLOCK_TIME); m_coinbase_txns.emplace_back( CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())) .vtx[0]); m_coinbase_txns.emplace_back( CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())) .vtx[0]); // Set key birthday to block time increased by the timestamp window, so // rescan will start at the block time. const int64_t KEY_TIME = BLOCK_TIME + TIMESTAMP_WINDOW; SetMockTime(KEY_TIME); m_coinbase_txns.emplace_back( CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())) .vtx[0]); NodeContext node; auto chain = interfaces::MakeChain(node, Params()); std::string backup_file = fs::PathToString(GetDataDir() / "wallet.backup"); // Import key into wallet and call dumpwallet to create backup file. { std::shared_ptr wallet = std::make_shared( chain.get(), "", CreateDummyWalletDatabase()); { auto spk_man = wallet->GetOrCreateLegacyScriptPubKeyMan(); LOCK2(wallet->cs_wallet, spk_man->cs_KeyStore); spk_man->mapKeyMetadata[coinbaseKey.GetPubKey().GetID()] .nCreateTime = KEY_TIME; spk_man->AddKeyPubKey(coinbaseKey, coinbaseKey.GetPubKey()); AddWallet(wallet); wallet->SetLastBlockProcessed( ::ChainActive().Height(), ::ChainActive().Tip()->GetBlockHash()); } util::Ref context; JSONRPCRequest request(context); request.params.setArray(); request.params.push_back(backup_file); ::dumpwallet().HandleRequest(GetConfig(), request); RemoveWallet(wallet, std::nullopt); } // Call importwallet RPC and verify all blocks with timestamps >= BLOCK_TIME // were scanned, and no prior blocks were scanned. { std::shared_ptr wallet = std::make_shared( chain.get(), "", CreateDummyWalletDatabase()); LOCK(wallet->cs_wallet); wallet->SetupLegacyScriptPubKeyMan(); util::Ref context; JSONRPCRequest request(context); request.params.setArray(); request.params.push_back(backup_file); AddWallet(wallet); wallet->SetLastBlockProcessed(::ChainActive().Height(), ::ChainActive().Tip()->GetBlockHash()); ::importwallet().HandleRequest(GetConfig(), request); RemoveWallet(wallet, std::nullopt); BOOST_CHECK_EQUAL(wallet->mapWallet.size(), 3U); BOOST_CHECK_EQUAL(m_coinbase_txns.size(), 103U); for (size_t i = 0; i < m_coinbase_txns.size(); ++i) { bool found = wallet->GetWalletTx(m_coinbase_txns[i]->GetId()); bool expected = i >= 100; BOOST_CHECK_EQUAL(found, expected); } } } // Check that GetImmatureCredit() returns a newly calculated value instead of // the cached value after a MarkDirty() call. // // This is a regression test written to verify a bugfix for the immature credit // function. Similar tests probably should be written for the other credit and // debit functions. BOOST_FIXTURE_TEST_CASE(coin_mark_dirty_immature_credit, TestChain100Setup) { NodeContext node; auto chain = interfaces::MakeChain(node, Params()); CWallet wallet(chain.get(), "", CreateDummyWalletDatabase()); auto spk_man = wallet.GetOrCreateLegacyScriptPubKeyMan(); CWalletTx wtx(&wallet, m_coinbase_txns.back()); LOCK2(wallet.cs_wallet, spk_man->cs_KeyStore); wallet.SetLastBlockProcessed(::ChainActive().Height(), ::ChainActive().Tip()->GetBlockHash()); CWalletTx::Confirmation confirm(CWalletTx::Status::CONFIRMED, ::ChainActive().Height(), ::ChainActive().Tip()->GetBlockHash(), 0); wtx.m_confirm = confirm; // Call GetImmatureCredit() once before adding the key to the wallet to // cache the current immature credit amount, which is 0. BOOST_CHECK_EQUAL(wtx.GetImmatureCredit(), Amount::zero()); // Invalidate the cached value, add the key, and make sure a new immature // credit amount is calculated. wtx.MarkDirty(); BOOST_CHECK(spk_man->AddKeyPubKey(coinbaseKey, coinbaseKey.GetPubKey())); BOOST_CHECK_EQUAL(wtx.GetImmatureCredit(), 50 * COIN); } static int64_t AddTx(ChainstateManager &chainman, CWallet &wallet, uint32_t lockTime, int64_t mockTime, int64_t blockTime) { CMutableTransaction tx; CWalletTx::Confirmation confirm; tx.nLockTime = lockTime; SetMockTime(mockTime); CBlockIndex *block = nullptr; if (blockTime > 0) { LOCK(cs_main); auto inserted = chainman.BlockIndex().emplace(BlockHash(GetRandHash()), new CBlockIndex); assert(inserted.second); const BlockHash &hash = inserted.first->first; block = inserted.first->second; block->nTime = blockTime; block->phashBlock = &hash; confirm = {CWalletTx::Status::CONFIRMED, block->nHeight, hash, 0}; } // If transaction is already in map, to avoid inconsistencies, // unconfirmation is needed before confirm again with different block. return wallet .AddToWallet(MakeTransactionRef(tx), confirm, [&](CWalletTx &wtx, bool /* new_tx */) { wtx.setUnconfirmed(); return true; }) ->nTimeSmart; } // Simple test to verify assignment of CWalletTx::nSmartTime value. Could be // expanded to cover more corner cases of smart time logic. BOOST_AUTO_TEST_CASE(ComputeTimeSmart) { // New transaction should use clock time if lower than block time. BOOST_CHECK_EQUAL(AddTx(*m_node.chainman, m_wallet, 1, 100, 120), 100); // Test that updating existing transaction does not change smart time. BOOST_CHECK_EQUAL(AddTx(*m_node.chainman, m_wallet, 1, 200, 220), 100); // New transaction should use clock time if there's no block time. BOOST_CHECK_EQUAL(AddTx(*m_node.chainman, m_wallet, 2, 300, 0), 300); // New transaction should use block time if lower than clock time. BOOST_CHECK_EQUAL(AddTx(*m_node.chainman, m_wallet, 3, 420, 400), 400); // New transaction should use latest entry time if higher than // min(block time, clock time). BOOST_CHECK_EQUAL(AddTx(*m_node.chainman, m_wallet, 4, 500, 390), 400); // If there are future entries, new transaction should use time of the // newest entry that is no more than 300 seconds ahead of the clock time. BOOST_CHECK_EQUAL(AddTx(*m_node.chainman, m_wallet, 5, 50, 600), 300); // Reset mock time for other tests. SetMockTime(0); } BOOST_AUTO_TEST_CASE(LoadReceiveRequests) { CTxDestination dest = PKHash(); LOCK(m_wallet.cs_wallet); WalletBatch batch{m_wallet.GetDatabase()}; m_wallet.AddDestData(batch, dest, "misc", "val_misc"); m_wallet.AddDestData(batch, dest, "rr0", "val_rr0"); m_wallet.AddDestData(batch, dest, "rr1", "val_rr1"); auto values = m_wallet.GetDestValues("rr"); BOOST_CHECK_EQUAL(values.size(), 2U); BOOST_CHECK_EQUAL(values[0], "val_rr0"); BOOST_CHECK_EQUAL(values[1], "val_rr1"); } // Test some watch-only LegacyScriptPubKeyMan methods by the procedure of // loading (LoadWatchOnly), checking (HaveWatchOnly), getting (GetWatchPubKey) // and removing (RemoveWatchOnly) a given PubKey, resp. its corresponding P2PK // Script. Results of the the impact on the address -> PubKey map is dependent // on whether the PubKey is a point on the curve static void TestWatchOnlyPubKey(LegacyScriptPubKeyMan *spk_man, const CPubKey &add_pubkey) { CScript p2pk = GetScriptForRawPubKey(add_pubkey); CKeyID add_address = add_pubkey.GetID(); CPubKey found_pubkey; LOCK(spk_man->cs_KeyStore); // all Scripts (i.e. also all PubKeys) are added to the general watch-only // set BOOST_CHECK(!spk_man->HaveWatchOnly(p2pk)); spk_man->LoadWatchOnly(p2pk); BOOST_CHECK(spk_man->HaveWatchOnly(p2pk)); // only PubKeys on the curve shall be added to the watch-only address -> // PubKey map bool is_pubkey_fully_valid = add_pubkey.IsFullyValid(); if (is_pubkey_fully_valid) { BOOST_CHECK(spk_man->GetWatchPubKey(add_address, found_pubkey)); BOOST_CHECK(found_pubkey == add_pubkey); } else { BOOST_CHECK(!spk_man->GetWatchPubKey(add_address, found_pubkey)); // passed key is unchanged BOOST_CHECK(found_pubkey == CPubKey()); } spk_man->RemoveWatchOnly(p2pk); BOOST_CHECK(!spk_man->HaveWatchOnly(p2pk)); if (is_pubkey_fully_valid) { BOOST_CHECK(!spk_man->GetWatchPubKey(add_address, found_pubkey)); // passed key is unchanged BOOST_CHECK(found_pubkey == add_pubkey); } } // Cryptographically invalidate a PubKey whilst keeping length and first byte static void PollutePubKey(CPubKey &pubkey) { std::vector pubkey_raw(pubkey.begin(), pubkey.end()); std::fill(pubkey_raw.begin() + 1, pubkey_raw.end(), 0); pubkey = CPubKey(pubkey_raw); assert(!pubkey.IsFullyValid()); assert(pubkey.IsValid()); } // Test watch-only logic for PubKeys BOOST_AUTO_TEST_CASE(WatchOnlyPubKeys) { CKey key; CPubKey pubkey; LegacyScriptPubKeyMan *spk_man = m_wallet.GetOrCreateLegacyScriptPubKeyMan(); BOOST_CHECK(!spk_man->HaveWatchOnly()); // uncompressed valid PubKey key.MakeNewKey(false); pubkey = key.GetPubKey(); assert(!pubkey.IsCompressed()); TestWatchOnlyPubKey(spk_man, pubkey); // uncompressed cryptographically invalid PubKey PollutePubKey(pubkey); TestWatchOnlyPubKey(spk_man, pubkey); // compressed valid PubKey key.MakeNewKey(true); pubkey = key.GetPubKey(); assert(pubkey.IsCompressed()); TestWatchOnlyPubKey(spk_man, pubkey); // compressed cryptographically invalid PubKey PollutePubKey(pubkey); TestWatchOnlyPubKey(spk_man, pubkey); // invalid empty PubKey pubkey = CPubKey(); TestWatchOnlyPubKey(spk_man, pubkey); } class ListCoinsTestingSetup : public TestChain100Setup { public: ListCoinsTestingSetup() { CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())); wallet = std::make_unique(m_chain.get(), "", CreateMockWalletDatabase()); { LOCK2(wallet->cs_wallet, ::cs_main); wallet->SetLastBlockProcessed( ::ChainActive().Height(), ::ChainActive().Tip()->GetBlockHash()); } bool firstRun; wallet->LoadWallet(firstRun); AddKey(*wallet, coinbaseKey); WalletRescanReserver reserver(*wallet); reserver.reserve(); CWallet::ScanResult result = wallet->ScanForWalletTransactions( ::ChainActive().Genesis()->GetBlockHash(), 0 /* start_height */, {} /* max_height */, reserver, false /* update */); BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::SUCCESS); BOOST_CHECK_EQUAL(result.last_scanned_block, ::ChainActive().Tip()->GetBlockHash()); BOOST_CHECK_EQUAL(*result.last_scanned_height, ::ChainActive().Height()); BOOST_CHECK(result.last_failed_block.IsNull()); } ~ListCoinsTestingSetup() { wallet.reset(); } CWalletTx &AddTx(CRecipient recipient) { CTransactionRef tx; Amount fee; int changePos = -1; bilingual_str error; CCoinControl dummy; { BOOST_CHECK(wallet->CreateTransaction({recipient}, tx, fee, changePos, error, dummy)); } wallet->CommitTransaction(tx, {}, {}); CMutableTransaction blocktx; { LOCK(wallet->cs_wallet); blocktx = CMutableTransaction(*wallet->mapWallet.at(tx->GetId()).tx); } CreateAndProcessBlock({CMutableTransaction(blocktx)}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())); LOCK(wallet->cs_wallet); wallet->SetLastBlockProcessed(wallet->GetLastBlockHeight() + 1, ::ChainActive().Tip()->GetBlockHash()); auto it = wallet->mapWallet.find(tx->GetId()); BOOST_CHECK(it != wallet->mapWallet.end()); CWalletTx::Confirmation confirm( CWalletTx::Status::CONFIRMED, ::ChainActive().Height(), ::ChainActive().Tip()->GetBlockHash(), 1); it->second.m_confirm = confirm; return it->second; } std::unique_ptr m_chain = interfaces::MakeChain(m_node, Params()); std::unique_ptr wallet; }; BOOST_FIXTURE_TEST_CASE(ListCoins, ListCoinsTestingSetup) { std::string coinbaseAddress = coinbaseKey.GetPubKey().GetID().ToString(); // Confirm ListCoins initially returns 1 coin grouped under coinbaseKey // address. std::map> list; { LOCK(wallet->cs_wallet); list = wallet->ListCoins(); } BOOST_CHECK_EQUAL(list.size(), 1U); BOOST_CHECK_EQUAL(boost::get(list.begin()->first).ToString(), coinbaseAddress); BOOST_CHECK_EQUAL(list.begin()->second.size(), 1U); // Check initial balance from one mature coinbase transaction. BOOST_CHECK_EQUAL(50 * COIN, wallet->GetAvailableBalance()); // Add a transaction creating a change address, and confirm ListCoins still // returns the coin associated with the change address underneath the // coinbaseKey pubkey, even though the change address has a different // pubkey. AddTx(CRecipient{GetScriptForRawPubKey({}), 1 * COIN, false /* subtract fee */}); { LOCK(wallet->cs_wallet); list = wallet->ListCoins(); } BOOST_CHECK_EQUAL(list.size(), 1U); BOOST_CHECK_EQUAL(boost::get(list.begin()->first).ToString(), coinbaseAddress); BOOST_CHECK_EQUAL(list.begin()->second.size(), 2U); // Lock both coins. Confirm number of available coins drops to 0. { LOCK(wallet->cs_wallet); std::vector available; wallet->AvailableCoins(available); BOOST_CHECK_EQUAL(available.size(), 2U); } for (const auto &group : list) { for (const auto &coin : group.second) { LOCK(wallet->cs_wallet); wallet->LockCoin(COutPoint(coin.tx->GetId(), coin.i)); } } { LOCK(wallet->cs_wallet); std::vector available; wallet->AvailableCoins(available); BOOST_CHECK_EQUAL(available.size(), 0U); } // Confirm ListCoins still returns same result as before, despite coins // being locked. { LOCK(wallet->cs_wallet); list = wallet->ListCoins(); } BOOST_CHECK_EQUAL(list.size(), 1U); BOOST_CHECK_EQUAL(boost::get(list.begin()->first).ToString(), coinbaseAddress); BOOST_CHECK_EQUAL(list.begin()->second.size(), 2U); } BOOST_FIXTURE_TEST_CASE(wallet_disableprivkeys, TestChain100Setup) { NodeContext node; auto chain = interfaces::MakeChain(node, Params()); std::shared_ptr wallet = std::make_shared(chain.get(), "", CreateDummyWalletDatabase()); wallet->SetupLegacyScriptPubKeyMan(); wallet->SetMinVersion(FEATURE_LATEST); wallet->SetWalletFlag(WALLET_FLAG_DISABLE_PRIVATE_KEYS); BOOST_CHECK(!wallet->TopUpKeyPool(1000)); CTxDestination dest; std::string error; BOOST_CHECK( !wallet->GetNewDestination(OutputType::LEGACY, "", dest, error)); } // Explicit calculation which is used to test the wallet constant static size_t CalculateP2PKHInputSize(bool use_max_sig) { // Generate ephemeral valid pubkey CKey key; key.MakeNewKey(true); CPubKey pubkey = key.GetPubKey(); // Generate pubkey hash PKHash key_hash(pubkey); // Create script to enter into keystore. Key hash can't be 0... CScript script = GetScriptForDestination(key_hash); // Add script to key store and key to watchonly FillableSigningProvider keystore; keystore.AddKeyPubKey(key, pubkey); // Fill in dummy signatures for fee calculation. SignatureData sig_data; if (!ProduceSignature(keystore, use_max_sig ? DUMMY_MAXIMUM_SIGNATURE_CREATOR : DUMMY_SIGNATURE_CREATOR, script, sig_data)) { // We're hand-feeding it correct arguments; shouldn't happen assert(false); } CTxIn tx_in; UpdateInput(tx_in, sig_data); return (size_t)GetVirtualTransactionInputSize(tx_in); } BOOST_FIXTURE_TEST_CASE(dummy_input_size_test, TestChain100Setup) { BOOST_CHECK(CalculateP2PKHInputSize(false) <= DUMMY_P2PKH_INPUT_SIZE); BOOST_CHECK_EQUAL(CalculateP2PKHInputSize(true), DUMMY_P2PKH_INPUT_SIZE); } bool malformed_descriptor(std::ios_base::failure e) { std::string s(e.what()); return s.find("Missing checksum") != std::string::npos; } BOOST_FIXTURE_TEST_CASE(wallet_descriptor_test, BasicTestingSetup) { std::vector malformed_record; CVectorWriter vw(0, 0, malformed_record, 0); vw << std::string("notadescriptor"); vw << (uint64_t)0; vw << (int32_t)0; vw << (int32_t)0; vw << (int32_t)1; VectorReader vr(0, 0, malformed_record, 0); WalletDescriptor w_desc; BOOST_CHECK_EXCEPTION(vr >> w_desc, std::ios_base::failure, malformed_descriptor); } //! Test CWallet::Create() and its behavior handling potential race //! conditions if it's called the same time an incoming transaction shows up in //! the mempool or a new block. //! //! It isn't possible to verify there aren't race condition in every case, so //! this test just checks two specific cases and ensures that timing of //! notifications in these cases doesn't prevent the wallet from detecting //! transactions. //! //! In the first case, block and mempool transactions are created before the //! wallet is loaded, but notifications about these transactions are delayed //! until after it is loaded. The notifications are superfluous in this case, so //! the test verifies the transactions are detected before they arrive. //! //! In the second case, block and mempool transactions are created after the //! wallet rescan and notifications are immediately synced, to verify the wallet //! must already have a handler in place for them, and there's no gap after //! rescanning where new transactions in new blocks could be lost. BOOST_FIXTURE_TEST_CASE(CreateWallet, TestChain100Setup) { // Create new wallet with known key and unload it. auto chain = interfaces::MakeChain(m_node, Params()); auto wallet = TestLoadWallet(*chain); CKey key; key.MakeNewKey(true); AddKey(*wallet, key); TestUnloadWallet(std::move(wallet)); // Add log hook to detect AddToWallet events from rescans, blockConnected, // and transactionAddedToMempool notifications int addtx_count = 0; DebugLogHelper addtx_counter("[default wallet] AddToWallet", [&](const std::string *s) { if (s) { ++addtx_count; } return false; }); bool rescan_completed = false; DebugLogHelper rescan_check("[default wallet] Rescan completed", [&](const std::string *s) { if (s) { rescan_completed = true; } return false; }); // Block the queue to prevent the wallet receiving blockConnected and // transactionAddedToMempool notifications, and create block and mempool // transactions paying to the wallet std::promise promise; CallFunctionInValidationInterfaceQueue( [&promise] { promise.get_future().wait(); }); std::string error; m_coinbase_txns.push_back( CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())) .vtx[0]); auto block_tx = TestSimpleSpend(*m_coinbase_txns[0], 0, coinbaseKey, GetScriptForRawPubKey(key.GetPubKey())); m_coinbase_txns.push_back( CreateAndProcessBlock({block_tx}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())) .vtx[0]); auto mempool_tx = TestSimpleSpend(*m_coinbase_txns[1], 0, coinbaseKey, GetScriptForRawPubKey(key.GetPubKey())); BOOST_CHECK( chain->broadcastTransaction(GetConfig(), MakeTransactionRef(mempool_tx), DEFAULT_TRANSACTION_MAXFEE, false, error)); // Reload wallet and make sure new transactions are detected despite events // being blocked wallet = TestLoadWallet(*chain); BOOST_CHECK(rescan_completed); BOOST_CHECK_EQUAL(addtx_count, 2); { LOCK(wallet->cs_wallet); BOOST_CHECK_EQUAL(wallet->mapWallet.count(block_tx.GetId()), 1U); BOOST_CHECK_EQUAL(wallet->mapWallet.count(mempool_tx.GetId()), 1U); } // Unblock notification queue and make sure stale blockConnected and // transactionAddedToMempool events are processed promise.set_value(); SyncWithValidationInterfaceQueue(); BOOST_CHECK_EQUAL(addtx_count, 4); TestUnloadWallet(std::move(wallet)); // Load wallet again, this time creating new block and mempool transactions // paying to the wallet as the wallet finishes loading and syncing the // queue so the events have to be handled immediately. Releasing the wallet // lock during the sync is a little artificial but is needed to avoid a // deadlock during the sync and simulates a new block notification happening // as soon as possible. addtx_count = 0; auto handler = HandleLoadWallet( [&](std::unique_ptr wallet_param) - EXCLUSIVE_LOCKS_REQUIRED(wallet_param->wallet()->cs_wallet) { + EXCLUSIVE_LOCKS_REQUIRED(wallet_param->wallet()->cs_wallet, + cs_wallets) { BOOST_CHECK(rescan_completed); m_coinbase_txns.push_back( CreateAndProcessBlock( {}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())) .vtx[0]); block_tx = TestSimpleSpend(*m_coinbase_txns[2], 0, coinbaseKey, GetScriptForRawPubKey(key.GetPubKey())); m_coinbase_txns.push_back( CreateAndProcessBlock( {block_tx}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())) .vtx[0]); mempool_tx = TestSimpleSpend(*m_coinbase_txns[3], 0, coinbaseKey, GetScriptForRawPubKey(key.GetPubKey())); BOOST_CHECK(chain->broadcastTransaction( GetConfig(), MakeTransactionRef(mempool_tx), DEFAULT_TRANSACTION_MAXFEE, false, error)); + LEAVE_CRITICAL_SECTION(cs_wallets); LEAVE_CRITICAL_SECTION(wallet_param->wallet()->cs_wallet); SyncWithValidationInterfaceQueue(); ENTER_CRITICAL_SECTION(wallet_param->wallet()->cs_wallet); + ENTER_CRITICAL_SECTION(cs_wallets); }); wallet = TestLoadWallet(*chain); BOOST_CHECK_EQUAL(addtx_count, 4); { LOCK(wallet->cs_wallet); BOOST_CHECK_EQUAL(wallet->mapWallet.count(block_tx.GetId()), 1U); BOOST_CHECK_EQUAL(wallet->mapWallet.count(mempool_tx.GetId()), 1U); } TestUnloadWallet(std::move(wallet)); } BOOST_FIXTURE_TEST_CASE(ZapSelectTx, TestChain100Setup) { auto chain = interfaces::MakeChain(m_node, Params()); auto wallet = TestLoadWallet(*chain); CKey key; key.MakeNewKey(true); AddKey(*wallet, key); std::string error; m_coinbase_txns.push_back( CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())) .vtx[0]); auto block_tx = TestSimpleSpend(*m_coinbase_txns[0], 0, coinbaseKey, GetScriptForRawPubKey(key.GetPubKey())); CreateAndProcessBlock({block_tx}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())); SyncWithValidationInterfaceQueue(); { auto block_id = block_tx.GetId(); auto prev_id = m_coinbase_txns[0]->GetId(); LOCK(wallet->cs_wallet); BOOST_CHECK(wallet->HasWalletSpend(prev_id)); BOOST_CHECK_EQUAL(wallet->mapWallet.count(block_id), 1u); std::vector vIdIn{block_id}, vIdOut; BOOST_CHECK_EQUAL(wallet->ZapSelectTx(vIdIn, vIdOut), DBErrors::LOAD_OK); BOOST_CHECK(!wallet->HasWalletSpend(prev_id)); BOOST_CHECK_EQUAL(wallet->mapWallet.count(block_id), 0u); } TestUnloadWallet(std::move(wallet)); } BOOST_AUTO_TEST_SUITE_END() diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index adcca2f6a..37666b739 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -1,5098 +1,5098 @@ // Copyright (c) 2009-2010 Satoshi Nakamoto // Copyright (c) 2009-2019 The Bitcoin Core developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include