From 3bf9037c65a8b75851a74b865905357c49199da1 Mon Sep 17 00:00:00 2001 From: PastaClaw Date: Sat, 11 Jul 2026 22:01:21 -0500 Subject: [PATCH 1/3] Merge bitcoin/bitcoin#25685: wallet: Faster transaction creation by removing pre-set-inputs fetching responsibility from Coin Selection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 3fcb545ab26be3e785b5e5654be0bdc77099d827 bench: benchmark transaction creation process (furszy) a8a75346d7e7247596c8a580d65ceaad49c97b97 wallet: SelectCoins, return early if target is covered by preset-inputs (furszy) f41712a734dc119f8a5e053a9cfa1f0411b5e8f1 wallet: simplify preset inputs selection target check (furszy) 5baedc33519661af9d19efcefd23dca8998d2547 wallet: remove fetch pre-selected-inputs responsibility from SelectCoins (furszy) 295852f61998a025b0b28a0671e6e1cf0dc08d0d wallet: encapsulate pre-selected-inputs lookup into its own function (furszy) 37e7887cb4bfd7db6eb462ed0741c45aea22a990 wallet: skip manually selected coins from 'AvailableCoins' result (furszy) 94c0766b0cd1990c1399a745c88c2ba4c685d8d1 wallet: skip available coins fetch if "other inputs" are disallowed (furszy) Dash adaptations (folded for bisectability, so this commit builds on its own without a follow-up adaptation commit): - bench/wallet_create_tx.cpp: Dash's CWallet constructor takes a CoinJoin loader, SetupDescriptorScriptPubKeyMans() takes mnemonic and mnemonic passphrase arguments, and CreateTransaction() takes wallet::RANDOM_CHANGE_POSITION rather than a bare -1. - bench/wallet_create_tx.cpp and test/util/wallet.{h,cpp}: Dash only supports OutputType::LEGACY, so getNewDestination() takes no output type and the benchmark drops upstream's output_type parameter. - bench/wallet_create_tx.cpp: Dash's BlockManager::AddToBlockIndex() takes the block hash as an explicit argument. - bench/wallet_create_tx.cpp: Dash's CoinsResult exposes legacy/other vectors instead of a map keyed by OutputType, so the pre-selected inputs are read from res.legacy rather than res.coins.at(output_type). - wallet/spend.cpp: TrimPreSelectedInputs() keeps Dash's CCoinControl::fRequireAllInputs=false behaviour, which upstream does not have, working on top of the new "return early when the preset inputs already cover the target" path. The trimmed subset is now walked in PreSelectedInputs::coins order (std::set, ordered by outpoint) instead of the previous AvailableCoins order. The invariant is unchanged: the trimmed set still covers the target, and it is only ever computed on a preset set that already covers it. Which particular subset is chosen may differ, so coinselector_tests' Dash-specific minimum_inputs_test now asserts the input count and that the target is covered, rather than one exact selected value. - rpc/evo.cpp: upstream flips the CCoinControl::m_allow_other_inputs default from false to true, so FundSpecialTx() now sets it to false explicitly to keep funding restricted to the requested address. Upstream makes the equivalent adjustment for the Qt send dialog. Pull request description: #### # Context (Current Flow on Master) In the transaction creation process, in order to select which coins the new transaction will spend, we first obtain all the available coins known by the wallet, which means walking-through the wallet txes map, gathering the ones that fulfill certain spendability requirements in a vector. This coins vector is then provided to the Coin Selection process, which first checks if the user has manually selected any input (which could be internal, aka known by the wallet, or external), and if it does, it fetches them by searching each of them inside the wallet and/or inside the Coin Control external tx data. Then, after finding the pre-selected-inputs and gathering them in a vector, the Coin Selection process walks-through the entire available coins vector once more just to erase coins that are in both vectors. So the Coin Selection process doesn’t pick them twice (duplicate inputs inside the same transaction). #### # Process Workflow Changes Now, a new method, `FetchCoins` will be responsible for: 1) Lookup the user pre-selected-inputs (which can be internal or external). 2) And, fetch the available coins in the wallet (excluding the already fetched ones). Which will occur prior to the Coin Selection process. Which allows us to never include the pre-selected-inputs inside the available coins vector in the first place, as well as doing other nice improvements (written below). So, Coin Selection can perform its main responsibility without mixing it with having to fetch internal/external coins nor any slow and unneeded duplicate coins verification. #### # Summarizing the Improvements: 1) If any pre-selected-input lookup fail, the process will return the error right away. 2) The pre-selected-inputs lookup failure causes are properly described on the return error. 3) Faster Coin Selection: no longer need to remove the pre-set inputs from the available coins vector. 4) Faster transaction creation for transactions that only use manually selected inputs. ACKs for top commit: S3RK: Code Review ACK 3fcb545ab26be3e785b5e5654be0bdc77099d827 achow101: ACK 3fcb545ab26be3e785b5e5654be0bdc77099d827 aureleoules: reACK 3fcb545ab26be3e785b5e5654be0bdc77099d827 Tree-SHA512: 42f833e92f40c348007ca565a4c98039e6f1ff25d8322bc2b27115824744779baf0b0a38452e4e2cdcba45076473f1028079bbd0f670020481ec5d3db42e4731 (cherry picked from commit f37bd15d472fdc7dd3d40cafaba9e8dfddd6b530) --- src/Makefile.bench.include | 1 + src/bench/wallet_create_tx.cpp | 142 +++++++++++ src/qt/sendcoinsdialog.cpp | 4 +- src/test/util/wallet.cpp | 7 +- src/test/util/wallet.h | 6 +- src/wallet/coincontrol.h | 2 +- src/wallet/coinselection.cpp | 6 + src/wallet/coinselection.h | 1 + src/wallet/interfaces.cpp | 2 + src/wallet/spend.cpp | 244 ++++++++++--------- src/wallet/spend.h | 40 ++- src/wallet/test/coinselector_tests.cpp | 38 ++- src/wallet/test/wallet_tests.cpp | 10 + test/functional/rpc_psbt.py | 2 +- test/functional/wallet_fundrawtransaction.py | 16 +- test/functional/wallet_send.py | 2 +- 16 files changed, 385 insertions(+), 138 deletions(-) create mode 100644 src/bench/wallet_create_tx.cpp diff --git a/src/Makefile.bench.include b/src/Makefile.bench.include index 7dec51f2b0c1..fb9cfa5001c2 100644 --- a/src/Makefile.bench.include +++ b/src/Makefile.bench.include @@ -93,6 +93,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..2576341fabc1 --- /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 200 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 = wallet::GetAvailableBalance(wallet); // 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 1b25c171abb5..e2a2be3e13dd 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, 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