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) 2022-present The Bitcoin Core developers2// Distributed under the MIT software license, see the accompanying3// file COPYING or https://www.opensource.org/licenses/mit-license.php.5#include <test/util/setup_common.h>7#include <wallet/coinselection.h>8#include <wallet/spend.h>9#include <wallet/test/util.h>10#include <wallet/wallet.h>12#include <boost/test/unit_test.hpp>14namespace wallet {15BOOST_FIXTURE_TEST_SUITE(group_outputs_tests, TestingSetup)17static int nextLockTime = 0;19static std::shared_ptr<CWallet> NewWallet(const node::NodeContext& m_node)20{21std::unique_ptr<CWallet> wallet = std::make_unique<CWallet>(m_node.chain.get(), "", CreateMockableWalletDatabase());22LOCK(wallet->cs_wallet);23wallet->SetWalletFlag(WALLET_FLAG_DESCRIPTORS);24wallet->SetupDescriptorScriptPubKeyMans();25return wallet;26}28static void addCoin(CoinsResult& coins,29CWallet& wallet,30const CTxDestination& dest,31const CAmount& nValue,32bool is_from_me,33CFeeRate fee_rate = CFeeRate(0),34int depth = 6)35{36CMutableTransaction tx;37tx.nLockTime = nextLockTime++; // so all transactions get different hashes38tx.vout.resize(1);39tx.vout[0].nValue = nValue;40tx.vout[0].scriptPubKey = GetScriptForDestination(dest);42const auto txid{tx.GetHash()};43LOCK(wallet.cs_wallet);44auto ret = wallet.mapWallet.emplace(std::piecewise_construct, std::forward_as_tuple(txid), std::forward_as_tuple(MakeTransactionRef(std::move(tx)), TxStateInactive{}));45assert(ret.second);46CWalletTx& wtx = (*ret.first).second;47const auto& txout = wtx.GetTx()->vout.at(0);48coins.Add(*Assert(OutputTypeFromDestination(dest)),49{COutPoint(wtx.GetHash(), 0),50txout,51depth,52CalculateMaximumSignedInputSize(txout, &wallet, /*coin_control=*/nullptr),53/*solvable=*/ true,54/*safe=*/ true,55wtx.GetTxTime(),56is_from_me,57fee_rate});58}60CoinSelectionParams makeSelectionParams(FastRandomContext& rand, bool avoid_partial_spends)61{62return CoinSelectionParams{63rand,64/*change_output_size=*/ 0,65/*change_spend_size=*/ 0,66/*min_change_target=*/ CENT,67/*effective_feerate=*/ CFeeRate(0),68/*long_term_feerate=*/ CFeeRate(0),69/*discard_feerate=*/ CFeeRate(0),70/*tx_noinputs_size=*/ 0,71/*avoid_partial=*/ avoid_partial_spends,72};73}75class GroupVerifier76{77public:78std::shared_ptr<CWallet> wallet{nullptr};79CoinsResult coins_pool;80FastRandomContext rand;82void GroupVerify(const OutputType type,83const CoinEligibilityFilter& filter,84bool avoid_partial_spends,85bool positive_only,86int expected_size)87{88OutputGroupTypeMap groups = GroupOutputs(*wallet, coins_pool, makeSelectionParams(rand, avoid_partial_spends), {{filter}})[filter];89std::vector<OutputGroup>& groups_out = positive_only ? groups.groups_by_type[type].positive_group :90groups.groups_by_type[type].mixed_group;91BOOST_CHECK_EQUAL(groups_out.size(), expected_size);92}94void GroupAndVerify(const OutputType type,95const CoinEligibilityFilter& filter,96int expected_with_partial_spends_size,97int expected_without_partial_spends_size,98bool positive_only)99{100// First avoid partial spends101GroupVerify(type, filter, /*avoid_partial_spends=*/false, positive_only, expected_with_partial_spends_size);102// Second don't avoid partial spends103GroupVerify(type, filter, /*avoid_partial_spends=*/true, positive_only, expected_without_partial_spends_size);104}105};107BOOST_AUTO_TEST_CASE(outputs_grouping_tests)108{109const auto& wallet = NewWallet(m_node);110GroupVerifier group_verifier;111group_verifier.wallet = wallet;113const CoinEligibilityFilter& BASIC_FILTER{1, 6, 0};115// #################################################################################116// 10 outputs from different txs going to the same script117// 1) if partial spends is enabled --> must not be grouped118// 2) if partial spends is not enabled --> must be grouped into a single OutputGroup119// #################################################################################121unsigned long GROUP_SIZE = 10;122const CTxDestination dest = *Assert(wallet->GetNewDestination(OutputType::BECH32, ""));123for (unsigned long i = 0; i < GROUP_SIZE; i++) {124addCoin(group_verifier.coins_pool, *wallet, dest, 10 * COIN, /*is_from_me=*/true);125}127group_verifier.GroupAndVerify(OutputType::BECH32,128BASIC_FILTER,129/*expected_with_partial_spends_size=*/ GROUP_SIZE,130/*expected_without_partial_spends_size=*/ 1,131/*positive_only=*/ true);133// ####################################################################################134// 3) 10 more UTXO are added with a different script --> must be grouped into a single135// group for avoid partial spends and 10 different output groups for partial spends136// ####################################################################################138const CTxDestination dest2 = *Assert(wallet->GetNewDestination(OutputType::BECH32, ""));139for (unsigned long i = 0; i < GROUP_SIZE; i++) {140addCoin(group_verifier.coins_pool, *wallet, dest2, 5 * COIN, /*is_from_me=*/true);141}143group_verifier.GroupAndVerify(OutputType::BECH32,144BASIC_FILTER,145/*expected_with_partial_spends_size=*/ GROUP_SIZE * 2,146/*expected_without_partial_spends_size=*/ 2,147/*positive_only=*/ true);149// ################################################################################150// 4) Now add a negative output --> which will be skipped if "positive_only" is set151// ################################################################################153const CTxDestination dest3 = *Assert(wallet->GetNewDestination(OutputType::BECH32, ""));154addCoin(group_verifier.coins_pool, *wallet, dest3, 1, true, CFeeRate(100));155BOOST_CHECK(group_verifier.coins_pool.coins[OutputType::BECH32].back().GetEffectiveValue() <= 0);157// First expect no changes with "positive_only" enabled158group_verifier.GroupAndVerify(OutputType::BECH32,159BASIC_FILTER,160/*expected_with_partial_spends_size=*/ GROUP_SIZE * 2,161/*expected_without_partial_spends_size=*/ 2,162/*positive_only=*/ true);164// Then expect changes with "positive_only" disabled165group_verifier.GroupAndVerify(OutputType::BECH32,166BASIC_FILTER,167/*expected_with_partial_spends_size=*/ GROUP_SIZE * 2 + 1,168/*expected_without_partial_spends_size=*/ 3,169/*positive_only=*/ false);172// ##############################################################################173// 5) Try to add a non-eligible UTXO (due not fulfilling the min depth target for174// "not mine" UTXOs) --> it must not be added to any group175// ##############################################################################177const CTxDestination dest4 = *Assert(wallet->GetNewDestination(OutputType::BECH32, ""));178addCoin(group_verifier.coins_pool, *wallet, dest4, 6 * COIN,179/*is_from_me=*/false, CFeeRate(0), /*depth=*/5);181// Expect no changes from this round and the previous one (point 4)182group_verifier.GroupAndVerify(OutputType::BECH32,183BASIC_FILTER,184/*expected_with_partial_spends_size=*/ GROUP_SIZE * 2 + 1,185/*expected_without_partial_spends_size=*/ 3,186/*positive_only=*/ false);189// ##############################################################################190// 6) Try to add a non-eligible UTXO (due not fulfilling the min depth target for191// "mine" UTXOs) --> it must not be added to any group192// ##############################################################################194const CTxDestination dest5 = *Assert(wallet->GetNewDestination(OutputType::BECH32, ""));195addCoin(group_verifier.coins_pool, *wallet, dest5, 6 * COIN,196/*is_from_me=*/true, CFeeRate(0), /*depth=*/0);198// Expect no changes from this round and the previous one (point 5)199group_verifier.GroupAndVerify(OutputType::BECH32,200BASIC_FILTER,201/*expected_with_partial_spends_size=*/ GROUP_SIZE * 2 + 1,202/*expected_without_partial_spends_size=*/ 3,203/*positive_only=*/ false);205// ###########################################################################################206// 7) Surpass the OUTPUT_GROUP_MAX_ENTRIES and verify that a second partial group gets created207// ###########################################################################################209const CTxDestination dest7 = *Assert(wallet->GetNewDestination(OutputType::BECH32, ""));210uint16_t NUM_SINGLE_ENTRIES = 101;211for (unsigned long i = 0; i < NUM_SINGLE_ENTRIES; i++) { // OUTPUT_GROUP_MAX_ENTRIES{100}212addCoin(group_verifier.coins_pool, *wallet, dest7, 9 * COIN, /*is_from_me=*/true);213}215// Exclude partial groups only adds one more group to the previous test case (point 6)216int PREVIOUS_ROUND_COUNT = GROUP_SIZE * 2 + 1;217group_verifier.GroupAndVerify(OutputType::BECH32,218BASIC_FILTER,219/*expected_with_partial_spends_size=*/ PREVIOUS_ROUND_COUNT + NUM_SINGLE_ENTRIES,220/*expected_without_partial_spends_size=*/ 4,221/*positive_only=*/ false);223// Include partial groups should add one more group inside the "avoid partial spends" count224const CoinEligibilityFilter& avoid_partial_groups_filter{1, 6, 0, 0, /*include_partial=*/ true};225group_verifier.GroupAndVerify(OutputType::BECH32,226avoid_partial_groups_filter,227/*expected_with_partial_spends_size=*/ PREVIOUS_ROUND_COUNT + NUM_SINGLE_ENTRIES,228/*expected_without_partial_spends_size=*/ 5,229/*positive_only=*/ false);230}232BOOST_AUTO_TEST_SUITE_END()233} // end namespace wallet
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.