/tmp/bitcoin/src/wallet/test/wallet_tests.cpp
Line | Count | Source |
1 | | // Copyright (c) 2012-present The Bitcoin Core developers |
2 | | // Distributed under the MIT software license, see the accompanying |
3 | | // file COPYING or http://www.opensource.org/licenses/mit-license.php. |
4 | | |
5 | | #include <wallet/wallet.h> |
6 | | |
7 | | #include <cstdint> |
8 | | #include <future> |
9 | | #include <memory> |
10 | | #include <vector> |
11 | | |
12 | | #include <addresstype.h> |
13 | | #include <interfaces/chain.h> |
14 | | #include <key_io.h> |
15 | | #include <node/blockstorage.h> |
16 | | #include <node/types.h> |
17 | | #include <policy/policy.h> |
18 | | #include <rpc/server.h> |
19 | | #include <script/solver.h> |
20 | | #include <test/util/common.h> |
21 | | #include <test/util/logging.h> |
22 | | #include <test/util/random.h> |
23 | | #include <test/util/setup_common.h> |
24 | | #include <util/translation.h> |
25 | | #include <validation.h> |
26 | | #include <validationinterface.h> |
27 | | #include <wallet/coincontrol.h> |
28 | | #include <wallet/context.h> |
29 | | #include <wallet/receive.h> |
30 | | #include <wallet/spend.h> |
31 | | #include <wallet/test/util.h> |
32 | | #include <wallet/test/wallet_test_fixture.h> |
33 | | |
34 | | #include <boost/test/unit_test.hpp> |
35 | | #include <univalue.h> |
36 | | |
37 | | using node::MAX_BLOCKFILE_SIZE; |
38 | | |
39 | | namespace wallet { |
40 | | |
41 | | // Ensure that fee levels defined in the wallet are at least as high |
42 | | // as the default levels for node policy. |
43 | | static_assert(DEFAULT_TRANSACTION_MINFEE >= DEFAULT_MIN_RELAY_TX_FEE, "wallet minimum fee is smaller than default relay fee"); |
44 | | static_assert(WALLET_INCREMENTAL_RELAY_FEE >= DEFAULT_INCREMENTAL_RELAY_FEE, "wallet incremental fee is smaller than default incremental relay fee"); |
45 | | |
46 | | BOOST_FIXTURE_TEST_SUITE(wallet_tests, WalletTestingSetup) |
47 | | |
48 | | static CMutableTransaction TestSimpleSpend(const CTransaction& from, uint32_t index, const CKey& key, const CScript& pubkey) |
49 | 5 | { |
50 | 5 | CMutableTransaction mtx; |
51 | 5 | mtx.vout.emplace_back(from.vout[index].nValue - DEFAULT_TRANSACTION_MAXFEE, pubkey); |
52 | 5 | mtx.vin.push_back({CTxIn{from.GetHash(), index}}); |
53 | 5 | FillableSigningProvider keystore; |
54 | 5 | keystore.AddKey(key); |
55 | 5 | std::map<COutPoint, Coin> coins; |
56 | 5 | coins[mtx.vin[0].prevout].out = from.vout[index]; |
57 | 5 | std::map<int, bilingual_str> input_errors; |
58 | 5 | BOOST_CHECK(SignTransaction(mtx, &keystore, coins, {.sighash_type = SIGHASH_ALL}, input_errors)); |
59 | 5 | return mtx; |
60 | 5 | } |
61 | | |
62 | | static void AddKey(CWallet& wallet, const CKey& key) |
63 | 6 | { |
64 | 6 | LOCK(wallet.cs_wallet); |
65 | 6 | FlatSigningProvider provider; |
66 | 6 | std::string error; |
67 | 6 | auto descs = Parse("combo(" + EncodeSecret(key) + ")", provider, error, /* require_checksum=*/ false); |
68 | 6 | assert(descs.size() == 1); |
69 | 6 | auto& desc = descs.at(0); |
70 | 6 | WalletDescriptor w_desc(std::move(desc), 0, 0, 1, 1); |
71 | 6 | Assert(wallet.AddWalletDescriptor(w_desc, provider, "", false)); |
72 | 6 | } |
73 | | |
74 | | BOOST_FIXTURE_TEST_CASE(update_non_range_descriptor, TestingSetup) |
75 | 1 | { |
76 | 1 | CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase()); |
77 | 1 | { |
78 | 1 | LOCK(wallet.cs_wallet); |
79 | 1 | wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS); |
80 | 1 | auto key{GenerateRandomKey()}; |
81 | 1 | auto desc_str{"combo(" + EncodeSecret(key) + ")"}; |
82 | 1 | FlatSigningProvider provider; |
83 | 1 | std::string error; |
84 | 1 | auto descs{Parse(desc_str, provider, error, /* require_checksum=*/ false)}; |
85 | 1 | auto& desc{descs.at(0)}; |
86 | 1 | WalletDescriptor w_desc{std::move(desc), 0, 0, 0, 0}; |
87 | 1 | BOOST_CHECK(wallet.AddWalletDescriptor(w_desc, provider, "", false)); |
88 | | // Wallet should update the non-range descriptor successfully |
89 | 1 | BOOST_CHECK(wallet.AddWalletDescriptor(w_desc, provider, "", false)); |
90 | 1 | } |
91 | 1 | } |
92 | | |
93 | | BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions, TestChain100Setup) |
94 | 1 | { |
95 | | // Cap last block file size, and mine new block in a new block file. |
96 | 1 | CBlockIndex* oldTip = WITH_LOCK(Assert(m_node.chainman)->GetMutex(), return m_node.chainman->ActiveChain().Tip()); |
97 | 1 | WITH_LOCK(::cs_main, m_node.chainman->m_blockman.GetBlockFileInfo(oldTip->GetBlockPos().nFile)->nSize = MAX_BLOCKFILE_SIZE); |
98 | 1 | CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())); |
99 | 1 | CBlockIndex* newTip = WITH_LOCK(Assert(m_node.chainman)->GetMutex(), return m_node.chainman->ActiveChain().Tip()); |
100 | | |
101 | | // Verify ScanForWalletTransactions fails to read an unknown start block. |
102 | 1 | { |
103 | 1 | CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase()); |
104 | 1 | { |
105 | 1 | LOCK(wallet.cs_wallet); |
106 | 1 | LOCK(Assert(m_node.chainman)->GetMutex()); |
107 | 1 | wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS); |
108 | 1 | wallet.SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash()); |
109 | 1 | } |
110 | 1 | AddKey(wallet, coinbaseKey); |
111 | 1 | WalletRescanReserver reserver(wallet); |
112 | 1 | reserver.reserve(); |
113 | 1 | CWallet::ScanResult result = wallet.ScanForWalletTransactions(/*start_block=*/{}, /*start_height=*/0, /*max_height=*/{}, reserver, /*save_progress=*/false); |
114 | 1 | BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::FAILURE); |
115 | 1 | BOOST_CHECK(result.last_failed_block.IsNull()); |
116 | 1 | BOOST_CHECK(result.last_scanned_block.IsNull()); |
117 | 1 | BOOST_CHECK(!result.last_scanned_height); |
118 | 1 | BOOST_CHECK_EQUAL(GetBalance(wallet).m_mine_immature, 0); |
119 | 1 | } |
120 | | |
121 | | // Verify ScanForWalletTransactions picks up transactions in both the old |
122 | | // and new block files. |
123 | 1 | { |
124 | 1 | CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase()); |
125 | 1 | { |
126 | 1 | LOCK(wallet.cs_wallet); |
127 | 1 | LOCK(Assert(m_node.chainman)->GetMutex()); |
128 | 1 | wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS); |
129 | 1 | wallet.SetLastBlockProcessed(newTip->nHeight, newTip->GetBlockHash()); |
130 | 1 | } |
131 | 1 | AddKey(wallet, coinbaseKey); |
132 | 1 | WalletRescanReserver reserver(wallet); |
133 | 1 | std::chrono::steady_clock::time_point fake_time; |
134 | 7 | reserver.setNow([&] { fake_time += 60s; return fake_time; }); |
135 | 1 | reserver.reserve(); |
136 | | |
137 | 1 | { |
138 | 1 | CBlockLocator locator; |
139 | 1 | BOOST_CHECK(WalletBatch{wallet.GetDatabase()}.ReadBestBlock(locator)); |
140 | 1 | BOOST_REQUIRE(!locator.IsNull()); |
141 | 1 | BOOST_CHECK(locator.vHave.front() == newTip->GetBlockHash()); |
142 | 1 | } |
143 | | |
144 | 1 | CWallet::ScanResult result = wallet.ScanForWalletTransactions(/*start_block=*/oldTip->GetBlockHash(), /*start_height=*/oldTip->nHeight, /*max_height=*/{}, reserver, /*save_progress=*/true); |
145 | 1 | BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::SUCCESS); |
146 | 1 | BOOST_CHECK(result.last_failed_block.IsNull()); |
147 | 1 | BOOST_CHECK_EQUAL(result.last_scanned_block, newTip->GetBlockHash()); |
148 | 1 | BOOST_CHECK_EQUAL(*result.last_scanned_height, newTip->nHeight); |
149 | 1 | BOOST_CHECK_EQUAL(GetBalance(wallet).m_mine_immature, 100 * COIN); |
150 | | |
151 | 1 | { |
152 | 1 | CBlockLocator locator; |
153 | 1 | BOOST_CHECK(WalletBatch{wallet.GetDatabase()}.ReadBestBlock(locator)); |
154 | 1 | BOOST_REQUIRE(!locator.IsNull()); |
155 | 1 | BOOST_CHECK(locator.vHave.front() == newTip->GetBlockHash()); |
156 | 1 | } |
157 | 1 | } |
158 | | |
159 | | // Prune the older block file. |
160 | 1 | int file_number; |
161 | 1 | { |
162 | 1 | LOCK(cs_main); |
163 | 1 | file_number = oldTip->GetBlockPos().nFile; |
164 | 1 | Assert(m_node.chainman)->m_blockman.PruneOneBlockFile(file_number); |
165 | 1 | } |
166 | 1 | m_node.chainman->m_blockman.UnlinkPrunedFiles({file_number}); |
167 | | |
168 | | // Verify ScanForWalletTransactions only picks transactions in the new block |
169 | | // file. |
170 | 1 | { |
171 | 1 | CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase()); |
172 | 1 | { |
173 | 1 | LOCK(wallet.cs_wallet); |
174 | 1 | LOCK(Assert(m_node.chainman)->GetMutex()); |
175 | 1 | wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS); |
176 | 1 | wallet.SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash()); |
177 | 1 | } |
178 | 1 | AddKey(wallet, coinbaseKey); |
179 | 1 | WalletRescanReserver reserver(wallet); |
180 | 1 | reserver.reserve(); |
181 | 1 | CWallet::ScanResult result = wallet.ScanForWalletTransactions(/*start_block=*/oldTip->GetBlockHash(), /*start_height=*/oldTip->nHeight, /*max_height=*/{}, reserver, /*save_progress=*/false); |
182 | 1 | BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::FAILURE); |
183 | 1 | BOOST_CHECK_EQUAL(result.last_failed_block, oldTip->GetBlockHash()); |
184 | 1 | BOOST_CHECK_EQUAL(result.last_scanned_block, newTip->GetBlockHash()); |
185 | 1 | BOOST_CHECK_EQUAL(*result.last_scanned_height, newTip->nHeight); |
186 | 1 | BOOST_CHECK_EQUAL(GetBalance(wallet).m_mine_immature, 50 * COIN); |
187 | 1 | } |
188 | | |
189 | | // Prune the remaining block file. |
190 | 1 | { |
191 | 1 | LOCK(cs_main); |
192 | 1 | file_number = newTip->GetBlockPos().nFile; |
193 | 1 | Assert(m_node.chainman)->m_blockman.PruneOneBlockFile(file_number); |
194 | 1 | } |
195 | 1 | m_node.chainman->m_blockman.UnlinkPrunedFiles({file_number}); |
196 | | |
197 | | // Verify ScanForWalletTransactions scans no blocks. |
198 | 1 | { |
199 | 1 | CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase()); |
200 | 1 | { |
201 | 1 | LOCK(wallet.cs_wallet); |
202 | 1 | LOCK(Assert(m_node.chainman)->GetMutex()); |
203 | 1 | wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS); |
204 | 1 | wallet.SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash()); |
205 | 1 | } |
206 | 1 | AddKey(wallet, coinbaseKey); |
207 | 1 | WalletRescanReserver reserver(wallet); |
208 | 1 | reserver.reserve(); |
209 | 1 | CWallet::ScanResult result = wallet.ScanForWalletTransactions(/*start_block=*/oldTip->GetBlockHash(), /*start_height=*/oldTip->nHeight, /*max_height=*/{}, reserver, /*save_progress=*/false); |
210 | 1 | BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::FAILURE); |
211 | 1 | BOOST_CHECK_EQUAL(result.last_failed_block, newTip->GetBlockHash()); |
212 | 1 | BOOST_CHECK(result.last_scanned_block.IsNull()); |
213 | 1 | BOOST_CHECK(!result.last_scanned_height); |
214 | 1 | BOOST_CHECK_EQUAL(GetBalance(wallet).m_mine_immature, 0); |
215 | 1 | } |
216 | 1 | } |
217 | | |
218 | | BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions_abort, TestChain100Setup) |
219 | 1 | { |
220 | 1 | CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase()); |
221 | 1 | uint256 genesis_hash; |
222 | 1 | { |
223 | 1 | LOCK(wallet.cs_wallet); |
224 | 1 | LOCK(Assert(m_node.chainman)->GetMutex()); |
225 | 1 | wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS); |
226 | 1 | wallet.SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash()); |
227 | 1 | genesis_hash = m_node.chainman->ActiveChain().Genesis()->GetBlockHash(); |
228 | 1 | } |
229 | | |
230 | | // An abort requested while no rescan is held is stale and must |
231 | | // not cancel a later scan. |
232 | 1 | wallet.AbortRescan(); |
233 | 1 | WalletRescanReserver reserver(wallet); |
234 | 1 | BOOST_CHECK(reserver.reserve()); |
235 | 1 | BOOST_CHECK(!wallet.IsAbortingRescan()); |
236 | | |
237 | | // An abort requested after the reservation but before the scan starts |
238 | | // (e.g. while importdescriptors is still deriving keys) must cancel the |
239 | | // scan. |
240 | 1 | wallet.AbortRescan(); |
241 | 1 | CWallet::ScanResult result = wallet.ScanForWalletTransactions(genesis_hash, /*start_height=*/0, /*max_height=*/{}, reserver, /*save_progress=*/false); |
242 | 1 | BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::USER_ABORT); |
243 | 1 | BOOST_CHECK(result.last_scanned_block.IsNull()); |
244 | 1 | BOOST_CHECK(!result.last_scanned_height); |
245 | 1 | BOOST_CHECK(result.last_failed_block.IsNull()); |
246 | 1 | } |
247 | | |
248 | | // This test verifies that wallet settings can be added and removed |
249 | | // concurrently, ensuring no race conditions occur during either process. |
250 | | BOOST_FIXTURE_TEST_CASE(write_wallet_settings_concurrently, TestingSetup) |
251 | 1 | { |
252 | 1 | auto chain = m_node.chain.get(); |
253 | 1 | const auto NUM_WALLETS{5}; |
254 | | |
255 | | // Since we're counting the number of wallets, ensure we start without any. |
256 | 1 | BOOST_REQUIRE(chain->getRwSetting("wallet").isNull()); |
257 | | |
258 | 2 | const auto& check_concurrent_wallet = [&](const auto& settings_function, int num_expected_wallets) { |
259 | 2 | std::vector<std::thread> threads; |
260 | 2 | threads.reserve(NUM_WALLETS); |
261 | 12 | for (auto i{0}; i < NUM_WALLETS; ++i) threads.emplace_back(settings_function, i); |
262 | 10 | for (auto& t : threads) t.join(); |
263 | | |
264 | 2 | auto wallets = chain->getRwSetting("wallet"); |
265 | 2 | BOOST_CHECK_EQUAL(wallets.getValues().size(), num_expected_wallets); |
266 | 2 | }; wallet_tests.cpp:_ZZN6wallet12wallet_tests34write_wallet_settings_concurrently11test_methodEvENK3$_1clIZNS1_11test_methodEvE3$_0EEDaRKT_i Line | Count | Source | 258 | 1 | const auto& check_concurrent_wallet = [&](const auto& settings_function, int num_expected_wallets) { | 259 | 1 | std::vector<std::thread> threads; | 260 | 1 | threads.reserve(NUM_WALLETS); | 261 | 6 | for (auto i{0}; i < NUM_WALLETS; ++i) threads.emplace_back(settings_function, i); | 262 | 5 | for (auto& t : threads) t.join(); | 263 | | | 264 | 1 | auto wallets = chain->getRwSetting("wallet"); | 265 | | BOOST_CHECK_EQUAL(wallets.getValues().size(), num_expected_wallets); | 266 | 1 | }; |
wallet_tests.cpp:_ZZN6wallet12wallet_tests34write_wallet_settings_concurrently11test_methodEvENK3$_1clIZNS1_11test_methodEvE3$_2EEDaRKT_i Line | Count | Source | 258 | 1 | const auto& check_concurrent_wallet = [&](const auto& settings_function, int num_expected_wallets) { | 259 | 1 | std::vector<std::thread> threads; | 260 | 1 | threads.reserve(NUM_WALLETS); | 261 | 6 | for (auto i{0}; i < NUM_WALLETS; ++i) threads.emplace_back(settings_function, i); | 262 | 5 | for (auto& t : threads) t.join(); | 263 | | | 264 | 1 | auto wallets = chain->getRwSetting("wallet"); | 265 | | BOOST_CHECK_EQUAL(wallets.getValues().size(), num_expected_wallets); | 266 | 1 | }; |
|
267 | | |
268 | | // Add NUM_WALLETS wallets concurrently, ensure we end up with NUM_WALLETS stored. |
269 | 5 | check_concurrent_wallet([&chain](int i) { |
270 | 5 | Assert(AddWalletSetting(*chain, strprintf("wallet_%d", i))); |
271 | 5 | }, |
272 | 1 | /*num_expected_wallets=*/NUM_WALLETS); |
273 | | |
274 | | // Remove NUM_WALLETS wallets concurrently, ensure we end up with 0 wallets. |
275 | 5 | check_concurrent_wallet([&chain](int i) { |
276 | 5 | Assert(RemoveWalletSetting(*chain, strprintf("wallet_%d", i))); |
277 | 5 | }, |
278 | 1 | /*num_expected_wallets=*/0); |
279 | 1 | } |
280 | | |
281 | | static int64_t AddTx(ChainstateManager& chainman, CWallet& wallet, uint32_t lockTime, std::chrono::seconds mock_time, int64_t blockTime) |
282 | 6 | { |
283 | 6 | CMutableTransaction tx; |
284 | 6 | TxState state = TxStateInactive{}; |
285 | 6 | tx.nLockTime = lockTime; |
286 | 6 | FakeNodeClock clock{mock_time}; |
287 | 6 | CBlockIndex* block = nullptr; |
288 | 6 | if (blockTime > 0) { |
289 | 5 | LOCK(cs_main); |
290 | 5 | auto inserted = chainman.BlockIndex().emplace(std::piecewise_construct, std::make_tuple(GetRandHash()), std::make_tuple()); |
291 | 5 | assert(inserted.second); |
292 | 5 | const uint256& hash = inserted.first->first; |
293 | 5 | block = &inserted.first->second; |
294 | 5 | block->nTime = blockTime; |
295 | 5 | block->phashBlock = &hash; |
296 | 5 | state = TxStateConfirmed{hash, block->nHeight, /*index=*/0}; |
297 | 5 | } |
298 | 6 | return wallet.AddToWallet(MakeTransactionRef(tx), state, [&](CWalletTx& wtx, bool /* new_tx */) { |
299 | | // Assign wtx.m_state to simplify test and avoid the need to simulate |
300 | | // reorg events. Without this, AddToWallet asserts false when the same |
301 | | // transaction is confirmed in different blocks. |
302 | 6 | wtx.m_state = state; |
303 | 6 | return true; |
304 | 6 | })->nTimeSmart; |
305 | 6 | } |
306 | | |
307 | | // Simple test to verify assignment of CWalletTx::nSmartTime value. Could be |
308 | | // expanded to cover more corner cases of smart time logic. |
309 | | BOOST_AUTO_TEST_CASE(ComputeTimeSmart) |
310 | 1 | { |
311 | | // New transaction should use clock time if lower than block time. |
312 | 1 | BOOST_CHECK_EQUAL(AddTx(*m_node.chainman, m_wallet, 1, 100s, 120), 100); |
313 | | |
314 | | // Test that updating existing transaction does not change smart time. |
315 | 1 | BOOST_CHECK_EQUAL(AddTx(*m_node.chainman, m_wallet, 1, 200s, 220), 100); |
316 | | |
317 | | // New transaction should use clock time if there's no block time. |
318 | 1 | BOOST_CHECK_EQUAL(AddTx(*m_node.chainman, m_wallet, 2, 300s, 0), 300); |
319 | | |
320 | | // New transaction should use block time if lower than clock time. |
321 | 1 | BOOST_CHECK_EQUAL(AddTx(*m_node.chainman, m_wallet, 3, 420s, 400), 400); |
322 | | |
323 | | // New transaction should use latest entry time if higher than |
324 | | // min(block time, clock time). |
325 | 1 | BOOST_CHECK_EQUAL(AddTx(*m_node.chainman, m_wallet, 4, 500s, 390), 400); |
326 | | |
327 | | // If there are future entries, new transaction should use time of the |
328 | | // newest entry that is no more than 300 seconds ahead of the clock time. |
329 | 1 | BOOST_CHECK_EQUAL(AddTx(*m_node.chainman, m_wallet, 5, 50s, 600), 300); |
330 | 1 | } |
331 | | |
332 | | void TestLoadWallet(const std::string& name, DatabaseFormat format, std::function<void(std::shared_ptr<CWallet>)> f) |
333 | 3 | { |
334 | 3 | node::NodeContext node; |
335 | 3 | auto chain{interfaces::MakeChain(node)}; |
336 | 3 | DatabaseOptions options; |
337 | 3 | options.require_format = format; |
338 | 3 | DatabaseStatus status; |
339 | 3 | bilingual_str error; |
340 | 3 | std::vector<bilingual_str> warnings; |
341 | 3 | auto database{MakeWalletDatabase(name, options, status, error)}; |
342 | 3 | auto wallet{std::make_shared<CWallet>(chain.get(), "", std::move(database))}; |
343 | 3 | BOOST_CHECK_EQUAL(wallet->PopulateWalletFromDB(error, warnings), DBErrors::LOAD_OK); |
344 | 3 | WITH_LOCK(wallet->cs_wallet, f(wallet)); |
345 | 3 | } |
346 | | |
347 | | BOOST_FIXTURE_TEST_CASE(LoadReceiveRequests, TestingSetup) |
348 | 1 | { |
349 | 1 | for (DatabaseFormat format : DATABASE_FORMATS) { |
350 | 1 | const std::string name{strprintf("receive-requests-%i", format)}; |
351 | 1 | TestLoadWallet(name, format, [](std::shared_ptr<CWallet> wallet) EXCLUSIVE_LOCKS_REQUIRED(wallet->cs_wallet) { |
352 | 1 | BOOST_CHECK(!wallet->IsAddressPreviouslySpent(PKHash())); |
353 | 1 | WalletBatch batch{wallet->GetDatabase()}; |
354 | 1 | BOOST_CHECK(batch.WriteAddressPreviouslySpent(PKHash(), true)); |
355 | 1 | BOOST_CHECK(batch.WriteAddressPreviouslySpent(ScriptHash(), true)); |
356 | 1 | BOOST_CHECK(wallet->SetAddressReceiveRequest(batch, PKHash(), "0", "val_rr00")); |
357 | 1 | BOOST_CHECK(wallet->EraseAddressReceiveRequest(batch, PKHash(), "0")); |
358 | 1 | BOOST_CHECK(wallet->SetAddressReceiveRequest(batch, PKHash(), "1", "val_rr10")); |
359 | 1 | BOOST_CHECK(wallet->SetAddressReceiveRequest(batch, PKHash(), "1", "val_rr11")); |
360 | 1 | BOOST_CHECK(wallet->SetAddressReceiveRequest(batch, ScriptHash(), "2", "val_rr20")); |
361 | 1 | }); |
362 | 1 | TestLoadWallet(name, format, [](std::shared_ptr<CWallet> wallet) EXCLUSIVE_LOCKS_REQUIRED(wallet->cs_wallet) { |
363 | 1 | BOOST_CHECK(wallet->IsAddressPreviouslySpent(PKHash())); |
364 | 1 | BOOST_CHECK(wallet->IsAddressPreviouslySpent(ScriptHash())); |
365 | 1 | auto requests = wallet->GetAddressReceiveRequests(); |
366 | 1 | auto erequests = {"val_rr11", "val_rr20"}; |
367 | 1 | BOOST_CHECK_EQUAL_COLLECTIONS(requests.begin(), requests.end(), std::begin(erequests), std::end(erequests)); |
368 | 1 | RunWithinTxn(wallet->GetDatabase(), /*process_desc=*/"test", [](WalletBatch& batch){ |
369 | 1 | BOOST_CHECK(batch.WriteAddressPreviouslySpent(PKHash(), false)); |
370 | 1 | BOOST_CHECK(batch.EraseAddressData(ScriptHash())); |
371 | 1 | return true; |
372 | 1 | }); |
373 | 1 | }); |
374 | 1 | TestLoadWallet(name, format, [](std::shared_ptr<CWallet> wallet) EXCLUSIVE_LOCKS_REQUIRED(wallet->cs_wallet) { |
375 | 1 | BOOST_CHECK(!wallet->IsAddressPreviouslySpent(PKHash())); |
376 | 1 | BOOST_CHECK(!wallet->IsAddressPreviouslySpent(ScriptHash())); |
377 | 1 | auto requests = wallet->GetAddressReceiveRequests(); |
378 | 1 | auto erequests = {"val_rr11"}; |
379 | 1 | BOOST_CHECK_EQUAL_COLLECTIONS(requests.begin(), requests.end(), std::begin(erequests), std::end(erequests)); |
380 | 1 | }); |
381 | 1 | } |
382 | 1 | } |
383 | | |
384 | | class ListCoinsTestingSetup : public TestChain100Setup |
385 | | { |
386 | | public: |
387 | | ListCoinsTestingSetup() |
388 | 2 | { |
389 | 2 | CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())); |
390 | 2 | wallet = CreateSyncedWallet(*m_node.chain, WITH_LOCK(Assert(m_node.chainman)->GetMutex(), return m_node.chainman->ActiveChain()), coinbaseKey); |
391 | 2 | } |
392 | | |
393 | | ~ListCoinsTestingSetup() |
394 | 2 | { |
395 | 2 | wallet.reset(); |
396 | 2 | } |
397 | | |
398 | | CWalletTx& AddTx(CRecipient recipient) |
399 | 5 | { |
400 | 5 | CTransactionRef tx; |
401 | 5 | CCoinControl dummy; |
402 | 5 | { |
403 | 5 | auto res = CreateTransaction(*wallet, {recipient}, /*change_pos=*/std::nullopt, dummy); |
404 | 5 | BOOST_CHECK(res); |
405 | 5 | tx = res->tx; |
406 | 5 | } |
407 | 5 | wallet->CommitTransaction(tx); |
408 | 5 | CMutableTransaction blocktx; |
409 | 5 | { |
410 | 5 | LOCK(wallet->cs_wallet); |
411 | 5 | blocktx = CMutableTransaction(*wallet->mapWallet.at(tx->GetHash()).GetTx()); |
412 | 5 | } |
413 | 5 | CreateAndProcessBlock({CMutableTransaction(blocktx)}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())); |
414 | | |
415 | 5 | LOCK(wallet->cs_wallet); |
416 | 5 | LOCK(Assert(m_node.chainman)->GetMutex()); |
417 | 5 | wallet->SetLastBlockProcessed(wallet->GetLastBlockHeight() + 1, m_node.chainman->ActiveChain().Tip()->GetBlockHash()); |
418 | 5 | auto it = wallet->mapWallet.find(tx->GetHash()); |
419 | 5 | BOOST_CHECK(it != wallet->mapWallet.end()); |
420 | 5 | it->second.m_state = TxStateConfirmed{m_node.chainman->ActiveChain().Tip()->GetBlockHash(), m_node.chainman->ActiveChain().Height(), /*index=*/1}; |
421 | 5 | return it->second; |
422 | 5 | } |
423 | | |
424 | | std::unique_ptr<CWallet> wallet; |
425 | | }; |
426 | | |
427 | | BOOST_FIXTURE_TEST_CASE(ListCoinsTest, ListCoinsTestingSetup) |
428 | 1 | { |
429 | 1 | std::string coinbaseAddress = coinbaseKey.GetPubKey().GetID().ToString(); |
430 | | |
431 | | // Confirm ListCoins initially returns 1 coin grouped under coinbaseKey |
432 | | // address. |
433 | 1 | std::map<CTxDestination, std::vector<COutput>> list; |
434 | 1 | { |
435 | 1 | LOCK(wallet->cs_wallet); |
436 | 1 | list = ListCoins(*wallet); |
437 | 1 | } |
438 | 1 | BOOST_CHECK_EQUAL(list.size(), 1U); |
439 | 1 | BOOST_CHECK_EQUAL(std::get<PKHash>(list.begin()->first).ToString(), coinbaseAddress); |
440 | 1 | BOOST_CHECK_EQUAL(list.begin()->second.size(), 1U); |
441 | | |
442 | | // Check initial balance from one mature coinbase transaction. |
443 | 1 | BOOST_CHECK_EQUAL(50 * COIN, WITH_LOCK(wallet->cs_wallet, return AvailableCoins(*wallet).GetTotalAmount())); |
444 | | |
445 | | // Add a transaction creating a change address, and confirm ListCoins still |
446 | | // returns the coin associated with the change address underneath the |
447 | | // coinbaseKey pubkey, even though the change address has a different |
448 | | // pubkey. |
449 | 1 | AddTx(CRecipient{PubKeyDestination{{}}, 1 * COIN, /*subtract_fee=*/false}); |
450 | 1 | { |
451 | 1 | LOCK(wallet->cs_wallet); |
452 | 1 | list = ListCoins(*wallet); |
453 | 1 | } |
454 | 1 | BOOST_CHECK_EQUAL(list.size(), 1U); |
455 | 1 | BOOST_CHECK_EQUAL(std::get<PKHash>(list.begin()->first).ToString(), coinbaseAddress); |
456 | 1 | BOOST_CHECK_EQUAL(list.begin()->second.size(), 2U); |
457 | | |
458 | | // Lock both coins. Confirm number of available coins drops to 0. |
459 | 1 | { |
460 | 1 | LOCK(wallet->cs_wallet); |
461 | 1 | BOOST_CHECK_EQUAL(AvailableCoins(*wallet).Size(), 2U); |
462 | 1 | } |
463 | 1 | for (const auto& group : list) { |
464 | 2 | for (const auto& coin : group.second) { |
465 | 2 | LOCK(wallet->cs_wallet); |
466 | 2 | wallet->LockCoin(coin.outpoint, /*persist=*/false); |
467 | 2 | } |
468 | 1 | } |
469 | 1 | { |
470 | 1 | LOCK(wallet->cs_wallet); |
471 | 1 | BOOST_CHECK_EQUAL(AvailableCoins(*wallet).Size(), 0U); |
472 | 1 | } |
473 | | // Confirm ListCoins still returns same result as before, despite coins |
474 | | // being locked. |
475 | 1 | { |
476 | 1 | LOCK(wallet->cs_wallet); |
477 | 1 | list = ListCoins(*wallet); |
478 | 1 | } |
479 | 1 | BOOST_CHECK_EQUAL(list.size(), 1U); |
480 | 1 | BOOST_CHECK_EQUAL(std::get<PKHash>(list.begin()->first).ToString(), coinbaseAddress); |
481 | 1 | BOOST_CHECK_EQUAL(list.begin()->second.size(), 2U); |
482 | 1 | } |
483 | | |
484 | | void TestCoinsResult(ListCoinsTest& context, OutputType out_type, CAmount amount, |
485 | | std::map<OutputType, size_t>& expected_coins_sizes) |
486 | 4 | { |
487 | 4 | LOCK(context.wallet->cs_wallet); |
488 | 4 | util::Result<CTxDestination> dest = Assert(context.wallet->GetNewDestination(out_type, "")); |
489 | 4 | CWalletTx& wtx = context.AddTx(CRecipient{*dest, amount, /*fSubtractFeeFromAmount=*/true}); |
490 | 4 | CoinFilterParams filter; |
491 | 4 | filter.skip_locked = false; |
492 | 4 | CoinsResult available_coins = AvailableCoins(*context.wallet, nullptr, std::nullopt, filter); |
493 | | // Lock outputs so they are not spent in follow-up transactions |
494 | 12 | for (uint32_t i = 0; i < wtx.GetTx()->vout.size(); i++) context.wallet->LockCoin({wtx.GetHash(), i}, /*persist=*/false); |
495 | 4 | for (const auto& [type, size] : expected_coins_sizes) BOOST_CHECK_EQUAL(size, available_coins.coins[type].size()); |
496 | 4 | } |
497 | | |
498 | | BOOST_FIXTURE_TEST_CASE(BasicOutputTypesTest, ListCoinsTest) |
499 | 1 | { |
500 | 1 | std::map<OutputType, size_t> expected_coins_sizes; |
501 | 4 | for (const auto& out_type : OUTPUT_TYPES) { expected_coins_sizes[out_type] = 0U; } |
502 | | |
503 | | // Verify our wallet has one usable coinbase UTXO before starting |
504 | | // This UTXO is a P2PK, so it should show up in the Other bucket |
505 | 1 | expected_coins_sizes[OutputType::UNKNOWN] = 1U; |
506 | 1 | CoinsResult available_coins = WITH_LOCK(wallet->cs_wallet, return AvailableCoins(*wallet)); |
507 | 1 | BOOST_CHECK_EQUAL(available_coins.Size(), expected_coins_sizes[OutputType::UNKNOWN]); |
508 | 1 | BOOST_CHECK_EQUAL(available_coins.coins[OutputType::UNKNOWN].size(), expected_coins_sizes[OutputType::UNKNOWN]); |
509 | | |
510 | | // We will create a self transfer for each of the OutputTypes and |
511 | | // verify it is put in the correct bucket after running GetAvailablecoins |
512 | | // |
513 | | // For each OutputType, We expect 2 UTXOs in our wallet following the self transfer: |
514 | | // 1. One UTXO as the recipient |
515 | | // 2. One UTXO from the change, due to payment address matching logic |
516 | | |
517 | 4 | for (const auto& out_type : OUTPUT_TYPES) { |
518 | 4 | if (out_type == OutputType::UNKNOWN) continue; |
519 | 4 | expected_coins_sizes[out_type] = 2U; |
520 | 4 | TestCoinsResult(*this, out_type, 1 * COIN, expected_coins_sizes); |
521 | 4 | } |
522 | 1 | } |
523 | | |
524 | | BOOST_FIXTURE_TEST_CASE(wallet_disableprivkeys, TestChain100Setup) |
525 | 1 | { |
526 | 1 | const std::shared_ptr<CWallet> wallet = std::make_shared<CWallet>(m_node.chain.get(), "", CreateMockableWalletDatabase()); |
527 | 1 | LOCK(wallet->cs_wallet); |
528 | 1 | wallet->SetWalletFlag(WALLET_FLAG_DESCRIPTORS); |
529 | 1 | wallet->SetWalletFlag(WALLET_FLAG_DISABLE_PRIVATE_KEYS); |
530 | 1 | BOOST_CHECK(!wallet->GetNewDestination(OutputType::BECH32, "")); |
531 | 1 | } |
532 | | |
533 | | // Explicit calculation which is used to test the wallet constant |
534 | | // We get the same virtual size due to rounding(weight/4) for both use_max_sig values |
535 | | static size_t CalculateNestedKeyhashInputSize(bool use_max_sig) |
536 | 2 | { |
537 | | // Generate ephemeral valid pubkey |
538 | 2 | CKey key = GenerateRandomKey(); |
539 | 2 | CPubKey pubkey = key.GetPubKey(); |
540 | | |
541 | | // Generate pubkey hash |
542 | 2 | uint160 key_hash(Hash160(pubkey)); |
543 | | |
544 | | // Create inner-script to enter into keystore. Key hash can't be 0... |
545 | 2 | CScript inner_script = CScript() << OP_0 << std::vector<unsigned char>(key_hash.begin(), key_hash.end()); |
546 | | |
547 | | // Create outer P2SH script for the output |
548 | 2 | uint160 script_id(Hash160(inner_script)); |
549 | 2 | CScript script_pubkey = CScript() << OP_HASH160 << std::vector<unsigned char>(script_id.begin(), script_id.end()) << OP_EQUAL; |
550 | | |
551 | | // Add inner-script to key store and key to watchonly |
552 | 2 | FillableSigningProvider keystore; |
553 | 2 | keystore.AddCScript(inner_script); |
554 | 2 | keystore.AddKeyPubKey(key, pubkey); |
555 | | |
556 | | // Fill in dummy signatures for fee calculation. |
557 | 2 | SignatureData sig_data; |
558 | | |
559 | 2 | if (!ProduceSignature(keystore, use_max_sig ? DUMMY_MAXIMUM_SIGNATURE_CREATOR : DUMMY_SIGNATURE_CREATOR, script_pubkey, sig_data)) { |
560 | | // We're hand-feeding it correct arguments; shouldn't happen |
561 | 0 | assert(false); |
562 | 0 | } |
563 | | |
564 | 2 | CTxIn tx_in; |
565 | 2 | UpdateInput(tx_in, sig_data); |
566 | 2 | return (size_t)GetVirtualTransactionInputSize(tx_in); |
567 | 2 | } |
568 | | |
569 | | BOOST_FIXTURE_TEST_CASE(dummy_input_size_test, TestChain100Setup) |
570 | 1 | { |
571 | 1 | BOOST_CHECK_EQUAL(CalculateNestedKeyhashInputSize(false), DUMMY_NESTED_P2WPKH_INPUT_SIZE); |
572 | 1 | BOOST_CHECK_EQUAL(CalculateNestedKeyhashInputSize(true), DUMMY_NESTED_P2WPKH_INPUT_SIZE); |
573 | 1 | } |
574 | | |
575 | | bool malformed_descriptor(std::ios_base::failure e) |
576 | 1 | { |
577 | 1 | std::string s(e.what()); |
578 | 1 | return s.find("Missing checksum") != std::string::npos; |
579 | 1 | } |
580 | | |
581 | | BOOST_FIXTURE_TEST_CASE(wallet_descriptor_test, BasicTestingSetup) |
582 | 1 | { |
583 | 1 | std::vector<unsigned char> malformed_record; |
584 | 1 | VectorWriter vw{malformed_record, 0}; |
585 | 1 | vw << std::string("notadescriptor"); |
586 | 1 | vw << uint64_t{0}; |
587 | 1 | vw << int32_t{0}; |
588 | 1 | vw << int32_t{0}; |
589 | 1 | vw << int32_t{1}; |
590 | | |
591 | 1 | SpanReader vr{malformed_record}; |
592 | 1 | WalletDescriptor w_desc; |
593 | 1 | BOOST_CHECK_EXCEPTION(vr >> w_desc, std::ios_base::failure, malformed_descriptor); |
594 | 1 | } |
595 | | |
596 | | //! Test CWallet::CreateNew() and its behavior handling potential race |
597 | | //! conditions if it's called the same time an incoming transaction shows up in |
598 | | //! the mempool or a new block. |
599 | | //! |
600 | | //! It isn't possible to verify there aren't race condition in every case, so |
601 | | //! this test just checks two specific cases and ensures that timing of |
602 | | //! notifications in these cases doesn't prevent the wallet from detecting |
603 | | //! transactions. |
604 | | //! |
605 | | //! In the first case, block and mempool transactions are created before the |
606 | | //! wallet is loaded, but notifications about these transactions are delayed |
607 | | //! until after it is loaded. The notifications are superfluous in this case, so |
608 | | //! the test verifies the transactions are detected before they arrive. |
609 | | //! |
610 | | //! In the second case, block and mempool transactions are created after the |
611 | | //! wallet rescan and notifications are immediately synced, to verify the wallet |
612 | | //! must already have a handler in place for them, and there's no gap after |
613 | | //! rescanning where new transactions in new blocks could be lost. |
614 | | BOOST_FIXTURE_TEST_CASE(CreateWallet, TestChain100Setup) |
615 | 1 | { |
616 | 1 | m_args.ForceSetArg("-unsafesqlitesync", "1"); |
617 | | // Create new wallet with known key and unload it. |
618 | 1 | WalletContext context; |
619 | 1 | context.args = &m_args; |
620 | 1 | context.chain = m_node.chain.get(); |
621 | 1 | auto wallet = TestCreateWallet(context); |
622 | 1 | CKey key = GenerateRandomKey(); |
623 | 1 | AddKey(*wallet, key); |
624 | 1 | TestUnloadWallet(std::move(wallet)); |
625 | | |
626 | | |
627 | | // Add log hook to detect AddToWallet events from rescans, blockConnected, |
628 | | // and transactionAddedToMempool notifications |
629 | 1 | int addtx_count = 0; |
630 | 10 | DebugLogHelper addtx_counter("[default wallet] AddToWallet", [&](const std::string* s) { |
631 | 10 | if (s) ++addtx_count; |
632 | 10 | return false; |
633 | 10 | }); |
634 | | |
635 | | |
636 | 1 | bool rescan_completed = false; |
637 | 2 | DebugLogHelper rescan_check("[default wallet] Rescan completed", [&](const std::string* s) { |
638 | 2 | if (s) rescan_completed = true; |
639 | 2 | return false; |
640 | 2 | }); |
641 | | |
642 | | |
643 | | // Block the queue to prevent the wallet receiving blockConnected and |
644 | | // transactionAddedToMempool notifications, and create block and mempool |
645 | | // transactions paying to the wallet |
646 | 1 | std::promise<void> promise; |
647 | 1 | m_node.validation_signals->CallFunctionInValidationInterfaceQueue([&promise] { |
648 | 1 | promise.get_future().wait(); |
649 | 1 | }); |
650 | 1 | std::string error; |
651 | 1 | m_coinbase_txns.push_back(CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())).vtx[0]); |
652 | 1 | auto block_tx = TestSimpleSpend(*m_coinbase_txns[0], 0, coinbaseKey, GetScriptForRawPubKey(key.GetPubKey())); |
653 | 1 | m_coinbase_txns.push_back(CreateAndProcessBlock({block_tx}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())).vtx[0]); |
654 | 1 | auto mempool_tx = TestSimpleSpend(*m_coinbase_txns[1], 0, coinbaseKey, GetScriptForRawPubKey(key.GetPubKey())); |
655 | 1 | BOOST_CHECK(m_node.chain->broadcastTransaction(MakeTransactionRef(mempool_tx), DEFAULT_TRANSACTION_MAXFEE, node::TxBroadcast::MEMPOOL_NO_BROADCAST, error)); |
656 | | |
657 | | |
658 | | // Reload wallet and make sure new transactions are detected despite events |
659 | | // being blocked |
660 | | // Loading will also ask for current mempool transactions |
661 | 1 | wallet = TestLoadWallet(context); |
662 | 1 | BOOST_CHECK(rescan_completed); |
663 | | // AddToWallet events for block_tx and mempool_tx (x2) |
664 | 1 | BOOST_CHECK_EQUAL(addtx_count, 3); |
665 | 1 | { |
666 | 1 | LOCK(wallet->cs_wallet); |
667 | 1 | BOOST_CHECK(wallet->mapWallet.contains(block_tx.GetHash())); |
668 | 1 | BOOST_CHECK(wallet->mapWallet.contains(mempool_tx.GetHash())); |
669 | 1 | } |
670 | | |
671 | | |
672 | | // Unblock notification queue and make sure stale blockConnected and |
673 | | // transactionAddedToMempool events are processed |
674 | 1 | promise.set_value(); |
675 | 1 | m_node.validation_signals->SyncWithValidationInterfaceQueue(); |
676 | | // AddToWallet events for block_tx and mempool_tx events are counted a |
677 | | // second time as the notification queue is processed |
678 | 1 | BOOST_CHECK_EQUAL(addtx_count, 5); |
679 | | |
680 | | |
681 | 1 | TestUnloadWallet(std::move(wallet)); |
682 | | |
683 | | |
684 | | // Load wallet again, this time creating new block and mempool transactions |
685 | | // paying to the wallet as the wallet finishes loading and syncing the |
686 | | // queue so the events have to be handled immediately. Releasing the wallet |
687 | | // lock during the sync is a little artificial but is needed to avoid a |
688 | | // deadlock during the sync and simulates a new block notification happening |
689 | | // as soon as possible. |
690 | 1 | addtx_count = 0; |
691 | 1 | auto handler = HandleLoadWallet(context, [&](std::unique_ptr<interfaces::Wallet> wallet) { |
692 | 1 | BOOST_CHECK(rescan_completed); |
693 | 1 | m_coinbase_txns.push_back(CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())).vtx[0]); |
694 | 1 | block_tx = TestSimpleSpend(*m_coinbase_txns[2], 0, coinbaseKey, GetScriptForRawPubKey(key.GetPubKey())); |
695 | 1 | m_coinbase_txns.push_back(CreateAndProcessBlock({block_tx}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())).vtx[0]); |
696 | 1 | mempool_tx = TestSimpleSpend(*m_coinbase_txns[3], 0, coinbaseKey, GetScriptForRawPubKey(key.GetPubKey())); |
697 | 1 | BOOST_CHECK(m_node.chain->broadcastTransaction(MakeTransactionRef(mempool_tx), DEFAULT_TRANSACTION_MAXFEE, node::TxBroadcast::MEMPOOL_NO_BROADCAST, error)); |
698 | 1 | m_node.validation_signals->SyncWithValidationInterfaceQueue(); |
699 | 1 | }); |
700 | 1 | wallet = TestLoadWallet(context); |
701 | | // Since mempool transactions are requested at the end of loading, there will |
702 | | // be 2 additional AddToWallet calls, one from the previous test, and a duplicate for mempool_tx |
703 | 1 | BOOST_CHECK_EQUAL(addtx_count, 2 + 2); |
704 | 1 | { |
705 | 1 | LOCK(wallet->cs_wallet); |
706 | 1 | BOOST_CHECK(wallet->mapWallet.contains(block_tx.GetHash())); |
707 | 1 | BOOST_CHECK(wallet->mapWallet.contains(mempool_tx.GetHash())); |
708 | 1 | } |
709 | | |
710 | | |
711 | 1 | TestUnloadWallet(std::move(wallet)); |
712 | 1 | } |
713 | | |
714 | | BOOST_FIXTURE_TEST_CASE(CreateWalletWithoutChain, BasicTestingSetup) |
715 | 1 | { |
716 | 1 | WalletContext context; |
717 | 1 | context.args = &m_args; |
718 | 1 | auto wallet = TestCreateWallet(context); |
719 | 1 | BOOST_CHECK(wallet); |
720 | 1 | WaitForDeleteWallet(std::move(wallet)); |
721 | 1 | } |
722 | | |
723 | | BOOST_FIXTURE_TEST_CASE(RemoveTxs, TestChain100Setup) |
724 | 1 | { |
725 | 1 | m_args.ForceSetArg("-unsafesqlitesync", "1"); |
726 | 1 | WalletContext context; |
727 | 1 | context.args = &m_args; |
728 | 1 | context.chain = m_node.chain.get(); |
729 | 1 | auto wallet = TestCreateWallet(context); |
730 | 1 | CKey key = GenerateRandomKey(); |
731 | 1 | AddKey(*wallet, key); |
732 | | |
733 | 1 | m_coinbase_txns.push_back(CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())).vtx[0]); |
734 | 1 | auto block_tx = TestSimpleSpend(*m_coinbase_txns[0], 0, coinbaseKey, GetScriptForRawPubKey(key.GetPubKey())); |
735 | 1 | CreateAndProcessBlock({block_tx}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())); |
736 | | |
737 | 1 | m_node.validation_signals->SyncWithValidationInterfaceQueue(); |
738 | | |
739 | 1 | { |
740 | 1 | auto block_hash = block_tx.GetHash(); |
741 | 1 | auto prev_tx = m_coinbase_txns[0]; |
742 | | |
743 | 1 | LOCK(wallet->cs_wallet); |
744 | 1 | BOOST_CHECK(wallet->HasWalletSpend(prev_tx)); |
745 | 1 | BOOST_CHECK(wallet->mapWallet.contains(block_hash)); |
746 | | |
747 | 1 | std::vector<Txid> vHashIn{ block_hash }; |
748 | 1 | BOOST_CHECK(wallet->RemoveTxs(vHashIn)); |
749 | | |
750 | 1 | BOOST_CHECK(!wallet->HasWalletSpend(prev_tx)); |
751 | 1 | BOOST_CHECK(!wallet->mapWallet.contains(block_hash)); |
752 | 1 | } |
753 | | |
754 | 1 | TestUnloadWallet(std::move(wallet)); |
755 | 1 | } |
756 | | |
757 | | BOOST_AUTO_TEST_SUITE_END() |
758 | | } // namespace wallet |