Branch data Line data Source code
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 : 1 : static std::shared_ptr<CWallet> NewWallet(const node::NodeContext& m_node)
20 : : {
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 : 124 : 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 : 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 [ + - + - : 248 : 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 [ + - + - : 248 : coins.Add(*Assert(OutputTypeFromDestination(dest)),
+ - + - +
- + - +
- ]
49 [ + - ]: 124 : {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 [ + - ]: 124 : fee_rate});
58 : 248 : }
59 : :
60 : 16 : CoinSelectionParams makeSelectionParams(FastRandomContext& rand, bool avoid_partial_spends)
61 : : {
62 : 16 : return CoinSelectionParams{
63 : : rand,
64 : : /*change_output_size=*/ 0,
65 : : /*change_spend_size=*/ 0,
66 : : /*min_change_target=*/ CENT,
67 : 16 : /*effective_feerate=*/ CFeeRate(0),
68 : 16 : /*long_term_feerate=*/ CFeeRate(0),
69 : 16 : /*discard_feerate=*/ CFeeRate(0),
70 : : /*tx_noinputs_size=*/ 0,
71 : : /*avoid_partial=*/ avoid_partial_spends,
72 : 16 : };
73 : : }
74 : :
75 : : class GroupVerifier
76 : : {
77 : : public:
78 : : std::shared_ptr<CWallet> wallet{nullptr};
79 : : CoinsResult coins_pool;
80 : : FastRandomContext rand;
81 : :
82 : 16 : void GroupVerify(const OutputType type,
83 : : const CoinEligibilityFilter& filter,
84 : : bool avoid_partial_spends,
85 : : bool positive_only,
86 : : int expected_size)
87 : : {
88 [ + - + - : 32 : OutputGroupTypeMap groups = GroupOutputs(*wallet, coins_pool, makeSelectionParams(rand, avoid_partial_spends), {{filter}})[filter];
+ - + + ]
89 [ + - ]: 6 : std::vector<OutputGroup>& groups_out = positive_only ? groups.groups_by_type[type].positive_group :
90 [ + + + - ]: 22 : groups.groups_by_type[type].mixed_group;
91 [ + - - + : 16 : BOOST_CHECK_EQUAL(groups_out.size(), expected_size);
+ - ]
92 : 16 : }
93 : :
94 : 8 : 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 : 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 [ + - + - : 7 : BOOST_AUTO_TEST_CASE(outputs_grouping_tests)
+ - + - -
+ + - + -
+ - + - +
- + - - +
+ - + - +
- + - + -
+ - - + +
- + - + -
+ - + - +
- + - - +
+ - + - +
- + - + -
+ - - + +
- ]
108 : : {
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 [ + - + - : 2 : 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 : : }
126 : :
127 [ + - ]: 1 : 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 [ + - + - : 2 : 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 : : }
142 : :
143 [ + - ]: 1 : 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 [ + - + - : 2 : const CTxDestination dest3 = *Assert(wallet->GetNewDestination(OutputType::BECH32, ""));
+ - + - ]
154 [ + - ]: 1 : addCoin(group_verifier.coins_pool, *wallet, dest3, 1, true, CFeeRate(100));
155 [ + - + - : 2 : 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 : : 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 [ + - ]: 1 : 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 [ + - + - : 2 : 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 : : 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 [ + - + - : 2 : 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 : : 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 [ + - + - : 2 : 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 : : }
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 : : 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 [ + - ]: 1 : const CoinEligibilityFilter& avoid_partial_groups_filter{1, 6, 0, 0, /*include_partial=*/ true};
225 [ + - ]: 1 : 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 [ + - ]: 2 : }
231 : :
232 : : BOOST_AUTO_TEST_SUITE_END()
233 : : } // end namespace wallet
|