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 : 0 : BOOST_FIXTURE_TEST_SUITE(group_outputs_tests, TestingSetup)
16 : :
17 : 0 : static int nextLockTime = 0;
18 : 0 :
19 : 0 : static std::shared_ptr<CWallet> NewWallet(const node::NodeContext& m_node)
20 : : {
21 : 0 : std::unique_ptr<CWallet> wallet = std::make_unique<CWallet>(m_node.chain.get(), "", CreateMockableWalletDatabase());
22 : 0 : wallet->LoadWallet();
23 : 0 : LOCK(wallet->cs_wallet);
24 : 0 : wallet->SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
25 : 0 : wallet->SetupDescriptorScriptPubKeyMans();
26 : 0 : return wallet;
27 : 0 : }
28 : :
29 : 0 : 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 : 0 : int depth = 6)
36 : : {
37 : 0 : CMutableTransaction tx;
38 : 0 : tx.nLockTime = nextLockTime++; // so all transactions get different hashes
39 : 0 : tx.vout.resize(1);
40 : 0 : tx.vout[0].nValue = nValue;
41 : 0 : tx.vout[0].scriptPubKey = GetScriptForDestination(dest);
42 : :
43 : 0 : const uint256& txid = tx.GetHash();
44 : 0 : LOCK(wallet.cs_wallet);
45 : 0 : auto ret = wallet.mapWallet.emplace(std::piecewise_construct, std::forward_as_tuple(txid), std::forward_as_tuple(MakeTransactionRef(std::move(tx)), TxStateInactive{}));
46 : 0 : assert(ret.second);
47 : 0 : CWalletTx& wtx = (*ret.first).second;
48 : 0 : const auto& txout = wtx.tx->vout.at(0);
49 : 0 : coins.Add(*Assert(OutputTypeFromDestination(dest)),
50 : 0 : {COutPoint(wtx.GetHash(), 0),
51 : 0 : txout,
52 : 0 : depth,
53 : 0 : CalculateMaximumSignedInputSize(txout, &wallet, /*coin_control=*/nullptr),
54 : : /*spendable=*/ true,
55 : : /*solvable=*/ true,
56 : : /*safe=*/ true,
57 : 0 : wtx.GetTxTime(),
58 : 0 : is_from_me,
59 : 0 : fee_rate});
60 : 0 : }
61 : :
62 : 0 : CoinSelectionParams makeSelectionParams(FastRandomContext& rand, bool avoid_partial_spends)
63 : : {
64 : 0 : return CoinSelectionParams{
65 : 0 : rand,
66 : : /*change_output_size=*/ 0,
67 : : /*change_spend_size=*/ 0,
68 : : /*min_change_target=*/ CENT,
69 : 0 : /*effective_feerate=*/ CFeeRate(0),
70 : 0 : /*long_term_feerate=*/ CFeeRate(0),
71 : 0 : /*discard_feerate=*/ CFeeRate(0),
72 : : /*tx_noinputs_size=*/ 0,
73 : 0 : /*avoid_partial=*/ avoid_partial_spends,
74 : 0 : };
75 : : }
76 : :
77 : 0 : class GroupVerifier
78 : : {
79 : : public:
80 : 0 : std::shared_ptr<CWallet> wallet{nullptr};
81 : : CoinsResult coins_pool;
82 : : FastRandomContext rand;
83 : :
84 : 0 : void GroupVerify(const OutputType type,
85 : : const CoinEligibilityFilter& filter,
86 : : bool avoid_partial_spends,
87 : : bool positive_only,
88 : : int expected_size)
89 : : {
90 : 0 : OutputGroupTypeMap groups = GroupOutputs(*wallet, coins_pool, makeSelectionParams(rand, avoid_partial_spends), {{filter}})[filter];
91 : 0 : std::vector<OutputGroup>& groups_out = positive_only ? groups.groups_by_type[type].positive_group :
92 : 0 : groups.groups_by_type[type].mixed_group;
93 : 0 : BOOST_CHECK_EQUAL(groups_out.size(), expected_size);
94 : 0 : }
95 : :
96 : 0 : void GroupAndVerify(const OutputType type,
97 : : const CoinEligibilityFilter& filter,
98 : : int expected_with_partial_spends_size,
99 : 0 : int expected_without_partial_spends_size,
100 : : bool positive_only)
101 : : {
102 : : // First avoid partial spends
103 : 0 : GroupVerify(type, filter, /*avoid_partial_spends=*/false, positive_only, expected_with_partial_spends_size);
104 : : // Second don't avoid partial spends
105 : 0 : GroupVerify(type, filter, /*avoid_partial_spends=*/true, positive_only, expected_without_partial_spends_size);
106 : 0 : }
107 : : };
108 : :
109 : 0 : BOOST_AUTO_TEST_CASE(outputs_grouping_tests)
110 : : {
111 : 0 : const auto& wallet = NewWallet(m_node);
112 : 0 : GroupVerifier group_verifier;
113 : 0 : group_verifier.wallet = wallet;
114 : :
115 : 0 : 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 : 0 : unsigned long GROUP_SIZE = 10;
124 : 0 : const CTxDestination dest = *Assert(wallet->GetNewDestination(OutputType::BECH32, ""));
125 : 0 : for (unsigned long i = 0; i < GROUP_SIZE; i++) {
126 : 0 : addCoin(group_verifier.coins_pool, *wallet, dest, 10 * COIN, /*is_from_me=*/true);
127 : 0 : }
128 : :
129 : 0 : group_verifier.GroupAndVerify(OutputType::BECH32,
130 : 0 : BASIC_FILTER,
131 : 0 : /*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 : 0 : const CTxDestination dest2 = *Assert(wallet->GetNewDestination(OutputType::BECH32, ""));
141 : 0 : for (unsigned long i = 0; i < GROUP_SIZE; i++) {
142 : 0 : addCoin(group_verifier.coins_pool, *wallet, dest2, 5 * COIN, /*is_from_me=*/true);
143 : 0 : }
144 : :
145 : 0 : group_verifier.GroupAndVerify(OutputType::BECH32,
146 : 0 : BASIC_FILTER,
147 : 0 : /*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 : 0 : const CTxDestination dest3 = *Assert(wallet->GetNewDestination(OutputType::BECH32, ""));
156 : 0 : addCoin(group_verifier.coins_pool, *wallet, dest3, 1, true, CFeeRate(100));
157 : 0 : BOOST_CHECK(group_verifier.coins_pool.coins[OutputType::BECH32].back().GetEffectiveValue() <= 0);
158 : :
159 : : // First expect no changes with "positive_only" enabled
160 : 0 : group_verifier.GroupAndVerify(OutputType::BECH32,
161 : 0 : BASIC_FILTER,
162 : 0 : /*expected_with_partial_spends_size=*/ GROUP_SIZE * 2,
163 : 0 : /*expected_without_partial_spends_size=*/ 2,
164 : 0 : /*positive_only=*/ true);
165 : 0 :
166 : 0 : // Then expect changes with "positive_only" disabled
167 : 0 : group_verifier.GroupAndVerify(OutputType::BECH32,
168 : 0 : BASIC_FILTER,
169 : 0 : /*expected_with_partial_spends_size=*/ GROUP_SIZE * 2 + 1,
170 : 0 : /*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 : 0 : const CTxDestination dest4 = *Assert(wallet->GetNewDestination(OutputType::BECH32, ""));
180 : 0 : addCoin(group_verifier.coins_pool, *wallet, dest4, 6 * COIN,
181 : 0 : /*is_from_me=*/false, CFeeRate(0), /*depth=*/5);
182 : :
183 : : // Expect no changes from this round and the previous one (point 4)
184 : 0 : group_verifier.GroupAndVerify(OutputType::BECH32,
185 : 0 : BASIC_FILTER,
186 : 0 : /*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 : 0 : const CTxDestination dest5 = *Assert(wallet->GetNewDestination(OutputType::BECH32, ""));
197 : 0 : addCoin(group_verifier.coins_pool, *wallet, dest5, 6 * COIN,
198 : 0 : /*is_from_me=*/true, CFeeRate(0), /*depth=*/0);
199 : :
200 : : // Expect no changes from this round and the previous one (point 5)
201 : 0 : group_verifier.GroupAndVerify(OutputType::BECH32,
202 : 0 : BASIC_FILTER,
203 : 0 : /*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 : 0 : const CTxDestination dest7 = *Assert(wallet->GetNewDestination(OutputType::BECH32, ""));
212 : 0 : uint16_t NUM_SINGLE_ENTRIES = 101;
213 : 0 : for (unsigned long i = 0; i < NUM_SINGLE_ENTRIES; i++) { // OUTPUT_GROUP_MAX_ENTRIES{100}
214 : 0 : addCoin(group_verifier.coins_pool, *wallet, dest7, 9 * COIN, /*is_from_me=*/true);
215 : 0 : }
216 : :
217 : : // Exclude partial groups only adds one more group to the previous test case (point 6)
218 : 0 : int PREVIOUS_ROUND_COUNT = GROUP_SIZE * 2 + 1;
219 : 0 : group_verifier.GroupAndVerify(OutputType::BECH32,
220 : 0 : BASIC_FILTER,
221 : 0 : /*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 : 0 : const CoinEligibilityFilter& avoid_partial_groups_filter{1, 6, 0, 0, /*include_partial=*/ true};
227 : 0 : group_verifier.GroupAndVerify(OutputType::BECH32,
228 : 0 : avoid_partial_groups_filter,
229 : 0 : /*expected_with_partial_spends_size=*/ PREVIOUS_ROUND_COUNT + NUM_SINGLE_ENTRIES,
230 : : /*expected_without_partial_spends_size=*/ 5,
231 : : /*positive_only=*/ false);
232 : 0 : }
233 : :
234 : 0 : BOOST_AUTO_TEST_SUITE_END()
235 : : } // end namespace wallet
|