Skip to content

Commit 0f204dd

Browse files
author
MarcoFalke
committed
Merge #18727: test: Add CreateWalletFromFile test
7918c1b test: Add CreateWalletFromFile test (Russell Yanofsky) Pull request description: Add unit test calling CreateWalletFromFile, which isn't currently called from other unit tests, with some basic checks to make sure it rescans and registers for notifications correctly. Motivation for this change was to try to write a test that would fail without the early `handleNotifications` call in ef8c6ca from bitcoin/bitcoin#16426, but succeed with it: https://github.com/bitcoin/bitcoin/blob/ef8c6ca60767cac589d98ca57ee33179608ccda8/src/wallet/wallet.cpp#L3978-L3986 However, writing a full test for the race condition that call prevents isn't possible without the locking changes from #16426. So this PR just adds as much test coverage as is possible now. This new test is also useful for bitcoin/bitcoin#15719, since it detects the stale notifications.transactionAddedToMempool notifications that PR eliminates. ACKs for top commit: MarcoFalke: ACK 7918c1b jonatack: ACK 7918c1b Tree-SHA512: 44035aee698ecb722c6039d061d8fac2011e9da0b314e4aff19be1d610b53cacff99016b34d6b84669bb3b61041b2318d9d8e3363658f087802ae4aa36ca17b8
2 parents 978c5a2 + 7918c1b commit 0f204dd

File tree

3 files changed

+131
-5
lines changed

3 files changed

+131
-5
lines changed

src/test/util/logging.cpp

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,13 @@
1111

1212
#include <stdexcept>
1313

14-
DebugLogHelper::DebugLogHelper(std::string message)
15-
: m_message{std::move(message)}
14+
DebugLogHelper::DebugLogHelper(std::string message, MatchFn match)
15+
: m_message{std::move(message)}, m_match(std::move(match))
1616
{
1717
m_print_connection = LogInstance().PushBackCallback(
1818
[this](const std::string& s) {
1919
if (m_found) return;
20-
m_found = s.find(m_message) != std::string::npos;
20+
m_found = s.find(m_message) != std::string::npos && m_match(&s);
2121
});
2222
noui_test_redirect();
2323
}
@@ -26,7 +26,7 @@ void DebugLogHelper::check_found()
2626
{
2727
noui_reconnect();
2828
LogInstance().DeleteCallback(m_print_connection);
29-
if (!m_found) {
29+
if (!m_found && m_match(nullptr)) {
3030
throw std::runtime_error(strprintf("'%s' not found in debug log\n", m_message));
3131
}
3232
}

src/test/util/logging.h

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,22 @@ class DebugLogHelper
1717
bool m_found{false};
1818
std::list<std::function<void(const std::string&)>>::iterator m_print_connection;
1919

20+
//! Custom match checking function.
21+
//!
22+
//! Invoked with pointers to lines containing matching strings, and with
23+
//! null if check_found() is called without any successful match.
24+
//!
25+
//! Can return true to enable default DebugLogHelper behavior of:
26+
//! (1) ending search after first successful match, and
27+
//! (2) raising an error in check_found if no match was found
28+
//! Can return false to do the opposite in either case.
29+
using MatchFn = std::function<bool(const std::string* line)>;
30+
MatchFn m_match;
31+
2032
void check_found();
2133

2234
public:
23-
DebugLogHelper(std::string message);
35+
DebugLogHelper(std::string message, MatchFn match = [](const std::string*){ return true; });
2436
~DebugLogHelper() { check_found(); }
2537
};
2638

src/wallet/test/wallet_tests.cpp

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
#include <wallet/wallet.h>
66

7+
#include <future>
78
#include <memory>
89
#include <stdint.h>
910
#include <vector>
@@ -12,6 +13,7 @@
1213
#include <node/context.h>
1314
#include <policy/policy.h>
1415
#include <rpc/server.h>
16+
#include <test/util/logging.h>
1517
#include <test/util/setup_common.h>
1618
#include <validation.h>
1719
#include <wallet/coincontrol.h>
@@ -26,6 +28,36 @@ extern UniValue importwallet(const JSONRPCRequest& request);
2628

2729
BOOST_FIXTURE_TEST_SUITE(wallet_tests, WalletTestingSetup)
2830

31+
static std::shared_ptr<CWallet> TestLoadWallet(interfaces::Chain& chain)
32+
{
33+
std::string error;
34+
std::vector<std::string> warnings;
35+
auto wallet = CWallet::CreateWalletFromFile(chain, WalletLocation(""), error, warnings);
36+
wallet->postInitProcess();
37+
return wallet;
38+
}
39+
40+
static void TestUnloadWallet(std::shared_ptr<CWallet>&& wallet)
41+
{
42+
SyncWithValidationInterfaceQueue();
43+
wallet->m_chain_notifications_handler.reset();
44+
UnloadWallet(std::move(wallet));
45+
}
46+
47+
static CMutableTransaction TestSimpleSpend(const CTransaction& from, uint32_t index, const CKey& key, const CScript& pubkey)
48+
{
49+
CMutableTransaction mtx;
50+
mtx.vout.push_back({from.vout[index].nValue - DEFAULT_TRANSACTION_MAXFEE, pubkey});
51+
mtx.vin.push_back({CTxIn{from.GetHash(), index}});
52+
FillableSigningProvider keystore;
53+
keystore.AddKey(key);
54+
std::map<COutPoint, Coin> coins;
55+
coins[mtx.vin[0].prevout].out = from.vout[index];
56+
std::map<int, std::string> input_errors;
57+
BOOST_CHECK(SignTransaction(mtx, &keystore, coins, SIGHASH_ALL, input_errors));
58+
return mtx;
59+
}
60+
2961
static void AddKey(CWallet& wallet, const CKey& key)
3062
{
3163
auto spk_man = wallet.GetOrCreateLegacyScriptPubKeyMan();
@@ -658,4 +690,86 @@ BOOST_FIXTURE_TEST_CASE(wallet_descriptor_test, BasicTestingSetup)
658690
BOOST_CHECK_EXCEPTION(vr >> w_desc, std::ios_base::failure, malformed_descriptor);
659691
}
660692

693+
//! Test CreateWalletFromFile function and its behavior handling potential race
694+
//! conditions if it's called the same time an incoming transaction shows up in
695+
//! the mempool or a new block.
696+
//!
697+
//! It isn't possible for a unit test to totally verify there aren't race
698+
//! conditions without hooking into the implementation more, so this test just
699+
//! verifies that new transactions are detected during loading without any
700+
//! notifications at all, to infer that timing of notifications shouldn't
701+
//! matter. The test could be extended to cover other scenarios in the future.
702+
BOOST_FIXTURE_TEST_CASE(CreateWalletFromFile, TestChain100Setup)
703+
{
704+
// Create new wallet with known key and unload it.
705+
auto chain = interfaces::MakeChain(m_node);
706+
auto wallet = TestLoadWallet(*chain);
707+
CKey key;
708+
key.MakeNewKey(true);
709+
AddKey(*wallet, key);
710+
TestUnloadWallet(std::move(wallet));
711+
712+
// Add log hook to detect AddToWallet events from rescans, blockConnected,
713+
// and transactionAddedToMempool notifications
714+
int addtx_count = 0;
715+
DebugLogHelper addtx_counter("[default wallet] AddToWallet", [&](const std::string* s) {
716+
if (s) ++addtx_count;
717+
return false;
718+
});
719+
720+
bool rescan_completed = false;
721+
DebugLogHelper rescan_check("[default wallet] Rescan completed", [&](const std::string* s) {
722+
if (s) {
723+
// For now, just assert that cs_main is being held during the
724+
// rescan, ensuring that a new block couldn't be connected
725+
// that the wallet would miss. After
726+
// https://github.com/bitcoin/bitcoin/pull/16426 when cs_main is no
727+
// longer held, the test can be extended to append a new block here
728+
// and check it's handled correctly.
729+
AssertLockHeld(::cs_main);
730+
rescan_completed = true;
731+
}
732+
return false;
733+
});
734+
735+
// Block the queue to prevent the wallet receiving blockConnected and
736+
// transactionAddedToMempool notifications, and create block and mempool
737+
// transactions paying to the wallet
738+
std::promise<void> promise;
739+
CallFunctionInValidationInterfaceQueue([&promise] {
740+
promise.get_future().wait();
741+
});
742+
std::string error;
743+
m_coinbase_txns.push_back(CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())).vtx[0]);
744+
auto block_tx = TestSimpleSpend(*m_coinbase_txns[0], 0, coinbaseKey, GetScriptForRawPubKey(key.GetPubKey()));
745+
m_coinbase_txns.push_back(CreateAndProcessBlock({block_tx}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())).vtx[0]);
746+
auto mempool_tx = TestSimpleSpend(*m_coinbase_txns[1], 0, coinbaseKey, GetScriptForRawPubKey(key.GetPubKey()));
747+
BOOST_CHECK(chain->broadcastTransaction(MakeTransactionRef(mempool_tx), DEFAULT_TRANSACTION_MAXFEE, false, error));
748+
749+
// Reload wallet and make sure new transactions are detected despite events
750+
// being blocked
751+
wallet = TestLoadWallet(*chain);
752+
BOOST_CHECK(rescan_completed);
753+
BOOST_CHECK_EQUAL(addtx_count, 2);
754+
unsigned int block_tx_time, mempool_tx_time;
755+
{
756+
LOCK(wallet->cs_wallet);
757+
block_tx_time = wallet->mapWallet.at(block_tx.GetHash()).nTimeReceived;
758+
mempool_tx_time = wallet->mapWallet.at(mempool_tx.GetHash()).nTimeReceived;
759+
}
760+
761+
// Unblock notification queue and make sure stale blockConnected and
762+
// transactionAddedToMempool events are processed
763+
promise.set_value();
764+
SyncWithValidationInterfaceQueue();
765+
BOOST_CHECK_EQUAL(addtx_count, 4);
766+
{
767+
LOCK(wallet->cs_wallet);
768+
BOOST_CHECK_EQUAL(block_tx_time, wallet->mapWallet.at(block_tx.GetHash()).nTimeReceived);
769+
BOOST_CHECK_EQUAL(mempool_tx_time, wallet->mapWallet.at(mempool_tx.GetHash()).nTimeReceived);
770+
}
771+
772+
TestUnloadWallet(std::move(wallet));
773+
}
774+
661775
BOOST_AUTO_TEST_SUITE_END()

0 commit comments

Comments
 (0)