|
| 1 | +// Copyright (c) 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 <test/util/setup_common.h> |
| 6 | + |
| 7 | +#include <algorithm> |
| 8 | +#include <array> |
| 9 | +#include <cstdint> |
| 10 | +#include <vector> |
| 11 | + |
| 12 | +#include <chain.h> |
| 13 | +#include <chainlock/chainlock.h> |
| 14 | +#include <coinjoin/coinjoin.h> |
| 15 | +#include <coinjoin/common.h> |
| 16 | +#include <llmq/context.h> |
| 17 | +#include <script/script.h> |
| 18 | +#include <uint256.h> |
| 19 | +#include <util/check.h> |
| 20 | +#include <util/time.h> |
| 21 | + |
| 22 | +#include <boost/test/unit_test.hpp> |
| 23 | + |
| 24 | +BOOST_FIXTURE_TEST_SUITE(coinjoin_inouts_tests, TestingSetup) |
| 25 | + |
| 26 | +static CScript P2PKHScript(uint8_t tag = 0x01) |
| 27 | +{ |
| 28 | + // OP_DUP OP_HASH160 <20-byte-tag> OP_EQUALVERIFY OP_CHECKSIG |
| 29 | + std::vector<unsigned char> hash(20, tag); |
| 30 | + return CScript{} << OP_DUP << OP_HASH160 << hash << OP_EQUALVERIFY << OP_CHECKSIG; |
| 31 | +} |
| 32 | + |
| 33 | +BOOST_AUTO_TEST_CASE(broadcasttx_isvalidstructure_good_and_bad) |
| 34 | +{ |
| 35 | + // Good: equal vin/vout sizes, vin count >= min participants, <= max*entry_size, P2PKH outputs with standard denominations |
| 36 | + CCoinJoinBroadcastTx good; |
| 37 | + { |
| 38 | + CMutableTransaction mtx; |
| 39 | + // Use min pool participants (e.g. 3). Build 3 inputs and 3 denominated outputs |
| 40 | + const int participants = std::max(3, CoinJoin::GetMinPoolParticipants()); |
| 41 | + for (int i = 0; i < participants; ++i) { |
| 42 | + CTxIn in; |
| 43 | + in.prevout = COutPoint(uint256::ONE, static_cast<uint32_t>(i)); |
| 44 | + mtx.vin.push_back(in); |
| 45 | + // Pick the smallest denomination |
| 46 | + CTxOut out{CoinJoin::GetSmallestDenomination(), P2PKHScript(static_cast<uint8_t>(i))}; |
| 47 | + mtx.vout.push_back(out); |
| 48 | + } |
| 49 | + good.tx = MakeTransactionRef(mtx); |
| 50 | + good.m_protxHash = uint256::ONE; // at least one of (outpoint, protxhash) must be set |
| 51 | + } |
| 52 | + BOOST_CHECK(good.IsValidStructure()); |
| 53 | + |
| 54 | + // Bad: both identifiers null |
| 55 | + CCoinJoinBroadcastTx bad_ids = good; |
| 56 | + bad_ids.m_protxHash = uint256{}; |
| 57 | + bad_ids.masternodeOutpoint.SetNull(); |
| 58 | + BOOST_CHECK(!bad_ids.IsValidStructure()); |
| 59 | + |
| 60 | + // Bad: vin/vout size mismatch |
| 61 | + CCoinJoinBroadcastTx bad_sizes = good; |
| 62 | + { |
| 63 | + CMutableTransaction mtx(*good.tx); |
| 64 | + mtx.vout.pop_back(); |
| 65 | + bad_sizes.tx = MakeTransactionRef(mtx); |
| 66 | + } |
| 67 | + BOOST_CHECK(!bad_sizes.IsValidStructure()); |
| 68 | + |
| 69 | + // Bad: non-P2PKH output |
| 70 | + CCoinJoinBroadcastTx bad_script = good; |
| 71 | + { |
| 72 | + CMutableTransaction mtx(*good.tx); |
| 73 | + mtx.vout[0].scriptPubKey = CScript() << OP_RETURN << std::vector<unsigned char>{'x'}; |
| 74 | + bad_script.tx = MakeTransactionRef(mtx); |
| 75 | + } |
| 76 | + BOOST_CHECK(!bad_script.IsValidStructure()); |
| 77 | + |
| 78 | + // Bad: non-denominated amount |
| 79 | + CCoinJoinBroadcastTx bad_amount = good; |
| 80 | + { |
| 81 | + CMutableTransaction mtx(*good.tx); |
| 82 | + mtx.vout[0].nValue = 42; // not a valid denom |
| 83 | + bad_amount.tx = MakeTransactionRef(mtx); |
| 84 | + } |
| 85 | + BOOST_CHECK(!bad_amount.IsValidStructure()); |
| 86 | +} |
| 87 | + |
| 88 | +BOOST_AUTO_TEST_CASE(queue_timeout_bounds) |
| 89 | +{ |
| 90 | + CCoinJoinQueue dsq; |
| 91 | + dsq.nDenom = CoinJoin::AmountToDenomination(CoinJoin::GetSmallestDenomination()); |
| 92 | + dsq.m_protxHash = uint256::ONE; |
| 93 | + dsq.nTime = GetAdjustedTime(); |
| 94 | + // current time -> not out of bounds |
| 95 | + BOOST_CHECK(!dsq.IsTimeOutOfBounds()); |
| 96 | + |
| 97 | + // Too old (beyond COINJOIN_QUEUE_TIMEOUT) |
| 98 | + SetMockTime(GetTime() + (COINJOIN_QUEUE_TIMEOUT + 1)); |
| 99 | + BOOST_CHECK(dsq.IsTimeOutOfBounds()); |
| 100 | + |
| 101 | + // Too far in the future |
| 102 | + SetMockTime(GetTime() - 2 * (COINJOIN_QUEUE_TIMEOUT + 1)); // move back to anchor baseline |
| 103 | + dsq.nTime = GetAdjustedTime() + (COINJOIN_QUEUE_TIMEOUT + 1); |
| 104 | + BOOST_CHECK(dsq.IsTimeOutOfBounds()); |
| 105 | + |
| 106 | + // Reset mock time |
| 107 | + SetMockTime(0); |
| 108 | +} |
| 109 | + |
| 110 | +BOOST_AUTO_TEST_CASE(broadcasttx_expiry_height_logic) |
| 111 | +{ |
| 112 | + // Build a valid-looking CCoinJoinBroadcastTx with confirmed height |
| 113 | + CCoinJoinBroadcastTx dstx; |
| 114 | + { |
| 115 | + CMutableTransaction mtx; |
| 116 | + const int participants = std::max(3, CoinJoin::GetMinPoolParticipants()); |
| 117 | + for (int i = 0; i < participants; ++i) { |
| 118 | + mtx.vin.emplace_back(COutPoint(uint256::TWO, i)); |
| 119 | + mtx.vout.emplace_back(CoinJoin::GetSmallestDenomination(), P2PKHScript(static_cast<uint8_t>(i))); |
| 120 | + } |
| 121 | + dstx.tx = MakeTransactionRef(mtx); |
| 122 | + dstx.m_protxHash = uint256::ONE; |
| 123 | + // mark as confirmed at height 100 |
| 124 | + dstx.SetConfirmedHeight(100); |
| 125 | + } |
| 126 | + |
| 127 | + // Minimal CBlockIndex with required fields |
| 128 | + // Create a minimal block index to satisfy the interface |
| 129 | + CBlockIndex index; |
| 130 | + uint256 blk_hash = uint256S("03"); |
| 131 | + index.nHeight = 125; // 125 - 100 == 25 > 24 → expired by height |
| 132 | + index.phashBlock = &blk_hash; |
| 133 | + BOOST_CHECK(dstx.IsExpired(&index, *Assert(m_node.llmq_ctx->clhandler))); |
| 134 | +} |
| 135 | + |
| 136 | +BOOST_AUTO_TEST_SUITE_END() |
0 commit comments