Skip to content

Commit 55fd182

Browse files
Merge dashpay#6803: test: add CoinJoin DSTX and inouts tests
f038f0a chore: run clang-format (pasta) c0df94c refactor: use wildcard pattern for CoinJoin test files in non-backported.txt (pasta) da4b39b test: address review feedback for CoinJoin test improvements (pasta) dc9a4f8 test: add CoinJoin base manager and queue tests (pasta) 1951a27 feat: add GetConfirmedHeight method to CoinJoin class (pasta) 0e154f7 test: enhance CoinJoin DSTX height management tests (pasta) 4b00a0e test: add CoinJoin DSTX and inouts tests (pasta) Pull request description: ## Issue being fixed or feature implemented This commit introduces two new test files: `coinjoin_dstxmanager_tests.cpp` and `coinjoin_inouts_tests.cpp`. The `coinjoin_dstxmanager_tests` file includes tests for managing CoinJoin broadcast transactions, while the `coinjoin_inouts_tests` file focuses on validating the structure and behavior of CoinJoin transactions. Both files are added to the test suite in the Makefile for comprehensive testing coverage. ## What was done? ## How Has This Been Tested? Tests pass locally ## Breaking Changes None ## Checklist: _Go over all the following points, and put an `x` in all the boxes that apply._ - [ ] I have performed a self-review of my own code - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] I have added or updated relevant unit/integration/functional/e2e tests - [ ] I have made corresponding changes to the documentation - [x] I have assigned this pull request to a milestone _(for repository code-owners and collaborators only)_ ACKs for top commit: UdjinM6: utACK f038f0a knst: utACK f038f0a Tree-SHA512: 3b137eea5b804568287725daad261bab1d4948c4cd7e2775b7a0c19e7433545c1a798740ed0faaa534d187cbbcf0bafba54c0ff5547fc32cd1a921f5053f66a0
2 parents d4202b5 + f038f0a commit 55fd182

File tree

7 files changed

+358
-0
lines changed

7 files changed

+358
-0
lines changed

src/Makefile.test.include

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,10 @@ BITCOIN_TESTS =\
120120
test/fs_tests.cpp \
121121
test/getarg_tests.cpp \
122122
test/governance_validators_tests.cpp \
123+
test/coinjoin_inouts_tests.cpp \
124+
test/coinjoin_dstxmanager_tests.cpp \
125+
test/coinjoin_basemanager_tests.cpp \
126+
test/coinjoin_queue_tests.cpp \
123127
test/hash_tests.cpp \
124128
test/httpserver_tests.cpp \
125129
test/i2p_tests.cpp \

src/coinjoin/coinjoin.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,8 @@ class CCoinJoinBroadcastTx
287287
bool Sign(const CActiveMasternodeManager& mn_activeman);
288288
[[nodiscard]] bool CheckSignature(const CBLSPublicKey& blsPubKey) const;
289289

290+
// Used only for unit tests
291+
[[nodiscard]] std::optional<int> GetConfirmedHeight() const { return nConfirmedHeight; }
290292
void SetConfirmedHeight(std::optional<int> nConfirmedHeightIn) { assert(nConfirmedHeightIn == std::nullopt || *nConfirmedHeightIn > 0); nConfirmedHeight = nConfirmedHeightIn; }
291293
bool IsExpired(const CBlockIndex* pindex, const llmq::CChainLocksHandler& clhandler) const;
292294
[[nodiscard]] bool IsValidStructure() const;
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
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 <coinjoin/coinjoin.h>
8+
#include <cstddef>
9+
#include <uint256.h>
10+
11+
#include <boost/test/unit_test.hpp>
12+
13+
class TestBaseManager : public CCoinJoinBaseManager
14+
{
15+
public:
16+
void PushQueue(const CCoinJoinQueue& q)
17+
{
18+
LOCK(cs_vecqueue);
19+
vecCoinJoinQueue.push_back(q);
20+
}
21+
size_t QueueSize() const
22+
{
23+
LOCK(cs_vecqueue);
24+
return vecCoinJoinQueue.size();
25+
}
26+
void CallCheckQueue() { CheckQueue(); }
27+
};
28+
29+
BOOST_FIXTURE_TEST_SUITE(coinjoin_basemanager_tests, BasicTestingSetup)
30+
31+
static CCoinJoinQueue MakeQueue(int denom, int64_t nTime, bool fReady, const COutPoint& outpoint)
32+
{
33+
CCoinJoinQueue q;
34+
q.nDenom = denom;
35+
q.masternodeOutpoint = outpoint;
36+
q.m_protxHash = uint256::ONE;
37+
q.nTime = nTime;
38+
q.fReady = fReady;
39+
return q;
40+
}
41+
42+
BOOST_AUTO_TEST_CASE(checkqueue_removes_timeouts)
43+
{
44+
TestBaseManager man;
45+
const int denom = CoinJoin::AmountToDenomination(CoinJoin::GetSmallestDenomination());
46+
const int64_t now = GetAdjustedTime();
47+
// Non-expired
48+
man.PushQueue(MakeQueue(denom, now, false, COutPoint(uint256S("11"), 0)));
49+
// Expired (too old)
50+
man.PushQueue(MakeQueue(denom, now - COINJOIN_QUEUE_TIMEOUT - 1, false, COutPoint(uint256S("12"), 0)));
51+
52+
BOOST_CHECK_EQUAL(man.QueueSize(), 2U);
53+
man.CallCheckQueue();
54+
// One should be removed
55+
BOOST_CHECK_EQUAL(man.QueueSize(), 1U);
56+
}
57+
58+
BOOST_AUTO_TEST_CASE(getqueueitem_marks_tried_once)
59+
{
60+
TestBaseManager man;
61+
const int denom = CoinJoin::AmountToDenomination(CoinJoin::GetSmallestDenomination());
62+
const int64_t now = GetAdjustedTime();
63+
CCoinJoinQueue dsq = MakeQueue(denom, now, false, COutPoint(uint256S("21"), 0));
64+
man.PushQueue(dsq);
65+
66+
CCoinJoinQueue picked;
67+
// First retrieval should succeed
68+
BOOST_CHECK(man.GetQueueItemAndTry(picked));
69+
// No other items left to try (picked is marked tried inside)
70+
CCoinJoinQueue picked2;
71+
BOOST_CHECK(!man.GetQueueItemAndTry(picked2));
72+
}
73+
74+
BOOST_AUTO_TEST_SUITE_END()
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
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 <chain.h>
8+
#include <coinjoin/coinjoin.h>
9+
#include <coinjoin/common.h>
10+
#include <coinjoin/context.h>
11+
#include <script/script.h>
12+
#include <uint256.h>
13+
14+
#include <algorithm>
15+
#include <array>
16+
#include <vector>
17+
18+
#include <boost/test/unit_test.hpp>
19+
20+
BOOST_FIXTURE_TEST_SUITE(coinjoin_dstxmanager_tests, TestingSetup)
21+
22+
static CCoinJoinBroadcastTx MakeDSTX(int vin_vout_count = 3)
23+
{
24+
CCoinJoinBroadcastTx dstx;
25+
CMutableTransaction mtx;
26+
const int count = std::max(vin_vout_count, 1);
27+
for (int i = 0; i < count; ++i) {
28+
mtx.vin.emplace_back(COutPoint(uint256S("0a"), i));
29+
// Use denominated P2PKH outputs
30+
CScript spk;
31+
spk << OP_DUP << OP_HASH160 << std::vector<unsigned char>(20, i) << OP_EQUALVERIFY << OP_CHECKSIG;
32+
mtx.vout.emplace_back(CoinJoin::GetSmallestDenomination(), spk);
33+
}
34+
dstx.tx = MakeTransactionRef(mtx);
35+
dstx.m_protxHash = uint256::ONE;
36+
return dstx;
37+
}
38+
39+
BOOST_AUTO_TEST_CASE(add_get_dstx)
40+
{
41+
CCoinJoinBroadcastTx dstx = MakeDSTX();
42+
auto& man = *Assert(Assert(m_node.cj_ctx)->dstxman);
43+
// Not present initially
44+
BOOST_CHECK(!man.GetDSTX(dstx.tx->GetHash()));
45+
// Add
46+
man.AddDSTX(dstx);
47+
// Fetch back
48+
auto got = man.GetDSTX(dstx.tx->GetHash());
49+
BOOST_CHECK(static_cast<bool>(got));
50+
BOOST_CHECK_EQUAL(got.tx->GetHash().ToString(), dstx.tx->GetHash().ToString());
51+
}
52+
53+
BOOST_AUTO_TEST_CASE(update_heights_block_connect_disconnect)
54+
{
55+
CCoinJoinBroadcastTx dstx = MakeDSTX();
56+
auto& man = *Assert(Assert(m_node.cj_ctx)->dstxman);
57+
man.AddDSTX(dstx);
58+
59+
// Create a fake block containing the tx
60+
auto block = std::make_shared<CBlock>();
61+
block->vtx.push_back(dstx.tx);
62+
CBlockIndex index;
63+
index.nHeight = 100;
64+
uint256 bh = uint256S("0b");
65+
index.phashBlock = &bh;
66+
67+
// Height should set to 100 on connect
68+
man.BlockConnected(block, &index);
69+
{
70+
auto got = man.GetDSTX(dstx.tx->GetHash());
71+
BOOST_CHECK(static_cast<bool>(got));
72+
BOOST_CHECK(got.GetConfirmedHeight().has_value());
73+
BOOST_CHECK_EQUAL(*got.GetConfirmedHeight(), 100);
74+
}
75+
76+
// Height should clear on disconnect
77+
man.BlockDisconnected(block, nullptr);
78+
{
79+
auto got = man.GetDSTX(dstx.tx->GetHash());
80+
BOOST_CHECK(static_cast<bool>(got));
81+
BOOST_CHECK(!got.GetConfirmedHeight().has_value());
82+
}
83+
}
84+
85+
BOOST_AUTO_TEST_SUITE_END()

src/test/coinjoin_inouts_tests.cpp

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
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()

src/test/coinjoin_queue_tests.cpp

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
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 <bls/bls.h>
8+
#include <coinjoin/coinjoin.h>
9+
#include <masternode/node.h>
10+
#include <uint256.h>
11+
12+
#include <boost/test/unit_test.hpp>
13+
14+
BOOST_FIXTURE_TEST_SUITE(coinjoin_queue_tests, BasicTestingSetup)
15+
16+
static CBLSSecretKey MakeSecretKey()
17+
{
18+
// Generate a dummy operator keypair for signing
19+
CBLSSecretKey sk;
20+
sk.MakeNewKey();
21+
return sk;
22+
}
23+
24+
BOOST_AUTO_TEST_CASE(queue_sign_and_verify)
25+
{
26+
// Build active MN manager with operator key using node context wiring
27+
CActiveMasternodeManager mn_activeman(MakeSecretKey(), *Assert(m_node.connman), m_node.dmnman);
28+
29+
CCoinJoinQueue q;
30+
q.nDenom = CoinJoin::AmountToDenomination(CoinJoin::GetSmallestDenomination());
31+
q.masternodeOutpoint = COutPoint(uint256S("aa"), 1);
32+
q.m_protxHash = uint256::ONE;
33+
q.nTime = GetAdjustedTime();
34+
q.fReady = false;
35+
36+
// Sign and verify with corresponding pubkey
37+
BOOST_CHECK(q.Sign(mn_activeman));
38+
const CBLSPublicKey pub = mn_activeman.GetPubKey();
39+
BOOST_CHECK(q.CheckSignature(pub));
40+
}
41+
42+
BOOST_AUTO_TEST_CASE(queue_hashes_and_equality)
43+
{
44+
CCoinJoinQueue a, b;
45+
a.nDenom = b.nDenom = CoinJoin::AmountToDenomination(CoinJoin::GetSmallestDenomination());
46+
a.masternodeOutpoint = b.masternodeOutpoint = COutPoint(uint256S("bb"), 2);
47+
a.m_protxHash = b.m_protxHash = uint256::ONE;
48+
a.nTime = b.nTime = GetAdjustedTime();
49+
a.fReady = b.fReady = true;
50+
51+
BOOST_CHECK(a == b);
52+
BOOST_CHECK(a.GetHash() == b.GetHash());
53+
BOOST_CHECK(a.GetSignatureHash() == b.GetSignatureHash());
54+
}
55+
56+
BOOST_AUTO_TEST_SUITE_END()

test/util/data/non-backported.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ src/stats/*.cpp
3939
src/stats/*.h
4040
src/test/block_reward_reallocation_tests.cpp
4141
src/test/bls_tests.cpp
42+
src/test/coinjoin_*.cpp
4243
src/test/dip0020opcodes_tests.cpp
4344
src/test/dynamic_activation*.cpp
4445
src/test/evo*.cpp

0 commit comments

Comments
 (0)