diff --git a/src/Makefile.bench.include b/src/Makefile.bench.include index be7bfe2d2dcc..2cd403b48297 100644 --- a/src/Makefile.bench.include +++ b/src/Makefile.bench.include @@ -91,6 +91,7 @@ if ENABLE_WALLET bench_bench_dash_SOURCES += bench/coin_selection.cpp bench_bench_dash_SOURCES += bench/wallet_balance.cpp bench_bench_dash_SOURCES += bench/wallet_create.cpp +bench_bench_dash_SOURCES += bench/wallet_create_tx.cpp bench_bench_dash_SOURCES += bench/wallet_loading.cpp bench_bench_dash_LDADD += $(BDB_LIBS) $(SQLITE_LIBS) endif diff --git a/src/bench/wallet_create_tx.cpp b/src/bench/wallet_create_tx.cpp new file mode 100644 index 000000000000..59e6d99bfa34 --- /dev/null +++ b/src/bench/wallet_create_tx.cpp @@ -0,0 +1,142 @@ +// Copyright (c) 2022 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or https://www.opensource.org/licenses/mit-license.php. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using wallet::CWallet; +using wallet::CreateMockWalletDatabase; +using wallet::DBErrors; +using wallet::WALLET_FLAG_DESCRIPTORS; + +struct TipBlock +{ + uint256 prev_block_hash; + int64_t prev_block_time; + int tip_height; +}; + +TipBlock getTip(const CChainParams& params, const node::NodeContext& context) +{ + auto tip = WITH_LOCK(::cs_main, return context.chainman->ActiveTip()); + return (tip) ? TipBlock{tip->GetBlockHash(), tip->GetBlockTime(), tip->nHeight} : + TipBlock{params.GenesisBlock().GetHash(), params.GenesisBlock().GetBlockTime(), 0}; +} + +void generateFakeBlock(const CChainParams& params, + const node::NodeContext& context, + CWallet& wallet, + const CScript& coinbase_out_script) +{ + TipBlock tip{getTip(params, context)}; + + // Create block + CBlock block; + CMutableTransaction coinbase_tx; + coinbase_tx.vin.resize(1); + coinbase_tx.vin[0].prevout.SetNull(); + coinbase_tx.vout.resize(2); + coinbase_tx.vout[0].scriptPubKey = coinbase_out_script; + coinbase_tx.vout[0].nValue = 49 * COIN; + coinbase_tx.vin[0].scriptSig = CScript() << ++tip.tip_height << OP_0; + coinbase_tx.vout[1].scriptPubKey = coinbase_out_script; // extra output + coinbase_tx.vout[1].nValue = 1 * COIN; + block.vtx = {MakeTransactionRef(std::move(coinbase_tx))}; + + block.nVersion = VERSIONBITS_LAST_OLD_BLOCK_VERSION; + block.hashPrevBlock = tip.prev_block_hash; + block.hashMerkleRoot = BlockMerkleRoot(block); + block.nTime = ++tip.prev_block_time; + block.nBits = params.GenesisBlock().nBits; + block.nNonce = 0; + + { + LOCK(::cs_main); + // Add it to the index + CBlockIndex* pindex{context.chainman->m_blockman.AddToBlockIndex(block, block.GetHash(), context.chainman->m_best_header)}; + // add it to the chain + context.chainman->ActiveChain().SetTip(*pindex); + } + + // notify wallet + const auto& pindex = WITH_LOCK(::cs_main, return context.chainman->ActiveChain().Tip()); + wallet.blockConnected(kernel::MakeBlockInfo(pindex, &block)); +} + +struct PreSelectInputs { + // How many coins from the wallet the process should select + int num_of_internal_inputs; + // future: this could have external inputs as well. +}; + +static void WalletCreateTx(benchmark::Bench& bench, bool allow_other_inputs, std::optional preset_inputs) +{ + const auto test_setup = MakeNoLogFileContext(); + + CWallet wallet{test_setup->m_node.chain.get(), test_setup->m_node.coinjoin_loader.get(), "", gArgs, CreateMockWalletDatabase()}; + { + LOCK(wallet.cs_wallet); + wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS); + wallet.SetupDescriptorScriptPubKeyMans("", ""); + if (wallet.LoadWallet() != DBErrors::LOAD_OK) assert(false); + } + + // Generate destinations + CScript dest = GetScriptForDestination(getNewDestination(wallet)); + + // Generate chain; each coinbase will have two outputs to fill-up the wallet + const auto& params = Params(); + unsigned int chain_size = 5000; // 5k blocks means 10k UTXO for the wallet (minus 100 due COINBASE_MATURITY) + for (unsigned int i = 0; i < chain_size; ++i) { + generateFakeBlock(params, test_setup->m_node, wallet, dest); + } + + // Check available balance + auto bal = WITH_LOCK(wallet.cs_wallet, return wallet::AvailableCoins(wallet).total_amount); // Cache + assert(bal == 50 * COIN * (chain_size - COINBASE_MATURITY)); + + wallet::CCoinControl coin_control; + coin_control.m_allow_other_inputs = allow_other_inputs; + + CAmount target = 0; + if (preset_inputs) { + // Select inputs, each has 49 DASH + const auto& res = WITH_LOCK(wallet.cs_wallet, + return wallet::AvailableCoins(wallet, nullptr, std::nullopt, 1, MAX_MONEY, + MAX_MONEY, preset_inputs->num_of_internal_inputs)); + for (int i = 0; i < preset_inputs->num_of_internal_inputs; i++) { + const auto& coin{res.legacy[i]}; + target += coin.txout.nValue; + coin_control.Select(coin.outpoint); + } + } + + // If automatic coin selection is enabled, add the value of another UTXO to the target + if (coin_control.m_allow_other_inputs) target += 50 * COIN; + std::vector recipients = {{dest, target, true}}; + + bench.epochIterations(5).run([&] { + LOCK(wallet.cs_wallet); + const auto& tx_res = CreateTransaction(wallet, recipients, wallet::RANDOM_CHANGE_POSITION, coin_control); + assert(tx_res); + }); +} + +static void WalletCreateTxUseOnlyPresetInputs(benchmark::Bench& bench) { WalletCreateTx(bench, /*allow_other_inputs=*/false, + {{/*num_of_internal_inputs=*/4}}); } + +static void WalletCreateTxUsePresetInputsAndCoinSelection(benchmark::Bench& bench) { WalletCreateTx(bench, /*allow_other_inputs=*/true, + {{/*num_of_internal_inputs=*/4}}); } + +BENCHMARK(WalletCreateTxUseOnlyPresetInputs, benchmark::PriorityLevel::LOW) +BENCHMARK(WalletCreateTxUsePresetInputsAndCoinSelection, benchmark::PriorityLevel::LOW) diff --git a/src/qt/sendcoinsdialog.cpp b/src/qt/sendcoinsdialog.cpp index db2a304b2410..ec012b6db580 100644 --- a/src/qt/sendcoinsdialog.cpp +++ b/src/qt/sendcoinsdialog.cpp @@ -324,7 +324,9 @@ bool SendCoinsDialog::send(const QList& recipients, QString& updateCoinControlState(); - prepareStatus = model->prepareTransaction(*m_current_transaction, *m_coin_control); + CCoinControl coin_control = *m_coin_control; + coin_control.m_allow_other_inputs = !coin_control.HasSelected(); // future, could introduce a checkbox to customize this value. + prepareStatus = model->prepareTransaction(*m_current_transaction, coin_control); // process prepareStatus and on error generate message shown to user processSendCoinsReturn(prepareStatus, @@ -895,8 +897,13 @@ void SendCoinsDialog::useAvailableBalance(SendCoinsEntry* entry) // Include watch-only for wallets without private key m_coin_control->fAllowWatchOnly = model->wallet().privateKeysDisabled() && !model->wallet().hasExternalSigner(); + // Same behavior as send: if we have selected coins, only obtain their available balance. + // Copy to avoid modifying the member's data. + CCoinControl coin_control = *m_coin_control; + coin_control.m_allow_other_inputs = !coin_control.HasSelected(); + // Calculate available amount to send. - CAmount amount = model->wallet().getAvailableBalance(*m_coin_control); + CAmount amount = model->wallet().getAvailableBalance(coin_control); for (int i = 0; i < ui->entries->count(); ++i) { SendCoinsEntry* e = qobject_cast(ui->entries->itemAt(i)->widget()); if (e && !e->isHidden() && e != entry) { diff --git a/src/qt/sendcoinsdialog.h b/src/qt/sendcoinsdialog.h index 90e1eff6ce6e..faa25983cc2d 100644 --- a/src/qt/sendcoinsdialog.h +++ b/src/qt/sendcoinsdialog.h @@ -50,6 +50,9 @@ class SendCoinsDialog : public QDialog void pasteEntry(const SendCoinsRecipient &rv); bool handlePaymentRequest(const SendCoinsRecipient &recipient); + // Only used for testing-purposes + wallet::CCoinControl* getCoinControl() { return m_coin_control.get(); } + public Q_SLOTS: void clear(); void reject() override; diff --git a/src/qt/test/wallettests.cpp b/src/qt/test/wallettests.cpp index f051563a8532..1acd202a7c22 100644 --- a/src/qt/test/wallettests.cpp +++ b/src/qt/test/wallettests.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -99,6 +100,42 @@ QModelIndex FindTx(const QAbstractItemModel& model, const uint256& txid) return {}; } +// Verify the 'useAvailableBalance' functionality. With and without manually selected coins. +// Case 1: No coin control selected coins. +// 'useAvailableBalance' should fill the amount edit box with the total available balance +// Case 2: With coin control selected coins. +// 'useAvailableBalance' should fill the amount edit box with the sum of the selected coins values. +void VerifyUseAvailableBalance(SendCoinsDialog& sendCoinsDialog, const WalletModel& walletModel) +{ + // Verify first entry amount and "useAvailableBalance" button + QVBoxLayout* entries = sendCoinsDialog.findChild("entries"); + QVERIFY(entries->count() == 1); // only one entry + SendCoinsEntry* send_entry = qobject_cast(entries->itemAt(0)->widget()); + QVERIFY(send_entry->getValue().amount == 0); + // Now click "useAvailableBalance", check updated balance (the entire wallet balance should be set) + Q_EMIT send_entry->useAvailableBalance(send_entry); + QVERIFY(send_entry->getValue().amount == walletModel.wallet().getBalance()); + + // Now manually select two coins and click on "useAvailableBalance". Then check updated balance + // (only the sum of the selected coins should be set). + int COINS_TO_SELECT = 2; + auto coins = walletModel.wallet().listCoins(); + CAmount sum_selected_coins = 0; + int selected = 0; + QVERIFY(coins.size() == 1); // context check, coins received only on one destination + for (const auto& [outpoint, tx_out] : coins.begin()->second) { + sendCoinsDialog.getCoinControl()->Select(outpoint); + sum_selected_coins += tx_out.txout.nValue; + if (++selected == COINS_TO_SELECT) break; + } + QVERIFY(selected == COINS_TO_SELECT); + + // Now that we have 2 coins selected, "useAvailableBalance" should update the balance label only with + // the sum of them. + Q_EMIT send_entry->useAvailableBalance(send_entry); + QVERIFY(send_entry->getValue().amount == sum_selected_coins); +} + //! Simple qt wallet tests. // // Test widgets can be debugged interactively calling show() on them and @@ -171,6 +208,9 @@ void TestGUI(interfaces::Node& node) QCOMPARE(balanceText, balanceComparison); } + // Check 'UseAvailableBalance' functionality + VerifyUseAvailableBalance(sendCoinsDialog, walletModel); + // Send two transactions, and verify they are added to transaction list. TransactionTableModel* transactionTableModel = walletModel.getTransactionTableModel(); QCOMPARE(transactionTableModel->rowCount({}), 105); diff --git a/src/rpc/evo.cpp b/src/rpc/evo.cpp index 59ff1c0e7cc4..f8d1c29e8ae6 100644 --- a/src/rpc/evo.cpp +++ b/src/rpc/evo.cpp @@ -366,6 +366,8 @@ static void FundSpecialTx(CWallet& wallet, CMutableTransaction& tx, const Specia CCoinControl coinControl; coinControl.destChange = fundDest; + // Fund from the given address only, spending no more of its coins than necessary. + coinControl.m_allow_other_inputs = false; coinControl.fRequireAllInputs = false; for (const auto& out : AvailableCoinsListUnspent(wallet).all()) { diff --git a/src/test/util/wallet.cpp b/src/test/util/wallet.cpp index e1d59b3bc76e..d610fe314428 100644 --- a/src/test/util/wallet.cpp +++ b/src/test/util/wallet.cpp @@ -25,7 +25,12 @@ const std::string ADDRESS_BCRT1_UNSPENDABLE = "bcrt1qqqqqqqqqqqqqqqqqqqqqqqqqqqq #ifdef ENABLE_WALLET std::string getnewaddress(CWallet& w) { - return EncodeDestination(*Assert(w.GetNewDestination(""))); + return EncodeDestination(getNewDestination(w)); +} + +CTxDestination getNewDestination(CWallet& w) +{ + return *Assert(w.GetNewDestination("")); } // void importaddress(CWallet& wallet, const std::string& address) diff --git a/src/test/util/wallet.h b/src/test/util/wallet.h index d8a81c2d8224..9a96f663daec 100644 --- a/src/test/util/wallet.h +++ b/src/test/util/wallet.h @@ -5,6 +5,8 @@ #ifndef BITCOIN_TEST_UTIL_WALLET_H #define BITCOIN_TEST_UTIL_WALLET_H +#include