diff --git a/src/wallet/test/wallet_tests.cpp b/src/wallet/test/wallet_tests.cpp index 6a11a012f..8fcee0d0a 100644 --- a/src/wallet/test/wallet_tests.cpp +++ b/src/wallet/test/wallet_tests.cpp @@ -1,445 +1,503 @@ // Copyright (c) 2012-2016 The Bitcoin Core developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include BOOST_FIXTURE_TEST_SUITE(wallet_tests, WalletTestingSetup) static void AddKey(CWallet &wallet, const CKey &key) { LOCK(wallet.cs_wallet); wallet.AddKeyPubKey(key, key.GetPubKey()); } -// Note: when backporting PR14957, see PR15321 and ensure both LockAnnotations -// are in place. -BOOST_FIXTURE_TEST_CASE(rescan, TestChain100Setup) { +BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions, TestChain100Setup) { auto chain = interfaces::MakeChain(); // Cap last block file size, and mine new block in a new block file. const CBlockIndex *const null_block = nullptr; CBlockIndex *oldTip = ::ChainActive().Tip(); GetBlockFileInfo(oldTip->GetBlockPos().nFile)->nSize = MAX_BLOCKFILE_SIZE; CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())); CBlockIndex *newTip = ::ChainActive().Tip(); LockAnnotation lock(::cs_main); auto locked_chain = chain->lock(); + // Verify ScanForWalletTransactions accommodates a null start block. + { + CWallet wallet(Params(), *chain, WalletLocation(), + WalletDatabase::CreateDummy()); + AddKey(wallet, coinbaseKey); + WalletRescanReserver reserver(&wallet); + reserver.reserve(); + const CBlockIndex *stop_block = null_block + 1, + *failed_block = null_block + 1; + BOOST_CHECK_EQUAL( + wallet.ScanForWalletTransactions(nullptr, nullptr, reserver, + failed_block, stop_block), + CWallet::ScanResult::SUCCESS); + BOOST_CHECK_EQUAL(failed_block, null_block); + BOOST_CHECK_EQUAL(stop_block, null_block); + BOOST_CHECK_EQUAL(wallet.GetImmatureBalance(), Amount::zero()); + } + // Verify ScanForWalletTransactions picks up transactions in both the old // and new block files. { CWallet wallet(Params(), *chain, WalletLocation(), WalletDatabase::CreateDummy()); AddKey(wallet, coinbaseKey); WalletRescanReserver reserver(&wallet); reserver.reserve(); - const CBlockIndex *stop_block, *failed_block; + const CBlockIndex *stop_block = null_block + 1, + *failed_block = null_block + 1; BOOST_CHECK_EQUAL( wallet.ScanForWalletTransactions(oldTip, nullptr, reserver, failed_block, stop_block), CWallet::ScanResult::SUCCESS); BOOST_CHECK_EQUAL(failed_block, null_block); BOOST_CHECK_EQUAL(stop_block, newTip); BOOST_CHECK_EQUAL(wallet.GetImmatureBalance(), 100 * COIN); } // Prune the older block file. PruneOneBlockFile(oldTip->GetBlockPos().nFile); UnlinkPrunedFiles({oldTip->GetBlockPos().nFile}); // Verify ScanForWalletTransactions only picks transactions in the new block // file. { CWallet wallet(Params(), *chain, WalletLocation(), WalletDatabase::CreateDummy()); AddKey(wallet, coinbaseKey); WalletRescanReserver reserver(&wallet); reserver.reserve(); - const CBlockIndex *stop_block, *failed_block; + const CBlockIndex *stop_block = null_block + 1, + *failed_block = null_block + 1; BOOST_CHECK_EQUAL( wallet.ScanForWalletTransactions(oldTip, nullptr, reserver, failed_block, stop_block), CWallet::ScanResult::FAILURE); BOOST_CHECK_EQUAL(failed_block, oldTip); BOOST_CHECK_EQUAL(stop_block, newTip); BOOST_CHECK_EQUAL(wallet.GetImmatureBalance(), 50 * COIN); } + // Prune the remaining block file. + PruneOneBlockFile(newTip->GetBlockPos().nFile); + UnlinkPrunedFiles({newTip->GetBlockPos().nFile}); + + // Verify ScanForWalletTransactions scans no blocks. + { + CWallet wallet(Params(), *chain, WalletLocation(), + WalletDatabase::CreateDummy()); + AddKey(wallet, coinbaseKey); + WalletRescanReserver reserver(&wallet); + reserver.reserve(); + const CBlockIndex *stop_block = null_block + 1, + *failed_block = null_block + 1; + BOOST_CHECK_EQUAL( + wallet.ScanForWalletTransactions(oldTip, nullptr, reserver, + failed_block, stop_block), + CWallet::ScanResult::FAILURE); + BOOST_CHECK_EQUAL(failed_block, newTip); + BOOST_CHECK_EQUAL(stop_block, null_block); + BOOST_CHECK_EQUAL(wallet.GetImmatureBalance(), Amount::zero()); + } +} + +BOOST_FIXTURE_TEST_CASE(importmulti_rescan, TestChain100Setup) { + auto chain = interfaces::MakeChain(); + + // Cap last block file size, and mine new block in a new block file. + CBlockIndex *oldTip = ::ChainActive().Tip(); + GetBlockFileInfo(oldTip->GetBlockPos().nFile)->nSize = MAX_BLOCKFILE_SIZE; + CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())); + CBlockIndex *newTip = ::ChainActive().Tip(); + + LockAnnotation lock(::cs_main); + auto locked_chain = chain->lock(); + + // Prune the older block file. + PruneOneBlockFile(oldTip->GetBlockPos().nFile); + UnlinkPrunedFiles({oldTip->GetBlockPos().nFile}); + // Verify importmulti RPC returns failure for a key whose creation time is // before the missing block, and success for a key whose creation time is // after. { std::shared_ptr wallet = std::make_shared( Params(), *chain, WalletLocation(), WalletDatabase::CreateDummy()); AddWallet(wallet); UniValue keys; keys.setArray(); UniValue key; key.setObject(); key.pushKV("scriptPubKey", HexStr(GetScriptForRawPubKey(coinbaseKey.GetPubKey()))); key.pushKV("timestamp", 0); key.pushKV("internal", UniValue(true)); keys.push_back(key); key.clear(); key.setObject(); CKey futureKey; futureKey.MakeNewKey(true); key.pushKV("scriptPubKey", HexStr(GetScriptForRawPubKey(futureKey.GetPubKey()))); key.pushKV("timestamp", newTip->GetBlockTimeMax() + TIMESTAMP_WINDOW + 1); key.pushKV("internal", UniValue(true)); keys.push_back(key); JSONRPCRequest request; request.params.setArray(); request.params.push_back(keys); UniValue response = importmulti(GetConfig(), request); BOOST_CHECK_EQUAL( response.write(), strprintf("[{\"success\":false,\"error\":{\"code\":-1,\"message\":" "\"Rescan failed for key with creation timestamp %d. " "There was an error reading a block from time %d, which " "is after or within %d seconds of key creation, and " "could contain transactions pertaining to the key. As a " "result, transactions and coins using this key may not " "appear in the wallet. This error could be caused by " "pruning or data corruption (see bitcoind log for " "details) and could be dealt with by downloading and " "rescanning the relevant blocks (see -reindex and " "-rescan options).\"}},{\"success\":true}]", 0, oldTip->GetBlockTimeMax(), TIMESTAMP_WINDOW)); RemoveWallet(wallet); } } // Verify importwallet RPC starts rescan at earliest block with timestamp // greater or equal than key birthday. Previously there was a bug where // importwallet RPC would start the scan at the latest block with timestamp less // than or equal to key birthday. BOOST_FIXTURE_TEST_CASE(importwallet_rescan, TestChain100Setup) { auto chain = interfaces::MakeChain(); // Create two blocks with same timestamp to verify that importwallet rescan // will pick up both blocks, not just the first. const int64_t BLOCK_TIME = ::ChainActive().Tip()->GetBlockTimeMax() + 5; SetMockTime(BLOCK_TIME); m_coinbase_txns.emplace_back( CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())) .vtx[0]); m_coinbase_txns.emplace_back( CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())) .vtx[0]); // Set key birthday to block time increased by the timestamp window, so // rescan will start at the block time. const int64_t KEY_TIME = BLOCK_TIME + TIMESTAMP_WINDOW; SetMockTime(KEY_TIME); m_coinbase_txns.emplace_back( CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())) .vtx[0]); auto locked_chain = chain->lock(); std::string backup_file = (GetDataDir() / "wallet.backup").string(); // Import key into wallet and call dumpwallet to create backup file. { std::shared_ptr wallet = std::make_shared( Params(), *chain, WalletLocation(), WalletDatabase::CreateDummy()); LOCK(wallet->cs_wallet); wallet->mapKeyMetadata[coinbaseKey.GetPubKey().GetID()].nCreateTime = KEY_TIME; wallet->AddKeyPubKey(coinbaseKey, coinbaseKey.GetPubKey()); JSONRPCRequest request; request.params.setArray(); request.params.push_back(backup_file); AddWallet(wallet); ::dumpwallet(GetConfig(), request); RemoveWallet(wallet); } // Call importwallet RPC and verify all blocks with timestamps >= BLOCK_TIME // were scanned, and no prior blocks were scanned. { std::shared_ptr wallet = std::make_shared( Params(), *chain, WalletLocation(), WalletDatabase::CreateDummy()); JSONRPCRequest request; request.params.setArray(); request.params.push_back(backup_file); AddWallet(wallet); ::importwallet(GetConfig(), request); RemoveWallet(wallet); LOCK(wallet->cs_wallet); BOOST_CHECK_EQUAL(wallet->mapWallet.size(), 3U); BOOST_CHECK_EQUAL(m_coinbase_txns.size(), 103U); for (size_t i = 0; i < m_coinbase_txns.size(); ++i) { bool found = wallet->GetWalletTx(m_coinbase_txns[i]->GetId()); bool expected = i >= 100; BOOST_CHECK_EQUAL(found, expected); } } SetMockTime(0); } // Check that GetImmatureCredit() returns a newly calculated value instead of // the cached value after a MarkDirty() call. // // This is a regression test written to verify a bugfix for the immature credit // function. Similar tests probably should be written for the other credit and // debit functions. BOOST_FIXTURE_TEST_CASE(coin_mark_dirty_immature_credit, TestChain100Setup) { auto chain = interfaces::MakeChain(); CWallet wallet(Params(), *chain, WalletLocation(), WalletDatabase::CreateDummy()); CWalletTx wtx(&wallet, m_coinbase_txns.back()); auto locked_chain = chain->lock(); LOCK(wallet.cs_wallet); wtx.hashBlock = ::ChainActive().Tip()->GetBlockHash(); wtx.nIndex = 0; // Call GetImmatureCredit() once before adding the key to the wallet to // cache the current immature credit amount, which is 0. BOOST_CHECK_EQUAL(wtx.GetImmatureCredit(*locked_chain), Amount::zero()); // Invalidate the cached value, add the key, and make sure a new immature // credit amount is calculated. wtx.MarkDirty(); wallet.AddKeyPubKey(coinbaseKey, coinbaseKey.GetPubKey()); BOOST_CHECK_EQUAL(wtx.GetImmatureCredit(*locked_chain), 50 * COIN); } static int64_t AddTx(CWallet &wallet, uint32_t lockTime, int64_t mockTime, int64_t blockTime) { CMutableTransaction tx; tx.nLockTime = lockTime; SetMockTime(mockTime); CBlockIndex *block = nullptr; if (blockTime > 0) { LockAnnotation lock(::cs_main); auto locked_chain = wallet.chain().lock(); auto inserted = mapBlockIndex.emplace(BlockHash(GetRandHash()), new CBlockIndex); assert(inserted.second); const BlockHash &hash = inserted.first->first; block = inserted.first->second; block->nTime = blockTime; block->phashBlock = &hash; } CWalletTx wtx(&wallet, MakeTransactionRef(tx)); if (block) { wtx.SetMerkleBranch(block, 0); } { LOCK(cs_main); wallet.AddToWallet(wtx); } LOCK(wallet.cs_wallet); return wallet.mapWallet.at(wtx.GetId()).nTimeSmart; } // Simple test to verify assignment of CWalletTx::nSmartTime value. Could be // expanded to cover more corner cases of smart time logic. BOOST_AUTO_TEST_CASE(ComputeTimeSmart) { // New transaction should use clock time if lower than block time. BOOST_CHECK_EQUAL(AddTx(m_wallet, 1, 100, 120), 100); // Test that updating existing transaction does not change smart time. BOOST_CHECK_EQUAL(AddTx(m_wallet, 1, 200, 220), 100); // New transaction should use clock time if there's no block time. BOOST_CHECK_EQUAL(AddTx(m_wallet, 2, 300, 0), 300); // New transaction should use block time if lower than clock time. BOOST_CHECK_EQUAL(AddTx(m_wallet, 3, 420, 400), 400); // New transaction should use latest entry time if higher than // min(block time, clock time). BOOST_CHECK_EQUAL(AddTx(m_wallet, 4, 500, 390), 400); // If there are future entries, new transaction should use time of the // newest entry that is no more than 300 seconds ahead of the clock time. BOOST_CHECK_EQUAL(AddTx(m_wallet, 5, 50, 600), 300); // Reset mock time for other tests. SetMockTime(0); } BOOST_AUTO_TEST_CASE(LoadReceiveRequests) { CTxDestination dest = CKeyID(); LOCK(m_wallet.cs_wallet); m_wallet.AddDestData(dest, "misc", "val_misc"); m_wallet.AddDestData(dest, "rr0", "val_rr0"); m_wallet.AddDestData(dest, "rr1", "val_rr1"); auto values = m_wallet.GetDestValues("rr"); BOOST_CHECK_EQUAL(values.size(), 2U); BOOST_CHECK_EQUAL(values[0], "val_rr0"); BOOST_CHECK_EQUAL(values[1], "val_rr1"); } class ListCoinsTestingSetup : public TestChain100Setup { public: ListCoinsTestingSetup() { CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())); wallet = std::make_unique(Params(), *m_chain, WalletLocation(), WalletDatabase::CreateMock()); bool firstRun; wallet->LoadWallet(firstRun); AddKey(*wallet, coinbaseKey); WalletRescanReserver reserver(wallet.get()); reserver.reserve(); const CBlockIndex *const null_block = nullptr; - const CBlockIndex *stop_block, *failed_block; + const CBlockIndex *stop_block = null_block + 1, + *failed_block = null_block + 1; BOOST_CHECK_EQUAL(wallet->ScanForWalletTransactions( ChainActive().Genesis(), nullptr, reserver, failed_block, stop_block), CWallet::ScanResult::SUCCESS); BOOST_CHECK_EQUAL(stop_block, ChainActive().Tip()); BOOST_CHECK_EQUAL(failed_block, null_block); } ~ListCoinsTestingSetup() { wallet.reset(); } CWalletTx &AddTx(CRecipient recipient) { CTransactionRef tx; CReserveKey reservekey(wallet.get()); Amount fee; int changePos = -1; std::string error; CCoinControl dummy; BOOST_CHECK(wallet->CreateTransaction(*m_locked_chain, {recipient}, tx, reservekey, fee, changePos, error, dummy)); CValidationState state; BOOST_CHECK( wallet->CommitTransaction(tx, {}, {}, reservekey, nullptr, state)); CMutableTransaction blocktx; { LOCK(wallet->cs_wallet); blocktx = CMutableTransaction(*wallet->mapWallet.at(tx->GetId()).tx); } CreateAndProcessBlock({CMutableTransaction(blocktx)}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())); LOCK(wallet->cs_wallet); auto it = wallet->mapWallet.find(tx->GetId()); BOOST_CHECK(it != wallet->mapWallet.end()); it->second.SetMerkleBranch(::ChainActive().Tip(), 1); return it->second; } std::unique_ptr m_chain = interfaces::MakeChain(); // Temporary. Removed in upcoming lock cleanup std::unique_ptr m_locked_chain = m_chain->assumeLocked(); std::unique_ptr wallet; }; BOOST_FIXTURE_TEST_CASE(ListCoins, ListCoinsTestingSetup) { std::string coinbaseAddress = coinbaseKey.GetPubKey().GetID().ToString(); // Confirm ListCoins initially returns 1 coin grouped under coinbaseKey // address. std::map> list; { LOCK2(cs_main, wallet->cs_wallet); list = wallet->ListCoins(*m_locked_chain); } BOOST_CHECK_EQUAL(list.size(), 1U); BOOST_CHECK_EQUAL(boost::get(list.begin()->first).ToString(), coinbaseAddress); BOOST_CHECK_EQUAL(list.begin()->second.size(), 1U); // Check initial balance from one mature coinbase transaction. BOOST_CHECK_EQUAL(50 * COIN, wallet->GetAvailableBalance()); // Add a transaction creating a change address, and confirm ListCoins still // returns the coin associated with the change address underneath the // coinbaseKey pubkey, even though the change address has a different // pubkey. AddTx(CRecipient{GetScriptForRawPubKey({}), 1 * COIN, false /* subtract fee */}); { LOCK2(cs_main, wallet->cs_wallet); list = wallet->ListCoins(*m_locked_chain); } BOOST_CHECK_EQUAL(list.size(), 1U); BOOST_CHECK_EQUAL(boost::get(list.begin()->first).ToString(), coinbaseAddress); BOOST_CHECK_EQUAL(list.begin()->second.size(), 2U); // Lock both coins. Confirm number of available coins drops to 0. { LOCK2(cs_main, wallet->cs_wallet); std::vector available; wallet->AvailableCoins(*m_locked_chain, available); BOOST_CHECK_EQUAL(available.size(), 2U); } for (const auto &group : list) { for (const auto &coin : group.second) { LOCK(wallet->cs_wallet); wallet->LockCoin(COutPoint(coin.tx->GetId(), coin.i)); } } { LOCK2(cs_main, wallet->cs_wallet); std::vector available; wallet->AvailableCoins(*m_locked_chain, available); BOOST_CHECK_EQUAL(available.size(), 0U); } // Confirm ListCoins still returns same result as before, despite coins // being locked. { LOCK2(cs_main, wallet->cs_wallet); list = wallet->ListCoins(*m_locked_chain); } BOOST_CHECK_EQUAL(list.size(), 1U); BOOST_CHECK_EQUAL(boost::get(list.begin()->first).ToString(), coinbaseAddress); BOOST_CHECK_EQUAL(list.begin()->second.size(), 2U); } BOOST_FIXTURE_TEST_CASE(wallet_disableprivkeys, TestChain100Setup) { auto chain = interfaces::MakeChain(); std::shared_ptr wallet = std::make_shared( Params(), *chain, WalletLocation(), WalletDatabase::CreateDummy()); wallet->SetMinVersion(FEATURE_LATEST); wallet->SetWalletFlag(WALLET_FLAG_DISABLE_PRIVATE_KEYS); BOOST_CHECK(!wallet->TopUpKeyPool(1000)); CPubKey pubkey; BOOST_CHECK(!wallet->GetKeyFromPool(pubkey, false)); } BOOST_AUTO_TEST_SUITE_END() diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index cd62620eb..da85bb85e 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -1,4954 +1,4955 @@ // Copyright (c) 2009-2010 Satoshi Nakamoto // Copyright (c) 2009-2016 The Bitcoin Core developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include