Skip to content

refactor: simplify CoinJoin class hierarchy#7248

Open
knst wants to merge 8 commits intodashpay:developfrom
knst:refactor-simplify-cj-hierarchy
Open

refactor: simplify CoinJoin class hierarchy#7248
knst wants to merge 8 commits intodashpay:developfrom
knst:refactor-simplify-cj-hierarchy

Conversation

@knst
Copy link
Copy Markdown
Collaborator

@knst knst commented Mar 24, 2026

Issue being fixed or feature implemented

Current coinjoin subsystem have multiple classes that inherits from each other but some of them doesn't provide any real abstraction level yet complicate architecture.

CJWalletManagerImpl, CCoinJoinClientManager, and CCoinJoinClientQueueManager are 2 separate pieces of implementation of CJWalletManager while CJWalletManagerImpl is already private and could keep all implementation inside.

What was done?

This PR simplify CJ architecture by removing boiler code and unneeded abstraction levels to make code easier to support, read and improve.

The changes for this PR has been spotted and half-done during #7234 but it's not a direct dependency of final solution, so far as it was possible to fully cut CJ from dash-chainstate implementation.

  • replaces all references to unique_ptr to just a reference or just unique_ptr to more clear sequence of object initialization in CJ and ownership
  • inline the middleman class CoinJoinWalletManager to CJWalletManagerImpl which is already private and hidden
  • remove class CCoinJoinClientQueueManager; logic inlined into CJWalletManagerImpl
  • remove interface CoinJoinQueueNotify - que notification no longer needed as a separate abstraction
  • replaced inheritance of CCoinJoinServer : public CCoinJoinBaseManager to composition (m_queueman member)
  • remove inheritance of regression tests from CCoinJoinBaseManager
  • rename CCoinJoinBaseManager to CoinJoinQueueManager and make it non-virtual

CoinJoin Class Diagram — Before vs After [by claude]

BEFORE

CCoinJoinBaseManager          CoinJoinQueueNotify
 (virtual ctor/dtor)           (interface)
 - vecCoinJoinQueue             - OnQueueUpdate() [pure]
 - cs_vecqueue
        ▲                              ▲
        │ inherits              │ implements
        ├─────────────────┐     │
CCoinJoinClientQueueManager  CCoinJoinClientManager
 (owned by CJWalletManagerImpl)  (map entry per wallet)
        │
        │ inherits
        │
 CCoinJoinServer ──also inherits──▶ CCoinJoinBaseSession
                                         ▲
                                         │ inherits
                                   CCoinJoinClientSession


CJWalletManager (abstract)
 └── CJWalletManagerImpl
      ├── unique_ptr<CCoinJoinClientQueueManager>  m_basequeueman
      └── map<wallet* → CCoinJoinClientManager>

---
AFTER

CoinJoinQueueManager           ← renamed (no C prefix), no virtual ctor/dtor
 - vecCoinJoinQueue               no longer a base class at all
 - cs_vecqueue
 + TryHasQueueFromMasternode()    new TRY_LOCK helpers
 + TryCheckDuplicate()
 + TryAddQueue()


CCoinJoinServer  ──inherits──▶  CCoinJoinBaseSession
 - m_queueman: CoinJoinQueueManager   ← OWNED by value (composition)
                                         ▲
                                         │ inherits
                                   CCoinJoinClientSession


CJWalletManager (abstract)
 └── CJWalletManagerImpl
      ├── unique_ptr<CoinJoinQueueManager>  m_queueman  ← nullptr if !relay_txes
      └── map<wallet* → CCoinJoinClientManager>
               │
               │  CCoinJoinClientManager
               │   - m_queueman: CoinJoinQueueManager*   ← non-owning ptr
               │                 (points into CJWalletManagerImpl::m_queueman)

How Has This Been Tested?

Run unit & functional tests.

Breaking Changes

N/A

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

@knst knst added this to the 24 milestone Mar 24, 2026
@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 24, 2026

⚠️ Potential Merge Conflicts Detected

This PR has potential conflicts with the following open PRs:

Please coordinate with the authors of these PRs to avoid merge conflicts.

Copy link
Copy Markdown

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

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: d4cfc61d20

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

{
LOCK(cs_ProcessDSQueue);

if (m_queueman->HasQueue(dsq.GetHash())) return ret;
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 duplicate check keyed by masternode outpoint

This replaces the old q == dsq duplicate gate with HasQueue(dsq.GetHash()), but CCoinJoinQueue::GetHash() is based on serialized fields that exclude masternodeOutpoint (coinjoin.h serialization), while the previous equality check included the outpoint. In the specific case where incoming DSQUEUE messages have m_protxHash unset (which this function still accepts), two different masternodes broadcasting in the same second with the same denom/readiness can hash-collide and the second queue is dropped as a duplicate, reducing queue propagation and join opportunities.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

@chatgpt-codex-connector, is it a new issue or is it moved code?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 24, 2026

Walkthrough

The PR replaces CCoinJoinBaseManager with a new CoinJoinQueueManager and converts queue management from inheritance to composition. Queue operations (add/check/try-lock APIs) are moved into CoinJoinQueueManager; server and client code now delegate queue checks and mutations to that member. DSQUEUE processing and queue routing were centralized in CJWalletManagerImpl, which validates DSQUEUE messages and either routes ready queues to client managers or enqueues them in CoinJoinQueueManager. Tests were removed for the old base manager and new unit tests added for CoinJoinQueueManager.

sequenceDiagram
  participant Peer as "Peer (network)"
  participant WalletMgr as "CJWalletManagerImpl"
  participant QueueMgr as "CoinJoinQueueManager"
  participant ClientMgr as "CCoinJoinClientManager(s)"
  participant Server as "CCoinJoinServer"

  Peer->>WalletMgr: Send DSQUEUE
  WalletMgr->>WalletMgr: Validate DSQUEUE (MN, sig, thresholds)
  alt Ready
    WalletMgr->>ClientMgr: TrySubmitDenominate (iterate clients)
    ClientMgr-->>WalletMgr: Accept / Reject
  else Not ready or rejected
    WalletMgr->>QueueMgr: AddQueue(dsq) / TryAddQueue(dsq)
    QueueMgr-->>WalletMgr: Enqueued / Busy
  end

  Note right of QueueMgr: Periodic maintenance
  Server->>QueueMgr: CheckQueue() / GetQueueItemAndTry()
  QueueMgr-->>Server: Provide eligible queue item / purge expired
  Server->>ClientMgr: Promote queue item to session
  ClientMgr-->>Server: Session actions
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.16% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately reflects the main refactoring objective of simplifying the CoinJoin class hierarchy by removing intermediate abstractions and inlining classes.
Description check ✅ Passed The description comprehensively explains the issue being fixed, the specific changes made, the rationale, and includes a detailed before/after class diagram illustrating the architectural simplifications.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/coinjoin/server.cpp (1)

90-97: ⚠️ Potential issue | 🟡 Minor

Silent bypass on lock contention may weaken anti-spam protection.

When TryHasQueueFromMasternode returns std::nullopt due to lock contention (Line 91), the function returns early without rejecting the request. This allows a DSACCEPT through without verifying whether this masternode already has a queue, potentially bypassing the duplicate-queue protection.

Consider whether this is the intended behavior. If the check is critical, a blocking LOCK or retry mechanism may be more appropriate.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/coinjoin/server.cpp` around lines 90 - 97, The current early return when
m_queueman.TryHasQueueFromMasternode(m_mn_activeman.GetOutPoint()) returns
std::nullopt silently bypasses the duplicate-queue check; instead, handle lock
contention explicitly: either acquire the appropriate lock around the call or
implement a short retry loop (e.g., 3 attempts with small backoff) calling
TryHasQueueFromMasternode again, and if it still returns std::nullopt log the
contention and reject the request by calling PushStatus(peer, STATUS_REJECTED,
ERR_RECENT) (and keep the existing log message path when *hasQueue is true).
Ensure references to m_queueman, TryHasQueueFromMasternode,
m_mn_activeman.GetOutPoint, PushStatus, STATUS_REJECTED, and ERR_RECENT are used
so reviewers can locate the change.
🧹 Nitpick comments (3)
src/coinjoin/coinjoin.h (1)

341-346: Lambda captures by value unnecessarily.

The lambda at Line 345 captures q by value (auto q), which copies each CCoinJoinQueue during iteration. Use a const reference for efficiency:

Suggested fix
     bool HasQueue(const uint256& queueHash) EXCLUSIVE_LOCKS_REQUIRED(!cs_vecqueue)
     {
         LOCK(cs_vecqueue);
         return std::any_of(vecCoinJoinQueue.begin(), vecCoinJoinQueue.end(),
-                           [&queueHash](auto q) { return q.GetHash() == queueHash; });
+                           [&queueHash](const auto& q) { return q.GetHash() == queueHash; });
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/coinjoin/coinjoin.h` around lines 341 - 346, The HasQueue method iterates
vecCoinJoinQueue using a lambda that copies each CCoinJoinQueue (auto q),
causing unnecessary copies; change the lambda parameter to take each element by
const reference (e.g., const auto& q) so the comparison q.GetHash() == queueHash
uses a reference and avoids copying. Update the lambda in std::any_of inside
HasQueue accordingly.
src/coinjoin/server.cpp (1)

193-194: Queue addition silently skipped on lock contention.

When TryAddQueue fails to acquire the lock, the queue is not added and the function returns without any logging. The PeerRelayDSQ at Line 194 is never reached, but there's no indication of why the queue wasn't added.

Consider adding debug logging when the lock cannot be acquired:

Suggested logging
-        if (!m_queueman.TryAddQueue(dsq)) return;
+        if (!m_queueman.TryAddQueue(dsq)) {
+            LogPrint(BCLog::COINJOIN, "DSQUEUE -- failed to add queue (lock contention), masternode=%s\n", dsq.masternodeOutpoint.ToStringShort());
+            return;
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/coinjoin/server.cpp` around lines 193 - 194, Queue addition can silently
fail when m_queueman.TryAddQueue(dsq) can't acquire the lock; add a debug/error
log when TryAddQueue returns false so the failure is visible and easier to
diagnose, then return early as before. Locate the call site where
TryAddQueue(dsq) is invoked (near m_queueman.TryAddQueue and
m_peer_manager->PeerRelayDSQ) and insert a logging statement that includes
context (e.g., dsq identifier or peer info) before returning, ensuring you do
not call PeerRelayDSQ when TryAddQueue fails.
src/coinjoin/walletman.cpp (1)

264-269: Consider adding a log message for time-out-of-bounds rejection.

Line 266 silently returns when dsq.IsTimeOutOfBounds(), while other rejection paths either log or return misbehaving errors. A debug log would help operators diagnose queue processing issues.

📝 Optional: Add logging for time bounds rejection
         LogPrint(BCLog::COINJOIN, "DSQUEUE -- %s new\n", dsq.ToString());

-        if (dsq.IsTimeOutOfBounds()) return ret;
+        if (dsq.IsTimeOutOfBounds()) {
+            LogPrint(BCLog::COINJOIN, "DSQUEUE -- time out of bounds, ignoring: %s\n", dsq.ToString());
+            return ret;
+        }

         auto dmn = tip_mn_list.GetValidMNByCollateral(dsq.masternodeOutpoint);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/coinjoin/walletman.cpp` around lines 264 - 269, The
dsq.IsTimeOutOfBounds() branch silently returns without logging, making queue
rejections hard to diagnose; update the block around the call to
dsq.IsTimeOutOfBounds() (in the same scope where LogPrint(BCLog::COINJOIN,
"DSQUEUE -- %s new\n", dsq.ToString()) and before
tip_mn_list.GetValidMNByCollateral(...) is called) to emit a debug/log message
(e.g., using LogPrint or the existing logging facility) indicating the DSQueue
entry was rejected due to timeout bounds and include dsq.ToString() or relevant
time details, then return ret as before so behavior is unchanged but now
observable.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/coinjoin/server.cpp`:
- Around line 159-160: Adjust formatting to satisfy clang-format: reformat the
block using m_queueman.TryCheckDuplicate(dsq) and the subsequent conditional so
LogPrint alignment matches project style (ensure consistent spacing and
indentation around the if (!isDup.has_value()) return;), then re-run
clang-format across src/coinjoin/server.cpp and fix spacing in the object where
pushKV is used (ensure spacing around commas/parentheses matches project
formatting rules) so both the LogPrint alignment and pushKV spacing conform to
CI; you can simply run the repository's clang-format or apply the project's
formatting rules to these occurrences.
- Around line 159-165: The early return when m_queueman.TryCheckDuplicate(dsq)
returns no value lets messages bypass anti-spam under lock contention and the
LogPrint message is misleading; update the DSQUEUE handling to treat a missing
optional (no lock acquired) as a rejection: log a clear warning that the
duplicate check failed due to lock contention (include
dsq.masternodeOutpoint.ToStringShort() and peer id `from`) and drop the message
instead of returning silently, and change the existing LogPrint that fires when
*isDup is true to explicitly state "duplicate dsq message" (referencing
m_queueman.TryCheckDuplicate, dsq, and LogPrint) so the two paths are
distinguishable.

In `@src/coinjoin/walletman.cpp`:
- Around line 181-186: flushWallet currently calls getClient() which releases
cs_wallet_manager_map before returning a raw pointer, creating a
TOCTOU/use-after-free if removeWallet() runs concurrently; modify
CJWalletManagerImpl::flushWallet to acquire the cs_wallet_manager_map mutex and
either (a) perform ResetPool() and StopMixing() while holding that lock, or (b)
copy/move the wallet's unique_ptr (from the map entry obtained under the lock)
into a local smart pointer so the wallet cannot be destroyed after the lock is
released, then call clientman->ResetPool() and clientman->StopMixing();
reference symbols: CJWalletManagerImpl::flushWallet, getClient, removeWallet,
cs_wallet_manager_map, and the wallet unique_ptr stored in the manager map.

---

Outside diff comments:
In `@src/coinjoin/server.cpp`:
- Around line 90-97: The current early return when
m_queueman.TryHasQueueFromMasternode(m_mn_activeman.GetOutPoint()) returns
std::nullopt silently bypasses the duplicate-queue check; instead, handle lock
contention explicitly: either acquire the appropriate lock around the call or
implement a short retry loop (e.g., 3 attempts with small backoff) calling
TryHasQueueFromMasternode again, and if it still returns std::nullopt log the
contention and reject the request by calling PushStatus(peer, STATUS_REJECTED,
ERR_RECENT) (and keep the existing log message path when *hasQueue is true).
Ensure references to m_queueman, TryHasQueueFromMasternode,
m_mn_activeman.GetOutPoint, PushStatus, STATUS_REJECTED, and ERR_RECENT are used
so reviewers can locate the change.

---

Nitpick comments:
In `@src/coinjoin/coinjoin.h`:
- Around line 341-346: The HasQueue method iterates vecCoinJoinQueue using a
lambda that copies each CCoinJoinQueue (auto q), causing unnecessary copies;
change the lambda parameter to take each element by const reference (e.g., const
auto& q) so the comparison q.GetHash() == queueHash uses a reference and avoids
copying. Update the lambda in std::any_of inside HasQueue accordingly.

In `@src/coinjoin/server.cpp`:
- Around line 193-194: Queue addition can silently fail when
m_queueman.TryAddQueue(dsq) can't acquire the lock; add a debug/error log when
TryAddQueue returns false so the failure is visible and easier to diagnose, then
return early as before. Locate the call site where TryAddQueue(dsq) is invoked
(near m_queueman.TryAddQueue and m_peer_manager->PeerRelayDSQ) and insert a
logging statement that includes context (e.g., dsq identifier or peer info)
before returning, ensuring you do not call PeerRelayDSQ when TryAddQueue fails.

In `@src/coinjoin/walletman.cpp`:
- Around line 264-269: The dsq.IsTimeOutOfBounds() branch silently returns
without logging, making queue rejections hard to diagnose; update the block
around the call to dsq.IsTimeOutOfBounds() (in the same scope where
LogPrint(BCLog::COINJOIN, "DSQUEUE -- %s new\n", dsq.ToString()) and before
tip_mn_list.GetValidMNByCollateral(...) is called) to emit a debug/log message
(e.g., using LogPrint or the existing logging facility) indicating the DSQueue
entry was rejected due to timeout bounds and include dsq.ToString() or relevant
time details, then return ret as before so behavior is unchanged but now
observable.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: eb6ca1b7-3151-4797-966b-a6a0cea6e276

📥 Commits

Reviewing files that changed from the base of the PR and between 65366a4 and d4cfc61.

📒 Files selected for processing (11)
  • contrib/auto_gdb/simple_class_obj.py
  • src/Makefile.test.include
  • src/coinjoin/client.cpp
  • src/coinjoin/client.h
  • src/coinjoin/coinjoin.cpp
  • src/coinjoin/coinjoin.h
  • src/coinjoin/server.cpp
  • src/coinjoin/server.h
  • src/coinjoin/walletman.cpp
  • src/test/coinjoin_basemanager_tests.cpp
  • src/test/coinjoin_queue_tests.cpp
💤 Files with no reviewable changes (2)
  • src/Makefile.test.include
  • src/test/coinjoin_basemanager_tests.cpp

Comment on lines +159 to +160
const auto isDup = m_queueman.TryCheckDuplicate(dsq);
if (!isDup.has_value()) return;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix formatting to pass CI.

The pipeline reports clang-format failures at lines 159-160 (LogPrint alignment) and line 904 (pushKV spacing). Run the formatter to resolve these.

Also applies to: 904-904

🧰 Tools
🪛 GitHub Actions: Clang Diff Format Check

[error] 159-160: Clang format check failed (clang-format-diff.py). Formatting differences found in server.cpp: LogPrint arguments were rewrapped/aligned.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/coinjoin/server.cpp` around lines 159 - 160, Adjust formatting to satisfy
clang-format: reformat the block using m_queueman.TryCheckDuplicate(dsq) and the
subsequent conditional so LogPrint alignment matches project style (ensure
consistent spacing and indentation around the if (!isDup.has_value()) return;),
then re-run clang-format across src/coinjoin/server.cpp and fix spacing in the
object where pushKV is used (ensure spacing around commas/parentheses matches
project formatting rules) so both the LogPrint alignment and pushKV spacing
conform to CI; you can simply run the repository's clang-format or apply the
project's formatting rules to these occurrences.

Comment on lines 181 to 186
void CJWalletManagerImpl::flushWallet(const std::string& name)
{
walletman.Flush(name);
auto* clientman = Assert(getClient(name));
clientman->ResetPool();
clientman->StopMixing();
}
Copy link
Copy Markdown

@coderabbitai coderabbitai bot Mar 24, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if flushWallet callers could be concurrent with removeWallet
rg -n -C3 'flushWallet|removeWallet' --type cpp

Repository: dashpay/dash

Length of output: 6249


🏁 Script executed:

# First, find and view the getClient() implementation
rg -n 'getClient\(' src/coinjoin/walletman.cpp -A 10 | head -50

Repository: dashpay/dash

Length of output: 1549


🏁 Script executed:

# Check the context in interfaces.cpp where flushWallet is called
sed -n '100,115p' src/coinjoin/interfaces.cpp

Repository: dashpay/dash

Length of output: 703


🏁 Script executed:

# Find all callers of flushWallet to understand threading context
rg -n 'flushWallet' --type cpp -B 3 -A 3

Repository: dashpay/dash

Length of output: 1985


🏁 Script executed:

# Find callers of FlushWallet interface method
rg -n 'FlushWallet\(' --type cpp -B 3 -A 3

Repository: dashpay/dash

Length of output: 1482


🏁 Script executed:

# Check wallet loading/unloading patterns to see if concurrent calls are possible
rg -n 'wallet.*Flush|wallet.*Remove' --type cpp | head -30

Repository: dashpay/dash

Length of output: 1110


Hold lock during flush operations to prevent TOCTOU race condition.

getClient() releases cs_wallet_manager_map before returning the raw pointer. If another thread calls removeWallet() concurrently, the unique_ptr is destroyed while flushWallet() still holds a reference, leading to use-after-free. This is inconsistent with removeWallet() which safely erases under the lock.

Proposed fix
 void CJWalletManagerImpl::flushWallet(const std::string& name)
 {
-    auto* clientman = Assert(getClient(name));
-    clientman->ResetPool();
-    clientman->StopMixing();
+    LOCK(cs_wallet_manager_map);
+    auto it = m_wallet_manager_map.find(name);
+    if (it != m_wallet_manager_map.end()) {
+        it->second->ResetPool();
+        it->second->StopMixing();
+    }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/coinjoin/walletman.cpp` around lines 181 - 186, flushWallet currently
calls getClient() which releases cs_wallet_manager_map before returning a raw
pointer, creating a TOCTOU/use-after-free if removeWallet() runs concurrently;
modify CJWalletManagerImpl::flushWallet to acquire the cs_wallet_manager_map
mutex and either (a) perform ResetPool() and StopMixing() while holding that
lock, or (b) copy/move the wallet's unique_ptr (from the map entry obtained
under the lock) into a local smart pointer so the wallet cannot be destroyed
after the lock is released, then call clientman->ResetPool() and
clientman->StopMixing(); reference symbols: CJWalletManagerImpl::flushWallet,
getClient, removeWallet, cs_wallet_manager_map, and the wallet unique_ptr stored
in the manager map.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

it's existing issue; shared_ptr should be used. I will prepare new PR with this fix.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@knst, understood — thanks for the confirmation! Using shared_ptr (or an RAII scope-guard over cs_wallet_manager_map) would be a clean fix for this.

Would you like me to open a GitHub issue to track this follow-up so it doesn't get lost?

Copy link
Copy Markdown

@thepastaclaw thepastaclaw left a comment

Choose a reason for hiding this comment

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

Code Review

Clean mechanical refactoring. No blocking issues found. There is one genuine performance bug (copy-by-value in a hot-path lambda) and a const-correctness inconsistency. Two nitpicks about log quality and test coverage round out the review.

Reviewed commit: d4cfc61

🟡 2 suggestion(s) | 💬 2 nitpick(s)

1 additional finding

🟡 suggestion: HasQueue lambda copies each CCoinJoinQueue by value

src/coinjoin/coinjoin.h (lines 344-345)

[&queueHash](auto q) copies every CCoinJoinQueue per iteration, including its heap-allocated std::vector<unsigned char> vchSig. This is called on every incoming DSQUEUE message (via ProcessDSQueue) and for inventory checks (AlreadyHave). The adjacent GetQueueFromHash (line 350) and HasQueueFromMasternode (line 358) both correctly use const auto& q—this one should too.

💡 Suggested change
    return std::any_of(vecCoinJoinQueue.begin(), vecCoinJoinQueue.end(),
                       [&queueHash](const auto& q) { return q.GetHash() == queueHash; });
🤖 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/coinjoin/coinjoin.h`:
- [SUGGESTION] lines 344-345: HasQueue lambda copies each CCoinJoinQueue by value
  `[&queueHash](auto q)` copies every `CCoinJoinQueue` per iteration, including its heap-allocated `std::vector<unsigned char> vchSig`. This is called on every incoming `DSQUEUE` message (via `ProcessDSQueue`) and for inventory checks (`AlreadyHave`). The adjacent `GetQueueFromHash` (line 350) and `HasQueueFromMasternode` (line 358) both correctly use `const auto& q`—this one should too.
- [SUGGESTION] lines 341-351: HasQueue and GetQueueFromHash should be const
  Both methods only read `vecCoinJoinQueue` under `LOCK(cs_vecqueue)`, and `cs_vecqueue` is already `mutable Mutex` (line 327). `GetQueueSize()` (line 338) and `HasQueueFromMasternode()` (line 355) are already `const`. The inconsistency matters because `CJWalletManagerImpl::hasQueue` is a `const` method that calls `m_queueman->HasQueue()`—this only compiles because `std::unique_ptr::operator->()` doesn't propagate const, silently bypassing const-correctness.

Comment on lines 349 to 351
LOCK(cs_vecqueue);
return util::find_if_opt(vecCoinJoinQueue, [&queueHash](const auto& q) { return q.GetHash() == queueHash; });
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Suggestion: HasQueue and GetQueueFromHash should be const

Both methods only read vecCoinJoinQueue under LOCK(cs_vecqueue), and cs_vecqueue is already mutable Mutex (line 327). GetQueueSize() (line 338) and HasQueueFromMasternode() (line 355) are already const. The inconsistency matters because CJWalletManagerImpl::hasQueue is a const method that calls m_queueman->HasQueue()—this only compiles because std::unique_ptr::operator->() doesn't propagate const, silently bypassing const-correctness.

source: ['claude']

🤖 Fix this 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/coinjoin/coinjoin.h`:
- [SUGGESTION] lines 341-351: HasQueue and GetQueueFromHash should be const
  Both methods only read `vecCoinJoinQueue` under `LOCK(cs_vecqueue)`, and `cs_vecqueue` is already `mutable Mutex` (line 327). `GetQueueSize()` (line 338) and `HasQueueFromMasternode()` (line 355) are already `const`. The inconsistency matters because `CJWalletManagerImpl::hasQueue` is a `const` method that calls `m_queueman->HasQueue()`—this only compiles because `std::unique_ptr::operator->()` doesn't propagate const, silently bypassing const-correctness.

Comment on lines +159 to +163
const auto isDup = m_queueman.TryCheckDuplicate(dsq);
if (!isDup.has_value()) return;
if (*isDup) {
LogPrint(BCLog::COINJOIN, "DSQUEUE -- Peer %d is sending WAY too many dsq messages for a masternode with collateral %s\n", from, dsq.masternodeOutpoint.ToStringShort());
return;
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: TryCheckDuplicate conflates exact duplicates with rate abuse in the log

TryCheckDuplicate returns true for both exact duplicates (q == dsq) and same-MN-same-readiness matches. The caller logs "WAY too many dsq messages" for both cases. Exact duplicates are benign repeated messages (P2P retransmission), not rate abuse. The log is misleading for that case. Consider either splitting the return value or accepting the minor log noise with a comment.

source: ['claude']

Comment on lines +110 to +141
BOOST_AUTO_TEST_CASE(queuemanager_checkqueue_removes_timeouts)
{
CoinJoinQueueManager man;
const int denom = CoinJoin::AmountToDenomination(CoinJoin::GetSmallestDenomination());
const int64_t now = GetAdjustedTime();
// Non-expired
man.AddQueue(MakeQueue(denom, now, false, COutPoint(uint256S("11"), 0)));
// Expired (too old)
man.AddQueue(MakeQueue(denom, now - COINJOIN_QUEUE_TIMEOUT - 1, false, COutPoint(uint256S("12"), 0)));

BOOST_CHECK_EQUAL(man.GetQueueSize(), 2);
man.CheckQueue();
// One should be removed
BOOST_CHECK_EQUAL(man.GetQueueSize(), 1);
}

BOOST_AUTO_TEST_CASE(queuemanager_getqueueitem_marks_tried_once)
{
CoinJoinQueueManager man;
const int denom = CoinJoin::AmountToDenomination(CoinJoin::GetSmallestDenomination());
const int64_t now = GetAdjustedTime();
CCoinJoinQueue dsq = MakeQueue(denom, now, false, COutPoint(uint256S("21"), 0));
man.AddQueue(dsq);

CCoinJoinQueue picked;
// First retrieval should succeed
BOOST_CHECK(man.GetQueueItemAndTry(picked));
// No other items left to try (picked is marked tried inside)
CCoinJoinQueue picked2;
BOOST_CHECK(!man.GetQueueItemAndTry(picked2));
}

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: TryCheckDuplicate lacks a dedicated test

The migrated tests cover AddQueue, CheckQueue, and GetQueueItemAndTry. TryCheckDuplicate encodes non-trivial duplicate detection logic (exact match vs. same-MN-same-readiness) that would benefit from a targeted test. The other Try* methods are thin TRY_LOCK wrappers and lower priority.

source: ['claude']

@github-actions
Copy link
Copy Markdown

This pull request has conflicts, please rebase.

knst added 7 commits March 27, 2026 16:34
…tManagerImpl

CJWalletManagerImpl is private implementation of CJWalletManager
It helps to hide internal details of CJWalletManagerImpl which used public CCoinJoinClientManager
It reduce amount of abstractions and complexity of implementation.
…Impl

It reduce amount of abstractions and levels of inheritance
While CJWalletManagerImpl is already private
@knst knst force-pushed the refactor-simplify-cj-hierarchy branch from d4cfc61 to 267e340 Compare March 27, 2026 09:44
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (2)
src/coinjoin/coinjoin.h (1)

341-351: 🛠️ Refactor suggestion | 🟠 Major

HasQueue and GetQueueFromHash should be const.

Both methods only read vecCoinJoinQueue under LOCK(cs_vecqueue), and cs_vecqueue is already mutable Mutex. GetQueueSize() (line 338) and HasQueueFromMasternode() (line 355) are already const. The inconsistency matters because CJWalletManagerImpl::hasQueue is a const method that calls m_queueman->HasQueue()—this compiles only because std::unique_ptr::operator->() doesn't propagate const.

Suggested fix
-    bool HasQueue(const uint256& queueHash) EXCLUSIVE_LOCKS_REQUIRED(!cs_vecqueue)
+    bool HasQueue(const uint256& queueHash) const EXCLUSIVE_LOCKS_REQUIRED(!cs_vecqueue)
     {
         LOCK(cs_vecqueue);
         return std::any_of(vecCoinJoinQueue.begin(), vecCoinJoinQueue.end(),
                            [&queueHash](auto q) { return q.GetHash() == queueHash; });
     }
-    std::optional<CCoinJoinQueue> GetQueueFromHash(const uint256& queueHash) EXCLUSIVE_LOCKS_REQUIRED(!cs_vecqueue)
+    std::optional<CCoinJoinQueue> GetQueueFromHash(const uint256& queueHash) const EXCLUSIVE_LOCKS_REQUIRED(!cs_vecqueue)
     {
         LOCK(cs_vecqueue);
         return util::find_if_opt(vecCoinJoinQueue, [&queueHash](const auto& q) { return q.GetHash() == queueHash; });
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/coinjoin/coinjoin.h` around lines 341 - 351, Change the signatures of
HasQueue and GetQueueFromHash to be const (append "const" after the
EXCLUSIVE_LOCKS_REQUIRED annotation) so they can be called from const callers;
in HasQueue also avoid copying queue entries by changing the lambda to take
const auto& q (instead of auto q) so the comparison q.GetHash() works on const
references. Update the declarations/definitions of HasQueue and GetQueueFromHash
accordingly (functions: HasQueue, GetQueueFromHash).
src/coinjoin/walletman.cpp (1)

181-186: ⚠️ Potential issue | 🟠 Major

Hold lock during flush operations to prevent TOCTOU race condition.

getClient() acquires cs_wallet_manager_map but releases it before returning the raw pointer. If another thread calls removeWallet() concurrently, the unique_ptr is destroyed while flushWallet() still holds a reference, leading to use-after-free.

Proposed fix
 void CJWalletManagerImpl::flushWallet(const std::string& name)
 {
-    auto* clientman = Assert(getClient(name));
-    clientman->ResetPool();
-    clientman->StopMixing();
+    LOCK(cs_wallet_manager_map);
+    auto it = m_wallet_manager_map.find(name);
+    if (it != m_wallet_manager_map.end()) {
+        it->second->ResetPool();
+        it->second->StopMixing();
+    }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/coinjoin/walletman.cpp` around lines 181 - 186, flushWallet currently
calls getClient() which drops cs_wallet_manager_map before using the returned
raw pointer, risking use-after-free if removeWallet() runs concurrently; modify
CJWalletManagerImpl::flushWallet to acquire and hold the cs_wallet_manager_map
lock (e.g., std::unique_lock or LOCK(cs_wallet_manager_map)) for the duration of
the operation, then call Assert(getClient(name)) and invoke
clientman->ResetPool() and clientman->StopMixing() while the lock is held so the
underlying unique_ptr cannot be destroyed by removeWallet(); keep references to
the same symbols (CJWalletManagerImpl::flushWallet, getClient, removeWallet,
cs_wallet_manager_map, ResetPool, StopMixing).
🧹 Nitpick comments (1)
src/coinjoin/walletman.cpp (1)

112-118: Consider simplifying the destructor.

The destructor explicitly resets each unique_ptr under lock, but simply clearing the map would achieve the same effect more idiomatically:

Suggested simplification
 CJWalletManagerImpl::~CJWalletManagerImpl()
 {
     LOCK(cs_wallet_manager_map);
-    for (auto& [_, clientman] : m_wallet_manager_map) {
-        clientman.reset();
-    }
+    m_wallet_manager_map.clear();
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/coinjoin/walletman.cpp` around lines 112 - 118, The destructor
CJWalletManagerImpl::~CJWalletManagerImpl currently locks cs_wallet_manager_map
and manually resets each unique_ptr in m_wallet_manager_map; simplify it by
keeping the LOCK(cs_wallet_manager_map) but replace the for-loop with
m_wallet_manager_map.clear() to idiomatically destroy all entries (unique_ptrs)
and release resources in one call, preserving thread-safety.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/coinjoin/coinjoin.h`:
- Around line 360-366: Run clang-format (or adjust formatting) on the
declarations of TryHasQueueFromMasternode and TryCheckDuplicate so the trailing
attribute EXCLUSIVE_LOCKS_REQUIRED(!cs_vecqueue) is on its own line / matches
the project's clang-format rules; locate the declarations for the functions
TryHasQueueFromMasternode(const COutPoint& outpoint) and TryCheckDuplicate(const
CCoinJoinQueue& dsq) in coinjoin.h and reflow the line breaks/spacing to conform
to the project's style (or run the project's clang-format command to auto-fix).

In `@src/coinjoin/walletman.cpp`:
- Around line 41-54: Several function declarations (e.g., getClient,
processMessage, getMixingMasternodes, addWallet, removeWallet, and
UpdatedBlockTip) have incorrect line breaks before the EXCLUSIVE_LOCKS_REQUIRED
annotations; run clang-format on src/coinjoin/walletman.cpp to reflow these
declarations so the EXCLUSIVE_LOCKS_REQUIRED(...) annotation is on the same line
as the function declaration (also fix the similar mismatches around the
declarations at the block noted as lines 75-78).

---

Duplicate comments:
In `@src/coinjoin/coinjoin.h`:
- Around line 341-351: Change the signatures of HasQueue and GetQueueFromHash to
be const (append "const" after the EXCLUSIVE_LOCKS_REQUIRED annotation) so they
can be called from const callers; in HasQueue also avoid copying queue entries
by changing the lambda to take const auto& q (instead of auto q) so the
comparison q.GetHash() works on const references. Update the
declarations/definitions of HasQueue and GetQueueFromHash accordingly
(functions: HasQueue, GetQueueFromHash).

In `@src/coinjoin/walletman.cpp`:
- Around line 181-186: flushWallet currently calls getClient() which drops
cs_wallet_manager_map before using the returned raw pointer, risking
use-after-free if removeWallet() runs concurrently; modify
CJWalletManagerImpl::flushWallet to acquire and hold the cs_wallet_manager_map
lock (e.g., std::unique_lock or LOCK(cs_wallet_manager_map)) for the duration of
the operation, then call Assert(getClient(name)) and invoke
clientman->ResetPool() and clientman->StopMixing() while the lock is held so the
underlying unique_ptr cannot be destroyed by removeWallet(); keep references to
the same symbols (CJWalletManagerImpl::flushWallet, getClient, removeWallet,
cs_wallet_manager_map, ResetPool, StopMixing).

---

Nitpick comments:
In `@src/coinjoin/walletman.cpp`:
- Around line 112-118: The destructor CJWalletManagerImpl::~CJWalletManagerImpl
currently locks cs_wallet_manager_map and manually resets each unique_ptr in
m_wallet_manager_map; simplify it by keeping the LOCK(cs_wallet_manager_map) but
replace the for-loop with m_wallet_manager_map.clear() to idiomatically destroy
all entries (unique_ptrs) and release resources in one call, preserving
thread-safety.
🪄 Autofix (Beta)

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

Run ID: 57057b69-cf5e-4597-b88c-8b009c337f25

📥 Commits

Reviewing files that changed from the base of the PR and between d4cfc61 and 267e340.

📒 Files selected for processing (11)
  • contrib/auto_gdb/simple_class_obj.py
  • src/Makefile.test.include
  • src/coinjoin/client.cpp
  • src/coinjoin/client.h
  • src/coinjoin/coinjoin.cpp
  • src/coinjoin/coinjoin.h
  • src/coinjoin/server.cpp
  • src/coinjoin/server.h
  • src/coinjoin/walletman.cpp
  • src/test/coinjoin_basemanager_tests.cpp
  • src/test/coinjoin_queue_tests.cpp
💤 Files with no reviewable changes (2)
  • src/Makefile.test.include
  • src/test/coinjoin_basemanager_tests.cpp
✅ Files skipped from review due to trivial changes (1)
  • src/coinjoin/server.cpp
🚧 Files skipped from review as they are similar to previous changes (4)
  • contrib/auto_gdb/simple_class_obj.py
  • src/coinjoin/server.h
  • src/coinjoin/client.cpp
  • src/coinjoin/coinjoin.cpp

Comment on lines +360 to +366
}
//! TRY_LOCK variant: returns nullopt if lock can't be acquired; true if any queue entry has this
//! outpoint (any readiness).
[[nodiscard]] std::optional<bool> TryHasQueueFromMasternode(const COutPoint& outpoint) const EXCLUSIVE_LOCKS_REQUIRED(!cs_vecqueue);
//! TRY_LOCK combined duplicate check: returns nullopt if lock can't be acquired; true if dsq is
//! an exact duplicate or the masternode is sending too many dsqs with the same readiness.
[[nodiscard]] std::optional<bool> TryCheckDuplicate(const CCoinJoinQueue& dsq) const EXCLUSIVE_LOCKS_REQUIRED(!cs_vecqueue);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Address clang-format pipeline failures.

The pipeline reports formatting mismatches on the TryHasQueueFromMasternode and TryCheckDuplicate declarations. Run clang-format to fix line breaks before EXCLUSIVE_LOCKS_REQUIRED.

🧰 Tools
🪛 GitHub Actions: Clang Diff Format Check

[error] 360-362: Clang-format check mismatch: TryHasQueueFromMasternode declaration formatting (line break added before EXCLUSIVE_LOCKS_REQUIRED).


[error] 364-366: Clang-format check mismatch: TryCheckDuplicate declaration formatting (line break added before EXCLUSIVE_LOCKS_REQUIRED).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/coinjoin/coinjoin.h` around lines 360 - 366, Run clang-format (or adjust
formatting) on the declarations of TryHasQueueFromMasternode and
TryCheckDuplicate so the trailing attribute
EXCLUSIVE_LOCKS_REQUIRED(!cs_vecqueue) is on its own line / matches the
project's clang-format rules; locate the declarations for the functions
TryHasQueueFromMasternode(const COutPoint& outpoint) and TryCheckDuplicate(const
CCoinJoinQueue& dsq) in coinjoin.h and reflow the line breaks/spacing to conform
to the project's style (or run the project's clang-format command to auto-fix).

@knst knst force-pushed the refactor-simplify-cj-hierarchy branch from 267e340 to c363fff Compare March 27, 2026 11:08
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 27, 2026

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
{}

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (2)
src/coinjoin/walletman.cpp (2)

42-50: ⚠️ Potential issue | 🟡 Minor

Re-run clang-format on the updated declarations.

The diff-format job is still failing on the EXCLUSIVE_LOCKS_REQUIRED(...) wrapping in this block and on the ProcessDSQueue signature, so CI will stay red as-is.

Also applies to: 54-54, 75-78

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/coinjoin/walletman.cpp` around lines 42 - 50, Re-run clang-format on
src/coinjoin/walletman.cpp to fix the wrapping/spacing of the thread-safety
annotations and the ProcessDSQueue signature: ensure
EXCLUSIVE_LOCKS_REQUIRED(...) stays attached to the function declaration (not
split awkwardly onto its own line) for getClient, processMessage,
getMixingMasternodes, addWallet, removeWallet, flushWallet and that the
ProcessDSQueue/processMessage signature formatting matches project style; run
the repository clang-format configuration (or the same .clang-format used by CI)
and re-commit the formatted changes so the diff-format job passes.

143-147: ⚠️ Potential issue | 🔴 Critical

flushWallet() still has a use-after-free race.

getClient() releases cs_wallet_manager_map before returning the raw pointer, and flushWallet() dereferences it later. A concurrent removeWallet() can erase that unique_ptr in between, so ResetPool() / StopMixing() can run on freed memory.

Suggested fix
 void CJWalletManagerImpl::flushWallet(const std::string& name)
 {
-    auto* clientman = Assert(getClient(name));
-    clientman->ResetPool();
-    clientman->StopMixing();
+    LOCK(cs_wallet_manager_map);
+    if (auto it = m_wallet_manager_map.find(name); it != m_wallet_manager_map.end()) {
+        it->second->ResetPool();
+        it->second->StopMixing();
+    }
 }

If either callee can re-enter CJWalletManagerImpl, use an ownership-bearing handle instead of a borrowed raw pointer.

Also applies to: 181-185

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/coinjoin/walletman.cpp` around lines 143 - 147, getClient currently
returns a raw pointer to a CCoinJoinClientManager after releasing
cs_wallet_manager_map, which allows removeWallet to erase the unique_ptr and
cause use-after-free when flushWallet calls ResetPool/StopMixing; change the
ownership model so callers get an ownership-bearing handle: replace stored
unique_ptrs in m_wallet_manager_map with shared_ptrs (or return a
std::shared_ptr created from the existing unique_ptr) and change getClient to
return std::shared_ptr<CCoinJoinClientManager> (or an explicit owner handle) so
flushWallet (and any callers of ResetPool/StopMixing) hold a reference that
prevents removal races, and update removeWallet/CJWalletManagerImpl methods to
use/erase the shared_ptr accordingly while keeping cs_wallet_manager_map locked
only for map access.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/coinjoin/walletman.cpp`:
- Around line 254-273: Resolve the masternode identity before hashing the
DSQueue: call tip_mn_list.GetValidMNByCollateral(dsq.masternodeOutpoint) first,
reject if no dmn, then if dsq.m_protxHash is non-null and does not match
dmn->proTxHash return ret, otherwise set dsq.m_protxHash = dmn->proTxHash when
null; only after that run HasQueue(dsq.GetHash()) and
HasQueueFromMasternode(dsq.masternodeOutpoint, dsq.fReady) (and later
AddQueue(dsq)), so the hash-based duplicate fast-path uses the
normalized/validated proTxHash.
- Around line 120-124: The Schedule method currently skips scheduling
maintenance when m_relay_txes is false, which prevents
CJWalletManagerImpl::DoMaintenance from running for wallets in that mode; remove
the early return that checks m_relay_txes so scheduler.scheduleEvery is always
called, relying on DoMaintenance's internal check (and
CCoinJoinClientManager::DoMaintenance behavior) to no-op when m_queueman ==
nullptr; update CJWalletManagerImpl::Schedule to always bind and schedule
DoMaintenance with the connman reference.

---

Duplicate comments:
In `@src/coinjoin/walletman.cpp`:
- Around line 42-50: Re-run clang-format on src/coinjoin/walletman.cpp to fix
the wrapping/spacing of the thread-safety annotations and the ProcessDSQueue
signature: ensure EXCLUSIVE_LOCKS_REQUIRED(...) stays attached to the function
declaration (not split awkwardly onto its own line) for getClient,
processMessage, getMixingMasternodes, addWallet, removeWallet, flushWallet and
that the ProcessDSQueue/processMessage signature formatting matches project
style; run the repository clang-format configuration (or the same .clang-format
used by CI) and re-commit the formatted changes so the diff-format job passes.
- Around line 143-147: getClient currently returns a raw pointer to a
CCoinJoinClientManager after releasing cs_wallet_manager_map, which allows
removeWallet to erase the unique_ptr and cause use-after-free when flushWallet
calls ResetPool/StopMixing; change the ownership model so callers get an
ownership-bearing handle: replace stored unique_ptrs in m_wallet_manager_map
with shared_ptrs (or return a std::shared_ptr created from the existing
unique_ptr) and change getClient to return
std::shared_ptr<CCoinJoinClientManager> (or an explicit owner handle) so
flushWallet (and any callers of ResetPool/StopMixing) hold a reference that
prevents removal races, and update removeWallet/CJWalletManagerImpl methods to
use/erase the shared_ptr accordingly while keeping cs_wallet_manager_map locked
only for map access.
🪄 Autofix (Beta)

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

Run ID: 381c0ef8-1cbb-45fa-b92e-aed055d1909a

📥 Commits

Reviewing files that changed from the base of the PR and between 267e340 and c363fff.

📒 Files selected for processing (1)
  • src/coinjoin/walletman.cpp

Comment on lines 120 to 124
void CJWalletManagerImpl::Schedule(CConnman& connman, CScheduler& scheduler)
{
if (!m_relay_txes) return;
scheduler.scheduleEvery(std::bind(&CCoinJoinClientQueueManager::DoMaintenance, std::ref(*queueman)),
std::chrono::seconds{1});
scheduler.scheduleEvery(std::bind(&CoinJoinWalletManager::DoMaintenance, std::ref(walletman), std::ref(connman)),
scheduler.scheduleEvery(std::bind(&CJWalletManagerImpl::DoMaintenance, this, std::ref(connman)),
std::chrono::seconds{1});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't gate wallet maintenance on relay_txes.

DoMaintenance() already skips queue work when m_queueman == nullptr. This early return only disables CCoinJoinClientManager::DoMaintenance(...) for the relay_txes == false path, so wallets added in that mode never get periodic maintenance.

Suggested fix
 void CJWalletManagerImpl::Schedule(CConnman& connman, CScheduler& scheduler)
 {
-    if (!m_relay_txes) return;
     scheduler.scheduleEvery(std::bind(&CJWalletManagerImpl::DoMaintenance, this, std::ref(connman)),
                             std::chrono::seconds{1});
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
void CJWalletManagerImpl::Schedule(CConnman& connman, CScheduler& scheduler)
{
if (!m_relay_txes) return;
scheduler.scheduleEvery(std::bind(&CCoinJoinClientQueueManager::DoMaintenance, std::ref(*queueman)),
std::chrono::seconds{1});
scheduler.scheduleEvery(std::bind(&CoinJoinWalletManager::DoMaintenance, std::ref(walletman), std::ref(connman)),
scheduler.scheduleEvery(std::bind(&CJWalletManagerImpl::DoMaintenance, this, std::ref(connman)),
std::chrono::seconds{1});
void CJWalletManagerImpl::Schedule(CConnman& connman, CScheduler& scheduler)
{
scheduler.scheduleEvery(std::bind(&CJWalletManagerImpl::DoMaintenance, this, std::ref(connman)),
std::chrono::seconds{1});
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/coinjoin/walletman.cpp` around lines 120 - 124, The Schedule method
currently skips scheduling maintenance when m_relay_txes is false, which
prevents CJWalletManagerImpl::DoMaintenance from running for wallets in that
mode; remove the early return that checks m_relay_txes so
scheduler.scheduleEvery is always called, relying on DoMaintenance's internal
check (and CCoinJoinClientManager::DoMaintenance behavior) to no-op when
m_queueman == nullptr; update CJWalletManagerImpl::Schedule to always bind and
schedule DoMaintenance with the connman reference.

Comment on lines +254 to +273
if (m_queueman->HasQueue(dsq.GetHash())) return ret;

if (m_queueman->HasQueueFromMasternode(dsq.masternodeOutpoint, dsq.fReady)) {
// no way the same mn can send another dsq with the same readiness this soon
LogPrint(BCLog::COINJOIN, /* Continued */
"DSQUEUE -- Peer %d is sending WAY too many dsq messages for a masternode with collateral %s\n",
from, dsq.masternodeOutpoint.ToStringShort());
return ret;
}

LogPrint(BCLog::COINJOIN, "DSQUEUE -- %s new\n", dsq.ToString());

if (dsq.IsTimeOutOfBounds()) return ret;

auto dmn = tip_mn_list.GetValidMNByCollateral(dsq.masternodeOutpoint);
if (!dmn) return ret;

if (dsq.m_protxHash.IsNull()) {
dsq.m_protxHash = dmn->proTxHash;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Normalize and validate the masternode identity before HasQueue().

Here HasQueue(dsq.GetHash()) hashes the pre-normalized object, but Line 271 can still mutate dsq.m_protxHash before AddQueue(dsq). Repeated outpoint-only DSQUEUEs then miss the duplicate fast-path and fall into the "WAY too many dsq messages" branch instead; a non-null but mismatched m_protxHash is also accepted today.

Resolve dmn first, reject conflicting non-null m_protxHash, fill the null case, and only then run the hash-based duplicate check.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/coinjoin/walletman.cpp` around lines 254 - 273, Resolve the masternode
identity before hashing the DSQueue: call
tip_mn_list.GetValidMNByCollateral(dsq.masternodeOutpoint) first, reject if no
dmn, then if dsq.m_protxHash is non-null and does not match dmn->proTxHash
return ret, otherwise set dsq.m_protxHash = dmn->proTxHash when null; only after
that run HasQueue(dsq.GetHash()) and
HasQueueFromMasternode(dsq.masternodeOutpoint, dsq.fReady) (and later
AddQueue(dsq)), so the hash-based duplicate fast-path uses the
normalized/validated proTxHash.

@knst knst force-pushed the refactor-simplify-cj-hierarchy branch from c363fff to eb2142d Compare March 27, 2026 13:22
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (2)
src/coinjoin/walletman.cpp (2)

251-273: ⚠️ Potential issue | 🟠 Major

Normalize m_protxHash before duplicate detection to prevent bypass.

HasQueue(dsq.GetHash()) at Line 254 computes the hash before dsq.m_protxHash is filled at Line 271-273. If a DSQUEUE arrives with a null m_protxHash, the hash used for duplicate detection differs from the hash of the normalized object added via AddQueue(). This allows bypassing the duplicate check.

Resolve the masternode (dmn) and normalize m_protxHash before the hash-based duplicate checks:

Proposed fix
     {
         LOCK(cs_ProcessDSQueue);
 
+        auto dmn = tip_mn_list.GetValidMNByCollateral(dsq.masternodeOutpoint);
+        if (!dmn) return ret;
+
+        if (dsq.m_protxHash.IsNull()) {
+            dsq.m_protxHash = dmn->proTxHash;
+        } else if (dsq.m_protxHash != dmn->proTxHash) {
+            // Reject mismatched proTxHash
+            ret.m_error = MisbehavingError{10};
+            return ret;
+        }
+
         if (m_queueman->HasQueue(dsq.GetHash())) return ret;
 
         if (m_queueman->HasQueueFromMasternode(dsq.masternodeOutpoint, dsq.fReady)) {
             // no way the same mn can send another dsq with the same readiness this soon
             LogPrint(BCLog::COINJOIN, /* Continued */
                      "DSQUEUE -- Peer %d is sending WAY too many dsq messages for a masternode with collateral %s\n",
                      from, dsq.masternodeOutpoint.ToStringShort());
             return ret;
         }
 
         LogPrint(BCLog::COINJOIN, "DSQUEUE -- %s new\n", dsq.ToString());
 
         if (dsq.IsTimeOutOfBounds()) return ret;
 
-        auto dmn = tip_mn_list.GetValidMNByCollateral(dsq.masternodeOutpoint);
-        if (!dmn) return ret;
-
-        if (dsq.m_protxHash.IsNull()) {
-            dsq.m_protxHash = dmn->proTxHash;
-        }
-
         if (!dsq.CheckSignature(dmn->pdmnState->pubKeyOperator.Get())) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/coinjoin/walletman.cpp` around lines 251 - 273, Move masternode
resolution and normalization of dsq.m_protxHash before the duplicate/hash
checks: call tip_mn_list.GetValidMNByCollateral(dsq.masternodeOutpoint) early,
and if it returns a valid dmn and dsq.m_protxHash.IsNull() set dsq.m_protxHash =
dmn->proTxHash, then perform m_queueman->HasQueue(dsq.GetHash()) and
m_queueman->HasQueueFromMasternode(...) as before; this ensures the hash used by
HasQueue/HasQueueFromMasternode matches the normalized object later added via
AddQueue().

181-186: ⚠️ Potential issue | 🔴 Critical

TOCTOU race condition still present in flushWallet().

getClient() releases cs_wallet_manager_map before returning. If removeWallet() is called concurrently, the unique_ptr is destroyed while flushWallet() still holds a dangling pointer, causing use-after-free. This issue was flagged previously and remains unaddressed.

Proposed fix
 void CJWalletManagerImpl::flushWallet(const std::string& name)
 {
-    auto* clientman = Assert(getClient(name));
-    clientman->ResetPool();
-    clientman->StopMixing();
+    LOCK(cs_wallet_manager_map);
+    auto it = m_wallet_manager_map.find(name);
+    if (it != m_wallet_manager_map.end()) {
+        it->second->ResetPool();
+        it->second->StopMixing();
+    }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/coinjoin/walletman.cpp` around lines 181 - 186, flushWallet() currently
calls getClient() which releases cs_wallet_manager_map before returning, leaving
clientman as a raw pointer and allowing removeWallet() to destroy the unique_ptr
concurrently; change the code so flushWallet() obtains a safe reference while
holding the wallet-manager map lock or uses a shared ownership type: either (A)
modify getClient()/CJWalletManagerImpl to return a std::shared_ptr to the client
manager (instead of a raw pointer) and use that shared_ptr in flushWallet(), or
(B) lock cs_wallet_manager_map inside flushWallet(), copy or move a
std::shared_ptr/borrowed handle to clientman while locked, then release the lock
and call ResetPool()/StopMixing() on the stable handle; ensure removeWallet()
still erases only after dropping its shared_ptr so no use-after-free occurs
(refer to symbols: flushWallet, getClient, removeWallet, cs_wallet_manager_map,
CJWalletManagerImpl, clientman).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/coinjoin/walletman.cpp`:
- Around line 43-44: The EXCLUSIVE_LOCKS_REQUIRED attribute is misformatted on
multi-line declarations (e.g., the processMessage method) causing clang-format
pipeline failures; run clang-format and adjust the declaration formatting so the
EXCLUSIVE_LOCKS_REQUIRED annotation is placed per project style (move it to the
correct position/line for multi-line function declarations or combine it with
the closing parenthesis as clang-format expects) for processMessage and the
other similar multi-line declarations (the other EXCLUSIVE_LOCKS_REQUIRED
occurrence around the same area) so the linting passes.
- Around line 120-125: Remove the early return in CJWalletManagerImpl::Schedule
that checks m_relay_txes so the scheduler always registers DoMaintenance;
specifically, delete the guard "if (!m_relay_txes) return;" in
CJWalletManagerImpl::Schedule and always call
scheduler.scheduleEvery(std::bind(&CJWalletManagerImpl::DoMaintenance, this,
std::ref(connman)), std::chrono::seconds{1}); Do not change DoMaintenance
itself—it already checks m_queueman before calling CheckQueue and
clientman->DoMaintenance can safely run regardless of m_relay_txes.

---

Duplicate comments:
In `@src/coinjoin/walletman.cpp`:
- Around line 251-273: Move masternode resolution and normalization of
dsq.m_protxHash before the duplicate/hash checks: call
tip_mn_list.GetValidMNByCollateral(dsq.masternodeOutpoint) early, and if it
returns a valid dmn and dsq.m_protxHash.IsNull() set dsq.m_protxHash =
dmn->proTxHash, then perform m_queueman->HasQueue(dsq.GetHash()) and
m_queueman->HasQueueFromMasternode(...) as before; this ensures the hash used by
HasQueue/HasQueueFromMasternode matches the normalized object later added via
AddQueue().
- Around line 181-186: flushWallet() currently calls getClient() which releases
cs_wallet_manager_map before returning, leaving clientman as a raw pointer and
allowing removeWallet() to destroy the unique_ptr concurrently; change the code
so flushWallet() obtains a safe reference while holding the wallet-manager map
lock or uses a shared ownership type: either (A) modify
getClient()/CJWalletManagerImpl to return a std::shared_ptr to the client
manager (instead of a raw pointer) and use that shared_ptr in flushWallet(), or
(B) lock cs_wallet_manager_map inside flushWallet(), copy or move a
std::shared_ptr/borrowed handle to clientman while locked, then release the lock
and call ResetPool()/StopMixing() on the stable handle; ensure removeWallet()
still erases only after dropping its shared_ptr so no use-after-free occurs
(refer to symbols: flushWallet, getClient, removeWallet, cs_wallet_manager_map,
CJWalletManagerImpl, clientman).
🪄 Autofix (Beta)

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

Run ID: 0131a9db-68c7-4cf9-bb31-ca09df52862e

📥 Commits

Reviewing files that changed from the base of the PR and between 267e340 and eb2142d.

📒 Files selected for processing (1)
  • src/coinjoin/walletman.cpp

Comment on lines 43 to +44
MessageProcessingResult processMessage(CNode& peer, CChainState& chainstate, CConnman& connman, CTxMemPool& mempool,
std::string_view msg_type, CDataStream& vRecv) override;
std::string_view msg_type, CDataStream& vRecv) override EXCLUSIVE_LOCKS_REQUIRED(!cs_ProcessDSQueue, !cs_wallet_manager_map);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Address remaining clang-format pipeline failures.

The pipeline is still reporting formatting mismatches for EXCLUSIVE_LOCKS_REQUIRED placement on multi-line declarations. Run clang-format to fix these before merging.

Also applies to: 77-78

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/coinjoin/walletman.cpp` around lines 43 - 44, The
EXCLUSIVE_LOCKS_REQUIRED attribute is misformatted on multi-line declarations
(e.g., the processMessage method) causing clang-format pipeline failures; run
clang-format and adjust the declaration formatting so the
EXCLUSIVE_LOCKS_REQUIRED annotation is placed per project style (move it to the
correct position/line for multi-line function declarations or combine it with
the closing parenthesis as clang-format expects) for processMessage and the
other similar multi-line declarations (the other EXCLUSIVE_LOCKS_REQUIRED
occurrence around the same area) so the linting passes.

Comment on lines 120 to 125
void CJWalletManagerImpl::Schedule(CConnman& connman, CScheduler& scheduler)
{
if (!m_relay_txes) return;
scheduler.scheduleEvery(std::bind(&CCoinJoinClientQueueManager::DoMaintenance, std::ref(*queueman)),
std::chrono::seconds{1});
scheduler.scheduleEvery(std::bind(&CoinJoinWalletManager::DoMaintenance, std::ref(walletman), std::ref(connman)),
scheduler.scheduleEvery(std::bind(&CJWalletManagerImpl::DoMaintenance, this, std::ref(connman)),
std::chrono::seconds{1});
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check how relay_txes=false is used and whether wallet maintenance is expected
rg -n -B3 -A3 'relay_txes.*false|relay_txes == false' --type cpp

Repository: dashpay/dash

Length of output: 584


🏁 Script executed:

#!/bin/bash
# Look at DoMaintenance implementation to see what it does
rg -n -A15 'void CJWalletManagerImpl::DoMaintenance' src/coinjoin/walletman.cpp

Repository: dashpay/dash

Length of output: 836


🏁 Script executed:

#!/bin/bash
# Check if wallets can be added/used when relay_txes=false
rg -n -B5 -A5 'relay_txes.*true|relay_txes.*=' src/coinjoin/walletman.cpp | head -50

Repository: dashpay/dash

Length of output: 38


🏁 Script executed:

#!/bin/bash
# Check if there are tests showing expected behavior when relay_txes=false
fd -e cpp -e h test | xargs rg -l 'relay_txes' 2>/dev/null || echo "No test files found with relay_txes"

Repository: dashpay/dash

Length of output: 91


🏁 Script executed:

#!/bin/bash
# Check initialization and usage of m_relay_txes in the class
rg -n 'm_relay_txes' src/coinjoin/walletman.cpp | head -20

Repository: dashpay/dash

Length of output: 242


🏁 Script executed:

#!/bin/bash
# Check if AddWallet/wallets can be added when relay_txes is false
rg -n -B5 -A5 'AddWallet|AddClient' src/coinjoin/walletman.cpp | head -40

Repository: dashpay/dash

Length of output: 38


🏁 Script executed:

#!/bin/bash
# Check the header file for m_relay_txes declaration and comments
cat src/coinjoin/walletman.h | grep -n -B3 -A3 'relay_txes'

Repository: dashpay/dash

Length of output: 528


🏁 Script executed:

#!/bin/bash
# Find where CJWalletManager::make() is called
rg -n 'CJWalletManager::make|CJWalletManager\(' --type cpp -B2 -A2

Repository: dashpay/dash

Length of output: 1906


🏁 Script executed:

#!/bin/bash
# Check if there are any comments about relay_txes meaning or usage
rg -n -B2 -A2 'relay.*tx|relay_txes' src/coinjoin/ --type cpp | head -60

Repository: dashpay/dash

Length of output: 4048


Remove the early return that gates wallet maintenance on relay_txes.

When m_relay_txes is false, Schedule() early returns and prevents all maintenance from running. However, the intent is to disable only queue processing (as documented: "Null when relay_txes is false (no queue processing)"), not wallet maintenance. The DoMaintenance() method already properly handles a null m_queueman with a null-check before calling CheckQueue() (line 196), so wallet-level maintenance via clientman->DoMaintenance() can safely run regardless. Remove the if (!m_relay_txes) return; guard and let the scheduler always invoke DoMaintenance().

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/coinjoin/walletman.cpp` around lines 120 - 125, Remove the early return
in CJWalletManagerImpl::Schedule that checks m_relay_txes so the scheduler
always registers DoMaintenance; specifically, delete the guard "if
(!m_relay_txes) return;" in CJWalletManagerImpl::Schedule and always call
scheduler.scheduleEvery(std::bind(&CJWalletManagerImpl::DoMaintenance, this,
std::ref(connman)), std::chrono::seconds{1}); Do not change DoMaintenance
itself—it already checks m_queueman before calling CheckQueue and
clientman->DoMaintenance can safely run regardless of m_relay_txes.

Copy link
Copy Markdown

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

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: eb2142db5b

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

{
LOCK(cs_ProcessDSQueue);

if (m_queueman->HasQueue(dsq.GetHash())) return ret;
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 DSQUEUE duplicate gate keyed by masternode outpoint

Replace this hash-only duplicate check with the previous outpoint-aware logic. CCoinJoinQueue::GetHash() is derived from serialized network fields and does not include masternodeOutpoint (SERIALIZE_METHODS in coinjoin.h), whereas the old q == dsq path compared outpoints. In this function, DSQUEUE messages with null m_protxHash are still accepted, so two different masternodes broadcasting the same denom/readiness/timestamp can collide on hash and cause the second valid queue to be dropped as a duplicate, reducing queue relay and join opportunities.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Legacy dsq-s are no longer a thing, m_protxHash can't be null.

@knst knst requested review from PastaPastaPasta and UdjinM6 March 27, 2026 18:10
Copy link
Copy Markdown

@UdjinM6 UdjinM6 left a comment

Choose a reason for hiding this comment

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

utACK eb2142d

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.

3 participants