Skip to content

fix(llmq): purge pending recovered sigs in BanNode - #7563

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

fix(llmq): purge pending recovered sigs in BanNode#7563
PastaPastaPasta wants to merge 1 commit 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

⛔ Blockers found — Opus deferred (commit bb11a28)
Canonical validated blockers: 2

@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

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 banned node before updating share-manager ban state. Cleanup no longer depends on transient ban status. The RemoveNodesIf documentation now distinguishes pending unverified signatures from locally reconstructed signatures.

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

Sequence Diagram(s)

sequenceDiagram
  participant Peer
  participant CSigningManager
  participant ShareManager
  Peer->>CSigningManager: Send invalid recovered-signature message
  CSigningManager->>CSigningManager: Call BanNode(peer)
  CSigningManager->>CSigningManager: Remove pending unverified signatures
  CSigningManager->>ShareManager: Update share-manager ban state
Loading

Possibly related PRs

  • dashpay/dash#7524: Cleans up per-node pending LLMQ state when peers are banned or disconnected.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
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.
Title check ✅ Passed The title clearly identifies the main change: purging pending recovered signatures in BanNode.
Description check ✅ Passed The description explains the BanNode cleanup, recovered-signature paths, rationale, testing, and unchanged behavior.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 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.

Comment thread src/llmq/net_signing.cpp
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.

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