Line data Source code
1 : // Copyright (c) 2020-2025 The Dash Core developers
2 : // Distributed under the MIT software license, see the accompanying
3 : // file COPYING or http://www.opensource.org/licenses/mit-license.php.
4 :
5 : #include <test/util/setup_common.h>
6 :
7 : #include <coinjoin/client.h>
8 : #include <coinjoin/coinjoin.h>
9 : #include <coinjoin/walletman.h>
10 : #include <coinjoin/options.h>
11 : #include <coinjoin/util.h>
12 : #include <consensus/amount.h>
13 : #include <node/context.h>
14 : #include <util/translation.h>
15 : #include <policy/settings.h>
16 : #include <validation.h>
17 : #include <wallet/context.h>
18 : #include <wallet/spend.h>
19 : #include <wallet/wallet.h>
20 :
21 : #include <boost/test/unit_test.hpp>
22 :
23 : namespace wallet {
24 146 : BOOST_FIXTURE_TEST_SUITE(coinjoin_tests, BasicTestingSetup)
25 :
26 149 : BOOST_AUTO_TEST_CASE(coinjoin_options_tests)
27 : {
28 1 : BOOST_CHECK_EQUAL(CCoinJoinClientOptions::GetSessions(), DEFAULT_COINJOIN_SESSIONS);
29 1 : BOOST_CHECK_EQUAL(CCoinJoinClientOptions::GetRounds(), DEFAULT_COINJOIN_ROUNDS);
30 1 : BOOST_CHECK_EQUAL(CCoinJoinClientOptions::GetRandomRounds(), COINJOIN_RANDOM_ROUNDS);
31 1 : BOOST_CHECK_EQUAL(CCoinJoinClientOptions::GetAmount(), DEFAULT_COINJOIN_AMOUNT);
32 1 : BOOST_CHECK_EQUAL(CCoinJoinClientOptions::GetDenomsGoal(), DEFAULT_COINJOIN_DENOMS_GOAL);
33 1 : BOOST_CHECK_EQUAL(CCoinJoinClientOptions::GetDenomsHardCap(), DEFAULT_COINJOIN_DENOMS_HARDCAP);
34 :
35 1 : BOOST_CHECK_EQUAL(CCoinJoinClientOptions::IsEnabled(), false);
36 1 : BOOST_CHECK_EQUAL(CCoinJoinClientOptions::IsMultiSessionEnabled(), DEFAULT_COINJOIN_MULTISESSION);
37 :
38 1 : CCoinJoinClientOptions::SetEnabled(true);
39 1 : BOOST_CHECK_EQUAL(CCoinJoinClientOptions::IsEnabled(), true);
40 1 : CCoinJoinClientOptions::SetEnabled(false);
41 1 : BOOST_CHECK_EQUAL(CCoinJoinClientOptions::IsEnabled(), false);
42 :
43 1 : CCoinJoinClientOptions::SetMultiSessionEnabled(!DEFAULT_COINJOIN_MULTISESSION);
44 1 : BOOST_CHECK_EQUAL(CCoinJoinClientOptions::IsMultiSessionEnabled(), !DEFAULT_COINJOIN_MULTISESSION);
45 1 : CCoinJoinClientOptions::SetMultiSessionEnabled(DEFAULT_COINJOIN_MULTISESSION);
46 1 : BOOST_CHECK_EQUAL(CCoinJoinClientOptions::IsMultiSessionEnabled(), DEFAULT_COINJOIN_MULTISESSION);
47 :
48 1 : CCoinJoinClientOptions::SetRounds(DEFAULT_COINJOIN_ROUNDS + 10);
49 1 : BOOST_CHECK_EQUAL(CCoinJoinClientOptions::GetRounds(), DEFAULT_COINJOIN_ROUNDS + 10);
50 1 : CCoinJoinClientOptions::SetAmount(DEFAULT_COINJOIN_AMOUNT + 50);
51 1 : BOOST_CHECK_EQUAL(CCoinJoinClientOptions::GetAmount(), DEFAULT_COINJOIN_AMOUNT + 50);
52 1 : }
53 :
54 149 : BOOST_AUTO_TEST_CASE(coinjoin_collateral_tests)
55 : {
56 : // Good collateral values
57 : static_assert(CoinJoin::IsCollateralAmount(0.00010000 * COIN));
58 : static_assert(CoinJoin::IsCollateralAmount(0.00012345 * COIN));
59 : static_assert(CoinJoin::IsCollateralAmount(0.00032123 * COIN));
60 : static_assert(CoinJoin::IsCollateralAmount(0.00019000 * COIN));
61 :
62 : // Bad collateral values
63 : static_assert(!CoinJoin::IsCollateralAmount(0.00009999 * COIN));
64 : static_assert(!CoinJoin::IsCollateralAmount(0.00040001 * COIN));
65 : static_assert(!CoinJoin::IsCollateralAmount(0.00100000 * COIN));
66 : static_assert(!CoinJoin::IsCollateralAmount(0.00100001 * COIN));
67 1 : }
68 :
69 149 : BOOST_AUTO_TEST_CASE(coinjoin_pending_dsa_request_tests)
70 : {
71 1 : CPendingDsaRequest dsa_request;
72 1 : BOOST_CHECK(dsa_request.GetProTxHash() == uint256());
73 1 : BOOST_CHECK(dsa_request.GetDSA() == CCoinJoinAccept());
74 1 : BOOST_CHECK_EQUAL(dsa_request.IsExpired(), true);
75 1 : CPendingDsaRequest dsa_request_2;
76 1 : BOOST_CHECK(dsa_request == dsa_request_2);
77 1 : CCoinJoinAccept cja;
78 1 : cja.nDenom = 4;
79 1 : uint256 proTxHash{uint256::ONE};
80 1 : CPendingDsaRequest custom_request(proTxHash, cja);
81 1 : BOOST_CHECK(custom_request.GetProTxHash() == proTxHash);
82 1 : BOOST_CHECK(custom_request.GetDSA() == cja);
83 1 : BOOST_CHECK_EQUAL(custom_request.IsExpired(), false);
84 1 : SetMockTime(GetTime() + 15);
85 1 : BOOST_CHECK_EQUAL(custom_request.IsExpired(), false);
86 1 : SetMockTime(GetTime() + 1);
87 1 : BOOST_CHECK_EQUAL(custom_request.IsExpired(), true);
88 :
89 1 : BOOST_CHECK(dsa_request != custom_request);
90 1 : BOOST_CHECK(!(dsa_request == custom_request));
91 1 : BOOST_CHECK(!dsa_request);
92 1 : BOOST_CHECK(custom_request);
93 1 : }
94 :
95 149 : BOOST_AUTO_TEST_CASE(coinjoin_dstxin_tests)
96 : {
97 1 : CTxDSIn txin;
98 1 : BOOST_CHECK(txin.prevPubKey == CScript());
99 1 : BOOST_CHECK_EQUAL(txin.fHasSig, false);
100 1 : BOOST_CHECK_EQUAL(txin.nRounds, -10);
101 1 : CTxDSIn custom_txin(txin, CScript(4), -9);
102 1 : BOOST_CHECK(custom_txin.prevPubKey == CScript(4));
103 1 : BOOST_CHECK_EQUAL(custom_txin.fHasSig, false);
104 1 : BOOST_CHECK_EQUAL(custom_txin.nRounds, -9);
105 1 : }
106 :
107 149 : BOOST_AUTO_TEST_CASE(coinjoin_status_update_tests)
108 : {
109 1 : CCoinJoinStatusUpdate cjsu;
110 1 : BOOST_CHECK_EQUAL(cjsu.nSessionID, 0);
111 1 : BOOST_CHECK_EQUAL(cjsu.nState, POOL_STATE_IDLE);
112 1 : BOOST_CHECK_EQUAL(cjsu.nEntriesCount, 0);
113 1 : BOOST_CHECK_EQUAL(cjsu.nStatusUpdate, STATUS_ACCEPTED);
114 1 : BOOST_CHECK_EQUAL(cjsu.nMessageID, MSG_NOERR);
115 1 : CCoinJoinStatusUpdate custom_cjsu(1, POOL_STATE_QUEUE, 1, STATUS_REJECTED, ERR_QUEUE_FULL);
116 1 : BOOST_CHECK_EQUAL(custom_cjsu.nSessionID, 1);
117 1 : BOOST_CHECK_EQUAL(custom_cjsu.nState, POOL_STATE_QUEUE);
118 1 : BOOST_CHECK_EQUAL(custom_cjsu.nEntriesCount, 1);
119 1 : BOOST_CHECK_EQUAL(custom_cjsu.nStatusUpdate, STATUS_REJECTED);
120 1 : BOOST_CHECK_EQUAL(custom_cjsu.nMessageID, ERR_QUEUE_FULL);
121 1 : }
122 :
123 149 : BOOST_AUTO_TEST_CASE(coinjoin_accept_tests)
124 : {
125 1 : CCoinJoinAccept cja;
126 1 : BOOST_CHECK_EQUAL(cja.nDenom, 0);
127 1 : BOOST_CHECK_EQUAL(cja.txCollateral.GetHash(), CMutableTransaction().GetHash());
128 : // CMutableTransaction custom_cmt()
129 1 : }
130 :
131 : class CTransactionBuilderTestSetup : public TestChain100Setup
132 : {
133 : public:
134 2 : CTransactionBuilderTestSetup() :
135 2 : wallet{std::make_unique<CWallet>(m_node.chain.get(), m_node.coinjoin_loader.get(), "", m_args, CreateMockWalletDatabase())}
136 : {
137 2 : context.args = &m_args;
138 2 : context.chain = m_node.chain.get();
139 2 : context.coinjoin_loader = m_node.coinjoin_loader.get();
140 2 : CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey()));
141 2 : wallet->SetupLegacyScriptPubKeyMan();
142 2 : wallet->LoadWallet();
143 2 : AddWallet(context, wallet);
144 : {
145 2 : LOCK2(wallet->cs_wallet, ::cs_main);
146 2 : wallet->GetLegacyScriptPubKeyMan()->AddKeyPubKey(coinbaseKey, coinbaseKey.GetPubKey());
147 2 : wallet->SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash());
148 2 : }
149 2 : WalletRescanReserver reserver(*wallet);
150 2 : reserver.reserve();
151 2 : CWallet::ScanResult result = wallet->ScanForWalletTransactions(/*start_block=*/wallet->chain().getBlockHash(0),
152 2 : /*start_height=*/0, /*max_height=*/{}, reserver,
153 : /*fUpdate=*/true, /*save_progress=*/false);
154 2 : BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::SUCCESS);
155 2 : }
156 :
157 2 : ~CTransactionBuilderTestSetup()
158 : {
159 2 : RemoveWallet(context, wallet, /*load_on_start=*/std::nullopt);
160 2 : }
161 :
162 : WalletContext context;
163 : const std::shared_ptr<CWallet> wallet;
164 :
165 8 : CWalletTx& AddTxToChain(uint256 nTxHash)
166 : {
167 8 : decltype(wallet->mapWallet)::iterator it;
168 8 : CMutableTransaction blocktx;
169 : {
170 8 : LOCK(wallet->cs_wallet);
171 8 : it = wallet->mapWallet.find(nTxHash);
172 8 : BOOST_REQUIRE(it != wallet->mapWallet.end());
173 8 : blocktx = CMutableTransaction(*it->second.tx);
174 8 : }
175 8 : CreateAndProcessBlock({blocktx}, GetScriptForRawPubKey(coinbaseKey.GetPubKey()));
176 8 : LOCK2(wallet->cs_wallet, ::cs_main);
177 8 : wallet->SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash());
178 8 : it->second.m_state = TxStateConfirmed{m_node.chainman->ActiveChain().Tip()->GetBlockHash(), m_node.chainman->ActiveChain().Height(), /*index=*/1};
179 8 : return it->second;
180 8 : }
181 2 : CompactTallyItem GetTallyItem(const std::vector<CAmount>& vecAmounts)
182 : {
183 2 : CompactTallyItem tallyItem;
184 2 : ReserveDestination reserveDest(wallet.get());
185 2 : int nChangePosRet{RANDOM_CHANGE_POSITION};
186 2 : CCoinControl coinControl;
187 2 : coinControl.m_feerate = CFeeRate(1000);
188 : {
189 2 : LOCK(wallet->cs_wallet);
190 2 : auto dest_opt = reserveDest.GetReservedDestination(false);
191 2 : BOOST_REQUIRE(dest_opt);
192 2 : tallyItem.txdest = *dest_opt;
193 2 : }
194 8 : for (CAmount nAmount : vecAmounts) {
195 6 : CTransactionRef tx;
196 : {
197 6 : auto res = CreateTransaction(*wallet, {{GetScriptForDestination(tallyItem.txdest), nAmount, false}}, nChangePosRet, coinControl);
198 6 : BOOST_REQUIRE(res);
199 6 : tx = res->tx;
200 6 : nChangePosRet = res->change_pos;
201 6 : }
202 : {
203 6 : LOCK2(wallet->cs_wallet, ::cs_main);
204 6 : wallet->CommitTransaction(tx, {}, {});
205 6 : }
206 6 : AddTxToChain(tx->GetHash());
207 18 : for (uint32_t n = 0; n < tx->vout.size(); ++n) {
208 12 : if (nChangePosRet != RANDOM_CHANGE_POSITION && int(n) == nChangePosRet) {
209 : // Skip the change output to only return the requested coins
210 6 : continue;
211 : }
212 6 : tallyItem.outpoints.emplace_back(COutPoint{tx->GetHash(), n});
213 6 : tallyItem.nAmount += tx->vout[n].nValue;
214 6 : }
215 6 : }
216 2 : BOOST_REQUIRE_EQUAL(tallyItem.outpoints.size(), vecAmounts.size());
217 2 : reserveDest.KeepDestination();
218 2 : return tallyItem;
219 2 : }
220 : };
221 :
222 148 : BOOST_FIXTURE_TEST_CASE(coinjoin_manager_start_stop_tests, CTransactionBuilderTestSetup)
223 : {
224 1 : auto& cj_man = *Assert(m_node.cj_walletman->getClient(""));
225 1 : BOOST_CHECK_EQUAL(cj_man.IsMixing(), false);
226 1 : BOOST_CHECK_EQUAL(cj_man.StartMixing(), true);
227 1 : BOOST_CHECK_EQUAL(cj_man.IsMixing(), true);
228 1 : BOOST_CHECK_EQUAL(cj_man.StartMixing(), false);
229 1 : cj_man.StopMixing();
230 1 : BOOST_CHECK_EQUAL(cj_man.IsMixing(), false);
231 1 : }
232 :
233 148 : BOOST_FIXTURE_TEST_CASE(CTransactionBuilderTest, CTransactionBuilderTestSetup)
234 : {
235 : // NOTE: Mock wallet version is FEATURE_BASE which means that it uses uncompressed pubkeys
236 : // (65 bytes instead of 33 bytes) and we use Low R signatures, so CTxIn size is 179 bytes.
237 : // Each output is 34 bytes, vin and vout compact sizes are 1 byte each.
238 : // Therefore base size (i.e. for a tx with 1 input, 0 outputs) is expected to be
239 : // 4(n32bitVersion) + 1(vin size) + 179(vin[0]) + 1(vout size) + 4(nLockTime) = 189 bytes.
240 :
241 1 : minRelayTxFee = CFeeRate(DEFAULT_MIN_RELAY_TX_FEE);
242 : // Tests with single outpoint tallyItem
243 : {
244 1 : CompactTallyItem tallyItem = GetTallyItem({4999});
245 1 : CTransactionBuilder txBuilder(*wallet, tallyItem);
246 :
247 1 : BOOST_CHECK_EQUAL(txBuilder.CountOutputs(), 0);
248 1 : BOOST_CHECK_EQUAL(txBuilder.GetAmountInitial(), tallyItem.nAmount);
249 1 : BOOST_CHECK_EQUAL(txBuilder.GetAmountLeft(), 4810); // 4999 - 189
250 :
251 1 : BOOST_CHECK(txBuilder.CouldAddOutput(4776)); // 4810 - 34
252 1 : BOOST_CHECK(!txBuilder.CouldAddOutput(4777));
253 :
254 1 : BOOST_CHECK(txBuilder.CouldAddOutput(0));
255 1 : BOOST_CHECK(!txBuilder.CouldAddOutput(-1));
256 :
257 1 : BOOST_CHECK(txBuilder.CouldAddOutputs({1000, 1000, 2708})); // (4810 - 34 * 3) split in 3 outputs
258 1 : BOOST_CHECK(!txBuilder.CouldAddOutputs({1000, 1000, 2709}));
259 :
260 1 : BOOST_CHECK_EQUAL(txBuilder.AddOutput(4999), nullptr);
261 1 : BOOST_CHECK_EQUAL(txBuilder.AddOutput(-1), nullptr);
262 :
263 1 : CTransactionBuilderOutput* output = txBuilder.AddOutput();
264 1 : BOOST_CHECK(output->UpdateAmount(txBuilder.GetAmountLeft()));
265 1 : BOOST_CHECK(output->UpdateAmount(1));
266 1 : BOOST_CHECK(output->UpdateAmount(output->GetAmount() + txBuilder.GetAmountLeft()));
267 1 : BOOST_CHECK(!output->UpdateAmount(output->GetAmount() + 1));
268 1 : BOOST_CHECK(!output->UpdateAmount(0));
269 1 : BOOST_CHECK(!output->UpdateAmount(-1));
270 1 : BOOST_CHECK_EQUAL(txBuilder.CountOutputs(), 1);
271 :
272 1 : bilingual_str strResult;
273 1 : BOOST_REQUIRE(txBuilder.Commit(strResult));
274 1 : CWalletTx& wtx = AddTxToChain(uint256S(strResult.original));
275 1 : BOOST_CHECK_EQUAL(wtx.tx->vout.size(), txBuilder.CountOutputs()); // should have no change output
276 1 : BOOST_CHECK_EQUAL(wtx.tx->vout[0].nValue, output->GetAmount());
277 1 : BOOST_CHECK(wtx.tx->vout[0].scriptPubKey == output->GetScript());
278 1 : }
279 : // Tests with multiple outpoint tallyItem
280 : {
281 1 : CompactTallyItem tallyItem = GetTallyItem({10000, 20000, 30000, 40000, 50000});
282 1 : CTransactionBuilder txBuilder(*wallet, tallyItem);
283 1 : std::vector<CTransactionBuilderOutput*> vecOutputs;
284 1 : bilingual_str strResult;
285 :
286 1 : auto output = txBuilder.AddOutput(100);
287 1 : BOOST_CHECK(output != nullptr);
288 1 : BOOST_CHECK(!txBuilder.Commit(strResult));
289 :
290 1 : if (output != nullptr) {
291 1 : output->UpdateAmount(1000);
292 1 : vecOutputs.push_back(output);
293 1 : }
294 100 : while (vecOutputs.size() < 100) {
295 99 : output = txBuilder.AddOutput(1000 + vecOutputs.size());
296 99 : if (output == nullptr) {
297 0 : break;
298 : }
299 99 : vecOutputs.push_back(output);
300 : }
301 1 : BOOST_CHECK_EQUAL(vecOutputs.size(), 100);
302 1 : BOOST_CHECK_EQUAL(txBuilder.CountOutputs(), vecOutputs.size());
303 1 : BOOST_REQUIRE(txBuilder.Commit(strResult));
304 1 : CWalletTx& wtx = AddTxToChain(uint256S(strResult.original));
305 1 : BOOST_CHECK_EQUAL(wtx.tx->vout.size(), txBuilder.CountOutputs() + 1); // should have change output
306 102 : for (const auto& out : wtx.tx->vout) {
307 201 : auto it = std::find_if(vecOutputs.begin(), vecOutputs.end(), [&](CTransactionBuilderOutput* output) -> bool {
308 200 : return output->GetAmount() == out.nValue && output->GetScript() == out.scriptPubKey;
309 0 : });
310 101 : if (it != vecOutputs.end()) {
311 100 : vecOutputs.erase(it);
312 100 : } else {
313 : // change output
314 1 : BOOST_CHECK_EQUAL(txBuilder.GetAmountLeft() - 34, out.nValue);
315 : }
316 : }
317 1 : BOOST_CHECK(vecOutputs.size() == 0);
318 1 : }
319 1 : }
320 :
321 146 : BOOST_AUTO_TEST_SUITE_END()
322 : } // namespace wallet
|