/tmp/bitcoin/src/wallet/test/group_outputs_tests.cpp
Line | Count | Source |
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 | 1 | { |
21 | 1 | std::unique_ptr<CWallet> wallet = std::make_unique<CWallet>(m_node.chain.get(), "", CreateMockableWalletDatabase()); |
22 | 1 | LOCK(wallet->cs_wallet); |
23 | 1 | wallet->SetWalletFlag(WALLET_FLAG_DESCRIPTORS); |
24 | 1 | wallet->SetupDescriptorScriptPubKeyMans(); |
25 | 1 | return wallet; |
26 | 1 | } |
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 | 124 | { |
36 | 124 | CMutableTransaction tx; |
37 | 124 | tx.nLockTime = nextLockTime++; // so all transactions get different hashes |
38 | 124 | tx.vout.resize(1); |
39 | 124 | tx.vout[0].nValue = nValue; |
40 | 124 | tx.vout[0].scriptPubKey = GetScriptForDestination(dest); |
41 | | |
42 | 124 | const auto txid{tx.GetHash()}; |
43 | 124 | LOCK(wallet.cs_wallet); |
44 | 124 | auto ret = wallet.mapWallet.emplace(std::piecewise_construct, std::forward_as_tuple(txid), std::forward_as_tuple(MakeTransactionRef(std::move(tx)), TxStateInactive{})); |
45 | 124 | assert(ret.second); |
46 | 124 | CWalletTx& wtx = (*ret.first).second; |
47 | 124 | const auto& txout = wtx.tx->vout.at(0); |
48 | 124 | coins.Add(*Assert(OutputTypeFromDestination(dest)), |
49 | 124 | {COutPoint(wtx.GetHash(), 0), |
50 | 124 | txout, |
51 | 124 | depth, |
52 | 124 | CalculateMaximumSignedInputSize(txout, &wallet, /*coin_control=*/nullptr), |
53 | 124 | /*solvable=*/ true, |
54 | 124 | /*safe=*/ true, |
55 | 124 | wtx.GetTxTime(), |
56 | 124 | is_from_me, |
57 | 124 | fee_rate}); |
58 | 124 | } |
59 | | |
60 | | CoinSelectionParams makeSelectionParams(FastRandomContext& rand, bool avoid_partial_spends) |
61 | 16 | { |
62 | 16 | return CoinSelectionParams{ |
63 | 16 | rand, |
64 | 16 | /*change_output_size=*/ 0, |
65 | 16 | /*change_spend_size=*/ 0, |
66 | 16 | /*min_change_target=*/ CENT, |
67 | 16 | /*effective_feerate=*/ CFeeRate(0), |
68 | 16 | /*long_term_feerate=*/ CFeeRate(0), |
69 | 16 | /*discard_feerate=*/ CFeeRate(0), |
70 | 16 | /*tx_noinputs_size=*/ 0, |
71 | 16 | /*avoid_partial=*/ avoid_partial_spends, |
72 | 16 | }; |
73 | 16 | } |
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 | 16 | { |
88 | 16 | OutputGroupTypeMap groups = GroupOutputs(*wallet, coins_pool, makeSelectionParams(rand, avoid_partial_spends), {{filter}})[filter]; |
89 | 16 | std::vector<OutputGroup>& groups_out = positive_only ? groups.groups_by_type[type].positive_group : |
90 | 16 | groups.groups_by_type[type].mixed_group; |
91 | 16 | BOOST_CHECK_EQUAL(groups_out.size(), expected_size); |
92 | 16 | } |
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 | 8 | { |
100 | | // First avoid partial spends |
101 | 8 | GroupVerify(type, filter, /*avoid_partial_spends=*/false, positive_only, expected_with_partial_spends_size); |
102 | | // Second don't avoid partial spends |
103 | 8 | GroupVerify(type, filter, /*avoid_partial_spends=*/true, positive_only, expected_without_partial_spends_size); |
104 | 8 | } |
105 | | }; |
106 | | |
107 | | BOOST_AUTO_TEST_CASE(outputs_grouping_tests) |
108 | 1 | { |
109 | 1 | const auto& wallet = NewWallet(m_node); |
110 | 1 | GroupVerifier group_verifier; |
111 | 1 | group_verifier.wallet = wallet; |
112 | | |
113 | 1 | 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 | 1 | unsigned long GROUP_SIZE = 10; |
122 | 1 | const CTxDestination dest = *Assert(wallet->GetNewDestination(OutputType::BECH32, "")); |
123 | 11 | for (unsigned long i = 0; i < GROUP_SIZE; i++) { |
124 | 10 | addCoin(group_verifier.coins_pool, *wallet, dest, 10 * COIN, /*is_from_me=*/true); |
125 | 10 | } |
126 | | |
127 | 1 | group_verifier.GroupAndVerify(OutputType::BECH32, |
128 | 1 | BASIC_FILTER, |
129 | 1 | /*expected_with_partial_spends_size=*/ GROUP_SIZE, |
130 | 1 | /*expected_without_partial_spends_size=*/ 1, |
131 | 1 | /*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 | 1 | const CTxDestination dest2 = *Assert(wallet->GetNewDestination(OutputType::BECH32, "")); |
139 | 11 | for (unsigned long i = 0; i < GROUP_SIZE; i++) { |
140 | 10 | addCoin(group_verifier.coins_pool, *wallet, dest2, 5 * COIN, /*is_from_me=*/true); |
141 | 10 | } |
142 | | |
143 | 1 | group_verifier.GroupAndVerify(OutputType::BECH32, |
144 | 1 | BASIC_FILTER, |
145 | 1 | /*expected_with_partial_spends_size=*/ GROUP_SIZE * 2, |
146 | 1 | /*expected_without_partial_spends_size=*/ 2, |
147 | 1 | /*positive_only=*/ true); |
148 | | |
149 | | // ################################################################################ |
150 | | // 4) Now add a negative output --> which will be skipped if "positive_only" is set |
151 | | // ################################################################################ |
152 | | |
153 | 1 | const CTxDestination dest3 = *Assert(wallet->GetNewDestination(OutputType::BECH32, "")); |
154 | 1 | addCoin(group_verifier.coins_pool, *wallet, dest3, 1, true, CFeeRate(100)); |
155 | 1 | BOOST_CHECK(group_verifier.coins_pool.coins[OutputType::BECH32].back().GetEffectiveValue() <= 0); |
156 | | |
157 | | // First expect no changes with "positive_only" enabled |
158 | 1 | group_verifier.GroupAndVerify(OutputType::BECH32, |
159 | 1 | BASIC_FILTER, |
160 | 1 | /*expected_with_partial_spends_size=*/ GROUP_SIZE * 2, |
161 | 1 | /*expected_without_partial_spends_size=*/ 2, |
162 | 1 | /*positive_only=*/ true); |
163 | | |
164 | | // Then expect changes with "positive_only" disabled |
165 | 1 | group_verifier.GroupAndVerify(OutputType::BECH32, |
166 | 1 | BASIC_FILTER, |
167 | 1 | /*expected_with_partial_spends_size=*/ GROUP_SIZE * 2 + 1, |
168 | 1 | /*expected_without_partial_spends_size=*/ 3, |
169 | 1 | /*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 | 1 | const CTxDestination dest4 = *Assert(wallet->GetNewDestination(OutputType::BECH32, "")); |
178 | 1 | addCoin(group_verifier.coins_pool, *wallet, dest4, 6 * COIN, |
179 | 1 | /*is_from_me=*/false, CFeeRate(0), /*depth=*/5); |
180 | | |
181 | | // Expect no changes from this round and the previous one (point 4) |
182 | 1 | group_verifier.GroupAndVerify(OutputType::BECH32, |
183 | 1 | BASIC_FILTER, |
184 | 1 | /*expected_with_partial_spends_size=*/ GROUP_SIZE * 2 + 1, |
185 | 1 | /*expected_without_partial_spends_size=*/ 3, |
186 | 1 | /*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 | 1 | const CTxDestination dest5 = *Assert(wallet->GetNewDestination(OutputType::BECH32, "")); |
195 | 1 | addCoin(group_verifier.coins_pool, *wallet, dest5, 6 * COIN, |
196 | 1 | /*is_from_me=*/true, CFeeRate(0), /*depth=*/0); |
197 | | |
198 | | // Expect no changes from this round and the previous one (point 5) |
199 | 1 | group_verifier.GroupAndVerify(OutputType::BECH32, |
200 | 1 | BASIC_FILTER, |
201 | 1 | /*expected_with_partial_spends_size=*/ GROUP_SIZE * 2 + 1, |
202 | 1 | /*expected_without_partial_spends_size=*/ 3, |
203 | 1 | /*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 | 1 | const CTxDestination dest7 = *Assert(wallet->GetNewDestination(OutputType::BECH32, "")); |
210 | 1 | uint16_t NUM_SINGLE_ENTRIES = 101; |
211 | 102 | for (unsigned long i = 0; i < NUM_SINGLE_ENTRIES; i++) { // OUTPUT_GROUP_MAX_ENTRIES{100} |
212 | 101 | addCoin(group_verifier.coins_pool, *wallet, dest7, 9 * COIN, /*is_from_me=*/true); |
213 | 101 | } |
214 | | |
215 | | // Exclude partial groups only adds one more group to the previous test case (point 6) |
216 | 1 | int PREVIOUS_ROUND_COUNT = GROUP_SIZE * 2 + 1; |
217 | 1 | group_verifier.GroupAndVerify(OutputType::BECH32, |
218 | 1 | BASIC_FILTER, |
219 | 1 | /*expected_with_partial_spends_size=*/ PREVIOUS_ROUND_COUNT + NUM_SINGLE_ENTRIES, |
220 | 1 | /*expected_without_partial_spends_size=*/ 4, |
221 | 1 | /*positive_only=*/ false); |
222 | | |
223 | | // Include partial groups should add one more group inside the "avoid partial spends" count |
224 | 1 | const CoinEligibilityFilter& avoid_partial_groups_filter{1, 6, 0, 0, /*include_partial=*/ true}; |
225 | 1 | group_verifier.GroupAndVerify(OutputType::BECH32, |
226 | 1 | avoid_partial_groups_filter, |
227 | 1 | /*expected_with_partial_spends_size=*/ PREVIOUS_ROUND_COUNT + NUM_SINGLE_ENTRIES, |
228 | 1 | /*expected_without_partial_spends_size=*/ 5, |
229 | 1 | /*positive_only=*/ false); |
230 | 1 | } |
231 | | |
232 | | BOOST_AUTO_TEST_SUITE_END() |
233 | | } // end namespace wallet |