Line data Source code
1 : // Copyright (c) 2014-2025 The Dash Core developers
2 : // Distributed under the MIT/X11 software license, see the accompanying
3 : // file COPYING or http://www.opensource.org/licenses/mit-license.php.
4 :
5 : #include <coinjoin/server.h>
6 :
7 : #include <active/masternode.h>
8 : #include <evo/deterministicmns.h>
9 : #include <masternode/meta.h>
10 : #include <masternode/sync.h>
11 :
12 : #include <core_io.h>
13 : #include <net.h>
14 : #include <net_processing.h>
15 : #include <netmessagemaker.h>
16 : #include <scheduler.h>
17 : #include <script/interpreter.h>
18 : #include <shutdown.h>
19 : #include <streams.h>
20 : #include <txmempool.h>
21 : #include <util/moneystr.h>
22 : #include <util/system.h>
23 : #include <validation.h>
24 :
25 : #include <univalue.h>
26 :
27 : #include <ranges>
28 :
29 1320 : CCoinJoinServer::CCoinJoinServer(PeerManagerInternal* peer_manager, ChainstateManager& chainman, CConnman& _connman,
30 : CDeterministicMNManager& dmnman, CDSTXManager& dstxman, CMasternodeMetaMan& mn_metaman,
31 : CTxMemPool& mempool, const CActiveMasternodeManager& mn_activeman,
32 : const CMasternodeSync& mn_sync, const llmq::CInstantSendManager& isman) :
33 660 : NetHandler(peer_manager),
34 660 : m_chainman{chainman},
35 660 : connman{_connman},
36 660 : m_dmnman{dmnman},
37 660 : m_dstxman{dstxman},
38 660 : m_mn_metaman{mn_metaman},
39 660 : mempool{mempool},
40 660 : m_mn_activeman{mn_activeman},
41 660 : m_mn_sync{mn_sync},
42 660 : m_isman{isman},
43 660 : vecSessionCollaterals{},
44 660 : fUnitTest{false}
45 1320 : {
46 1320 : }
47 :
48 1980 : CCoinJoinServer::~CCoinJoinServer() = default;
49 :
50 94137 : void CCoinJoinServer::ProcessMessage(CNode& peer, const std::string& msg_type, CDataStream& vRecv)
51 : {
52 94137 : if (!m_mn_sync.IsBlockchainSynced()) return;
53 :
54 94137 : if (msg_type == NetMsgType::DSACCEPT) {
55 0 : ProcessDSACCEPT(peer, vRecv);
56 94137 : } else if (msg_type == NetMsgType::DSQUEUE) {
57 0 : ProcessDSQUEUE(peer.GetId(), vRecv);
58 94137 : } else if (msg_type == NetMsgType::DSVIN) {
59 0 : ProcessDSVIN(peer, vRecv);
60 94137 : } else if (msg_type == NetMsgType::DSSIGNFINALTX) {
61 0 : ProcessDSSIGNFINALTX(vRecv);
62 0 : }
63 94137 : }
64 :
65 0 : void CCoinJoinServer::ProcessDSACCEPT(CNode& peer, CDataStream& vRecv)
66 : {
67 0 : assert(m_mn_metaman.IsValid());
68 :
69 0 : if (IsSessionReady()) {
70 : // too many users in this session already, reject new ones
71 0 : LogPrint(BCLog::COINJOIN, "DSACCEPT -- queue is already full!\n");
72 0 : PushStatus(peer, STATUS_REJECTED, ERR_QUEUE_FULL);
73 0 : return;
74 : }
75 :
76 0 : CCoinJoinAccept dsa;
77 0 : vRecv >> dsa;
78 :
79 0 : LogPrint(BCLog::COINJOIN, "DSACCEPT -- nDenom %d (%s) txCollateral %s", dsa.nDenom, CoinJoin::DenominationToString(dsa.nDenom), dsa.txCollateral.ToString()); /* Continued */
80 :
81 0 : auto mnList = m_dmnman.GetListAtChainTip();
82 0 : auto dmn = mnList.GetValidMNByCollateral(m_mn_activeman.GetOutPoint());
83 0 : if (!dmn) {
84 0 : PushStatus(peer, STATUS_REJECTED, ERR_MN_LIST);
85 0 : return;
86 : }
87 :
88 0 : if (vecSessionCollaterals.empty()) {
89 : {
90 0 : const auto hasQueue = m_queueman.TryHasQueueFromMasternode(m_mn_activeman.GetOutPoint());
91 0 : if (!hasQueue.has_value()) return;
92 0 : if (*hasQueue) {
93 : // refuse to create another queue this often
94 0 : LogPrint(BCLog::COINJOIN, "DSACCEPT -- last dsq is still in queue, refuse to mix\n");
95 0 : PushStatus(peer, STATUS_REJECTED, ERR_RECENT);
96 0 : return;
97 : }
98 : }
99 :
100 0 : if (m_mn_metaman.IsMixingThresholdExceeded(dmn->proTxHash, mnList.GetCounts().enabled())) {
101 0 : if (fLogIPs) {
102 0 : LogPrint(BCLog::COINJOIN, "DSACCEPT -- last dsq too recent, must wait: peer=%d, addr=%s\n",
103 : peer.GetId(), peer.addr.ToStringAddrPort());
104 0 : } else {
105 0 : LogPrint(BCLog::COINJOIN, "DSACCEPT -- last dsq too recent, must wait: peer=%d\n", peer.GetId());
106 : }
107 0 : PushStatus(peer, STATUS_REJECTED, ERR_RECENT);
108 0 : return;
109 : }
110 0 : }
111 :
112 0 : PoolMessage nMessageID = MSG_NOERR;
113 :
114 0 : bool fResult = nSessionID == 0 ? CreateNewSession(dsa, nMessageID)
115 0 : : AddUserToExistingSession(dsa, nMessageID);
116 0 : if (fResult) {
117 0 : LogPrint(BCLog::COINJOIN, "DSACCEPT -- is compatible, please submit!\n");
118 0 : PushStatus(peer, STATUS_ACCEPTED, nMessageID);
119 0 : return;
120 : } else {
121 0 : LogPrint(BCLog::COINJOIN, "DSACCEPT -- not compatible with existing transactions!\n");
122 0 : PushStatus(peer, STATUS_REJECTED, nMessageID);
123 0 : return;
124 : }
125 0 : }
126 :
127 0 : void CCoinJoinServer::ProcessDSQUEUE(NodeId from, CDataStream& vRecv)
128 : {
129 0 : assert(m_mn_metaman.IsValid());
130 :
131 0 : CCoinJoinQueue dsq;
132 0 : vRecv >> dsq;
133 :
134 0 : WITH_LOCK(cs_main, m_peer_manager->PeerEraseObjectRequest(from, CInv{MSG_DSQ, dsq.GetHash()}));
135 :
136 : // Validate denomination first
137 0 : if (!CoinJoin::IsValidDenomination(dsq.nDenom)) {
138 0 : LogPrint(BCLog::COINJOIN, "DSQUEUE -- invalid denomination %d from peer %d\n", dsq.nDenom, from);
139 0 : m_peer_manager->PeerMisbehaving(from, 10);
140 0 : return;
141 : }
142 :
143 0 : if (dsq.masternodeOutpoint.IsNull() && dsq.m_protxHash.IsNull()) {
144 0 : m_peer_manager->PeerMisbehaving(from, 100);
145 0 : return;
146 : }
147 :
148 0 : const auto tip_mn_list = m_dmnman.GetListAtChainTip();
149 0 : if (dsq.masternodeOutpoint.IsNull()) {
150 0 : if (auto dmn = tip_mn_list.GetValidMN(dsq.m_protxHash)) {
151 0 : dsq.masternodeOutpoint = dmn->collateralOutpoint;
152 0 : } else {
153 0 : m_peer_manager->PeerMisbehaving(from, 10);
154 0 : return;
155 : }
156 0 : }
157 :
158 : {
159 0 : const auto isDup = m_queueman.TryCheckDuplicate(dsq);
160 0 : if (!isDup.has_value()) return;
161 0 : if (*isDup) {
162 0 : LogPrint(BCLog::COINJOIN, "DSQUEUE -- Peer %d is sending WAY too many dsq messages for a masternode with collateral %s\n", from, dsq.masternodeOutpoint.ToStringShort());
163 0 : return;
164 : }
165 : }
166 :
167 0 : LogPrint(BCLog::COINJOIN, "DSQUEUE -- %s new\n", dsq.ToString());
168 :
169 0 : if (dsq.IsTimeOutOfBounds()) return;
170 :
171 0 : auto dmn = tip_mn_list.GetValidMNByCollateral(dsq.masternodeOutpoint);
172 0 : if (!dmn) return;
173 :
174 0 : if (dsq.m_protxHash.IsNull()) {
175 0 : dsq.m_protxHash = dmn->proTxHash;
176 0 : }
177 :
178 0 : if (!dsq.CheckSignature(dmn->pdmnState->pubKeyOperator.Get())) {
179 0 : m_peer_manager->PeerMisbehaving(from, 10);
180 0 : return;
181 : }
182 :
183 0 : if (!dsq.fReady) {
184 : //don't allow a few nodes to dominate the queuing process
185 0 : if (m_mn_metaman.IsMixingThresholdExceeded(dmn->proTxHash, tip_mn_list.GetCounts().enabled())) {
186 0 : LogPrint(BCLog::COINJOIN, "DSQUEUE -- node sending too many dsq messages, masternode=%s\n", dmn->proTxHash.ToString());
187 0 : return;
188 : }
189 0 : m_mn_metaman.AllowMixing(dmn->proTxHash);
190 :
191 0 : LogPrint(BCLog::COINJOIN, "DSQUEUE -- new CoinJoin queue, masternode=%s, queue=%s\n", dmn->proTxHash.ToString(), dsq.ToString());
192 :
193 0 : if (!m_queueman.TryAddQueue(dsq)) return;
194 0 : m_peer_manager->PeerRelayDSQ(dsq);
195 0 : }
196 0 : }
197 :
198 0 : void CCoinJoinServer::ProcessDSVIN(CNode& peer, CDataStream& vRecv)
199 : {
200 : //do we have enough users in the current session?
201 0 : if (!IsSessionReady()) {
202 0 : LogPrint(BCLog::COINJOIN, "DSVIN -- session not complete!\n");
203 0 : PushStatus(peer, STATUS_REJECTED, ERR_SESSION);
204 0 : return;
205 : }
206 :
207 0 : CCoinJoinEntry entry;
208 0 : vRecv >> entry;
209 :
210 0 : LogPrint(BCLog::COINJOIN, "DSVIN -- txCollateral %s", entry.txCollateral->ToString()); /* Continued */
211 :
212 0 : PoolMessage nMessageID = MSG_NOERR;
213 :
214 0 : entry.addr = peer.addr;
215 0 : if (AddEntry(entry, nMessageID)) {
216 0 : PushStatus(peer, STATUS_ACCEPTED, nMessageID);
217 0 : CheckPool();
218 0 : LOCK(cs_coinjoin);
219 0 : RelayStatus(STATUS_ACCEPTED);
220 0 : } else {
221 0 : PushStatus(peer, STATUS_REJECTED, nMessageID);
222 : }
223 0 : }
224 :
225 0 : void CCoinJoinServer::ProcessDSSIGNFINALTX(CDataStream& vRecv)
226 : {
227 0 : std::vector<CTxIn> vecTxIn;
228 0 : vRecv >> vecTxIn;
229 :
230 0 : LogPrint(BCLog::COINJOIN, "DSSIGNFINALTX -- vecTxIn.size() %s\n", vecTxIn.size());
231 :
232 0 : int nTxInIndex = 0;
233 0 : int nTxInsCount = (int)vecTxIn.size();
234 :
235 0 : for (const auto& txin : vecTxIn) {
236 0 : nTxInIndex++;
237 0 : if (!AddScriptSig(txin)) {
238 0 : LogPrint(BCLog::COINJOIN, "DSSIGNFINALTX -- AddScriptSig() failed at %d/%d, session: %d\n", nTxInIndex, nTxInsCount, nSessionID);
239 0 : LOCK(cs_coinjoin);
240 0 : RelayStatus(STATUS_REJECTED);
241 : return;
242 0 : }
243 0 : LogPrint(BCLog::COINJOIN, "DSSIGNFINALTX -- AddScriptSig() %d/%d success\n", nTxInIndex, nTxInsCount);
244 : }
245 : // all is good
246 0 : CheckPool();
247 0 : }
248 :
249 0 : void CCoinJoinServer::SetNull()
250 : {
251 0 : AssertLockHeld(cs_coinjoin);
252 : // MN side
253 0 : vecSessionCollaterals.clear();
254 :
255 0 : CCoinJoinBaseSession::SetNull();
256 0 : m_queueman.SetNull();
257 0 : }
258 :
259 : //
260 : // Check the mixing progress and send client updates if a Masternode
261 : //
262 55208 : void CCoinJoinServer::CheckPool()
263 : {
264 55208 : if (int entries = GetEntriesCount(); entries != 0)
265 0 : LogPrint(BCLog::COINJOIN, "CCoinJoinServer::CheckPool -- entries count %lu\n", entries);
266 :
267 : // If we have an entry for each collateral, then create final tx
268 55208 : if (nState == POOL_STATE_ACCEPTING_ENTRIES && size_t(GetEntriesCount()) == vecSessionCollaterals.size()) {
269 0 : LogPrint(BCLog::COINJOIN, "CCoinJoinServer::CheckPool -- FINALIZE TRANSACTIONS\n");
270 0 : CreateFinalTransaction();
271 0 : return;
272 : }
273 :
274 : // Check for Time Out
275 : // If we timed out while accepting entries, then if we have more than minimum, create final tx
276 55208 : if (nState == POOL_STATE_ACCEPTING_ENTRIES && CCoinJoinServer::HasTimedOut() &&
277 0 : GetEntriesCount() >= CoinJoin::GetMinPoolParticipants()) {
278 : // Punish misbehaving participants
279 0 : ChargeFees();
280 : // Try to complete this session ignoring the misbehaving ones
281 0 : CreateFinalTransaction();
282 0 : return;
283 : }
284 :
285 : // If we have all the signatures, try to compile the transaction
286 55208 : if (nState == POOL_STATE_SIGNING && IsSignaturesComplete()) {
287 0 : LogPrint(BCLog::COINJOIN, "CCoinJoinServer::CheckPool -- SIGNING\n");
288 0 : CommitFinalTransaction();
289 0 : return;
290 : }
291 55208 : }
292 :
293 0 : void CCoinJoinServer::CreateFinalTransaction()
294 : {
295 0 : AssertLockNotHeld(cs_coinjoin);
296 0 : LogPrint(BCLog::COINJOIN, "CCoinJoinServer::CreateFinalTransaction -- FINALIZE TRANSACTIONS\n");
297 :
298 0 : LOCK(cs_coinjoin);
299 :
300 0 : CMutableTransaction txNew;
301 :
302 : // make our new transaction
303 0 : for (const auto& entry : vecEntries) {
304 0 : for (const auto& txout : entry.vecTxOut) {
305 0 : txNew.vout.push_back(txout);
306 : }
307 0 : for (const auto& txdsin : entry.vecTxDSIn) {
308 0 : txNew.vin.push_back(txdsin);
309 : }
310 : }
311 :
312 0 : sort(txNew.vin.begin(), txNew.vin.end(), CompareInputBIP69());
313 0 : sort(txNew.vout.begin(), txNew.vout.end(), CompareOutputBIP69());
314 :
315 0 : finalMutableTransaction = txNew;
316 0 : LogPrint(BCLog::COINJOIN, "CCoinJoinServer::CreateFinalTransaction -- finalMutableTransaction=%s", /* Continued */
317 : txNew.ToString());
318 :
319 : // request signatures from clients
320 0 : SetState(POOL_STATE_SIGNING);
321 0 : RelayFinalTransaction(CTransaction(finalMutableTransaction));
322 0 : }
323 :
324 0 : void CCoinJoinServer::CommitFinalTransaction()
325 : {
326 0 : AssertLockNotHeld(cs_coinjoin);
327 :
328 0 : CTransactionRef finalTransaction = WITH_LOCK(cs_coinjoin, return MakeTransactionRef(finalMutableTransaction));
329 0 : uint256 hashTx = finalTransaction->GetHash();
330 :
331 0 : LogPrint(BCLog::COINJOIN, "CCoinJoinServer::CommitFinalTransaction -- finalTransaction=%s", /* Continued */
332 : finalTransaction->ToString());
333 :
334 : {
335 : // See if the transaction is valid
336 0 : TRY_LOCK(::cs_main, lockMain);
337 0 : mempool.PrioritiseTransaction(hashTx, 0.1 * COIN);
338 0 : if (!lockMain || !ATMPIfSaneFee(m_chainman, finalTransaction)) {
339 0 : LogPrint(BCLog::COINJOIN, /* Continued */
340 : "CCoinJoinServer::CommitFinalTransaction -- ATMPIfSaneFee() error: Transaction not valid\n");
341 0 : WITH_LOCK(cs_coinjoin, SetNull());
342 : // not much we can do in this case, just notify clients
343 0 : RelayCompletedTransaction(ERR_INVALID_TX);
344 0 : return;
345 : }
346 0 : }
347 :
348 0 : LogPrint(BCLog::COINJOIN, "CCoinJoinServer::CommitFinalTransaction -- CREATING DSTX\n");
349 :
350 : // create and sign masternode dstx transaction
351 0 : if (!m_dstxman.GetDSTX(hashTx)) {
352 0 : CCoinJoinBroadcastTx dstxNew(finalTransaction, m_mn_activeman.GetOutPoint(), m_mn_activeman.GetProTxHash(),
353 0 : GetAdjustedTime());
354 0 : dstxNew.vchSig = m_mn_activeman.SignBasic(dstxNew.GetSignatureHash());
355 0 : m_dstxman.AddDSTX(dstxNew);
356 0 : }
357 :
358 0 : LogPrint(BCLog::COINJOIN, "CCoinJoinServer::CommitFinalTransaction -- TRANSMITTING DSTX\n");
359 :
360 0 : CInv inv(MSG_DSTX, hashTx);
361 0 : m_peer_manager->PeerRelayInv(inv);
362 :
363 : // Tell the clients it was successful
364 0 : RelayCompletedTransaction(MSG_SUCCESS);
365 :
366 : // Randomly charge clients
367 0 : ChargeRandomFees();
368 :
369 : // Reset
370 0 : LogPrint(BCLog::COINJOIN, "CCoinJoinServer::CommitFinalTransaction -- COMPLETED -- RESETTING\n");
371 0 : WITH_LOCK(cs_coinjoin, SetNull());
372 0 : }
373 :
374 : //
375 : // Charge clients a fee if they're abusive
376 : //
377 : // Why bother? CoinJoin uses collateral to ensure abuse to the process is kept to a minimum.
378 : // The submission and signing stages are completely separate. In the cases where
379 : // a client submits a transaction then refused to sign, there must be a cost. Otherwise, they
380 : // would be able to do this over and over again and bring the mixing to a halt.
381 : //
382 : // How does this work? Messages to Masternodes come in via NetMsgType::DSVIN, these require a valid collateral
383 : // transaction for the client to be able to enter the pool. This transaction is kept by the Masternode
384 : // until the transaction is either complete or fails.
385 : //
386 0 : void CCoinJoinServer::ChargeFees() const
387 : {
388 0 : AssertLockNotHeld(cs_coinjoin);
389 :
390 : //we don't need to charge collateral for every offence.
391 0 : if (GetRand<int>(/*nMax=*/100) > 33) return;
392 :
393 0 : std::vector<CTransactionRef> vecOffendersCollaterals;
394 :
395 0 : if (nState == POOL_STATE_ACCEPTING_ENTRIES) {
396 0 : LOCK(cs_coinjoin);
397 0 : for (const auto& txCollateral : vecSessionCollaterals) {
398 0 : bool fFound = std::ranges::any_of(vecEntries, [&txCollateral](const auto& entry) {
399 0 : return *entry.txCollateral == *txCollateral;
400 : });
401 :
402 : // This queue entry didn't send us the promised transaction
403 0 : if (!fFound) {
404 0 : LogPrint(BCLog::COINJOIN, /* Continued */
405 : "CCoinJoinServer::ChargeFees -- found uncooperative node (didn't send transaction), found "
406 : "offence\n");
407 0 : vecOffendersCollaterals.push_back(txCollateral);
408 0 : }
409 : }
410 0 : }
411 :
412 0 : if (nState == POOL_STATE_SIGNING) {
413 : // who didn't sign?
414 0 : LOCK(cs_coinjoin);
415 0 : for (const auto& entry : vecEntries) {
416 0 : for (const auto& txdsin : entry.vecTxDSIn) {
417 0 : if (!txdsin.fHasSig) {
418 0 : LogPrint(BCLog::COINJOIN, /* Continued */
419 : "CCoinJoinServer::ChargeFees -- found uncooperative node (didn't sign), found offence\n");
420 0 : vecOffendersCollaterals.push_back(entry.txCollateral);
421 0 : }
422 : }
423 : }
424 0 : }
425 :
426 : // no offences found
427 0 : if (vecOffendersCollaterals.empty()) return;
428 :
429 : //mostly offending? Charge sometimes
430 0 : if (vecOffendersCollaterals.size() >= vecSessionCollaterals.size() - 1 && GetRand<int>(/*nMax=*/100) > 33) return;
431 :
432 : //everyone is an offender? That's not right
433 0 : if (vecOffendersCollaterals.size() >= vecSessionCollaterals.size()) return;
434 :
435 : //charge one of the offenders randomly
436 0 : Shuffle(vecOffendersCollaterals.begin(), vecOffendersCollaterals.end(), FastRandomContext());
437 :
438 0 : if (nState == POOL_STATE_ACCEPTING_ENTRIES || nState == POOL_STATE_SIGNING) {
439 0 : LogPrint(BCLog::COINJOIN, /* Continued */
440 : "CCoinJoinServer::ChargeFees -- found uncooperative node (didn't %s transaction), charging fees: %s",
441 : (nState == POOL_STATE_SIGNING) ? "sign" : "send", vecOffendersCollaterals[0]->ToString());
442 0 : ConsumeCollateral(vecOffendersCollaterals[0]);
443 0 : }
444 0 : }
445 :
446 : /*
447 : Charge the collateral randomly.
448 : Mixing is completely free, to pay miners we randomly pay the collateral of users.
449 :
450 : Collateral Fee Charges:
451 :
452 : Being that mixing has "no fees" we need to have some kind of cost associated
453 : with using it to stop abuse. Otherwise, it could serve as an attack vector and
454 : allow endless transaction that would bloat Dash and make it unusable. To
455 : stop these kinds of attacks 1 in 10 successful transactions are charged. This
456 : adds up to a cost of 0.001DRK per transaction on average.
457 : */
458 0 : void CCoinJoinServer::ChargeRandomFees() const
459 : {
460 0 : for (const auto& txCollateral : vecSessionCollaterals) {
461 0 : if (GetRand<int>(/*nMax=*/100) > 10) return;
462 0 : LogPrint(BCLog::COINJOIN, /* Continued */
463 : "CCoinJoinServer::ChargeRandomFees -- charging random fees, txCollateral=%s", txCollateral->ToString());
464 0 : ConsumeCollateral(txCollateral);
465 : }
466 0 : }
467 :
468 0 : void CCoinJoinServer::ConsumeCollateral(const CTransactionRef& txref) const
469 : {
470 0 : LOCK(::cs_main);
471 0 : if (!ATMPIfSaneFee(m_chainman, txref)) {
472 0 : LogPrint(BCLog::COINJOIN, "%s -- ATMPIfSaneFee failed\n", __func__);
473 0 : } else {
474 0 : m_peer_manager->PeerRelayTransaction(txref->GetHash());
475 0 : LogPrint(BCLog::COINJOIN, "%s -- Collateral was consumed\n", __func__);
476 : }
477 0 : }
478 :
479 55208 : bool CCoinJoinServer::HasTimedOut() const
480 : {
481 55208 : if (nState == POOL_STATE_IDLE) return false;
482 :
483 0 : int nTimeout = (nState == POOL_STATE_SIGNING) ? COINJOIN_SIGNING_TIMEOUT : COINJOIN_QUEUE_TIMEOUT;
484 :
485 0 : return GetTime() - nTimeLastSuccessfulStep >= nTimeout;
486 55208 : }
487 :
488 : //
489 : // Check for extraneous timeout
490 : //
491 55208 : void CCoinJoinServer::CheckTimeout()
492 : {
493 55208 : m_queueman.CheckQueue();
494 :
495 : // Too early to do anything
496 55208 : if (!CCoinJoinServer::HasTimedOut()) return;
497 :
498 0 : LogPrint(BCLog::COINJOIN, "CCoinJoinServer::CheckTimeout -- %s timed out -- resetting\n",
499 : (nState == POOL_STATE_SIGNING) ? "Signing" : "Session");
500 0 : ChargeFees();
501 0 : WITH_LOCK(cs_coinjoin, SetNull());
502 55208 : }
503 :
504 : /*
505 : Check to see if we're ready for submissions from clients
506 : After receiving multiple dsa messages, the queue will switch to "accepting entries"
507 : which is the active state right before merging the transaction
508 : */
509 55208 : void CCoinJoinServer::CheckForCompleteQueue()
510 : {
511 55208 : if (nState == POOL_STATE_QUEUE && IsSessionReady()) {
512 0 : SetState(POOL_STATE_ACCEPTING_ENTRIES);
513 :
514 0 : CCoinJoinQueue dsq(nSessionDenom, m_mn_activeman.GetOutPoint(), m_mn_activeman.GetProTxHash(),
515 0 : GetAdjustedTime(), true);
516 0 : LogPrint(BCLog::COINJOIN, "CCoinJoinServer::CheckForCompleteQueue -- queue is ready, signing and relaying (%s) " /* Continued */
517 : "with %d participants\n", dsq.ToString(), vecSessionCollaterals.size());
518 0 : dsq.vchSig = m_mn_activeman.SignBasic(dsq.GetSignatureHash());
519 0 : m_peer_manager->PeerRelayDSQ(dsq);
520 0 : m_queueman.AddQueue(std::move(dsq));
521 0 : }
522 55208 : }
523 :
524 : // Check to make sure a given input matches an input in the pool and its scriptSig is valid
525 0 : bool CCoinJoinServer::IsInputScriptSigValid(const CTxIn& txin) const
526 : {
527 0 : AssertLockHeld(cs_coinjoin);
528 0 : CMutableTransaction txNew;
529 0 : txNew.vin.clear();
530 0 : txNew.vout.clear();
531 :
532 0 : int nTxInIndex = -1;
533 0 : CScript sigPubKey = CScript();
534 :
535 : {
536 0 : int i = 0;
537 0 : for (const auto &entry: vecEntries) {
538 0 : for (const auto &txout: entry.vecTxOut) {
539 0 : txNew.vout.push_back(txout);
540 : }
541 0 : for (const auto &txdsin: entry.vecTxDSIn) {
542 0 : txNew.vin.push_back(txdsin);
543 :
544 0 : if (txdsin.prevout == txin.prevout) {
545 0 : nTxInIndex = i;
546 0 : sigPubKey = txdsin.prevPubKey;
547 0 : }
548 0 : i++;
549 : }
550 : }
551 : }
552 0 : if (nTxInIndex >= 0) { //might have to do this one input at a time?
553 0 : txNew.vin[nTxInIndex].scriptSig = txin.scriptSig;
554 0 : LogPrint(BCLog::COINJOIN, "CCoinJoinServer::IsInputScriptSigValid -- verifying scriptSig %s\n", ScriptToAsmStr(txin.scriptSig).substr(0, 24));
555 : // TODO we're using amount=0 here but we should use the correct amount. This works because Dash ignores the amount while signing/verifying (only used in Bitcoin/Segwit)
556 0 : if (!VerifyScript(txNew.vin[nTxInIndex].scriptSig, sigPubKey, SCRIPT_VERIFY_P2SH | SCRIPT_VERIFY_STRICTENC, MutableTransactionSignatureChecker(&txNew, nTxInIndex, 0, MissingDataBehavior::ASSERT_FAIL))) {
557 0 : LogPrint(BCLog::COINJOIN, "CCoinJoinServer::IsInputScriptSigValid -- VerifyScript() failed on input %d\n", nTxInIndex);
558 0 : return false;
559 : }
560 0 : } else {
561 0 : LogPrint(BCLog::COINJOIN, "CCoinJoinServer::IsInputScriptSigValid -- Failed to find matching input in pool, %s\n", txin.ToString());
562 0 : return false;
563 : }
564 :
565 0 : LogPrint(BCLog::COINJOIN, "CCoinJoinServer::IsInputScriptSigValid -- Successfully validated input and scriptSig\n");
566 0 : return true;
567 0 : }
568 :
569 : //
570 : // Add a client's transaction inputs/outputs to the pool
571 : //
572 0 : bool CCoinJoinServer::AddEntry(const CCoinJoinEntry& entry, PoolMessage& nMessageIDRet)
573 : {
574 0 : AssertLockNotHeld(cs_coinjoin);
575 :
576 0 : if (size_t(GetEntriesCount()) >= vecSessionCollaterals.size()) {
577 0 : LogPrint(BCLog::COINJOIN, "CCoinJoinServer::%s -- ERROR: entries is full!\n", __func__);
578 0 : nMessageIDRet = ERR_ENTRIES_FULL;
579 0 : return false;
580 : }
581 :
582 0 : if (!CoinJoin::IsCollateralValid(m_chainman, m_isman, mempool, *entry.txCollateral)) {
583 0 : LogPrint(BCLog::COINJOIN, "CCoinJoinServer::%s -- ERROR: collateral not valid!\n", __func__);
584 0 : nMessageIDRet = ERR_INVALID_COLLATERAL;
585 0 : return false;
586 : }
587 :
588 0 : if (entry.vecTxDSIn.size() > COINJOIN_ENTRY_MAX_SIZE) {
589 0 : LogPrint(BCLog::COINJOIN, "CCoinJoinServer::%s -- ERROR: too many inputs! %d/%d\n", __func__, entry.vecTxDSIn.size(), COINJOIN_ENTRY_MAX_SIZE);
590 0 : nMessageIDRet = ERR_MAXIMUM;
591 0 : ConsumeCollateral(entry.txCollateral);
592 0 : return false;
593 : }
594 :
595 0 : std::vector<CTxIn> vin;
596 0 : for (const auto& txin : entry.vecTxDSIn) {
597 0 : LogPrint(BCLog::COINJOIN, "CCoinJoinServer::%s -- txin=%s\n", __func__, txin.ToString());
598 0 : LOCK(cs_coinjoin);
599 0 : for (const auto& inner_entry : vecEntries) {
600 0 : if (std::ranges::any_of(inner_entry.vecTxDSIn,
601 0 : [&txin](const auto& txdsin) { return txdsin.prevout == txin.prevout; })) {
602 0 : LogPrint(BCLog::COINJOIN, "CCoinJoinServer::%s -- ERROR: already have this txin in entries\n", __func__);
603 0 : nMessageIDRet = ERR_ALREADY_HAVE;
604 : // Two peers sent the same input? Can't really say who is the malicious one here,
605 : // could be that someone is picking someone else's inputs randomly trying to force
606 : // collateral consumption. Do not punish.
607 0 : return false;
608 : }
609 : }
610 0 : vin.emplace_back(txin);
611 0 : }
612 :
613 0 : bool fConsumeCollateral{false};
614 0 : if (!IsValidInOuts(m_chainman.ActiveChainstate(), m_isman, mempool, vin, entry.vecTxOut, nMessageIDRet,
615 : &fConsumeCollateral)) {
616 0 : LogPrint(BCLog::COINJOIN, "CCoinJoinServer::%s -- ERROR! IsValidInOuts() failed: %s\n", __func__, CoinJoin::GetMessageByID(nMessageIDRet).translated);
617 0 : if (fConsumeCollateral) {
618 0 : ConsumeCollateral(entry.txCollateral);
619 0 : }
620 0 : return false;
621 : }
622 :
623 0 : WITH_LOCK(cs_coinjoin, vecEntries.push_back(entry));
624 :
625 0 : LogPrint(BCLog::COINJOIN, "CCoinJoinServer::%s -- adding entry %d of %d required\n", __func__, GetEntriesCount(), CoinJoin::GetMaxPoolParticipants());
626 0 : nMessageIDRet = MSG_ENTRIES_ADDED;
627 :
628 0 : return true;
629 0 : }
630 :
631 0 : bool CCoinJoinServer::AddScriptSig(const CTxIn& txinNew)
632 : {
633 0 : AssertLockNotHeld(cs_coinjoin);
634 0 : LogPrint(BCLog::COINJOIN, "CCoinJoinServer::AddScriptSig -- scriptSig=%s\n", ScriptToAsmStr(txinNew.scriptSig).substr(0, 24));
635 :
636 0 : LOCK(cs_coinjoin);
637 0 : for (const auto& entry : vecEntries) {
638 0 : if (std::ranges::any_of(entry.vecTxDSIn,
639 0 : [&txinNew](const auto& txdsin) { return txdsin.scriptSig == txinNew.scriptSig; })) {
640 0 : LogPrint(BCLog::COINJOIN, "CCoinJoinServer::AddScriptSig -- already exists\n");
641 0 : return false;
642 : }
643 : }
644 :
645 0 : if (!IsInputScriptSigValid(txinNew)) {
646 0 : LogPrint(BCLog::COINJOIN, "CCoinJoinServer::AddScriptSig -- Invalid scriptSig\n");
647 0 : return false;
648 : }
649 :
650 0 : LogPrint(BCLog::COINJOIN, "CCoinJoinServer::AddScriptSig -- scriptSig=%s new\n", ScriptToAsmStr(txinNew.scriptSig).substr(0, 24));
651 :
652 0 : for (auto& txin : finalMutableTransaction.vin) {
653 0 : if (txin.prevout == txinNew.prevout && txin.nSequence == txinNew.nSequence) {
654 0 : txin.scriptSig = txinNew.scriptSig;
655 0 : LogPrint(BCLog::COINJOIN, "CCoinJoinServer::AddScriptSig -- adding to finalMutableTransaction, scriptSig=%s\n", ScriptToAsmStr(txinNew.scriptSig).substr(0, 24));
656 0 : }
657 : }
658 0 : for (auto& entry : vecEntries) {
659 0 : if (entry.AddScriptSig(txinNew)) {
660 0 : LogPrint(BCLog::COINJOIN, "CCoinJoinServer::AddScriptSig -- adding to entries, scriptSig=%s\n", ScriptToAsmStr(txinNew.scriptSig).substr(0, 24));
661 0 : return true;
662 : }
663 : }
664 :
665 0 : LogPrint(BCLog::COINJOIN, "CCoinJoinServer::AddScriptSig -- Couldn't set sig!\n");
666 0 : return false;
667 0 : }
668 :
669 : // Check to make sure everything is signed
670 0 : bool CCoinJoinServer::IsSignaturesComplete() const
671 : {
672 0 : AssertLockNotHeld(cs_coinjoin);
673 0 : LOCK(cs_coinjoin);
674 :
675 0 : return std::ranges::all_of(vecEntries, [](const auto& entry) {
676 0 : return std::ranges::all_of(entry.vecTxDSIn, [](const auto& txdsin) { return txdsin.fHasSig; });
677 : });
678 0 : }
679 :
680 0 : bool CCoinJoinServer::IsAcceptableDSA(const CCoinJoinAccept& dsa, PoolMessage& nMessageIDRet) const
681 : {
682 : // is denom even something legit?
683 0 : if (!CoinJoin::IsValidDenomination(dsa.nDenom)) {
684 0 : LogPrint(BCLog::COINJOIN, "CCoinJoinServer::%s -- denom not valid!\n", __func__);
685 0 : nMessageIDRet = ERR_DENOM;
686 0 : return false;
687 : }
688 :
689 : // check collateral
690 0 : if (!fUnitTest && !CoinJoin::IsCollateralValid(m_chainman, m_isman, mempool, CTransaction(dsa.txCollateral))) {
691 0 : LogPrint(BCLog::COINJOIN, "CCoinJoinServer::%s -- collateral not valid!\n", __func__);
692 0 : nMessageIDRet = ERR_INVALID_COLLATERAL;
693 0 : return false;
694 : }
695 :
696 0 : return true;
697 0 : }
698 :
699 0 : bool CCoinJoinServer::CreateNewSession(const CCoinJoinAccept& dsa, PoolMessage& nMessageIDRet)
700 : {
701 0 : if (nSessionID != 0) return false;
702 :
703 : // new session can only be started in idle mode
704 0 : if (nState != POOL_STATE_IDLE) {
705 0 : nMessageIDRet = ERR_MODE;
706 0 : LogPrint(BCLog::COINJOIN, "CCoinJoinServer::CreateNewSession -- incompatible mode: nState=%d\n", nState);
707 0 : return false;
708 : }
709 :
710 0 : if (!IsAcceptableDSA(dsa, nMessageIDRet)) {
711 0 : return false;
712 : }
713 :
714 : // start new session
715 0 : nMessageIDRet = MSG_NOERR;
716 0 : nSessionID = GetRand<int>(/*nMax=*/999999) + 1;
717 0 : nSessionDenom = dsa.nDenom;
718 :
719 0 : SetState(POOL_STATE_QUEUE);
720 :
721 0 : if (!fUnitTest) {
722 : //broadcast that I'm accepting entries, only if it's the first entry through
723 0 : CCoinJoinQueue dsq(nSessionDenom, m_mn_activeman.GetOutPoint(), m_mn_activeman.GetProTxHash(),
724 0 : GetAdjustedTime(), false);
725 0 : LogPrint(BCLog::COINJOIN, "CCoinJoinServer::CreateNewSession -- signing and relaying new queue: %s\n", dsq.ToString());
726 0 : dsq.vchSig = m_mn_activeman.SignBasic(dsq.GetSignatureHash());
727 0 : m_peer_manager->PeerRelayDSQ(dsq);
728 0 : m_queueman.AddQueue(std::move(dsq));
729 0 : }
730 :
731 0 : vecSessionCollaterals.push_back(MakeTransactionRef(dsa.txCollateral));
732 0 : LogPrint(BCLog::COINJOIN, "CCoinJoinServer::CreateNewSession -- new session created, nSessionID: %d nSessionDenom: %d (%s) vecSessionCollaterals.size(): %d CoinJoin::GetMaxPoolParticipants(): %d\n",
733 : nSessionID, nSessionDenom, CoinJoin::DenominationToString(nSessionDenom), vecSessionCollaterals.size(), CoinJoin::GetMaxPoolParticipants());
734 :
735 0 : return true;
736 0 : }
737 :
738 0 : bool CCoinJoinServer::AddUserToExistingSession(const CCoinJoinAccept& dsa, PoolMessage& nMessageIDRet)
739 : {
740 0 : if (nSessionID == 0 || IsSessionReady()) return false;
741 :
742 0 : if (!IsAcceptableDSA(dsa, nMessageIDRet)) {
743 0 : return false;
744 : }
745 :
746 : // we only add new users to an existing session when we are in queue mode
747 0 : if (nState != POOL_STATE_QUEUE) {
748 0 : nMessageIDRet = ERR_MODE;
749 0 : LogPrint(BCLog::COINJOIN, "CCoinJoinServer::AddUserToExistingSession -- incompatible mode: nState=%d\n", nState);
750 0 : return false;
751 : }
752 :
753 0 : if (dsa.nDenom != nSessionDenom) {
754 0 : LogPrint(BCLog::COINJOIN, "CCoinJoinServer::AddUserToExistingSession -- incompatible denom %d (%s) != nSessionDenom %d (%s)\n",
755 : dsa.nDenom, CoinJoin::DenominationToString(dsa.nDenom), nSessionDenom, CoinJoin::DenominationToString(nSessionDenom));
756 0 : nMessageIDRet = ERR_DENOM;
757 0 : return false;
758 : }
759 :
760 : // count new user as accepted to an existing session
761 :
762 0 : nMessageIDRet = MSG_NOERR;
763 0 : vecSessionCollaterals.push_back(MakeTransactionRef(dsa.txCollateral));
764 :
765 0 : LogPrint(BCLog::COINJOIN, "CCoinJoinServer::AddUserToExistingSession -- new user accepted, nSessionID: %d nSessionDenom: %d (%s) vecSessionCollaterals.size(): %d CoinJoin::GetMaxPoolParticipants(): %d\n",
766 : nSessionID, nSessionDenom, CoinJoin::DenominationToString(nSessionDenom), vecSessionCollaterals.size(), CoinJoin::GetMaxPoolParticipants());
767 :
768 0 : return true;
769 0 : }
770 :
771 : // Returns true if either max size has been reached or if the mix timed out and min size was reached
772 0 : bool CCoinJoinServer::IsSessionReady() const
773 : {
774 0 : if (nState == POOL_STATE_QUEUE) {
775 0 : if ((int)vecSessionCollaterals.size() >= CoinJoin::GetMaxPoolParticipants()) {
776 0 : return true;
777 : }
778 0 : if (CCoinJoinServer::HasTimedOut() && (int)vecSessionCollaterals.size() >= CoinJoin::GetMinPoolParticipants()) {
779 0 : return true;
780 : }
781 0 : }
782 0 : if (nState == POOL_STATE_ACCEPTING_ENTRIES) {
783 0 : return true;
784 : }
785 0 : return false;
786 0 : }
787 :
788 0 : void CCoinJoinServer::RelayFinalTransaction(const CTransaction& txFinal)
789 : {
790 0 : AssertLockHeld(cs_coinjoin);
791 0 : LogPrint(BCLog::COINJOIN, "CCoinJoinServer::%s -- nSessionID: %d nSessionDenom: %d (%s)\n",
792 : __func__, nSessionID, nSessionDenom, CoinJoin::DenominationToString(nSessionDenom));
793 :
794 : // final mixing tx with empty signatures should be relayed to mixing participants only
795 0 : for (const auto& entry : vecEntries) {
796 0 : bool fOk = connman.ForNode(entry.addr, [&txFinal, this](CNode* pnode) {
797 0 : CNetMsgMaker msgMaker(pnode->GetCommonVersion());
798 0 : connman.PushMessage(pnode, msgMaker.Make(NetMsgType::DSFINALTX, nSessionID.load(), txFinal));
799 0 : return true;
800 0 : });
801 0 : if (!fOk) {
802 : // no such node? maybe this client disconnected or our own connection went down
803 0 : RelayStatus(STATUS_REJECTED);
804 0 : break;
805 : }
806 : }
807 0 : }
808 :
809 0 : void CCoinJoinServer::PushStatus(CNode& peer, PoolStatusUpdate nStatusUpdate, PoolMessage nMessageID) const
810 : {
811 0 : CCoinJoinStatusUpdate psssup(nSessionID, nState, 0, nStatusUpdate, nMessageID);
812 0 : connman.PushMessage(&peer, CNetMsgMaker(peer.GetCommonVersion()).Make(NetMsgType::DSSTATUSUPDATE, psssup));
813 0 : }
814 :
815 0 : void CCoinJoinServer::RelayStatus(PoolStatusUpdate nStatusUpdate, PoolMessage nMessageID)
816 : {
817 0 : AssertLockHeld(cs_coinjoin);
818 0 : unsigned int nDisconnected{};
819 : // status updates should be relayed to mixing participants only
820 0 : for (const auto& entry : vecEntries) {
821 : // make sure everyone is still connected
822 0 : bool fOk = connman.ForNode(entry.addr, [&nStatusUpdate, &nMessageID, this](CNode* pnode) {
823 0 : PushStatus(*pnode, nStatusUpdate, nMessageID);
824 0 : return true;
825 : });
826 0 : if (!fOk) {
827 : // no such node? maybe this client disconnected or our own connection went down
828 0 : ++nDisconnected;
829 0 : }
830 : }
831 0 : if (nDisconnected == 0) return; // all is clear
832 :
833 : // something went wrong
834 0 : LogPrint(BCLog::COINJOIN, "CCoinJoinServer::%s -- can't continue, %llu client(s) disconnected, nSessionID: %d nSessionDenom: %d (%s)\n",
835 : __func__, nDisconnected, nSessionID, nSessionDenom, CoinJoin::DenominationToString(nSessionDenom));
836 :
837 : // notify everyone else that this session should be terminated
838 0 : for (const auto& entry : vecEntries) {
839 0 : connman.ForNode(entry.addr, [this](CNode* pnode) {
840 0 : PushStatus(*pnode, STATUS_REJECTED, MSG_NOERR);
841 0 : return true;
842 : });
843 : }
844 :
845 0 : if (nDisconnected == vecEntries.size()) {
846 : // all clients disconnected, there is probably some issues with our own connection
847 : // do not charge any fees, just reset the pool
848 0 : SetNull();
849 0 : }
850 0 : }
851 :
852 0 : void CCoinJoinServer::RelayCompletedTransaction(PoolMessage nMessageID)
853 : {
854 0 : AssertLockNotHeld(cs_coinjoin);
855 0 : LogPrint(BCLog::COINJOIN, "CCoinJoinServer::%s -- nSessionID: %d nSessionDenom: %d (%s)\n",
856 : __func__, nSessionID, nSessionDenom, CoinJoin::DenominationToString(nSessionDenom));
857 :
858 : // final mixing tx with empty signatures should be relayed to mixing participants only
859 0 : LOCK(cs_coinjoin);
860 0 : for (const auto& entry : vecEntries) {
861 0 : bool fOk = connman.ForNode(entry.addr, [&nMessageID, this](CNode* pnode) {
862 0 : CNetMsgMaker msgMaker(pnode->GetCommonVersion());
863 0 : connman.PushMessage(pnode, msgMaker.Make(NetMsgType::DSCOMPLETE, nSessionID.load(), nMessageID));
864 0 : return true;
865 0 : });
866 0 : if (!fOk) {
867 : // no such node? maybe client disconnected or our own connection went down
868 0 : RelayStatus(STATUS_REJECTED);
869 0 : break;
870 : }
871 : }
872 0 : }
873 :
874 0 : void CCoinJoinServer::SetState(PoolState nStateNew)
875 : {
876 0 : if (nStateNew == POOL_STATE_ERROR) {
877 0 : LogPrint(BCLog::COINJOIN, "CCoinJoinServer::SetState -- Can't set state to ERROR as a Masternode. \n");
878 0 : return;
879 : }
880 :
881 0 : LogPrint(BCLog::COINJOIN, "CCoinJoinServer::SetState -- nState: %d, nStateNew: %d\n", nState, nStateNew);
882 0 : nTimeLastSuccessfulStep = GetTime();
883 0 : nState = nStateNew;
884 0 : }
885 :
886 660 : void CCoinJoinServer::Schedule(CScheduler& scheduler)
887 : {
888 1320 : scheduler.scheduleEvery(
889 57671 : [this]() -> void {
890 57011 : if (!m_mn_sync.IsBlockchainSynced()) return;
891 55363 : if (ShutdownRequested()) return;
892 :
893 55208 : CheckForCompleteQueue();
894 55208 : CheckPool();
895 55208 : CheckTimeout();
896 57011 : },
897 660 : std::chrono::seconds{1});
898 660 : }
899 :
900 0 : void CCoinJoinServer::GetJsonInfo(UniValue& obj) const
901 : {
902 0 : obj.clear();
903 0 : obj.setObject();
904 0 : obj.pushKV("queue_size", m_queueman.GetQueueSize());
905 0 : obj.pushKV("denomination", ValueFromAmount(CoinJoin::DenominationToAmount(nSessionDenom)));
906 0 : obj.pushKV("state", GetStateString());
907 0 : obj.pushKV("entries_count", GetEntriesCount());
908 0 : }
909 :
910 33371 : bool CCoinJoinServer::AlreadyHave(const CInv& inv)
911 : {
912 33371 : return (inv.type == MSG_DSQ) ? m_queueman.HasQueue(inv.hash) : false;
913 : }
914 :
915 814 : bool CCoinJoinServer::ProcessGetData(CNode& pfrom, const CInv& inv, CConnman& connman, const CNetMsgMaker& msgMaker)
916 : {
917 814 : if (inv.type != MSG_DSQ) return false;
918 :
919 0 : auto opt_dsq = m_queueman.GetQueueFromHash(inv.hash);
920 0 : if (!opt_dsq.has_value()) return false;
921 :
922 0 : connman.PushMessage(&pfrom, msgMaker.Make(NetMsgType::DSQUEUE, *opt_dsq));
923 0 : return true;
924 814 : }
|