Line data Source code
1 : // Copyright (c) 2012-2021 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 <future>
8 : #include <iostream>
9 : #include <memory>
10 : #include <stdint.h>
11 : #include <vector>
12 :
13 : #include <coinjoin/client.h>
14 : #include <interfaces/chain.h>
15 : #include <interfaces/coinjoin.h>
16 : #include <key_io.h>
17 : #include <node/blockstorage.h>
18 : #include <policy/policy.h>
19 : #include <rpc/rawtransaction_util.h>
20 : #include <rpc/server.h>
21 : #include <test/util/logging.h>
22 : #include <test/util/setup_common.h>
23 : #include <util/translation.h>
24 : #include <policy/settings.h>
25 : #include <validation.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 <univalue.h>
35 :
36 : using node::MAX_BLOCKFILE_SIZE;
37 : using node::UnlinkPrunedFiles;
38 :
39 : namespace wallet {
40 : RPCHelpMan importmulti();
41 : RPCHelpMan dumpwallet();
42 : RPCHelpMan importwallet();
43 : extern RPCHelpMan getnewaddress();
44 : extern RPCHelpMan getrawchangeaddress();
45 : extern RPCHelpMan getaddressinfo();
46 : extern RPCHelpMan addmultisigaddress();
47 :
48 : // Ensure that fee levels defined in the wallet are at least as high
49 : // as the default levels for node policy.
50 : static_assert(DEFAULT_TRANSACTION_MINFEE >= DEFAULT_MIN_RELAY_TX_FEE, "wallet minimum fee is smaller than default relay fee");
51 :
52 146 : BOOST_FIXTURE_TEST_SUITE(wallet_tests, WalletTestingSetup)
53 :
54 5 : static std::shared_ptr<CWallet> TestLoadWallet(WalletContext& context)
55 : {
56 5 : DatabaseOptions options;
57 5 : options.create_flags = WALLET_FLAG_DESCRIPTORS;
58 : DatabaseStatus status;
59 5 : bilingual_str error;
60 5 : std::vector<bilingual_str> warnings;
61 5 : auto database = MakeWalletDatabase("", options, status, error);
62 5 : auto wallet = CWallet::Create(context, "", std::move(database), options.create_flags, error, warnings);
63 5 : if (context.coinjoin_loader) {
64 : // TODO: see CreateWalletWithoutChain
65 4 : AddWallet(context, wallet);
66 4 : }
67 5 : NotifyWalletLoaded(context, wallet);
68 5 : return wallet;
69 5 : }
70 :
71 4 : static void TestUnloadWallet(WalletContext& context, std::shared_ptr<CWallet>&& wallet)
72 : {
73 4 : std::vector<bilingual_str> warnings;
74 4 : SyncWithValidationInterfaceQueue();
75 4 : wallet->m_chain_notifications_handler.reset();
76 4 : RemoveWallet(context, wallet, /*load_on_start=*/std::nullopt, warnings);
77 4 : UnloadWallet(std::move(wallet));
78 4 : }
79 :
80 5 : static CMutableTransaction TestSimpleSpend(const CTransaction& from, uint32_t index, const CKey& key, const CScript& pubkey)
81 : {
82 5 : CMutableTransaction mtx;
83 5 : mtx.vout.emplace_back(from.vout[index].nValue - DEFAULT_TRANSACTION_MAXFEE, pubkey);
84 5 : mtx.vin.push_back({CTxIn{from.GetHash(), index}});
85 5 : FillableSigningProvider keystore;
86 5 : keystore.AddKey(key);
87 5 : std::map<COutPoint, Coin> coins;
88 5 : coins[mtx.vin[0].prevout].out = from.vout[index];
89 5 : std::map<int, bilingual_str> input_errors;
90 5 : BOOST_CHECK(SignTransaction(mtx, &keystore, coins, SIGHASH_ALL, input_errors));
91 5 : return mtx;
92 5 : }
93 :
94 7 : static void AddKey(CWallet& wallet, const CKey& key)
95 : {
96 7 : LOCK(wallet.cs_wallet);
97 7 : FlatSigningProvider provider;
98 7 : std::string error;
99 7 : std::unique_ptr<Descriptor> desc = Parse("combo(" + EncodeSecret(key) + ")", provider, error, /* require_checksum=*/ false);
100 7 : assert(desc);
101 7 : WalletDescriptor w_desc(std::move(desc), 0, 0, 1, 1);
102 7 : if (!wallet.AddWalletDescriptor(w_desc, provider, "", false)) assert(false);
103 7 : }
104 :
105 149 : BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions, TestChain100Setup)
106 : {
107 : // Cap last block file size, and mine new block in a new block file.
108 1 : CBlockIndex* oldTip = m_node.chainman->ActiveChain().Tip();
109 2 : WITH_LOCK(::cs_main, m_node.chainman->m_blockman.GetBlockFileInfo(oldTip->GetBlockPos().nFile)->nSize = MAX_BLOCKFILE_SIZE);
110 1 : CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey()));
111 1 : CBlockIndex* newTip = m_node.chainman->ActiveChain().Tip();
112 :
113 : // Verify ScanForWalletTransactions fails to read an unknown start block.
114 : {
115 1 : CWallet wallet(m_node.chain.get(), m_node.coinjoin_loader.get(), "", m_args, CreateDummyWalletDatabase());
116 1 : wallet.SetupLegacyScriptPubKeyMan();
117 : {
118 1 : LOCK(wallet.cs_wallet);
119 1 : wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
120 1 : wallet.SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash());
121 1 : }
122 1 : AddKey(wallet, coinbaseKey);
123 1 : WalletRescanReserver reserver(wallet);
124 1 : reserver.reserve();
125 1 : CWallet::ScanResult result = wallet.ScanForWalletTransactions(/*start_block=*/{}, /*start_height=*/0, /*max_height=*/{}, reserver, /*fUpdate=*/false, /*save_progress=*/false);
126 1 : BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::FAILURE);
127 1 : BOOST_CHECK(result.last_failed_block.IsNull());
128 1 : BOOST_CHECK(result.last_scanned_block.IsNull());
129 1 : BOOST_CHECK(!result.last_scanned_height);
130 1 : BOOST_CHECK_EQUAL(GetBalance(wallet).m_mine_immature, 0);
131 1 : }
132 :
133 : // Verify ScanForWalletTransactions picks up transactions in both the old
134 : // and new block files.
135 : {
136 1 : CWallet wallet(m_node.chain.get(), m_node.coinjoin_loader.get(), "", m_args, CreateMockWalletDatabase());
137 : {
138 1 : LOCK(wallet.cs_wallet);
139 1 : wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
140 1 : wallet.SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash());
141 1 : }
142 1 : AddKey(wallet, coinbaseKey);
143 1 : WalletRescanReserver reserver(wallet);
144 1 : std::chrono::steady_clock::time_point fake_time;
145 8 : reserver.setNow([&] { fake_time += 60s; return fake_time; });
146 1 : reserver.reserve();
147 :
148 : {
149 1 : CBlockLocator locator;
150 1 : BOOST_CHECK(!WalletBatch{wallet.GetDatabase()}.ReadBestBlock(locator));
151 1 : BOOST_CHECK(locator.IsNull());
152 1 : }
153 :
154 1 : CWallet::ScanResult result = wallet.ScanForWalletTransactions(/*start_block=*/oldTip->GetBlockHash(), /*start_height=*/oldTip->nHeight, /*max_height=*/{}, reserver, /*fUpdate=*/false, /*save_progress=*/true);
155 1 : BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::SUCCESS);
156 1 : BOOST_CHECK(result.last_failed_block.IsNull());
157 1 : BOOST_CHECK_EQUAL(result.last_scanned_block, newTip->GetBlockHash());
158 1 : BOOST_CHECK_EQUAL(*result.last_scanned_height, newTip->nHeight);
159 1 : BOOST_CHECK_EQUAL(GetBalance(wallet).m_mine_immature, 1000 * COIN);
160 :
161 : {
162 1 : CBlockLocator locator;
163 1 : BOOST_CHECK(WalletBatch{wallet.GetDatabase()}.ReadBestBlock(locator));
164 1 : BOOST_CHECK(!locator.IsNull());
165 1 : }
166 1 : }
167 :
168 : // Prune the older block file.
169 : int file_number;
170 : {
171 1 : LOCK(::cs_main);
172 1 : file_number = oldTip->GetBlockPos().nFile;
173 1 : Assert(m_node.chainman)->m_blockman.PruneOneBlockFile(file_number);
174 1 : }
175 1 : UnlinkPrunedFiles({file_number});
176 :
177 : // Verify ScanForWalletTransactions only picks transactions in the new block
178 : // file.
179 : {
180 1 : CWallet wallet(m_node.chain.get(), m_node.coinjoin_loader.get(), "", m_args, CreateDummyWalletDatabase());
181 : {
182 1 : LOCK(wallet.cs_wallet);
183 1 : wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
184 1 : wallet.SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash());
185 1 : }
186 1 : AddKey(wallet, coinbaseKey);
187 1 : WalletRescanReserver reserver(wallet);
188 1 : reserver.reserve();
189 1 : CWallet::ScanResult result = wallet.ScanForWalletTransactions(/*start_block=*/oldTip->GetBlockHash(), /*start_height=*/oldTip->nHeight, /*max_height=*/{}, reserver, /*fUpdate=*/false, /*save_progress=*/false);
190 1 : BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::FAILURE);
191 1 : BOOST_CHECK_EQUAL(result.last_failed_block, oldTip->GetBlockHash());
192 1 : BOOST_CHECK_EQUAL(result.last_scanned_block, newTip->GetBlockHash());
193 1 : BOOST_CHECK_EQUAL(*result.last_scanned_height, newTip->nHeight);
194 1 : BOOST_CHECK_EQUAL(GetBalance(wallet).m_mine_immature, 500 * COIN);
195 1 : }
196 :
197 : // Prune the remaining block file.
198 : {
199 1 : LOCK(::cs_main);
200 1 : file_number = newTip->GetBlockPos().nFile;
201 1 : Assert(m_node.chainman)->m_blockman.PruneOneBlockFile(file_number);
202 1 : }
203 1 : UnlinkPrunedFiles({file_number});
204 :
205 : // Verify ScanForWalletTransactions scans no blocks.
206 : {
207 1 : CWallet wallet(m_node.chain.get(), m_node.coinjoin_loader.get(), "", m_args, CreateDummyWalletDatabase());
208 : {
209 1 : LOCK(wallet.cs_wallet);
210 1 : wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
211 1 : wallet.SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash());
212 1 : }
213 1 : AddKey(wallet, coinbaseKey);
214 1 : WalletRescanReserver reserver(wallet);
215 1 : reserver.reserve();
216 1 : CWallet::ScanResult result = wallet.ScanForWalletTransactions(/*start_block=*/oldTip->GetBlockHash(), /*start_height=*/oldTip->nHeight, /*max_height=*/{}, reserver, /*fUpdate=*/false, /*save_progress=*/false);
217 1 : BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::FAILURE);
218 1 : BOOST_CHECK_EQUAL(result.last_failed_block, newTip->GetBlockHash());
219 1 : BOOST_CHECK(result.last_scanned_block.IsNull());
220 1 : BOOST_CHECK(!result.last_scanned_height);
221 1 : BOOST_CHECK_EQUAL(GetBalance(wallet).m_mine_immature, 0);
222 1 : }
223 1 : }
224 :
225 149 : BOOST_FIXTURE_TEST_CASE(importmulti_rescan, TestChain100Setup)
226 : {
227 : // Cap last block file size, and mine new block in a new block file.
228 1 : CBlockIndex* oldTip = m_node.chainman->ActiveChain().Tip();
229 2 : WITH_LOCK(::cs_main, m_node.chainman->m_blockman.GetBlockFileInfo(oldTip->GetBlockPos().nFile)->nSize = MAX_BLOCKFILE_SIZE);
230 1 : CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey()));
231 1 : CBlockIndex* newTip = m_node.chainman->ActiveChain().Tip();
232 :
233 : // Prune the older block file.
234 : int file_number;
235 : {
236 1 : LOCK(::cs_main);
237 1 : file_number = oldTip->GetBlockPos().nFile;
238 1 : Assert(m_node.chainman)->m_blockman.PruneOneBlockFile(file_number);
239 1 : }
240 1 : UnlinkPrunedFiles({file_number});
241 :
242 : // Verify importmulti RPC returns failure for a key whose creation time is
243 : // before the missing block, and success for a key whose creation time is
244 : // after.
245 : {
246 1 : const std::shared_ptr<CWallet> wallet = std::make_shared<CWallet>(m_node.chain.get(), m_node.coinjoin_loader.get(), "", m_args, CreateDummyWalletDatabase());
247 1 : wallet->SetupLegacyScriptPubKeyMan();
248 2 : WITH_LOCK(wallet->cs_wallet, wallet->SetLastBlockProcessed(newTip->nHeight, newTip->GetBlockHash()));
249 1 : WalletContext context;
250 1 : context.args = &m_args;
251 1 : AddWallet(context, wallet);
252 1 : UniValue keys;
253 1 : keys.setArray();
254 1 : UniValue key;
255 1 : key.setObject();
256 1 : key.pushKV("scriptPubKey", HexStr(GetScriptForRawPubKey(coinbaseKey.GetPubKey())));
257 1 : key.pushKV("timestamp", 0);
258 1 : key.pushKV("internal", UniValue(true));
259 1 : keys.push_back(key);
260 1 : key.clear();
261 1 : key.setObject();
262 1 : CKey futureKey = GenerateRandomKey();
263 1 : key.pushKV("scriptPubKey", HexStr(GetScriptForRawPubKey(futureKey.GetPubKey())));
264 1 : key.pushKV("timestamp", newTip->GetBlockTimeMax() + TIMESTAMP_WINDOW + 1);
265 1 : key.pushKV("internal", UniValue(true));
266 1 : keys.push_back(key);
267 1 : JSONRPCRequest request;
268 1 : request.context = context;
269 1 : request.params.setArray();
270 1 : request.params.push_back(keys);
271 :
272 1 : UniValue response = wallet::importmulti().HandleRequest(request);
273 1 : BOOST_CHECK_EQUAL(response.write(),
274 : strprintf("[{\"success\":false,\"error\":{\"code\":-1,\"message\":\"Rescan failed for key with creation "
275 : "timestamp %d. There was an error reading a block from time %d, which is after or within %d "
276 : "seconds of key creation, and could contain transactions pertaining to the key. As a result, "
277 : "transactions and coins using this key may not appear in the wallet. This error could be caused "
278 : "by pruning or data corruption (see dashd log for details) and could be dealt with by "
279 : "downloading and rescanning the relevant blocks (see -reindex and -rescan "
280 : "options).\"}},{\"success\":true}]",
281 : 0, oldTip->GetBlockTimeMax(), TIMESTAMP_WINDOW));
282 1 : RemoveWallet(context, wallet, /*load_on_start=*/std::nullopt);
283 1 : }
284 1 : }
285 :
286 : // Verify importwallet RPC starts rescan at earliest block with timestamp
287 : // greater or equal than key birthday. Previously there was a bug where
288 : // importwallet RPC would start the scan at the latest block with timestamp less
289 : // than or equal to key birthday.
290 149 : BOOST_FIXTURE_TEST_CASE(importwallet_rescan, TestChain100Setup)
291 : {
292 : // Create two blocks with same timestamp to verify that importwallet rescan
293 : // will pick up both blocks, not just the first.
294 1 : const int64_t BLOCK_TIME = m_node.chainman->ActiveChain().Tip()->GetBlockTimeMax() + 5;
295 1 : SetMockTime(BLOCK_TIME);
296 1 : m_coinbase_txns.emplace_back(CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())).vtx[0]);
297 1 : m_coinbase_txns.emplace_back(CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())).vtx[0]);
298 :
299 : // Set key birthday to block time increased by the timestamp window, so
300 : // rescan will start at the block time.
301 1 : const int64_t KEY_TIME = BLOCK_TIME + 7200;
302 1 : SetMockTime(KEY_TIME);
303 1 : m_coinbase_txns.emplace_back(CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())).vtx[0]);
304 :
305 1 : std::string backup_file = fs::PathToString(m_args.GetDataDirNet() / "wallet.backup");
306 :
307 : // Import key into wallet and call dumpwallet to create backup file.
308 : {
309 1 : WalletContext context;
310 1 : context.args = &m_args;
311 1 : const std::shared_ptr<CWallet> wallet = std::make_shared<CWallet>(m_node.chain.get(), m_node.coinjoin_loader.get(), "", m_args, CreateDummyWalletDatabase());
312 : {
313 1 : auto spk_man = wallet->GetOrCreateLegacyScriptPubKeyMan();
314 1 : LOCK2(wallet->cs_wallet, spk_man->cs_KeyStore);
315 1 : spk_man->mapKeyMetadata[coinbaseKey.GetPubKey().GetID()].nCreateTime = KEY_TIME;
316 1 : spk_man->AddKeyPubKey(coinbaseKey, coinbaseKey.GetPubKey());
317 :
318 1 : AddWallet(context, wallet);
319 1 : wallet->SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash());
320 1 : }
321 1 : JSONRPCRequest request;
322 1 : request.context = context;
323 1 : request.params.setArray();
324 1 : request.params.push_back(backup_file);
325 :
326 1 : wallet::dumpwallet().HandleRequest(request);
327 1 : RemoveWallet(context, wallet, /*load_on_start=*/std::nullopt);
328 1 : }
329 :
330 : // Call importwallet RPC and verify all blocks with timestamps >= BLOCK_TIME
331 : // were scanned, and no prior blocks were scanned.
332 : {
333 1 : const std::shared_ptr<CWallet> wallet = std::make_shared<CWallet>(m_node.chain.get(), m_node.coinjoin_loader.get(), "", m_args, CreateDummyWalletDatabase());
334 1 : LOCK(wallet->cs_wallet);
335 1 : wallet->SetupLegacyScriptPubKeyMan();
336 :
337 1 : WalletContext context;
338 1 : context.args = &m_args;
339 1 : JSONRPCRequest request;
340 1 : request.context = context;
341 1 : request.params.setArray();
342 1 : request.params.push_back(backup_file);
343 1 : AddWallet(context, wallet);
344 1 : wallet->SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash());
345 1 : wallet::importwallet().HandleRequest(request);
346 1 : RemoveWallet(context, wallet, /*load_on_start=*/std::nullopt);
347 :
348 1 : BOOST_CHECK_EQUAL(wallet->mapWallet.size(), 3U);
349 1 : BOOST_CHECK_EQUAL(m_coinbase_txns.size(), 103U);
350 104 : for (size_t i = 0; i < m_coinbase_txns.size(); ++i) {
351 103 : bool found = wallet->GetWalletTx(m_coinbase_txns[i]->GetHash());
352 103 : bool expected = i >= 100;
353 103 : BOOST_CHECK_EQUAL(found, expected);
354 103 : }
355 1 : }
356 1 : }
357 :
358 : // Check that GetImmatureCredit() returns a newly calculated value instead of
359 : // the cached value after a MarkDirty() call.
360 : //
361 : // This is a regression test written to verify a bugfix for the immature credit
362 : // function. Similar tests probably should be written for the other credit and
363 : // debit functions.
364 149 : BOOST_FIXTURE_TEST_CASE(coin_mark_dirty_immature_credit, TestChain100Setup)
365 : {
366 1 : CWallet wallet(m_node.chain.get(), m_node.coinjoin_loader.get(), "", m_args, CreateDummyWalletDatabase());
367 1 : CWalletTx wtx{m_coinbase_txns.back(), TxStateConfirmed{m_node.chainman->ActiveChain().Tip()->GetBlockHash(), m_node.chainman->ActiveChain().Height(), /*index=*/0}};
368 :
369 1 : LOCK(wallet.cs_wallet);
370 1 : wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
371 1 : wallet.SetupDescriptorScriptPubKeyMans("", "");
372 :
373 1 : wallet.SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash());
374 :
375 : // Call GetImmatureCredit() once before adding the key to the wallet to
376 : // cache the current immature credit amount, which is 0.
377 1 : BOOST_CHECK_EQUAL(CachedTxGetImmatureCredit(wallet, wtx, ISMINE_SPENDABLE), 0);
378 :
379 : // Invalidate the cached value, add the key, and make sure a new immature
380 : // credit amount is calculated.
381 1 : wtx.MarkDirty();
382 1 : AddKey(wallet, coinbaseKey);
383 1 : BOOST_CHECK_EQUAL(CachedTxGetImmatureCredit(wallet, wtx, ISMINE_SPENDABLE), 500*COIN);
384 1 : }
385 :
386 6 : static int64_t AddTx(ChainstateManager& chainman, CWallet& wallet, uint32_t lockTime, int64_t mockTime, int64_t blockTime)
387 : {
388 6 : CMutableTransaction tx;
389 6 : TxState state = TxStateInactive{};
390 6 : tx.nLockTime = lockTime;
391 6 : SetMockTime(mockTime);
392 6 : CBlockIndex* block = nullptr;
393 6 : if (blockTime > 0) {
394 5 : LOCK(::cs_main);
395 5 : auto inserted = chainman.BlockIndex().emplace(std::piecewise_construct, std::make_tuple(GetRandHash()), std::make_tuple());
396 5 : assert(inserted.second);
397 5 : const uint256& hash = inserted.first->first;
398 5 : block = &inserted.first->second;
399 5 : block->nTime = blockTime;
400 5 : block->phashBlock = &hash;
401 5 : state = TxStateConfirmed{hash, block->nHeight, /*index=*/0};
402 5 : }
403 12 : return wallet.AddToWallet(MakeTransactionRef(tx), state, [&](CWalletTx& wtx, bool /* new_tx */) {
404 : // Assign wtx.m_state to simplify test and avoid the need to simulate
405 : // reorg events. Without this, AddToWallet asserts false when the same
406 : // transaction is confirmed in different blocks.
407 6 : wtx.m_state = state;
408 6 : return true;
409 6 : })->nTimeSmart;
410 6 : }
411 :
412 : // Simple test to verify assignment of CWalletTx::nSmartTime value. Could be
413 : // expanded to cover more corner cases of smart time logic.
414 148 : BOOST_AUTO_TEST_CASE(ComputeTimeSmart)
415 : {
416 : // New transaction should use clock time if lower than block time.
417 1 : BOOST_CHECK_EQUAL(AddTx(*m_node.chainman, m_wallet, 1, 100, 120), 100);
418 :
419 : // Test that updating existing transaction does not change smart time.
420 1 : BOOST_CHECK_EQUAL(AddTx(*m_node.chainman, m_wallet, 1, 200, 220), 100);
421 :
422 : // New transaction should use clock time if there's no block time.
423 1 : BOOST_CHECK_EQUAL(AddTx(*m_node.chainman, m_wallet, 2, 300, 0), 300);
424 :
425 : // New transaction should use block time if lower than clock time.
426 1 : BOOST_CHECK_EQUAL(AddTx(*m_node.chainman, m_wallet, 3, 420, 400), 400);
427 :
428 : // New transaction should use latest entry time if higher than
429 : // min(block time, clock time).
430 1 : BOOST_CHECK_EQUAL(AddTx(*m_node.chainman, m_wallet, 4, 500, 390), 400);
431 :
432 : // If there are future entries, new transaction should use time of the
433 : // newest entry that is no more than 300 seconds ahead of the clock time.
434 1 : BOOST_CHECK_EQUAL(AddTx(*m_node.chainman, m_wallet, 5, 50, 600), 300);
435 1 : }
436 :
437 : static const DatabaseFormat DATABASE_FORMATS[] = {
438 : #ifdef USE_SQLITE
439 : DatabaseFormat::SQLITE,
440 : #endif
441 : #ifdef USE_BDB
442 : DatabaseFormat::BERKELEY,
443 : #endif
444 : };
445 :
446 6 : void TestLoadWallet(const std::string& name, DatabaseFormat format, const ArgsManager& args, std::function<void(std::shared_ptr<CWallet>)> f)
447 : {
448 6 : DatabaseOptions options;
449 6 : options.require_format = format;
450 : DatabaseStatus status;
451 6 : bilingual_str error;
452 6 : std::vector<bilingual_str> warnings;
453 6 : auto database{MakeWalletDatabase(name, options, status, error)};
454 6 : auto wallet{std::make_shared<CWallet>(/*chain=*/nullptr, /*coinjoin_loader=*/nullptr, "", args, std::move(database))};
455 6 : BOOST_CHECK_EQUAL(wallet->LoadWallet(), DBErrors::LOAD_OK);
456 12 : WITH_LOCK(wallet->cs_wallet, f(wallet));
457 6 : }
458 :
459 149 : BOOST_FIXTURE_TEST_CASE(LoadReceiveRequests, TestingSetup)
460 : {
461 3 : for (DatabaseFormat format : DATABASE_FORMATS) {
462 2 : const std::string name{strprintf("receive-requests-%i", format)};
463 4 : TestLoadWallet(name, format, m_args, [](std::shared_ptr<CWallet> wallet) EXCLUSIVE_LOCKS_REQUIRED(wallet->cs_wallet) {
464 2 : BOOST_CHECK(!wallet->IsAddressPreviouslySpent(PKHash()));
465 2 : WalletBatch batch{wallet->GetDatabase()};
466 2 : BOOST_CHECK(batch.WriteAddressPreviouslySpent(PKHash(), true));
467 2 : BOOST_CHECK(batch.WriteAddressPreviouslySpent(ScriptHash(), true));
468 2 : BOOST_CHECK(wallet->SetAddressReceiveRequest(batch, PKHash(), "0", "val_rr00"));
469 2 : BOOST_CHECK(wallet->EraseAddressReceiveRequest(batch, PKHash(), "0"));
470 2 : BOOST_CHECK(wallet->SetAddressReceiveRequest(batch, PKHash(), "1", "val_rr10"));
471 2 : BOOST_CHECK(wallet->SetAddressReceiveRequest(batch, PKHash(), "1", "val_rr11"));
472 2 : BOOST_CHECK(wallet->SetAddressReceiveRequest(batch, ScriptHash(), "2", "val_rr20"));
473 2 : });
474 4 : TestLoadWallet(name, format, m_args, [](std::shared_ptr<CWallet> wallet) EXCLUSIVE_LOCKS_REQUIRED(wallet->cs_wallet) {
475 2 : BOOST_CHECK(wallet->IsAddressPreviouslySpent(PKHash()));
476 2 : BOOST_CHECK(wallet->IsAddressPreviouslySpent(ScriptHash()));
477 2 : auto requests = wallet->GetAddressReceiveRequests();
478 2 : auto erequests = {"val_rr11", "val_rr20"};
479 2 : BOOST_CHECK_EQUAL_COLLECTIONS(requests.begin(), requests.end(), std::begin(erequests), std::end(erequests));
480 2 : WalletBatch batch{wallet->GetDatabase()};
481 2 : BOOST_CHECK(batch.WriteAddressPreviouslySpent(PKHash(), false));
482 2 : BOOST_CHECK(batch.EraseAddressData(ScriptHash()));
483 2 : });
484 4 : TestLoadWallet(name, format, m_args, [](std::shared_ptr<CWallet> wallet) EXCLUSIVE_LOCKS_REQUIRED(wallet->cs_wallet) {
485 2 : BOOST_CHECK(!wallet->IsAddressPreviouslySpent(PKHash()));
486 2 : BOOST_CHECK(!wallet->IsAddressPreviouslySpent(ScriptHash()));
487 2 : auto requests = wallet->GetAddressReceiveRequests();
488 2 : auto erequests = {"val_rr11"};
489 2 : BOOST_CHECK_EQUAL_COLLECTIONS(requests.begin(), requests.end(), std::begin(erequests), std::end(erequests));
490 2 : });
491 2 : }
492 1 : }
493 :
494 : // Test some watch-only LegacyScriptPubKeyMan methods by the procedure of loading (LoadWatchOnly),
495 : // checking (HaveWatchOnly), getting (GetWatchPubKey) and removing (RemoveWatchOnly) a
496 : // given PubKey, resp. its corresponding P2PK Script. Results of the impact on
497 : // the address -> PubKey map is dependent on whether the PubKey is a point on the curve
498 5 : static void TestWatchOnlyPubKey(LegacyScriptPubKeyMan* spk_man, const CPubKey& add_pubkey)
499 : {
500 5 : CScript p2pk = GetScriptForRawPubKey(add_pubkey);
501 5 : CKeyID add_address = add_pubkey.GetID();
502 5 : CPubKey found_pubkey;
503 5 : LOCK(spk_man->cs_KeyStore);
504 :
505 : // all Scripts (i.e. also all PubKeys) are added to the general watch-only set
506 5 : BOOST_CHECK(!spk_man->HaveWatchOnly(p2pk));
507 5 : spk_man->LoadWatchOnly(p2pk);
508 5 : BOOST_CHECK(spk_man->HaveWatchOnly(p2pk));
509 :
510 : // only PubKeys on the curve shall be added to the watch-only address -> PubKey map
511 5 : bool is_pubkey_fully_valid = add_pubkey.IsFullyValid();
512 5 : if (is_pubkey_fully_valid) {
513 2 : BOOST_CHECK(spk_man->GetWatchPubKey(add_address, found_pubkey));
514 2 : BOOST_CHECK(found_pubkey == add_pubkey);
515 2 : } else {
516 3 : BOOST_CHECK(!spk_man->GetWatchPubKey(add_address, found_pubkey));
517 3 : BOOST_CHECK(found_pubkey == CPubKey()); // passed key is unchanged
518 : }
519 :
520 5 : spk_man->RemoveWatchOnly(p2pk);
521 5 : BOOST_CHECK(!spk_man->HaveWatchOnly(p2pk));
522 :
523 5 : if (is_pubkey_fully_valid) {
524 2 : BOOST_CHECK(!spk_man->GetWatchPubKey(add_address, found_pubkey));
525 2 : BOOST_CHECK(found_pubkey == add_pubkey); // passed key is unchanged
526 2 : }
527 5 : }
528 :
529 : // Cryptographically invalidate a PubKey whilst keeping length and first byte
530 2 : static void PollutePubKey(CPubKey& pubkey)
531 : {
532 2 : assert(pubkey.size() >= 1);
533 2 : std::vector<unsigned char> pubkey_raw;
534 2 : pubkey_raw.push_back(pubkey[0]);
535 2 : pubkey_raw.insert(pubkey_raw.end(), pubkey.size() - 1, 0);
536 2 : pubkey = CPubKey(pubkey_raw);
537 2 : assert(!pubkey.IsFullyValid());
538 2 : assert(pubkey.IsValid());
539 2 : }
540 :
541 : // Test watch-only logic for PubKeys
542 148 : BOOST_AUTO_TEST_CASE(WatchOnlyPubKeys)
543 : {
544 1 : CKey key;
545 1 : CPubKey pubkey;
546 1 : LegacyScriptPubKeyMan* spk_man = m_wallet.GetOrCreateLegacyScriptPubKeyMan();
547 :
548 1 : BOOST_CHECK(!spk_man->HaveWatchOnly());
549 :
550 : // uncompressed valid PubKey
551 1 : key.MakeNewKey(false);
552 1 : pubkey = key.GetPubKey();
553 1 : assert(!pubkey.IsCompressed());
554 1 : TestWatchOnlyPubKey(spk_man, pubkey);
555 :
556 : // uncompressed cryptographically invalid PubKey
557 1 : PollutePubKey(pubkey);
558 1 : TestWatchOnlyPubKey(spk_man, pubkey);
559 :
560 : // compressed valid PubKey
561 1 : key.MakeNewKey(true);
562 1 : pubkey = key.GetPubKey();
563 1 : assert(pubkey.IsCompressed());
564 1 : TestWatchOnlyPubKey(spk_man, pubkey);
565 :
566 : // compressed cryptographically invalid PubKey
567 1 : PollutePubKey(pubkey);
568 1 : TestWatchOnlyPubKey(spk_man, pubkey);
569 :
570 : // invalid empty PubKey
571 1 : pubkey = CPubKey();
572 1 : TestWatchOnlyPubKey(spk_man, pubkey);
573 1 : }
574 :
575 : class ListCoinsTestingSetup : public TestChain100Setup
576 : {
577 : public:
578 2 : ListCoinsTestingSetup()
579 : {
580 2 : CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey()));
581 2 : wallet = CreateSyncedWallet(*m_node.chain, *m_node.coinjoin_loader, m_node.chainman->ActiveChain(), m_args, coinbaseKey);
582 2 : }
583 :
584 2 : ~ListCoinsTestingSetup()
585 : {
586 2 : wallet.reset();
587 2 : }
588 :
589 1 : CWalletTx& AddTx(CRecipient recipient)
590 : {
591 1 : CTransactionRef tx;
592 1 : CCoinControl dummy;
593 : {
594 1 : auto res = CreateTransaction(*wallet, {recipient}, RANDOM_CHANGE_POSITION, dummy);
595 1 : BOOST_CHECK(res);
596 1 : tx = res->tx;
597 1 : }
598 1 : wallet->CommitTransaction(tx, {}, {});
599 1 : CMutableTransaction blocktx;
600 : {
601 1 : LOCK(wallet->cs_wallet);
602 1 : blocktx = CMutableTransaction(*wallet->mapWallet.at(tx->GetHash()).tx);
603 1 : }
604 1 : CreateAndProcessBlock({CMutableTransaction(blocktx)}, GetScriptForRawPubKey(coinbaseKey.GetPubKey()));
605 :
606 1 : LOCK(wallet->cs_wallet);
607 1 : wallet->SetLastBlockProcessed(wallet->GetLastBlockHeight() + 1, m_node.chainman->ActiveChain().Tip()->GetBlockHash());
608 1 : auto it = wallet->mapWallet.find(tx->GetHash());
609 1 : BOOST_CHECK(it != wallet->mapWallet.end());
610 1 : it->second.m_state = TxStateConfirmed{m_node.chainman->ActiveChain().Tip()->GetBlockHash(), m_node.chainman->ActiveChain().Height(), /*index=*/1};
611 1 : return it->second;
612 1 : }
613 :
614 : std::unique_ptr<CWallet> wallet;
615 : };
616 :
617 148 : BOOST_FIXTURE_TEST_CASE(ListCoinsTest, ListCoinsTestingSetup)
618 : {
619 1 : std::string coinbaseAddress = coinbaseKey.GetPubKey().GetID().ToString();
620 :
621 : // Confirm ListCoins initially returns 1 coin grouped under coinbaseKey
622 : // address.
623 1 : std::map<CTxDestination, std::vector<COutput>> list;
624 : {
625 1 : LOCK(wallet->cs_wallet);
626 1 : list = ListCoins(*wallet);
627 1 : }
628 1 : BOOST_CHECK_EQUAL(list.size(), 1U);
629 1 : BOOST_CHECK_EQUAL(std::get<PKHash>(list.begin()->first).ToString(), coinbaseAddress);
630 1 : BOOST_CHECK_EQUAL(list.begin()->second.size(), 1U);
631 :
632 : // Check initial balance from one mature coinbase transaction.
633 1 : BOOST_CHECK_EQUAL(500 * COIN, GetAvailableBalance(*wallet));
634 :
635 : // Add a transaction creating a change address, and confirm ListCoins still
636 : // returns the coin associated with the change address underneath the
637 : // coinbaseKey pubkey, even though the change address has a different
638 : // pubkey.
639 1 : AddTx(CRecipient{GetScriptForRawPubKey({}), 1 * COIN, false /* subtract fee */});
640 : {
641 1 : LOCK(wallet->cs_wallet);
642 1 : list = ListCoins(*wallet);
643 1 : }
644 1 : BOOST_CHECK_EQUAL(list.size(), 1U);
645 1 : BOOST_CHECK_EQUAL(std::get<PKHash>(list.begin()->first).ToString(), coinbaseAddress);
646 1 : BOOST_CHECK_EQUAL(list.begin()->second.size(), 2U);
647 :
648 : // Lock both coins. Confirm number of available coins drops to 0.
649 : {
650 1 : LOCK(wallet->cs_wallet);
651 1 : BOOST_CHECK_EQUAL(AvailableCoinsListUnspent(*wallet).size(), 2U);
652 1 : }
653 2 : for (const auto& group : list) {
654 3 : for (const auto& coin : group.second) {
655 2 : LOCK(wallet->cs_wallet);
656 2 : wallet->LockCoin(coin.outpoint);
657 2 : }
658 : }
659 : {
660 1 : LOCK(wallet->cs_wallet);
661 1 : BOOST_CHECK_EQUAL(AvailableCoinsListUnspent(*wallet).size(), 0U);
662 1 : }
663 : // Confirm ListCoins still returns same result as before, despite coins
664 : // being locked.
665 : {
666 1 : LOCK(wallet->cs_wallet);
667 1 : list = ListCoins(*wallet);
668 1 : }
669 1 : BOOST_CHECK_EQUAL(list.size(), 1U);
670 1 : BOOST_CHECK_EQUAL(std::get<PKHash>(list.begin()->first).ToString(), coinbaseAddress);
671 1 : BOOST_CHECK_EQUAL(list.begin()->second.size(), 2U);
672 1 : }
673 :
674 149 : BOOST_FIXTURE_TEST_CASE(wallet_disableprivkeys, TestChain100Setup)
675 : {
676 : {
677 1 : const std::shared_ptr<CWallet> wallet = std::make_shared<CWallet>(m_node.chain.get(), m_node.coinjoin_loader.get(), "", m_args, CreateDummyWalletDatabase());
678 1 : wallet->SetupLegacyScriptPubKeyMan();
679 1 : wallet->SetMinVersion(FEATURE_LATEST);
680 1 : wallet->SetWalletFlag(WALLET_FLAG_DISABLE_PRIVATE_KEYS);
681 1 : BOOST_CHECK(!wallet->TopUpKeyPool(1000));
682 1 : BOOST_CHECK(!wallet->GetNewDestination(""));
683 1 : }
684 : {
685 1 : const std::shared_ptr<CWallet> wallet = std::make_shared<CWallet>(m_node.chain.get(), m_node.coinjoin_loader.get(), "", m_args, CreateDummyWalletDatabase());
686 1 : LOCK(wallet->cs_wallet);
687 1 : wallet->SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
688 1 : wallet->SetMinVersion(FEATURE_LATEST);
689 1 : wallet->SetWalletFlag(WALLET_FLAG_DISABLE_PRIVATE_KEYS);
690 1 : BOOST_CHECK(!wallet->GetNewDestination(""));
691 1 : }
692 1 : }
693 :
694 : // Explicit calculation which is used to test the wallet constant
695 : // We get the same virtual size due to rounding(weight/4) for both use_max_sig values
696 2 : static size_t CalculateNestedKeyhashInputSize(bool use_max_sig)
697 : {
698 : // Generate ephemeral valid pubkey
699 2 : CKey key = GenerateRandomKey();
700 2 : CPubKey pubkey = key.GetPubKey();
701 :
702 : // Generate pubkey hash
703 2 : uint160 key_hash(Hash160(pubkey));
704 :
705 : // Create inner-script to enter into keystore. Key hash can't be 0...
706 2 : CScript inner_script = CScript() << OP_0 << std::vector<unsigned char>(key_hash.begin(), key_hash.end());
707 :
708 : // Create outer P2SH script for the output
709 2 : CScript script_pubkey = GetScriptForRawPubKey(pubkey);
710 :
711 : // Add inner-script to key store and key to watchonly
712 2 : FillableSigningProvider keystore;
713 2 : keystore.AddCScript(inner_script);
714 2 : keystore.AddKeyPubKey(key, pubkey);
715 :
716 : // Fill in dummy signatures for fee calculation.
717 2 : SignatureData sig_data;
718 :
719 2 : if (!ProduceSignature(keystore, use_max_sig ? DUMMY_MAXIMUM_SIGNATURE_CREATOR : DUMMY_SIGNATURE_CREATOR, script_pubkey, sig_data)) {
720 : // We're hand-feeding it correct arguments; shouldn't happen
721 0 : assert(false);
722 : }
723 :
724 2 : CTxIn tx_in;
725 2 : UpdateInput(tx_in, sig_data);
726 2 : return ::GetSerializeSize(tx_in, PROTOCOL_VERSION);
727 2 : }
728 :
729 149 : BOOST_FIXTURE_TEST_CASE(dummy_input_size_test, TestChain100Setup)
730 : {
731 1 : BOOST_CHECK_EQUAL(CalculateNestedKeyhashInputSize(false), DUMMY_NESTED_P2PKH_INPUT_SIZE);
732 1 : BOOST_CHECK_EQUAL(CalculateNestedKeyhashInputSize(true), DUMMY_NESTED_P2PKH_INPUT_SIZE + 1);
733 1 : }
734 :
735 1 : bool malformed_descriptor(std::ios_base::failure e)
736 : {
737 1 : std::string s(e.what());
738 1 : return s.find("Missing checksum") != std::string::npos;
739 1 : }
740 :
741 149 : BOOST_FIXTURE_TEST_CASE(wallet_descriptor_test, BasicTestingSetup)
742 : {
743 1 : std::vector<unsigned char> malformed_record;
744 1 : CVectorWriter vw(0, 0, malformed_record, 0);
745 1 : vw << std::string("notadescriptor");
746 1 : vw << uint64_t{0};
747 1 : vw << int32_t{0};
748 1 : vw << int32_t{0};
749 1 : vw << int32_t{1};
750 :
751 1 : SpanReader vr{0, 0, malformed_record};
752 1 : WalletDescriptor w_desc;
753 2 : BOOST_CHECK_EXCEPTION(vr >> w_desc, std::ios_base::failure, malformed_descriptor);
754 2 : }
755 :
756 : //! Test CWallet::Create() and its behavior handling potential race
757 : //! conditions if it's called the same time an incoming transaction shows up in
758 : //! the mempool or a new block.
759 : //!
760 : //! It isn't possible to verify there aren't race condition in every case, so
761 : //! this test just checks two specific cases and ensures that timing of
762 : //! notifications in these cases doesn't prevent the wallet from detecting
763 : //! transactions.
764 : //!
765 : //! In the first case, block and mempool transactions are created before the
766 : //! wallet is loaded, but notifications about these transactions are delayed
767 : //! until after it is loaded. The notifications are superfluous in this case, so
768 : //! the test verifies the transactions are detected before they arrive.
769 : //!
770 : //! In the second case, block and mempool transactions are created after the
771 : //! wallet rescan and notifications are immediately synced, to verify the wallet
772 : //! must already have a handler in place for them, and there's no gap after
773 : //! rescanning where new transactions in new blocks could be lost.
774 149 : BOOST_FIXTURE_TEST_CASE(CreateWallet, TestChain100Setup)
775 : {
776 1 : m_args.ForceSetArg("-unsafesqlitesync", "1");
777 : // Create new wallet with known key and unload it.
778 1 : WalletContext context;
779 1 : context.args = &m_args;
780 1 : context.chain = m_node.chain.get();
781 1 : context.coinjoin_loader = m_node.coinjoin_loader.get();
782 1 : auto wallet = TestLoadWallet(context);
783 1 : CKey key = GenerateRandomKey();
784 1 : AddKey(*wallet, key);
785 1 : TestUnloadWallet(context, std::move(wallet));
786 :
787 :
788 : // Add log hook to detect AddToWallet events from rescans, blockConnected,
789 : // and transactionAddedToMempool notifications
790 1 : int addtx_count = 0;
791 8 : DebugLogHelper addtx_counter("[default wallet] AddToWallet", [&](const std::string* s) {
792 7 : if (s) ++addtx_count;
793 7 : return false;
794 : });
795 :
796 :
797 1 : bool rescan_completed = false;
798 3 : DebugLogHelper rescan_check("[default wallet] Rescan completed", [&](const std::string* s) {
799 2 : if (s) rescan_completed = true;
800 2 : return false;
801 : });
802 :
803 :
804 : // Block the queue to prevent the wallet receiving blockConnected and
805 : // transactionAddedToMempool notifications, and create block and mempool
806 : // transactions paying to the wallet
807 1 : std::promise<void> promise;
808 2 : CallFunctionInValidationInterfaceQueue([&promise] {
809 1 : promise.get_future().wait();
810 1 : });
811 1 : bilingual_str error;
812 1 : m_coinbase_txns.push_back(CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())).vtx[0]);
813 1 : auto block_tx = TestSimpleSpend(*m_coinbase_txns[0], 0, coinbaseKey, GetScriptForRawPubKey(key.GetPubKey()));
814 1 : m_coinbase_txns.push_back(CreateAndProcessBlock({block_tx}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())).vtx[0]);
815 1 : auto mempool_tx = TestSimpleSpend(*m_coinbase_txns[1], 0, coinbaseKey, GetScriptForRawPubKey(key.GetPubKey()));
816 1 : BOOST_CHECK(m_node.chain->broadcastTransaction(MakeTransactionRef(mempool_tx), DEFAULT_TRANSACTION_MAXFEE, false, error));
817 :
818 :
819 : // Reload wallet and make sure new transactions are detected despite events
820 : // being blocked
821 1 : wallet = TestLoadWallet(context);
822 1 : BOOST_CHECK(rescan_completed);
823 : // AddToWallet events for block_tx and mempool_tx
824 1 : BOOST_CHECK_EQUAL(addtx_count, 2);
825 : {
826 1 : LOCK(wallet->cs_wallet);
827 1 : BOOST_CHECK_EQUAL(wallet->mapWallet.count(block_tx.GetHash()), 1U);
828 1 : BOOST_CHECK_EQUAL(wallet->mapWallet.count(mempool_tx.GetHash()), 1U);
829 1 : }
830 :
831 :
832 : // Unblock notification queue and make sure stale blockConnected and
833 : // transactionAddedToMempool events are processed
834 1 : promise.set_value();
835 1 : SyncWithValidationInterfaceQueue();
836 : // AddToWallet events for block_tx and mempool_tx events are counted a
837 : // second time as the notification queue is processed
838 1 : BOOST_CHECK_EQUAL(addtx_count, 4);
839 :
840 1 : TestUnloadWallet(context, std::move(wallet));
841 :
842 :
843 : // Load wallet again, this time creating new block and mempool transactions
844 : // paying to the wallet as the wallet finishes loading and syncing the
845 : // queue so the events have to be handled immediately. Releasing the wallet
846 : // lock during the sync is a little artificial but is needed to avoid a
847 : // deadlock during the sync and simulates a new block notification happening
848 : // as soon as possible.
849 1 : addtx_count = 0;
850 2 : auto handler = HandleLoadWallet(context, [&](std::unique_ptr<interfaces::Wallet> wallet) {
851 1 : BOOST_CHECK(rescan_completed);
852 1 : m_coinbase_txns.push_back(CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())).vtx[0]);
853 1 : block_tx = TestSimpleSpend(*m_coinbase_txns[2], 0, coinbaseKey, GetScriptForRawPubKey(key.GetPubKey()));
854 1 : m_coinbase_txns.push_back(CreateAndProcessBlock({block_tx}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())).vtx[0]);
855 1 : mempool_tx = TestSimpleSpend(*m_coinbase_txns[3], 0, coinbaseKey, GetScriptForRawPubKey(key.GetPubKey()));
856 1 : BOOST_CHECK(m_node.chain->broadcastTransaction(MakeTransactionRef(mempool_tx), DEFAULT_TRANSACTION_MAXFEE, false, error));
857 1 : SyncWithValidationInterfaceQueue();
858 1 : });
859 1 : wallet = TestLoadWallet(context);
860 1 : BOOST_CHECK_EQUAL(addtx_count, 2);
861 : {
862 1 : LOCK(wallet->cs_wallet);
863 1 : BOOST_CHECK_EQUAL(wallet->mapWallet.count(block_tx.GetHash()), 1U);
864 1 : BOOST_CHECK_EQUAL(wallet->mapWallet.count(mempool_tx.GetHash()), 1U);
865 1 : }
866 :
867 1 : TestUnloadWallet(context, std::move(wallet));
868 1 : }
869 :
870 149 : BOOST_FIXTURE_TEST_CASE(CreateWalletWithoutChain, BasicTestingSetup)
871 : {
872 1 : WalletContext context;
873 1 : context.args = &m_args;
874 1 : context.coinjoin_loader = nullptr; // TODO: FIX FIX FIX
875 1 : auto wallet = TestLoadWallet(context);
876 1 : BOOST_CHECK(wallet);
877 1 : UnloadWallet(std::move(wallet));
878 1 : }
879 :
880 149 : BOOST_FIXTURE_TEST_CASE(ZapSelectTx, TestChain100Setup)
881 : {
882 1 : m_args.ForceSetArg("-unsafesqlitesync", "1");
883 1 : WalletContext context;
884 1 : context.args = &m_args;
885 1 : context.chain = m_node.chain.get();
886 1 : context.coinjoin_loader = m_node.coinjoin_loader.get();
887 1 : auto wallet = TestLoadWallet(context);
888 1 : CKey key = GenerateRandomKey();
889 1 : AddKey(*wallet, key);
890 :
891 1 : std::string error;
892 1 : m_coinbase_txns.push_back(CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())).vtx[0]);
893 1 : auto block_tx = TestSimpleSpend(*m_coinbase_txns[0], 0, coinbaseKey, GetScriptForRawPubKey(key.GetPubKey()));
894 1 : CreateAndProcessBlock({block_tx}, GetScriptForRawPubKey(coinbaseKey.GetPubKey()));
895 :
896 1 : SyncWithValidationInterfaceQueue();
897 :
898 : {
899 1 : auto block_hash = block_tx.GetHash();
900 1 : auto prev_tx = m_coinbase_txns[0];
901 :
902 1 : LOCK(wallet->cs_wallet);
903 1 : BOOST_CHECK(wallet->HasWalletSpend(prev_tx));
904 1 : BOOST_CHECK_EQUAL(wallet->mapWallet.count(block_hash), 1u);
905 :
906 1 : std::vector<uint256> vHashIn{ block_hash }, vHashOut;
907 1 : BOOST_CHECK_EQUAL(wallet->ZapSelectTx(vHashIn, vHashOut), DBErrors::LOAD_OK);
908 :
909 1 : BOOST_CHECK(!wallet->HasWalletSpend(prev_tx));
910 1 : BOOST_CHECK_EQUAL(wallet->mapWallet.count(block_hash), 0u);
911 1 : }
912 :
913 1 : TestUnloadWallet(context, std::move(wallet));
914 1 : }
915 :
916 : /* --------------------------- Dash-specific tests start here --------------------------- */
917 : namespace {
918 : constexpr CAmount fallbackFee = 1000;
919 : } // anonymous namespace
920 :
921 1 : static void AddLegacyKey(CWallet& wallet, const CKey& key)
922 : {
923 1 : auto spk_man = wallet.GetOrCreateLegacyScriptPubKeyMan();
924 1 : LOCK2(wallet.cs_wallet, spk_man->cs_KeyStore);
925 1 : spk_man->AddKeyPubKey(key, key.GetPubKey());
926 1 : }
927 :
928 : // Verify getaddressinfo RPC produces more or less expected results
929 149 : BOOST_FIXTURE_TEST_CASE(rpc_getaddressinfo, TestChain100Setup)
930 : {
931 1 : WalletContext context;
932 1 : context.args = &m_args;
933 1 : context.chain = m_node.chain.get();
934 1 : context.coinjoin_loader = m_node.coinjoin_loader.get();
935 1 : const std::shared_ptr<CWallet> wallet = std::make_shared<CWallet>(m_node.chain.get(), m_node.coinjoin_loader.get(), "", m_args, CreateMockWalletDatabase());
936 1 : wallet->SetupLegacyScriptPubKeyMan();
937 1 : AddWallet(context, wallet);
938 1 : JSONRPCRequest request;
939 1 : request.context = context;
940 1 : UniValue response;
941 :
942 : // test p2pkh
943 1 : std::string addr;
944 1 : BOOST_CHECK_NO_THROW(addr = wallet::getrawchangeaddress().HandleRequest(request).get_str());
945 :
946 1 : request.params.clear();
947 1 : request.params.setArray();
948 1 : request.params.push_back(addr);
949 1 : BOOST_CHECK_NO_THROW(response = wallet::getaddressinfo().HandleRequest(request).get_obj());
950 :
951 1 : BOOST_CHECK_EQUAL(response.find_value("ismine").get_bool(), true);
952 1 : BOOST_CHECK_EQUAL(response.find_value("solvable").get_bool(), true);
953 1 : BOOST_CHECK_EQUAL(response.find_value("iswatchonly").get_bool(), false);
954 1 : BOOST_CHECK_EQUAL(response.find_value("isscript").get_bool(), false);
955 1 : BOOST_CHECK_EQUAL(response.find_value("ischange").get_bool(), true);
956 1 : BOOST_CHECK(response.find_value("pubkeys").isNull());
957 1 : BOOST_CHECK(response.find_value("addresses").isNull());
958 1 : BOOST_CHECK(response.find_value("sigsrequired").isNull());
959 1 : BOOST_CHECK(response.find_value("label").isNull());
960 :
961 : // test p2sh/multisig
962 1 : std::string addr1;
963 1 : std::string addr2;
964 1 : BOOST_CHECK_NO_THROW(addr1 = wallet::getnewaddress().HandleRequest(request).get_str());
965 1 : BOOST_CHECK_NO_THROW(addr2 = wallet::getnewaddress().HandleRequest(request).get_str());
966 :
967 1 : UniValue keys;
968 1 : keys.setArray();
969 1 : keys.push_back(addr1);
970 1 : keys.push_back(addr2);
971 :
972 1 : request.params.clear();
973 1 : request.params.setArray();
974 1 : request.params.push_back(2);
975 1 : request.params.push_back(keys);
976 :
977 1 : BOOST_CHECK_NO_THROW(response = wallet::addmultisigaddress().HandleRequest(request));
978 :
979 1 : std::string multisig = response.get_obj().find_value("address").get_str();
980 :
981 1 : request.params.clear();
982 1 : request.params.setArray();
983 1 : request.params.push_back(multisig);
984 1 : BOOST_CHECK_NO_THROW(response = wallet::getaddressinfo().HandleRequest(request).get_obj());
985 :
986 1 : BOOST_CHECK_EQUAL(response.find_value("ismine").get_bool(), true);
987 1 : BOOST_CHECK_EQUAL(response.find_value("solvable").get_bool(), true);
988 1 : BOOST_CHECK_EQUAL(response.find_value("iswatchonly").get_bool(), false);
989 1 : BOOST_CHECK_EQUAL(response.find_value("isscript").get_bool(), true);
990 1 : BOOST_CHECK_EQUAL(response.find_value("ischange").get_bool(), false);
991 1 : BOOST_CHECK_EQUAL(response.find_value("sigsrequired").getInt<int>(), 2);
992 1 : BOOST_CHECK(response.find_value("label").isNull());
993 :
994 1 : UniValue labels = response.find_value("labels").get_array();
995 1 : UniValue pubkeys = response.find_value("pubkeys").get_array();
996 1 : UniValue addresses = response.find_value("addresses").get_array();
997 :
998 1 : BOOST_CHECK_EQUAL(labels.size(), 1);
999 1 : BOOST_CHECK_EQUAL(labels[0].get_str(), "");
1000 1 : BOOST_CHECK_EQUAL(addresses.size(), 2);
1001 1 : BOOST_CHECK_EQUAL(addresses[0].get_str(), addr1);
1002 1 : BOOST_CHECK_EQUAL(addresses[1].get_str(), addr2);
1003 1 : BOOST_CHECK_EQUAL(pubkeys.size(), 2);
1004 :
1005 1 : RemoveWallet(context, wallet, /*load_on_start=*/std::nullopt);
1006 1 : }
1007 :
1008 : class CreateTransactionTestSetup : public TestChain100Setup
1009 : {
1010 : public:
1011 : enum ChangeTest {
1012 : Skip,
1013 : NoChangeExpected,
1014 : ChangeExpected,
1015 : };
1016 :
1017 : // Result strings to test
1018 1 : const std::string strInsufficientFunds = "Insufficient funds.";
1019 1 : const std::string strAmountNotNegative = "Transaction amounts must not be negative";
1020 1 : const std::string strAtLeastOneRecipient = "Transaction must have at least one recipient";
1021 1 : const std::string strTooSmallToPayFee = "The transaction amount is too small to pay the fee";
1022 1 : const std::string strTooSmallAfterFee = "The transaction amount is too small to send after the fee has been deducted";
1023 1 : const std::string strTooSmall = "Transaction amount too small";
1024 1 : const std::string strUnableToLocateCoinJoin1 = "Unable to locate enough non-denominated funds for this transaction.";
1025 1 : const std::string strUnableToLocateCoinJoin2 = "Unable to locate enough mixed funds for this transaction. CoinJoin uses exact denominated amounts to send funds, you might simply need to mix some more coins.";
1026 1 : const std::string strTransactionTooLarge = "Transaction too large";
1027 1 : const std::string strChangeIndexOutOfRange = "Transaction change output index out of range";
1028 1 : const std::string strExceededMaxTries = "Exceeded max tries.";
1029 1 : const std::string strMaxFeeExceeded = "Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)";
1030 :
1031 2 : CreateTransactionTestSetup()
1032 1 : : wallet{std::make_unique<CWallet>(m_node.chain.get(), m_node.coinjoin_loader.get(), "", m_args, CreateMockWalletDatabase())}
1033 : {
1034 1 : context.args = &m_args;
1035 1 : context.chain = m_node.chain.get();
1036 1 : context.coinjoin_loader = m_node.coinjoin_loader.get();
1037 1 : CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey()));
1038 1 : wallet->LoadWallet();
1039 1 : AddWallet(context, wallet);
1040 1 : AddLegacyKey(*wallet, coinbaseKey);
1041 1 : WalletRescanReserver reserver(*wallet);
1042 1 : reserver.reserve();
1043 : {
1044 1 : LOCK(wallet->cs_wallet);
1045 1 : wallet->SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash());
1046 1 : }
1047 1 : CWallet::ScanResult result = wallet->ScanForWalletTransactions(m_node.chainman->ActiveChain().Genesis()->GetBlockHash(), /*start_height=*/0, /*max_height=*/{}, reserver, /*fUpdate=*/false, /*save_progress=*/false);
1048 1 : BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::SUCCESS);
1049 1 : }
1050 :
1051 1 : ~CreateTransactionTestSetup()
1052 : {
1053 1 : RemoveWallet(context, wallet, /*load_on_start=*/std::nullopt);
1054 1 : }
1055 :
1056 : CCoinControl coinControl;
1057 : WalletContext context;
1058 : const std::shared_ptr<CWallet> wallet;
1059 :
1060 : template <typename T>
1061 200 : bool CheckEqual(const T expected, const T actual)
1062 : {
1063 200 : BOOST_CHECK_EQUAL(expected, actual);
1064 200 : return expected == actual;
1065 0 : }
1066 :
1067 109 : bool CreateTransaction(const std::vector<std::pair<CAmount, bool>>& vecEntries, bool fCreateShouldSucceed = true, ChangeTest changeTest = ChangeTest::Skip)
1068 : {
1069 109 : return CreateTransaction(vecEntries, {}, RANDOM_CHANGE_POSITION, fCreateShouldSucceed, changeTest);
1070 0 : }
1071 13 : bool CreateTransaction(const std::vector<std::pair<CAmount, bool>>& vecEntries, std::string strErrorExpected, bool fCreateShouldSucceed = true, ChangeTest changeTest = ChangeTest::Skip)
1072 : {
1073 13 : return CreateTransaction(vecEntries, strErrorExpected, RANDOM_CHANGE_POSITION, fCreateShouldSucceed, changeTest);
1074 0 : }
1075 :
1076 127 : bool CreateTransaction(const std::vector<std::pair<CAmount, bool>>& vecEntries, std::string strErrorExpected, int nChangePosRequest = RANDOM_CHANGE_POSITION, bool fCreateShouldSucceed = true, ChangeTest changeTest = ChangeTest::Skip)
1077 : {
1078 127 : CTransactionRef tx;
1079 127 : int nChangePos = nChangePosRequest;
1080 127 : bilingual_str strError;
1081 :
1082 127 : bool fCreationSucceeded{false};
1083 : {
1084 127 : auto res = wallet::CreateTransaction(*wallet, GetRecipients(vecEntries), nChangePos, coinControl);
1085 127 : if (res) {
1086 56 : fCreationSucceeded = true;
1087 56 : tx = res->tx;
1088 56 : nChangePos = res->change_pos;
1089 56 : } else {
1090 71 : strError = util::ErrorString(res);
1091 : }
1092 127 : }
1093 127 : bool fHitMaxTries = strError.original == strExceededMaxTries;
1094 : // This should never happen.
1095 127 : if (fHitMaxTries) {
1096 0 : BOOST_CHECK(!fHitMaxTries);
1097 0 : return false;
1098 : }
1099 : // Verify the creation succeeds if expected and fails if not.
1100 127 : if (!CheckEqual(fCreateShouldSucceed, fCreationSucceeded)) {
1101 0 : return false;
1102 : }
1103 : // Verify the expected error string if there is one provided
1104 127 : if (strErrorExpected.size() && !CheckEqual(strErrorExpected, strError.original)) {
1105 0 : return false;
1106 : }
1107 127 : if (!fCreateShouldSucceed) {
1108 : // No need to evaluate the following if the creation should have failed.
1109 71 : return true;
1110 : }
1111 : // Verify there is no change output if there wasn't any expected
1112 90 : bool fChangeTestPassed = changeTest == ChangeTest::Skip ||
1113 44 : (changeTest == ChangeTest::ChangeExpected && nChangePos != RANDOM_CHANGE_POSITION) ||
1114 10 : (changeTest == ChangeTest::NoChangeExpected && nChangePos == RANDOM_CHANGE_POSITION);
1115 56 : BOOST_CHECK(fChangeTestPassed);
1116 56 : if (!fChangeTestPassed) {
1117 0 : return false;
1118 : }
1119 : // Verify the change is at the requested position if there was a request
1120 56 : if (nChangePosRequest != RANDOM_CHANGE_POSITION && !CheckEqual(nChangePosRequest, nChangePos)) {
1121 0 : return false;
1122 : }
1123 : // Verify the number of requested outputs does match the resulting outputs
1124 56 : return CheckEqual(vecEntries.size(), tx->vout.size() - (nChangePos != RANDOM_CHANGE_POSITION ? 1 : 0));
1125 127 : }
1126 :
1127 133 : std::vector<CRecipient> GetRecipients(const std::vector<std::pair<CAmount, bool>>& vecEntries)
1128 : {
1129 133 : std::vector<CRecipient> vecRecipients;
1130 6319 : for (auto entry : vecEntries) {
1131 6186 : JSONRPCRequest request;
1132 6186 : request.context = context;
1133 6186 : vecRecipients.push_back({GetScriptForDestination(DecodeDestination(wallet::getnewaddress().HandleRequest(request).get_str())), entry.first, entry.second});
1134 6186 : }
1135 133 : return vecRecipients;
1136 133 : }
1137 :
1138 6 : std::vector<COutPoint> GetCoins(const std::vector<std::pair<CAmount, bool>>& vecEntries)
1139 : {
1140 6 : CTransactionRef tx;
1141 6 : int nChangePosRet{RANDOM_CHANGE_POSITION};
1142 6 : CCoinControl coinControl;
1143 : {
1144 6 : auto res = wallet::CreateTransaction(*wallet, GetRecipients(vecEntries), nChangePosRet, coinControl);
1145 6 : BOOST_CHECK(res);
1146 6 : tx = res->tx;
1147 6 : nChangePosRet = res->change_pos;
1148 6 : }
1149 6 : wallet->CommitTransaction(tx, {}, {});
1150 6 : CMutableTransaction blocktx;
1151 : {
1152 6 : LOCK(wallet->cs_wallet);
1153 6 : blocktx = CMutableTransaction(*tx);
1154 6 : }
1155 6 : CreateAndProcessBlock({CMutableTransaction(blocktx)}, GetScriptForRawPubKey(coinbaseKey.GetPubKey()));
1156 6 : LOCK(wallet->cs_wallet);
1157 6 : auto it = wallet->mapWallet.find(tx->GetHash());
1158 6 : BOOST_CHECK(it != wallet->mapWallet.end());
1159 6 : wallet->SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash());
1160 6 : it->second.m_state = TxStateConfirmed{m_node.chainman->ActiveChain().Tip()->GetBlockHash(), m_node.chainman->ActiveChain().Height(), /*index=*/1};
1161 :
1162 6 : std::vector<COutPoint> vecOutpoints;
1163 : size_t n;
1164 44 : for (n = 0; n < tx->vout.size(); ++n) {
1165 38 : if (nChangePosRet != RANDOM_CHANGE_POSITION && int(n) == nChangePosRet) {
1166 : // Skip the change output to only return the requested coins
1167 6 : continue;
1168 : }
1169 32 : vecOutpoints.push_back(COutPoint(tx->GetHash(), n));
1170 32 : }
1171 6 : assert(vecOutpoints.size() == vecEntries.size());
1172 6 : return vecOutpoints;
1173 6 : }
1174 : };
1175 :
1176 148 : BOOST_FIXTURE_TEST_CASE(CreateTransactionTest, CreateTransactionTestSetup)
1177 : {
1178 1 : minRelayTxFee = CFeeRate(DEFAULT_MIN_RELAY_TX_FEE);
1179 :
1180 7 : auto runTest = [&](const int nTestId, const CAmount nFeeRate, const std::map<int, std::pair<bool, ChangeTest>>& mapExpected) {
1181 6 : coinControl.m_feerate = CFeeRate(nFeeRate);
1182 90 : const std::map<int, std::vector<std::pair<CAmount, bool>>> mapTestCases{
1183 6 : {0, {{1000, false}}},
1184 6 : {1, {{1000, true}}},
1185 6 : {2, {{10000, false}}},
1186 6 : {3, {{10000, true}}},
1187 6 : {4, {{34000, false}, {40000, false}}},
1188 6 : {5, {{37000, false}, {40000, false}}},
1189 6 : {6, {{50000, false}, {50000, false}}},
1190 6 : {7, {{50000, true}, {50000, false}}},
1191 6 : {8, {{50000, false}, {50001, false}}},
1192 6 : {9, {{50000, true}, {50001, true}}},
1193 6 : {10, {{100000, false}}},
1194 6 : {11, {{100000, true}}},
1195 6 : {12, {{100001, false}}},
1196 6 : {13, {{100001, true}}}
1197 : };
1198 6 : assert(mapTestCases.size() == mapExpected.size());
1199 90 : for (size_t i = 0; i < mapTestCases.size(); ++i) {
1200 84 : if (!CreateTransaction(mapTestCases.at(i), mapExpected.at(i).first, mapExpected.at(i).second)) {
1201 0 : std::cout << strprintf("CreateTransactionTest failed at: %d - %d\n", nTestId, i) << std::endl;
1202 0 : }
1203 84 : }
1204 6 : };
1205 :
1206 : // First run the tests with only one input containing 100k duffs
1207 : {
1208 1 : coinControl = CCoinControl();
1209 1 : coinControl.Select(GetCoins({{100000, false}})[0]);
1210 :
1211 : // Start with fallback feerate
1212 14 : runTest(1, fallbackFee, {
1213 1 : {0, {true, ChangeTest::ChangeExpected}},
1214 1 : {1, {true, ChangeTest::ChangeExpected}},
1215 1 : {2, {true, ChangeTest::ChangeExpected}},
1216 1 : {3, {true, ChangeTest::ChangeExpected}},
1217 1 : {4, {true, ChangeTest::ChangeExpected}},
1218 1 : {5, {true, ChangeTest::ChangeExpected}},
1219 1 : {6, {false, ChangeTest::Skip}},
1220 1 : {7, {true, ChangeTest::NoChangeExpected}},
1221 1 : {8, {false, ChangeTest::Skip}},
1222 1 : {9, {false, ChangeTest::Skip}},
1223 1 : {10, {false, ChangeTest::Skip}},
1224 1 : {11, {true, ChangeTest::NoChangeExpected}},
1225 1 : {12, {false, ChangeTest::Skip}},
1226 1 : {13, {false, ChangeTest::Skip}}
1227 : });
1228 : // Now with 100x fallback feerate
1229 14 : runTest(2, fallbackFee * 100, {
1230 1 : {0, {true, ChangeTest::ChangeExpected}},
1231 1 : {1, {false, ChangeTest::Skip}},
1232 1 : {2, {true, ChangeTest::ChangeExpected}},
1233 1 : {3, {false, ChangeTest::Skip}},
1234 1 : {4, {true, ChangeTest::NoChangeExpected}},
1235 1 : {5, {false, ChangeTest::Skip}},
1236 1 : {6, {false, ChangeTest::Skip}},
1237 1 : {7, {true, ChangeTest::NoChangeExpected}},
1238 1 : {8, {false, ChangeTest::Skip}},
1239 1 : {9, {false, ChangeTest::Skip}},
1240 1 : {10, {false, ChangeTest::Skip}},
1241 1 : {11, {true, ChangeTest::NoChangeExpected}},
1242 1 : {12, {false, ChangeTest::Skip}},
1243 1 : {13, {false, ChangeTest::Skip}}
1244 : });
1245 : }
1246 : // Now use 4 different inputs with a total of 100k duff
1247 : {
1248 1 : coinControl = CCoinControl();
1249 1 : auto setCoins = GetCoins({{1000, false}, {5000, false}, {10000, false}, {84000, false}});
1250 5 : for (auto coin : setCoins) {
1251 4 : coinControl.Select(coin);
1252 : }
1253 :
1254 : // Start with fallback feerate
1255 14 : runTest(3, fallbackFee, {
1256 1 : {0, {true, ChangeTest::ChangeExpected}},
1257 1 : {1, {false, ChangeTest::Skip}},
1258 1 : {2, {true, ChangeTest::ChangeExpected}},
1259 1 : {3, {true, ChangeTest::ChangeExpected}},
1260 1 : {4, {true, ChangeTest::ChangeExpected}},
1261 1 : {5, {true, ChangeTest::ChangeExpected}},
1262 1 : {6, {false, ChangeTest::Skip}},
1263 1 : {7, {true, ChangeTest::NoChangeExpected}},
1264 1 : {8, {false, ChangeTest::Skip}},
1265 1 : {9, {false, ChangeTest::Skip}},
1266 1 : {10, {false, ChangeTest::Skip}},
1267 1 : {11, {true, ChangeTest::NoChangeExpected}},
1268 1 : {12, {false, ChangeTest::Skip}},
1269 1 : {13, {false, ChangeTest::Skip}}
1270 : });
1271 : // Now with 100x fallback feerate
1272 14 : runTest(4, fallbackFee * 100, {
1273 1 : {0, {true, ChangeTest::ChangeExpected}},
1274 1 : {1, {false, ChangeTest::Skip}},
1275 1 : {2, {true, ChangeTest::ChangeExpected}},
1276 1 : {3, {false, ChangeTest::Skip}},
1277 1 : {4, {false, ChangeTest::Skip}},
1278 1 : {5, {false, ChangeTest::Skip}},
1279 1 : {6, {false, ChangeTest::Skip}},
1280 1 : {7, {false, ChangeTest::Skip}},
1281 1 : {8, {false, ChangeTest::Skip}},
1282 1 : {9, {false, ChangeTest::Skip}},
1283 1 : {10, {false, ChangeTest::Skip}},
1284 1 : {11, {true, ChangeTest::NoChangeExpected}},
1285 1 : {12, {false, ChangeTest::Skip}},
1286 1 : {13, {false, ChangeTest::Skip}}
1287 : });
1288 1 : }
1289 :
1290 : // Last use 10 equal inputs with a total of 100k duff
1291 : {
1292 1 : coinControl = CCoinControl();
1293 6 : auto setCoins = GetCoins({{10000, false}, {10000, false}, {10000, false}, {10000, false}, {10000, false},
1294 5 : {10000, false}, {10000, false}, {10000, false}, {10000, false}, {10000, false}});
1295 :
1296 11 : for (auto coin : setCoins) {
1297 10 : coinControl.Select(coin);
1298 : }
1299 :
1300 : // Start with fallback feerate
1301 14 : runTest(5, fallbackFee, {
1302 1 : {0, {true, ChangeTest::ChangeExpected}},
1303 1 : {1, {false, ChangeTest::Skip}},
1304 1 : {2, {true, ChangeTest::ChangeExpected}},
1305 1 : {3, {true, ChangeTest::ChangeExpected}},
1306 1 : {4, {true, ChangeTest::ChangeExpected}},
1307 1 : {5, {true, ChangeTest::ChangeExpected}},
1308 1 : {6, {false, ChangeTest::Skip}},
1309 1 : {7, {true, ChangeTest::NoChangeExpected}},
1310 1 : {8, {false, ChangeTest::Skip}},
1311 1 : {9, {false, ChangeTest::Skip}},
1312 1 : {10, {false, ChangeTest::Skip}},
1313 1 : {11, {true, ChangeTest::NoChangeExpected}},
1314 1 : {12, {false, ChangeTest::Skip}},
1315 1 : {13, {false, ChangeTest::Skip}}
1316 : });
1317 : // Now with 100x fallback feerate
1318 14 : runTest(6, fallbackFee * 100, {
1319 1 : {0, {false, ChangeTest::Skip}},
1320 1 : {1, {false, ChangeTest::Skip}},
1321 1 : {2, {false, ChangeTest::Skip}},
1322 1 : {3, {false, ChangeTest::Skip}},
1323 1 : {4, {false, ChangeTest::Skip}},
1324 1 : {5, {false, ChangeTest::Skip}},
1325 1 : {6, {false, ChangeTest::Skip}},
1326 1 : {7, {false, ChangeTest::Skip}},
1327 1 : {8, {false, ChangeTest::Skip}},
1328 1 : {9, {false, ChangeTest::Skip}},
1329 1 : {10, {false, ChangeTest::Skip}},
1330 1 : {11, {false, ChangeTest::Skip}},
1331 1 : {12, {false, ChangeTest::Skip}},
1332 1 : {13, {false, ChangeTest::Skip}}
1333 : });
1334 1 : }
1335 : // Some tests without selected coins in coinControl, let the wallet decide
1336 : // which inputs to use
1337 : {
1338 1 : coinControl = CCoinControl();
1339 1 : coinControl.m_feerate = CFeeRate(fallbackFee);
1340 11 : auto setCoins = GetCoins({{1000, false}, {1000, false}, {1000, false}, {1000, false}, {1000, false},
1341 5 : {1100, false}, {1200, false}, {1300, false}, {1400, false}, {1500, false},
1342 5 : {3000, false}, {3000, false}, {2000, false}, {2000, false}, {1000, false}});
1343 : // Lock all other coins which were already in the wallet
1344 : {
1345 1 : LOCK(wallet->cs_wallet);
1346 28 : for (auto coin : AvailableCoinsListUnspent(*wallet).all()) {
1347 27 : if (std::find(setCoins.begin(), setCoins.end(), coin.outpoint) == setCoins.end()) {
1348 12 : wallet->LockCoin(coin.outpoint);
1349 12 : }
1350 27 : }
1351 1 : }
1352 :
1353 1 : BOOST_CHECK(CreateTransaction({{100, false}}, false));
1354 1 : BOOST_CHECK(CreateTransaction({{1000, true}}, true));
1355 1 : BOOST_CHECK(CreateTransaction({{1100, false}}, true));
1356 1 : BOOST_CHECK(CreateTransaction({{1100, true}}, true));
1357 1 : BOOST_CHECK(CreateTransaction({{2200, false}}, true));
1358 1 : BOOST_CHECK(CreateTransaction({{3300, false}}, true));
1359 1 : BOOST_CHECK(CreateTransaction({{4400, false}}, true));
1360 1 : BOOST_CHECK(CreateTransaction({{5500, false}}, true));
1361 1 : BOOST_CHECK(CreateTransaction({{5500, true}}, true));
1362 1 : BOOST_CHECK(CreateTransaction({{6600, false}}, true));
1363 1 : BOOST_CHECK(CreateTransaction({{7700, false}}, true));
1364 1 : BOOST_CHECK(CreateTransaction({{8800, false}}, true));
1365 1 : BOOST_CHECK(CreateTransaction({{9900, false}}, true));
1366 1 : BOOST_CHECK(CreateTransaction({{9900, true}}, true));
1367 1 : BOOST_CHECK(CreateTransaction({{10000, false}}, true));
1368 1 : BOOST_CHECK(CreateTransaction({{10000, false}, {10000, false}}, false));
1369 1 : BOOST_CHECK(CreateTransaction({{10000, false}, {12500, true}}, true));
1370 1 : BOOST_CHECK(CreateTransaction({{10000, true}, {10000, true}}, true));
1371 1 : BOOST_CHECK(CreateTransaction({{1000, false}, {2000, false}, {3000, false}, {4000, false}}, true));
1372 1 : BOOST_CHECK(CreateTransaction({{1234, false}}, true));
1373 1 : BOOST_CHECK(CreateTransaction({{1234, false}, {4321, false}}, true));
1374 1 : BOOST_CHECK(CreateTransaction({{1234, false}, {4321, false}, {5678, false}}, true));
1375 1 : BOOST_CHECK(CreateTransaction({{1234, false}, {4321, false}, {5678, false}, {8765, false}}, false));
1376 1 : BOOST_CHECK(CreateTransaction({{1234, false}, {4321, false}, {5678, false}, {8765, true}}, true));
1377 1 : BOOST_CHECK(CreateTransaction({{1000000, false}}, false));
1378 :
1379 1 : LOCK(wallet->cs_wallet);
1380 1 : wallet->UnlockAllCoins();
1381 1 : }
1382 : // Test if the change output ends up at the requested position
1383 : {
1384 1 : coinControl = CCoinControl();
1385 1 : coinControl.m_feerate = CFeeRate(fallbackFee);
1386 1 : coinControl.Select(GetCoins({{100000, false}})[0]);
1387 :
1388 1 : BOOST_CHECK(CreateTransaction({{25000, false}, {25000, false}, {25000, false}}, {}, 0, true, ChangeTest::ChangeExpected));
1389 1 : BOOST_CHECK(CreateTransaction({{25000, false}, {25000, false}, {25000, false}}, {}, 1, true, ChangeTest::ChangeExpected));
1390 1 : BOOST_CHECK(CreateTransaction({{25000, false}, {25000, false}, {25000, false}}, {}, 2, true, ChangeTest::ChangeExpected));
1391 1 : BOOST_CHECK(CreateTransaction({{25000, false}, {25000, false}, {25000, false}}, {}, 3, true, ChangeTest::ChangeExpected));
1392 : }
1393 : // Test error cases
1394 : {
1395 1 : coinControl = CCoinControl();
1396 1 : coinControl.m_feerate = CFeeRate(fallbackFee);
1397 : // First try to send something without any coins available
1398 : {
1399 : // Lock all other coins
1400 : {
1401 1 : LOCK(wallet->cs_wallet);
1402 30 : for (auto coin : AvailableCoinsListUnspent(*wallet).all()) {
1403 29 : wallet->LockCoin(coin.outpoint);
1404 29 : }
1405 1 : }
1406 :
1407 1 : BOOST_CHECK(CreateTransaction({{1000, false}}, strInsufficientFunds, false));
1408 1 : BOOST_CHECK(CreateTransaction({{1000, true}}, strInsufficientFunds, false));
1409 1 : coinControl.nCoinType = CoinType::ONLY_NONDENOMINATED;
1410 1 : BOOST_CHECK(CreateTransaction({{1000, true}}, strUnableToLocateCoinJoin1, false));
1411 1 : coinControl.nCoinType = CoinType::ONLY_FULLY_MIXED;
1412 1 : BOOST_CHECK(CreateTransaction({{1000, true}}, strUnableToLocateCoinJoin2, false));
1413 :
1414 1 : LOCK(wallet->cs_wallet);
1415 1 : wallet->UnlockAllCoins();
1416 1 : }
1417 :
1418 : // Just to create nCount output recipes to use in tests below
1419 1 : std::vector<std::pair<CAmount, bool>> vecOutputEntries{{5000, false}};
1420 4 : auto createOutputEntries = [&](int nCount) {
1421 2940 : while (vecOutputEntries.size() <= size_t(nCount)) {
1422 2937 : vecOutputEntries.push_back(vecOutputEntries.back());
1423 : }
1424 3 : if (vecOutputEntries.size() > size_t(nCount)) {
1425 3 : int nDiff = vecOutputEntries.size() - nCount;
1426 3 : vecOutputEntries.erase(vecOutputEntries.begin(), vecOutputEntries.begin() + nDiff);
1427 3 : }
1428 3 : };
1429 :
1430 1 : coinControl = CCoinControl();
1431 1 : coinControl.m_feerate = CFeeRate(fallbackFee);
1432 1 : coinControl.Select(GetCoins({{100 * COIN, false}})[0]);
1433 :
1434 1 : BOOST_CHECK(CreateTransaction({{-5000, false}}, strAmountNotNegative, false));
1435 1 : BOOST_CHECK(CreateTransaction({}, strAtLeastOneRecipient, false));
1436 1 : BOOST_CHECK(CreateTransaction({{545, false}}, strTooSmall, false));
1437 1 : BOOST_CHECK(CreateTransaction({{545, true}}, strTooSmall, false));
1438 1 : BOOST_CHECK(CreateTransaction({{546, true}}, strTooSmallAfterFee, false));
1439 :
1440 1 : createOutputEntries(100);
1441 1 : vecOutputEntries.push_back({600, true});
1442 1 : BOOST_CHECK(CreateTransaction(vecOutputEntries, strTooSmallToPayFee, false));
1443 1 : vecOutputEntries.pop_back();
1444 :
1445 1 : createOutputEntries(2934);
1446 1 : BOOST_CHECK(CreateTransaction(vecOutputEntries, {}, true));
1447 1 : createOutputEntries(2935);
1448 1 : BOOST_CHECK(CreateTransaction(vecOutputEntries, strTransactionTooLarge, false));
1449 :
1450 1 : wallet->m_default_max_tx_fee = 0;
1451 1 : BOOST_CHECK(CreateTransaction({{5000, false}}, strMaxFeeExceeded, false));
1452 1 : wallet->m_default_max_tx_fee = DEFAULT_TRANSACTION_MAXFEE;
1453 :
1454 1 : BOOST_CHECK(CreateTransaction({{5000, false}, {5000, false}, {5000, false}}, strChangeIndexOutOfRange, 4, false));
1455 1 : }
1456 1 : }
1457 :
1458 : // Check SelectCoinsGroupedByAddresses() behaviour
1459 148 : BOOST_FIXTURE_TEST_CASE(select_coins_grouped_by_addresses, ListCoinsTestingSetup)
1460 : {
1461 : // Check initial balance from one mature coinbase transaction.
1462 1 : BOOST_CHECK_EQUAL(GetAvailableBalance(*wallet), 500 * COIN);
1463 :
1464 : {
1465 1 : std::vector<CompactTallyItem> vecTally = wallet->SelectCoinsGroupedByAddresses(/*fSkipDenominated=*/false,
1466 : /*fAnonymizable=*/false,
1467 : /*fSkipUnconfirmed=*/false,
1468 : /*nMaxOupointsPerAddress=*/100);
1469 1 : BOOST_CHECK_EQUAL(vecTally.size(), 1);
1470 1 : BOOST_CHECK_EQUAL(vecTally.at(0).nAmount, 500 * COIN);
1471 1 : BOOST_CHECK_EQUAL(vecTally.at(0).outpoints.size(), 1);
1472 1 : }
1473 :
1474 : // Create two conflicting transactions, add one to the wallet and mine the other one.
1475 1 : CCoinControl dummy;
1476 1 : auto ret1 = CreateTransaction(*wallet, {CRecipient{GetScriptForRawPubKey({}), 2 * COIN, true /* subtract fee */}},
1477 : RANDOM_CHANGE_POSITION, dummy);
1478 1 : BOOST_CHECK(ret1);
1479 1 : const auto& txr1 = ret1->tx;
1480 1 : auto ret2 = CreateTransaction(*wallet, {CRecipient{GetScriptForRawPubKey({}), 1 * COIN, true /* subtract fee */}},
1481 : RANDOM_CHANGE_POSITION, dummy);
1482 1 : BOOST_CHECK(ret2);
1483 1 : const auto& txr2 = ret2->tx;
1484 1 : wallet->CommitTransaction(txr1, {}, {});
1485 1 : BOOST_CHECK_EQUAL(GetAvailableBalance(*wallet), 0);
1486 1 : CreateAndProcessBlock({CMutableTransaction(*txr2)}, GetScriptForRawPubKey({}));
1487 : {
1488 1 : LOCK(wallet->cs_wallet);
1489 1 : wallet->SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash());
1490 1 : }
1491 :
1492 : // Reveal the mined tx, it should conflict with the one we have in the wallet already.
1493 1 : WalletRescanReserver reserver(*wallet);
1494 1 : reserver.reserve();
1495 1 : auto result = wallet->ScanForWalletTransactions(m_node.chainman->ActiveChain().Genesis()->GetBlockHash(), /*start_height=*/0, /*max_height=*/{}, reserver, /*fUpdate=*/false, /*save_progress=*/false);
1496 1 : BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::SUCCESS);
1497 : {
1498 1 : LOCK(wallet->cs_wallet);
1499 1 : const auto& conflicts = wallet->GetConflicts(txr2->GetHash());
1500 1 : BOOST_CHECK_EQUAL(conflicts.size(), 2);
1501 1 : BOOST_CHECK_EQUAL(conflicts.count(txr1->GetHash()), 1);
1502 1 : BOOST_CHECK_EQUAL(conflicts.count(txr2->GetHash()), 1);
1503 1 : }
1504 :
1505 : // Committed tx is the one that should be marked as "conflicting".
1506 : // Make sure that available balance and SelectCoinsGroupedByAddresses results match.
1507 1 : const auto vecTally = wallet->SelectCoinsGroupedByAddresses(/*fSkipDenominated=*/false,
1508 : /*fAnonymizable=*/false,
1509 : /*fSkipUnconfirmed=*/false,
1510 : /*nMaxOupointsPerAddress=*/100);
1511 1 : BOOST_CHECK_EQUAL(vecTally.size(), 2);
1512 1 : BOOST_CHECK_EQUAL(vecTally.at(0).outpoints.size(), 1);
1513 1 : BOOST_CHECK_EQUAL(vecTally.at(1).outpoints.size(), 1);
1514 1 : BOOST_CHECK_EQUAL(vecTally.at(0).nAmount + vecTally.at(1).nAmount, (500 + 499) * COIN);
1515 1 : BOOST_CHECK_EQUAL(GetAvailableBalance(*wallet), (500 + 499) * COIN);
1516 1 : }
1517 :
1518 : /** RAII class that provides access to a FailDatabase. Which fails if needed. */
1519 : class FailBatch : public DatabaseBatch
1520 : {
1521 : private:
1522 : bool m_pass{true};
1523 0 : bool ReadKey(CDataStream&& key, CDataStream& value) override { return m_pass; }
1524 33 : bool WriteKey(CDataStream&& key, CDataStream&& value, bool overwrite=true) override { return m_pass; }
1525 0 : bool EraseKey(CDataStream&& key) override { return m_pass; }
1526 0 : bool HasKey(CDataStream&& key) override { return m_pass; }
1527 0 : bool ErasePrefix(Span<const std::byte> prefix) override { return m_pass; }
1528 :
1529 : public:
1530 38 : explicit FailBatch(bool pass) : m_pass(pass) {}
1531 0 : void Flush() override {}
1532 0 : void Close() override {}
1533 :
1534 0 : bool StartCursor() override { return true; }
1535 0 : bool ReadAtCursor(CDataStream& ssKey, CDataStream& ssValue, bool& complete) override { return false; }
1536 0 : void CloseCursor() override {}
1537 0 : bool TxnBegin() override { return false; }
1538 0 : bool TxnCommit() override { return false; }
1539 0 : bool TxnAbort() override { return false; }
1540 : };
1541 :
1542 : /** A dummy WalletDatabase that does nothing, only fails if needed.**/
1543 1 : class FailDatabase : public WalletDatabase
1544 : {
1545 : public:
1546 1 : bool m_pass{true}; // false when this db should fail
1547 :
1548 0 : void Open() override {};
1549 0 : void AddRef() override {}
1550 0 : void RemoveRef() override {}
1551 0 : bool Rewrite(const char* pszSkip=nullptr) override { return true; }
1552 0 : bool Backup(const std::string& strDest) const override { return true; }
1553 0 : void Close() override {}
1554 0 : void Flush() override {}
1555 0 : bool PeriodicFlush() override { return true; }
1556 30 : void IncrementUpdateCounter() override { ++nUpdateCounter; }
1557 0 : void ReloadDbEnv() override {}
1558 0 : std::string Filename() override { return "faildb"; }
1559 0 : std::string Format() override { return "faildb"; }
1560 19 : std::unique_ptr<DatabaseBatch> MakeBatch(bool flush_on_close = true) override { return std::make_unique<FailBatch>(m_pass); }
1561 : };
1562 :
1563 : /**
1564 : * Checks a wallet invalid state where the inputs (prev-txs) of a new arriving transaction are not marked dirty,
1565 : * while the transaction that spends them exist inside the in-memory wallet tx map (not stored on db due a db write failure).
1566 : */
1567 149 : BOOST_FIXTURE_TEST_CASE(wallet_sync_tx_invalid_state_test, TestChain100Setup)
1568 : {
1569 1 : CWallet wallet(m_node.chain.get(), m_node.coinjoin_loader.get(), "", m_args, std::make_unique<FailDatabase>());
1570 : {
1571 1 : LOCK(wallet.cs_wallet);
1572 1 : wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
1573 1 : wallet.SetupDescriptorScriptPubKeyMans("", "");
1574 1 : }
1575 :
1576 : // Add tx to wallet
1577 1 : const auto& op_dest = wallet.GetNewDestination("");
1578 1 : BOOST_ASSERT(op_dest);
1579 :
1580 1 : CMutableTransaction mtx;
1581 1 : mtx.vout.emplace_back(COIN, GetScriptForDestination(*op_dest));
1582 1 : mtx.vin.emplace_back(g_insecure_rand_ctx.rand256(), 0);
1583 1 : const auto& tx_id_to_spend = wallet.AddToWallet(MakeTransactionRef(mtx), TxStateInMempool{})->GetHash();
1584 :
1585 : {
1586 : // Cache and verify available balance for the wtx
1587 1 : LOCK(wallet.cs_wallet);
1588 1 : const CWalletTx* wtx_to_spend = wallet.GetWalletTx(tx_id_to_spend);
1589 1 : BOOST_CHECK_EQUAL(CachedTxGetAvailableCredit(wallet, *wtx_to_spend), 1 * COIN);
1590 1 : }
1591 :
1592 : // Now the good case:
1593 : // 1) Add a transaction that spends the previously created transaction
1594 : // 2) Verify that the available balance of this new tx and the old one is updated (prev tx is marked dirty)
1595 :
1596 1 : mtx.vin.clear();
1597 1 : mtx.vin.emplace_back(tx_id_to_spend, 0);
1598 1 : wallet.transactionAddedToMempool(MakeTransactionRef(mtx), 0);
1599 1 : const uint256& good_tx_id = mtx.GetHash();
1600 :
1601 : {
1602 : // Verify balance update for the new tx and the old one
1603 1 : LOCK(wallet.cs_wallet);
1604 1 : const CWalletTx* new_wtx = wallet.GetWalletTx(good_tx_id);
1605 1 : BOOST_CHECK_EQUAL(CachedTxGetAvailableCredit(wallet, *new_wtx), 1 * COIN);
1606 :
1607 : // Now the old wtx
1608 1 : const CWalletTx* wtx_to_spend = wallet.GetWalletTx(tx_id_to_spend);
1609 1 : BOOST_CHECK_EQUAL(CachedTxGetAvailableCredit(wallet, *wtx_to_spend), 0 * COIN);
1610 1 : }
1611 :
1612 : // Now the bad case:
1613 : // 1) Make db always fail
1614 : // 2) Try to add a transaction that spends the previously created transaction and
1615 : // verify that we are not moving forward if the wallet cannot store it
1616 1 : static_cast<FailDatabase&>(wallet.GetDatabase()).m_pass = false;
1617 1 : mtx.vin.clear();
1618 1 : mtx.vin.emplace_back(good_tx_id, 0);
1619 1 : BOOST_CHECK_EXCEPTION(wallet.transactionAddedToMempool(MakeTransactionRef(mtx), 0),
1620 : std::runtime_error,
1621 : HasReason("DB error adding transaction to wallet, write failed"));
1622 2 : }
1623 :
1624 146 : BOOST_AUTO_TEST_SUITE_END()
1625 : } // namespace wallet
|