Skip to content

fix: data races and check-then-act races in the CoinJoin server - #7537

Merged
PastaPastaPasta merged 6 commits into
dashpay:developfrom
PastaPastaPasta:fix/coinjoin-server-data-races
Oct 16, 2026
Merged

PastaPastaPasta merged 6 commits into
dashpay:developfrom
PastaPastaPasta:fix/coinjoin-server-data-races

Conversation

@PastaPastaPasta

@PastaPastaPasta PastaPastaPasta commented Aug 3, 2026 •

Copy link
Copy Markdown
Member

Issue being fixed or feature implemented

CCoinJoinServer state is touched by two threads: msghand (single-threaded, via ProcessDS*) and the 1s scheduler tick (CheckForCompleteQueue / CheckPool / CheckTimeout), plus RPC threads in GetJsonInfo(). CheckPool() runs on both: the scheduler tick and msghand via ProcessDSVIN / ProcessDSSIGNFINALTX.

Several pieces of the original version of this PR have since landed on develop separately (#7597 nSessionDenom atomic, #7599 entry admission bound to the session snapshot, plus the single-snapshot CheckPool() and the session-bound CreateFinalTransaction() from the promotion/demotion work). This PR is now rebased on top of those and contains what is still missing:

  1. CheckPool() was not single-flight. Two concurrent calls (scheduler + msghand) could both act on the same snapshot. If both finalize, clients receive DSFINALTX twice and sign twice, and the duplicate signatures make AddScriptSig() fail, which relays STATUS_REJECTED and aborts the session for everyone. Both calls could also commit the same final transaction.
  2. CommitFinalTransaction() was not bound to a session. It committed whatever finalMutableTransaction held, even when the session had timed out and been reset after the decision. That pushes a cleared transaction through ATMP and notifies participants of a failure that never happened.
  3. CheckTimeout() raced finalization and commit. It tested HasTimedOut() without a lock and could reset a session halfway through CheckPool() finalizing or committing it. It also reset sessions that could still advance, because the final collateral, entry or signature can arrive after the scheduler's CheckPool() snapshot. In that case msghand skips its own CheckPool() while the scheduler holds the guard.
  4. Offender selection and charging were not atomic with closing admission. ChargeFees() sampled nState three times, so it could select "didn't send" offenders and then charge and log them as "didn't sign". On the timeout-finalize path it ran before eligibility was rechecked, so an ineligible session was charged once there and again by the CheckTimeout() reset that followed.
  5. The session collaterals were not GUARDED_BY. vecSessionCollaterals was unannotated while its companion prevout index was guarded. CreateNewSession() still read it after releasing the lock, and nothing prevented the next unlocked read.
  6. The completion tail mutated live state. If every participant had disconnected, RelayCompletedTransaction() could reset the session. A replacement session could then open before random charging and the unconditional tail reset ran, and those would charge or clear the replacement. The invalid-transaction path also reset the pool before notifying anyone, so ERR_INVALID_TX reached nobody.
  7. A DSSIGNFINALTX message was not applied atomically. Each input went through its own locked AddScriptSig(). A signing timeout landing between two inputs of one already-received message would select the sender as a non-signer, charge them, and reset the session.

What was done?

Six commits, each standalone:

fix(coinjoin): decide server state transitions under cs_coinjoin: addresses 1, 2 and the triple sample in 4. A new cs_check_pool, acquired only via TRY_LOCK, makes CheckPool() single-shot without ever blocking msghand. Signature completeness joins the existing pool snapshot. CommitFinalTransaction() takes the session id the decision was made for and revalidates it. SetState() and IsSignaturesComplete() now require cs_coinjoin, and ChargeFees() samples nState once.

fix(coinjoin): guard the session collaterals with cs_coinjoin: addresses 5. The collateral transactions and their prevout index become a single SessionCollaterals member marked GUARDED_BY(cs_coinjoin), so they cannot drift apart or be annotated inconsistently. Reintroducing an unlocked read is now a -Wthread-safety compile error.

fix(coinjoin): serialize timeout transitions: addresses 3 (first half) and the rest of 4. CheckTimeout() uses the same cs_check_pool guard. Offender selection moves into SelectCollateralToCharge() under cs_coinjoin, in the same lock scope that resets the session (CheckTimeout()) or moves it to SIGNING (CreateFinalTransaction(session_id, charge_fees)). The timeout-finalize path only charges once the live session is confirmed eligible. Collateral is consumed after cs_coinjoin is released, because ConsumeCollateral() takes cs_main. The session may be reset before that consumption runs, so the selected collateral stays reserved in m_collaterals_being_charged (which survives SetNull()) until ConsumeCollateral() has finished. During that time CreateNewSession() and AddUserToExistingSession() refuse a dsa whose collateral spends one of its inputs. The charged participant therefore cannot be readmitted into the next session with a collateral that is about to be spent, and the fee is still taken. AddEntry()'s ConsumeCollateralIfCurrentSession() takes the same reservation.

fix(coinjoin): prioritize progress over timeout reset: addresses 3 (second half). Before resetting, CheckTimeout() rechecks under cs_coinjoin whether the session can still advance: queue ready, an entry for every collateral, enough entries covering both sides of the session denomination, or every signature in. If so it leaves the session to the next tick. The rules mirror the decisions CheckPool() and CreateFinalTransaction() make, so a deferred session is always acted on. The decision is the pure function ShouldResetTimedOutSession(), which CheckTimeout() calls with the session's progress read under cs_coinjoin.

fix(coinjoin): bind completion tail to session snapshot: addresses 6. The committed transaction, participants and collaterals are captured under cs_coinjoin. Completion notification is side-effect-free, and only the matching signing session is reset. The invalid-transaction path notifies the captured participants. CheckPool()'s uncovered-session reset captures its participants and resets in one step, then tells them.

fix(coinjoin): apply a DSSIGNFINALTX message atomically: addresses 7. The signing session the sender was checked against is revalidated after decoding, and the whole batch is applied in one cs_coinjoin critical section. The rejection relay for a failed input happens inside that same section.

Lock discipline

No new cs_coinjoin-across-cs_main edge is introduced: offender collateral is consumed only after cs_coinjoin is released, and signature verification already ran under cs_coinjoin. cs_check_pool is TRY_LOCK-only, taken only by CheckPool() and CheckTimeout() while holding nothing else, so cs_check_pool → cs_coinjoin / cs_main is unidirectional and no ABBA cycle is possible.

How Has This Been Tested?

macOS arm64, depends build, clang with -Wthread-safety -Werror, rebased on develop c8bfd7a.

  • Every one of the six commits builds test_dash and passes coinjoin_inouts_tests on its own.
  • Unit suites coinjoin_inouts_tests, coinjoin_tests, coinjoin_queue_tests, coinjoin_dstxmanager_tests pass.
  • Functional rpc_coinjoin.py and p2p_dstx.py pass.
  • lint-whitespace.py, lint-includes.py, lint-circular-dependencies.py pass.

New/updated unit tests in coinjoin_inouts_tests:

  • server_timeout_resets_only_sessions_that_cannot_advance is one table over ShouldResetTimedOutSession(). Rows that must be left alone: a session that has not timed out; a ready queue; an entry for every collateral (even with a side uncovered, which CheckPool() resets itself and tells the participants); enough covered entries with one straggler; every input signed. Rows that must be reset: a queue that is not ready; too few entries; enough entries with a side uncovered and one straggler; a missing signature. Mutations checked: removing the deferral, the signing-advance clause, either accepting-advance clause, or the side-coverage condition each fails at least one row.
  • server_finalization_rechecks_live_side_coverage now exercises the charging finalize path.

The cs_check_pool single-flight guard, the completion-tail snapshot and the DSSIGNFINALTX atomicity change close timing windows that cannot be reproduced deterministically in a unit test without driving private synchronization state, so they have no dedicated test. The completion tail is bound to the session id and participant list captured under cs_coinjoin, and the reset checks both the session id and nState, which is visible in the diff. They were verified by inspection and by the existing server_signfinaltx_* tests, which still pass. The charge reservation is in the same position: its window lies between two lock scopes inside one call. It was verified by checking that every reservation is released exactly once, after ConsumeCollateral() returns, on every path; that admission checks it under the same cs_coinjoin scope that admits; by -Wthread-safety -Werror; and by an independent code review.

Breaking Changes

None. No protocol, serialization or RPC change. Two debug log lines have vecSessionCollaterals.size(): renamed to participants:.

Checklist:

  • I have performed a self-review of my own code
  • I have made corresponding changes to the documentation
  • I have assigned this pull request to a milestone (for repository code-owners and collaborators only)

@coderabbitai

coderabbitai Bot commented Aug 3, 2026 •

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

CoinJoin session denomination storage is now atomic. Server collateral data uses synchronized session state. Pool checks and timeout handling use serialized execution and consistent session snapshots. Final transaction operations validate session IDs. Entry admission revalidates session state and rejects duplicate collateral prevouts. Lock contracts and concurrency tests were updated.

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

Sequence Diagram(s)

sequenceDiagram
  participant CoinJoinServer
  participant cs_check_pool
  participant cs_coinjoin
  participant Chainstate
  participant FinalTransaction
  CoinJoinServer->>cs_check_pool: serialize pool or timeout processing
  CoinJoinServer->>cs_coinjoin: capture session state and session_id
  CoinJoinServer->>Chainstate: validate entry or collateral
  Chainstate-->>CoinJoinServer: return validation result
  CoinJoinServer->>cs_coinjoin: revalidate session and capacity
  CoinJoinServer->>FinalTransaction: create or commit with session_id
  FinalTransaction->>cs_coinjoin: reject stale session
Loading

Possibly related PRs

Suggested reviewers: thepastaclaw, udjinm6

Pre-merge checks: Passed 4 · Failed 1

Failed 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 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 summarizes the main changes: fixing data races and check-then-act races in the CoinJoin server.
Description check Passed The description directly explains the CoinJoin race conditions addressed, the synchronization changes, testing, and scope of the pull request.
✨ 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.

@thepastaclaw

thepastaclaw commented Aug 3, 2026 •

Copy link
Copy Markdown
Collaborator

✅ Final review complete — no blockers (commit a231f05) · triage: normal

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d449bb2c8e

ℹ️ About Codex in GitHub

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

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

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

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

Comment thread src/coinjoin/server.cpp
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown

This pull request has conflicts, please rebase.

@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

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)

930-945: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Revalidate the session identity and the denomination under cs_coinjoin.

Line 930 compares dsa.nDenom to nSessionDenom without the lock. IsAcceptableDSA() at Line 919 runs a mempool test-accept before that. The revalidation at Line 942 only checks nSessionID == 0, not that the session is still the same session.

A scheduler-thread timeout can call SetNull(), and another DSACCEPT can open a new session with a different denomination, all inside that window. The observed values then belong to the old session, while the collateral at Line 960 is committed to the new one. The peer becomes a participant of a session whose denomination it never agreed to, and it is charged when it does not submit a matching entry.

Capture nSessionID before the expensive validation, then compare both the session ID and the denomination under the lock.

🐛 Proposed revalidation
-bool CCoinJoinServer::AddUserToExistingSession(const CCoinJoinAccept& dsa, PoolMessage& nMessageIDRet)
-{
-    // Cheap gates first: IsAcceptableDSA() below runs a mempool test-accept, which a full or
-    // absent session must not pay for.
-    if (nSessionID == 0 || WITH_LOCK(cs_coinjoin, return IsSessionReady())) return false;
+bool CCoinJoinServer::AddUserToExistingSession(const CCoinJoinAccept& dsa, PoolMessage& nMessageIDRet)
+{
+    // Cheap gates first: IsAcceptableDSA() below runs a mempool test-accept, which a full or
+    // absent session must not pay for.
+    const int session_id{nSessionID};
+    if (session_id == 0 || WITH_LOCK(cs_coinjoin, return IsSessionReady())) return false;
-    if (nSessionID == 0 || nState != POOL_STATE_QUEUE || IsSessionReady()) {
+    // The session that passed the checks above must still be the current one, with the same
+    // denomination: a reset plus a new session would otherwise admit this collateral to a
+    // session the peer never agreed to.
+    if (nSessionID != session_id || dsa.nDenom != nSessionDenom || nState != POOL_STATE_QUEUE ||
+        IsSessionReady()) {
         nMessageIDRet = ERR_MODE;
         return false;
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/coinjoin/server.cpp` around lines 930 - 945, Update
CCoinJoinServer::AddUserToExistingSession to capture the current session ID
before IsAcceptableDSA and the unlocked denomination validation, then under
cs_coinjoin require both nSessionID and nSessionDenom to match those captured
values. Reject with the existing error path if either changed, while preserving
the current state and readiness checks.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/coinjoin/server.cpp`:
- Around line 392-407: Revalidate nSessionID before every tail operation in
CCoinJoinServer::CommitFinalTransaction after the unlocked ATMP/signing/relay
work. Add a reset_if_current helper that locks cs_coinjoin and calls SetNull
only when nSessionID equals session_id, replace both WITH_LOCK(cs_coinjoin,
SetNull()) calls with it, and guard ChargeRandomFees and both
RelayCompletedTransaction calls with the same session check so a newer session
is never reset, charged, or notified.

---

Outside diff comments:
In `@src/coinjoin/server.cpp`:
- Around line 930-945: Update CCoinJoinServer::AddUserToExistingSession to
capture the current session ID before IsAcceptableDSA and the unlocked
denomination validation, then under cs_coinjoin require both nSessionID and
nSessionDenom to match those captured values. Reject with the existing error
path if either changed, while preserving the current state and readiness checks.
🪄 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 Plus

Run ID: bf00fe4b-c585-415c-a7cd-e1a42f94bc41

📥 Commits

Reviewing files that changed from the base of the PR and between ffaa783 and 8c005c8.

📒 Files selected for processing (4)
  • src/coinjoin/client.cpp
  • src/coinjoin/coinjoin.h
  • src/coinjoin/server.cpp
  • src/coinjoin/server.h

Comment thread src/coinjoin/server.cpp

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Preliminary review — Codex only

Two in-scope CoinJoin lifecycle races remain at the exact head. A validated entry can be committed after its session has stopped accepting entries, and the scheduler can reset a session while its guarded finalization or commit is still running; the lint-only follow-up commit should also be folded into the commits that introduced those log calls.

Validated blockers were found in the Codex precheck. Sonnet 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)

🔴 2 blocking | 🟡 1 suggestion(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 `src/coinjoin/server.cpp`:
- [BLOCKING] src/coinjoin/server.cpp:766-775: Revalidate the session state before committing the entry
  The commit-time check verifies only the session ID. While IsCollateralValid() or IsValidInOuts() is running, CheckPool() can take the timed-out ChargeAndFinalize path and transition the same session from POOL_STATE_ACCEPTING_ENTRIES to POOL_STATE_SIGNING without changing its ID. AddEntry() then appends an entry after finalMutableTransaction has already been built and relayed, so the accepted participant's input is absent from that transaction while its new unsigned entry prevents IsSignaturesComplete() from succeeding. The session eventually times out and may charge that participant. Require the captured session to still be accepting entries inside the locked commit block.
- [BLOCKING] src/coinjoin/server.cpp:297-301: Keep timeout resets inside the pool single-flight guard
  cs_check_pool serializes CheckPool() only. If the message-handling thread holds it while finalizing or committing, the scheduler skips CheckPool() at line 1098 but immediately runs the unguarded CheckTimeout() at line 1099. During finalization, CheckTimeout() can observe the old timed-out accepting state and then reset the session immediately after CreateFinalTransaction() relays DSFINALTX, causing all returned signatures to be rejected. During commit, it can clear vecEntries and the final transaction before RelayCompletedTransaction(MSG_SUCCESS), leaving clients unnotified even though the DSTX is relayed. CheckTimeout's charging and reset must use the same single-flight guard and skip the scheduler tick when that guard is contended.

In `<commit:8c005c8>`:
- [SUGGESTION] <commit:8c005c8>:1: Squash the lint-only follow-up into its originating commits
  Commit 8c005c841c6 only adds lint-logs.py continuation markers to calls introduced earlier in this PR. The CheckForCompleteQueue marker belongs in 4b8ba4e6b78, while the CreateNewSession and AddUserToExistingSession markers belong in abcaaeaee70. Fold those hunks into their originating commits and drop the standalone lint-fix commit so each commit remains independently lint-clean.

Comment thread src/coinjoin/server.cpp Outdated
Comment thread src/coinjoin/server.cpp
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown

This pull request has conflicts, please rebase.

@PastaPastaPasta
PastaPastaPasta force-pushed the fix/coinjoin-server-data-races branch from 8c005c8 to aaa6d04 Compare August 4, 2026 05:21

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: aaa6d0464a

ℹ️ About Codex in GitHub

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

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

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

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

Comment thread src/coinjoin/server.cpp Outdated

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Preliminary review — Codex only

All three carried-forward findings remain valid at head aaa6d04: two blocking CoinJoin lifecycle races and the suggestion to fold the lint-only follow-up into its originating commits. The latest rebase delta only adapts the ProcessGetData override to the updated base interface and introduces no new findings. The lifecycle races can admit an entry after finalization or reset a session during finalization/commit, so changes are still required.

Validated blockers were found in the Codex precheck. Sonnet 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)

🔴 2 blocking | 🟡 1 suggestion(s)

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

2 carried-forward finding(s) already raised on this PR; not re-posting as new inline comments.

🤖 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 `<commit:aaa6d04>`:
- [SUGGESTION] <commit:aaa6d04>:1: Squash the lint-only follow-up into its originating commits
  Commit aaa6d0464a4 only adds four lint-logs.py continuation markers to LogPrint calls introduced earlier in this PR. The CheckForCompleteQueue marker belongs in 96cf3768cab, while the CreateNewSession and AddUserToExistingSession markers belong in 7443d022e09. Fold those hunks into their originating commits and drop the standalone lint-only commit so each substantive commit is independently lint-clean.

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

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)

At exact head bb056c6, the AddEntry state revalidation and CheckTimeout single-flight serialization fix both previously blocking CoinJoin lifecycle races. Two non-blocking commit-history cleanups remain: fold the lint-only follow-up into its originating commits and split or fold the final review-correction rollup into the commits it completes.
Source: reviewers gpt-5.6-sol (general) and gpt-5.6-sol (dash-core-commit-history); final verifier gpt-5.6-sol. openclaw-agent/cliproxy/gpt-5.6-sol is orchestration-only and is 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)

🟡 2 suggestion(s)

2 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 `<commit:aaa6d04>`:
- [SUGGESTION] <commit:aaa6d04>:1: Squash the lint-only follow-up into its originating commits
  Commit aaa6d0464a4 only adds four lint-logs.py continuation markers to LogPrint calls introduced earlier in this PR. The CheckForCompleteQueue marker belongs in 96cf3768cab, while the CreateNewSession and AddUserToExistingSession markers belong in 7443d022e09. Fold those hunks into their originating commits and drop the standalone lint-only commit so each substantive commit is independently lint-clean.

In `<commit:bb056c6>`:
- [SUGGESTION] <commit:bb056c6>:1: Fold the final race corrections into the commits they complete
  Commit bb056c6d397 combines review corrections for two distinct earlier changes. The AddEntry state check completes the session revalidation introduced by cc270396780, while the CheckTimeout guard, atomic offender selection and timeout regression test complete the transition-serialization work introduced by 96cf3768cab. Fold the AddEntry hunk into cc270396780 and the timeout production changes into 96cf3768cab; the regression test can either accompany those changes or remain as a focused test commit.

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

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 locking and session-revalidation changes address the targeted CoinJoin races, and both prior commit-organization findings were resolved by rewriting the stack. The exact head cannot compile because src/coinjoin/server.cpp contains an unmatched closing brace; the cs_check_pool documentation is also stale now that CheckTimeout() shares the guard.
Source: reviewers gpt-5.6-sol (general) and gpt-5.6-sol (dash-core-commit-history); final verifier gpt-5.6-sol. openclaw-agent/cliproxy/gpt-5.6-sol is orchestration-only and 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 | 💬 1 nitpick(s)

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

In `src/coinjoin/server.cpp`:
- [BLOCKING] src/coinjoin/server.cpp:617: Remove the unmatched closing brace
  `CheckTimeout()` is fully closed on line 616, and this additional `}` has no namespace, class, or other surrounding scope to close. It leaves an unmatched brace at global scope, so the following `CheckForCompleteQueue()` definition cannot be parsed and this translation unit will fail to compile. Remove line 617.

In `src/coinjoin/server.h`:
- [NITPICK] src/coinjoin/server.h:99-103: Update the cs_check_pool lock-discipline comment
  The comment states that `cs_check_pool` is never taken by another code path, but `CheckTimeout()` now deliberately acquires it to serialize timeout resets with `CheckPool()` finalization and commit. Update this concurrency-sensitive documentation to describe both production users and preserve the explanation that contention causes a skipped round rather than blocking `msghand`.

Comment thread src/coinjoin/server.cpp Outdated
Comment thread src/coinjoin/server.h Outdated

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 865d0bc33a

ℹ️ About Codex in GitHub

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

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

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

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

Comment thread src/coinjoin/server.cpp

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

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)

At exact head 68ad451, the targeted CoinJoin races are addressed and both prior findings are fixed; no in-scope correctness blocker remains. Four non-blocking commit-history improvements remain: fold three follow-up corrections into their originating commits and document the rationale for the late concurrency fixes.
Source: reviewers gpt-5.6-sol (general) and gpt-5.6-sol (dash-core-commit-history); final verifier gpt-5.6-sol. openclaw-agent/cliproxy/gpt-5.6-sol is orchestration-only and 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)

🟡 4 suggestion(s)

4 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 `<commit:aaa6d04>`:
- [SUGGESTION] <commit:aaa6d04>:1: Fold the lint-only commit into its originating changes
  Commit aaa6d0464a4 only adds lint-logs continuation markers to LogPrint calls introduced earlier in this stack. Move the CheckForCompleteQueue marker into 96cf3768cab and the CreateNewSession and AddUserToExistingSession markers into 7443d022e09, then drop aaa6d0464a4. This keeps each substantive commit independently lint-clean and avoids retaining a CI-only follow-up.

In `<commit:bb056c6>`:
- [SUGGESTION] <commit:bb056c6>:1: Fold the entry-state correction into the entry revalidation commit
  Commit cc270396780 says an entry can only be committed to the session for which it was validated, but its commit-time gate checks only the session ID. Commit bb056c6d397 later completes that invariant by also requiring POOL_STATE_ACCEPTING_ENTRIES, alongside unrelated timeout-serialization work. Move the AddEntry state-check hunk into cc270396780 so that commit is complete and accurate on its own.
- [SUGGESTION] <commit:bb056c6>:1: Add rationale to the late concurrency-fix commit messages
  Commits bb056c6d397, 7da61132813, and 68ad4510c1f have empty bodies despite encoding non-obvious concurrency invariants. Add concise bodies explaining the timeout/finalization interleaving, why denomination validation must use a captured session snapshot, and why newly actionable work must take priority over timeout reset. This preserves the reasoning needed by future git-log and bisect readers without relying on the PR discussion.

In `<commit:865d0bc>`:
- [SUGGESTION] <commit:865d0bc>:1: Fold the pool-lock documentation correction into the code change
  Commit bb056c6d397 makes CheckTimeout acquire cs_check_pool but leaves the preceding comment claiming no other path takes the guard. Commit 865d0bc33a8 only corrects that stale documentation. Fold the final comment into bb056c6d397 and drop the documentation-only follow-up so the concurrency change documents its lock discipline when introduced.

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

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 addresses the targeted CoinJoin races, including timeout serialization, session-bound validation, progress-before-timeout handling, and completion-tail snapshots; no correctness blocker remains. Four non-blocking commit-history improvements remain valid: fold three corrective follow-ups into their originating commits and document the rationale for the late concurrency fixes.
Source: reviewers gpt-5.6-sol (general) and gpt-5.6-sol (dash-core-commit-history); final verifier gpt-5.6-sol. openclaw-agent/cliproxy/gpt-5.6-sol is orchestration-only and 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)

🟡 4 suggestion(s)

4 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 `<commit:aaa6d04>`:
- [SUGGESTION] <commit:aaa6d04>: Fold the lint-only commit into its originating changes
  Commit aaa6d0464a4 only adds four lint-logs continuation markers to calls introduced earlier in this stack. Move the CheckForCompleteQueue marker into 96cf3768cab and the three CreateNewSession/AddUserToExistingSession markers into 7443d022e09, then drop aaa6d0464a4. This keeps each substantive commit independently lint-clean and avoids retaining a CI-only follow-up.

In `<commit:bb056c6>`:
- [SUGGESTION] <commit:bb056c6>: Fold the entry-state correction into the entry revalidation commit
  Commit cc270396780 says an entry can only be committed to the session for which it was validated, but its commit-time gate checks only the session ID. Commit bb056c6d397 later completes that invariant by also requiring POOL_STATE_ACCEPTING_ENTRIES, alongside timeout-serialization work. Move the AddEntry state-check hunk into cc270396780 so that commit is complete and accurate on its own.
- [SUGGESTION] <commit:bb056c6>: Add rationale to the late concurrency-fix commit messages
  Commits bb056c6d397, 7da61132813, 68ad4510c1f, and f708bb3e396 have empty bodies despite encoding non-obvious concurrency invariants. Add concise bodies explaining the timeout/finalization interleaving, why validation uses a captured denomination, why actionable work takes priority over timeout reset, and why completion notification, fee charging, and reset remain bound to the captured session. This preserves the reasoning needed by future git-log and bisect readers without relying on the PR discussion.

In `<commit:865d0bc>`:
- [SUGGESTION] <commit:865d0bc>: Fold the pool-lock documentation correction into the code change
  Commit bb056c6d397 makes CheckTimeout acquire cs_check_pool but retains the preceding comment claiming no other path takes the guard. Commit 865d0bc33a8 only corrects that stale documentation. Fold the final comment into bb056c6d397 and drop the documentation-only follow-up so the concurrency change documents its lock discipline when introduced.

@PastaPastaPasta
PastaPastaPasta force-pushed the fix/coinjoin-server-data-races branch 2 times, most recently from dfbde94 to 597b549 Compare August 10, 2026 15:53

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

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)

At exact head 597b549, the CoinJoin session transitions, timeout handling, entry admission, collateral charging, and completion tail are consistently bound to locked session snapshots; no in-scope correctness issue remains. All four prior commit-history findings are fixed in the rewritten eight-commit stack, and the sole CodeRabbit concern is resolved by the shared pool guard plus snapshot-based completion handling.
Source: reviewers gpt-5.6-sol (general) and gpt-5.6-sol (dash-core-commit-history); final verifier gpt-5.6-sol. openclaw-agent/cliproxy/gpt-5.6-sol is orchestration-only and 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-server-data-races branch from 311fe44 to 1d59788 Compare October 13, 2026 18:10
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-13T21:01:21.257708Z a231f05 New commits
ℹ️ About Codex in GitHub

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

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

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-review — Final validation — Phase 1 + Phase 2

The series correctly serializes pool decisions and binds completion work to session snapshots, but offender collateral consumption remains outside the session ownership boundary. After a timeout reset or a completion-triggered reset, a replacement session can admit the same collateral before the deferred consumption occurs, causing the wrong participant to be charged. The timeout regression test also still lacks an independent negative case for the side-coverage condition.

🔴 1 blocking | 🟡 1 suggestion(s)

1 carried-forward finding(s) already raised on this PR; not re-posting as new inline comments.

Review provenance

Source: reviewer 1: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 2: gpt-6.1-sol (agent: phase2-reviewer, role: dash-core-commit-history); reviewer 3: muse-spark-1.3-contributor (agent: phase1-reviewer, role: general); reviewer 4: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 5: gpt-6.1-sol (agent: phase2-reviewer, role: dash-core-commit-history); final verifier: gpt-6.1-sol (agent: sol-verifier, role: final-verifier)

  • Triage: critical by gpt-6.1-sol (effort low) — The intricate concurrency changes in src/coinjoin/server.cpp directly alter signature application in ProcessDSSIGNFINALTX, collateral charging, and final transaction commitment, meeting the bar for changes to signatures and funds movement.
  • Phase 1 reviewers: muse-spark-1.3-contributor — general (completed, effort xhigh); agent phase1-reviewer
  • Phase 1 model: muse-spark-1.3-contributor — not quota-gated; passed over gemini-3.8-flash-high (antigravity below 15% reserve: weekly 14% left, 5h 100% left), glm-5.3-flash (not used above high effort; tier asks max)
  • Single stage: Phase 1 and Phase 2 reviewed this head side by side, with no blocker gate between them (triage tier)
  • Fresh final gate: an independent Phase-2 review ran after iterative findings were reconciled
  • Fresh verifier: gpt-6.1-sol — final-verifier; agent sol-verifier
  • Phase 2 reviewers: gpt-6.1-sol — general (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — dash-core-commit-history (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — general (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — dash-core-commit-history (completed, effort xhigh); agent phase2-reviewer
🤖 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:448-449: Prevent deferred offender collateral consumption after session ownership changes
  The collateral is selected while holding `cs_coinjoin`, but it is consumed only after that lock is released. In `CreateFinalTransaction()`, `RelayFinalTransaction()` runs before this call and can invoke `RelayStatus()`; if all entry participants have disconnected, `RelayStatus()` calls `SetNull()`. `CheckTimeout()` has the same ordering more directly: it calls `SetNull()` at lines 707-708 and consumes the previously selected collateral at lines 711-712. Neither path prevents `ProcessDSACCEPT()` from creating a replacement session while `cs_check_pool` remains held, because admission does not acquire that guard and session collaterals are not pinned in the mempool. The replacement can therefore admit the same collateral before `ConsumeCollateral()` runs, violating the no-charge reset behavior and potentially spending the replacement participant's collateral. Preserve explicit reservation/ownership of the selected collateral until consumption completes, or use an equivalent atomic protocol; a simple post-reset session-ID check would avoid the wrong charge but silently lose the intended fee.

In `src/test/coinjoin_inouts_tests.cpp`:
- [SUGGESTION] src/test/coinjoin_inouts_tests.cpp:516-530: Cover non-actionable sessions in the timeout regression test
  (existing thread: https://github.com/dashpay/dash/pull/7537#discussion_r4210151998)
  `SeedTimedOutSession()` hardcodes `MixShape::STANDARD` for every seeded collateral and entry at lines 227-235. Consequently, the accepting rows either have `entries == collaterals`, which intentionally defers to `CheckPool()`, or have fewer than the minimum entries; none independently exercises the `GetMixSideCountsLocked().IsCovered()` conjunct at `CheckTimeout()` lines 699-701. Removing only that conjunct would leave all current rows passing. Add a negative accepting-session case with enough entries but an uncovered side and a missing participant—for example, three demotions and one promotion received with a second promoter still missing—and assert `POOL_STATE_IDLE`. This specifically protects the side-coverage reset decision while retaining the intentional full-entry deferral behavior.

Comment thread src/coinjoin/server.cpp Outdated
Comment on lines +448 to +449
if (collateral_to_charge) {
ConsumeCollateral(collateral_to_charge);

@thepastaclaw thepastaclaw Oct 13, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

✅ Resolved at cdb4951b; see the replies below.

🔴 Blocking: Prevent deferred offender collateral consumption after session ownership changes

The collateral is selected while holding cs_coinjoin, but it is consumed only after that lock is released. In CreateFinalTransaction(), RelayFinalTransaction() runs before this call and can invoke RelayStatus(); if all entry participants have disconnected, RelayStatus() calls SetNull(). CheckTimeout() has the same ordering more directly: it calls SetNull() at lines 707-708 and consumes the previously selected collateral at lines 711-712. Neither path prevents ProcessDSACCEPT() from creating a replacement session while cs_check_pool remains held, because admission does not acquire that guard and session collaterals are not pinned in the mempool. The replacement can therefore admit the same collateral before ConsumeCollateral() runs, violating the no-charge reset behavior and potentially spending the replacement participant's collateral. Preserve explicit reservation/ownership of the selected collateral until consumption completes, or use an equivalent atomic protocol; a simple post-reset session-ID check would avoid the wrong charge but silently lose the intended fee.

source: gpt-6.1-sol (phase2-reviewer: general, dash-core-commit-history)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Valid, and this PR introduced it. On develop, ChargeFees() consumed the collateral before CheckTimeout()'s SetNull() and before finalization. This PR moved the selection under cs_coinjoin and the ATMP to after the lock is released, which opened the gap.

Impact: a collateral is only ever sent to the masternode, and only its owner can sign another tx that spends the same input. So a readmitted copy always belongs to the offender who was being charged, and no other participant could be charged through this path. What actually happened was that the charged participant could re-enter the next session with a collateral about to be spent.

Fixed in 0ba94a6 ("serialize timeout transitions") with an explicit reservation:

  • SelectCollateralToCharge() records the chosen collateral in m_collaterals_being_charged. It is guarded by cs_coinjoin and survives SetNull().
  • ConsumeReservedCollateral() releases the reservation only after ConsumeCollateral() returns.
  • Under cs_coinjoin, CreateNewSession() and AddUserToExistingSession() refuse a dsa whose collateral spends any input of a reserved one (ERR_INVALID_COLLATERAL).

The fee is kept, and once it is consumed the mempool keeps the collateral out. ConsumeCollateralIfCurrentSession() (AddEntry's charge) has the same gap against a concurrent timeout, so it takes the same reservation.

There is no unit test. The window sits between two lock scopes inside one call, and the server's interface can't observe it without a test hook. I verified it by reviewing the lock scopes, with clang -Wthread-safety -Werror, and with an independent review.


🤖 Posted autonomously by Claude on behalf of pasta.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Resolved (re-reviewed at cdb4951b): The selected collateral is now reserved under cs_coinjoin, admission rejects collaterals overlapping that reservation, and ConsumeReservedCollateral() releases it only after consumption completes. Because the reservation survives SetNull(), a replacement session cannot reuse the collateral during the deferred charging window.

@thepastaclaw thepastaclaw added the pastaclaw:changes-requested thepastaclaw's latest review requested changes label Oct 13, 2026
CheckTimeout() reset the pool while CheckPool() could be finalizing or committing the very same session on the message-handling thread: HasTimedOut() was tested without any lock and the reset could land between CheckPool()'s decision and its execution, clearing a live session mid-step. CheckTimeout() now takes the same cs_check_pool guard as CheckPool() - with TRY_LOCK, so a contended scheduler tick is skipped instead of blocking the message-handling thread - which makes timeout resets and finalize/commit single-flight.

Offender selection also moves under cs_coinjoin, into SelectCollateralToCharge(), and into the same lock scope that closes the corresponding admission path: CheckTimeout() selects and resets atomically, and CreateFinalTransaction() selects and transitions to SIGNING atomically, so an entry that crossed the cutoff on time can no longer be charged as missing. The timeout finalization path also only charges once the live session is confirmed eligible; previously ChargeFees() ran first, and an ineligible session was charged again by the CheckTimeout() reset that followed. The collateral is consumed only after the lock is released, because ConsumeCollateral() takes cs_main and mempool submission must not run under cs_coinjoin.

The session can be reset before that consumption runs: CheckTimeout() resets it right away, and RelayStatus() can reset it while CreateFinalTransaction() relays the final transaction. Session collaterals are only test-accepted, so until the consumption reaches the mempool nothing stops the charged participant from being readmitted into the next session with a collateral that is about to be spent: the same one, or a re-signed one spending the same input. The selected collateral is therefore reserved until ConsumeCollateral() has finished, and CreateNewSession() and AddUserToExistingSession() refuse a dsa whose collateral spends one of its inputs. The fee is still taken, and afterwards the mempool keeps the consumed collateral out. A post-reset session check would have avoided the readmission only by dropping the fee. AddEntry() consumes through ConsumeCollateralIfCurrentSession(), which has the same gap against a concurrent timeout, and takes the same reservation.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@PastaPastaPasta
PastaPastaPasta force-pushed the fix/coinjoin-server-data-races branch from 1d59788 to b7893f5 Compare October 13, 2026 19:13
@thepastaclaw thepastaclaw removed the pastaclaw:changes-requested thepastaclaw's latest review requested changes label Oct 13, 2026

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b7893f5815

ℹ️ About Codex in GitHub

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

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

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

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

Comment thread src/test/coinjoin_inouts_tests.cpp Outdated
Comment on lines +224 to +227
nSessionID = 1;
nState = state;
nTimeLastSuccessfulStep = GetTime() -
(state == POOL_STATE_SIGNING ? COINJOIN_SIGNING_TIMEOUT : COINJOIN_QUEUE_TIMEOUT);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Test timeout transitions through the public surface

This helper constructs the whole timed-out session by assigning the protected session ID, state, timer, collateral map, and entry vector directly, so the regression can pass even if normal DSA admission or state transitions can never produce the seeded invariant. The repository requires unit tests to use standard fixtures and the public surface rather than injected internal state, and directs unobservable cases to an extracted testable function or functional test; restructure this setup instead of adding a state-injection seam.

AGENTS.md reference: AGENTS.md:L134-L134

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 562150b (folded into "prioritize progress over timeout reset"). The timeout decision is now a pure function, ShouldResetTimedOutSession(const CoinJoinSessionProgress&). CheckTimeout() fills the progress under cs_coinjoin and calls it. The test calls that function directly with one row per case, plus the negative rows: not timed out, queue not ready, too few entries, a side uncovered with a straggler, a signature missing. SeedTimedOutSession() is gone, and SetNull() is private again. Mutation-checked: removing the deferral, the signing-advance clause, either accepting-advance clause, or the side-coverage check each makes a row fail.


🤖 Posted autonomously by Claude on behalf of pasta.

Comment thread src/coinjoin/server.cpp
// From here on the mempool, not the reservation, keeps a consumed collateral out of new sessions.
LOCK(cs_coinjoin);
const auto it = std::ranges::find(m_collaterals_being_charged, txref);
if (Assume(it != m_collaterals_being_charged.end())) m_collaterals_being_charged.erase(it);

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 Preserve the reservation through in-flight admission

After the scheduler's CheckTimeout() resets a session, a new DSACCEPT can validate the same prevout in IsAcceptableDSA() before ConsumeCollateral() wins cs_main, then be delayed while the consume succeeds and this line erases the reservation. Its final admission check consequently sees neither a reservation nor the now-stale validation result and accepts a collateral whose input is already spent, stalling the replacement session; retain a consumed-prevout epoch/tombstone through in-flight admission or revalidate after observing reservation removal.

AGENTS.md reference: AGENTS.md:L247-L247

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The race is real, but it gives nobody a new capability, so I'm not adding machinery for it. Only two parties can resubmit that prevout: its owner, or someone replaying a collateral transaction the owner already signed. Either way the input is spent by the time the admission lands. AddEntry()'s own IsCollateralValid() then rejects that participant's entry with ERR_INVALID_COLLATERAL and charges nothing, and charging it at the replacement session's timeout is a no-op because ATMP fails. The worst case is one dead slot that holds the session until timeout. Anyone can already cause that: the owner by double-spending its collateral right after admission, and a replayer by taking a slot with any unspent collateral it has observed. That existing replay is worse for the victim, since that collateral can actually be charged. Closing this window would take a consume-generation counter plus a revalidation under cs_coinjoin, for no change in what an attacker can do.


🤖 Posted autonomously by Claude on behalf of pasta.

@PastaPastaPasta
PastaPastaPasta force-pushed the fix/coinjoin-server-data-races branch from b7893f5 to cdb4951 Compare October 13, 2026 19:34

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-review — Final validation — Phase 1 + Phase 2

The current head addresses the two previously reported issues: deferred collateral remains reserved through consumption, and timeout coverage now exercises both actionable and reset paths. One remaining admission race can still accept a collateral after its reservation is cleared but before an in-flight DSACCEPT commits, leaving a spent collateral occupying a replacement session slot.

🟡 1 suggestion(s)

Review provenance

Source: reviewer 1: muse-spark-1.3-contributor (agent: phase1-reviewer, role: general); reviewer 2: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 3: gpt-6.1-sol (agent: phase2-reviewer, role: dash-core-commit-history); reviewer 4: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 5: gpt-6.1-sol (agent: phase2-reviewer, role: dash-core-commit-history); final verifier: gpt-6.1-sol (agent: sol-verifier, role: final-verifier)

  • Triage: normal by gpt-6.1-sol (effort low) — This is a large and intricate concurrency fix in CoinJoin server state management, but it does not itself change consensus, funds movement, cryptography, signatures or key handling, peer-facing deserialization, or storage migrations.
  • Phase 1 reviewers: muse-spark-1.3-contributor — general (completed, effort xhigh); agent phase1-reviewer
  • Phase 1 model: muse-spark-1.3-contributor — not quota-gated; passed over gemini-3.8-flash-high (antigravity below 15% reserve: weekly 14% left, 5h 100% left), glm-5.3-flash (not used above high effort; tier asks max)
  • Fresh final gate: an independent Phase-2 review ran after iterative findings were reconciled
  • Fresh verifier: gpt-6.1-sol — final-verifier; agent sol-verifier
  • Phase 2 reviewers: gpt-6.1-sol — general (completed, effort high); agent phase2-reviewer, gpt-6.1-sol — dash-core-commit-history (completed, effort high); agent phase2-reviewer, gpt-6.1-sol — general (completed, effort high); agent phase2-reviewer, gpt-6.1-sol — dash-core-commit-history (completed, effort high); agent phase2-reviewer
🤖 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`:
- [SUGGESTION] src/coinjoin/server.cpp:638: Preserve the reservation through in-flight admission
  `ConsumeReservedCollateral()` erases `m_collaterals_being_charged` immediately after consuming the collateral. However, `CreateNewSession()` and `AddUserToExistingSession()` perform `IsAcceptableDSA()` before taking their final `cs_coinjoin` admission lock, and only then check `SpendsCollateralBeingCharged()`. An in-flight DSACCEPT can therefore validate the collateral while it is still unspent, wait for `ConsumeReservedCollateral()` to consume and erase the reservation, and then pass the final reservation check. The replacement session accepts a collateral whose input is already spent; its later entry submission fails validation and leaves a dead participant slot until timeout. Preserve a consumed-prevout tombstone or generation through in-flight admission, and reject an admission whose validation predates the corresponding consumption.

Comment thread src/coinjoin/server.cpp
// From here on the mempool, not the reservation, keeps a consumed collateral out of new sessions.
LOCK(cs_coinjoin);
const auto it = std::ranges::find(m_collaterals_being_charged, txref);
if (Assume(it != m_collaterals_being_charged.end())) m_collaterals_being_charged.erase(it);

@thepastaclaw thepastaclaw Oct 13, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

⏭️ Deferred (maintainer decision; not blocking) at cdb4951b; see the replies below.

🟡 Suggestion: Preserve the reservation through in-flight admission

ConsumeReservedCollateral() erases m_collaterals_being_charged immediately after consuming the collateral. However, CreateNewSession() and AddUserToExistingSession() perform IsAcceptableDSA() before taking their final cs_coinjoin admission lock, and only then check SpendsCollateralBeingCharged(). An in-flight DSACCEPT can therefore validate the collateral while it is still unspent, wait for ConsumeReservedCollateral() to consume and erase the reservation, and then pass the final reservation check. The replacement session accepts a collateral whose input is already spent; its later entry submission fails validation and leaves a dead participant slot until timeout. Preserve a consumed-prevout tombstone or generation through in-flight admission, and reject an admission whose validation predates the corresponding consumption.

source: muse-spark-1.3-contributor (phase1-reviewer: general)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Same race as the Codex thread on this line (#7537 (comment)). I'm deliberately not adding a tombstone or generation for it. The only way to win it is to resubmit that exact prevout, which takes either its owner or someone replaying the owner's already-signed collateral transaction. Either party can already leave a dead slot in a session today by spending or replaying a collateral right after admission. Once the input is spent, AddEntry() rejects that participant with ERR_INVALID_COLLATERAL, charges no one, and the slot clears at the session timeout. The reservation exists so a reset can't hand a collateral that's being charged to a new session as a valid one. That still holds; this window adds no new capability.


🤖 Posted autonomously by Claude on behalf of pasta.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

You're right that this does not introduce a new capability: the same stale-validation window already exists when a collateral is spent or replayed immediately after admission, and AddEntry() rejects the now-invalid collateral without charging it. The reservation still protects the reset-to-new-session interval until consumption completes; I accept the deliberate decision not to add a tombstone or generation here.

Noted as intentionally deferred; I will not press it further here.

@thepastaclaw thepastaclaw added the pastaclaw:commented thepastaclaw's latest review was comment-only label Oct 13, 2026

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-review after discussion — commit cdb4951

Standing review was COMMENTED; this re-review is APPROVE: no blocking findings remain.

Approved: the discussion withdrew or deferred every finding the full review of cdb4951b (run 4891) raised, and the approval rests on that review.

Same commit as the standing review; the inline threads above carry the per-finding outcome. This follow-up exists only to correct the verdict.

Review provenance

Source: conversation lane gpt-6.1-sol (agent: conversation); no reviewer or verifier lanes ran for this follow-up

  • Verdict moved because the inline discussion withdrew or deferred findings of the standing review of this commit; the code was not re-reviewed

@thepastaclaw thepastaclaw added pastaclaw:approved thepastaclaw's latest review approved this PR and removed pastaclaw:commented thepastaclaw's latest review was comment-only labels Oct 13, 2026
PastaPastaPasta and others added 3 commits October 13, 2026 15:51
The scheduler calls CheckPool() and CheckTimeout() separately, leaving a gap in which the final collateral, entry, or signature can arrive after CheckPool() takes its snapshot. The message thread then skips its own CheckPool() while the scheduler holds cs_check_pool, and an unconditional timeout reset would discard a session that can now advance.

Recheck readiness, finalizability (including session-denomination side coverage, as CheckPool() and CreateFinalTransaction() require) and signature completeness under cs_coinjoin before resetting. Actionable work takes priority and is picked up by the next scheduler tick. The decision is a pure function of the session's progress, ShouldResetTimedOutSession(), so the unit test covers each case directly instead of seeding a timed-out session into the server.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Completion relay previously read and mutated live session state after CommitFinalTransaction() released cs_coinjoin. If all old participants disconnected, RelayCompletedTransaction() could reset the session, allowing a replacement session to open before random charging and the unconditional tail reset; those operations could then charge or clear the replacement. The invalid-transaction path also reset the pool before notifying anyone, so its ERR_INVALID_TX went to an empty participant list.

Capture the committed transaction, participants, and collaterals under cs_coinjoin. Relay and charge only those snapshots, keep completion notification side-effect-free, and reset only the matching signing session. The invalid-transaction path now notifies the captured participants before reset, and CheckPool()'s uncovered-session reset captures its participants and resets in one step before telling them.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A DSSIGNFINALTX message can carry several signed inputs, but each was applied through a separate AddScriptSig() call that took and released cs_coinjoin on its own, and the signing-state and participant checks were released before the body was even decoded. At the signing-timeout boundary the scheduler's CheckTimeout() could take the lock between two inputs of the same already-received message, still find the sender's later inputs unsigned, select the sender as a non-signer to charge, and reset the session; the remaining inputs then failed against the cleared pool.

Capture the signing session the sender was checked against, then revalidate it after decoding and apply the whole batch under one cs_coinjoin critical section, relaying a rejection from inside the same section if an input fails. AddScriptSig() now requires cs_coinjoin. The lock is released before CheckPool(), which then commits the fully signed session; CheckTimeout() sees IsSignaturesComplete() and defers to it. No cs_main is taken in this section: signature verification already ran under cs_coinjoin inside AddScriptSig().

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@PastaPastaPasta
PastaPastaPasta force-pushed the fix/coinjoin-server-data-races branch from cdb4951 to a231f05 Compare October 13, 2026 20:52
@PastaPastaPasta

Copy link
Copy Markdown
Member Author

Fixed the linux64_tsan-build failure: clang's -Wmissing-designated-field-initializers rejected two timeout-table rows that leave out sides. CoinJoinSessionProgress::sides is now value-initialized (sides{}), like the struct's other members. The fix is folded into "prioritize progress over timeout reset" (now d1e23c7, head a231f05), and no other commit changed. Compiling the test with that warning as an error reproduces both CI errors on the previous header and is clean on the new one. The four coinjoin unit suites pass. #7568 is restacked on top with its commits unchanged.


🤖 Posted autonomously by Claude on behalf of pasta.

@thepastaclaw thepastaclaw removed the pastaclaw:approved thepastaclaw's latest review approved this PR label Oct 13, 2026

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-review — Final validation — Phase 1 + Phase 2

The complete six-commit CoinJoin server change was independently reviewed at the exact head. The session-state locking, timeout deferral, collateral reservation, session snapshots, and atomic signature handling are consistent with the stated race fixes; no new in-scope correctness or security findings remain. CI evidence shows formatting, merge, and main checks passing, while several build and lint jobs were still queued at the snapshot.

🔴 0 blocking | 🟡 0 suggestion(s) | 💬 0 nitpick(s)

Review provenance

Source: reviewer 1: muse-spark-1.3-contributor (agent: phase1-reviewer, role: general); reviewer 2: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 3: gpt-6.1-sol (agent: phase2-reviewer, role: dash-core-commit-history); reviewer 4: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 5: gpt-6.1-sol (agent: phase2-reviewer, role: dash-core-commit-history); final verifier: gpt-6.1-sol (agent: sol-verifier, role: final-verifier)

  • Triage: normal by gpt-6.1-sol (effort low) — This is a substantial concurrency and state-machine change in CoinJoin server logic, but the diff does not itself alter consensus rules, funds movement, cryptography, signatures or key handling, peer-facing deserialization, or storage migrations.
  • Phase 1 reviewers: muse-spark-1.3-contributor — general (completed, effort xhigh); agent phase1-reviewer
  • Phase 1 model: muse-spark-1.3-contributor — not quota-gated; passed over gemini-3.8-flash-high (antigravity below 15% reserve: weekly 14% left, 5h 100% left), glm-5.3-flash (not used above high effort; tier asks max)
  • Fresh final gate: an independent Phase-2 review ran after iterative findings were reconciled
  • Fresh verifier: gpt-6.1-sol — final-verifier; agent sol-verifier
  • Phase 2 reviewers: gpt-6.1-sol — general (completed, effort high); agent phase2-reviewer, gpt-6.1-sol — dash-core-commit-history (completed, effort high); agent phase2-reviewer, gpt-6.1-sol — general (completed, effort high); agent phase2-reviewer, gpt-6.1-sol — dash-core-commit-history (completed, effort high); agent phase2-reviewer
🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify the current code and confirm that no unresolved issues remain.

No unresolved findings remain from the prior review on this head.

@thepastaclaw thepastaclaw added the pastaclaw:approved thepastaclaw's latest review approved this PR label Oct 13, 2026
@PastaPastaPasta

Copy link
Copy Markdown
Member Author

The linux64_tsan-test failure at a231f05 looks unrelated to this PR.

  • One test failed: p2p_blocksonly.py, on all 3 attempts. It waits for a peer with relay permission to relay a transaction to a second peer while the node is in -blocksonly mode, and the wait_until timed out after 240 s. The test exercises no CoinJoin code, and the log has no ThreadSanitizer reports.
  • The same commits pass this job elsewhere: fix: don't charge CoinJoin participants for failures the masternode can't attribute to them #7568 contains this PR's six commits unchanged, and its linux64_tsan-test passed on head e328cd7.
  • Other first-attempt failures passed on retry: feature_llmq_chainlocks, feature_dip4_coinbasemerkleroots, feature_llmq_singlenode and feature_timestampindex.

A re-run of the job should clear it. I don't have permission to trigger one.


🤖 Posted autonomously by Claude on behalf of pasta.

@PastaPastaPasta
PastaPastaPasta merged commit 6d5ffe8 into dashpay:develop Oct 16, 2026
88 of 89 checks passed
@thepastaclaw thepastaclaw removed the pastaclaw:approved thepastaclaw's latest review approved this PR label Oct 16, 2026
PastaPastaPasta added a commit to PastaPastaPasta/dash that referenced this pull request Oct 16, 2026
…oinJoin server

a231f05 fix(coinjoin): apply a DSSIGNFINALTX message atomically (pasta)
873295f fix(coinjoin): bind completion tail to session snapshot (pasta)
d1e23c7 fix(coinjoin): prioritize progress over timeout reset (pasta)
0ba94a6 fix(coinjoin): serialize timeout transitions (pasta)
97daa21 fix(coinjoin): guard the session collaterals with cs_coinjoin (pasta)
3fdd82d fix(coinjoin): decide server state transitions under cs_coinjoin (pasta)

Pull request description:

  ## Issue being fixed or feature implemented

  `CCoinJoinServer` state is touched by two threads: `msghand` (single-threaded, via `ProcessDS*`) and the 1s scheduler tick (`CheckForCompleteQueue` / `CheckPool` / `CheckTimeout`), plus RPC threads in `GetJsonInfo()`. `CheckPool()` runs on **both**: the scheduler tick and `msghand` via `ProcessDSVIN` / `ProcessDSSIGNFINALTX`.

  Several pieces of the original version of this PR have since landed on develop separately (dashpay#7597 `nSessionDenom` atomic, dashpay#7599 entry admission bound to the session snapshot, plus the single-snapshot `CheckPool()` and the session-bound `CreateFinalTransaction()` from the promotion/demotion work). This PR is now rebased on top of those and contains what is still missing:

  1. **`CheckPool()` was not single-flight.** Two concurrent calls (scheduler + `msghand`) could both act on the same snapshot. If both finalize, clients receive `DSFINALTX` twice and sign twice, and the duplicate signatures make `AddScriptSig()` fail, which relays `STATUS_REJECTED` and aborts the session for everyone. Both calls could also commit the same final transaction.
  2. **`CommitFinalTransaction()` was not bound to a session.** It committed whatever `finalMutableTransaction` held, even when the session had timed out and been reset after the decision. That pushes a cleared transaction through ATMP and notifies participants of a failure that never happened.
  3. **`CheckTimeout()` raced finalization and commit.** It tested `HasTimedOut()` without a lock and could reset a session halfway through `CheckPool()` finalizing or committing it. It also reset sessions that could still advance, because the final collateral, entry or signature can arrive after the scheduler's `CheckPool()` snapshot. In that case `msghand` skips its own `CheckPool()` while the scheduler holds the guard.
  4. **Offender selection and charging were not atomic with closing admission.** `ChargeFees()` sampled `nState` three times, so it could select "didn't send" offenders and then charge and log them as "didn't sign". On the timeout-finalize path it ran before eligibility was rechecked, so an ineligible session was charged once there and again by the `CheckTimeout()` reset that followed.
  5. **The session collaterals were not `GUARDED_BY`.** `vecSessionCollaterals` was unannotated while its companion prevout index was guarded. `CreateNewSession()` still read it after releasing the lock, and nothing prevented the next unlocked read.
  6. **The completion tail mutated live state.** If every participant had disconnected, `RelayCompletedTransaction()` could reset the session. A replacement session could then open before random charging and the unconditional tail reset ran, and those would charge or clear the replacement. The invalid-transaction path also reset the pool *before* notifying anyone, so `ERR_INVALID_TX` reached nobody.
  7. **A `DSSIGNFINALTX` message was not applied atomically.** Each input went through its own locked `AddScriptSig()`. A signing timeout landing between two inputs of one already-received message would select the sender as a non-signer, charge them, and reset the session.

  ## What was done?

  Six commits, each standalone:

  **`fix(coinjoin): decide server state transitions under cs_coinjoin`**: addresses 1, 2 and the triple sample in 4. A new `cs_check_pool`, acquired **only** via `TRY_LOCK`, makes `CheckPool()` single-shot without ever blocking `msghand`. Signature completeness joins the existing pool snapshot. `CommitFinalTransaction()` takes the session id the decision was made for and revalidates it. `SetState()` and `IsSignaturesComplete()` now require `cs_coinjoin`, and `ChargeFees()` samples `nState` once.

  **`fix(coinjoin): guard the session collaterals with cs_coinjoin`**: addresses 5. The collateral transactions and their prevout index become a single `SessionCollaterals` member marked `GUARDED_BY(cs_coinjoin)`, so they cannot drift apart or be annotated inconsistently. Reintroducing an unlocked read is now a `-Wthread-safety` compile error.

  **`fix(coinjoin): serialize timeout transitions`**: addresses 3 (first half) and the rest of 4. `CheckTimeout()` uses the same `cs_check_pool` guard. Offender selection moves into `SelectCollateralToCharge()` under `cs_coinjoin`, in the same lock scope that resets the session (`CheckTimeout()`) or moves it to `SIGNING` (`CreateFinalTransaction(session_id, charge_fees)`). The timeout-finalize path only charges once the live session is confirmed eligible. Collateral is consumed after `cs_coinjoin` is released, because `ConsumeCollateral()` takes `cs_main`. The session may be reset before that consumption runs, so the selected collateral stays reserved in `m_collaterals_being_charged` (which survives `SetNull()`) until `ConsumeCollateral()` has finished. During that time `CreateNewSession()` and `AddUserToExistingSession()` refuse a dsa whose collateral spends one of its inputs. The charged participant therefore cannot be readmitted into the next session with a collateral that is about to be spent, and the fee is still taken. `AddEntry()`'s `ConsumeCollateralIfCurrentSession()` takes the same reservation.

  **`fix(coinjoin): prioritize progress over timeout reset`**: addresses 3 (second half). Before resetting, `CheckTimeout()` rechecks under `cs_coinjoin` whether the session can still advance: queue ready, an entry for every collateral, enough entries covering both sides of the session denomination, or every signature in. If so it leaves the session to the next tick. The rules mirror the decisions `CheckPool()` and `CreateFinalTransaction()` make, so a deferred session is always acted on. The decision is the pure function `ShouldResetTimedOutSession()`, which `CheckTimeout()` calls with the session's progress read under `cs_coinjoin`.

  **`fix(coinjoin): bind completion tail to session snapshot`**: addresses 6. The committed transaction, participants and collaterals are captured under `cs_coinjoin`. Completion notification is side-effect-free, and only the matching signing session is reset. The invalid-transaction path notifies the captured participants. `CheckPool()`'s uncovered-session reset captures its participants and resets in one step, then tells them.

  **`fix(coinjoin): apply a DSSIGNFINALTX message atomically`**: addresses 7. The signing session the sender was checked against is revalidated after decoding, and the whole batch is applied in one `cs_coinjoin` critical section. The rejection relay for a failed input happens inside that same section.

  ### Lock discipline

  No new `cs_coinjoin`-across-`cs_main` edge is introduced: offender collateral is consumed only after `cs_coinjoin` is released, and signature verification already ran under `cs_coinjoin`. `cs_check_pool` is `TRY_LOCK`-only, taken only by `CheckPool()` and `CheckTimeout()` while holding nothing else, so `cs_check_pool` → `cs_coinjoin` / `cs_main` is unidirectional and no ABBA cycle is possible.

  ## How Has This Been Tested?

  macOS arm64, depends build, clang with `-Wthread-safety -Werror`, rebased on develop c8bfd7a.

  - Every one of the six commits builds `test_dash` and passes `coinjoin_inouts_tests` on its own.
  - Unit suites `coinjoin_inouts_tests`, `coinjoin_tests`, `coinjoin_queue_tests`, `coinjoin_dstxmanager_tests` pass.
  - Functional `rpc_coinjoin.py` and `p2p_dstx.py` pass.
  - `lint-whitespace.py`, `lint-includes.py`, `lint-circular-dependencies.py` pass.

  New/updated unit tests in `coinjoin_inouts_tests`:

  - `server_timeout_resets_only_sessions_that_cannot_advance` is one table over `ShouldResetTimedOutSession()`. Rows that must be left alone: a session that has not timed out; a ready queue; an entry for every collateral (even with a side uncovered, which `CheckPool()` resets itself and tells the participants); enough covered entries with one straggler; every input signed. Rows that must be reset: a queue that is not ready; too few entries; enough entries with a side uncovered and one straggler; a missing signature. Mutations checked: removing the deferral, the signing-advance clause, either accepting-advance clause, or the side-coverage condition each fails at least one row.
  - `server_finalization_rechecks_live_side_coverage` now exercises the charging finalize path.

  The `cs_check_pool` single-flight guard, the completion-tail snapshot and the `DSSIGNFINALTX` atomicity change close timing windows that cannot be reproduced deterministically in a unit test without driving private synchronization state, so they have no dedicated test. The completion tail is bound to the session id and participant list captured under `cs_coinjoin`, and the reset checks both the session id and `nState`, which is visible in the diff. They were verified by inspection and by the existing `server_signfinaltx_*` tests, which still pass. The charge reservation is in the same position: its window lies between two lock scopes inside one call. It was verified by checking that every reservation is released exactly once, after `ConsumeCollateral()` returns, on every path; that admission checks it under the same `cs_coinjoin` scope that admits; by `-Wthread-safety -Werror`; and by an independent code review.

  ## Breaking Changes

  None. No protocol, serialization or RPC change. Two debug log lines have `vecSessionCollaterals.size():` renamed to `participants:`.

  ## Checklist:
  - [x] I have performed a self-review of my own code
  - [ ] I have made corresponding changes to the documentation
  - [ ] I have assigned this pull request to a milestone _(for repository code-owners and collaborators only)_

Top commit has no ACKs.

Tree-SHA512: 0938a87650a83d76c71121b9defd3e0c2548f7749eaf0eb049a7c61a37344ac20011eba50a246d3bcdbcf5870f74d60b6b0fbe55578291de64f15537df353b2e
(cherry picked from commit 6d5ffe8)
PastaPastaPasta added a commit to PastaPastaPasta/dash that referenced this pull request Oct 16, 2026
…oinJoin server

a231f05 fix(coinjoin): apply a DSSIGNFINALTX message atomically (pasta)
873295f fix(coinjoin): bind completion tail to session snapshot (pasta)
d1e23c7 fix(coinjoin): prioritize progress over timeout reset (pasta)
0ba94a6 fix(coinjoin): serialize timeout transitions (pasta)
97daa21 fix(coinjoin): guard the session collaterals with cs_coinjoin (pasta)
3fdd82d fix(coinjoin): decide server state transitions under cs_coinjoin (pasta)

Pull request description:

  ## Issue being fixed or feature implemented

  `CCoinJoinServer` state is touched by two threads: `msghand` (single-threaded, via `ProcessDS*`) and the 1s scheduler tick (`CheckForCompleteQueue` / `CheckPool` / `CheckTimeout`), plus RPC threads in `GetJsonInfo()`. `CheckPool()` runs on **both**: the scheduler tick and `msghand` via `ProcessDSVIN` / `ProcessDSSIGNFINALTX`.

  Several pieces of the original version of this PR have since landed on develop separately (dashpay#7597 `nSessionDenom` atomic, dashpay#7599 entry admission bound to the session snapshot, plus the single-snapshot `CheckPool()` and the session-bound `CreateFinalTransaction()` from the promotion/demotion work). This PR is now rebased on top of those and contains what is still missing:

  1. **`CheckPool()` was not single-flight.** Two concurrent calls (scheduler + `msghand`) could both act on the same snapshot. If both finalize, clients receive `DSFINALTX` twice and sign twice, and the duplicate signatures make `AddScriptSig()` fail, which relays `STATUS_REJECTED` and aborts the session for everyone. Both calls could also commit the same final transaction.
  2. **`CommitFinalTransaction()` was not bound to a session.** It committed whatever `finalMutableTransaction` held, even when the session had timed out and been reset after the decision. That pushes a cleared transaction through ATMP and notifies participants of a failure that never happened.
  3. **`CheckTimeout()` raced finalization and commit.** It tested `HasTimedOut()` without a lock and could reset a session halfway through `CheckPool()` finalizing or committing it. It also reset sessions that could still advance, because the final collateral, entry or signature can arrive after the scheduler's `CheckPool()` snapshot. In that case `msghand` skips its own `CheckPool()` while the scheduler holds the guard.
  4. **Offender selection and charging were not atomic with closing admission.** `ChargeFees()` sampled `nState` three times, so it could select "didn't send" offenders and then charge and log them as "didn't sign". On the timeout-finalize path it ran before eligibility was rechecked, so an ineligible session was charged once there and again by the `CheckTimeout()` reset that followed.
  5. **The session collaterals were not `GUARDED_BY`.** `vecSessionCollaterals` was unannotated while its companion prevout index was guarded. `CreateNewSession()` still read it after releasing the lock, and nothing prevented the next unlocked read.
  6. **The completion tail mutated live state.** If every participant had disconnected, `RelayCompletedTransaction()` could reset the session. A replacement session could then open before random charging and the unconditional tail reset ran, and those would charge or clear the replacement. The invalid-transaction path also reset the pool *before* notifying anyone, so `ERR_INVALID_TX` reached nobody.
  7. **A `DSSIGNFINALTX` message was not applied atomically.** Each input went through its own locked `AddScriptSig()`. A signing timeout landing between two inputs of one already-received message would select the sender as a non-signer, charge them, and reset the session.

  ## What was done?

  Six commits, each standalone:

  **`fix(coinjoin): decide server state transitions under cs_coinjoin`**: addresses 1, 2 and the triple sample in 4. A new `cs_check_pool`, acquired **only** via `TRY_LOCK`, makes `CheckPool()` single-shot without ever blocking `msghand`. Signature completeness joins the existing pool snapshot. `CommitFinalTransaction()` takes the session id the decision was made for and revalidates it. `SetState()` and `IsSignaturesComplete()` now require `cs_coinjoin`, and `ChargeFees()` samples `nState` once.

  **`fix(coinjoin): guard the session collaterals with cs_coinjoin`**: addresses 5. The collateral transactions and their prevout index become a single `SessionCollaterals` member marked `GUARDED_BY(cs_coinjoin)`, so they cannot drift apart or be annotated inconsistently. Reintroducing an unlocked read is now a `-Wthread-safety` compile error.

  **`fix(coinjoin): serialize timeout transitions`**: addresses 3 (first half) and the rest of 4. `CheckTimeout()` uses the same `cs_check_pool` guard. Offender selection moves into `SelectCollateralToCharge()` under `cs_coinjoin`, in the same lock scope that resets the session (`CheckTimeout()`) or moves it to `SIGNING` (`CreateFinalTransaction(session_id, charge_fees)`). The timeout-finalize path only charges once the live session is confirmed eligible. Collateral is consumed after `cs_coinjoin` is released, because `ConsumeCollateral()` takes `cs_main`. The session may be reset before that consumption runs, so the selected collateral stays reserved in `m_collaterals_being_charged` (which survives `SetNull()`) until `ConsumeCollateral()` has finished. During that time `CreateNewSession()` and `AddUserToExistingSession()` refuse a dsa whose collateral spends one of its inputs. The charged participant therefore cannot be readmitted into the next session with a collateral that is about to be spent, and the fee is still taken. `AddEntry()`'s `ConsumeCollateralIfCurrentSession()` takes the same reservation.

  **`fix(coinjoin): prioritize progress over timeout reset`**: addresses 3 (second half). Before resetting, `CheckTimeout()` rechecks under `cs_coinjoin` whether the session can still advance: queue ready, an entry for every collateral, enough entries covering both sides of the session denomination, or every signature in. If so it leaves the session to the next tick. The rules mirror the decisions `CheckPool()` and `CreateFinalTransaction()` make, so a deferred session is always acted on. The decision is the pure function `ShouldResetTimedOutSession()`, which `CheckTimeout()` calls with the session's progress read under `cs_coinjoin`.

  **`fix(coinjoin): bind completion tail to session snapshot`**: addresses 6. The committed transaction, participants and collaterals are captured under `cs_coinjoin`. Completion notification is side-effect-free, and only the matching signing session is reset. The invalid-transaction path notifies the captured participants. `CheckPool()`'s uncovered-session reset captures its participants and resets in one step, then tells them.

  **`fix(coinjoin): apply a DSSIGNFINALTX message atomically`**: addresses 7. The signing session the sender was checked against is revalidated after decoding, and the whole batch is applied in one `cs_coinjoin` critical section. The rejection relay for a failed input happens inside that same section.

  ### Lock discipline

  No new `cs_coinjoin`-across-`cs_main` edge is introduced: offender collateral is consumed only after `cs_coinjoin` is released, and signature verification already ran under `cs_coinjoin`. `cs_check_pool` is `TRY_LOCK`-only, taken only by `CheckPool()` and `CheckTimeout()` while holding nothing else, so `cs_check_pool` → `cs_coinjoin` / `cs_main` is unidirectional and no ABBA cycle is possible.

  ## How Has This Been Tested?

  macOS arm64, depends build, clang with `-Wthread-safety -Werror`, rebased on develop c8bfd7a.

  - Every one of the six commits builds `test_dash` and passes `coinjoin_inouts_tests` on its own.
  - Unit suites `coinjoin_inouts_tests`, `coinjoin_tests`, `coinjoin_queue_tests`, `coinjoin_dstxmanager_tests` pass.
  - Functional `rpc_coinjoin.py` and `p2p_dstx.py` pass.
  - `lint-whitespace.py`, `lint-includes.py`, `lint-circular-dependencies.py` pass.

  New/updated unit tests in `coinjoin_inouts_tests`:

  - `server_timeout_resets_only_sessions_that_cannot_advance` is one table over `ShouldResetTimedOutSession()`. Rows that must be left alone: a session that has not timed out; a ready queue; an entry for every collateral (even with a side uncovered, which `CheckPool()` resets itself and tells the participants); enough covered entries with one straggler; every input signed. Rows that must be reset: a queue that is not ready; too few entries; enough entries with a side uncovered and one straggler; a missing signature. Mutations checked: removing the deferral, the signing-advance clause, either accepting-advance clause, or the side-coverage condition each fails at least one row.
  - `server_finalization_rechecks_live_side_coverage` now exercises the charging finalize path.

  The `cs_check_pool` single-flight guard, the completion-tail snapshot and the `DSSIGNFINALTX` atomicity change close timing windows that cannot be reproduced deterministically in a unit test without driving private synchronization state, so they have no dedicated test. The completion tail is bound to the session id and participant list captured under `cs_coinjoin`, and the reset checks both the session id and `nState`, which is visible in the diff. They were verified by inspection and by the existing `server_signfinaltx_*` tests, which still pass. The charge reservation is in the same position: its window lies between two lock scopes inside one call. It was verified by checking that every reservation is released exactly once, after `ConsumeCollateral()` returns, on every path; that admission checks it under the same `cs_coinjoin` scope that admits; by `-Wthread-safety -Werror`; and by an independent code review.

  ## Breaking Changes

  None. No protocol, serialization or RPC change. Two debug log lines have `vecSessionCollaterals.size():` renamed to `participants:`.

  ## Checklist:
  - [x] I have performed a self-review of my own code
  - [ ] I have made corresponding changes to the documentation
  - [ ] I have assigned this pull request to a milestone _(for repository code-owners and collaborators only)_

Top commit has no ACKs.

Tree-SHA512: 0938a87650a83d76c71121b9defd3e0c2548f7749eaf0eb049a7c61a37344ac20011eba50a246d3bcdbcf5870f74d60b6b0fbe55578291de64f15537df353b2e
(cherry picked from commit 6d5ffe8)
PastaPastaPasta added a commit that referenced this pull request Oct 17, 2026
6c02b2b ci: let DEPENDS_ARCH override the machine arch for multiprocess and tsan (pasta)
ce5dbf3 Merge #7888: fix(consensus): canonical ExtNetInfo encoding and strict netInfo decoding (pasta)
54cba5c Merge #7537: fix: data races and check-then-act races in the CoinJoin server (pasta)
08d8bca Merge #7909: feat(mining): build the masternode list incrementally during block assembly (pasta)
5633761 Merge #7895: test: cover retroactive provider tx refusal, fix wording (pasta)
cbb9500 Merge #7846: fix: treat pending owner keys and Platform node IDs as one conflict namespace (pasta)
8efdf5e Merge #7857: fix: mine a pending ProUpServTx before a key change of its masternode (pasta)
e56e6ae Merge #7842: fix(wallet): reject mnemonic passphrases that seed derivation truncates (pasta)
79480ff test: follow-up for #7821: check if wallet is locked after failed upgrade instead creating brand-new scenario (Konstantin Akimov)
a56ed47 fix(net): announce sporks to new peers instead of requesting them during mnsync (pasta)
cdaea5f Merge #7894: fix: due to logical conflicts feature_pruning.py had been broken while (pasta)
98d92f6 Merge #7817: backport: partial bitcoin#34156 restore cleanup ownership (pasta)
35e6d18 Merge #7858: fix: do not InstantSend-lock provider transactions (pasta)
904d73d Merge #7881: fix: refuse to sign shared registrar updates that move the funder's coins (pasta)
95982f5 Merge #7782: fix(wallet): persist pending-observation coin locks once the record becomes readable (pasta)
cdf65d0 Merge #7836: backport: partial bitcoin#25812, bitcoin#29855 (pasta)
cc5716f Merge #7824: fix: retain initialized DKG sessions during inventory reads (pasta)
588e458 Merge #7827: backport: partial bitcoin#35984 (pasta)
795c6f1 Merge #7839: fix: release InstantSend tracking after mempool eviction (pasta)
dc1a6b2 Merge #7875: docs(rpc): clarify getchainlockbyheight availability (pasta)
e4798ff Merge #7876: fix(consensus): capture operator key scheme changes with unchanged bytes in state diffs (pasta)
fe68ecb Merge #7871: backport!: Merge bitcoin#27217: wallet: Replace use of purpose strings with an enum (pasta)
8348d22 Merge #7867: fix: refuse masternode list RPCs for blocks outside the active chain (pasta)
861a579 Merge #7866: fix: stop `masternode payments` at the genesis block (pasta)
efcef71 Merge #7521: fix: update masternode rate limit on failed governance trigger path (pasta)
0623182 Merge #7831: fix: bound governance object signatures and reset the governance cache (pasta)
b230657 Merge #7841: fix: authenticate cached InstantSend proof bytes (pasta)
a1f37ea Merge #7820: fix: hold chainstate lock during CoinJoin input validation (pasta)
50a5617 Merge #7829: fix: preserve wallet permissions when changing passphrases (pasta)
2ff0103 Merge #7818: fix: return REST errors for unavailable deployment data (pasta)
cb39369 Merge #7834: fix: validate Electrum import input boundaries (pasta)
e7e3daa Merge #7821: fix: relock encrypted wallets after HD upgrade failures (pasta)
2fea45f Merge #7792: fix: treat missing snapshots during evodb repair as repair errors (pasta)
3c5c039 Merge #7838: fix(wallet): authenticate unlock deadline extensions (pasta)
0bf80f8 Merge #7810: fix(rpc): handle unavailable masternode payment inputs (pasta)
bcefee4 Merge #7822: fix: reject malformed detached quorum share batches (pasta)
86a7737 Merge #7828: fix: omit recovery words from invalid mnemonic errors (pasta)
886d5c4 Merge #7816: fix: check credit pool amount accumulation (pasta)
00fc3b9 Merge #7851: fix(rpc): correct masternode status help for top-level service field (pasta)
0e75c22 Merge #7814: fix(qt): restore HTML escaping of send confirmation recipients (pasta)

Pull request description:

  ## Issue being fixed or feature implemented

  Backports for v24.0.0-rc.4, on top of `v24.0.x` at the v24.0.0-rc.3 tag (0364ed6, #7808).

  ## What was done?

  Cherry-picked (`git cherry-pick -m1 -x`) every develop merge since rc.3 that carries `backport-candidate-24.0.x`, plus eight unlabeled fixes picked after an audit of every other develop merge since rc.3 (marked † below). They are in develop merge order:

  - #7814 fix(qt): restore HTML escaping of send confirmation recipients †
  - #7851 fix(rpc): correct masternode status help for top-level service field
  - #7816 fix: check credit pool amount accumulation
  - #7828 fix: omit recovery words from invalid mnemonic errors †
  - #7822 fix: reject malformed detached quorum share batches †
  - #7810 fix(rpc): handle unavailable masternode payment inputs †
  - #7838 fix(wallet): authenticate unlock deadline extensions
  - #7792 fix: treat missing snapshots during evodb repair as repair errors
  - #7821 fix: relock encrypted wallets after HD upgrade failures
  - #7834 fix: validate Electrum import input boundaries
  - #7818 fix: return REST errors for unavailable deployment data
  - #7829 fix: preserve wallet permissions when changing passphrases
  - #7820 fix: hold chainstate lock during CoinJoin input validation
  - #7841 fix: authenticate cached InstantSend proof bytes
  - #7831 fix: bound governance object signatures and reset the governance cache
  - #7521 fix: update masternode rate limit on failed governance trigger path
  - #7866 fix: stop `masternode payments` at the genesis block
  - #7867 fix: refuse masternode list RPCs for blocks outside the active chain †
  - #7871 backport!: bitcoin#27217: wallet: Replace use of purpose strings with an enum
  - #7876 fix(consensus): capture operator key scheme changes with unchanged bytes in state diffs
  - #7875 docs(rpc): clarify getchainlockbyheight availability †
  - #7839 fix: release InstantSend tracking after mempool eviction
  - #7827 backport: partial bitcoin#35984
  - #7824 fix: retain initialized DKG sessions during inventory reads
  - #7836 backport: partial bitcoin#25812, bitcoin#29855
  - #7782 fix(wallet): persist pending-observation coin locks once the record becomes readable
  - #7881 fix: refuse to sign shared registrar updates that move the funder's coins †
  - #7858 fix: do not InstantSend-lock provider transactions
  - #7817 backport: partial bitcoin#34156 restore cleanup ownership
  - #7894 test: fix feature_pruning.py restart arguments † (needed because #7792 makes the evodb repair fatal, which the old restart arguments trip)
  - #7893 fix(net): announce sporks to new peers instead of requesting them during mnsync
  - 4a19742 (from #7859) test: follow-up for #7821: check if wallet is locked after failed upgrade instead creating brand-new scenario. Test-only, picked as its commit. Without it, #7821's `test_failed_upgrade()` expects RPC error -1 for a 257-byte mnemonic passphrase, which #7842's new precheck rejects with -8 first, so `wallet_upgradetohd.py --legacy-wallet` fails. On develop, #7859 replaced that scenario before #7842 merged. The rest of #7859 is test cleanup and docs and is left out.
  - #7842 fix(wallet): reject mnemonic passphrases that seed derivation truncates
  - #7857 fix: mine a pending ProUpServTx before a key change of its masternode
  - #7846 fix: treat pending owner keys and Platform node IDs as one conflict namespace
  - #7895 test: cover retroactive provider tx refusal, fix wording
  - #7909 feat(mining): build the masternode list incrementally during block assembly
  - #7537 fix: data races and check-then-act races in the CoinJoin server
  - #7888 fix(consensus): canonical ExtNetInfo encoding and strict netInfo decoding

  34 of the 39 picks change exactly the same lines as their develop merges. #7888 and #7782 needed test-only changes (see testing), and three others needed adaptation, described in their commit messages:

  - **#7893** was merged to develop twice: f99212b with an earlier revision of its commit, then 72a5997 with the final one, so develop now announces sporks twice in the VERACK handler (a develop fix is open separately). Here the PR's final commit 45acdf3 is picked once instead of either merge. In `src/node/sync_manager.cpp`, this branch still sent `GETSPORKS` from the mnsync tick on every network, because #7847, which limited it to test chains, is not included. The request is removed entirely, as on develop. Mainnet spork values are hardcoded in `CSporkManager::GetSporkValue`, so the mainnet request had no effect.
  - **#7857** and **#7909**: develop folded `CChainstateHelper` into `Chainstate` (d5c1bd7), which is not on this branch, so the new code goes through `ChainHelper().special_tx` / `m_chain_helper.special_tx` instead of `m_special_tx`. The neighbouring develop-only `CSpecialTxProcessor::GetDeterministicMNListHash` is left out.

  #7888 tightens consensus rules for ExtAddr provider transactions behind V24, so I checked it against testnet. V24 is not active there yet (the tip's CbTx is still version 3), so the chain has no ExtAddr ProTxs the new rules could reject. rc.3's own serializer always writes canonical, sorted purposes, so every ProTx it builds passes the new rules. Mixed rc.3/rc.4 networks could only split on a hand-crafted non-canonical ExtAddr ProTx mined after V24 activates. So testnet miners and masternodes should move to rc.4 before V24 is signalled.

  Unlabeled develop fixes since rc.3 that were audited and left out:
  - Worth considering before the final release: #7869 (startup block replay after an unclean shutdown asserts on a quorum commitment, so recovery needs `-reindex-chainstate`; not a v24 regression), #7833 (gate signing intake before quorum retrieval, DoS hardening), #7682 (intermittent `relic_conf.h` parallel-build failure).
  - Left for 24.1: #7812, #7826, #7844, #7847, #7850, #7868, plus the routine upstream backports (#7780, #7806, #7830, #7855, #7856, #7862, #7863, #7865, #7883, #7884, #7898, #7906), refactors (#7805, #7878, #7880), and CI, docs and test-only changes.

  One CI-only commit comes last. It takes only the `ci/test` hunks of 6f71fa5 (#7901), which make `ci/test/00_setup_env_native_{multiprocess,tsan}.sh` honor `DEPENDS_ARCH`. Pull requests into `v24.0.x` run develop's workflow files (`pull_request_target`), and since #7901 those check the depends cache on x86_64 runners and pass the aarch64 build arch in `DEPENDS_ARCH`. Without the hunks, `linux64_multiprocess / Build depends` fails on any depends cache miss (`Build runner computes HOST=aarch64-linux-gnu but check-cache used x86_64-pc-linux-gnu`). Without `DEPENDS_ARCH` the scripts still use `uname -m`, so this branch's own push CI is unchanged.

  No version change is needed. `configure.ac` is already 24.0.0 with `CLIENT_VERSION_IS_RELEASE` false, so the build takes its version string from the `v24.0.0-rc.4` tag.

  ## How Has This Been Tested?

  Built and tested at 6c02b2b on macOS arm64 against depends, with `--enable-debug --enable-werror --disable-hardening`.

  - `make check`: all unit test suites pass, including `test_dash-qt`.
  - Functional tests, all passing: `feature_evo_shares_activation.py`, `feature_governance_objects.py`, `feature_llmq_signing.py` (plain and `--spork21`), `feature_masternode_shares.py`, `feature_sporks.py`, `interface_rest.py`, `p2p_instantsend.py`, `p2p_quorum_data.py`, `rpc_masternode.py`, `rpc_psbt.py`, `rpc_quorum.py`, `wallet_backup.py`, `wallet_dump.py`, `wallet_encryption.py`, `wallet_labels.py` and `wallet_upgradetohd.py`, each with `--descriptors` and `--legacy-wallet` where it supports them.

  - Each pick's changed lines were compared with its develop merge (or, for #7893, its PR commit). They are identical except for the #7893, #7857 and #7909 adaptations above, and the #7888 test adaptation below.
  - A local macOS arm64 debug build (`--enable-debug --enable-werror`) compiled most of the tree and stopped on a single error in `src/test/evo_netinfo_tests.cpp`. #7888's new test used `CNetAddr::V2()`, the serialization-parameter API from bitcoin#25284, which this branch lacks. The pick now writes that entry through an `OverrideStream` with `ADDRV2_FORMAT`, the way `NetInfoEntry::Serialize` does on this branch (see its commit message).
  - #7782's new unit test reloads the wallet with develop's three-argument `CWallet` constructor. On this branch the constructor still takes the CoinJoin loader and an `ArgsManager`, so the test passes them the way its fixture already does (see the commit message).
  - With both test fixes, a full local `make` (`dashd`, `dash-qt`, `test_dash`, `test_dash-qt`) completes with no errors.
  - `feature_pruning.py` is an extended test that CI does not run. #7894 is included because #7792 makes its restart path fail.

  ## Breaking Changes

  Carried over from the picked PRs:

  - #7876: rc.1 to rc.3 nodes that connected an affected block stored a wrong masternode list diff for it. Those nodes need `-reindex`; `-reindex-chainstate` is not enough for nodes that already marked the following block invalid.
  - #7888: stricter ExtAddr `netInfo` decoding and validation behind V24 (see above). The RFC1918 check now applies even with `-allowprivatenet`.
  - #7858: provider transactions no longer get InstantSend locks. Software that waits for an islock on a ProTx should wait for a ChainLock or confirmations instead.
  - #7857: a `ProUpServTx` spending an output of a pending operator key change, asset lock, asset unlock or MNHF signal is rejected from the mempool until that transaction confirms.
  - #7831: governance object signatures over 96 bytes are rejected, and the governance cache is discarded and re-synced on first start (`doc/release-notes-7831.md`).
  - #7871: address purposes are limited to `send`, `receive` and `refund`. Wallets with other purpose strings load with warnings, and `listlabels` errors on an unknown purpose (`doc/release-notes-27217.md`).
  - #7842: creating an HD wallet with a mnemonic passphrase over 248 bytes fails unless `-allow-deprecated-long-mnemonics` is set, and over 256 bytes always fails (`doc/release-notes-7842.md`).
  - #7792: a node whose evodb is missing a masternode list snapshot now stops at startup and asks for `-reindex`, and `evodb repair` reports it in `repairErrors`.
  - #7867: `protx info <proTxHash> <blockHash>`, `protx listdiff` and `masternode payments <blockHash>` return an error for blocks outside the active chain.
  - #7881: `protx shared_combine` on a `ProUpSharedRegTx` is refused unless its inputs are unlocked coins of a single address of the calling wallet, change returns to that address, and the fee is within `-maxtxfee`.
  - #7836: PSBTs referencing nonexistent previous outputs fail to deserialize (RPC error -22).
  - #7834: negative Electrum-import rescan indices are rejected.
  - #7893: on this branch, mainnet nodes no longer send `GETSPORKS` during mnsync (no effect, mainnet spork values are hardcoded).

  ## Checklist:
  - [x] I have performed a self-review of my own code
  - [ ] I have made corresponding changes to the documentation
  - [ ] I have assigned this pull request to a milestone

  🤖 Generated with [Claude Code](https://claude.com/claude-code)

Top commit has no ACKs.

Tree-SHA512: 7de9535ca51620f4ddf5a46df2c3341a5e5fdd2263606f83f7db6b576c31ff7b82e1b285bf66988ac4b2729a9060513c45710135f61d7fd5c147b670d5f405dc
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants