Skip to content

test: cover CoinJoin client restart handling#7420

Draft
thepastaclaw wants to merge 1 commit into
dashpay:developfrom
thepastaclaw:test/coinjoin-mixing-functional-remake
Draft

test: cover CoinJoin client restart handling#7420
thepastaclaw wants to merge 1 commit into
dashpay:developfrom
thepastaclaw:test/coinjoin-mixing-functional-remake

Conversation

@thepastaclaw

@thepastaclaw thepastaclaw commented Jul 7, 2026

Copy link
Copy Markdown

Issue being fixed or feature implemented

Remakes the closed CoinJoin functional test PR #7407 as a signed, test-only
change focused on the active CoinJoin client lifetime path.

The test covers real regtest CoinJoin queue/session startup, participant restart
while a session is active, client stop/reset cleanup, and the masternode-side
CoinJoin RPC shape.

This was initially kept draft while PR #7259 fixed CoinJoin client-lifetime handling around wallet operations. #7259 merged on 2026-07-10, and this branch is now rebased onto develop containing the fix. It remains draft until the new-head TSAN and multiprocess jobs confirm the deadlock regression is gone.

What was done?

  • Added wallet_coinjoin_mixing.py.
  • Registered the test in test_runner.py.
  • Kept the remake test-only:
    • no C++ changes
    • no -coinjoinrandomrounds runtime option
    • no masternode connection behavior changes
  • Addressed test: add CoinJoin functional mixing coverage #7407 / test: cover CoinJoin client restart handling #7420 feedback:
    • signed replacement commit
    • dropped the unrelated runtime option / C++ changes
    • narrowed the test to deterministic restart/reset coverage instead of trying
      to cover full anonymization, reorgs, and mixed-fund spends in one PR
    • handles rapid wallet-backup shutdown warnings through a local helper
    • waits for CoinJoin session protxhash assignment before resolving the
      selected masternode
  • Documented the fix: dangling point to cj client #7259 dependency in the test and commit messages.

How Has This Been Tested?

Initial local validation in a built worktree:

./autogen.sh
./configure --without-gui --disable-bench --disable-fuzz-binary
make -j16 src/dashd src/dash-cli
python3 -m py_compile test/functional/feature_coinjoin_mixing.py test/functional/test_runner.py
test/lint/lint-python.py test/functional/feature_coinjoin_mixing.py test/functional/test_runner.py
git diff --check
test/functional/test_runner.py feature_coinjoin_mixing.py --jobs=1 --timeout-factor=2

After narrowing/renaming the test and addressing review feedback:

python3 -m py_compile test/functional/wallet_coinjoin_mixing.py
git diff --check
python3 test/functional/test_runner.py wallet_coinjoin_mixing.py

All local commands above passed. The functional test run printed the standard
warning that unrelated dashd processes were already running on the machine, but
the test completed successfully.

The branch is rebased on the merged #7259 fix. The PR remains draft while new-head CI confirms the TSAN and multiprocess deadlock failures are resolved.

Breaking Changes

None.

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 (for repository code-owners
    and collaborators only)

@thepastaclaw

Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

This PR adds a new functional test, wallet_coinjoin_mixing.py, which exercises CoinJoin mixing behavior across regtest wallet nodes and masternodes. It covers RPC availability checks on masternodes, mixing session startup and recovery after a wallet restart, stop/reset state transitions, and validation of observed sessions and queues. The test is also registered in test_runner.py's BASE_SCRIPTS list.

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

Sequence Diagram(s)

sequenceDiagram
  participant Test as WalletCoinJoinMixingTest
  participant WalletA
  participant WalletB
  participant Masternode

  Test->>WalletA: fund with denom/collateral UTXOs
  Test->>WalletB: fund with denom/collateral UTXOs
  Test->>WalletA: start mixing
  Test->>WalletB: start mixing
  WalletA->>Masternode: create mixing session/queue
  WalletB->>Masternode: join queue
  Test->>WalletA: stop node mid-session
  Test->>WalletB: verify release/reset and cleared locked UTXOs
  Test->>WalletA: restart node
  Test->>Test: pump_mixing (advance mocktime, mine blocks)
  Test->>WalletA: stop and reset mixing
  Test->>WalletB: stop and reset mixing
  Test->>Test: verify_sessions_and_queues
Loading

Related issues: None specified.

Related PRs: None specified.

Suggested labels: tests, functional-tests, coinjoin

Suggested reviewers: dash core functional-test maintainers familiar with CoinJoin/PrivateSend logic

🐕 A test pup sniffs through mixing queues,
Restarts nodes, checks locked UTXO clues,
Masternodes hide their client-side flair,
Sessions and queues verified with care,
Wagging its tail — the mix tests pass through!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
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.
Title check ✅ Passed The title accurately summarizes the new CoinJoin client restart-handling functional test.
Description check ✅ Passed The description is directly about the CoinJoin restart/reset test and runner registration.
✨ 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.

@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: 1

🤖 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 `@test/functional/feature_coinjoin_mixing.py`:
- Line 348: The restart sequence in feature_coinjoin_mixing.py is using direct
stop_node()/restart_node() calls even though the test enables wallet backups,
which can trigger BACKUP_EXISTS_WARNING on stderr. Update the affected node
restart in the coinjoin mixing test to use the backup-tolerant helper used
elsewhere in this suite, and apply it consistently around the node 2 restart so
the stop/start flow avoids the default stderr check.
🪄 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: 3581b034-dde9-45d2-84b7-fbf8f2c81571

📥 Commits

Reviewing files that changed from the base of the PR and between f2ae9f0 and bad0a36f51814393148a1a741fbc36b757266dd5.

📒 Files selected for processing (2)
  • test/functional/feature_coinjoin_mixing.py
  • test/functional/test_runner.py

Comment thread test/functional/feature_coinjoin_mixing.py Outdated
Comment thread test/functional/test_runner.py Outdated
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown

✅ No Merge Conflicts Detected

This PR currently has no conflicts with other open PRs.

@thepastaclaw
thepastaclaw force-pushed the test/coinjoin-mixing-functional-remake branch from bad0a36 to d02f297 Compare July 7, 2026 20:39
@thepastaclaw thepastaclaw changed the title test: add CoinJoin functional mixing coverage test: cover CoinJoin client restart handling Jul 7, 2026
@thepastaclaw

Copy link
Copy Markdown
Author

@coderabbitai review

Updated the PR after local build/test validation. Scope is now narrowed to deterministic CoinJoin client restart/reset coverage with the #7259 semantic dependency kept documented; full anonymization/reorg/spend coverage was removed from this PR because it was not passing reliably locally.

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

@thepastaclaw Thanks for the update — I'll re-review the PR with the narrowed scope in mind (deterministic CoinJoin client restart/reset coverage, with full anonymization/reorg/spend coverage removed for now due to local flakiness).

(=^・ω・^=) noted the scope change, digging in now


✏️ Learnings added
Learnt from: thepastaclaw
URL: https://github.com/dashpay/dash/pull/7420

Timestamp: 2026-07-07T20:39:51.440Z
Learning: In dashpay/dash PR `#7420` ("test: add CoinJoin functional mixing coverage"), the scope was narrowed after initial review: full anonymization/reorg/spend coverage in feature_coinjoin_mixing.py was removed because it was not passing reliably locally. The PR now focuses on deterministic CoinJoin client restart/reset coverage, while still documenting a semantic dependency on PR `#7259` for CoinJoin client-lifetime handling around wallet operations.

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.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@thepastaclaw

Copy link
Copy Markdown
Author

Handled knst's naming point in 7f80f9bd5cb: the narrowed test is wallet/client-side CoinJoin restart coverage now, so I renamed it to wallet_coinjoin_mixing.py and updated the runner entry. Validation: git diff --check and python3 -m py_compile test/functional/wallet_coinjoin_mixing.py passed; the earlier test_runner.py --list probe is not supported by this runner and failed in argument parsing, not from the rename.

@thepastaclaw

Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@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: 1

🤖 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 `@test/functional/wallet_coinjoin_mixing.py`:
- Around line 187-195: The polling predicate in the CoinJoin session handling
can fail too early when sessions[0].get('protxhash') is still None during
initialization. Update the logic around the wait_until callback in
wallet_coinjoin_mixing.py to return False until protxhash is assigned, before
calling get_mninfo or asserting on the masternode lookup. Keep the existing
connected_mns check and connection setup in the same predicate, but only proceed
once a valid protxhash is available.
🪄 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: 2f1d5da8-af53-4e0b-a44a-f0e6063b4203

📥 Commits

Reviewing files that changed from the base of the PR and between bad0a36f51814393148a1a741fbc36b757266dd5 and 7f80f9bd5cbdf3beb634477a89a90d1c80f73aa5.

📒 Files selected for processing (2)
  • test/functional/test_runner.py
  • test/functional/wallet_coinjoin_mixing.py
✅ Files skipped from review due to trivial changes (1)
  • test/functional/test_runner.py

Comment thread test/functional/wallet_coinjoin_mixing.py
@thepastaclaw

Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@thepastaclaw

Copy link
Copy Markdown
Author

CI note for the current head 4db7b4225b3: the remaining red jobs are expected for this draft.

Both linux64_tsan-test / Test source and linux64_multiprocess-test / Test source fail in wallet_coinjoin_mixing.py when the test reaches the active CoinJoin restart path. The failure is the existing CoinJoin client/wallet lock-order abort at src/coinjoin/client.cpp:697, which is the behavior this PR intentionally exposes and which depends on #7259 before the test can be green on develop.

All other visible checks passed, the test passes locally in the configured worktree, and the current review threads are resolved. I am leaving the branch unchanged rather than rerunning CI or pushing a no-op change.

@thepastaclaw
thepastaclaw force-pushed the test/coinjoin-mixing-functional-remake branch from 4db7b42 to bbfea64 Compare July 10, 2026 16:56
@thepastaclaw

thepastaclaw commented Jul 21, 2026

Copy link
Copy Markdown
Author

✅ Final review complete — no blockers (commit 9e09480)

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Final validation — Codex + Sonnet

The test is well scoped and the three historical review issues are resolved at the current head. Two assertions can still pass without exercising the participant-restart and reset-unlock paths the test claims to cover; the corrective follow-up commits and stale draft-status comment should also be cleaned up before merge.

Review provenance

  • Codex reviewers: gpt-5.6-sol — general (completed), gpt-5.6-sol — dash-core-commit-history (completed)
  • Verifier: gpt-5.6-sol — final-verifier (fallback)
  • Sonnet reviewers: claude-sonnet-5 — general (completed), claude-sonnet-5 — dash-core-commit-history (failed), claude-sonnet-5 — dash-core-commit-history (completed)

🟡 3 suggestion(s) | 💬 1 nitpick(s)

1 additional finding(s) omitted (not in diff).

🤖 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 `test/functional/wallet_coinjoin_mixing.py`:
- [SUGGESTION] test/functional/wallet_coinjoin_mixing.py:244-250: Wait for node 2 to join the active session
  `wait_for_joiner_visible_queue()` only returns after node 1 reports a session. The current `any(...)` predicate is therefore normally true immediately after `coinjoin('start')` on node 2, even though that RPC only enables mixing and does not synchronously create a session. Node 2 can be stopped before participating at all, while node 1 eventually times out and lets the test pass. Wait until both wallets report sessions using the same non-null masternode hash before restarting node 2.
- [SUGGESTION] test/functional/wallet_coinjoin_mixing.py:127-133: Assert reset cleanup before manually unlocking coins
  `lockunspent(True)` unconditionally unlocks every wallet output after `coinjoin reset`, so the later `listlockunspent() == []` checks pass even if reset failed to release CoinJoin's session locks. The node 2 check is weakened further by the preceding process restart, which inherently clears CoinJoin's non-persistent locks. `CCoinJoinClientManager::resetPool()` synchronously invokes `ResetPool()` and `UnlockCoins()` for each session before clearing the session deque, so assert those postconditions directly rather than repairing them first.

In `<commit:f8d93a0b>`:
- [SUGGESTION] <commit:f8d93a0b>:1: Squash the corrective commits into the test-introducing commit
  Commits `f8d93a0b` and `bbfea64b` only refine the new test introduced by `5a2ca996`: the first renames and reclassifies the file, and the second adds a null guard to its polling predicate. Neither is an independently useful historical step, and the intermediate test revision contains the known polling bug. Fold both into the introducing commit and replace its transient statement that the PR intentionally remains draft with durable rationale referencing the lifetime fix from #7259, which is already present in this branch's ancestry.

Comment thread test/functional/wallet_coinjoin_mixing.py Outdated
Comment thread test/functional/wallet_coinjoin_mixing.py Outdated
Comment thread test/functional/wallet_coinjoin_mixing.py Outdated
Add a regtest functional test for CoinJoin client queue/session startup,
participant restart while a shared session is active, stop/reset cleanup,
and masternode-side RPC behavior.

The restart path exercises the CoinJoin client lifetime handling fixed by
dashpay#7259 and keeps this regression coverage scoped to functional tests.
@thepastaclaw
thepastaclaw force-pushed the test/coinjoin-mixing-functional-remake branch from bbfea64 to 9e09480 Compare July 22, 2026 23:08
@thepastaclaw

Copy link
Copy Markdown
Author

Addressed the latest review findings in signed commit 9e09480c251:

  • wait for both wallets to report a session on the same non-null masternode before restarting node 2
  • verify coinjoin reset synchronously clears sessions and CoinJoin locks, without force-unlocking coins
  • replace the stale pre-fix: dangling point to cj client #7259 draft-status comment with durable regression rationale

The three-commit PR stack was folded into one clean test commit. Local validation passed: Python compilation, targeted repository flake8 checks, git diff --check, and wallet_coinjoin_mixing.py against the exact previous-head build.

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Final validation — Codex + Sonnet

This delta squashes the branch into a single clean commit and correctly fixes all four previously flagged issues: node 2 is now confirmed to have joined the same masternode session before being restarted, stop_and_reset_mixing asserts empty sessions/locked-unspent directly after the reset RPC instead of masking failures with a manual lockunspent, the corrective commits are folded into one coherent commit, and the stale draft-status comment was replaced with durable rationale. One legitimate test-coverage nitpick remains: the survivor's reset call at line 269 happens after CCoinJoinClientSession::CheckTimeout() has already unlocked coins via the automatic POOL_STATE_ERROR transition, so the adjacent listlockunspent()==[] assertion at that specific call site doesn't actually exercise reset-driven unlocking (that behavior is still validated elsewhere in test_stop_and_reset against a live, non-error session).

Review provenance

  • Codex reviewers: gpt-5.6-sol — general (completed), gpt-5.6-sol — dash-core-commit-history (completed)
  • Verifier: claude-sonnet-5 — final-verifier
  • Sonnet reviewers: claude-sonnet-5 — general (failed), claude-sonnet-5 — dash-core-commit-history (completed), claude-sonnet-5 — general (failed), claude-sonnet-5 — general (completed)

🟡 1 suggestion(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 `test/functional/wallet_coinjoin_mixing.py`:
- [SUGGESTION] test/functional/wallet_coinjoin_mixing.py:261-269: Survivor reset call doesn't exercise reset-driven coin unlocking
  survivor_released_session() waits until the survivor's session has disappeared or reached POOL_STATE_ERROR before calling coinjoin('reset'). In src/coinjoin/client.cpp, CCoinJoinClientSession::CheckTimeout() sets POOL_STATE_ERROR and calls UnlockCoins() synchronously in the same step (lines 314-315), before the deque entry ever surfaces the ERROR state to the test's polling predicate. By the time stop_and_reset_mixing([self.w1]) runs coinjoin('reset') at line 269, the session's locked coins have already been released by the automatic timeout path, not by CCoinJoinClientSession::ResetPool()'s own UnlockCoins() call. So the listlockunspent()==[] assertion added inside stop_and_reset_mixing passes here regardless of whether reset() itself unlocks coins correctly — this specific call site provides no coverage for that behavior (it's still covered by the test_stop_and_reset() call against a live, non-error session later in the test, so this is a coverage gap rather than a correctness bug).

Comment on lines +261 to +269
def survivor_released_session():
self.pump_mixing(wallets=[self.w1])
info = self.w1.getcoinjoininfo()
return not info['sessions'] or any(session['state'] == 'ERROR' for session in info['sessions'])

self.wait_until(survivor_released_session, timeout=120, sleep=0.25)
# Reset the survivor before restarting it so cleanup is attributable
# to the RPC rather than process restart.
self.stop_and_reset_mixing([self.w1])

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

🟡 Suggestion: Survivor reset call doesn't exercise reset-driven coin unlocking

survivor_released_session() waits until the survivor's session has disappeared or reached POOL_STATE_ERROR before calling coinjoin('reset'). In src/coinjoin/client.cpp, CCoinJoinClientSession::CheckTimeout() sets POOL_STATE_ERROR and calls UnlockCoins() synchronously in the same step (lines 314-315), before the deque entry ever surfaces the ERROR state to the test's polling predicate. By the time stop_and_reset_mixing([self.w1]) runs coinjoin('reset') at line 269, the session's locked coins have already been released by the automatic timeout path, not by CCoinJoinClientSession::ResetPool()'s own UnlockCoins() call. So the listlockunspent()==[] assertion added inside stop_and_reset_mixing passes here regardless of whether reset() itself unlocks coins correctly — this specific call site provides no coverage for that behavior (it's still covered by the test_stop_and_reset() call against a live, non-error session later in the test, so this is a coverage gap rather than a correctness bug).

source: ['codex']

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