-
Notifications
You must be signed in to change notification settings - Fork 1.2k
fix: bind CoinJoin entry admission to session snapshot #7599
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
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -200,7 +200,7 @@ void CCoinJoinServer::ProcessDSQUEUE(NodeId from, CDataStream& vRecv) | |
| void CCoinJoinServer::ProcessDSVIN(CNode& peer, CDataStream& vRecv) | ||
| { | ||
| //do we have enough users in the current session? | ||
| if (!IsSessionReady()) { | ||
| if (!WITH_LOCK(cs_coinjoin, return IsSessionReady())) { | ||
| LogPrint(BCLog::COINJOIN, "DSVIN -- session not complete!\n"); | ||
| PushStatus(peer, STATUS_REJECTED, ERR_SESSION); | ||
| return; | ||
|
|
@@ -620,10 +620,21 @@ bool CCoinJoinServer::AddEntry(const CCoinJoinEntry& entry, PoolMessage& nMessag | |
| { | ||
| AssertLockNotHeld(cs_coinjoin); | ||
|
|
||
| if (size_t(GetEntriesCount()) >= vecSessionCollaterals.size()) { | ||
| LogPrint(BCLog::COINJOIN, "CCoinJoinServer::%s -- ERROR: entries is full!\n", __func__); | ||
| nMessageIDRet = ERR_ENTRIES_FULL; | ||
| return false; | ||
| int session_id; | ||
| int session_denom; | ||
| { | ||
| LOCK(cs_coinjoin); | ||
| if (nSessionID == 0 || nState != POOL_STATE_ACCEPTING_ENTRIES) { | ||
| nMessageIDRet = ERR_SESSION; | ||
| return false; | ||
| } | ||
| if (size_t(GetEntriesCountLocked()) >= vecSessionCollaterals.size()) { | ||
| LogPrint(BCLog::COINJOIN, "CCoinJoinServer::%s -- ERROR: entries is full!\n", __func__); | ||
| nMessageIDRet = ERR_ENTRIES_FULL; | ||
| return false; | ||
| } | ||
| session_id = nSessionID; | ||
| session_denom = nSessionDenom; | ||
| } | ||
|
|
||
| if (entry.vecTxDSIn.size() > COINJOIN_ENTRY_MAX_SIZE || entry.vecTxOut.size() > COINJOIN_ENTRY_MAX_SIZE) { | ||
|
|
@@ -635,11 +646,13 @@ bool CCoinJoinServer::AddEntry(const CCoinJoinEntry& entry, PoolMessage& nMessag | |
| CTransactionRef txCollateralToConsume; | ||
| { | ||
| LOCK(cs_coinjoin); | ||
| const auto it = std::ranges::find_if(vecSessionCollaterals, [&entry](const auto& txCollateral) { | ||
| return *entry.txCollateral == *txCollateral; | ||
| }); | ||
| if (it != vecSessionCollaterals.end()) { | ||
| txCollateralToConsume = *it; | ||
| if (IsCurrentSession(session_id, session_denom, POOL_STATE_ACCEPTING_ENTRIES)) { | ||
| const auto it = std::ranges::find_if(vecSessionCollaterals, [&entry](const auto& txCollateral) { | ||
| return *entry.txCollateral == *txCollateral; | ||
| }); | ||
| if (it != vecSessionCollaterals.end()) { | ||
| txCollateralToConsume = *it; | ||
| } | ||
| } | ||
| } | ||
| if (txCollateralToConsume) { | ||
|
|
@@ -655,34 +668,62 @@ bool CCoinJoinServer::AddEntry(const CCoinJoinEntry& entry, PoolMessage& nMessag | |
| } | ||
|
|
||
| std::vector<CTxIn> vin; | ||
| vin.reserve(entry.vecTxDSIn.size()); | ||
| for (const auto& txin : entry.vecTxDSIn) { | ||
| LogPrint(BCLog::COINJOIN, "CCoinJoinServer::%s -- txin=%s\n", __func__, txin.ToString()); | ||
| LOCK(cs_coinjoin); | ||
| for (const auto& inner_entry : vecEntries) { | ||
| if (std::ranges::any_of(inner_entry.vecTxDSIn, | ||
| [&txin](const auto& txdsin) { return txdsin.prevout == txin.prevout; })) { | ||
| LogPrint(BCLog::COINJOIN, "CCoinJoinServer::%s -- ERROR: already have this txin in entries\n", __func__); | ||
| nMessageIDRet = ERR_ALREADY_HAVE; | ||
| // Two peers sent the same input? Can't really say who is the malicious one here, | ||
| // could be that someone is picking someone else's inputs randomly trying to force | ||
| // collateral consumption. Do not punish. | ||
| return false; | ||
| } | ||
| } | ||
| vin.emplace_back(txin); | ||
| } | ||
|
|
||
| bool fConsumeCollateral{false}; | ||
| if (!IsValidInOuts(m_chainman.ActiveChainstate(), m_isman, mempool, vin, entry.vecTxOut, nMessageIDRet, | ||
| &fConsumeCollateral)) { | ||
| if (!IsValidInOuts(m_chainman.ActiveChainstate(), m_isman, mempool, vin, entry.vecTxOut, session_denom, | ||
| nMessageIDRet, &fConsumeCollateral)) { | ||
| LogPrint(BCLog::COINJOIN, "CCoinJoinServer::%s -- ERROR! IsValidInOuts() failed: %s\n", __func__, CoinJoin::GetMessageByID(nMessageIDRet).translated); | ||
| if (fConsumeCollateral) { | ||
| ConsumeCollateral(entry.txCollateral); | ||
| CTransactionRef txCollateralToConsume; | ||
| { | ||
| LOCK(cs_coinjoin); | ||
| if (IsCurrentSession(session_id, session_denom, POOL_STATE_ACCEPTING_ENTRIES)) { | ||
| const auto it = std::ranges::find_if(vecSessionCollaterals, [&entry](const auto& txCollateral) { | ||
| return *entry.txCollateral == *txCollateral; | ||
| }); | ||
| if (it != vecSessionCollaterals.end()) { | ||
| txCollateralToConsume = *it; | ||
| } | ||
| } | ||
| } | ||
| if (txCollateralToConsume) { | ||
| ConsumeCollateral(txCollateralToConsume); | ||
| } | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| WITH_LOCK(cs_coinjoin, vecEntries.push_back(entry)); | ||
| { | ||
| LOCK(cs_coinjoin); | ||
| if (!IsCurrentSession(session_id, session_denom, POOL_STATE_ACCEPTING_ENTRIES)) { | ||
| nMessageIDRet = ERR_SESSION; | ||
| return false; | ||
| } | ||
| if (size_t(GetEntriesCountLocked()) >= vecSessionCollaterals.size()) { | ||
|
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: avoid using C-style casts in C++ code
Member
Author
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. not a c style cast
Member
Author
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. for reference; size_t() / size_t{} are well defined and generally ok; (size_t)() isn't |
||
| nMessageIDRet = ERR_ENTRIES_FULL; | ||
| return false; | ||
| } | ||
| for (const auto& txin : vin) { | ||
| for (const auto& inner_entry : vecEntries) { | ||
| if (std::ranges::any_of(inner_entry.vecTxDSIn, | ||
| [&txin](const auto& txdsin) { return txdsin.prevout == txin.prevout; })) { | ||
| LogPrint(BCLog::COINJOIN, "CCoinJoinServer::%s -- ERROR: already have this txin in entries\n", | ||
| __func__); | ||
| nMessageIDRet = ERR_ALREADY_HAVE; | ||
| // Two peers sent the same input? Can't really say who is the malicious one here, | ||
| // could be that someone is picking someone else's inputs randomly trying to force | ||
| // collateral consumption. Do not punish. | ||
| return false; | ||
| } | ||
| } | ||
| } | ||
| vecEntries.push_back(entry); | ||
| } | ||
|
|
||
| LogPrint(BCLog::COINJOIN, "CCoinJoinServer::%s -- adding entry %d of %d required\n", __func__, GetEntriesCount(), CoinJoin::GetMaxPoolParticipants()); | ||
| nMessageIDRet = MSG_ENTRIES_ADDED; | ||
|
|
@@ -767,6 +808,12 @@ void CCoinJoinServer::CommitSessionCollateral(const CMutableTransaction& txColla | |
| } | ||
| } | ||
|
|
||
| bool CCoinJoinServer::IsCurrentSession(int session_id, int session_denom, PoolState state) const | ||
| { | ||
| AssertLockHeld(cs_coinjoin); | ||
| return nSessionID == session_id && nSessionDenom == session_denom && nState == state; | ||
| } | ||
|
|
||
| bool CCoinJoinServer::CreateNewSession(const CCoinJoinAccept& dsa, PoolMessage& nMessageIDRet) | ||
| { | ||
| if (nSessionID != 0) return false; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,6 +20,7 @@ | |
| #include <uint256.h> | ||
| #include <util/check.h> | ||
| #include <util/time.h> | ||
| #include <validation.h> | ||
|
|
||
| #include <boost/test/unit_test.hpp> | ||
|
|
||
|
|
@@ -188,6 +189,13 @@ class TestableCoinJoinServer : public CCoinJoinServer | |
| LOCK(cs_coinjoin); | ||
| vecEntries.push_back(std::move(entry)); | ||
| } | ||
|
|
||
| bool ValidateInOuts(Chainstate& active_chainstate, const llmq::CInstantSendManager& isman, | ||
|
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: this helper is quite useless, consider inlining it
Member
Author
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. Removed the wrapper in ca442a6 — the test now calls the static helper directly. 🤖 Posted autonomously by Claude on behalf of pasta. |
||
| const CTxMemPool& mempool, const std::vector<CTxIn>& vin, const std::vector<CTxOut>& vout, | ||
| int session_denom, PoolMessage& message, bool& consume_collateral) | ||
| { | ||
| return IsValidInOuts(active_chainstate, isman, mempool, vin, vout, session_denom, message, &consume_collateral); | ||
| } | ||
| }; | ||
|
|
||
| static std::unique_ptr<CNode> MakePeer(NodeId id, uint32_t ipv4) | ||
|
|
@@ -287,6 +295,26 @@ BOOST_AUTO_TEST_CASE(server_signfinaltx_participant_oversized_count_is_rejected_ | |
| BOOST_CHECK_EQUAL(server.GetEntriesCount(), 1); | ||
| } | ||
|
|
||
| BOOST_AUTO_TEST_CASE(server_validation_uses_session_denom_snapshot) | ||
| { | ||
| 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)); | ||
|
|
||
| const int session_denom{CoinJoin::AmountToDenomination(CoinJoin::GetSmallestDenomination())}; | ||
| const std::vector<CTxIn> vin{CTxIn{COutPoint{uint256::ONE, 0}}}; | ||
| const std::vector<CTxOut> vout{CTxOut{CoinJoin::GetSmallestDenomination(), P2PKHScript()}}; | ||
| PoolMessage message{MSG_NOERR}; | ||
| bool consume_collateral{false}; | ||
|
|
||
| BOOST_CHECK(!server.ValidateInOuts(Assert(m_node.chainman)->ActiveChainstate(), *Assert(m_node.llmq_ctx->isman), | ||
| *Assert(m_node.mempool), vin, vout, session_denom, message, consume_collateral)); | ||
| BOOST_CHECK_EQUAL(message, ERR_MISSING_TX); | ||
| BOOST_CHECK(!consume_collateral); | ||
|
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. 🟡 Suggestion: Exercise the session race rather than only the validation helper This test calls the static IsValidInOuts() helper with an explicit denomination, so it confirms that the helper no longer reads mutable session state. It never calls AddEntry(), however, and would still pass if AddEntry() stopped capturing the denomination or if the authoritative session, capacity, or input-uniqueness checks at server.cpp:701-725 were removed. Add a deterministic test seam that pauses entry validation after the initial snapshot, resets or advances the session or admits a competing entry, and then verifies that the stale entry is rejected without being appended or charged against the replacement session. source: ['codex']
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. I'd suggest to refactor this regressions test and cut all harness ; just direct call of
Member
Author
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. Done in ca442a6: dropped the 🤖 Posted autonomously by Claude on behalf of pasta. |
||
| } | ||
|
|
||
| BOOST_AUTO_TEST_CASE(entry_deserializes_vectors_through_wire_cap) | ||
| { | ||
| const size_t wire_cap{CoinJoin::GetMaxPoolInputOutputCount()}; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: it's moved code, but generally avoid using C-style casts
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This ISN'T a C style cast