Skip to content

fix: purge pending recovered sigs in BanNode - #7563

Open
PastaPastaPasta wants to merge 3 commits into
dashpay:developfrom
PastaPastaPasta:t3code/drop-messages-from-banned-nodes
Open

fix: purge pending recovered sigs in BanNode#7563
PastaPastaPasta wants to merge 3 commits into
dashpay:developfrom
PastaPastaPasta:t3code/drop-messages-from-banned-nodes

Conversation

@PastaPastaPasta

@PastaPastaPasta PastaPastaPasta commented Aug 8, 2026

Copy link
Copy Markdown
Member

Issue being fixed or feature implemented

PR #7402 bounded CSigningManager::pendingRecoveredSigs and added a 5s cleanup that drops pending recovered sigs for peers matching PeerIsBanned(). That predicate is effectively a no-op: m_should_discourage is a ~100ms one-shot cleared on the next SendMessages pass, and after FinalizeNode the peer is gone so PeerIsBanned is false again. Flood backlog from a banned peer can therefore keep burning the single recsig worker even after we have already decided to ban.

Sig-shares already purge eagerly in BanNodeMarkAsBanned. Recovered-sig ban sites were inconsistent: invalid QSIGREC llmqType and bad BLS batch sources used raw PeerMisbehaving(100) without dropping the pending queue, and BanNode itself never touched pendingRecoveredSigs.

This supersedes #7483. Rather than keying a periodic sweep on banned/connected state, reclaim is eager on the ban choke point.

What was done?

  • Widened NetSigning::BanNode to drop that node's pendingRecoveredSigs.
  • Replaced the one-use RemoveNodesIf predicate API with a direct CSigningManager::RemoveNode(NodeId).
  • Routed the two raw recsig PeerMisbehaving(100) sites (invalid QSIGREC llmqType; bad BLS after batch verify) through BanNode so every NetSigning score-100 path purges eagerly.
  • Removed the 5s PeerIsBanned pending-recsig sweep entirely. Kept m_sig_manager.Cleanup() for DB age.

Caps from #7402 remain the bound for peers that disconnect without misbehavior. Shares' RemoveBannedNodeStates() (100ms PeerIsBanned poll) is intentionally unchanged. Silent over-cap drops in VerifyAndProcessRecoveredSig remain silent. Local reconstruction (nodeId == -1) is still skipped by BanNode and is not touched by RemoveNode.

How Has This Been Tested?

  • git diff --check
  • Code-path review of all NetSigning score-100 sites through BanNode

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 Aug 8, 2026

Copy link
Copy Markdown

✅ Final review complete — no blockers (commit 249d088)

@PastaPastaPasta
PastaPastaPasta force-pushed the t3code/drop-messages-from-banned-nodes branch from 2be660a to 38d9364 Compare August 8, 2026 21:35
@PastaPastaPasta PastaPastaPasta changed the title fix(llmq): drop pending recovered sigs on ban and disconnect fix(llmq): purge pending recovered sigs in BanNode Aug 8, 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: 64194d7716

ℹ️ 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/llmq/net_signing.cpp Outdated

if (!Params().GetLLMQ(recoveredSig->getLlmqType()).has_value()) {
m_peer_manager->PeerMisbehaving(pfrom.GetId(), 100);
BanNode(pfrom.GetId());

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 Avoid banning sig shares for a QSIGREC failure

When a peer with NoBan permission and an existing sig-share node state sends this malformed QSIGREC, BanNode() now also calls MarkAsBanned(). If SendMessages() clears the transient m_should_discourage flag before the cleaning thread observes it, the peer remains connected while its node state stays permanently marked banned, causing TryAddPendingIncomingSigShare() to discard all later valid QSIGSHAREs from that trusted connection. Use a recovered-sig-only purge here rather than the broader sig-share ban path.

AGENTS.md reference: AGENTS.md:L169-L169

Useful? React with 👍 / 👎.

@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Invalid LLMQ types and invalid recovered signatures now use BanNode(). BanNode() removes queued, unverified recovered signatures for the node and can conditionally mark its signature shares as banned. Peer finalization removes pending recovered signatures for disconnected nodes. Cleanup no longer depends on transient ban status. Locally reconstructed signatures remain untouched.

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

Sequence Diagram(s)

sequenceDiagram
  participant Peer
  participant NetSigning
  participant CSigningManager
  participant ShareManager
  Peer->>NetSigning: Send invalid recovered-signature message
  NetSigning->>NetSigning: Call BanNode(peer)
  NetSigning->>CSigningManager: RemoveNode(peer)
  CSigningManager-->>NetSigning: Remove pending unverified signatures
  NetSigning->>ShareManager: Conditionally mark shares banned
Loading

Possibly related PRs

  • dashpay/dash#7516: Modifies NetSigning handling for invalid LLMQ and signature messages.

Suggested reviewers: knst

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main change: purging pending recovered signatures in BanNode.
Description check ✅ Passed The description is detailed and directly explains the cleanup changes, affected paths, retained behavior, and testing.
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

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

Widen BanNode to drop CSigningManager::pendingRecoveredSigs for the banned node, and route the two raw recsig PeerMisbehaving(100) sites (invalid QSIGREC llmqType, bad BLS batch source) through BanNode so every NetSigning score-100 path purges eagerly like shares already do via MarkAsBanned.

Replace the unused-outside-BanNode RemoveNodesIf predicate API with a direct RemoveNode(NodeId), and remove the 5s PeerIsBanned pending-recsig sweep entirely: that predicate was a ~100ms one-shot and never reliably ran. Caps from dashpay#7402 remain the bound for peers that disconnect without misbehavior. Keep m_sig_manager.Cleanup() for DB age.
@PastaPastaPasta
PastaPastaPasta force-pushed the t3code/drop-messages-from-banned-nodes branch from 38d9364 to bb11a28 Compare August 8, 2026 21:37

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Preliminary review — Codex only

The eager purge is not durable: concurrent QSIGREC admission can repopulate the peer's queue after RemoveNode(), leaving the single recovered-signature worker to verify the residual backlog after disconnection. The newly routed recovered-signature failures also mark the separate sig-share state as banned, which can permanently disable valid sig-share traffic on NoBan and manual connections.
Source: reviewer backends: gpt-5.6-sol (general) and gpt-5.6-sol (dash-core-commit-history); final verifier backend: gpt-5.6-sol. Orchestration-only: openclaw-agent/cliproxy/gpt-5.6-sol (not reviewer evidence).

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

Review provenance

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

🔴 2 blocking

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

In `src/llmq/net_signing.cpp`:
- [BLOCKING] src/llmq/net_signing.cpp:312-314: Prevent recovered signatures from being requeued after the eager purge
  RemoveNode() only erases entries present while it holds cs_pending; it does not prevent a later VerifyAndProcessRecoveredSig() call for the same NodeId from inserting another entry. ProcessPendingRecoveredSigs() invokes BanNode() on the recovered-signature worker while the message handler can already be processing another QSIGREC. If removal wins the lock first, that in-flight message queues a residual signature afterward. Masternode connections can admit additional messages because their SendMessages() pass—and therefore MaybeDiscourageAndDisconnect()—is limited to the 100 ms cadence in CConnman::ThreadMessageHandler(). With the periodic sweep removed, these residual entries must be drained and BLS-verified after the peer is disconnected, defeating the stated cleanup guarantee. Fence further admission for the NodeId or perform another purge at a durable peer-disconnection/finalization point.
- [BLOCKING] src/llmq/net_signing.cpp:56-57: Do not permanently disable sig shares for a recovered-signature failure
  Routing this recovered-signature-only failure through BanNode() now calls CSigSharesManager::MarkAsBanned(), as does the bad recovered-signature batch path at line 267. If the peer has an existing sig-share node state, MarkAsBanned() sets its banned flag. For NoBan and manual connections, MaybeDiscourageAndDisconnect() clears m_should_discourage but intentionally keeps the connection alive. If that happens before RemoveBannedNodeStates() observes the transient flag, the node state is never removed and remains banned for the connection's lifetime. TryAddPendingIncomingSigShare(), CollectSigSharesToRequest(), CollectSigSharesToSend(), and CollectSigSharesToAnnounce() then suppress subsequent valid sig-share traffic. Keep the recovered-signature score and queue purge separate from MarkAsBanned(), reserving the latter for sig-share protocol failures.

Comment thread src/llmq/net_signing.cpp
Comment on lines +312 to +314
// Drop any not-yet-verified recovered sigs still queued for this peer so a flood's backlog
// does not keep burning the single recsig worker after we have already decided to ban.
m_sig_manager.RemoveNode(nodeId);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Blocking: Prevent recovered signatures from being requeued after the eager purge

RemoveNode() only erases entries present while it holds cs_pending; it does not prevent a later VerifyAndProcessRecoveredSig() call for the same NodeId from inserting another entry. ProcessPendingRecoveredSigs() invokes BanNode() on the recovered-signature worker while the message handler can already be processing another QSIGREC. If removal wins the lock first, that in-flight message queues a residual signature afterward. Masternode connections can admit additional messages because their SendMessages() pass—and therefore MaybeDiscourageAndDisconnect()—is limited to the 100 ms cadence in CConnman::ThreadMessageHandler(). With the periodic sweep removed, these residual entries must be drained and BLS-verified after the peer is disconnected, defeating the stated cleanup guarantee. Fence further admission for the NodeId or perform another purge at a durable peer-disconnection/finalization point.

source: ['codex']

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.

1. Requeue after RemoveNode — real race, not treating as blocking

Agreed that RemoveNode is a one-shot erase under cs_pending and does not fence later VerifyAndProcessRecoveredSig inserts for the same NodeId. Concurrent msg-thread admission after a worker-path ban can re-queue residual entries.

Why I’m not expanding the PR for it:

  • Memory stays bounded by the fix: bound pending recovered sig queue to prevent remote OOM #7402 caps (MAX_PENDING_RECSIGS_PER_NODE / TOTAL).
  • For normal peers the window is short: score → next SendMessages / MaybeDiscourageAndDisconnect → disconnect → no more ProcessMessages.
  • The old 5s PeerIsBanned sweep was not a durable fence either (m_should_discourage is ~100ms and false again after finalize), so restoring a periodic sweep would not honestly close this.
  • An admission blacklist in CSigningManager would work but duplicates peer lifecycle state inside LLMQ; a proper fence belongs on disconnect/FinalizeNode (or similar) if we want that later, not as a second ban bit next to the queue.

So the PR guarantee is intentional: drop what is already queued at ban time, eagerly. Residual post-ban admission is accepted residual under BanNode-only + caps, not a regression of unbounded backlog.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Resolved in 249d088Prevent recovered signatures from being requeued after the eager purge no longer present.

Auto-resolved by the review system based on the latest commit diff. If you believe this was closed in error, reopen the thread.

Comment thread src/llmq/net_signing.cpp Outdated
Comment on lines +56 to +57
if (!Params().GetLLMQ(recoveredSig->getLlmqType()).has_value()) {
m_peer_manager->PeerMisbehaving(pfrom.GetId(), 100);
BanNode(pfrom.GetId());

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Blocking: Do not permanently disable sig shares for a recovered-signature failure

Routing this recovered-signature-only failure through BanNode() now calls CSigSharesManager::MarkAsBanned(), as does the bad recovered-signature batch path at line 267. If the peer has an existing sig-share node state, MarkAsBanned() sets its banned flag. For NoBan and manual connections, MaybeDiscourageAndDisconnect() clears m_should_discourage but intentionally keeps the connection alive. If that happens before RemoveBannedNodeStates() observes the transient flag, the node state is never removed and remains banned for the connection's lifetime. TryAddPendingIncomingSigShare(), CollectSigSharesToRequest(), CollectSigSharesToSend(), and CollectSigSharesToAnnounce() then suppress subsequent valid sig-share traffic. Keep the recovered-signature score and queue purge separate from MarkAsBanned(), reserving the latter for sig-share protocol failures.

source: ['codex']

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.

2. Recsig failure → MarkAsBanned — declining for this PR

Yes, routing recsig score-100 paths through full BanNode now also runs MarkAsBanned, which can sticky-mute share traffic if the peer stays connected (NoBan/manual keep the connection after clearing m_should_discourage, so RemoveBannedNodeStates may never erase the node state).

I’m not splitting that out here:

  • NoBan/manual punishment edge cases are out of scope for this change.
  • For ordinary peers we disconnect on discourage; share nodeStates cleanup then follows existing disconnect reclaim in CSigSharesManager::Cleanup.
  • Pre-existing share-path failures already used full BanNode (misbehave + MarkAsBanned). Treating invalid recovered sigs the same is deliberate: one NetSigning score-100 choke point.

If we later want recsig-only score without muting shares, that can be a small follow-up that misbehaves + RemoveNode without MarkAsBanned on those two sites only.

Happy to revisit either point if we want a follow-up for disconnect-time reclaim or a recsig/share ban split.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Resolved in 249d088Do not permanently disable sig shares for a recovered-signature failure no longer present.

Auto-resolved by the review system based on the latest commit diff. If you believe this was closed in error, reopen the thread.

@PastaPastaPasta PastaPastaPasta left a comment

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.

LGTM

@PastaPastaPasta
PastaPastaPasta requested review from UdjinM6 and knst August 8, 2026 22:52
@PastaPastaPasta PastaPastaPasta changed the title fix(llmq): purge pending recovered sigs in BanNode fix: purge pending recovered sigs in BanNode Aug 12, 2026
PastaPastaPasta and others added 2 commits August 11, 2026 23:44
The eager purge in NetSigning::BanNode is racy: BanNode can run on a recsig/sig-share verification worker thread while the message handler is concurrently processing another QSIGREC from the same peer, so a fresh pending entry can be queued right after the purge and still burn BLS verification after the ban. FinalizeNode runs strictly after the peer's last ProcessMessages call (DeleteNode requires the node refcount to reach zero) and node ids are never reused, so purging there is final. It also reclaims the pending backlog of peers that disconnect without misbehaving instead of waiting for the drain loop to chew through it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ailures

Routing the two raw recsig score-100 sites (invalid QSIGREC llmqType, bad BLS batch source) through BanNode also started calling CSigSharesManager::MarkAsBanned, which those sites never did before. For NoBan/manual peers MaybeDiscourageAndDisconnect clears the discourage flag but keeps the connection, while the sticky nodeState.banned flag suppresses all subsequent sig-share traffic from the peer (TryAddPendingIncomingSigShare, CollectSigSharesToRequest/Send/Announce) for the connection lifetime. Keep the misbehavior score and the pending-recsig purge on those paths, but reserve MarkAsBanned for sig-share protocol failures.

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

@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.

🧹 Nitpick comments (1)
src/llmq/signing.cpp (1)

421-429: 🗄️ Data Integrity & Integration | 🔵 Trivial | 🏗️ Heavy lift

Add a targeted test for the cleanup invariant.

Test that RemoveNode removes only the selected peer’s queue, decrements pendingRecoveredSigsCount by the exact queue size, preserves another peer’s queue and pendingReconstructedRecoveredSigs, and remains safe for missing or repeated node IDs.

As per coding guidelines, “Choose and add targeted C++ unit tests for changed behavior, preferably in existing test files unless a new file is clearly justified.”

🤖 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/llmq/signing.cpp` around lines 421 - 429, Add a focused C++ unit test for
CSigningManager::RemoveNode covering removal of one peer’s queue, exact
pendingRecoveredSigsCount decrement, preservation of another peer’s queue and
pendingReconstructedRecoveredSigs, and safe no-op behavior for missing and
repeated node IDs. Place it in the existing signing-manager test suite and use
the manager’s observable state or established test accessors.

Source: Coding guidelines

🤖 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.

Nitpick comments:
In `@src/llmq/signing.cpp`:
- Around line 421-429: Add a focused C++ unit test for
CSigningManager::RemoveNode covering removal of one peer’s queue, exact
pendingRecoveredSigsCount decrement, preservation of another peer’s queue and
pendingReconstructedRecoveredSigs, and safe no-op behavior for missing and
repeated node IDs. Place it in the existing signing-manager test suite and use
the manager’s observable state or established test accessors.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e000918e-90c5-45b2-a1e0-79311838dd8d

📥 Commits

Reviewing files that changed from the base of the PR and between 38d9364 and 249d088.

📒 Files selected for processing (5)
  • src/llmq/net_signing.cpp
  • src/llmq/net_signing.h
  • src/llmq/signing.cpp
  • src/llmq/signing.h
  • src/net_processing.cpp
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/llmq/net_signing.cpp

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

The current head resolves both previously verified blockers: finalization now performs a durable recovered-signature purge after message processing has ceased, and recovered-signature-only failures no longer mark the separate sig-share state as banned. No remaining correctness defects were found, but the two corrective follow-up commits should be squashed into the original change so the branch does not preserve known-bad intermediate states.
Source: reviewer backends: gpt-5.6-sol (general) and gpt-5.6-sol (dash-core-commit-history); final verifier backend: gpt-5.6-sol. Orchestration-only: openclaw-agent/cliproxy/gpt-5.6-sol (not reviewer evidence).

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

Review provenance

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

🟡 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 `<commit:21fa89b>`:
- [SUGGESTION] <commit:21fa89b>:1: Squash the corrective follow-ups into the original purge commit
  Commits 21fa89b029e and 249d088c8ec explicitly repair defects introduced by bb11a289290: the first closes the post-purge requeue race, while the second prevents recovered-signature failures from incorrectly applying the sticky sig-share ban. Keeping the three commits separately leaves known-bad intermediate states in history and matches the fixup-commit pattern that CONTRIBUTING.md asks authors to squash. Fold both follow-ups into bb11a289290 and update the resulting commit message to describe the finalization purge and recovered-signature-only ban handling.

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