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