-
Notifications
You must be signed in to change notification settings - Fork 1.2k
refactor: separate CoinJoin offender selection policy #7566
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 all commits
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 | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -309,7 +309,7 @@ void CCoinJoinServer::CheckPool() | |||||||||||||||
| if (nState == POOL_STATE_ACCEPTING_ENTRIES && CCoinJoinServer::HasTimedOut() && | ||||||||||||||||
| GetEntriesCount() >= CoinJoin::GetMinPoolParticipants()) { | ||||||||||||||||
| // Punish misbehaving participants | ||||||||||||||||
| ChargeFees(); | ||||||||||||||||
| ChargeFees(FeePolicy::PROBABILISTIC); | ||||||||||||||||
| // Try to complete this session ignoring the misbehaving ones | ||||||||||||||||
| CreateFinalTransaction(); | ||||||||||||||||
| return; | ||||||||||||||||
|
|
@@ -416,63 +416,85 @@ void CCoinJoinServer::CommitFinalTransaction() | |||||||||||||||
| // transaction for the client to be able to enter the pool. This transaction is kept by the Masternode | ||||||||||||||||
| // until the transaction is either complete or fails. | ||||||||||||||||
| // | ||||||||||||||||
| void CCoinJoinServer::ChargeFees() const | ||||||||||||||||
| CTransactionRef CCoinJoinServer::SelectCollateralToCharge(FeePolicy policy) const | ||||||||||||||||
| { | ||||||||||||||||
| AssertLockNotHeld(cs_coinjoin); | ||||||||||||||||
|
|
||||||||||||||||
| //we don't need to charge collateral for every offence. | ||||||||||||||||
| if (GetRand<int>(/*nMax=*/100) > 33) return; | ||||||||||||||||
| AssertLockHeld(cs_coinjoin); | ||||||||||||||||
|
|
||||||||||||||||
| std::vector<CTransactionRef> vecOffendersCollaterals; | ||||||||||||||||
|
|
||||||||||||||||
| if (nState == POOL_STATE_ACCEPTING_ENTRIES) { | ||||||||||||||||
| LOCK(cs_coinjoin); | ||||||||||||||||
| for (const auto& txCollateral : vecSessionCollaterals) { | ||||||||||||||||
| bool fFound = std::ranges::any_of(vecEntries, [&txCollateral](const auto& entry) { | ||||||||||||||||
| return *entry.txCollateral == *txCollateral; | ||||||||||||||||
| }); | ||||||||||||||||
|
|
||||||||||||||||
| // This queue entry didn't send us the promised transaction | ||||||||||||||||
| if (!fFound) { | ||||||||||||||||
| LogPrint(BCLog::COINJOIN, /* Continued */ | ||||||||||||||||
| "CCoinJoinServer::ChargeFees -- found uncooperative node (didn't send transaction), found " | ||||||||||||||||
| "offence\n"); | ||||||||||||||||
| vecOffendersCollaterals.push_back(txCollateral); | ||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| if (nState == POOL_STATE_SIGNING) { | ||||||||||||||||
| } else if (nState == POOL_STATE_SIGNING) { | ||||||||||||||||
| // who didn't sign? | ||||||||||||||||
| LOCK(cs_coinjoin); | ||||||||||||||||
| for (const auto& entry : vecEntries) { | ||||||||||||||||
| for (const auto& txdsin : entry.vecTxDSIn) { | ||||||||||||||||
| if (!txdsin.fHasSig) { | ||||||||||||||||
| LogPrint(BCLog::COINJOIN, /* Continued */ | ||||||||||||||||
| "CCoinJoinServer::ChargeFees -- found uncooperative node (didn't sign), found offence\n"); | ||||||||||||||||
| vecOffendersCollaterals.push_back(entry.txCollateral); | ||||||||||||||||
| } | ||||||||||||||||
| bool fHasUnsignedInput = std::ranges::any_of(entry.vecTxDSIn, [](const auto& txdsin) { | ||||||||||||||||
| return !txdsin.fHasSig; | ||||||||||||||||
| }); | ||||||||||||||||
| if (fHasUnsignedInput) { | ||||||||||||||||
| vecOffendersCollaterals.push_back(entry.txCollateral); | ||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| // no offences found | ||||||||||||||||
| if (vecOffendersCollaterals.empty()) return; | ||||||||||||||||
| if (vecOffendersCollaterals.empty()) return nullptr; | ||||||||||||||||
|
|
||||||||||||||||
| if (policy == FeePolicy::PROBABILISTIC) { | ||||||||||||||||
| // we don't need to charge collateral for every offence. | ||||||||||||||||
| if (GetRand<int>(/*nMax=*/100) > 33) return nullptr; | ||||||||||||||||
|
|
||||||||||||||||
| //mostly offending? Charge sometimes | ||||||||||||||||
| if (vecOffendersCollaterals.size() >= vecSessionCollaterals.size() - 1 && GetRand<int>(/*nMax=*/100) > 33) return; | ||||||||||||||||
| // mostly offending? Charge sometimes | ||||||||||||||||
| if (vecOffendersCollaterals.size() >= vecSessionCollaterals.size() - 1 && GetRand<int>(/*nMax=*/100) > 33) return nullptr; | ||||||||||||||||
|
|
||||||||||||||||
| //everyone is an offender? That's not right | ||||||||||||||||
| if (vecOffendersCollaterals.size() >= vecSessionCollaterals.size()) return; | ||||||||||||||||
| // everyone is an offender? That's not right | ||||||||||||||||
| if (vecOffendersCollaterals.size() >= vecSessionCollaterals.size()) return nullptr; | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| //charge one of the offenders randomly | ||||||||||||||||
| // charge one of the offenders randomly | ||||||||||||||||
| Shuffle(vecOffendersCollaterals.begin(), vecOffendersCollaterals.end(), FastRandomContext()); | ||||||||||||||||
|
|
||||||||||||||||
| if (nState == POOL_STATE_ACCEPTING_ENTRIES || nState == POOL_STATE_SIGNING) { | ||||||||||||||||
| LogPrint(BCLog::COINJOIN, /* Continued */ | ||||||||||||||||
| "CCoinJoinServer::ChargeFees -- found uncooperative node (didn't %s transaction), charging fees: %s", | ||||||||||||||||
| (nState == POOL_STATE_SIGNING) ? "sign" : "send", vecOffendersCollaterals[0]->ToString()); | ||||||||||||||||
| ConsumeCollateral(vecOffendersCollaterals[0]); | ||||||||||||||||
| CTransactionRef selectedCollateral = vecOffendersCollaterals[0]; | ||||||||||||||||
|
|
||||||||||||||||
| if (policy == FeePolicy::PROBABILISTIC) { | ||||||||||||||||
| LogPrint(BCLog::COINJOIN, | ||||||||||||||||
| "CCoinJoinServer::SelectCollateralToCharge -- selected non-submitting participant for probabilistic penalty. state=%s, participants=%d, offenders=%d, txid=%s\n", | ||||||||||||||||
| GetStateString(), vecSessionCollaterals.size(), vecOffendersCollaterals.size(), selectedCollateral->GetHash().ToString()); | ||||||||||||||||
|
Comment on lines
+467
to
+470
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. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Log the correct offender type in the signing state.
Use generic “offending participant” text, or derive the text from 🤖 Prompt for AI Agents
Comment on lines
+467
to
+470
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. 💬 Nitpick: Describe signing-stage offenders accurately in the log The probabilistic policy is used in both accepting-entries and signing states. In the signing state, the selected participant submitted an entry but left at least one input unsigned, so calling it a
Suggested change
source: ['coderabbit'] |
||||||||||||||||
| } else if (policy == FeePolicy::GUARANTEED_ON_ABORT) { | ||||||||||||||||
| if (vecOffendersCollaterals.size() >= vecSessionCollaterals.size()) { | ||||||||||||||||
| LogPrint(BCLog::COINJOIN, | ||||||||||||||||
| "CCoinJoinServer::SelectCollateralToCharge -- all participants missing or uncooperative, selected participant for failed-session fee. state=%s, participants=%d, offenders=%d, txid=%s\n", | ||||||||||||||||
| GetStateString(), vecSessionCollaterals.size(), vecOffendersCollaterals.size(), selectedCollateral->GetHash().ToString()); | ||||||||||||||||
| } else { | ||||||||||||||||
| LogPrint(BCLog::COINJOIN, | ||||||||||||||||
| "CCoinJoinServer::SelectCollateralToCharge -- selected participant for failed-session fee. state=%s, participants=%d, offenders=%d, txid=%s\n", | ||||||||||||||||
| GetStateString(), vecSessionCollaterals.size(), vecOffendersCollaterals.size(), selectedCollateral->GetHash().ToString()); | ||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| return selectedCollateral; | ||||||||||||||||
|
Comment on lines
+419
to
+483
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. 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift Add targeted tests for fee-policy selection. This PR adds state-dependent offender selection and two fee policies. It contains no C++ test change. Add tests for accepting and signing states. Verify that selection only returns offender collateral, signing entries contribute at most one collateral, and As per coding guidelines, “Choose and add targeted C++ unit tests for changed behavior.” 🤖 Prompt for AI AgentsSource: Coding guidelines |
||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| void CCoinJoinServer::ChargeFees(FeePolicy policy) const | ||||||||||||||||
| { | ||||||||||||||||
| AssertLockNotHeld(cs_coinjoin); | ||||||||||||||||
|
|
||||||||||||||||
| CTransactionRef txCollateralToConsume; | ||||||||||||||||
| { | ||||||||||||||||
| LOCK(cs_coinjoin); | ||||||||||||||||
| txCollateralToConsume = SelectCollateralToCharge(policy); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| if (txCollateralToConsume) { | ||||||||||||||||
| ConsumeCollateral(txCollateralToConsume); | ||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
|
|
||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -52,17 +52,27 @@ class CCoinJoinServer : public CCoinJoinBaseSession, public NetHandler | |
|
|
||
| bool fUnitTest; | ||
|
|
||
| public: | ||
| enum class FeePolicy { | ||
| PROBABILISTIC, | ||
| GUARANTEED_ON_ABORT, | ||
| }; | ||
|
|
||
| protected: | ||
| /// Select a collateral to charge based on offender discovery and fee policy | ||
| CTransactionRef SelectCollateralToCharge(FeePolicy policy) const EXCLUSIVE_LOCKS_REQUIRED(cs_coinjoin); | ||
|
|
||
| /// Add a clients entry to the pool | ||
| bool AddEntry(const CCoinJoinEntry& entry, PoolMessage& nMessageIDRet) EXCLUSIVE_LOCKS_REQUIRED(!cs_coinjoin); | ||
| /// Add signature to a txin | ||
| bool AddScriptSig(const CTxIn& txin) EXCLUSIVE_LOCKS_REQUIRED(!cs_coinjoin); | ||
|
|
||
| /// Charge fees to bad actors (Charge clients a fee if they're abusive) | ||
| void ChargeFees() const EXCLUSIVE_LOCKS_REQUIRED(!cs_coinjoin); | ||
| void ChargeFees(FeePolicy policy = FeePolicy::PROBABILISTIC) const EXCLUSIVE_LOCKS_REQUIRED(!cs_coinjoin); | ||
|
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.
Useful? React with 👍 / 👎. |
||
| /// Rarely charge fees to pay miners | ||
| void ChargeRandomFees() const; | ||
| /// Consume collateral in cases when peer misbehaved | ||
| void ConsumeCollateral(const CTransactionRef& txref) const; | ||
| virtual void ConsumeCollateral(const CTransactionRef& txref) const; | ||
|
|
||
| /// Check for process | ||
| void CheckPool(); | ||
|
|
||
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.
🟡 Suggestion: Add regression coverage for offender selection policies
This is the PR's principal behavior change: a signing entry containing several unsigned inputs now contributes only one collateral candidate instead of receiving proportionally greater selection weight. The PR changes no tests, and the existing
coinjoin_inouts_testsdo not callSelectCollateralToCharge(), so restoring the old per-input loop would pass the current suite. Add focused C++ tests for accepting and signing states, including entries with different unsigned-input counts, selection exclusively from offenders, the no-offender case, guaranteed selection when all participants offend, and the deterministic probabilistic-policy exemptions.source: ['codex', 'coderabbit']