Skip to content

fix(net): clean recovered sig backlog after peer disconnect - #7483

Closed
thepastaclaw wants to merge 2 commits into
dashpay:developfrom
thepastaclaw:fix-recovered-sig-disconnect-cleanup
Closed

fix(net): clean recovered sig backlog after peer disconnect#7483
thepastaclaw wants to merge 2 commits into
dashpay:developfrom
thepastaclaw:fix-recovered-sig-disconnect-cleanup

Conversation

@thepastaclaw

@thepastaclaw thepastaclaw commented Jul 25, 2026

Copy link
Copy Markdown

Issue being fixed or feature implemented

The recovered-signature cleanup added in #7402 checks PeerIsBanned(node_id) every five seconds. That query reads the peer's one-shot m_should_discourage flag, which is cleared during the next SendMessages pass before disconnection completes. A cleanup tick therefore usually misses the short-lived state, and once FinalizeNode() removes the peer, PeerIsBanned() returns false. Pending recovered signatures from the peer can consequently remain queued instead of being reclaimed after disconnection.

What was done?

  • Added PeerManagerInternal::PeerIsConnected() using the peer map as the authoritative connection-lifecycle state.
  • Changed the periodic pending recovered-signature sweep to remove entries whose originating peer is no longer connected.
  • Kept the rationale concise: the banned flag is transient, while finalized node IDs are not reused.
  • Extended the existing peer-discouragement test to cover the transient banned state and connection lifecycle through FinalizeNode().

The existing RemoveBannedNodeStates() path for signature shares is intentionally unchanged because it runs in the fast message-processing loop while the discourage flag is observable.

How Has This Been Tested?

  • make -C src test/test_dash
  • ./src/test/test_dash --run_test=denialofservice_tests/peer_discouragement
  • ./src/test/test_dash --run_test=denialofservice_tests
  • ./src/test/test_dash --run_test=net_peer_connection_tests
  • test/lint/lint-whitespace.py
  • git diff --check
  • clang-format-diff.py on the PR diff

The two original standalone tests were removed after verifying that both still passed when the production cleanup predicate was reverted. The retained lifecycle assertions fail when PeerIsConnected() is replaced with the old transient banned-state behavior.

Breaking Changes

None.

Checklist

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

@thepastaclaw

thepastaclaw commented Jul 25, 2026

Copy link
Copy Markdown
Author

🔍 Review in progress — actively reviewing now (commit 6a2c939)

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Final validation — Codex + Sonnet

The change correctly replaces the transient discouragement flag with durable peer-map membership when pruning pending recovered signatures. Peer lifecycle, locking, node-ID uniqueness, local reconstruction handling, and the focused regression tests all support the implementation; no in-scope correctness issues were found. GitHub does not permit self-approval, so the canonical APPROVE result is transported as a COMMENT review.

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 (failed), claude-sonnet-5 — dash-core-commit-history (failed), claude-sonnet-5 — general (completed), claude-sonnet-5 — dash-core-commit-history (completed)

@thepastaclaw thepastaclaw changed the title fix(llmq): clean recovered sig backlog after peer disconnect fix(net): clean recovered sig backlog after peer disconnect Jul 25, 2026
…nned

NetSigning::WorkThreadSigning sweeps CSigningManager's pending (not-yet-
verified) recovered sigs every CLEANUP_INTERVAL{5s} so a flood's backlog does
not outlive the peer that sent it. That sweep was keyed on PeerIsBanned(),
which made it effectively a no-op.

PeerIsBanned() -> IsBanned() reads Peer::m_should_discourage, a one-shot flag
set by Misbehaving() and cleared by MaybeDiscourageAndDisconnect() on the very
next SendMessages pass (~100ms) just before the peer is disconnected. Once the
peer is finalized the Peer is erased from m_peer_map, so GetPeerRef() returns
nullptr and IsBanned() reports false again. The predicate could therefore only
match if a 5s tick happened to land inside a ~100ms window, so in practice the
backlog survived and the stated goal was never achieved.

Key the sweep on "no longer connected" instead. Disconnection is a durable,
terminal state -- node ids are never reused (CConnman::GetNewNodeId only
increments) -- so whenever the tick lands, it is observed. This also reclaims
the queues of peers that simply went away without misbehaving, and mirrors how
CSigSharesManager::Cleanup() already prunes its per-node state by disconnection.

A connected-state predicate was chosen over a FinalizeNode hook to keep the
change minimal: it preserves the existing throttled-sweep shape, adds no
cross-subsystem lifetime or ordering coupling, and is directly unit-testable.
The new PeerIsConnected() also needs a strictly smaller lock footprint than
PeerIsBanned() (m_peer_mutex only, no cs_main or m_misbehavior_mutex).

NetSigning::RemoveBannedNodeStates() is deliberately left on PeerIsBanned():
it runs from WorkThreadCleaning() on a 100ms loop that matches the flag's
lifetime, and its subsystem has its own disconnected-node sweep.

Locally reconstructed sigs are unaffected: they use NodeId -1 and live in
pendingReconstructedRecoveredSigs, a separate map keyed by sig hash that
RemoveNodesIf() does not touch.

Adds a regression test asserting the flag has already gone false by the time a
sweep could run, while the connection-keyed predicate stays true once the peer
is finalized, plus direct coverage of PeerIsConnected() across FinalizeNode().
@PastaPastaPasta
PastaPastaPasta force-pushed the fix-recovered-sig-disconnect-cleanup branch from ea71732 to 43cefa6 Compare August 8, 2026 18:05
Remove standalone tests that copied cleanup predicates without exercising the production sweep. Reuse the existing peer discouragement flow to cover the transient ban flag and finalized peer lifecycle, and keep the production rationale concise.

Co-Authored-By: Claude <noreply@anthropic.com>
@PastaPastaPasta

Copy link
Copy Markdown
Member

Superseded by #7563, which keeps the !PeerIsConnected disconnect reclaim and also purges pendingRecoveredSigs eagerly in BanNode (with the two raw recsig misbehavior sites routed through it).

Closing this PR in favor of that remake.


🤖 Posted autonomously by Claude on behalf of pasta.

@thepastaclaw

Copy link
Copy Markdown
Author

The linux64_asan-build failure is unrelated to this PR's net-processing/LLMQ changes, so no branch change is appropriate.

Both the original failing run and the run at the final head pass all Qt test cases, then LeakSanitizer reports the same Qt dependency shutdown leak in QDBusConnectionManager::executeConnectionRequest (86 bytes in 4 allocations). The identical signature reproduces on unmodified develop and is tracked in #7561.

The PR diff does not touch Qt or DBus, and the substantive PR was already superseded by #7563. I have left this branch untouched; maintainers can address the shared CI issue via #7561.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants