Inside Bitcoin
Opening the selected file…
Inside Bitcoin
Opening the selected file…
The code behind the network, with room to understand it.
Release published 2026-10-28 · Snapshot fetched 2026-11-01. Latest stable release at fetch time; development branches and Git history are excluded. Only the selected code window and visible folders load here.
1// Copyright (c) 2017-present The Bitcoin Core developers2// Distributed under the MIT software license, see the accompanying3// file COPYING or http://www.opensource.org/licenses/mit-license.php.5#include <consensus/amount.h>6#include <node/context.h>7#include <policy/policy.h>8#include <primitives/transaction.h>9#include <random.h>10#include <test/util/common.h>11#include <test/util/setup_common.h>12#include <util/translation.h>13#include <wallet/coincontrol.h>14#include <wallet/coinselection.h>15#include <wallet/spend.h>16#include <wallet/test/util.h>17#include <wallet/test/wallet_test_fixture.h>18#include <wallet/wallet.h>20#include <algorithm>21#include <boost/test/unit_test.hpp>22#include <random>24namespace wallet {25BOOST_FIXTURE_TEST_SUITE(coinselector_tests, WalletTestingSetup)27// how many times to run all the tests to have a chance to catch errors that only show up with particular random shuffles28#define RUN_TESTS 10030// some tests fail 1% of the time due to bad luck.31// we repeat those tests this many times and only complain if all iterations of the test fail32#define RANDOM_REPEATS 534static const CoinEligibilityFilter filter_standard(1, 6, 0);35static const CoinEligibilityFilter filter_confirmed(1, 1, 0);36static const CoinEligibilityFilter filter_standard_extra(6, 6, 0);37static int nextLockTime = 0;39static void add_coin(const CAmount& nValue, int nInput, SelectionResult& result)40{41CMutableTransaction tx;42tx.vout.resize(nInput + 1);43tx.vout[nInput].nValue = nValue;44tx.nLockTime = nextLockTime++; // so all transactions get different hashes45COutput output(COutPoint(tx.GetHash(), nInput), tx.vout.at(nInput), /*depth=*/1, /*input_bytes=*/-1, /*solvable=*/true, /*safe=*/true, /*time=*/0, /*from_me=*/false, /*fees=*/ 0);46OutputGroup group;47group.Insert(std::make_shared<COutput>(output), /*ancestors=*/ 0, /*cluster_count=*/ 0);48result.AddInput(group);49}51static void add_coin(const CAmount& nValue, int nInput, SelectionResult& result, CAmount fee, CAmount long_term_fee)52{53CMutableTransaction tx;54tx.vout.resize(nInput + 1);55tx.vout[nInput].nValue = nValue;56tx.nLockTime = nextLockTime++; // so all transactions get different hashes57std::shared_ptr<COutput> coin = std::make_shared<COutput>(COutPoint(tx.GetHash(), nInput), tx.vout.at(nInput), /*depth=*/1, /*input_bytes=*/148, /*solvable=*/true, /*safe=*/true, /*time=*/0, /*from_me=*/false, fee);58OutputGroup group;59group.Insert(coin, /*ancestors=*/ 0, /*cluster_count=*/ 0);60coin->long_term_fee = long_term_fee; // group.Insert() will modify long_term_fee, so we need to set it afterwards61result.AddInput(group);62}64static void add_coin(CoinsResult& available_coins, CWallet& wallet, const CAmount& nValue, CFeeRate feerate = CFeeRate(0), int nAge = 6*24, bool fIsFromMe = false, int nInput =0, bool spendable = false, int custom_size = 0)65{66CMutableTransaction tx;67tx.nLockTime = nextLockTime++; // so all transactions get different hashes68tx.vout.resize(nInput + 1);69tx.vout[nInput].nValue = nValue;70if (spendable) {71tx.vout[nInput].scriptPubKey = GetScriptForDestination(*Assert(wallet.GetNewDestination(OutputType::BECH32, "")));72}73Txid txid = tx.GetHash();75LOCK(wallet.cs_wallet);76auto ret = wallet.mapWallet.emplace(std::piecewise_construct, std::forward_as_tuple(txid), std::forward_as_tuple(MakeTransactionRef(std::move(tx)), TxStateInactive{}));77assert(ret.second);78CWalletTx& wtx = (*ret.first).second;79const auto& txout = wtx.GetTx()->vout.at(nInput);80available_coins.Add(OutputType::BECH32, {COutPoint(wtx.GetHash(), nInput), txout, nAge, custom_size == 0 ? CalculateMaximumSignedInputSize(txout, &wallet, /*coin_control=*/nullptr) : custom_size, /*solvable=*/true, /*safe=*/true, wtx.GetTxTime(), fIsFromMe, feerate});81}83// Helpers84std::optional<SelectionResult> KnapsackSolver(std::vector<OutputGroup>& groups, const CAmount& nTargetValue,85CAmount change_target, FastRandomContext& rng)86{87auto res{KnapsackSolver(groups, nTargetValue, change_target, rng, MAX_STANDARD_TX_WEIGHT)};88return res ? std::optional<SelectionResult>(*res) : std::nullopt;89}91std::optional<SelectionResult> SelectCoinsBnB(std::vector<OutputGroup>& utxo_pool, const CAmount& selection_target, const CAmount& cost_of_change)92{93auto res{SelectCoinsBnB(utxo_pool, selection_target, cost_of_change, MAX_STANDARD_TX_WEIGHT)};94return res ? std::optional<SelectionResult>(*res) : std::nullopt;95}97/** Check if SelectionResult a is equivalent to SelectionResult b.98* Equivalent means same input values, but maybe different inputs (i.e. same value, different prevout) */99static bool EquivalentResult(const SelectionResult& a, const SelectionResult& b)100{101std::vector<CAmount> a_amts;102std::vector<CAmount> b_amts;103for (const auto& coin : a.GetInputSet()) {104a_amts.push_back(coin->txout.nValue);105}106for (const auto& coin : b.GetInputSet()) {107b_amts.push_back(coin->txout.nValue);108}109std::sort(a_amts.begin(), a_amts.end());110std::sort(b_amts.begin(), b_amts.end());112std::pair<std::vector<CAmount>::iterator, std::vector<CAmount>::iterator> ret = std::mismatch(a_amts.begin(), a_amts.end(), b_amts.begin());113return ret.first == a_amts.end() && ret.second == b_amts.end();114}116/** Check if this selection is equal to another one. Equal means same inputs (i.e same value and prevout) */117static bool EqualResult(const SelectionResult& a, const SelectionResult& b)118{119std::pair<OutputSet::iterator, OutputSet::iterator> ret = std::mismatch(a.GetInputSet().begin(), a.GetInputSet().end(), b.GetInputSet().begin(),120[](const std::shared_ptr<COutput>& a, const std::shared_ptr<COutput>& b) {121return a->outpoint == b->outpoint;122});123return ret.first == a.GetInputSet().end() && ret.second == b.GetInputSet().end();124}126inline std::vector<OutputGroup>& GroupCoins(const std::vector<COutput>& available_coins, bool subtract_fee_outputs = false)127{128static std::vector<OutputGroup> static_groups;129static_groups.clear();130for (auto& coin : available_coins) {131static_groups.emplace_back();132OutputGroup& group = static_groups.back();133group.Insert(std::make_shared<COutput>(coin), /*ancestors=*/ 0, /*cluster_count=*/ 0);134group.m_subtract_fee_outputs = subtract_fee_outputs;135}136return static_groups;137}139inline std::vector<OutputGroup>& KnapsackGroupOutputs(const CoinsResult& available_coins, CWallet& wallet, const CoinEligibilityFilter& filter)140{141FastRandomContext rand{};142CoinSelectionParams coin_selection_params{143rand,144/*change_output_size=*/ 0,145/*change_spend_size=*/ 0,146/*min_change_target=*/ CENT,147/*effective_feerate=*/ CFeeRate(0),148/*long_term_feerate=*/ CFeeRate(0),149/*discard_feerate=*/ CFeeRate(0),150/*tx_noinputs_size=*/ 0,151/*avoid_partial=*/ false,152};153static OutputGroupTypeMap static_groups;154static_groups = GroupOutputs(wallet, available_coins, coin_selection_params, {{filter}})[filter];155return static_groups.all_groups.mixed_group;156}158static std::unique_ptr<CWallet> NewWallet(const node::NodeContext& m_node, const std::string& wallet_name = "")159{160std::unique_ptr<CWallet> wallet = std::make_unique<CWallet>(m_node.chain.get(), wallet_name, CreateMockableWalletDatabase());161LOCK(wallet->cs_wallet);162wallet->SetWalletFlag(WALLET_FLAG_DESCRIPTORS);163wallet->SetupDescriptorScriptPubKeyMans();164return wallet;165}167// Branch and bound coin selection tests168BOOST_AUTO_TEST_CASE(bnb_search_test)169{170FastRandomContext rand{};171// Setup172SelectionResult expected_result(CAmount(0), SelectionAlgorithm::BNB);173size_t expected_attempts;175////////////////////176// Behavior tests //177////////////////////179// Make sure that effective value is working in AttemptSelection when BnB is used180CoinSelectionParams coin_selection_params_bnb{181rand,182/*change_output_size=*/ 31,183/*change_spend_size=*/ 68,184/*min_change_target=*/ 0,185/*effective_feerate=*/ CFeeRate(3000),186/*long_term_feerate=*/ CFeeRate(1000),187/*discard_feerate=*/ CFeeRate(1000),188/*tx_noinputs_size=*/ 0,189/*avoid_partial=*/ false,190};191coin_selection_params_bnb.m_change_fee = coin_selection_params_bnb.m_effective_feerate.GetFee(coin_selection_params_bnb.change_output_size);192coin_selection_params_bnb.m_cost_of_change = coin_selection_params_bnb.m_effective_feerate.GetFee(coin_selection_params_bnb.change_spend_size) + coin_selection_params_bnb.m_change_fee;193coin_selection_params_bnb.min_viable_change = coin_selection_params_bnb.m_effective_feerate.GetFee(coin_selection_params_bnb.change_spend_size);195{196std::unique_ptr<CWallet> wallet = NewWallet(m_node);198CoinsResult available_coins;200add_coin(available_coins, *wallet, 1, coin_selection_params_bnb.m_effective_feerate);201available_coins.All().at(0).input_bytes = 40; // Make sure that it has a negative effective value. The next check should assert if this somehow got through. Otherwise it will fail202BOOST_CHECK(!SelectCoinsBnB(GroupCoins(available_coins.All()), 1 * CENT, coin_selection_params_bnb.m_cost_of_change));204// Test fees subtracted from output:205available_coins = {};206add_coin(available_coins, *wallet, 1 * CENT, coin_selection_params_bnb.m_effective_feerate);207available_coins.All().at(0).input_bytes = 40;208const auto result9 = SelectCoinsBnB(GroupCoins(available_coins.All()), 1 * CENT, coin_selection_params_bnb.m_cost_of_change);209BOOST_CHECK(result9);210BOOST_CHECK_EQUAL(result9->GetSelectedValue(), 1 * CENT);211expected_attempts = 1;212BOOST_CHECK_MESSAGE(result9->GetSelectionsEvaluated() == expected_attempts, strprintf("Expected %i attempts, but got %i", expected_attempts, result9->GetSelectionsEvaluated()));213}215{216std::unique_ptr<CWallet> wallet = NewWallet(m_node);218CoinsResult available_coins;220coin_selection_params_bnb.m_effective_feerate = CFeeRate(0);221add_coin(available_coins, *wallet, 5 * CENT, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true);222add_coin(available_coins, *wallet, 3 * CENT, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true);223add_coin(available_coins, *wallet, 2 * CENT, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true);224CCoinControl coin_control;225coin_control.m_allow_other_inputs = true;226COutput select_coin = available_coins.All().at(0);227coin_control.Select(select_coin.outpoint);228CoinsResult selected_input;229selected_input.Add(OutputType::BECH32, select_coin);230available_coins.Erase({available_coins.coins[OutputType::BECH32].begin()->outpoint});232LOCK(wallet->cs_wallet);233const auto result10 = SelectCoins(*wallet, available_coins, selected_input, 10 * CENT, coin_control, coin_selection_params_bnb);234BOOST_CHECK(result10);235expected_attempts = 3;236BOOST_CHECK_MESSAGE(result10->GetSelectionsEvaluated() == expected_attempts, strprintf("Expected %i attempts, but got %i", expected_attempts, result10->GetSelectionsEvaluated()));237}238{239std::unique_ptr<CWallet> wallet = NewWallet(m_node);240LOCK(wallet->cs_wallet); // Every 'SelectCoins' call requires it242CoinsResult available_coins;244// pre selected coin should be selected even if disadvantageous245coin_selection_params_bnb.m_effective_feerate = CFeeRate(5000);246coin_selection_params_bnb.m_long_term_feerate = CFeeRate(3000);248// Add selectable outputs, increasing their raw amounts by their input fee to make the effective value equal to the raw amount249CAmount input_fee = coin_selection_params_bnb.m_effective_feerate.GetFee(/*virtual_bytes=*/68); // bech32 input size (default test output type)250add_coin(available_coins, *wallet, 10 * CENT + input_fee, coin_selection_params_bnb.m_effective_feerate, 6 * 24, false, 0, true);
Independent explanations by MyBitcoinExplorer. Not affiliated with or endorsed by Bitcoin Core. Guides are educational and do not replace the source or its tests.
Bitcoin Core source © its contributors, distributed under the MIT license. Original notices and bundled third-party licenses are retained in the source tree.