group_outputs_tests.cpp raw
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
234