Skip to content

fix: charge one collateral when a CoinJoin session aborts - #7567

Closed
PastaPastaPasta wants to merge 2 commits into
dashpay:developfrom
PastaPastaPasta:fix/coinjoin-abort-fee
Closed

fix: charge one collateral when a CoinJoin session aborts#7567
PastaPastaPasta wants to merge 2 commits into
dashpay:developfrom
PastaPastaPasta:fix/coinjoin-abort-fee

Conversation

@PastaPastaPasta

Copy link
Copy Markdown
Member

Depends on #7566

Issue being fixed or feature implemented

When a CoinJoin mixing session aborts due to non-cooperation (missing entries or missing signatures), ChargeFees() previously subjected collateral selection to probabilistic gates and an "everyone is an offender" exemption. When all participants failed to submit or sign, zero fees were charged, allowing attackers to abort sessions without penalty.

What was done?

  • Updated CheckTimeout() to perform offender selection via FeePolicy::GUARANTEED_ON_ABORT and session state reset (SetNull()) atomically under cs_coinjoin.
  • Released cs_coinjoin before calling ConsumeCollateral().
  • Guaranteed that session aborts in POOL_STATE_ACCEPTING_ENTRIES and POOL_STATE_SIGNING consume exactly one collateral if non-cooperative participants exist, while POOL_STATE_QUEUE charges nobody.
  • Added 12 regression test cases in src/test/coinjoin_inouts_tests.cpp.

How Has This Been Tested?

  • Compiled src/test/test_dash.
  • Ran unit tests src/test/test_dash --run_test=coinjoin_inouts_tests (all 9 test cases passed).
  • Ran full unit test suite ./src/test/test_dash (0 errors).
  • Ran linters test/lint/all-lint.py.

Breaking Changes

None. Mixed-version operation is safe: only upgraded masternodes enforce the new failed-session fee.

Checklist:

  • 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
  • I have assigned this pull request to a milestone

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6b8231e819

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/coinjoin/server.cpp Outdated
Comment on lines +561 to +565
SetNull();
}

if (txCollateralToConsume) {
ConsumeCollateral(txCollateralToConsume);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep the old collateral reserved until consumption

When ProcessDSACCEPT runs concurrently after SetNull() releases cs_coinjoin, an offender can immediately open the newly idle server with the same collateral—or a re-signed transaction spending the same inputs—because the selected transaction has not entered the mempool yet. The subsequent ConsumeCollateral() then spends those inputs, leaving the new session with an invalid committed collateral and causing its entry or finalization to fail. Previously the reset happened only after ChargeFees() had consumed the collateral, so preserve the reservation/non-idle state until consumption completes or otherwise prevent those inputs from being admitted to the next session.

Useful? React with 👍 / 👎.

@thepastaclaw

thepastaclaw commented Aug 9, 2026

Copy link
Copy Markdown

✅ Final review complete — no blockers (commit b682741)

@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@PastaPastaPasta, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 41 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 4a744ad4-c8eb-4c32-a4dc-bd2f9fb2f0e1

📥 Commits

Reviewing files that changed from the base of the PR and between 6b8231e and b682741.

📒 Files selected for processing (2)
  • src/coinjoin/server.cpp
  • src/test/coinjoin_inouts_tests.cpp

Walkthrough

CoinJoin fee handling now separates collateral selection from collateral consumption. FeePolicy supports probabilistic charging and guaranteed charging on session abort. Offenders include participants with missing entries or unsigned inputs. Entry timeouts use probabilistic selection. Active-session aborts use guaranteed selection, reset the session while holding cs_coinjoin, and consume collateral after unlocking. Tests cover offender selection, deduplication, timeout behavior, and session reset safety.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CCoinJoinServer
  participant CoinJoinSession
  participant Collateral
  CCoinJoinServer->>CoinJoinSession: detect timeout under cs_coinjoin
  CCoinJoinServer->>Collateral: select collateral with FeePolicy
  CCoinJoinServer->>CoinJoinSession: reset session
  CCoinJoinServer->>Collateral: consume selected collateral
Loading

Possibly related PRs

  • dashpay/dash#7537: Synchronizes and protects CoinJoin server collateral selection, fee charging, and timeout logic.

Suggested reviewers: thepastaclaw

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: charging one collateral when a CoinJoin session aborts.
Description check ✅ Passed The description directly explains the abort-fee changes, implementation details, tests, compatibility, and affected session states.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (3)
src/test/coinjoin_inouts_tests.cpp (1)

236-246: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the server construction into a local factory.

The test builds TestableCoinJoinServer nine more times with the identical ten-argument list (s2 through s10). Any constructor change requires ten edits. Add one factory and call it per scenario.

♻️ Suggested refactor
     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));
+    auto MakeServer = [&]() {
+        return std::make_unique<TestableCoinJoinServer>(
+            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));
+    };
+    auto server = MakeServer();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/test/coinjoin_inouts_tests.cpp` around lines 236 - 246, In
coinjoin_offender_selection_and_abort_fee_scenarios, extract the repeated
ten-argument TestableCoinJoinServer construction into a local factory that
captures the shared dependencies and returns a server instance. Replace the
direct constructions for the initial server and scenarios s2 through s10 with
factory calls, preserving each scenario’s existing behavior.
src/coinjoin/server.cpp (1)

451-460: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Guard the unsigned subtraction at Line 456.

vecSessionCollaterals.size() - 1 wraps to SIZE_MAX when the vector is empty. The current code stays correct because Line 459 then returns nullptr, so this is not an active defect. The expression is still fragile if the gate order changes later. Compare with addition instead.

♻️ Suggested change
-        if (vecOffendersCollaterals.size() >= vecSessionCollaterals.size() - 1 && GetRand<int>(/*nMax=*/100) > 33) return nullptr;
+        if (vecOffendersCollaterals.size() + 1 >= vecSessionCollaterals.size() && GetRand<int>(/*nMax=*/100) > 33) return nullptr;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/coinjoin/server.cpp` around lines 451 - 460, Replace the unsigned
subtraction in the probabilistic policy check within the collateral-selection
logic with an equivalent addition-based boundary comparison, avoiding
vecSessionCollaterals.size() - 1 while preserving the existing behavior of the
offender and session collateral thresholds.
src/coinjoin/server.h (1)

70-75: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Drop the default FeePolicy argument.

All ChargeFees callers pass the policy explicitly. Require an explicit policy so each fee decision remains visible at the call site.

NetHandler already has a virtual destructor, so no destructor change is required.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/coinjoin/server.h` around lines 70 - 75, Remove the default
FeePolicy::PROBABILISTIC argument from the ChargeFees declaration in the
coinjoin server interface, requiring every caller to pass FeePolicy explicitly.
Leave the existing const and lock annotations unchanged; no destructor changes
are needed.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/test/coinjoin_inouts_tests.cpp`:
- Around line 488-489: Replace the placeholder cases 11 and 12 in the coinjoin
tests with executable assertions or remove them from the stated case count. At
minimum, implement case 11 by configuring POOL_STATE_ACCEPTING_ENTRIES, adding
sufficient collaterals and entries to satisfy GetMinPoolParticipants(), expiring
the timeout, invoking CheckPool, and asserting that no more than one collateral
is consumed; do not claim implicit coverage without a CheckPool call.
- Around line 442-444: Update the comment above the count_c0/count_c1 assertions
to remove the inaccurate “deduplication” wording and describe only the intended
equal-weight selection distribution. Leave the assertions and
SelectCollateralToCharge behavior unchanged.

---

Nitpick comments:
In `@src/coinjoin/server.cpp`:
- Around line 451-460: Replace the unsigned subtraction in the probabilistic
policy check within the collateral-selection logic with an equivalent
addition-based boundary comparison, avoiding vecSessionCollaterals.size() - 1
while preserving the existing behavior of the offender and session collateral
thresholds.

In `@src/coinjoin/server.h`:
- Around line 70-75: Remove the default FeePolicy::PROBABILISTIC argument from
the ChargeFees declaration in the coinjoin server interface, requiring every
caller to pass FeePolicy explicitly. Leave the existing const and lock
annotations unchanged; no destructor changes are needed.

In `@src/test/coinjoin_inouts_tests.cpp`:
- Around line 236-246: In coinjoin_offender_selection_and_abort_fee_scenarios,
extract the repeated ten-argument TestableCoinJoinServer construction into a
local factory that captures the shared dependencies and returns a server
instance. Replace the direct constructions for the initial server and scenarios
s2 through s10 with factory calls, preserving each scenario’s existing behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d05f5a2d-b61f-4e15-9d80-aebd436fc4cc

📥 Commits

Reviewing files that changed from the base of the PR and between 1fbf489 and 6b8231e.

📒 Files selected for processing (3)
  • src/coinjoin/server.cpp
  • src/coinjoin/server.h
  • src/test/coinjoin_inouts_tests.cpp

Comment thread src/test/coinjoin_inouts_tests.cpp
Comment on lines +488 to +489
// 11. Recoverable timeouts retain probabilistic policy (tested implicitly via CheckPool logic)
// 12. Successful-session random charging remains unchanged

@coderabbitai coderabbitai Bot Aug 9, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Replace the placeholder comments with real cases or remove them.

Cases 11 and 12 contain no assertions. No test in this file calls CheckPool, so the claim at Line 488 that the probabilistic recoverable-timeout path is "tested implicitly" is not supported. These two lines count toward the 12 cases stated in the PR description but verify nothing.

Case 11 is testable: set POOL_STATE_ACCEPTING_ENTRIES, add enough collaterals and entries to pass GetMinPoolParticipants(), expire the timeout, call CheckPool, and assert that at most one collateral is consumed.

Do you want me to add the two cases?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/test/coinjoin_inouts_tests.cpp` around lines 488 - 489, Replace the
placeholder cases 11 and 12 in the coinjoin tests with executable assertions or
remove them from the stated case count. At minimum, implement case 11 by
configuring POOL_STATE_ACCEPTING_ENTRIES, adding sufficient collaterals and
entries to satisfy GetMinPoolParticipants(), expiring the timeout, invoking
CheckPool, and asserting that no more than one collateral is consumed; do not
claim implicit coverage without a CheckPool call.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The placeholders indeed provide no executable coverage, and this file does not call CheckPool. The proposed “at most one collateral” assertion would still not prove that the probabilistic policy was retained because both policies select at most one collateral; a replacement test must distinguish the policy-specific outcome, or the numbered cases and advertised count should be removed.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline src/coinjoin/server.cpp --items all --type function --match 'CheckPool|CheckTimeout|ChargeFees|SelectCollateralToCharge'
ast-grep outline src/coinjoin/server.h --items all --type function --match 'CheckPool|CheckTimeout|ChargeFees|SelectCollateralToCharge'

rg -n -A70 -B15 'CCoinJoinServer::(CheckPool|CheckTimeout|ChargeFees|SelectCollateralToCharge)\s*\(' \
  src/coinjoin/server.cpp

Length of output: 13337


@thepastaclaw, agreed. I verified the paths.

CheckPool uses FeePolicy::PROBABILISTIC only for an accepting-entry timeout with at least GetMinPoolParticipants() entries. Its random gates mean that the consumed-collateral count cannot prove the selected policy.

The deterministic all-offenders condition distinguishes the policies, but it cannot reach this CheckPool branch because that branch requires entries.

I will revise this finding: remove cases 11 and 12 and remove them from the advertised count. Add a CheckPool policy regression only if this PR introduces a narrow deterministic selection or RNG seam.


✏️ Learnings added
Learnt from: thepastaclaw
Repo: dashpay/dash PR: 7567
File: src/test/coinjoin_inouts_tests.cpp:488-489
Timestamp: 2026-08-09T22:50:46.598Z
Learning: In `src/coinjoin/server.cpp`, `CCoinJoinServer::CheckPool` calls `ChargeFees(FeePolicy::PROBABILISTIC)` only after an accepting-entry timeout with at least `CoinJoin::GetMinPoolParticipants()` entries. `CCoinJoinServer::SelectCollateralToCharge(FeePolicy::PROBABILISTIC)` uses random gates, so a consumed-collateral count does not prove that `CheckPool` retained the probabilistic policy. The deterministic all-offenders distinction cannot exercise this `CheckPool` branch because the branch requires the minimum entry count.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

You are interacting with an AI system.

@PastaPastaPasta
PastaPastaPasta force-pushed the fix/coinjoin-abort-fee branch from 6b8231e to b682741 Compare August 9, 2026 22:08

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Final validation — Codex/Sol only (Phase 2 disabled)

The CoinJoin abort path correctly selects one offender, keeps the server non-idle during collateral consumption, and resets afterward; no functional defect was confirmed. The only in-scope issue is that two numbered “test cases” are comments rather than executable coverage, so the advertised test count is inaccurate.
Source: Reviewer backends: codex general — gpt-5.6-sol; codex dash-core-commit-history — gpt-5.6-sol. Final verifier backend: gpt-5.6-sol. Orchestration-only: openclaw-agent/cliproxy/gpt-5.6-sol (not reviewer evidence).

Validated zero-blocker Codex/Sol precheck evidence was promoted to final because Phase 2 (Sonnet/Opus) is temporarily disabled. This is Codex/Sol-only final validation, not Codex + Sonnet/Opus coverage.

Review provenance

  • Codex reviewers: gpt-5.6-sol — general (completed), gpt-5.6-sol — dash-core-commit-history (completed)
  • Verifier: gpt-5.6-sol — verifier
  • Sonnet/Opus: not run (Phase 2 disabled — temporary Codex/Sol-only final)
  • Secondary pass: disabled (temporary_phase2_sonnet_disable)

💬 1 nitpick(s)

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `src/test/coinjoin_inouts_tests.cpp`:
- [NITPICK] src/test/coinjoin_inouts_tests.cpp:488-489: Numbered cases 11 and 12 provide no test coverage
  These numbered cases contain no setup, calls, or assertions, and this test file never invokes `CheckPool`, so case 11's claimed implicit coverage does not exist. This also makes the PR description's claim of 12 regression cases inaccurate. Either add executable tests that distinguish `PROBABILISTIC` from `GUARANTEED_ON_ABORT` and exercise the successful-session path, or remove these numbered comments and correct the stated test count. Merely asserting that at most one collateral is consumed would not distinguish the policies because both select at most one collateral.

Comment on lines +488 to +489
// 11. Recoverable timeouts retain probabilistic policy (tested implicitly via CheckPool logic)
// 12. Successful-session random charging remains unchanged

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💬 Nitpick: Numbered cases 11 and 12 provide no test coverage

These numbered cases contain no setup, calls, or assertions, and this test file never invokes CheckPool, so case 11's claimed implicit coverage does not exist. This also makes the PR description's claim of 12 regression cases inaccurate. Either add executable tests that distinguish PROBABILISTIC from GUARANTEED_ON_ABORT and exercise the successful-session path, or remove these numbered comments and correct the stated test count. Merely asserting that at most one collateral is consumed would not distinguish the policies because both select at most one collateral.

source: ['coderabbit']

@PastaPastaPasta
PastaPastaPasta deleted the fix/coinjoin-abort-fee branch August 10, 2026 01:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants