forked from bitcoin/bitcoin
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
fix(net): bound CoinJoin message vector intake #7419
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
PastaPastaPasta
merged 3 commits into
dashpay:develop
from
thepastaclaw:fix/coinjoin-message-vector-bounds
Jul 20, 2026
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,26 +2,42 @@ | |
| // Distributed under the MIT/X11 software license, see the accompanying | ||
| // file COPYING or http://www.opensource.org/licenses/mit-license.php. | ||
|
|
||
| #include <test/util/setup_common.h> | ||
|
|
||
| #include <algorithm> | ||
| #include <array> | ||
| #include <cstdint> | ||
| #include <vector> | ||
|
|
||
| #include <active/masternode.h> | ||
| #include <bls/bls.h> | ||
| #include <chain.h> | ||
| #include <coinjoin/coinjoin.h> | ||
| #include <coinjoin/common.h> | ||
| #include <coinjoin/server.h> | ||
| #include <evo/chainhelper.h> | ||
| #include <llmq/context.h> | ||
| #include <masternode/sync.h> | ||
| #include <net.h> | ||
| #include <node/connection_types.h> | ||
| #include <protocol.h> | ||
| #include <script/script.h> | ||
| #include <streams.h> | ||
| #include <test/util/setup_common.h> | ||
| #include <uint256.h> | ||
| #include <util/check.h> | ||
| #include <util/time.h> | ||
|
|
||
| #include <boost/test/unit_test.hpp> | ||
|
|
||
| #include <algorithm> | ||
| #include <array> | ||
| #include <cstdint> | ||
| #include <memory> | ||
| #include <vector> | ||
|
|
||
| BOOST_FIXTURE_TEST_SUITE(coinjoin_inouts_tests, TestingSetup) | ||
|
|
||
| static CBLSSecretKey MakeSecretKey() | ||
| { | ||
| CBLSSecretKey sk; | ||
| sk.MakeNewKey(); | ||
| return sk; | ||
| } | ||
|
|
||
| static CScript P2PKHScript(uint8_t tag = 0x01) | ||
| { | ||
| // OP_DUP OP_HASH160 <20-byte-tag> OP_EQUALVERIFY OP_CHECKSIG | ||
|
|
@@ -155,6 +171,169 @@ BOOST_AUTO_TEST_CASE(entry_addscriptsig_matches_and_rejects) | |
| } | ||
| } | ||
|
|
||
| // Test-only subclass exposing the minimal seams needed to observe how | ||
| // ProcessDSSIGNFINALTX treats messages from participants vs. non-participants | ||
| // without standing up a full DKG-backed signing session. | ||
| class TestableCoinJoinServer : public CCoinJoinServer | ||
| { | ||
| public: | ||
| using CCoinJoinServer::CCoinJoinServer; | ||
|
|
||
| void EnterSigningState() { nState = POOL_STATE_SIGNING; } | ||
|
|
||
| void SeedParticipant(const CService& addr) EXCLUSIVE_LOCKS_REQUIRED(!cs_coinjoin) | ||
| { | ||
| CCoinJoinEntry entry; | ||
| entry.addr = addr; | ||
| LOCK(cs_coinjoin); | ||
| vecEntries.push_back(std::move(entry)); | ||
| } | ||
| }; | ||
|
|
||
| static std::unique_ptr<CNode> MakePeer(NodeId id, uint32_t ipv4) | ||
| { | ||
| in_addr peer_in_addr{}; | ||
| peer_in_addr.s_addr = htonl(ipv4); | ||
| auto peer = std::make_unique<CNode>(id, | ||
| /*sock=*/nullptr, | ||
| /*addrIn=*/CAddress{CService{peer_in_addr, 8333}, NODE_NETWORK}, | ||
| /*nKeyedNetGroupIn=*/0, | ||
| /*nLocalHostNonceIn=*/0, | ||
| /*addrBindIn=*/CAddress{}, | ||
| /*addrNameIn=*/std::string{}, | ||
| /*conn_type_in=*/ConnectionType::INBOUND, | ||
| /*inbound_onion=*/false); | ||
| peer->nVersion = PROTOCOL_VERSION; | ||
| peer->SetCommonVersion(PROTOCOL_VERSION); | ||
| return peer; | ||
| } | ||
|
|
||
| BOOST_AUTO_TEST_CASE(server_signfinaltx_nonparticipant_cannot_abort_session) | ||
| { | ||
| BOOST_REQUIRE(m_node.mn_sync); | ||
| m_node.mn_sync->SwitchToNextAsset(); | ||
| BOOST_REQUIRE(m_node.mn_sync->IsBlockchainSynced()); | ||
|
|
||
| CActiveMasternodeManager mn_activeman(*Assert(m_node.connman), *Assert(m_node.dmnman), MakeSecretKey()); | ||
| TestableCoinJoinServer server(m_node.peerman.get(), *Assert(m_node.chainman), *Assert(m_node.connman), | ||
| *Assert(m_node.dmnman), *Assert(m_node.dstxman), *Assert(m_node.mn_metaman), | ||
| *Assert(m_node.mempool), mn_activeman, *Assert(m_node.mn_sync), | ||
| *Assert(m_node.llmq_ctx->isman)); | ||
|
Comment on lines
+213
to
+221
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: consider moving this to the common helper ; and inline MakeSecretKey there |
||
|
|
||
| // Seed an active signing session with one participant. That participant's | ||
| // addr is deliberately not registered with connman -- a session-wide | ||
| // RelayStatus(REJECTED) would therefore see nDisconnected == vecEntries.size() | ||
| // and reset the pool state to POOL_STATE_IDLE via SetNull(). | ||
| auto participant = MakePeer(/*id=*/7, /*ipv4=*/0x0a000001); | ||
| server.SeedParticipant(participant->addr); | ||
| server.EnterSigningState(); | ||
| BOOST_REQUIRE_EQUAL(server.GetState(), int{POOL_STATE_SIGNING}); | ||
|
|
||
| auto nonparticipant = MakePeer(/*id=*/42, /*ipv4=*/0x01020304); | ||
| BOOST_REQUIRE(!(nonparticipant->addr == participant->addr)); | ||
|
|
||
| const size_t max_txins{CoinJoin::GetMaxPoolInputOutputCount()}; | ||
| CDataStream stream{SER_NETWORK, PROTOCOL_VERSION}; | ||
| WriteCompactSize(stream, max_txins + 1); | ||
|
|
||
| BOOST_CHECK_NO_THROW(server.ProcessMessage(*nonparticipant, NetMsgType::DSSIGNFINALTX, stream)); | ||
|
|
||
| // The non-participant must be rejected without touching session-wide state: | ||
| // the stream body must not have been read past the compact-size prefix, and | ||
| // the pool must still be in POOL_STATE_SIGNING with its entry intact. | ||
| BOOST_CHECK_EQUAL(stream.size(), GetSizeOfCompactSize(max_txins + 1)); | ||
| BOOST_CHECK_EQUAL(server.GetState(), int{POOL_STATE_SIGNING}); | ||
| BOOST_CHECK_EQUAL(server.GetEntriesCount(), 1); | ||
| } | ||
|
|
||
| BOOST_AUTO_TEST_CASE(server_signfinaltx_participant_oversized_count_is_rejected_locally) | ||
| { | ||
| BOOST_REQUIRE(m_node.mn_sync); | ||
| m_node.mn_sync->SwitchToNextAsset(); | ||
| BOOST_REQUIRE(m_node.mn_sync->IsBlockchainSynced()); | ||
|
|
||
| CActiveMasternodeManager mn_activeman(*Assert(m_node.connman), *Assert(m_node.dmnman), MakeSecretKey()); | ||
| TestableCoinJoinServer server(m_node.peerman.get(), *Assert(m_node.chainman), *Assert(m_node.connman), | ||
| *Assert(m_node.dmnman), *Assert(m_node.dstxman), *Assert(m_node.mn_metaman), | ||
| *Assert(m_node.mempool), mn_activeman, *Assert(m_node.mn_sync), | ||
| *Assert(m_node.llmq_ctx->isman)); | ||
|
|
||
| // Same setup, but this time the oversized DSSIGNFINALTX comes from the | ||
| // session participant itself. It must still be rejected without materializing | ||
| // the txin vector and without collapsing the session for everyone else. | ||
| auto participant = MakePeer(/*id=*/7, /*ipv4=*/0x0a000001); | ||
| server.SeedParticipant(participant->addr); | ||
| server.EnterSigningState(); | ||
|
|
||
| const size_t max_txins{CoinJoin::GetMaxPoolInputOutputCount()}; | ||
| CDataStream stream{SER_NETWORK, PROTOCOL_VERSION}; | ||
| WriteCompactSize(stream, max_txins + 1); | ||
|
|
||
| BOOST_CHECK_NO_THROW(server.ProcessMessage(*participant, NetMsgType::DSSIGNFINALTX, stream)); | ||
| BOOST_CHECK_EQUAL(stream.size(), 0U); | ||
| BOOST_CHECK_EQUAL(server.GetState(), int{POOL_STATE_SIGNING}); | ||
| BOOST_CHECK_EQUAL(server.GetEntriesCount(), 1); | ||
|
|
||
| // A count beyond the generic CompactSize cap is a malformed message, not a | ||
| // CoinJoin-level violation: it throws the standard deserialization error out | ||
| // to net processing. No txin is materialized and the session is left intact, | ||
| // so honest participants are unaffected. | ||
| CDataStream huge_stream{SER_NETWORK, PROTOCOL_VERSION}; | ||
| WriteCompactSize(huge_stream, uint64_t{MAX_SIZE} + 1); | ||
|
|
||
| BOOST_CHECK_THROW(server.ProcessMessage(*participant, NetMsgType::DSSIGNFINALTX, huge_stream), | ||
| std::ios_base::failure); | ||
| BOOST_CHECK_EQUAL(server.GetState(), int{POOL_STATE_SIGNING}); | ||
| BOOST_CHECK_EQUAL(server.GetEntriesCount(), 1); | ||
| } | ||
|
|
||
| BOOST_AUTO_TEST_CASE(entry_deserializes_vectors_through_wire_cap) | ||
| { | ||
| const size_t wire_cap{CoinJoin::GetMaxPoolInputOutputCount()}; | ||
| BOOST_REQUIRE_GT(wire_cap, COINJOIN_ENTRY_MAX_SIZE); | ||
|
|
||
| for (const size_t count : {size_t{0}, COINJOIN_ENTRY_MAX_SIZE, COINJOIN_ENTRY_MAX_SIZE + 1, wire_cap}) { | ||
| BOOST_TEST_CONTEXT("count=" << count) | ||
| { | ||
| CCoinJoinEntry entry; | ||
| entry.vecTxDSIn.resize(count); | ||
| entry.vecTxOut.resize(count); | ||
|
|
||
| CDataStream stream{SER_NETWORK, PROTOCOL_VERSION}; | ||
| stream << entry; | ||
|
|
||
| CCoinJoinEntry roundtripped; | ||
| BOOST_CHECK_NO_THROW(stream >> roundtripped); | ||
| BOOST_CHECK_EQUAL(roundtripped.vecTxDSIn.size(), count); | ||
| BOOST_CHECK_EQUAL(roundtripped.vecTxOut.size(), count); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| BOOST_AUTO_TEST_CASE(entry_rejects_inputs_above_wire_cap_before_materializing) | ||
| { | ||
| const size_t wire_cap{CoinJoin::GetMaxPoolInputOutputCount()}; | ||
| CDataStream stream{SER_NETWORK, PROTOCOL_VERSION}; | ||
| WriteCompactSize(stream, wire_cap + 1); | ||
|
|
||
| CCoinJoinEntry entry; | ||
| BOOST_CHECK_THROW(stream >> entry, std::ios_base::failure); | ||
| BOOST_CHECK(entry.vecTxDSIn.empty()); | ||
| } | ||
|
|
||
| BOOST_AUTO_TEST_CASE(entry_rejects_outputs_above_wire_cap_before_materializing) | ||
| { | ||
| const size_t wire_cap{CoinJoin::GetMaxPoolInputOutputCount()}; | ||
| CDataStream stream{SER_NETWORK, PROTOCOL_VERSION}; | ||
| WriteCompactSize(stream, 0); | ||
| stream << MakeTransactionRef(CMutableTransaction{}); | ||
| WriteCompactSize(stream, wire_cap + 1); | ||
|
|
||
| CCoinJoinEntry entry; | ||
| BOOST_CHECK_THROW(stream >> entry, std::ios_base::failure); | ||
| BOOST_CHECK(entry.vecTxOut.empty()); | ||
| } | ||
|
|
||
| BOOST_AUTO_TEST_CASE(queue_timeout_bounds) | ||
| { | ||
| CCoinJoinQueue dsq; | ||
|
|
||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.