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