mirror of
https://github.com/bitcoin/bitcoin.git
synced 2026-01-31 18:51:12 +00:00
30a94b1ab9ae850d55cb9eb606a06890437bc75e test, wallet: Remove concurrent writes test (Ava Chow) b44b7c03fef01e0b5db704e50762b3d16b3da69e wallet: Write best block record on unload (Ava Chow) 876a2585a8b69e12ac171a0d9ff5aab864067c42 wallet: Remove unnecessary database Close step on shutdown (Ava Chow) 98a1a5275c8c395fe47ff7f10109d75b06bc391d wallet: Remove chainStateFlushed (Ava Chow) 7fd3e1cf0c88553e0722048ce488f265883558e7 wallet, bench: Write a bestblock record in WalletMigration (Ava Chow) 6d3a8b195a826448c021dd189255ca41ba70cc5a wallet: Replace chainStateFlushed in loading with SetLastBlockProcessed (Ava Chow) 7bacabb204b6c34f9545f0b37e2c66296ad2c0de wallet: Update best block record after block dis/connect (Ava Chow) Pull request description: Implements the idea discussed in https://github.com/bitcoin/bitcoin/pull/29652#issuecomment-2010579484 Currently, `m_last_block_processed` and `m_last_block_processed_height` are not guaranteed to match the block locator stored in the wallet, nor do either of those fields actually represent the last block that the wallet is synced up to. This is confusing and unintuitive. This PR changes those last block fields to be updated whenever the wallet makes a change to the db for new transaction state found in new blocks. Whenever a block is received that contains a transaction relevant to the wallet, the last block locator will now be written to disk. Furthermore, every block disconnection will now write an updated locator. To ensure that the locator is relatively recent and loading rescans are fairly quick in the event of unplanned shutdown, it is also now written every 144 blocks (~1 day). Additionally it is now written when the wallet is unloaded so that it is accurate when the wallet is loaded again. Lastly, the `chainstateFlushed` notification in the wallet is changed to be a no-op. The best block locator record is no longer written when `chainstateFlushed` is received from the node since it should already be mostly up to date. ACKs for top commit: rkrux: ACK 30a94b1ab9ae850d55cb9eb606a06890437bc75e mzumsande: Code Review ACK 30a94b1ab9ae850d55cb9eb606a06890437bc75e ryanofsky: Code review ACK 30a94b1ab9ae850d55cb9eb606a06890437bc75e. Only changes since last review are using WriteBestBlock method more places and updating comments. Tree-SHA512: 46117541f8aaf13dde57430e813b4bbbd5e146e2632769675803c8e65a82f149a7cc6026489a127d32684b90124bd2b7c28216dbcfa6a47447300e8f3814e029
798 lines
34 KiB
C++
798 lines
34 KiB
C++
// Copyright (c) 2012-2022 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 <wallet/wallet.h>
|
|
|
|
#include <future>
|
|
#include <memory>
|
|
#include <stdint.h>
|
|
#include <vector>
|
|
|
|
#include <addresstype.h>
|
|
#include <interfaces/chain.h>
|
|
#include <key_io.h>
|
|
#include <node/blockstorage.h>
|
|
#include <policy/policy.h>
|
|
#include <rpc/server.h>
|
|
#include <script/solver.h>
|
|
#include <test/util/logging.h>
|
|
#include <test/util/random.h>
|
|
#include <test/util/setup_common.h>
|
|
#include <util/translation.h>
|
|
#include <validation.h>
|
|
#include <validationinterface.h>
|
|
#include <wallet/coincontrol.h>
|
|
#include <wallet/context.h>
|
|
#include <wallet/receive.h>
|
|
#include <wallet/spend.h>
|
|
#include <wallet/test/util.h>
|
|
#include <wallet/test/wallet_test_fixture.h>
|
|
|
|
#include <boost/test/unit_test.hpp>
|
|
#include <univalue.h>
|
|
|
|
using node::MAX_BLOCKFILE_SIZE;
|
|
|
|
namespace wallet {
|
|
|
|
// Ensure that fee levels defined in the wallet are at least as high
|
|
// as the default levels for node policy.
|
|
static_assert(DEFAULT_TRANSACTION_MINFEE >= DEFAULT_MIN_RELAY_TX_FEE, "wallet minimum fee is smaller than default relay fee");
|
|
static_assert(WALLET_INCREMENTAL_RELAY_FEE >= DEFAULT_INCREMENTAL_RELAY_FEE, "wallet incremental fee is smaller than default incremental relay fee");
|
|
|
|
BOOST_FIXTURE_TEST_SUITE(wallet_tests, WalletTestingSetup)
|
|
|
|
static CMutableTransaction TestSimpleSpend(const CTransaction& from, uint32_t index, const CKey& key, const CScript& pubkey)
|
|
{
|
|
CMutableTransaction mtx;
|
|
mtx.vout.emplace_back(from.vout[index].nValue - DEFAULT_TRANSACTION_MAXFEE, pubkey);
|
|
mtx.vin.push_back({CTxIn{from.GetHash(), index}});
|
|
FillableSigningProvider keystore;
|
|
keystore.AddKey(key);
|
|
std::map<COutPoint, Coin> coins;
|
|
coins[mtx.vin[0].prevout].out = from.vout[index];
|
|
std::map<int, bilingual_str> input_errors;
|
|
BOOST_CHECK(SignTransaction(mtx, &keystore, coins, SIGHASH_ALL, input_errors));
|
|
return mtx;
|
|
}
|
|
|
|
static void AddKey(CWallet& wallet, const CKey& key)
|
|
{
|
|
LOCK(wallet.cs_wallet);
|
|
FlatSigningProvider provider;
|
|
std::string error;
|
|
auto descs = Parse("combo(" + EncodeSecret(key) + ")", provider, error, /* require_checksum=*/ false);
|
|
assert(descs.size() == 1);
|
|
auto& desc = descs.at(0);
|
|
WalletDescriptor w_desc(std::move(desc), 0, 0, 1, 1);
|
|
auto spk_manager = *Assert(wallet.AddWalletDescriptor(w_desc, provider, "", false));
|
|
assert(spk_manager);
|
|
}
|
|
|
|
BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions, TestChain100Setup)
|
|
{
|
|
// Cap last block file size, and mine new block in a new block file.
|
|
CBlockIndex* oldTip = WITH_LOCK(Assert(m_node.chainman)->GetMutex(), return m_node.chainman->ActiveChain().Tip());
|
|
WITH_LOCK(::cs_main, m_node.chainman->m_blockman.GetBlockFileInfo(oldTip->GetBlockPos().nFile)->nSize = MAX_BLOCKFILE_SIZE);
|
|
CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey()));
|
|
CBlockIndex* newTip = WITH_LOCK(Assert(m_node.chainman)->GetMutex(), return m_node.chainman->ActiveChain().Tip());
|
|
|
|
// Verify ScanForWalletTransactions fails to read an unknown start block.
|
|
{
|
|
CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase());
|
|
{
|
|
LOCK(wallet.cs_wallet);
|
|
LOCK(Assert(m_node.chainman)->GetMutex());
|
|
wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
|
|
wallet.SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash());
|
|
}
|
|
AddKey(wallet, coinbaseKey);
|
|
WalletRescanReserver reserver(wallet);
|
|
reserver.reserve();
|
|
CWallet::ScanResult result = wallet.ScanForWalletTransactions(/*start_block=*/{}, /*start_height=*/0, /*max_height=*/{}, reserver, /*fUpdate=*/false, /*save_progress=*/false);
|
|
BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::FAILURE);
|
|
BOOST_CHECK(result.last_failed_block.IsNull());
|
|
BOOST_CHECK(result.last_scanned_block.IsNull());
|
|
BOOST_CHECK(!result.last_scanned_height);
|
|
BOOST_CHECK_EQUAL(GetBalance(wallet).m_mine_immature, 0);
|
|
}
|
|
|
|
// Verify ScanForWalletTransactions picks up transactions in both the old
|
|
// and new block files.
|
|
{
|
|
CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase());
|
|
{
|
|
LOCK(wallet.cs_wallet);
|
|
LOCK(Assert(m_node.chainman)->GetMutex());
|
|
wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
|
|
wallet.SetLastBlockProcessed(newTip->nHeight, newTip->GetBlockHash());
|
|
}
|
|
AddKey(wallet, coinbaseKey);
|
|
WalletRescanReserver reserver(wallet);
|
|
std::chrono::steady_clock::time_point fake_time;
|
|
reserver.setNow([&] { fake_time += 60s; return fake_time; });
|
|
reserver.reserve();
|
|
|
|
{
|
|
CBlockLocator locator;
|
|
BOOST_CHECK(WalletBatch{wallet.GetDatabase()}.ReadBestBlock(locator));
|
|
BOOST_CHECK(!locator.IsNull() && locator.vHave.front() == newTip->GetBlockHash());
|
|
}
|
|
|
|
CWallet::ScanResult result = wallet.ScanForWalletTransactions(/*start_block=*/oldTip->GetBlockHash(), /*start_height=*/oldTip->nHeight, /*max_height=*/{}, reserver, /*fUpdate=*/false, /*save_progress=*/true);
|
|
BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::SUCCESS);
|
|
BOOST_CHECK(result.last_failed_block.IsNull());
|
|
BOOST_CHECK_EQUAL(result.last_scanned_block, newTip->GetBlockHash());
|
|
BOOST_CHECK_EQUAL(*result.last_scanned_height, newTip->nHeight);
|
|
BOOST_CHECK_EQUAL(GetBalance(wallet).m_mine_immature, 100 * COIN);
|
|
|
|
{
|
|
CBlockLocator locator;
|
|
BOOST_CHECK(WalletBatch{wallet.GetDatabase()}.ReadBestBlock(locator));
|
|
BOOST_CHECK(!locator.IsNull() && locator.vHave.front() == newTip->GetBlockHash());
|
|
}
|
|
}
|
|
|
|
// Prune the older block file.
|
|
int file_number;
|
|
{
|
|
LOCK(cs_main);
|
|
file_number = oldTip->GetBlockPos().nFile;
|
|
Assert(m_node.chainman)->m_blockman.PruneOneBlockFile(file_number);
|
|
}
|
|
m_node.chainman->m_blockman.UnlinkPrunedFiles({file_number});
|
|
|
|
// Verify ScanForWalletTransactions only picks transactions in the new block
|
|
// file.
|
|
{
|
|
CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase());
|
|
{
|
|
LOCK(wallet.cs_wallet);
|
|
LOCK(Assert(m_node.chainman)->GetMutex());
|
|
wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
|
|
wallet.SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash());
|
|
}
|
|
AddKey(wallet, coinbaseKey);
|
|
WalletRescanReserver reserver(wallet);
|
|
reserver.reserve();
|
|
CWallet::ScanResult result = wallet.ScanForWalletTransactions(/*start_block=*/oldTip->GetBlockHash(), /*start_height=*/oldTip->nHeight, /*max_height=*/{}, reserver, /*fUpdate=*/false, /*save_progress=*/false);
|
|
BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::FAILURE);
|
|
BOOST_CHECK_EQUAL(result.last_failed_block, oldTip->GetBlockHash());
|
|
BOOST_CHECK_EQUAL(result.last_scanned_block, newTip->GetBlockHash());
|
|
BOOST_CHECK_EQUAL(*result.last_scanned_height, newTip->nHeight);
|
|
BOOST_CHECK_EQUAL(GetBalance(wallet).m_mine_immature, 50 * COIN);
|
|
}
|
|
|
|
// Prune the remaining block file.
|
|
{
|
|
LOCK(cs_main);
|
|
file_number = newTip->GetBlockPos().nFile;
|
|
Assert(m_node.chainman)->m_blockman.PruneOneBlockFile(file_number);
|
|
}
|
|
m_node.chainman->m_blockman.UnlinkPrunedFiles({file_number});
|
|
|
|
// Verify ScanForWalletTransactions scans no blocks.
|
|
{
|
|
CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase());
|
|
{
|
|
LOCK(wallet.cs_wallet);
|
|
LOCK(Assert(m_node.chainman)->GetMutex());
|
|
wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
|
|
wallet.SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash());
|
|
}
|
|
AddKey(wallet, coinbaseKey);
|
|
WalletRescanReserver reserver(wallet);
|
|
reserver.reserve();
|
|
CWallet::ScanResult result = wallet.ScanForWalletTransactions(/*start_block=*/oldTip->GetBlockHash(), /*start_height=*/oldTip->nHeight, /*max_height=*/{}, reserver, /*fUpdate=*/false, /*save_progress=*/false);
|
|
BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::FAILURE);
|
|
BOOST_CHECK_EQUAL(result.last_failed_block, newTip->GetBlockHash());
|
|
BOOST_CHECK(result.last_scanned_block.IsNull());
|
|
BOOST_CHECK(!result.last_scanned_height);
|
|
BOOST_CHECK_EQUAL(GetBalance(wallet).m_mine_immature, 0);
|
|
}
|
|
}
|
|
|
|
// This test verifies that wallet settings can be added and removed
|
|
// concurrently, ensuring no race conditions occur during either process.
|
|
BOOST_FIXTURE_TEST_CASE(write_wallet_settings_concurrently, TestingSetup)
|
|
{
|
|
auto chain = m_node.chain.get();
|
|
const auto NUM_WALLETS{5};
|
|
|
|
// Since we're counting the number of wallets, ensure we start without any.
|
|
BOOST_REQUIRE(chain->getRwSetting("wallet").isNull());
|
|
|
|
const auto& check_concurrent_wallet = [&](const auto& settings_function, int num_expected_wallets) {
|
|
std::vector<std::thread> threads;
|
|
threads.reserve(NUM_WALLETS);
|
|
for (auto i{0}; i < NUM_WALLETS; ++i) threads.emplace_back(settings_function, i);
|
|
for (auto& t : threads) t.join();
|
|
|
|
auto wallets = chain->getRwSetting("wallet");
|
|
BOOST_CHECK_EQUAL(wallets.getValues().size(), num_expected_wallets);
|
|
};
|
|
|
|
// Add NUM_WALLETS wallets concurrently, ensure we end up with NUM_WALLETS stored.
|
|
check_concurrent_wallet([&chain](int i) {
|
|
Assert(AddWalletSetting(*chain, strprintf("wallet_%d", i)));
|
|
},
|
|
/*num_expected_wallets=*/NUM_WALLETS);
|
|
|
|
// Remove NUM_WALLETS wallets concurrently, ensure we end up with 0 wallets.
|
|
check_concurrent_wallet([&chain](int i) {
|
|
Assert(RemoveWalletSetting(*chain, strprintf("wallet_%d", i)));
|
|
},
|
|
/*num_expected_wallets=*/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)
|
|
{
|
|
CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase());
|
|
|
|
LOCK(wallet.cs_wallet);
|
|
LOCK(Assert(m_node.chainman)->GetMutex());
|
|
CWalletTx wtx{m_coinbase_txns.back(), TxStateConfirmed{m_node.chainman->ActiveChain().Tip()->GetBlockHash(), m_node.chainman->ActiveChain().Height(), /*index=*/0}};
|
|
wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
|
|
wallet.SetupDescriptorScriptPubKeyMans();
|
|
|
|
wallet.SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash());
|
|
|
|
// Call GetImmatureCredit() once before adding the key to the wallet to
|
|
// cache the current immature credit amount, which is 0.
|
|
BOOST_CHECK_EQUAL(CachedTxGetImmatureCredit(wallet, wtx, ISMINE_SPENDABLE), 0);
|
|
|
|
// Invalidate the cached value, add the key, and make sure a new immature
|
|
// credit amount is calculated.
|
|
wtx.MarkDirty();
|
|
AddKey(wallet, coinbaseKey);
|
|
BOOST_CHECK_EQUAL(CachedTxGetImmatureCredit(wallet, wtx, ISMINE_SPENDABLE), 50*COIN);
|
|
}
|
|
|
|
static int64_t AddTx(ChainstateManager& chainman, CWallet& wallet, uint32_t lockTime, int64_t mockTime, int64_t blockTime)
|
|
{
|
|
CMutableTransaction tx;
|
|
TxState state = TxStateInactive{};
|
|
tx.nLockTime = lockTime;
|
|
SetMockTime(mockTime);
|
|
CBlockIndex* block = nullptr;
|
|
if (blockTime > 0) {
|
|
LOCK(cs_main);
|
|
auto inserted = chainman.BlockIndex().emplace(std::piecewise_construct, std::make_tuple(GetRandHash()), std::make_tuple());
|
|
assert(inserted.second);
|
|
const uint256& hash = inserted.first->first;
|
|
block = &inserted.first->second;
|
|
block->nTime = blockTime;
|
|
block->phashBlock = &hash;
|
|
state = TxStateConfirmed{hash, block->nHeight, /*index=*/0};
|
|
}
|
|
return wallet.AddToWallet(MakeTransactionRef(tx), state, [&](CWalletTx& wtx, bool /* new_tx */) {
|
|
// Assign wtx.m_state to simplify test and avoid the need to simulate
|
|
// reorg events. Without this, AddToWallet asserts false when the same
|
|
// transaction is confirmed in different blocks.
|
|
wtx.m_state = state;
|
|
return true;
|
|
})->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_node.chainman, m_wallet, 1, 100, 120), 100);
|
|
|
|
// Test that updating existing transaction does not change smart time.
|
|
BOOST_CHECK_EQUAL(AddTx(*m_node.chainman, m_wallet, 1, 200, 220), 100);
|
|
|
|
// New transaction should use clock time if there's no block time.
|
|
BOOST_CHECK_EQUAL(AddTx(*m_node.chainman, m_wallet, 2, 300, 0), 300);
|
|
|
|
// New transaction should use block time if lower than clock time.
|
|
BOOST_CHECK_EQUAL(AddTx(*m_node.chainman, 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_node.chainman, 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_node.chainman, m_wallet, 5, 50, 600), 300);
|
|
}
|
|
|
|
void TestLoadWallet(const std::string& name, DatabaseFormat format, std::function<void(std::shared_ptr<CWallet>)> f)
|
|
{
|
|
node::NodeContext node;
|
|
auto chain{interfaces::MakeChain(node)};
|
|
DatabaseOptions options;
|
|
options.require_format = format;
|
|
DatabaseStatus status;
|
|
bilingual_str error;
|
|
std::vector<bilingual_str> warnings;
|
|
auto database{MakeWalletDatabase(name, options, status, error)};
|
|
auto wallet{std::make_shared<CWallet>(chain.get(), "", std::move(database))};
|
|
BOOST_CHECK_EQUAL(wallet->LoadWallet(), DBErrors::LOAD_OK);
|
|
WITH_LOCK(wallet->cs_wallet, f(wallet));
|
|
}
|
|
|
|
BOOST_FIXTURE_TEST_CASE(LoadReceiveRequests, TestingSetup)
|
|
{
|
|
for (DatabaseFormat format : DATABASE_FORMATS) {
|
|
const std::string name{strprintf("receive-requests-%i", format)};
|
|
TestLoadWallet(name, format, [](std::shared_ptr<CWallet> wallet) EXCLUSIVE_LOCKS_REQUIRED(wallet->cs_wallet) {
|
|
BOOST_CHECK(!wallet->IsAddressPreviouslySpent(PKHash()));
|
|
WalletBatch batch{wallet->GetDatabase()};
|
|
BOOST_CHECK(batch.WriteAddressPreviouslySpent(PKHash(), true));
|
|
BOOST_CHECK(batch.WriteAddressPreviouslySpent(ScriptHash(), true));
|
|
BOOST_CHECK(wallet->SetAddressReceiveRequest(batch, PKHash(), "0", "val_rr00"));
|
|
BOOST_CHECK(wallet->EraseAddressReceiveRequest(batch, PKHash(), "0"));
|
|
BOOST_CHECK(wallet->SetAddressReceiveRequest(batch, PKHash(), "1", "val_rr10"));
|
|
BOOST_CHECK(wallet->SetAddressReceiveRequest(batch, PKHash(), "1", "val_rr11"));
|
|
BOOST_CHECK(wallet->SetAddressReceiveRequest(batch, ScriptHash(), "2", "val_rr20"));
|
|
});
|
|
TestLoadWallet(name, format, [](std::shared_ptr<CWallet> wallet) EXCLUSIVE_LOCKS_REQUIRED(wallet->cs_wallet) {
|
|
BOOST_CHECK(wallet->IsAddressPreviouslySpent(PKHash()));
|
|
BOOST_CHECK(wallet->IsAddressPreviouslySpent(ScriptHash()));
|
|
auto requests = wallet->GetAddressReceiveRequests();
|
|
auto erequests = {"val_rr11", "val_rr20"};
|
|
BOOST_CHECK_EQUAL_COLLECTIONS(requests.begin(), requests.end(), std::begin(erequests), std::end(erequests));
|
|
RunWithinTxn(wallet->GetDatabase(), /*process_desc*/"test", [](WalletBatch& batch){
|
|
BOOST_CHECK(batch.WriteAddressPreviouslySpent(PKHash(), false));
|
|
BOOST_CHECK(batch.EraseAddressData(ScriptHash()));
|
|
return true;
|
|
});
|
|
});
|
|
TestLoadWallet(name, format, [](std::shared_ptr<CWallet> wallet) EXCLUSIVE_LOCKS_REQUIRED(wallet->cs_wallet) {
|
|
BOOST_CHECK(!wallet->IsAddressPreviouslySpent(PKHash()));
|
|
BOOST_CHECK(!wallet->IsAddressPreviouslySpent(ScriptHash()));
|
|
auto requests = wallet->GetAddressReceiveRequests();
|
|
auto erequests = {"val_rr11"};
|
|
BOOST_CHECK_EQUAL_COLLECTIONS(requests.begin(), requests.end(), std::begin(erequests), std::end(erequests));
|
|
});
|
|
}
|
|
}
|
|
|
|
class ListCoinsTestingSetup : public TestChain100Setup
|
|
{
|
|
public:
|
|
ListCoinsTestingSetup()
|
|
{
|
|
CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey()));
|
|
wallet = CreateSyncedWallet(*m_node.chain, WITH_LOCK(Assert(m_node.chainman)->GetMutex(), return m_node.chainman->ActiveChain()), coinbaseKey);
|
|
}
|
|
|
|
~ListCoinsTestingSetup()
|
|
{
|
|
wallet.reset();
|
|
}
|
|
|
|
CWalletTx& AddTx(CRecipient recipient)
|
|
{
|
|
CTransactionRef tx;
|
|
CCoinControl dummy;
|
|
{
|
|
auto res = CreateTransaction(*wallet, {recipient}, /*change_pos=*/std::nullopt, dummy);
|
|
BOOST_CHECK(res);
|
|
tx = res->tx;
|
|
}
|
|
wallet->CommitTransaction(tx, {}, {});
|
|
CMutableTransaction blocktx;
|
|
{
|
|
LOCK(wallet->cs_wallet);
|
|
blocktx = CMutableTransaction(*wallet->mapWallet.at(tx->GetHash()).tx);
|
|
}
|
|
CreateAndProcessBlock({CMutableTransaction(blocktx)}, GetScriptForRawPubKey(coinbaseKey.GetPubKey()));
|
|
|
|
LOCK(wallet->cs_wallet);
|
|
LOCK(Assert(m_node.chainman)->GetMutex());
|
|
wallet->SetLastBlockProcessed(wallet->GetLastBlockHeight() + 1, m_node.chainman->ActiveChain().Tip()->GetBlockHash());
|
|
auto it = wallet->mapWallet.find(tx->GetHash());
|
|
BOOST_CHECK(it != wallet->mapWallet.end());
|
|
it->second.m_state = TxStateConfirmed{m_node.chainman->ActiveChain().Tip()->GetBlockHash(), m_node.chainman->ActiveChain().Height(), /*index=*/1};
|
|
return it->second;
|
|
}
|
|
|
|
std::unique_ptr<CWallet> wallet;
|
|
};
|
|
|
|
BOOST_FIXTURE_TEST_CASE(ListCoinsTest, ListCoinsTestingSetup)
|
|
{
|
|
std::string coinbaseAddress = coinbaseKey.GetPubKey().GetID().ToString();
|
|
|
|
// Confirm ListCoins initially returns 1 coin grouped under coinbaseKey
|
|
// address.
|
|
std::map<CTxDestination, std::vector<COutput>> list;
|
|
{
|
|
LOCK(wallet->cs_wallet);
|
|
list = ListCoins(*wallet);
|
|
}
|
|
BOOST_CHECK_EQUAL(list.size(), 1U);
|
|
BOOST_CHECK_EQUAL(std::get<PKHash>(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, WITH_LOCK(wallet->cs_wallet, return AvailableCoins(*wallet).GetTotalAmount()));
|
|
|
|
// 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{PubKeyDestination{{}}, 1 * COIN, /*subtract_fee=*/false});
|
|
{
|
|
LOCK(wallet->cs_wallet);
|
|
list = ListCoins(*wallet);
|
|
}
|
|
BOOST_CHECK_EQUAL(list.size(), 1U);
|
|
BOOST_CHECK_EQUAL(std::get<PKHash>(list.begin()->first).ToString(), coinbaseAddress);
|
|
BOOST_CHECK_EQUAL(list.begin()->second.size(), 2U);
|
|
|
|
// Lock both coins. Confirm number of available coins drops to 0.
|
|
{
|
|
LOCK(wallet->cs_wallet);
|
|
BOOST_CHECK_EQUAL(AvailableCoinsListUnspent(*wallet).Size(), 2U);
|
|
}
|
|
for (const auto& group : list) {
|
|
for (const auto& coin : group.second) {
|
|
LOCK(wallet->cs_wallet);
|
|
wallet->LockCoin(coin.outpoint);
|
|
}
|
|
}
|
|
{
|
|
LOCK(wallet->cs_wallet);
|
|
BOOST_CHECK_EQUAL(AvailableCoinsListUnspent(*wallet).Size(), 0U);
|
|
}
|
|
// Confirm ListCoins still returns same result as before, despite coins
|
|
// being locked.
|
|
{
|
|
LOCK(wallet->cs_wallet);
|
|
list = ListCoins(*wallet);
|
|
}
|
|
BOOST_CHECK_EQUAL(list.size(), 1U);
|
|
BOOST_CHECK_EQUAL(std::get<PKHash>(list.begin()->first).ToString(), coinbaseAddress);
|
|
BOOST_CHECK_EQUAL(list.begin()->second.size(), 2U);
|
|
}
|
|
|
|
void TestCoinsResult(ListCoinsTest& context, OutputType out_type, CAmount amount,
|
|
std::map<OutputType, size_t>& expected_coins_sizes)
|
|
{
|
|
LOCK(context.wallet->cs_wallet);
|
|
util::Result<CTxDestination> dest = Assert(context.wallet->GetNewDestination(out_type, ""));
|
|
CWalletTx& wtx = context.AddTx(CRecipient{*dest, amount, /*fSubtractFeeFromAmount=*/true});
|
|
CoinFilterParams filter;
|
|
filter.skip_locked = false;
|
|
CoinsResult available_coins = AvailableCoins(*context.wallet, nullptr, std::nullopt, filter);
|
|
// Lock outputs so they are not spent in follow-up transactions
|
|
for (uint32_t i = 0; i < wtx.tx->vout.size(); i++) context.wallet->LockCoin({wtx.GetHash(), i});
|
|
for (const auto& [type, size] : expected_coins_sizes) BOOST_CHECK_EQUAL(size, available_coins.coins[type].size());
|
|
}
|
|
|
|
BOOST_FIXTURE_TEST_CASE(BasicOutputTypesTest, ListCoinsTest)
|
|
{
|
|
std::map<OutputType, size_t> expected_coins_sizes;
|
|
for (const auto& out_type : OUTPUT_TYPES) { expected_coins_sizes[out_type] = 0U; }
|
|
|
|
// Verify our wallet has one usable coinbase UTXO before starting
|
|
// This UTXO is a P2PK, so it should show up in the Other bucket
|
|
expected_coins_sizes[OutputType::UNKNOWN] = 1U;
|
|
CoinsResult available_coins = WITH_LOCK(wallet->cs_wallet, return AvailableCoins(*wallet));
|
|
BOOST_CHECK_EQUAL(available_coins.Size(), expected_coins_sizes[OutputType::UNKNOWN]);
|
|
BOOST_CHECK_EQUAL(available_coins.coins[OutputType::UNKNOWN].size(), expected_coins_sizes[OutputType::UNKNOWN]);
|
|
|
|
// We will create a self transfer for each of the OutputTypes and
|
|
// verify it is put in the correct bucket after running GetAvailablecoins
|
|
//
|
|
// For each OutputType, We expect 2 UTXOs in our wallet following the self transfer:
|
|
// 1. One UTXO as the recipient
|
|
// 2. One UTXO from the change, due to payment address matching logic
|
|
|
|
for (const auto& out_type : OUTPUT_TYPES) {
|
|
if (out_type == OutputType::UNKNOWN) continue;
|
|
expected_coins_sizes[out_type] = 2U;
|
|
TestCoinsResult(*this, out_type, 1 * COIN, expected_coins_sizes);
|
|
}
|
|
}
|
|
|
|
BOOST_FIXTURE_TEST_CASE(wallet_disableprivkeys, TestChain100Setup)
|
|
{
|
|
const std::shared_ptr<CWallet> wallet = std::make_shared<CWallet>(m_node.chain.get(), "", CreateMockableWalletDatabase());
|
|
LOCK(wallet->cs_wallet);
|
|
wallet->SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
|
|
wallet->SetMinVersion(FEATURE_LATEST);
|
|
wallet->SetWalletFlag(WALLET_FLAG_DISABLE_PRIVATE_KEYS);
|
|
BOOST_CHECK(!wallet->GetNewDestination(OutputType::BECH32, ""));
|
|
}
|
|
|
|
// Explicit calculation which is used to test the wallet constant
|
|
// We get the same virtual size due to rounding(weight/4) for both use_max_sig values
|
|
static size_t CalculateNestedKeyhashInputSize(bool use_max_sig)
|
|
{
|
|
// Generate ephemeral valid pubkey
|
|
CKey key = GenerateRandomKey();
|
|
CPubKey pubkey = key.GetPubKey();
|
|
|
|
// Generate pubkey hash
|
|
uint160 key_hash(Hash160(pubkey));
|
|
|
|
// Create inner-script to enter into keystore. Key hash can't be 0...
|
|
CScript inner_script = CScript() << OP_0 << std::vector<unsigned char>(key_hash.begin(), key_hash.end());
|
|
|
|
// Create outer P2SH script for the output
|
|
uint160 script_id(Hash160(inner_script));
|
|
CScript script_pubkey = CScript() << OP_HASH160 << std::vector<unsigned char>(script_id.begin(), script_id.end()) << OP_EQUAL;
|
|
|
|
// Add inner-script to key store and key to watchonly
|
|
FillableSigningProvider keystore;
|
|
keystore.AddCScript(inner_script);
|
|
keystore.AddKeyPubKey(key, pubkey);
|
|
|
|
// Fill in dummy signatures for fee calculation.
|
|
SignatureData sig_data;
|
|
|
|
if (!ProduceSignature(keystore, use_max_sig ? DUMMY_MAXIMUM_SIGNATURE_CREATOR : DUMMY_SIGNATURE_CREATOR, script_pubkey, sig_data)) {
|
|
// We're hand-feeding it correct arguments; shouldn't happen
|
|
assert(false);
|
|
}
|
|
|
|
CTxIn tx_in;
|
|
UpdateInput(tx_in, sig_data);
|
|
return (size_t)GetVirtualTransactionInputSize(tx_in);
|
|
}
|
|
|
|
BOOST_FIXTURE_TEST_CASE(dummy_input_size_test, TestChain100Setup)
|
|
{
|
|
BOOST_CHECK_EQUAL(CalculateNestedKeyhashInputSize(false), DUMMY_NESTED_P2WPKH_INPUT_SIZE);
|
|
BOOST_CHECK_EQUAL(CalculateNestedKeyhashInputSize(true), DUMMY_NESTED_P2WPKH_INPUT_SIZE);
|
|
}
|
|
|
|
bool malformed_descriptor(std::ios_base::failure e)
|
|
{
|
|
std::string s(e.what());
|
|
return s.find("Missing checksum") != std::string::npos;
|
|
}
|
|
|
|
BOOST_FIXTURE_TEST_CASE(wallet_descriptor_test, BasicTestingSetup)
|
|
{
|
|
std::vector<unsigned char> malformed_record;
|
|
VectorWriter vw{malformed_record, 0};
|
|
vw << std::string("notadescriptor");
|
|
vw << uint64_t{0};
|
|
vw << int32_t{0};
|
|
vw << int32_t{0};
|
|
vw << int32_t{1};
|
|
|
|
SpanReader vr{malformed_record};
|
|
WalletDescriptor w_desc;
|
|
BOOST_CHECK_EXCEPTION(vr >> w_desc, std::ios_base::failure, malformed_descriptor);
|
|
}
|
|
|
|
//! Test CWallet::Create() and its behavior handling potential race
|
|
//! conditions if it's called the same time an incoming transaction shows up in
|
|
//! the mempool or a new block.
|
|
//!
|
|
//! It isn't possible to verify there aren't race condition in every case, so
|
|
//! this test just checks two specific cases and ensures that timing of
|
|
//! notifications in these cases doesn't prevent the wallet from detecting
|
|
//! transactions.
|
|
//!
|
|
//! In the first case, block and mempool transactions are created before the
|
|
//! wallet is loaded, but notifications about these transactions are delayed
|
|
//! until after it is loaded. The notifications are superfluous in this case, so
|
|
//! the test verifies the transactions are detected before they arrive.
|
|
//!
|
|
//! In the second case, block and mempool transactions are created after the
|
|
//! wallet rescan and notifications are immediately synced, to verify the wallet
|
|
//! must already have a handler in place for them, and there's no gap after
|
|
//! rescanning where new transactions in new blocks could be lost.
|
|
BOOST_FIXTURE_TEST_CASE(CreateWallet, TestChain100Setup)
|
|
{
|
|
m_args.ForceSetArg("-unsafesqlitesync", "1");
|
|
// Create new wallet with known key and unload it.
|
|
WalletContext context;
|
|
context.args = &m_args;
|
|
context.chain = m_node.chain.get();
|
|
auto wallet = TestLoadWallet(context);
|
|
CKey key = GenerateRandomKey();
|
|
AddKey(*wallet, key);
|
|
TestUnloadWallet(std::move(wallet));
|
|
|
|
|
|
// Add log hook to detect AddToWallet events from rescans, blockConnected,
|
|
// and transactionAddedToMempool notifications
|
|
int addtx_count = 0;
|
|
DebugLogHelper addtx_counter("[default wallet] AddToWallet", [&](const std::string* s) {
|
|
if (s) ++addtx_count;
|
|
return false;
|
|
});
|
|
|
|
|
|
bool rescan_completed = false;
|
|
DebugLogHelper rescan_check("[default wallet] Rescan completed", [&](const std::string* s) {
|
|
if (s) rescan_completed = true;
|
|
return false;
|
|
});
|
|
|
|
|
|
// Block the queue to prevent the wallet receiving blockConnected and
|
|
// transactionAddedToMempool notifications, and create block and mempool
|
|
// transactions paying to the wallet
|
|
std::promise<void> promise;
|
|
m_node.validation_signals->CallFunctionInValidationInterfaceQueue([&promise] {
|
|
promise.get_future().wait();
|
|
});
|
|
std::string error;
|
|
m_coinbase_txns.push_back(CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())).vtx[0]);
|
|
auto block_tx = TestSimpleSpend(*m_coinbase_txns[0], 0, coinbaseKey, GetScriptForRawPubKey(key.GetPubKey()));
|
|
m_coinbase_txns.push_back(CreateAndProcessBlock({block_tx}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())).vtx[0]);
|
|
auto mempool_tx = TestSimpleSpend(*m_coinbase_txns[1], 0, coinbaseKey, GetScriptForRawPubKey(key.GetPubKey()));
|
|
BOOST_CHECK(m_node.chain->broadcastTransaction(MakeTransactionRef(mempool_tx), DEFAULT_TRANSACTION_MAXFEE, false, error));
|
|
|
|
|
|
// Reload wallet and make sure new transactions are detected despite events
|
|
// being blocked
|
|
// Loading will also ask for current mempool transactions
|
|
wallet = TestLoadWallet(context);
|
|
BOOST_CHECK(rescan_completed);
|
|
// AddToWallet events for block_tx and mempool_tx (x2)
|
|
BOOST_CHECK_EQUAL(addtx_count, 3);
|
|
{
|
|
LOCK(wallet->cs_wallet);
|
|
BOOST_CHECK_EQUAL(wallet->mapWallet.count(block_tx.GetHash()), 1U);
|
|
BOOST_CHECK_EQUAL(wallet->mapWallet.count(mempool_tx.GetHash()), 1U);
|
|
}
|
|
|
|
|
|
// Unblock notification queue and make sure stale blockConnected and
|
|
// transactionAddedToMempool events are processed
|
|
promise.set_value();
|
|
m_node.validation_signals->SyncWithValidationInterfaceQueue();
|
|
// AddToWallet events for block_tx and mempool_tx events are counted a
|
|
// second time as the notification queue is processed
|
|
BOOST_CHECK_EQUAL(addtx_count, 5);
|
|
|
|
|
|
TestUnloadWallet(std::move(wallet));
|
|
|
|
|
|
// Load wallet again, this time creating new block and mempool transactions
|
|
// paying to the wallet as the wallet finishes loading and syncing the
|
|
// queue so the events have to be handled immediately. Releasing the wallet
|
|
// lock during the sync is a little artificial but is needed to avoid a
|
|
// deadlock during the sync and simulates a new block notification happening
|
|
// as soon as possible.
|
|
addtx_count = 0;
|
|
auto handler = HandleLoadWallet(context, [&](std::unique_ptr<interfaces::Wallet> wallet) {
|
|
BOOST_CHECK(rescan_completed);
|
|
m_coinbase_txns.push_back(CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())).vtx[0]);
|
|
block_tx = TestSimpleSpend(*m_coinbase_txns[2], 0, coinbaseKey, GetScriptForRawPubKey(key.GetPubKey()));
|
|
m_coinbase_txns.push_back(CreateAndProcessBlock({block_tx}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())).vtx[0]);
|
|
mempool_tx = TestSimpleSpend(*m_coinbase_txns[3], 0, coinbaseKey, GetScriptForRawPubKey(key.GetPubKey()));
|
|
BOOST_CHECK(m_node.chain->broadcastTransaction(MakeTransactionRef(mempool_tx), DEFAULT_TRANSACTION_MAXFEE, false, error));
|
|
m_node.validation_signals->SyncWithValidationInterfaceQueue();
|
|
});
|
|
wallet = TestLoadWallet(context);
|
|
// Since mempool transactions are requested at the end of loading, there will
|
|
// be 2 additional AddToWallet calls, one from the previous test, and a duplicate for mempool_tx
|
|
BOOST_CHECK_EQUAL(addtx_count, 2 + 2);
|
|
{
|
|
LOCK(wallet->cs_wallet);
|
|
BOOST_CHECK_EQUAL(wallet->mapWallet.count(block_tx.GetHash()), 1U);
|
|
BOOST_CHECK_EQUAL(wallet->mapWallet.count(mempool_tx.GetHash()), 1U);
|
|
}
|
|
|
|
|
|
TestUnloadWallet(std::move(wallet));
|
|
}
|
|
|
|
BOOST_FIXTURE_TEST_CASE(CreateWalletWithoutChain, BasicTestingSetup)
|
|
{
|
|
WalletContext context;
|
|
context.args = &m_args;
|
|
auto wallet = TestLoadWallet(context);
|
|
BOOST_CHECK(wallet);
|
|
WaitForDeleteWallet(std::move(wallet));
|
|
}
|
|
|
|
BOOST_FIXTURE_TEST_CASE(RemoveTxs, TestChain100Setup)
|
|
{
|
|
m_args.ForceSetArg("-unsafesqlitesync", "1");
|
|
WalletContext context;
|
|
context.args = &m_args;
|
|
context.chain = m_node.chain.get();
|
|
auto wallet = TestLoadWallet(context);
|
|
CKey key = GenerateRandomKey();
|
|
AddKey(*wallet, key);
|
|
|
|
std::string error;
|
|
m_coinbase_txns.push_back(CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey())).vtx[0]);
|
|
auto block_tx = TestSimpleSpend(*m_coinbase_txns[0], 0, coinbaseKey, GetScriptForRawPubKey(key.GetPubKey()));
|
|
CreateAndProcessBlock({block_tx}, GetScriptForRawPubKey(coinbaseKey.GetPubKey()));
|
|
|
|
m_node.validation_signals->SyncWithValidationInterfaceQueue();
|
|
|
|
{
|
|
auto block_hash = block_tx.GetHash();
|
|
auto prev_tx = m_coinbase_txns[0];
|
|
|
|
LOCK(wallet->cs_wallet);
|
|
BOOST_CHECK(wallet->HasWalletSpend(prev_tx));
|
|
BOOST_CHECK_EQUAL(wallet->mapWallet.count(block_hash), 1u);
|
|
|
|
std::vector<Txid> vHashIn{ block_hash };
|
|
BOOST_CHECK(wallet->RemoveTxs(vHashIn));
|
|
|
|
BOOST_CHECK(!wallet->HasWalletSpend(prev_tx));
|
|
BOOST_CHECK_EQUAL(wallet->mapWallet.count(block_hash), 0u);
|
|
}
|
|
|
|
TestUnloadWallet(std::move(wallet));
|
|
}
|
|
|
|
/**
|
|
* Checks a wallet invalid state where the inputs (prev-txs) of a new arriving transaction are not marked dirty,
|
|
* while the transaction that spends them exist inside the in-memory wallet tx map (not stored on db due a db write failure).
|
|
*/
|
|
BOOST_FIXTURE_TEST_CASE(wallet_sync_tx_invalid_state_test, TestingSetup)
|
|
{
|
|
CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase());
|
|
{
|
|
LOCK(wallet.cs_wallet);
|
|
wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
|
|
wallet.SetupDescriptorScriptPubKeyMans();
|
|
}
|
|
|
|
// Add tx to wallet
|
|
const auto op_dest{*Assert(wallet.GetNewDestination(OutputType::BECH32M, ""))};
|
|
|
|
CMutableTransaction mtx;
|
|
mtx.vout.emplace_back(COIN, GetScriptForDestination(op_dest));
|
|
mtx.vin.emplace_back(Txid::FromUint256(m_rng.rand256()), 0);
|
|
const auto& tx_id_to_spend = wallet.AddToWallet(MakeTransactionRef(mtx), TxStateInMempool{})->GetHash();
|
|
|
|
{
|
|
// Cache and verify available balance for the wtx
|
|
LOCK(wallet.cs_wallet);
|
|
const CWalletTx* wtx_to_spend = wallet.GetWalletTx(tx_id_to_spend);
|
|
BOOST_CHECK_EQUAL(CachedTxGetAvailableCredit(wallet, *wtx_to_spend), 1 * COIN);
|
|
}
|
|
|
|
// Now the good case:
|
|
// 1) Add a transaction that spends the previously created transaction
|
|
// 2) Verify that the available balance of this new tx and the old one is updated (prev tx is marked dirty)
|
|
|
|
mtx.vin.clear();
|
|
mtx.vin.emplace_back(tx_id_to_spend, 0);
|
|
wallet.transactionAddedToMempool(MakeTransactionRef(mtx));
|
|
const auto good_tx_id{mtx.GetHash()};
|
|
|
|
{
|
|
// Verify balance update for the new tx and the old one
|
|
LOCK(wallet.cs_wallet);
|
|
const CWalletTx* new_wtx = wallet.GetWalletTx(good_tx_id);
|
|
BOOST_CHECK_EQUAL(CachedTxGetAvailableCredit(wallet, *new_wtx), 1 * COIN);
|
|
|
|
// Now the old wtx
|
|
const CWalletTx* wtx_to_spend = wallet.GetWalletTx(tx_id_to_spend);
|
|
BOOST_CHECK_EQUAL(CachedTxGetAvailableCredit(wallet, *wtx_to_spend), 0 * COIN);
|
|
}
|
|
|
|
// Now the bad case:
|
|
// 1) Make db always fail
|
|
// 2) Try to add a transaction that spends the previously created transaction and
|
|
// verify that we are not moving forward if the wallet cannot store it
|
|
GetMockableDatabase(wallet).m_pass = false;
|
|
mtx.vin.clear();
|
|
mtx.vin.emplace_back(good_tx_id, 0);
|
|
BOOST_CHECK_EXCEPTION(wallet.transactionAddedToMempool(MakeTransactionRef(mtx)),
|
|
std::runtime_error,
|
|
HasReason("DB error adding transaction to wallet, write failed"));
|
|
}
|
|
|
|
BOOST_AUTO_TEST_SUITE_END()
|
|
} // namespace wallet
|