Skip to content

Commit d8e749b

Browse files
committed
test: wallet, add coverage for outputs grouping process
The following scenarios are covered: 1) 10 UTXO with the same script: partial spends is enabled --> outputs must not be grouped. 2) 10 UTXO with the same script: partial spends disabled --> outputs must be grouped. 3) 20 UTXO, 10 one from scriptA + 10 from scriptB: a) if partial spends is enabled --> outputs must not be grouped. b) if partial spends is not enabled --> 2 output groups expected (one per script). 3) Try to add a negative output (value - fee < 0): a) if "positive_only" is enabled --> negative output must be skipped. b) if "positive_only" is disabled --> negative output must be added. 4) Try to add a non-eligible UTXO (due not fulfilling the min depth target for "not mine" UTXOs) --> it must not be added to any group 5) Try to add a non-eligible UTXO (due not fulfilling the min depth target for "mine" UTXOs) --> it must not be added to any group 6) Surpass the 'OUTPUT_GROUP_MAX_ENTRIES' size and verify that a second partial group gets created.
1 parent 06ec8f9 commit d8e749b

File tree

2 files changed

+228
-1
lines changed

2 files changed

+228
-1
lines changed

src/Makefile.test.include

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,8 @@ BITCOIN_TESTS += \
179179
wallet/test/ismine_tests.cpp \
180180
wallet/test/rpc_util_tests.cpp \
181181
wallet/test/scriptpubkeyman_tests.cpp \
182-
wallet/test/walletload_tests.cpp
182+
wallet/test/walletload_tests.cpp \
183+
wallet/test/group_outputs_tests.cpp
183184

184185
FUZZ_SUITE_LD_COMMON +=\
185186
$(SQLITE_LIBS) \
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
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/wallet.h>
10+
11+
#include <boost/test/unit_test.hpp>
12+
13+
namespace wallet {
14+
BOOST_FIXTURE_TEST_SUITE(group_outputs_tests, TestingSetup)
15+
16+
static int nextLockTime = 0;
17+
18+
static std::shared_ptr<CWallet> NewWallet(const node::NodeContext& m_node)
19+
{
20+
std::unique_ptr<CWallet> wallet = std::make_unique<CWallet>(m_node.chain.get(), "", CreateMockWalletDatabase());
21+
wallet->LoadWallet();
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 uint256& 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+
/*spendable=*/ true,
54+
/*solvable=*/ true,
55+
/*safe=*/ true,
56+
wtx.GetTxTime(),
57+
is_from_me,
58+
fee_rate});
59+
}
60+
61+
CoinSelectionParams makeSelectionParams(FastRandomContext& rand, bool avoid_partial_spends)
62+
{
63+
return CoinSelectionParams{
64+
rand,
65+
/*change_output_size=*/ 0,
66+
/*change_spend_size=*/ 0,
67+
/*min_change_target=*/ CENT,
68+
/*effective_feerate=*/ CFeeRate(0),
69+
/*long_term_feerate=*/ CFeeRate(0),
70+
/*discard_feerate=*/ CFeeRate(0),
71+
/*tx_noinputs_size=*/ 0,
72+
/*avoid_partial=*/ avoid_partial_spends,
73+
};
74+
}
75+
76+
class GroupVerifier
77+
{
78+
public:
79+
std::shared_ptr<CWallet> wallet{nullptr};
80+
CoinsResult coins_pool;
81+
FastRandomContext rand;
82+
83+
void GroupVerify(const CoinEligibilityFilter& filter,
84+
bool avoid_partial_spends,
85+
bool positive_only,
86+
int expected_size)
87+
{
88+
std::vector<OutputGroup> groups = GroupOutputs(*wallet,
89+
coins_pool.All(),
90+
makeSelectionParams(rand, avoid_partial_spends),
91+
filter,
92+
positive_only);
93+
BOOST_CHECK_EQUAL(groups.size(), expected_size);
94+
}
95+
96+
void GroupAndVerify(const CoinEligibilityFilter& filter,
97+
int expected_with_partial_spends_size,
98+
int expected_without_partial_spends_size,
99+
bool positive_only)
100+
{
101+
// First avoid partial spends
102+
GroupVerify(filter, /*avoid_partial_spends=*/false, positive_only, expected_with_partial_spends_size);
103+
// Second don't avoid partial spends
104+
GroupVerify(filter, /*avoid_partial_spends=*/true, positive_only, expected_without_partial_spends_size);
105+
}
106+
};
107+
108+
BOOST_AUTO_TEST_CASE(outputs_grouping_tests)
109+
{
110+
const auto& wallet = NewWallet(m_node);
111+
GroupVerifier group_verifier;
112+
group_verifier.wallet = wallet;
113+
114+
const CoinEligibilityFilter& BASIC_FILTER{1, 6, 0};
115+
116+
// #################################################################################
117+
// 10 outputs from different txs going to the same script
118+
// 1) if partial spends is enabled --> must not be grouped
119+
// 2) if partial spends is not enabled --> must be grouped into a single OutputGroup
120+
// #################################################################################
121+
122+
unsigned long GROUP_SIZE = 10;
123+
const CTxDestination dest = *Assert(wallet->GetNewDestination(OutputType::BECH32, ""));
124+
for (unsigned long i = 0; i < GROUP_SIZE; i++) {
125+
addCoin(group_verifier.coins_pool, *wallet, dest, 10 * COIN, /*is_from_me=*/true);
126+
}
127+
128+
group_verifier.GroupAndVerify(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(BASIC_FILTER,
144+
/*expected_with_partial_spends_size=*/ GROUP_SIZE * 2,
145+
/*expected_without_partial_spends_size=*/ 2,
146+
/*positive_only=*/ true);
147+
148+
// ################################################################################
149+
// 4) Now add a negative output --> which will be skipped if "positive_only" is set
150+
// ################################################################################
151+
152+
const CTxDestination dest3 = *Assert(wallet->GetNewDestination(OutputType::BECH32, ""));
153+
addCoin(group_verifier.coins_pool, *wallet, dest3, 1, true, CFeeRate(100));
154+
BOOST_CHECK(group_verifier.coins_pool.coins[OutputType::BECH32].back().GetEffectiveValue() <= 0);
155+
156+
// First expect no changes with "positive_only" enabled
157+
group_verifier.GroupAndVerify(BASIC_FILTER,
158+
/*expected_with_partial_spends_size=*/ GROUP_SIZE * 2,
159+
/*expected_without_partial_spends_size=*/ 2,
160+
/*positive_only=*/ true);
161+
162+
// Then expect changes with "positive_only" disabled
163+
group_verifier.GroupAndVerify(BASIC_FILTER,
164+
/*expected_with_partial_spends_size=*/ GROUP_SIZE * 2 + 1,
165+
/*expected_without_partial_spends_size=*/ 3,
166+
/*positive_only=*/ false);
167+
168+
169+
// ##############################################################################
170+
// 5) Try to add a non-eligible UTXO (due not fulfilling the min depth target for
171+
// "not mine" UTXOs) --> it must not be added to any group
172+
// ##############################################################################
173+
174+
const CTxDestination dest4 = *Assert(wallet->GetNewDestination(OutputType::BECH32, ""));
175+
addCoin(group_verifier.coins_pool, *wallet, dest4, 6 * COIN,
176+
/*is_from_me=*/false, CFeeRate(0), /*depth=*/5);
177+
178+
// Expect no changes from this round and the previous one (point 4)
179+
group_verifier.GroupAndVerify(BASIC_FILTER,
180+
/*expected_with_partial_spends_size=*/ GROUP_SIZE * 2 + 1,
181+
/*expected_without_partial_spends_size=*/ 3,
182+
/*positive_only=*/ false);
183+
184+
185+
// ##############################################################################
186+
// 6) Try to add a non-eligible UTXO (due not fulfilling the min depth target for
187+
// "mine" UTXOs) --> it must not be added to any group
188+
// ##############################################################################
189+
190+
const CTxDestination dest5 = *Assert(wallet->GetNewDestination(OutputType::BECH32, ""));
191+
addCoin(group_verifier.coins_pool, *wallet, dest5, 6 * COIN,
192+
/*is_from_me=*/true, CFeeRate(0), /*depth=*/0);
193+
194+
// Expect no changes from this round and the previous one (point 5)
195+
group_verifier.GroupAndVerify(BASIC_FILTER,
196+
/*expected_with_partial_spends_size=*/ GROUP_SIZE * 2 + 1,
197+
/*expected_without_partial_spends_size=*/ 3,
198+
/*positive_only=*/ false);
199+
200+
// ###########################################################################################
201+
// 7) Surpass the OUTPUT_GROUP_MAX_ENTRIES and verify that a second partial group gets created
202+
// ###########################################################################################
203+
204+
const CTxDestination dest7 = *Assert(wallet->GetNewDestination(OutputType::BECH32, ""));
205+
uint16_t NUM_SINGLE_ENTRIES = 101;
206+
for (unsigned long i = 0; i < NUM_SINGLE_ENTRIES; i++) { // OUTPUT_GROUP_MAX_ENTRIES{100}
207+
addCoin(group_verifier.coins_pool, *wallet, dest7, 9 * COIN, /*is_from_me=*/true);
208+
}
209+
210+
// Exclude partial groups only adds one more group to the previous test case (point 6)
211+
int PREVIOUS_ROUND_COUNT = GROUP_SIZE * 2 + 1;
212+
group_verifier.GroupAndVerify(BASIC_FILTER,
213+
/*expected_with_partial_spends_size=*/ PREVIOUS_ROUND_COUNT + NUM_SINGLE_ENTRIES,
214+
/*expected_without_partial_spends_size=*/ 4,
215+
/*positive_only=*/ false);
216+
217+
// Include partial groups should add one more group inside the "avoid partial spends" count
218+
const CoinEligibilityFilter& avoid_partial_groups_filter{1, 6, 0, 0, /*include_partial=*/ true};
219+
group_verifier.GroupAndVerify(avoid_partial_groups_filter,
220+
/*expected_with_partial_spends_size=*/ PREVIOUS_ROUND_COUNT + NUM_SINGLE_ENTRIES,
221+
/*expected_without_partial_spends_size=*/ 5,
222+
/*positive_only=*/ false);
223+
}
224+
225+
BOOST_AUTO_TEST_SUITE_END()
226+
} // end namespace wallet

0 commit comments

Comments
 (0)