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