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