Skip to content

test: don't expect zero connections after protx operator-key change/revoke - #7506

Merged
PastaPastaPasta merged 1 commit into
dashpay:developfrom
PastaPastaPasta:test-6702-protx-version-disconnect-wait
Aug 2, 2026
Merged

test: don't expect zero connections after protx operator-key change/revoke#7506
PastaPastaPasta merged 1 commit into
dashpay:developfrom
PastaPastaPasta:test-6702-protx-version-disconnect-wait

Conversation

@PastaPastaPasta

Copy link
Copy Markdown
Member

Issue being fixed or feature implemented

Fixes the currently worst-offending functional-test flake, #6702 (feature_protx_version.py, formerly feature_dip3_v19.py). The two most recent CI occurrences (2026-07-30, one under TSAN and one under multiprocess, both exhausting all three retries) time out at the same predicate:

self.wait_until(lambda: self.nodes[node_idx].getconnectioncount() == 0)
AssertionError: ... not true after 240.0 seconds

at line 202 (operator-key change / v1→v3 migration path) and line 241 (test_revoke_protx).

The root cause is a race in the test, not an implementation bug. Two correct behaviors compose into a predicate that is not guaranteed to ever be observable:

  1. Changing a masternode's operator key (or revoking it) makes every peer that verified its MNAUTH drop the connection once it processes the trigger block (CMNAuth::NotifyMasternodeListChanged). This part works: in every captured failure, all pre-existing peers of the affected node are disconnected within ~1 second.
  2. The masternode remains a member of previously formed quorums (membership is snapshotted at DKG time), so CConnman::ThreadOpenMasternodeConnections on both sides legitimately re-establishes intra-quorum connections on its 100 ms–1 s retry loop, and the affected node also keeps accepting inbound quorum connections. These replacement connections are not dropped again, because the node stops sending MNAUTH once its configured operator key no longer matches the list.

Under load the replacement connections land before the teardown finishes, so getconnectioncount() never transiently reaches zero and the wait can only pass by timing luck. The zero-wait was itself introduced as the fix for the previous incarnation of this flake (#5863), where connect_nodes raced the disconnect storm — it traded one race for another.

The other getconnectioncount() == 0 waits in the tree (feature_llmq_signing.py, feature_llmq_simplepose.py) are not affected: they call setnetworkactive(false) first, which also blocks reconnections.

What was done?

Test-only change. Instead of expecting an instantaneous zero, the test now snapshots the affected node's peer ids immediately before mining the trigger transaction and waits until none of those ids remain in getpeerinfo(). Peer ids are monotonically increasing per node, so this deterministically detects that the key change/revocation dropped every pre-existing connection while tolerating concurrent (legitimate) quorum reconnections. Both call sites use a shared helper.

The alternative of quiescing the network via setnetworkactive(false) was deliberately not used: it would drop all connections regardless of whether the ban-disconnect logic works, gutting the behavioral assertion the wait provides.

How Has This Been Tested?

Reproduced and verified on macOS (M-series, 14 cores) at develop a61ade2ef84 with 15 concurrent copies of the test:

python3 test/functional/test_runner.py --jobs=15 --attempts=1 $(for _ in $(seq 15); do echo feature_protx_version.py; done)
  • Before the change: 6/15, 5/15 and 9/15 runs failed across three batches — every single failure at the line 202/241 zero-wait (no unrelated contention failures).
  • After the change: 45/45 runs passed (three batches of 15).
  • Re-checked out the pre-fix state afterwards and re-ran one batch: 9/15 failures returned, all matching the signature.
  • The 60 s local timeout vs. 240 s in CI is TEST_RUNNER_TIMEOUT_FACTOR=4 (ci/test/00_setup_env.sh); same predicate, same bug.
  • Sequential run of the fixed test passes in ~150 s, matching CI's passing runtime.

Breaking Changes

n/a

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added or updated relevant unit/integration/functional/e2e tests
  • I have made corresponding changes to the documentation
  • I have assigned this pull request to a milestone (for repository code-owners and collaborators only)

🤖 Generated with Claude Code

@thepastaclaw

thepastaclaw commented Aug 1, 2026

Copy link
Copy Markdown

✅ Final review complete — no blockers (commit dc27835)

@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 2081330a-5d80-4c6e-b1d5-899867d3f239

📥 Commits

Reviewing files that changed from the base of the PR and between 5e17e12 and dc27835.

📒 Files selected for processing (1)
  • test/functional/feature_protx_version.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • test/functional/feature_protx_version.py

Walkthrough

The ProTx version tests now capture existing peer IDs before migration and revocation changes. They wait for those peers to disconnect instead of waiting for zero total connections. The tests then reconnect and synchronize the masternode.

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

Possibly related PRs

Suggested reviewers: udjinm6

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the test change that avoids requiring zero connections after ProTx operator-key changes or revocation.
Description check ✅ Passed The description directly explains the functional-test flake, its cause, the peer-ID-based fix, and the validation results.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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

Inline comments:
In `@test/functional/feature_protx_version.py`:
- Around line 61-68: Update wait_for_peers_disconnected so it validates the
initial peer snapshot before polling: obtain the current peer IDs and fail if
the target node has no peers, then continue checking that every requested peer
ID disappears. Preserve the existing wait_until behavior for non-empty
snapshots.
🪄 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: 8942155b-2ddb-4b73-8353-a5a6948da9f5

📥 Commits

Reviewing files that changed from the base of the PR and between efe6dec and 5e17e12.

📒 Files selected for processing (1)
  • test/functional/feature_protx_version.py

Comment thread test/functional/feature_protx_version.py Outdated

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Final validation — Codex + Sonnet

The test-only change correctly replaces the racy zero-connection assertion with tracking of the exact peer IDs present before each operator-key change or revocation. Peer IDs are monotonically assigned for each node process, so the new predicate confirms that all pre-existing connections were dropped while allowing legitimate concurrent quorum reconnections; no in-scope issues were found.

Review provenance

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

@UdjinM6

UdjinM6 commented Aug 2, 2026

Copy link
Copy Markdown

pls see d93818b

@thepastaclaw

Copy link
Copy Markdown

Thanks — the vacuous-pass concern is valid. I applied the exact follow-up, squashed it into the original PR commit, and preserved attribution in thepastaclaw/dash@27ef76e2867. The bot account cannot push to PastaPastaPasta/test-6702-protx-version-disconnect-wait (GitHub 403), so Pasta needs to update the PR branch from that commit (or from your d93818b286d827004cd055401b99b98ac8bed1b2).

…evoke

feature_protx_version.py intermittently timed out (dash#6702) waiting for
getconnectioncount() == 0 on the masternode whose operator key was changed or
revoked. The disconnects it waits for do happen: every peer that verified the
masternode's MNAUTH drops the connection once it processes the trigger block
(CMNAuth::NotifyMasternodeListChanged). But the masternode remains a member of
previously formed quorums, so ThreadOpenMasternodeConnections on both sides
concurrently re-establishes intra-quorum connections. Under parallel-test load
these replacements land before the teardown finishes and the connection count
never observably reaches zero, so the wait can only pass by timing luck.

Instead of expecting an instantaneous zero, snapshot the node's peer ids right
before mining the trigger transaction and wait until none of them remain: peer
ids are monotonic, so this deterministically detects that the key
change/revocation dropped every pre-existing connection while tolerating
concurrent reconnects. The snapshot is asserted non-empty, since waiting for an
empty set to disappear would succeed immediately and the test would silently
stop asserting anything. The wait is expressed as set disjointness and the "why
not zero connections" rationale lives in one place instead of being repeated at
both call sites.

Reproduced with 15 parallel copies via test_runner.py at a61ade2: 6/15 +
5/15 + 9/15 runs failed before the change (every failure at the line 202/241
waits), 45/45 passed after it.

Co-Authored-By: UdjinM6 <UdjinM6@users.noreply.github.com>
@PastaPastaPasta
PastaPastaPasta force-pushed the test-6702-protx-version-disconnect-wait branch from 5e17e12 to dc27835 Compare August 2, 2026 17:18

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Final validation — Codex + Sonnet

Carried-forward prior findings: none — the previous final review at 5e17e12 had zero findings and nothing remains unresolved from it. New latest-delta findings: none. The delta (5e17e12..dc27835) is a single, purely mechanical amendment to the already-approved wait_for_peers_disconnected helper in feature_protx_version.py: it replaces an equivalent all() loop with peer_ids.isdisjoint(...), adds a defensive assert peer_ids guard against a trivially-true wait on an empty snapshot, and consolidates the rationale comment into a docstring instead of duplicating it at both call sites. Verified directly against the diff: both call sites still pass non-empty sets from get_peer_ids, the new predicate is behaviorally identical to the old one, and no logic, correctness, or coverage issues are introduced.

Review provenance

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

@PastaPastaPasta
PastaPastaPasta merged commit 63ce910 into dashpay:develop Aug 2, 2026
44 checks passed
@UdjinM6 UdjinM6 added this to the 24 milestone Aug 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants