Skip to content

fix: recheck live CoinJoin side coverage before finalizing timeout session - #7622

Open
PastaPastaPasta wants to merge 1 commit into
dashpay:developfrom
PastaPastaPasta:fix/coinjoin-finalization-coverage
Open

fix: recheck live CoinJoin side coverage before finalizing timeout session#7622
PastaPastaPasta wants to merge 1 commit into
dashpay:developfrom
PastaPastaPasta:fix/coinjoin-finalization-coverage

Conversation

@PastaPastaPasta

Copy link
Copy Markdown
Member

Issue being fixed

Follow-up to #7052 (promotion/demotion). CheckPool decides to finalize a timed-out POOL_STATE_ACCEPTING_ENTRIES session from a snapshot taken before ChargeFees(), but AddEntry can commit a pending DSVIN while that call runs since the session is still accepting entries. For example, a snapshot of 3 demotions is covered (0/3), but if the admitted set also contains 2 promoters, a promoter committing mid-fee-charge changes live sides to 1/3. CreateFinalTransaction previously only rechecked session identity/state and then published an uncovered transaction with a lone promoter.

What was done?

  • In CCoinJoinServer::CreateFinalTransaction, recheck live GetMixSideCountsLocked().IsCovered() and the minimum entry count under the same cs_coinjoin lock used to build the final transaction and transition to POOL_STATE_SIGNING.
  • Add src/test/coinjoin_inouts_tests.cpp:server_finalization_rechecks_live_side_coverage reproducing the 3D + 1P = 1/3 regression (must stay in accepting state).

How Has This Been Tested?

  • ./src/test/test_dash --run_test=coinjoin_inouts_tests/server_finalization_rechecks_live_side_coverage
  • Local build make -j14

This pull request was created by Codex.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@coderabbitai

coderabbitai Bot commented Aug 19, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 6c4b8bb6-4cf4-498c-9014-9ff700652466

📥 Commits

Reviewing files that changed from the base of the PR and between 3df8074 and b6f953e.

📒 Files selected for processing (1)
  • src/test/coinjoin_inouts_tests.cpp

Included review availability: Your plan provides up to 4 included reviews per hour; 2 remain after this review.


Walkthrough

CreateFinalTransaction now rechecks the live session under cs_coinjoin before building a final transaction. It rejects sessions with too few participants or incomplete promotion/demotion side coverage. The declaration moved within CCoinJoinServer without a signature change. Tests expose the method, update isman access, and verify that incomplete coverage keeps all entries in the accepting state.

Estimated code review effort: 2 (Simple) | ~10 minutes

Merge Risk: ⚪ Minimal · up to b6f95

This PR prevents finalization when live CoinJoin side coverage has changed during fee charging. No actionable merge-blocking risk remains after normal checks and review.

Suggested reviewers: knst, thepastaclaw, udjinm6

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
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 clearly and concisely describes the main change: rechecking live CoinJoin side coverage before finalization.
Description check ✅ Passed The description explains the affected timeout-session behavior, implementation, regression test, and validation steps.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 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.

@thepastaclaw

thepastaclaw commented Aug 19, 2026

Copy link
Copy Markdown

⛔ Blockers found — Opus deferred (commit b6f953e)
Canonical validated blockers: 1

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

The exact-head diff correctly revalidates the live participant count and side coverage while holding cs_coinjoin, preventing entry admission from invalidating eligibility before the session transitions to signing. The regression test directly exercises the uncovered 3-demotion/1-promotion state, and no in-scope defects were identified; the targeted test binary was not available in this worktree for execution.
Source: reviewers gpt-5.6-sol (general and dash-core-commit-history); final verifier gpt-5.6-sol. Orchestration-only: openclaw-agent/cliproxy/gpt-5.6-sol (not reviewer evidence).

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

Review provenance

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

@PastaPastaPasta
PastaPastaPasta force-pushed the fix/coinjoin-finalization-coverage branch from b091d07 to 3df8074 Compare August 20, 2026 13:32

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

The exact-head change revalidates the live participant minimum and side-coverage invariant while holding cs_coinjoin, preventing a concurrently admitted entry from producing an uncovered final transaction. The regression test exercises the 3-demotion/1-promotion state and confirms that the session remains open; no in-scope defects were identified.
Source: reviewers gpt-5.6-sol (general and dash-core-commit-history); final verifier claude-opus-4-6. Orchestration-only: openclaw-agent/cliproxy/gpt-5.6-sol (not reviewer evidence).

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

Review provenance

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

…ession

The timeout branch in CheckPool decides to finalize based on a snapshot
taken before ChargeFees, but entries can commit while that call runs
since the session stays in POOL_STATE_ACCEPTING_ENTRIES. This could
change a covered 0/3 side into an uncovered 1/3 side, which
CreateFinalTransaction previously accepted after only checking the
session id/state and then built from the live vecEntries.

Validate the live MixSideCounts and minimum entry count under the same
cs_coinjoin lock used to build the final transaction so the decision
cannot be invalidated between check and construction.

Add a unit test exercising the 3-demotion + 1-promotion (1/3)
regression that must stay in accepting state until the second
promoter arrives.
@PastaPastaPasta
PastaPastaPasta force-pushed the fix/coinjoin-finalization-coverage branch from 3df8074 to b6f953e Compare August 20, 2026 15:55
@PastaPastaPasta PastaPastaPasta changed the title fix(coinjoin): recheck live side coverage before finalizing timeout session fix: recheck live CoinJoin side coverage before finalizing timeout session Aug 20, 2026

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Preliminary review — Codex only

The live coverage recheck prevents construction of an uncovered final transaction, but its rejection path does not preserve the session in the production scheduler flow. Because the timeout remains expired, the immediately following timeout check charges fees again and resets the session, defeating the stated goal of waiting for the pending counterparty.
Source: reviewers gpt-5.6-sol (general and dash-core-commit-history); final verifier claude-opus-4-6. Orchestration-only: openclaw-agent/cliproxy/gpt-5.6-sol (not reviewer evidence).

Validated blockers were found in the Codex precheck. Opus is deferred until a fresh Codex revalidation clears the blocker gate.

Review provenance

  • Codex reviewers: gpt-5.6-sol — general (completed), gpt-5.6-sol — dash-core-commit-history (completed)
  • Verifier: gpt-5.6-sol — verifier
  • Sonnet: not run (deferred by blocker gate)

🔴 1 blocking

🤖 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/server.cpp`:
- [BLOCKING] src/coinjoin/server.cpp:385-388: Rejected finalization is immediately reset by CheckTimeout
  This return does not preserve the timed-out session in production. `Schedule()` invokes `CheckPool()` and then `CheckTimeout()` in the same callback. After `CreateFinalTransaction()` rejects the newly uncovered live state, `nState` remains `POOL_STATE_ACCEPTING_ENTRIES` and `nTimeLastSuccessfulStep` remains expired, so `CheckTimeout()` immediately invokes `ChargeFees()` a second time and calls `SetNull()`. The remaining admitted promoter therefore has almost no opportunity to restore coverage, contrary to the PR's stated behavior. Refresh or otherwise resolve the timeout before returning, and extend the regression test to exercise the `CheckPool()` followed by `CheckTimeout()` scheduler sequence rather than calling `CreateFinalTransaction()` alone.

Comment thread src/coinjoin/server.cpp
Comment on lines +385 to +388
if (vecEntries.size() < static_cast<size_t>(CoinJoin::GetMinPoolParticipants()) || !sides.IsCovered()) {
LogPrint(BCLog::COINJOIN, "CCoinJoinServer::CreateFinalTransaction -- session no longer eligible, entries=%d, sides=%d/%d\n",
vecEntries.size(), sides.inputs, sides.outputs);
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.

🔴 Blocking: Rejected finalization is immediately reset by CheckTimeout

This return does not preserve the timed-out session in production. Schedule() invokes CheckPool() and then CheckTimeout() in the same callback. After CreateFinalTransaction() rejects the newly uncovered live state, nState remains POOL_STATE_ACCEPTING_ENTRIES and nTimeLastSuccessfulStep remains expired, so CheckTimeout() immediately invokes ChargeFees() a second time and calls SetNull(). The remaining admitted promoter therefore has almost no opportunity to restore coverage, contrary to the PR's stated behavior. Refresh or otherwise resolve the timeout before returning, and extend the regression test to exercise the CheckPool() followed by CheckTimeout() scheduler sequence rather than calling CreateFinalTransaction() alone.

Suggested change
if (vecEntries.size() < static_cast<size_t>(CoinJoin::GetMinPoolParticipants()) || !sides.IsCovered()) {
LogPrint(BCLog::COINJOIN, "CCoinJoinServer::CreateFinalTransaction -- session no longer eligible, entries=%d, sides=%d/%d\n",
vecEntries.size(), sides.inputs, sides.outputs);
return;
if (vecEntries.size() < static_cast<size_t>(CoinJoin::GetMinPoolParticipants()) || !sides.IsCovered()) {
LogPrint(BCLog::COINJOIN, "CCoinJoinServer::CreateFinalTransaction -- session no longer eligible, entries=%d, sides=%d/%d\n",
vecEntries.size(), sides.inputs, sides.outputs);
nTimeLastSuccessfulStep = GetTime();
return;
}

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.

2 participants