Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Makefile.bench.include
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
142 changes: 142 additions & 0 deletions src/bench/wallet_create_tx.cpp
Original file line number Diff line number Diff line change
@@ -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 <bench/bench.h>
#include <chainparams.h>
#include <wallet/coincontrol.h>
#include <consensus/merkle.h>
#include <kernel/chain.h>
#include <node/context.h>
#include <test/util/setup_common.h>
#include <test/util/wallet.h>
#include <validation.h>
#include <wallet/spend.h>
#include <wallet/wallet.h>

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));

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: bench target does not compile for three intermediate commits (fixed by final commit in this same PR)

Commit c392040ca08 registers this new source in bench_bench_dash_SOURCES but calls wallet.blockConnected(block, pindex->nHeight). At that commit, CWallet declares only blockConnected(const interfaces::BlockInfo&), so a wallet-enabled benchmark build fails at c392040ca08 and remains broken through 0969ce0fe7e and cde726cc5e1. The final commit correctly changes the call to kernel::MakeBlockInfo, but Dash preserves commits when merging, so fold that adaptation into c392040ca08 or place it immediately afterward to keep the permanent history independently buildable and bisectable.

source: ['claude', 'codex']

}

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<PreSelectInputs> preset_inputs)
{
const auto test_setup = MakeNoLogFileContext<const TestingSetup>();

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<wallet::CRecipient> 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)
11 changes: 9 additions & 2 deletions src/qt/sendcoinsdialog.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,9 @@ bool SendCoinsDialog::send(const QList<SendCoinsRecipient>& 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,
Expand Down Expand Up @@ -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<SendCoinsEntry*>(ui->entries->itemAt(i)->widget());
if (e && !e->isHidden() && e != entry) {
Expand Down
3 changes: 3 additions & 0 deletions src/qt/sendcoinsdialog.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
40 changes: 40 additions & 0 deletions src/qt/test/wallettests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
#include <key_io.h>
#include <test/util/setup_common.h>
#include <validation.h>
#include <wallet/coincontrol.h>
#include <wallet/wallet.h>
#include <qt/overviewpage.h>
#include <qt/receivecoinsdialog.h>
Expand Down Expand Up @@ -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<QVBoxLayout*>("entries");
QVERIFY(entries->count() == 1); // only one entry
SendCoinsEntry* send_entry = qobject_cast<SendCoinsEntry*>(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
Expand Down Expand Up @@ -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);
Expand Down
2 changes: 2 additions & 0 deletions src/rpc/evo.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand Down
7 changes: 6 additions & 1 deletion src/test/util/wallet.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
6 changes: 5 additions & 1 deletion src/test/util/wallet.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
#ifndef BITCOIN_TEST_UTIL_WALLET_H
#define BITCOIN_TEST_UTIL_WALLET_H

#include <script/standard.h>

#include <string>

namespace wallet {
Expand All @@ -20,8 +22,10 @@ extern const std::string ADDRESS_BCRT1_UNSPENDABLE;

/** Import the address to the wallet */
void importaddress(wallet::CWallet& wallet, const std::string& address);
/** Returns a new address from the wallet */
/** Returns a new encoded destination from the wallet */
std::string getnewaddress(wallet::CWallet& w);
/** Returns a new destination from the wallet. Dash only supports OutputType::LEGACY. */
CTxDestination getNewDestination(wallet::CWallet& w);


#endif // BITCOIN_TEST_UTIL_WALLET_H
2 changes: 1 addition & 1 deletion src/wallet/coincontrol.h
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ class CCoinControl
bool m_include_unsafe_inputs = false;
//! If true, the selection process can add extra unselected inputs from the wallet
//! while requires all selected inputs be used
bool m_allow_other_inputs = false;
bool m_allow_other_inputs = true;
//! If false, only include as many inputs as necessary to fulfill a coin selection request. Only usable together with m_allow_other_inputs
bool fRequireAllInputs = true;
//! Includes watch only addresses which are solvable
Expand Down
Loading