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/coinjoin.h>
6 :
7 : #include <bls/bls.h>
8 : #include <chainlock/chainlock.h>
9 : #include <instantsend/instantsend.h>
10 : #include <util/helpers.h>
11 :
12 : #include <chain.h>
13 : #include <chainparams.h>
14 : #include <txmempool.h>
15 : #include <util/moneystr.h>
16 : #include <util/system.h>
17 : #include <util/translation.h>
18 : #include <validation.h>
19 : #include <tinyformat.h>
20 :
21 : #include <ranges>
22 : #include <string>
23 :
24 : constexpr static CAmount DEFAULT_MAX_RAW_TX_FEE{COIN / 10};
25 :
26 0 : bool CCoinJoinEntry::AddScriptSig(const CTxIn& txin)
27 : {
28 0 : for (auto& txdsin : vecTxDSIn) {
29 0 : if (txdsin.prevout == txin.prevout && txdsin.nSequence == txin.nSequence) {
30 0 : if (txdsin.fHasSig) return false;
31 :
32 0 : txdsin.scriptSig = txin.scriptSig;
33 0 : txdsin.fHasSig = true;
34 :
35 0 : return true;
36 : }
37 : }
38 :
39 0 : return false;
40 0 : }
41 :
42 4 : uint256 CCoinJoinQueue::GetSignatureHash() const
43 : {
44 4 : return SerializeHash(*this, SER_GETHASH, PROTOCOL_VERSION);
45 : }
46 2 : uint256 CCoinJoinQueue::GetHash() const { return SerializeHash(*this, SER_NETWORK, PROTOCOL_VERSION); }
47 :
48 1 : bool CCoinJoinQueue::CheckSignature(const CBLSPublicKey& blsPubKey) const
49 : {
50 1 : if (!CBLSSignature(Span{vchSig}, false).VerifyInsecure(blsPubKey, GetSignatureHash(), false)) {
51 0 : LogPrint(BCLog::COINJOIN, "CCoinJoinQueue::CheckSignature -- VerifyInsecure() failed\n");
52 0 : return false;
53 : }
54 :
55 1 : return true;
56 1 : }
57 :
58 17 : bool CCoinJoinQueue::IsTimeOutOfBounds(int64_t current_time) const
59 : {
60 17 : if (current_time < 0 || nTime < 0) return true;
61 13 : return current_time - nTime > COINJOIN_QUEUE_TIMEOUT ||
62 9 : nTime - current_time > COINJOIN_QUEUE_TIMEOUT;
63 17 : }
64 :
65 1 : [[nodiscard]] std::string CCoinJoinQueue::ToString() const
66 : {
67 1 : return strprintf("nDenom=%d, nTime=%lld, fReady=%s, fTried=%s, masternode=%s",
68 1 : nDenom, nTime, util::to_string(fReady), util::to_string(fTried), masternodeOutpoint.ToStringShort());
69 0 : }
70 :
71 0 : uint256 CCoinJoinBroadcastTx::GetSignatureHash() const
72 : {
73 0 : return SerializeHash(*this, SER_GETHASH, PROTOCOL_VERSION);
74 : }
75 :
76 0 : bool CCoinJoinBroadcastTx::CheckSignature(const CBLSPublicKey& blsPubKey) const
77 : {
78 0 : if (!CBLSSignature(Span{vchSig}, false).VerifyInsecure(blsPubKey, GetSignatureHash(), false)) {
79 0 : LogPrint(BCLog::COINJOIN, "CCoinJoinBroadcastTx::CheckSignature -- VerifyInsecure() failed\n");
80 0 : return false;
81 : }
82 :
83 0 : return true;
84 0 : }
85 :
86 209 : bool CCoinJoinBroadcastTx::IsValidStructure() const
87 : {
88 : // some trivial checks only
89 209 : if (masternodeOutpoint.IsNull() && m_protxHash.IsNull()) {
90 1 : return false;
91 : }
92 208 : if (tx->vin.size() != tx->vout.size()) {
93 3 : return false;
94 : }
95 205 : if (tx->vin.size() < size_t(CoinJoin::GetMinPoolParticipants())) {
96 0 : return false;
97 : }
98 205 : if (tx->vin.size() > CoinJoin::GetMaxPoolParticipants() * COINJOIN_ENTRY_MAX_SIZE) {
99 0 : return false;
100 : }
101 816 : return std::ranges::all_of(tx->vout, [](const auto& txOut) {
102 611 : return CoinJoin::IsDenominatedAmount(txOut.nValue) && txOut.scriptPubKey.IsPayToPublicKeyHash();
103 : });
104 209 : }
105 :
106 0 : void CCoinJoinBaseSession::SetNull()
107 : {
108 : // Both sides
109 0 : AssertLockHeld(cs_coinjoin);
110 0 : nState = POOL_STATE_IDLE;
111 0 : nSessionID = 0;
112 0 : nSessionDenom = 0;
113 0 : vecEntries.clear();
114 0 : finalMutableTransaction.vin.clear();
115 0 : finalMutableTransaction.vout.clear();
116 0 : nTimeLastSuccessfulStep = GetTime();
117 0 : }
118 :
119 0 : void CoinJoinQueueManager::SetNull()
120 : {
121 0 : LOCK(cs_vecqueue);
122 0 : vecCoinJoinQueue.clear();
123 0 : }
124 :
125 72768 : void CoinJoinQueueManager::CheckQueue()
126 : {
127 72768 : TRY_LOCK(cs_vecqueue, lockDS);
128 72768 : if (!lockDS) return; // it's ok to fail here, we run this quite frequently
129 :
130 : // check mixing queue objects for timeouts
131 72768 : auto it = vecCoinJoinQueue.begin();
132 72770 : while (it != vecCoinJoinQueue.end()) {
133 2 : if (it->IsTimeOutOfBounds()) {
134 1 : LogPrint(BCLog::COINJOIN, "CoinJoinQueueManager::%s -- Removing a queue (%s)\n", __func__, it->ToString());
135 1 : it = vecCoinJoinQueue.erase(it);
136 1 : } else {
137 1 : ++it;
138 : }
139 : }
140 72768 : }
141 :
142 0 : std::optional<bool> CoinJoinQueueManager::TryHasQueueFromMasternode(const COutPoint& outpoint) const
143 : {
144 0 : TRY_LOCK(cs_vecqueue, lockDS);
145 0 : if (!lockDS) return std::nullopt;
146 0 : return std::ranges::any_of(vecCoinJoinQueue, [&outpoint](const auto& q) { return q.masternodeOutpoint == outpoint; });
147 0 : }
148 :
149 0 : std::optional<bool> CoinJoinQueueManager::TryCheckDuplicate(const CCoinJoinQueue& dsq) const
150 : {
151 0 : TRY_LOCK(cs_vecqueue, lockDS);
152 0 : if (!lockDS) return std::nullopt;
153 0 : for (const auto& q : vecCoinJoinQueue) {
154 0 : if (q == dsq) return true;
155 0 : if (q.fReady == dsq.fReady && q.masternodeOutpoint == dsq.masternodeOutpoint) return true;
156 : }
157 0 : return false;
158 0 : }
159 :
160 0 : bool CoinJoinQueueManager::TryAddQueue(CCoinJoinQueue dsq)
161 : {
162 0 : TRY_LOCK(cs_vecqueue, lockDS);
163 0 : if (!lockDS) return false;
164 0 : vecCoinJoinQueue.push_back(std::move(dsq));
165 0 : return true;
166 0 : }
167 :
168 2 : bool CoinJoinQueueManager::GetQueueItemAndTry(CCoinJoinQueue& dsqRet)
169 : {
170 2 : TRY_LOCK(cs_vecqueue, lockDS);
171 2 : if (!lockDS) return false; // it's ok to fail here, we run this quite frequently
172 :
173 3 : for (auto& dsq : vecCoinJoinQueue) {
174 : // only try each queue once
175 2 : if (dsq.fTried || dsq.IsTimeOutOfBounds()) continue;
176 1 : dsq.fTried = true;
177 1 : dsqRet = dsq;
178 1 : return true;
179 : }
180 :
181 1 : return false;
182 2 : }
183 :
184 0 : std::string CCoinJoinBaseSession::GetStateString() const
185 : {
186 0 : switch (nState) {
187 : case POOL_STATE_IDLE:
188 0 : return "IDLE";
189 : case POOL_STATE_QUEUE:
190 0 : return "QUEUE";
191 : case POOL_STATE_ACCEPTING_ENTRIES:
192 0 : return "ACCEPTING_ENTRIES";
193 : case POOL_STATE_SIGNING:
194 0 : return "SIGNING";
195 : case POOL_STATE_ERROR:
196 0 : return "ERROR";
197 : default:
198 0 : return "UNKNOWN";
199 : }
200 0 : }
201 :
202 0 : bool CCoinJoinBaseSession::IsValidInOuts(CChainState& active_chainstate, const llmq::CInstantSendManager& isman,
203 : const CTxMemPool& mempool, const std::vector<CTxIn>& vin,
204 : const std::vector<CTxOut>& vout, PoolMessage& nMessageIDRet,
205 : bool* fConsumeCollateralRet) const
206 : {
207 0 : std::set<CScript> setScripPubKeys;
208 0 : nMessageIDRet = MSG_NOERR;
209 0 : if (fConsumeCollateralRet) *fConsumeCollateralRet = false;
210 :
211 0 : if (vin.size() != vout.size()) {
212 0 : LogPrint(BCLog::COINJOIN, "CCoinJoinBaseSession::%s -- ERROR: inputs vs outputs size mismatch! %d vs %d\n", __func__, vin.size(), vout.size());
213 0 : nMessageIDRet = ERR_SIZE_MISMATCH;
214 0 : if (fConsumeCollateralRet) *fConsumeCollateralRet = true;
215 0 : return false;
216 : }
217 :
218 0 : auto checkTxOut = [&](const CTxOut& txout) {
219 0 : if (int nDenom = CoinJoin::AmountToDenomination(txout.nValue); nDenom != nSessionDenom) {
220 0 : LogPrint(BCLog::COINJOIN, "CCoinJoinBaseSession::IsValidInOuts -- ERROR: incompatible denom %d (%s) != nSessionDenom %d (%s)\n",
221 : nDenom, CoinJoin::DenominationToString(nDenom), nSessionDenom, CoinJoin::DenominationToString(nSessionDenom));
222 0 : nMessageIDRet = ERR_DENOM;
223 0 : if (fConsumeCollateralRet) *fConsumeCollateralRet = true;
224 0 : return false;
225 : }
226 0 : if (!txout.scriptPubKey.IsPayToPublicKeyHash()) {
227 0 : LogPrint(BCLog::COINJOIN, "CCoinJoinBaseSession::IsValidInOuts -- ERROR: invalid script! scriptPubKey=%s\n", ScriptToAsmStr(txout.scriptPubKey));
228 0 : nMessageIDRet = ERR_INVALID_SCRIPT;
229 0 : if (fConsumeCollateralRet) *fConsumeCollateralRet = true;
230 0 : return false;
231 : }
232 0 : if (!setScripPubKeys.insert(txout.scriptPubKey).second) {
233 0 : LogPrint(BCLog::COINJOIN, "CCoinJoinBaseSession::IsValidInOuts -- ERROR: already have this script! scriptPubKey=%s\n", ScriptToAsmStr(txout.scriptPubKey));
234 0 : nMessageIDRet = ERR_ALREADY_HAVE;
235 0 : if (fConsumeCollateralRet) *fConsumeCollateralRet = true;
236 0 : return false;
237 : }
238 : // IsPayToPublicKeyHash() above already checks for scriptPubKey size,
239 : // no need to double-check, hence no usage of ERR_NON_STANDARD_PUBKEY
240 0 : return true;
241 0 : };
242 :
243 0 : CAmount nFees{0};
244 :
245 0 : for (const auto& txout : vout) {
246 0 : if (!checkTxOut(txout)) {
247 0 : return false;
248 : }
249 0 : nFees -= txout.nValue;
250 : }
251 :
252 0 : CCoinsViewMemPool viewMemPool(WITH_LOCK(::cs_main, return &active_chainstate.CoinsTip()), mempool);
253 :
254 0 : for (const auto& txin : vin) {
255 0 : LogPrint(BCLog::COINJOIN, "CCoinJoinBaseSession::%s -- txin=%s\n", __func__, txin.ToString());
256 :
257 0 : if (txin.prevout.IsNull()) {
258 0 : LogPrint(BCLog::COINJOIN, "CCoinJoinBaseSession::%s -- ERROR: invalid input!\n", __func__);
259 0 : nMessageIDRet = ERR_INVALID_INPUT;
260 0 : if (fConsumeCollateralRet) *fConsumeCollateralRet = true;
261 0 : return false;
262 : }
263 :
264 0 : Coin coin;
265 0 : if (!viewMemPool.GetCoin(txin.prevout, coin) || coin.IsSpent() ||
266 0 : (coin.nHeight == MEMPOOL_HEIGHT && !isman.IsLocked(txin.prevout.hash))) {
267 0 : LogPrint(BCLog::COINJOIN, "CCoinJoinBaseSession::%s -- ERROR: missing, spent or non-locked mempool input! txin=%s\n", __func__, txin.ToString());
268 0 : nMessageIDRet = ERR_MISSING_TX;
269 0 : return false;
270 : }
271 :
272 0 : if (!checkTxOut(coin.out)) {
273 0 : return false;
274 : }
275 :
276 0 : nFees += coin.out.nValue;
277 0 : }
278 :
279 : // The same size and denom for inputs and outputs ensures their total value is also the same,
280 : // no need to double-check. If not, we are doing something wrong, bail out.
281 0 : if (nFees != 0) {
282 0 : LogPrint(BCLog::COINJOIN, "CCoinJoinBaseSession::%s -- ERROR: non-zero fees! fees: %lld\n", __func__, nFees);
283 0 : nMessageIDRet = ERR_FEES;
284 0 : return false;
285 : }
286 :
287 0 : return true;
288 0 : }
289 :
290 : // Responsibility for checking fee sanity is moved from the mempool to the client (BroadcastTransaction)
291 : // but CoinJoin still requires ATMP with fee sanity checks so we need to implement them separately
292 0 : bool ATMPIfSaneFee(ChainstateManager& chainman, const CTransactionRef& tx, bool test_accept)
293 : {
294 0 : AssertLockHeld(::cs_main);
295 :
296 0 : const MempoolAcceptResult result = chainman.ProcessTransaction(tx, /*test_accept=*/true);
297 0 : if (result.m_result_type != MempoolAcceptResult::ResultType::VALID) {
298 : /* Fetch fee and fast-fail if ATMP fails regardless */
299 0 : return false;
300 0 : } else if (result.m_base_fees.value() > DEFAULT_MAX_RAW_TX_FEE) {
301 : /* Check fee against fixed upper limit */
302 0 : return false;
303 0 : } else if (test_accept) {
304 : /* Don't re-run ATMP if only doing test run */
305 0 : return true;
306 : }
307 0 : return chainman.ProcessTransaction(tx, test_accept).m_result_type == MempoolAcceptResult::ResultType::VALID;
308 0 : }
309 :
310 : // check to make sure the collateral provided by the client is valid
311 0 : bool CoinJoin::IsCollateralValid(ChainstateManager& chainman, const llmq::CInstantSendManager& isman,
312 : const CTxMemPool& mempool, const CTransaction& txCollateral)
313 : {
314 0 : if (txCollateral.vout.empty()) return false;
315 0 : if (txCollateral.nLockTime != 0) return false;
316 :
317 0 : CAmount nValueIn = 0;
318 0 : CAmount nValueOut = 0;
319 :
320 0 : for (const auto& txout : txCollateral.vout) {
321 0 : nValueOut += txout.nValue;
322 :
323 0 : if (!txout.scriptPubKey.IsPayToPublicKeyHash() && !txout.scriptPubKey.IsUnspendable()) {
324 0 : LogPrint(BCLog::COINJOIN, "CoinJoin::IsCollateralValid -- Invalid Script, txCollateral=%s", txCollateral.ToString()); /* Continued */
325 0 : return false;
326 : }
327 : }
328 :
329 0 : LOCK(::cs_main);
330 0 : CCoinsViewMemPool viewMemPool(&chainman.ActiveChainstate().CoinsTip(), mempool);
331 :
332 0 : for (const auto& txin : txCollateral.vin) {
333 0 : Coin coin;
334 0 : if (!viewMemPool.GetCoin(txin.prevout, coin) || coin.IsSpent() ||
335 0 : (coin.nHeight == MEMPOOL_HEIGHT && !isman.IsLocked(txin.prevout.hash))) {
336 0 : LogPrint(BCLog::COINJOIN, "CoinJoin::IsCollateralValid -- missing, spent or non-locked mempool input! txin=%s\n", txin.ToString());
337 0 : return false;
338 : }
339 0 : nValueIn += coin.out.nValue;
340 0 : }
341 :
342 : //collateral transactions are required to pay out a small fee to the miners
343 0 : if (nValueIn - nValueOut < GetCollateralAmount()) {
344 0 : LogPrint(BCLog::COINJOIN, "CoinJoin::IsCollateralValid -- did not include enough fees in transaction: fees: %d, txCollateral=%s", nValueOut - nValueIn, txCollateral.ToString()); /* Continued */
345 0 : return false;
346 : }
347 :
348 0 : LogPrint(BCLog::COINJOIN, "CoinJoin::IsCollateralValid -- %s", txCollateral.ToString()); /* Continued */
349 :
350 0 : if (!ATMPIfSaneFee(chainman, MakeTransactionRef(txCollateral), /*test_accept=*/true)) {
351 0 : LogPrint(BCLog::COINJOIN, "CoinJoin::IsCollateralValid -- didn't pass ATMPIfSaneFee()\n");
352 0 : return false;
353 : }
354 :
355 0 : return true;
356 0 : }
357 :
358 0 : bilingual_str CoinJoin::GetMessageByID(PoolMessage nMessageID)
359 : {
360 0 : switch (nMessageID) {
361 : case ERR_ALREADY_HAVE:
362 0 : return _("Already have that input.");
363 : case ERR_DENOM:
364 0 : return _("No matching denominations found for mixing.");
365 : case ERR_ENTRIES_FULL:
366 0 : return _("Entries are full.");
367 : case ERR_EXISTING_TX:
368 0 : return _("Not compatible with existing transactions.");
369 : case ERR_FEES:
370 0 : return _("Transaction fees are too high.");
371 : case ERR_INVALID_COLLATERAL:
372 0 : return _("Collateral not valid.");
373 : case ERR_INVALID_INPUT:
374 0 : return _("Input is not valid.");
375 : case ERR_INVALID_SCRIPT:
376 0 : return _("Invalid script detected.");
377 : case ERR_INVALID_TX:
378 0 : return _("Transaction not valid.");
379 : case ERR_MAXIMUM:
380 0 : return _("Entry exceeds maximum size.");
381 : case ERR_MN_LIST:
382 0 : return _("Not in the Masternode list.");
383 : case ERR_MODE:
384 0 : return _("Incompatible mode.");
385 : case ERR_QUEUE_FULL:
386 0 : return _("Masternode queue is full.");
387 : case ERR_RECENT:
388 0 : return _("Last queue was created too recently.");
389 : case ERR_SESSION:
390 0 : return _("Session not complete!");
391 : case ERR_MISSING_TX:
392 0 : return _("Missing input transaction information.");
393 : case ERR_VERSION:
394 0 : return _("Incompatible version.");
395 : case MSG_NOERR:
396 0 : return _("No errors detected.");
397 : case MSG_SUCCESS:
398 0 : return _("Transaction created successfully.");
399 : case MSG_ENTRIES_ADDED:
400 0 : return _("Your entries added successfully.");
401 : case ERR_SIZE_MISMATCH:
402 0 : return _("Inputs vs outputs size mismatch.");
403 : case ERR_NON_STANDARD_PUBKEY:
404 0 : case ERR_NOT_A_MN:
405 : default:
406 0 : return _("Unknown response.");
407 : }
408 0 : }
409 :
410 9105 : CDSTXManager::CDSTXManager(const chainlock::Chainlocks& chainlocks) :
411 3035 : m_chainlocks{chainlocks}
412 3035 : {
413 6070 : }
414 6070 : CDSTXManager::~CDSTXManager() = default;
415 :
416 3 : void CDSTXManager::AddDSTX(const CCoinJoinBroadcastTx& dstx)
417 : {
418 3 : AssertLockNotHeld(cs_mapdstx);
419 3 : LOCK(cs_mapdstx);
420 3 : mapDSTX.insert(std::make_pair(dstx.tx->GetHash(), dstx));
421 3 : }
422 :
423 68665 : CCoinJoinBroadcastTx CDSTXManager::GetDSTX(const uint256& hash)
424 : {
425 68665 : AssertLockNotHeld(cs_mapdstx);
426 68665 : LOCK(cs_mapdstx);
427 68665 : auto it = mapDSTX.find(hash);
428 68665 : return (it == mapDSTX.end()) ? CCoinJoinBroadcastTx() : it->second;
429 68665 : }
430 :
431 2 : bool CDSTXManager::IsTxExpired(const CCoinJoinBroadcastTx& tx, const CBlockIndex* pindex) const
432 : {
433 : // expire confirmed DSTXes after ~1h since confirmation or chainlocked
434 2 : const auto& opt_confirmed_height = tx.GetConfirmedHeight();
435 2 : if (!opt_confirmed_height.has_value() || pindex->nHeight < *opt_confirmed_height) return false; // not mined yet
436 2 : return (pindex->nHeight - *opt_confirmed_height > 24) ||
437 1 : m_chainlocks.HasChainLock(pindex->nHeight, *pindex->phashBlock); // mined more than an hour ago or chainlocked
438 2 : }
439 :
440 112865 : void CDSTXManager::CheckDSTXes(const CBlockIndex* pindex)
441 : {
442 112865 : AssertLockNotHeld(cs_mapdstx);
443 112865 : LOCK(cs_mapdstx);
444 112865 : auto it = mapDSTX.begin();
445 112867 : while (it != mapDSTX.end()) {
446 2 : if (IsTxExpired(it->second, pindex)) {
447 1 : mapDSTX.erase(it++);
448 1 : } else {
449 1 : ++it;
450 : }
451 : }
452 112865 : LogPrint(BCLog::COINJOIN, "CoinJoin::CheckDSTXes -- mapDSTX.size()=%llu\n", mapDSTX.size());
453 112865 : }
454 :
455 99321 : void CDSTXManager::UpdatedBlockTip(const CBlockIndex* pindex)
456 : {
457 99321 : if (pindex) {
458 99321 : CheckDSTXes(pindex);
459 99321 : }
460 99321 : }
461 :
462 13544 : void CDSTXManager::NotifyChainLock(const CBlockIndex* pindex)
463 : {
464 13544 : if (pindex) {
465 13544 : CheckDSTXes(pindex);
466 13544 : }
467 13544 : }
468 :
469 569093 : void CDSTXManager::UpdateDSTXConfirmedHeight(const CTransactionRef& tx, std::optional<int> nHeight)
470 : {
471 569093 : AssertLockHeld(cs_mapdstx);
472 :
473 569093 : auto it = mapDSTX.find(tx->GetHash());
474 569093 : if (it == mapDSTX.end()) {
475 569091 : return;
476 : }
477 :
478 2 : it->second.SetConfirmedHeight(nHeight);
479 2 : LogPrint(BCLog::COINJOIN, "CDSTXManager::%s -- txid=%s, nHeight=%d\n", __func__, tx->GetHash().ToString(), nHeight.value_or(-1));
480 569093 : }
481 :
482 36808 : void CDSTXManager::TransactionAddedToMempool(const CTransactionRef& tx)
483 : {
484 36808 : AssertLockNotHeld(cs_mapdstx);
485 36808 : LOCK(cs_mapdstx);
486 36808 : UpdateDSTXConfirmedHeight(tx, std::nullopt);
487 36808 : }
488 :
489 228493 : void CDSTXManager::BlockConnected(const std::shared_ptr<const CBlock>& pblock, const CBlockIndex* pindex)
490 : {
491 228493 : AssertLockNotHeld(cs_mapdstx);
492 228493 : LOCK(cs_mapdstx);
493 :
494 722985 : for (const auto& tx : pblock->vtx) {
495 494492 : UpdateDSTXConfirmedHeight(tx, pindex->nHeight);
496 : }
497 228493 : }
498 :
499 14237 : void CDSTXManager::BlockDisconnected(const std::shared_ptr<const CBlock>& pblock, const CBlockIndex*)
500 : {
501 14237 : AssertLockNotHeld(cs_mapdstx);
502 14237 : LOCK(cs_mapdstx);
503 52030 : for (const auto& tx : pblock->vtx) {
504 37793 : UpdateDSTXConfirmedHeight(tx, std::nullopt);
505 : }
506 14237 : }
507 :
508 207 : int CoinJoin::GetMinPoolParticipants() { return Params().PoolMinParticipants(); }
509 205 : int CoinJoin::GetMaxPoolParticipants() { return Params().PoolMaxParticipants(); }
|