Skip to content

Commit d76b76c

Browse files
fix(llmq): purge pending recovered sigs in BanNode
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: that predicate was a ~100ms one-shot and never reliably ran. Caps from #7402 remain the bound for peers that disconnect without misbehavior. The eager purge alone is racy: BanNode can run on a verification worker thread while the message handler is still processing a QSIGREC from the same peer, re-queueing an entry behind the purge. Back it up in FinalizeNode, which runs strictly after the peer's last ProcessMessages call (DeleteNode requires the node refcount to reach zero) and node ids are never reused, so that purge is final. It is gated on misbehavior >= DISCOURAGEMENT_THRESHOLD - BanNode always scores exactly 100 - so peers that disconnect without misbehaving keep their queue and drain as before; their pending sigs may be the only copy we ever receive. The recovered-sig-only failure paths pass mark_shares_banned=false so they do not set the sticky CSigSharesManager nodeState.banned flag: NoBan/manual peers survive the misbehavior score with the connection intact, and marking them would mute their sig-share traffic for the connection lifetime. MarkAsBanned stays reserved for sig-share protocol failures, as before. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent bb17062 commit d76b76c

5 files changed

Lines changed: 35 additions & 21 deletions

File tree

‎src/llmq/net_signing.cpp‎

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ void NetSigning::ProcessMessage(CNode& pfrom, const std::string& msg_type, CData
5454
recoveredSig->GetHash()}));
5555

5656
if (!Params().GetLLMQ(recoveredSig->getLlmqType()).has_value()) {
57-
m_peer_manager->PeerMisbehaving(pfrom.GetId(), 100);
57+
BanNode(pfrom.GetId(), /*mark_shares_banned=*/false);
5858
return;
5959
}
6060

@@ -264,7 +264,7 @@ bool NetSigning::ProcessPendingRecoveredSigs()
264264
for (const auto& [nodeId, v] : recSigsByNode) {
265265
if (batchVerifier.badSources.count(nodeId)) {
266266
LogPrint(BCLog::LLMQ, "NetSigning::%s -- invalid recSig from other node, banning peer=%d\n", __func__, nodeId);
267-
m_peer_manager->PeerMisbehaving(nodeId, 100);
267+
BanNode(nodeId, /*mark_shares_banned=*/false);
268268
continue;
269269
}
270270

@@ -288,10 +288,6 @@ void NetSigning::WorkThreadSigning()
288288
constexpr auto CLEANUP_INTERVAL{5s};
289289
if (cleanupThrottler.TryCleanup(CLEANUP_INTERVAL)) {
290290
m_sig_manager.Cleanup();
291-
// Drop pending recovered sigs queued by banned peers so a flood's backlog does not
292-
// persist after the peer is banned (RemoveBannedNodeStates only cleans the sig-shares
293-
// subsystem, not m_sig_manager's pending recovered sigs).
294-
m_sig_manager.RemoveNodesIf([this](NodeId node_id) { return m_peer_manager->PeerIsBanned(node_id); });
295291
}
296292

297293
// TODO Wakeup when pending signing is needed?
@@ -308,12 +304,17 @@ void NetSigning::RemoveBannedNodeStates()
308304
m_shares_manager->RemoveNodesIf([this](NodeId node_id) { return m_peer_manager->PeerIsBanned(node_id); });
309305
}
310306

311-
void NetSigning::BanNode(NodeId nodeId)
307+
void NetSigning::BanNode(NodeId nodeId, bool mark_shares_banned)
312308
{
313309
if (nodeId == -1) return;
314310

315311
m_peer_manager->PeerMisbehaving(nodeId, 100);
316-
if (m_shares_manager) {
312+
// Drop any not-yet-verified recovered sigs still queued for this peer so a flood's backlog
313+
// does not keep burning the single recsig worker after we have already decided to ban. A
314+
// QSIGREC processed concurrently with a ban issued from a worker thread can still re-queue
315+
// entries; FinalizeNode() purges those once the peer is gone for good.
316+
m_sig_manager.RemoveNode(nodeId);
317+
if (mark_shares_banned && m_shares_manager) {
317318
m_shares_manager->MarkAsBanned(nodeId);
318319
}
319320
}

‎src/llmq/net_signing.h‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,11 @@ class NetSigning final : public NetHandler, public CValidationInterface
7272
std::unordered_map<std::pair<Consensus::LLMQType, uint256>, CQuorumCPtr, StaticSaltedHasher>&& quorums);
7373

7474
void RemoveBannedNodeStates();
75-
void BanNode(NodeId nodeid);
75+
//! Score the peer with 100 misbehavior points and drop its not-yet-verified pending recovered
76+
//! sigs. mark_shares_banned additionally suppresses the peer's sig-share channel and must stay
77+
//! false for recovered-sig-only failures: NoBan/manual peers survive the misbehavior score, and
78+
//! the sticky sig-share ban would otherwise mute a still-connected peer's shares for good.
79+
void BanNode(NodeId nodeid, bool mark_shares_banned = true);
7680

7781
private:
7882
CSigningManager& m_sig_manager;

‎src/llmq/signing.cpp‎

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -418,17 +418,15 @@ void CSigningManager::VerifyAndProcessRecoveredSig(NodeId from, std::shared_ptr<
418418
++pendingRecoveredSigsCount;
419419
}
420420

421-
void CSigningManager::RemoveNodesIf(const std::function<bool(NodeId)>& predicate)
421+
void CSigningManager::RemoveNode(NodeId node_id)
422422
{
423423
LOCK(cs_pending);
424-
for (auto it = pendingRecoveredSigs.begin(); it != pendingRecoveredSigs.end();) {
425-
if (predicate(it->first)) {
426-
pendingRecoveredSigsCount -= it->second.size();
427-
it = pendingRecoveredSigs.erase(it);
428-
} else {
429-
++it;
430-
}
424+
auto it = pendingRecoveredSigs.find(node_id);
425+
if (it == pendingRecoveredSigs.end()) {
426+
return;
431427
}
428+
pendingRecoveredSigsCount -= it->second.size();
429+
pendingRecoveredSigs.erase(it);
432430
}
433431

434432
bool CSigningManager::CollectPendingRecoveredSigsToVerify(

‎src/llmq/signing.h‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
#include <sync.h>
1515
#include <unordered_lru_cache.h>
1616

17-
#include <functional>
1817
#include <memory>
1918
#include <string_view>
2019
#include <unordered_map>
@@ -217,9 +216,10 @@ class CSigningManager
217216
size_t maxUniqueSessions, std::unordered_map<NodeId, std::list<std::shared_ptr<const CRecoveredSig>>>& retSigShares,
218217
std::unordered_map<std::pair<Consensus::LLMQType, uint256>, CBLSPublicKey, StaticSaltedHasher>& ret_pubkeys)
219218
EXCLUSIVE_LOCKS_REQUIRED(!cs_pending);
220-
// Drop the pending (not-yet-verified) recovered sigs of any node matching the predicate, e.g.
221-
// banned peers. Without this, a flooded peer's backlog would persist even after it is banned.
222-
void RemoveNodesIf(const std::function<bool(NodeId)>& predicate) EXCLUSIVE_LOCKS_REQUIRED(!cs_pending);
219+
// Drop pending (not-yet-verified) recovered sigs queued by this peer (called eagerly by
220+
// NetSigning::BanNode and finally by PeerManagerImpl::FinalizeNode once a discouraged peer
221+
// is gone). Does not touch pendingReconstructedRecoveredSigs (local, node id -1).
222+
void RemoveNode(NodeId node_id) EXCLUSIVE_LOCKS_REQUIRED(!cs_pending);
223223
[[nodiscard]] std::vector<CRecoveredSigsListener*> GetListeners() const EXCLUSIVE_LOCKS_REQUIRED(!cs_listeners);
224224
// Returns true if recovered sigs should be send to listeners
225225
[[nodiscard]] bool ProcessRecoveredSig(const std::shared_ptr<const CRecoveredSig>& recoveredSig)

‎src/net_processing.cpp‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1798,6 +1798,17 @@ void PeerManagerImpl::FinalizeNode(const CNode& node) {
17981798
}
17991799
} // cs_main
18001800

1801+
if (misbehavior >= DISCOURAGEMENT_THRESHOLD) {
1802+
// Drop the not-yet-verified pending recovered sigs of a peer that crossed the
1803+
// discouragement threshold. NetSigning::BanNode purges eagerly (and always scores exactly
1804+
// DISCOURAGEMENT_THRESHOLD), but a QSIGREC processed concurrently with a ban issued from a
1805+
// verification worker thread can re-queue entries behind the purge. This runs strictly
1806+
// after the peer's last ProcessMessages call and node ids are never reused, so it is
1807+
// final. Peers that disconnect without misbehaving keep their queue and drain as before:
1808+
// their pending sigs may be the only copy we ever receive.
1809+
m_llmq_ctx.sigman->RemoveNode(nodeid);
1810+
}
1811+
18011812
if (node.fSuccessfullyConnected && misbehavior == 0 && !node.IsBlockOnlyConn() && !node.IsInboundConn()) {
18021813
// Only change visible addrman state for full outbound peers. We don't
18031814
// call Connected() for feeler connections since they don't have

0 commit comments

Comments
 (0)