wallet_tests.cpp raw
1 // Copyright (c) 2012-2022 The Limenka 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 <future>
8 #include <memory>
9 #include <stdint.h>
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 <policy/policy.h>
17 #include <rpc/server.h>
18 #include <script/solver.h>
19 #include <test/util/logging.h>
20 #include <test/util/random.h>
21 #include <test/util/setup_common.h>
22 #include <util/mempressure.h>
23 #include <util/translation.h>
24 #include <validation.h>
25 #include <validationinterface.h>
26 #include <wallet/coincontrol.h>
27 #include <wallet/context.h>
28 #include <wallet/receive.h>
29 #include <wallet/spend.h>
30 #include <wallet/test/util.h>
31 #include <wallet/test/wallet_test_fixture.h>
32
33 #include <boost/test/unit_test.hpp>
34 #include <test/util/boost_no_print_int128.h>
35 #include <univalue.h>
36
37 using node::MAX_BLOCKFILE_SIZE;
38
39 namespace wallet {
40 RPCHelpMan importmulti();
41 RPCHelpMan dumpwallet();
42 RPCHelpMan importwallet();
43
44 // Ensure that fee levels defined in the wallet are at least as high
45 // as the default levels for node policy.
46 static_assert(DEFAULT_TRANSACTION_MINFEE >= DEFAULT_MIN_RELAY_TX_FEE, "wallet minimum fee is smaller than default relay fee");
47 static_assert(WALLET_INCREMENTAL_RELAY_FEE >= DEFAULT_INCREMENTAL_RELAY_FEE, "wallet incremental fee is smaller than default incremental relay fee");
48
49 BOOST_FIXTURE_TEST_SUITE(wallet_tests, WalletTestingSetup)
50
51 static CMutableTransaction TestSimpleSpend(const CTransaction& from, uint32_t index, const CKey& key, const CScript& pubkey)
52 {
53 CMutableTransaction mtx;
54 mtx.vout.emplace_back(from.vout[index].nValue - DEFAULT_TRANSACTION_MAXFEE, pubkey);
55 mtx.vin.push_back({CTxIn{from.GetHash(), index}});
56 FillableSigningProvider keystore;
57 keystore.AddKey(key);
58 std::map<COutPoint, Coin> coins;
59 coins[mtx.vin[0].prevout].out = from.vout[index];
60 std::map<int, bilingual_str> input_errors;
61 BOOST_CHECK(SignTransaction(mtx, &keystore, coins, SIGHASH_ALL, input_errors));
62 return mtx;
63 }
64
65 static void AddKey(CWallet& wallet, const CKey& key)
66 {
67 LOCK(wallet.cs_wallet);
68 FlatSigningProvider provider;
69 std::string error;
70 auto descs = Parse("combo(" + EncodeSecret(key) + ")", provider, error, /* require_checksum=*/ false);
71 assert(descs.size() == 1);
72 auto& desc = descs.at(0);
73 WalletDescriptor w_desc(std::move(desc), 0, 0, 1, 1);
74 if (!wallet.AddWalletDescriptor(w_desc, provider, "", false)) assert(false);
75 }
76
77 BOOST_FIXTURE_TEST_CASE(update_non_range_descriptor, TestingSetup)
78 {
79 CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase());
80 {
81 LOCK(wallet.cs_wallet);
82 wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
83 auto key{GenerateRandomKey()};
84 auto desc_str{"combo(" + EncodeSecret(key) + ")"};
85 FlatSigningProvider provider;
86 std::string error;
87 auto descs{Parse(desc_str, provider, error, /* require_checksum=*/ false)};
88 auto& desc{descs.at(0)};
89 WalletDescriptor w_desc{std::move(desc), 0, 0, 0, 0};
90 BOOST_CHECK(wallet.AddWalletDescriptor(w_desc, provider, "", false));
91 // Wallet should update the non-range descriptor successfully
92 BOOST_CHECK(wallet.AddWalletDescriptor(w_desc, provider, "", false));
93 }
94 }
95
96 BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions, TestChain100Setup)
97 {
98 // Cap last block file size, and mine new block in a new block file.
99 CBlockIndex* oldTip = WITH_LOCK(Assert(m_node.chainman)->GetMutex(), return m_node.chainman->ActiveChain().Tip());
100 WITH_LOCK(::cs_main, m_node.chainman->m_blockman.GetBlockFileInfo(oldTip->GetBlockPos().nFile)->nSize = MAX_BLOCKFILE_SIZE);
101 CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey()));
102 CBlockIndex* newTip = WITH_LOCK(Assert(m_node.chainman)->GetMutex(), return m_node.chainman->ActiveChain().Tip());
103
104 // Verify ScanForWalletTransactions fails to read an unknown start block.
105 {
106 CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase());
107 {
108 LOCK(wallet.cs_wallet);
109 LOCK(Assert(m_node.chainman)->GetMutex());
110 wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
111 wallet.SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash());
112 }
113 AddKey(wallet, coinbaseKey);
114 WalletRescanReserver reserver(wallet);
115 reserver.reserve();
116 CWallet::ScanResult result = wallet.ScanForWalletTransactions(/*start_block=*/{}, /*start_height=*/0, /*max_height=*/{}, reserver, /*fUpdate=*/false, /*save_progress=*/false);
117 BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::FAILURE);
118 BOOST_CHECK(result.last_failed_block.IsNull());
119 BOOST_CHECK(result.last_scanned_block.IsNull());
120 BOOST_CHECK(!result.last_scanned_height);
121 BOOST_CHECK_EQUAL(GetBalance(wallet).m_mine_immature, 0);
122 }
123
124 // Verify ScanForWalletTransactions picks up transactions in both the old
125 // and new block files.
126 {
127 CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase());
128 {
129 LOCK(wallet.cs_wallet);
130 LOCK(Assert(m_node.chainman)->GetMutex());
131 wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
132 wallet.SetLastBlockProcessed(newTip->nHeight, newTip->GetBlockHash());
133 }
134 AddKey(wallet, coinbaseKey);
135 WalletRescanReserver reserver(wallet);
136 std::chrono::steady_clock::time_point fake_time;
137 reserver.setNow([&] { fake_time += 60s; return fake_time; });
138 reserver.reserve();
139
140 {
141 CBlockLocator locator;
142 BOOST_CHECK(WalletBatch{wallet.GetDatabase()}.ReadBestBlock(locator));
143 BOOST_CHECK(!locator.IsNull() && locator.vHave.front() == newTip->GetBlockHash());
144 }
145
146 CWallet::ScanResult result = wallet.ScanForWalletTransactions(/*start_block=*/oldTip->GetBlockHash(), /*start_height=*/oldTip->nHeight, /*max_height=*/{}, reserver, /*fUpdate=*/false, /*save_progress=*/true);
147 BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::SUCCESS);
148 BOOST_CHECK(result.last_failed_block.IsNull());
149 BOOST_CHECK_EQUAL(result.last_scanned_block, newTip->GetBlockHash());
150 BOOST_CHECK_EQUAL(*result.last_scanned_height, newTip->nHeight);
151 BOOST_CHECK_EQUAL(GetBalance(wallet).m_mine_immature, 100 * COIN);
152
153 {
154 CBlockLocator locator;
155 BOOST_CHECK(WalletBatch{wallet.GetDatabase()}.ReadBestBlock(locator));
156 BOOST_CHECK(!locator.IsNull() && locator.vHave.front() == newTip->GetBlockHash());
157 }
158 }
159
160 // Prune the older block file.
161 int file_number;
162 {
163 LOCK(cs_main);
164 file_number = oldTip->GetBlockPos().nFile;
165 Assert(m_node.chainman)->m_blockman.PruneOneBlockFile(file_number);
166 }
167 m_node.chainman->m_blockman.UnlinkPrunedFiles({file_number});
168
169 // Verify ScanForWalletTransactions only picks transactions in the new block
170 // file.
171 {
172 CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase());
173 {
174 LOCK(wallet.cs_wallet);
175 LOCK(Assert(m_node.chainman)->GetMutex());
176 wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
177 wallet.SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash());
178 }
179 AddKey(wallet, coinbaseKey);
180 WalletRescanReserver reserver(wallet);
181 reserver.reserve();
182 CWallet::ScanResult result = wallet.ScanForWalletTransactions(/*start_block=*/oldTip->GetBlockHash(), /*start_height=*/oldTip->nHeight, /*max_height=*/{}, reserver, /*fUpdate=*/false, /*save_progress=*/false);
183 BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::FAILURE);
184 BOOST_CHECK_EQUAL(result.last_failed_block, oldTip->GetBlockHash());
185 BOOST_CHECK_EQUAL(result.last_scanned_block, newTip->GetBlockHash());
186 BOOST_CHECK_EQUAL(*result.last_scanned_height, newTip->nHeight);
187 BOOST_CHECK_EQUAL(GetBalance(wallet).m_mine_immature, 50 * COIN);
188 }
189
190 // Prune the remaining block file.
191 {
192 LOCK(cs_main);
193 file_number = newTip->GetBlockPos().nFile;
194 Assert(m_node.chainman)->m_blockman.PruneOneBlockFile(file_number);
195 }
196 m_node.chainman->m_blockman.UnlinkPrunedFiles({file_number});
197
198 // Verify ScanForWalletTransactions scans no blocks.
199 {
200 CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase());
201 {
202 LOCK(wallet.cs_wallet);
203 LOCK(Assert(m_node.chainman)->GetMutex());
204 wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
205 wallet.SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash());
206 }
207 AddKey(wallet, coinbaseKey);
208 WalletRescanReserver reserver(wallet);
209 reserver.reserve();
210 CWallet::ScanResult result = wallet.ScanForWalletTransactions(/*start_block=*/oldTip->GetBlockHash(), /*start_height=*/oldTip->nHeight, /*max_height=*/{}, reserver, /*fUpdate=*/false, /*save_progress=*/false);
211 BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::FAILURE);
212 BOOST_CHECK_EQUAL(result.last_failed_block, newTip->GetBlockHash());
213 BOOST_CHECK(result.last_scanned_block.IsNull());
214 BOOST_CHECK(!result.last_scanned_height);
215 BOOST_CHECK_EQUAL(GetBalance(wallet).m_mine_immature, 0);
216 }
217 }
218
219 BOOST_FIXTURE_TEST_CASE(importmulti_rescan, TestChain100Setup)
220 {
221 // Cap last block file size, and mine new block in a new block file.
222 CBlockIndex* oldTip = WITH_LOCK(Assert(m_node.chainman)->GetMutex(), return m_node.chainman->ActiveChain().Tip());
223 WITH_LOCK(::cs_main, m_node.chainman->m_blockman.GetBlockFileInfo(oldTip->GetBlockPos().nFile)->nSize = MAX_BLOCKFILE_SIZE);
224 CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey()));
225 CBlockIndex* newTip = WITH_LOCK(Assert(m_node.chainman)->GetMutex(), return m_node.chainman->ActiveChain().Tip());
226
227 // Prune the older block file.
228 int file_number;
229 {
230 LOCK(cs_main);
231 file_number = oldTip->GetBlockPos().nFile;
232 Assert(m_node.chainman)->m_blockman.PruneOneBlockFile(file_number);
233 }
234 m_node.chainman->m_blockman.UnlinkPrunedFiles({file_number});
235
236 // Verify importmulti RPC returns failure for a key whose creation time is
237 // before the missing block, and success for a key whose creation time is
238 // after.
239 {
240 const std::shared_ptr<CWallet> wallet = std::make_shared<CWallet>(m_node.chain.get(), "", CreateMockableWalletDatabase());
241 wallet->SetupLegacyScriptPubKeyMan();
242 WITH_LOCK(wallet->cs_wallet, wallet->SetLastBlockProcessed(newTip->nHeight, newTip->GetBlockHash()));
243 WalletContext context;
244 context.args = &m_args;
245 AddWallet(context, wallet);
246 UniValue keys;
247 keys.setArray();
248 UniValue key;
249 key.setObject();
250 key.pushKV("scriptPubKey", HexStr(GetScriptForRawPubKey(coinbaseKey.GetPubKey())));
251 key.pushKV("timestamp", 0);
252 key.pushKV("internal", UniValue(true));
253 keys.push_back(key);
254 key.clear();
255 key.setObject();
256 CKey futureKey = GenerateRandomKey();
257 key.pushKV("scriptPubKey", HexStr(GetScriptForRawPubKey(futureKey.GetPubKey())));
258 key.pushKV("timestamp", newTip->GetBlockTimeMax() + TIMESTAMP_WINDOW + 1);
259 key.pushKV("internal", UniValue(true));
260 keys.push_back(std::move(key));
261 JSONRPCRequest request;
262 request.context = &context;
263 request.m_wallet_restriction = "";
264 request.params.setArray();
265 request.params.push_back(std::move(keys));
266
267 UniValue response = importmulti().HandleRequest(request);
268 BOOST_CHECK_EQUAL(response.write(),
269 strprintf("[{\"success\":false,\"error\":{\"code\":-1,\"message\":\"Rescan failed for key with creation "
270 "timestamp %d. There was an error reading a block from time %d, which is after or within %d "
271 "seconds of key creation, and could contain transactions pertaining to the key. As a result, "
272 "transactions and coins using this key may not appear in the wallet. This error could be caused "
273 "by pruning or data corruption (see limenkad log for details) and could be dealt with by "
274 "downloading and rescanning the relevant blocks (see -reindex option and rescanblockchain "
275 "RPC).\"}},{\"success\":true}]",
276 0, oldTip->GetBlockTimeMax(), TIMESTAMP_WINDOW));
277 RemoveWallet(context, wallet, /* load_on_start= */ std::nullopt);
278 }
279 }
280
281 // Verify importwallet RPC starts rescan at earliest block with timestamp
282 // greater or equal than key birthday. Previously there was a bug where
283 // importwallet RPC would start the scan at the latest block with timestamp less
284 // than or equal to key birthday.
285 BOOST_FIXTURE_TEST_CASE(importwallet_rescan, TestChain100Setup)
286 {
287 // Create two blocks with same timestamp to verify that importwallet rescan
288 // will pick up both blocks, not just the first.
289 const int64_t BLOCK_TIME = WITH_LOCK(Assert(m_node.chainman)->GetMutex(), return m_node.chainman->ActiveChain().Tip()->GetBlockTimeMax() + 5);
290 SetMockTime(BLOCK_TIME);
291 m_coinbase_txns.emplace_back(CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())).vtx[0]);
292 m_coinbase_txns.emplace_back(CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())).vtx[0]);
293
294 // Set key birthday to block time increased by the timestamp window, so
295 // rescan will start at the block time.
296 const int64_t KEY_TIME = BLOCK_TIME + TIMESTAMP_WINDOW;
297 SetMockTime(KEY_TIME);
298 m_coinbase_txns.emplace_back(CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())).vtx[0]);
299
300 std::string backup_file = fs::PathToString(m_args.GetDataDirNet() / "wallet.backup");
301
302 // Import key into wallet and call dumpwallet to create backup file.
303 {
304 WalletContext context;
305 context.args = &m_args;
306 const std::shared_ptr<CWallet> wallet = std::make_shared<CWallet>(m_node.chain.get(), "", CreateMockableWalletDatabase());
307 {
308 auto spk_man = wallet->GetOrCreateLegacyScriptPubKeyMan();
309 LOCK2(wallet->cs_wallet, spk_man->cs_KeyStore);
310 spk_man->mapKeyMetadata[coinbaseKey.GetPubKey().GetID()].nCreateTime = KEY_TIME;
311 spk_man->AddKeyPubKey(coinbaseKey, coinbaseKey.GetPubKey());
312
313 AddWallet(context, wallet);
314 LOCK(Assert(m_node.chainman)->GetMutex());
315 wallet->SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash());
316 }
317 JSONRPCRequest request;
318 request.context = &context;
319 request.m_wallet_restriction = "";
320 request.params.setArray();
321 request.params.push_back(backup_file);
322
323 wallet::dumpwallet().HandleRequest(request);
324 RemoveWallet(context, wallet, /* load_on_start= */ std::nullopt);
325 }
326
327 // Call importwallet RPC and verify all blocks with timestamps >= BLOCK_TIME
328 // were scanned, and no prior blocks were scanned.
329 {
330 const std::shared_ptr<CWallet> wallet = std::make_shared<CWallet>(m_node.chain.get(), "", CreateMockableWalletDatabase());
331 LOCK(wallet->cs_wallet);
332 wallet->SetupLegacyScriptPubKeyMan();
333
334 WalletContext context;
335 context.args = &m_args;
336 JSONRPCRequest request;
337 request.context = &context;
338 request.m_wallet_restriction = "";
339 request.params.setArray();
340 request.params.push_back(backup_file);
341 AddWallet(context, wallet);
342 LOCK(Assert(m_node.chainman)->GetMutex());
343 wallet->SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash());
344 wallet::importwallet().HandleRequest(request);
345 RemoveWallet(context, wallet, /* load_on_start= */ std::nullopt);
346
347 BOOST_CHECK_EQUAL(wallet->mapWallet.size(), 3U);
348 BOOST_CHECK_EQUAL(m_coinbase_txns.size(), 103U);
349 for (size_t i = 0; i < m_coinbase_txns.size(); ++i) {
350 bool found = wallet->GetWalletTx(m_coinbase_txns[i]->GetHash());
351 bool expected = i >= 100;
352 BOOST_CHECK_EQUAL(found, expected);
353 }
354 }
355 }
356
357 // This test verifies that wallet settings can be added and removed
358 // concurrently, ensuring no race conditions occur during either process.
359 BOOST_FIXTURE_TEST_CASE(write_wallet_settings_concurrently, TestingSetup)
360 {
361 auto chain = m_node.chain.get();
362 const auto NUM_WALLETS{5};
363
364 // Since we're counting the number of wallets, ensure we start without any.
365 BOOST_REQUIRE(chain->getRwSetting("wallet").isNull());
366
367 const auto& check_concurrent_wallet = [&](const auto& settings_function, int num_expected_wallets) {
368 std::vector<std::thread> threads;
369 threads.reserve(NUM_WALLETS);
370 for (auto i{0}; i < NUM_WALLETS; ++i) threads.emplace_back(settings_function, i);
371 for (auto& t : threads) t.join();
372
373 auto wallets = chain->getRwSetting("wallet");
374 BOOST_CHECK_EQUAL(wallets.getValues().size(), num_expected_wallets);
375 };
376
377 // Add NUM_WALLETS wallets concurrently, ensure we end up with NUM_WALLETS stored.
378 check_concurrent_wallet([&chain](int i) {
379 Assert(AddWalletSetting(*chain, strprintf("wallet_%d", i)));
380 },
381 /*num_expected_wallets=*/NUM_WALLETS);
382
383 // Remove NUM_WALLETS wallets concurrently, ensure we end up with 0 wallets.
384 check_concurrent_wallet([&chain](int i) {
385 Assert(RemoveWalletSetting(*chain, strprintf("wallet_%d", i)));
386 },
387 /*num_expected_wallets=*/0);
388 }
389
390 // Check that GetImmatureCredit() returns a newly calculated value instead of
391 // the cached value after a MarkDirty() call.
392 //
393 // This is a regression test written to verify a bugfix for the immature credit
394 // function. Similar tests probably should be written for the other credit and
395 // debit functions.
396 BOOST_FIXTURE_TEST_CASE(coin_mark_dirty_immature_credit, TestChain100Setup)
397 {
398 CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase());
399
400 LOCK(wallet.cs_wallet);
401 LOCK(Assert(m_node.chainman)->GetMutex());
402 CWalletTx wtx{m_coinbase_txns.back(), TxStateConfirmed{m_node.chainman->ActiveChain().Tip()->GetBlockHash(), m_node.chainman->ActiveChain().Height(), /*index=*/0}};
403 wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
404 wallet.SetupDescriptorScriptPubKeyMans();
405
406 wallet.SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash());
407
408 // Call GetImmatureCredit() once before adding the key to the wallet to
409 // cache the current immature credit amount, which is 0.
410 BOOST_CHECK_EQUAL(CachedTxGetImmatureCredit(wallet, wtx, ISMINE_SPENDABLE), 0);
411
412 // Invalidate the cached value, add the key, and make sure a new immature
413 // credit amount is calculated.
414 wtx.MarkDirty();
415 AddKey(wallet, coinbaseKey);
416 BOOST_CHECK_EQUAL(CachedTxGetImmatureCredit(wallet, wtx, ISMINE_SPENDABLE), 50*COIN);
417 }
418
419 static int64_t AddTx(ChainstateManager& chainman, CWallet& wallet, uint32_t lockTime, int64_t mockTime, int64_t blockTime)
420 {
421 CMutableTransaction tx;
422 TxState state = TxStateInactive{};
423 tx.nLockTime = lockTime;
424 SetMockTime(mockTime);
425 CBlockIndex* block = nullptr;
426 if (blockTime > 0) {
427 LOCK(cs_main);
428 auto inserted = chainman.BlockIndex().emplace(std::piecewise_construct, std::make_tuple(GetRandHash()), std::make_tuple());
429 assert(inserted.second);
430 const uint256& hash = inserted.first->first;
431 block = &inserted.first->second;
432 block->nTime = blockTime;
433 block->phashBlock = &hash;
434 state = TxStateConfirmed{hash, block->nHeight, /*index=*/0};
435 }
436 return wallet.AddToWallet(MakeTransactionRef(tx), state, [&](CWalletTx& wtx, bool /* new_tx */) {
437 // Assign wtx.m_state to simplify test and avoid the need to simulate
438 // reorg events. Without this, AddToWallet asserts false when the same
439 // transaction is confirmed in different blocks.
440 wtx.m_state = state;
441 return true;
442 })->nTimeSmart;
443 }
444
445 // Simple test to verify assignment of CWalletTx::nSmartTime value. Could be
446 // expanded to cover more corner cases of smart time logic.
447 BOOST_AUTO_TEST_CASE(ComputeTimeSmart)
448 {
449 // New transaction should use clock time if lower than block time.
450 BOOST_CHECK_EQUAL(AddTx(*m_node.chainman, m_wallet, 1, 100, 120), 100);
451
452 // Test that updating existing transaction does not change smart time.
453 BOOST_CHECK_EQUAL(AddTx(*m_node.chainman, m_wallet, 1, 200, 220), 100);
454
455 // New transaction should use clock time if there's no block time.
456 BOOST_CHECK_EQUAL(AddTx(*m_node.chainman, m_wallet, 2, 300, 0), 300);
457
458 // New transaction should use block time if lower than clock time.
459 BOOST_CHECK_EQUAL(AddTx(*m_node.chainman, m_wallet, 3, 420, 400), 400);
460
461 // New transaction should use latest entry time if higher than
462 // min(block time, clock time).
463 BOOST_CHECK_EQUAL(AddTx(*m_node.chainman, m_wallet, 4, 500, 390), 400);
464
465 // If there are future entries, new transaction should use time of the
466 // newest entry that is no more than 300 seconds ahead of the clock time.
467 BOOST_CHECK_EQUAL(AddTx(*m_node.chainman, m_wallet, 5, 50, 600), 300);
468 }
469
470 void TestLoadWallet(const std::string& name, DatabaseFormat format, std::function<void(std::shared_ptr<CWallet>)> f)
471 {
472 node::NodeContext node;
473 auto chain{interfaces::MakeChain(node)};
474 DatabaseOptions options;
475 options.require_format = format;
476 DatabaseStatus status;
477 bilingual_str error;
478 std::vector<bilingual_str> warnings;
479 auto database{MakeWalletDatabase(name, options, status, error)};
480 auto wallet{std::make_shared<CWallet>(chain.get(), "", std::move(database))};
481 BOOST_CHECK_EQUAL(wallet->LoadWallet(), DBErrors::LOAD_OK);
482 WITH_LOCK(wallet->cs_wallet, f(wallet));
483 }
484
485 BOOST_FIXTURE_TEST_CASE(LoadReceiveRequests, TestingSetup)
486 {
487 for (DatabaseFormat format : DATABASE_FORMATS) {
488 const std::string name{strprintf("receive-requests-%i", format)};
489 TestLoadWallet(name, format, [](std::shared_ptr<CWallet> wallet) EXCLUSIVE_LOCKS_REQUIRED(wallet->cs_wallet) {
490 BOOST_CHECK(!wallet->IsAddressPreviouslySpent(PKHash()));
491 WalletBatch batch{wallet->GetDatabase()};
492 BOOST_CHECK(batch.WriteAddressPreviouslySpent(PKHash(), true));
493 BOOST_CHECK(batch.WriteAddressPreviouslySpent(ScriptHash(), true));
494 BOOST_CHECK(wallet->SetAddressReceiveRequest(batch, PKHash(), "0", "val_rr00"));
495 BOOST_CHECK(wallet->EraseAddressReceiveRequest(batch, PKHash(), "0"));
496 BOOST_CHECK(wallet->SetAddressReceiveRequest(batch, PKHash(), "1", "val_rr10"));
497 BOOST_CHECK(wallet->SetAddressReceiveRequest(batch, PKHash(), "1", "val_rr11"));
498 BOOST_CHECK(wallet->SetAddressReceiveRequest(batch, ScriptHash(), "2", "val_rr20"));
499 });
500 TestLoadWallet(name, format, [](std::shared_ptr<CWallet> wallet) EXCLUSIVE_LOCKS_REQUIRED(wallet->cs_wallet) {
501 BOOST_CHECK(wallet->IsAddressPreviouslySpent(PKHash()));
502 BOOST_CHECK(wallet->IsAddressPreviouslySpent(ScriptHash()));
503 auto requests = wallet->GetAddressReceiveRequests();
504 auto erequests = {"val_rr11", "val_rr20"};
505 BOOST_CHECK_EQUAL_COLLECTIONS(requests.begin(), requests.end(), std::begin(erequests), std::end(erequests));
506 RunWithinTxn(wallet->GetDatabase(), /*process_desc*/"test", [](WalletBatch& batch){
507 BOOST_CHECK(batch.WriteAddressPreviouslySpent(PKHash(), false));
508 BOOST_CHECK(batch.EraseAddressData(ScriptHash()));
509 return true;
510 });
511 });
512 TestLoadWallet(name, format, [](std::shared_ptr<CWallet> wallet) EXCLUSIVE_LOCKS_REQUIRED(wallet->cs_wallet) {
513 BOOST_CHECK(!wallet->IsAddressPreviouslySpent(PKHash()));
514 BOOST_CHECK(!wallet->IsAddressPreviouslySpent(ScriptHash()));
515 auto requests = wallet->GetAddressReceiveRequests();
516 auto erequests = {"val_rr11"};
517 BOOST_CHECK_EQUAL_COLLECTIONS(requests.begin(), requests.end(), std::begin(erequests), std::end(erequests));
518 });
519 }
520 }
521
522 // Test some watch-only LegacyScriptPubKeyMan methods by the procedure of loading (LoadWatchOnly),
523 // checking (HaveWatchOnly), getting (GetWatchPubKey) and removing (RemoveWatchOnly) a
524 // given PubKey, resp. its corresponding P2PK Script. Results of the impact on
525 // the address -> PubKey map is dependent on whether the PubKey is a point on the curve
526 static void TestWatchOnlyPubKey(LegacyScriptPubKeyMan* spk_man, const CPubKey& add_pubkey)
527 {
528 CScript p2pk = GetScriptForRawPubKey(add_pubkey);
529 CKeyID add_address = add_pubkey.GetID();
530 CPubKey found_pubkey;
531 LOCK(spk_man->cs_KeyStore);
532
533 // all Scripts (i.e. also all PubKeys) are added to the general watch-only set
534 BOOST_CHECK(!spk_man->HaveWatchOnly(p2pk));
535 spk_man->LoadWatchOnly(p2pk);
536 BOOST_CHECK(spk_man->HaveWatchOnly(p2pk));
537
538 // only PubKeys on the curve shall be added to the watch-only address -> PubKey map
539 bool is_pubkey_fully_valid = add_pubkey.IsFullyValid();
540 if (is_pubkey_fully_valid) {
541 BOOST_CHECK(spk_man->GetWatchPubKey(add_address, found_pubkey));
542 BOOST_CHECK(found_pubkey == add_pubkey);
543 } else {
544 BOOST_CHECK(!spk_man->GetWatchPubKey(add_address, found_pubkey));
545 BOOST_CHECK(found_pubkey == CPubKey()); // passed key is unchanged
546 }
547
548 spk_man->RemoveWatchOnly(p2pk);
549 BOOST_CHECK(!spk_man->HaveWatchOnly(p2pk));
550
551 if (is_pubkey_fully_valid) {
552 BOOST_CHECK(!spk_man->GetWatchPubKey(add_address, found_pubkey));
553 BOOST_CHECK(found_pubkey == add_pubkey); // passed key is unchanged
554 }
555 }
556
557 // Cryptographically invalidate a PubKey whilst keeping length and first byte
558 static void PollutePubKey(CPubKey& pubkey)
559 {
560 assert(pubkey.size() >= 1);
561 std::vector<unsigned char> pubkey_raw;
562 pubkey_raw.push_back(pubkey[0]);
563 pubkey_raw.insert(pubkey_raw.end(), pubkey.size() - 1, 0);
564 pubkey = CPubKey(pubkey_raw);
565 assert(!pubkey.IsFullyValid());
566 assert(pubkey.IsValid());
567 }
568
569 // Test watch-only logic for PubKeys
570 BOOST_AUTO_TEST_CASE(WatchOnlyPubKeys)
571 {
572 CKey key;
573 CPubKey pubkey;
574 LegacyScriptPubKeyMan* spk_man = m_wallet.GetOrCreateLegacyScriptPubKeyMan();
575
576 BOOST_CHECK(!spk_man->HaveWatchOnly());
577
578 // uncompressed valid PubKey
579 key.MakeNewKey(false);
580 pubkey = key.GetPubKey();
581 assert(!pubkey.IsCompressed());
582 TestWatchOnlyPubKey(spk_man, pubkey);
583
584 // uncompressed cryptographically invalid PubKey
585 PollutePubKey(pubkey);
586 TestWatchOnlyPubKey(spk_man, pubkey);
587
588 // compressed valid PubKey
589 key.MakeNewKey(true);
590 pubkey = key.GetPubKey();
591 assert(pubkey.IsCompressed());
592 TestWatchOnlyPubKey(spk_man, pubkey);
593
594 // compressed cryptographically invalid PubKey
595 PollutePubKey(pubkey);
596 TestWatchOnlyPubKey(spk_man, pubkey);
597
598 // invalid empty PubKey
599 pubkey = CPubKey();
600 TestWatchOnlyPubKey(spk_man, pubkey);
601 }
602
603 class ListCoinsTestingSetup : public TestChain100Setup
604 {
605 public:
606 ListCoinsTestingSetup()
607 {
608 CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey()));
609 wallet = CreateSyncedWallet(*m_node.chain, WITH_LOCK(Assert(m_node.chainman)->GetMutex(), return m_node.chainman->ActiveChain()), coinbaseKey);
610 }
611
612 ~ListCoinsTestingSetup()
613 {
614 wallet.reset();
615 }
616
617 CWalletTx& AddTx(CRecipient recipient)
618 {
619 CTransactionRef tx;
620 CCoinControl dummy;
621 {
622 auto res = CreateTransaction(*wallet, {recipient}, /*change_pos=*/std::nullopt, dummy);
623 BOOST_CHECK(res);
624 tx = res->tx;
625 }
626 wallet->CommitTransaction(tx, {}, {});
627 CMutableTransaction blocktx;
628 {
629 LOCK(wallet->cs_wallet);
630 blocktx = CMutableTransaction(*wallet->mapWallet.at(tx->GetHash()).tx);
631 }
632 CreateAndProcessBlock({CMutableTransaction(blocktx)}, GetScriptForRawPubKey(coinbaseKey.GetPubKey()));
633
634 LOCK(wallet->cs_wallet);
635 LOCK(Assert(m_node.chainman)->GetMutex());
636 wallet->SetLastBlockProcessed(wallet->GetLastBlockHeight() + 1, m_node.chainman->ActiveChain().Tip()->GetBlockHash());
637 auto it = wallet->mapWallet.find(tx->GetHash());
638 BOOST_CHECK(it != wallet->mapWallet.end());
639 it->second.m_state = TxStateConfirmed{m_node.chainman->ActiveChain().Tip()->GetBlockHash(), m_node.chainman->ActiveChain().Height(), /*index=*/1};
640 return it->second;
641 }
642
643 std::unique_ptr<CWallet> wallet;
644 };
645
646 BOOST_FIXTURE_TEST_CASE(ListCoinsTest, ListCoinsTestingSetup)
647 {
648 std::string coinbaseAddress = coinbaseKey.GetPubKey().GetID().ToString();
649
650 // Confirm ListCoins initially returns 1 coin grouped under coinbaseKey
651 // address.
652 std::map<CTxDestination, std::vector<COutput>> list;
653 {
654 LOCK(wallet->cs_wallet);
655 list = ListCoins(*wallet);
656 }
657 BOOST_CHECK_EQUAL(list.size(), 1U);
658 BOOST_CHECK_EQUAL(std::get<PKHash>(list.begin()->first).ToString(), coinbaseAddress);
659 BOOST_CHECK_EQUAL(list.begin()->second.size(), 1U);
660
661 // Check initial balance from one mature coinbase transaction.
662 BOOST_CHECK_EQUAL(50 * COIN, WITH_LOCK(wallet->cs_wallet, return AvailableCoins(*wallet).GetTotalAmount()));
663
664 // Add a transaction creating a change address, and confirm ListCoins still
665 // returns the coin associated with the change address underneath the
666 // coinbaseKey pubkey, even though the change address has a different
667 // pubkey.
668 AddTx(CRecipient{PubKeyDestination{{}}, 1 * COIN, /*subtract_fee=*/false});
669 {
670 LOCK(wallet->cs_wallet);
671 list = ListCoins(*wallet);
672 }
673 BOOST_CHECK_EQUAL(list.size(), 1U);
674 BOOST_CHECK_EQUAL(std::get<PKHash>(list.begin()->first).ToString(), coinbaseAddress);
675 BOOST_CHECK_EQUAL(list.begin()->second.size(), 2U);
676
677 // Lock both coins. Confirm number of available coins drops to 0.
678 {
679 LOCK(wallet->cs_wallet);
680 BOOST_CHECK_EQUAL(AvailableCoinsListUnspent(*wallet).Size(), 2U);
681 }
682 for (const auto& group : list) {
683 for (const auto& coin : group.second) {
684 LOCK(wallet->cs_wallet);
685 wallet->LockCoin(coin.outpoint);
686 }
687 }
688 {
689 LOCK(wallet->cs_wallet);
690 BOOST_CHECK_EQUAL(AvailableCoinsListUnspent(*wallet).Size(), 0U);
691 }
692 // Confirm ListCoins still returns same result as before, despite coins
693 // being locked.
694 {
695 LOCK(wallet->cs_wallet);
696 list = ListCoins(*wallet);
697 }
698 BOOST_CHECK_EQUAL(list.size(), 1U);
699 BOOST_CHECK_EQUAL(std::get<PKHash>(list.begin()->first).ToString(), coinbaseAddress);
700 BOOST_CHECK_EQUAL(list.begin()->second.size(), 2U);
701 }
702
703 void TestCoinsResult(ListCoinsTest& context, OutputType out_type, CAmount amount,
704 std::map<OutputType, size_t>& expected_coins_sizes)
705 {
706 LOCK(context.wallet->cs_wallet);
707 util::Result<CTxDestination> dest = Assert(context.wallet->GetNewDestination(out_type, ""));
708 CWalletTx& wtx = context.AddTx(CRecipient{*dest, amount, /*fSubtractFeeFromAmount=*/true});
709 CoinFilterParams filter;
710 filter.skip_locked = false;
711 CoinsResult available_coins = AvailableCoins(*context.wallet, nullptr, std::nullopt, filter);
712 // Lock outputs so they are not spent in follow-up transactions
713 for (uint32_t i = 0; i < wtx.tx->vout.size(); i++) context.wallet->LockCoin({wtx.GetHash(), i});
714 for (const auto& [type, size] : expected_coins_sizes) BOOST_CHECK_EQUAL(size, available_coins.coins[type].size());
715 }
716
717 BOOST_FIXTURE_TEST_CASE(BasicOutputTypesTest, ListCoinsTest)
718 {
719 std::map<OutputType, size_t> expected_coins_sizes;
720 for (const auto& out_type : OUTPUT_TYPES) { expected_coins_sizes[out_type] = 0U; }
721
722 // Verify our wallet has one usable coinbase UTXO before starting
723 // This UTXO is a P2PK, so it should show up in the Other bucket
724 expected_coins_sizes[OutputType::UNKNOWN] = 1U;
725 CoinsResult available_coins = WITH_LOCK(wallet->cs_wallet, return AvailableCoins(*wallet));
726 BOOST_CHECK_EQUAL(available_coins.Size(), expected_coins_sizes[OutputType::UNKNOWN]);
727 BOOST_CHECK_EQUAL(available_coins.coins[OutputType::UNKNOWN].size(), expected_coins_sizes[OutputType::UNKNOWN]);
728
729 // We will create a self transfer for each of the OutputTypes and
730 // verify it is put in the correct bucket after running GetAvailablecoins
731 //
732 // For each OutputType, We expect 2 UTXOs in our wallet following the self transfer:
733 // 1. One UTXO as the recipient
734 // 2. One UTXO from the change, due to payment address matching logic
735
736 for (const auto& out_type : OUTPUT_TYPES) {
737 if (out_type == OutputType::UNKNOWN) continue;
738 expected_coins_sizes[out_type] = 2U;
739 TestCoinsResult(*this, out_type, 1 * COIN, expected_coins_sizes);
740 }
741 }
742
743 BOOST_FIXTURE_TEST_CASE(wallet_disableprivkeys, TestChain100Setup)
744 {
745 {
746 const std::shared_ptr<CWallet> wallet = std::make_shared<CWallet>(m_node.chain.get(), "", CreateMockableWalletDatabase());
747 wallet->SetupLegacyScriptPubKeyMan();
748 wallet->SetMinVersion(FEATURE_LATEST);
749 wallet->SetWalletFlag(WALLET_FLAG_DISABLE_PRIVATE_KEYS);
750 BOOST_CHECK(!wallet->TopUpKeyPool(1000));
751 BOOST_CHECK(!wallet->GetNewDestination(OutputType::BECH32, ""));
752 }
753 {
754 const std::shared_ptr<CWallet> wallet = std::make_shared<CWallet>(m_node.chain.get(), "", CreateMockableWalletDatabase());
755 LOCK(wallet->cs_wallet);
756 wallet->SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
757 wallet->SetMinVersion(FEATURE_LATEST);
758 wallet->SetWalletFlag(WALLET_FLAG_DISABLE_PRIVATE_KEYS);
759 BOOST_CHECK(!wallet->GetNewDestination(OutputType::BECH32, ""));
760 }
761 }
762
763 // Explicit calculation which is used to test the wallet constant
764 // We get the same virtual size due to rounding(weight/4) for both use_max_sig values
765 static size_t CalculateNestedKeyhashInputSize(bool use_max_sig)
766 {
767 // Generate ephemeral valid pubkey
768 CKey key = GenerateRandomKey();
769 CPubKey pubkey = key.GetPubKey();
770
771 // Generate pubkey hash
772 uint160 key_hash(Hash160(pubkey));
773
774 // Create inner-script to enter into keystore. Key hash can't be 0...
775 CScript inner_script = CScript() << OP_0 << std::vector<unsigned char>(key_hash.begin(), key_hash.end());
776
777 // Create outer P2SH script for the output
778 uint160 script_id(Hash160(inner_script));
779 CScript script_pubkey = CScript() << OP_HASH160 << std::vector<unsigned char>(script_id.begin(), script_id.end()) << OP_EQUAL;
780
781 // Add inner-script to key store and key to watchonly
782 FillableSigningProvider keystore;
783 keystore.AddCScript(inner_script);
784 keystore.AddKeyPubKey(key, pubkey);
785
786 // Fill in dummy signatures for fee calculation.
787 SignatureData sig_data;
788
789 if (!ProduceSignature(keystore, use_max_sig ? DUMMY_MAXIMUM_SIGNATURE_CREATOR : DUMMY_SIGNATURE_CREATOR, script_pubkey, sig_data)) {
790 // We're hand-feeding it correct arguments; shouldn't happen
791 assert(false);
792 }
793
794 CTxIn tx_in;
795 UpdateInput(tx_in, sig_data);
796 return (size_t)GetVirtualTransactionInputSize(tx_in);
797 }
798
799 BOOST_FIXTURE_TEST_CASE(dummy_input_size_test, TestChain100Setup)
800 {
801 BOOST_CHECK_EQUAL(CalculateNestedKeyhashInputSize(false), DUMMY_NESTED_P2WPKH_INPUT_SIZE);
802 BOOST_CHECK_EQUAL(CalculateNestedKeyhashInputSize(true), DUMMY_NESTED_P2WPKH_INPUT_SIZE);
803 }
804
805 bool malformed_descriptor(std::ios_base::failure e)
806 {
807 std::string s(e.what());
808 return s.find("Missing checksum") != std::string::npos;
809 }
810
811 BOOST_FIXTURE_TEST_CASE(wallet_descriptor_test, BasicTestingSetup)
812 {
813 std::vector<unsigned char> malformed_record;
814 VectorWriter vw{malformed_record, 0};
815 vw << std::string("notadescriptor");
816 vw << uint64_t{0};
817 vw << int32_t{0};
818 vw << int32_t{0};
819 vw << int32_t{1};
820
821 SpanReader vr{malformed_record};
822 WalletDescriptor w_desc;
823 BOOST_CHECK_EXCEPTION(vr >> w_desc, std::ios_base::failure, malformed_descriptor);
824 }
825
826 //! Test CWallet::Create() and its behavior handling potential race
827 //! conditions if it's called the same time an incoming transaction shows up in
828 //! the mempool or a new block.
829 //!
830 //! It isn't possible to verify there aren't race condition in every case, so
831 //! this test just checks two specific cases and ensures that timing of
832 //! notifications in these cases doesn't prevent the wallet from detecting
833 //! transactions.
834 //!
835 //! In the first case, block and mempool transactions are created before the
836 //! wallet is loaded, but notifications about these transactions are delayed
837 //! until after it is loaded. The notifications are superfluous in this case, so
838 //! the test verifies the transactions are detected before they arrive.
839 //!
840 //! In the second case, block and mempool transactions are created after the
841 //! wallet rescan and notifications are immediately synced, to verify the wallet
842 //! must already have a handler in place for them, and there's no gap after
843 //! rescanning where new transactions in new blocks could be lost.
844 BOOST_FIXTURE_TEST_CASE(CreateWallet, TestChain100Setup)
845 {
846 // FIXME: this test fails for some reason if there's a flush
847 g_low_memory_threshold = 0;
848
849 m_args.ForceSetArg("-unsafesqlitesync", "1");
850 // Create new wallet with known key and unload it.
851 WalletContext context;
852 context.args = &m_args;
853 context.chain = m_node.chain.get();
854 auto wallet = TestLoadWallet(context);
855 CKey key = GenerateRandomKey();
856 AddKey(*wallet, key);
857 TestUnloadWallet(std::move(wallet));
858
859
860 // Add log hook to detect AddToWallet events from rescans, blockConnected,
861 // and transactionAddedToMempool notifications
862 int addtx_count = 0;
863 DebugLogHelper addtx_counter("[default wallet] AddToWallet", [&](const std::string* s) {
864 if (s) ++addtx_count;
865 return false;
866 });
867
868
869 bool rescan_completed = false;
870 DebugLogHelper rescan_check("[default wallet] Rescan completed", [&](const std::string* s) {
871 if (s) rescan_completed = true;
872 return false;
873 });
874
875
876 // Block the queue to prevent the wallet receiving blockConnected and
877 // transactionAddedToMempool notifications, and create block and mempool
878 // transactions paying to the wallet
879 std::promise<void> promise;
880 m_node.validation_signals->CallFunctionInValidationInterfaceQueue([&promise] {
881 promise.get_future().wait();
882 });
883 std::string error;
884 m_coinbase_txns.push_back(CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())).vtx[0]);
885 auto block_tx = TestSimpleSpend(*m_coinbase_txns[0], 0, coinbaseKey, GetScriptForRawPubKey(key.GetPubKey()));
886 m_coinbase_txns.push_back(CreateAndProcessBlock({block_tx}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())).vtx[0]);
887 auto mempool_tx = TestSimpleSpend(*m_coinbase_txns[1], 0, coinbaseKey, GetScriptForDestination(PKHash(key.GetPubKey())));
888 BOOST_CHECK(m_node.chain->broadcastTransaction(MakeTransactionRef(mempool_tx), DEFAULT_TRANSACTION_MAXFEE, false, error));
889
890
891 // Reload wallet and make sure new transactions are detected despite events
892 // being blocked
893 // Loading will also ask for current mempool transactions
894 wallet = TestLoadWallet(context);
895 BOOST_CHECK(rescan_completed);
896 // AddToWallet events for block_tx and mempool_tx (x2)
897 BOOST_CHECK_EQUAL(addtx_count, 3);
898 {
899 LOCK(wallet->cs_wallet);
900 BOOST_CHECK_EQUAL(wallet->mapWallet.count(block_tx.GetHash()), 1U);
901 BOOST_CHECK_EQUAL(wallet->mapWallet.count(mempool_tx.GetHash()), 1U);
902 }
903
904
905 // Unblock notification queue and make sure stale blockConnected and
906 // transactionAddedToMempool events are processed
907 promise.set_value();
908 m_node.validation_signals->SyncWithValidationInterfaceQueue();
909 // AddToWallet events for block_tx and mempool_tx events are counted a
910 // second time as the notification queue is processed
911 BOOST_CHECK_EQUAL(addtx_count, 5);
912
913
914 TestUnloadWallet(std::move(wallet));
915
916
917 // Load wallet again, this time creating new block and mempool transactions
918 // paying to the wallet as the wallet finishes loading and syncing the
919 // queue so the events have to be handled immediately. Releasing the wallet
920 // lock during the sync is a little artificial but is needed to avoid a
921 // deadlock during the sync and simulates a new block notification happening
922 // as soon as possible.
923 addtx_count = 0;
924 auto handler = HandleLoadWallet(context, [&](std::unique_ptr<interfaces::Wallet> wallet) {
925 BOOST_CHECK(rescan_completed);
926 m_coinbase_txns.push_back(CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())).vtx[0]);
927 block_tx = TestSimpleSpend(*m_coinbase_txns[2], 0, coinbaseKey, GetScriptForRawPubKey(key.GetPubKey()));
928 m_coinbase_txns.push_back(CreateAndProcessBlock({block_tx}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())).vtx[0]);
929 mempool_tx = TestSimpleSpend(*m_coinbase_txns[3], 0, coinbaseKey, GetScriptForDestination(PKHash(key.GetPubKey())));
930 BOOST_CHECK(m_node.chain->broadcastTransaction(MakeTransactionRef(mempool_tx), DEFAULT_TRANSACTION_MAXFEE, false, error));
931 m_node.validation_signals->SyncWithValidationInterfaceQueue();
932 });
933 wallet = TestLoadWallet(context);
934 // Since mempool transactions are requested at the end of loading, there will
935 // be 2 additional AddToWallet calls, one from the previous test, and a duplicate for mempool_tx
936 BOOST_CHECK_EQUAL(addtx_count, 2 + 2);
937 {
938 LOCK(wallet->cs_wallet);
939 BOOST_CHECK_EQUAL(wallet->mapWallet.count(block_tx.GetHash()), 1U);
940 BOOST_CHECK_EQUAL(wallet->mapWallet.count(mempool_tx.GetHash()), 1U);
941 }
942
943
944 TestUnloadWallet(std::move(wallet));
945 }
946
947 BOOST_FIXTURE_TEST_CASE(CreateWalletWithoutChain, BasicTestingSetup)
948 {
949 WalletContext context;
950 context.args = &m_args;
951 auto wallet = TestLoadWallet(context);
952 BOOST_CHECK(wallet);
953 WaitForDeleteWallet(std::move(wallet));
954 }
955
956 BOOST_FIXTURE_TEST_CASE(RemoveTxs, TestChain100Setup)
957 {
958 m_args.ForceSetArg("-unsafesqlitesync", "1");
959 WalletContext context;
960 context.args = &m_args;
961 context.chain = m_node.chain.get();
962 auto wallet = TestLoadWallet(context);
963 CKey key = GenerateRandomKey();
964 AddKey(*wallet, key);
965
966 std::string error;
967 m_coinbase_txns.push_back(CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())).vtx[0]);
968 auto block_tx = TestSimpleSpend(*m_coinbase_txns[0], 0, coinbaseKey, GetScriptForRawPubKey(key.GetPubKey()));
969 CreateAndProcessBlock({block_tx}, GetScriptForRawPubKey(coinbaseKey.GetPubKey()));
970
971 m_node.validation_signals->SyncWithValidationInterfaceQueue();
972
973 {
974 auto block_hash = block_tx.GetHash();
975 auto prev_tx = m_coinbase_txns[0];
976
977 LOCK(wallet->cs_wallet);
978 BOOST_CHECK(wallet->HasWalletSpend(prev_tx));
979 BOOST_CHECK_EQUAL(wallet->mapWallet.count(block_hash), 1u);
980
981 std::vector<uint256> vHashIn{ block_hash };
982 BOOST_CHECK(wallet->RemoveTxs(vHashIn));
983
984 BOOST_CHECK(!wallet->HasWalletSpend(prev_tx));
985 BOOST_CHECK_EQUAL(wallet->mapWallet.count(block_hash), 0u);
986 }
987
988 TestUnloadWallet(std::move(wallet));
989 }
990
991 /**
992 * Checks a wallet invalid state where the inputs (prev-txs) of a new arriving transaction are not marked dirty,
993 * while the transaction that spends them exist inside the in-memory wallet tx map (not stored on db due a db write failure).
994 */
995 BOOST_FIXTURE_TEST_CASE(wallet_sync_tx_invalid_state_test, TestingSetup)
996 {
997 CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase());
998 {
999 LOCK(wallet.cs_wallet);
1000 wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
1001 wallet.SetupDescriptorScriptPubKeyMans();
1002 }
1003
1004 // Add tx to wallet
1005 const auto op_dest{*Assert(wallet.GetNewDestination(OutputType::BECH32M, ""))};
1006
1007 CMutableTransaction mtx;
1008 mtx.vout.emplace_back(COIN, GetScriptForDestination(op_dest));
1009 mtx.vin.emplace_back(Txid::FromUint256(m_rng.rand256()), 0);
1010 const auto& tx_id_to_spend = wallet.AddToWallet(MakeTransactionRef(mtx), TxStateInMempool{})->GetHash();
1011
1012 {
1013 // Cache and verify available balance for the wtx
1014 LOCK(wallet.cs_wallet);
1015 const CWalletTx* wtx_to_spend = wallet.GetWalletTx(tx_id_to_spend);
1016 BOOST_CHECK_EQUAL(CachedTxGetAvailableCredit(wallet, *wtx_to_spend), 1 * COIN);
1017 }
1018
1019 // Now the good case:
1020 // 1) Add a transaction that spends the previously created transaction
1021 // 2) Verify that the available balance of this new tx and the old one is updated (prev tx is marked dirty)
1022
1023 mtx.vin.clear();
1024 mtx.vin.emplace_back(tx_id_to_spend, 0);
1025 wallet.transactionAddedToMempool(MakeTransactionRef(mtx));
1026 const auto good_tx_id{mtx.GetHash()};
1027
1028 {
1029 // Verify balance update for the new tx and the old one
1030 LOCK(wallet.cs_wallet);
1031 const CWalletTx* new_wtx = wallet.GetWalletTx(good_tx_id.ToUint256());
1032 BOOST_CHECK_EQUAL(CachedTxGetAvailableCredit(wallet, *new_wtx), 1 * COIN);
1033
1034 // Now the old wtx
1035 const CWalletTx* wtx_to_spend = wallet.GetWalletTx(tx_id_to_spend);
1036 BOOST_CHECK_EQUAL(CachedTxGetAvailableCredit(wallet, *wtx_to_spend), 0 * COIN);
1037 }
1038
1039 // Now the bad case:
1040 // 1) Make db always fail
1041 // 2) Try to add a transaction that spends the previously created transaction and
1042 // verify that we are not moving forward if the wallet cannot store it
1043 GetMockableDatabase(wallet).m_pass = false;
1044 mtx.vin.clear();
1045 mtx.vin.emplace_back(good_tx_id, 0);
1046 BOOST_CHECK_EXCEPTION(wallet.transactionAddedToMempool(MakeTransactionRef(mtx)),
1047 std::runtime_error,
1048 HasReason("DB error adding transaction to wallet, write failed"));
1049 }
1050
1051 BOOST_AUTO_TEST_SUITE_END()
1052 } // namespace wallet
1053