Skip to content

Commit ecf2382

Browse files
Merge #7415: fix: bound pending sig share queue
bdca48c refactor: use CountedBucketMap for counting total amount of pending sigs (Konstantin Akimov) 355da45 fix: cap sig-share verification batch by share count (PastaClaw) 08d9533 fix: cap amount of unverified batches by 4 (Konstantin Akimov) e2862fe fix: the actual cap enforcement for pending signatures (Konstantin Akimov) 4666ef3 test: add regression tests for CountedBucketMap (Konstantin Akimov) ec15f90 refactor: use dedicated util structure to count internal objects in signing-shares (Konstantin Akimov) Pull request description: ## Issue being fixed or feature implemented Pending incoming LLMQ sig shares are admitted cheaply and verified later by worker threads. Direct and batched share traffic could grow retained pending verification work without per-node/global backpressure. This replaces closed PR #7413 and keeps the same scope: resource/backpressure hardening for LLMQ signing, with no consensus or user-facing behavior change. ## What was done? - Add per-node and global caps for pending incoming sig shares. - Track pending counts explicitly so cap checks do not require repeated scans. - Gate `QSIGSHARE` and `QBSIGSHARES` pending admission; over-cap shares are dropped without misbehavior scoring. - Clear pending incoming shares when a peer is banned and skip banned node states during collection. - Refuse new pending work for already-banned node states. - Add unit coverage for cap/drop/count cleanup behavior. ## How Has This Been Tested? - `./autogen.sh` - `./configure --without-gui --disable-bench --disable-fuzz-binary` - `make -C src test/test_dash -j$(sysctl -n hw.ncpu)` - `src/test/test_dash --run_test=llmq_utils_tests` - `git diff --check upstream/develop..HEAD` - `COMMIT_RANGE=upstream/develop..HEAD test/lint/lint-whitespace.py` ## Breaking Changes None. ## Checklist: - [x] I have performed a self-review of my own code - [x] I have commented my code, particularly in hard-to-understand areas - [x] 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)_ ACKs for top commit: kwvg: utACK bdca48c Tree-SHA512: e2403635a608aab5488dbf10b481a778e735a6e5b065eec5a4cc444818f61e89b3a688f82c14aaceae07fd714432d39c013769a54aa66e31bb8abddb3531aaf3
2 parents d9364c1 + bdca48c commit ecf2382

5 files changed

Lines changed: 244 additions & 60 deletions

File tree

src/llmq/net_signing.cpp

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -320,19 +320,31 @@ void NetSigning::WorkThreadDispatcher()
320320
}
321321
}
322322

323-
// Collect pending sig shares synchronously and dispatch each batch to a worker for parallel BLS verification
324-
while (!workInterrupt) {
323+
// Collect pending sig shares synchronously and dispatch each batch to a worker for parallel BLS verification.
324+
// Batches awaiting verification are bounded so that under flood shares back up in the capped
325+
// pending maps instead of migrating into the unbounded worker pool task queue.
326+
//
327+
// Each batch is bounded by actual share count (not unique-session count), so at most
328+
// MAX_UNVERIFIED_BATCHES * MAX_SHARES_PER_BATCH shares can be sitting in / on the worker pool at once.
329+
static constexpr int MAX_UNVERIFIED_BATCHES{4};
330+
static constexpr size_t MAX_SHARES_PER_BATCH{32};
331+
while (!workInterrupt && unverified_batches < MAX_UNVERIFIED_BATCHES) {
325332
std::unordered_map<NodeId, std::vector<CSigShare>> sigSharesByNodes;
326333
std::unordered_map<std::pair<Consensus::LLMQType, uint256>, CQuorumCPtr, StaticSaltedHasher> quorums;
327334

328-
const size_t nMaxBatchSize{32};
329-
bool more_work = m_shares_manager->CollectPendingSigSharesToVerify(nMaxBatchSize, sigSharesByNodes, quorums);
335+
bool more_work = m_shares_manager->CollectPendingSigSharesToVerify(MAX_SHARES_PER_BATCH, sigSharesByNodes, quorums);
330336

331337
if (sigSharesByNodes.empty()) {
332338
break;
333339
}
334340

341+
++unverified_batches;
335342
worker_pool.push([this, sigSharesByNodes = std::move(sigSharesByNodes), quorums = std::move(quorums)](int) mutable {
343+
// Ensures unverified_batches is decremented on every exit path, including exceptions.
344+
struct UnverifiedBatchGuard {
345+
std::atomic<int>& count;
346+
~UnverifiedBatchGuard() { --count; }
347+
} guard{unverified_batches};
336348
ProcessPendingSigShares(std::move(sigSharesByNodes), std::move(quorums));
337349
});
338350

src/llmq/net_signing.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
#include <thread>
1717

18+
#include <atomic>
1819
#include <memory>
1920

2021
class CSporkManager;
@@ -72,6 +73,7 @@ class NetSigning final : public NetHandler, public CValidationInterface
7273
std::thread shares_cleaning_thread;
7374
std::thread shares_dispatcher_thread;
7475
mutable ctpl::thread_pool worker_pool;
76+
std::atomic<int> unverified_batches{0};
7577

7678
CThreadInterrupt workInterrupt;
7779
};

src/llmq/signing_shares.cpp

Lines changed: 47 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ namespace {
3131
constexpr size_t MAX_SESSIONS_PER_PEER_FACTOR{4};
3232
constexpr size_t MIN_SESSIONS_PER_PEER{100};
3333

34+
// Incoming QSIGSHARE/QBSIGSHARES traffic is cheap to admit but drains only at BLS verification
35+
// speed, so unverified shares are bounded and over-cap shares dropped without misbehaviour scoring.
36+
constexpr size_t MAX_PENDING_SIG_SHARES_PER_NODE{1000};
37+
constexpr size_t MAX_PENDING_SIG_SHARES_TOTAL{10000};
38+
3439
size_t GetMaxSessionsForPeer(const Consensus::LLMQParams& params)
3540
{
3641
return std::max<size_t>(size_t(params.size) * MAX_SESSIONS_PER_PEER_FACTOR, MIN_SESSIONS_PER_PEER);
@@ -422,7 +427,7 @@ bool CSigSharesManager::ProcessMessageBatchedSigShares(const CNode& pfrom, const
422427
LOCK(cs);
423428
auto& nodeState = nodeStates[pfrom.GetId()];
424429
for (const auto& s : sigSharesToProcess) {
425-
nodeState.pendingIncomingSigShares.Add(s.GetKey(), s);
430+
TryAddPendingIncomingSigShare(pfrom.GetId(), nodeState, s);
426431
}
427432
return true;
428433
}
@@ -467,16 +472,43 @@ bool CSigSharesManager::ProcessMessageSigShare(NodeId fromId, const CSigShare& s
467472
}
468473

469474
auto& nodeState = nodeStates[fromId];
470-
nodeState.pendingIncomingSigShares.Add(sigShare.GetKey(), sigShare);
475+
TryAddPendingIncomingSigShare(fromId, nodeState, sigShare);
471476
}
472477

473478
LogPrint(BCLog::LLMQ_SIGS, "CSigSharesManager::%s -- signHash=%s, id=%s, msgHash=%s, member=%d, node=%d\n", __func__,
474479
signHash.ToString(), sigShare.getId().ToString(), sigShare.getMsgHash().ToString(), sigShare.getQuorumMember(), fromId);
475480
return true;
476481
}
477482

483+
bool CSigSharesManager::TryAddPendingIncomingSigShare(NodeId nodeId, CSigSharesNodeState& nodeState,
484+
const CSigShare& sigShare)
485+
{
486+
AssertLockHeld(cs);
487+
488+
if (nodeState.banned) {
489+
return false;
490+
}
491+
if (nodeState.pendingIncomingSigShares.Size() >= MAX_PENDING_SIG_SHARES_PER_NODE) {
492+
LogPrint(BCLog::LLMQ_SIGS, "CSigSharesManager::%s -- per-node pending sig shares cap reached (%d), dropping sigShare. node=%d\n",
493+
__func__, MAX_PENDING_SIG_SHARES_PER_NODE, nodeId);
494+
return false;
495+
}
496+
size_t total{0};
497+
for (const auto& [_, ns] : nodeStates) {
498+
// the size of nodeStates is limited by DEFAULT_MAX_PEER_CONNECTIONS(125) so it should not be performance issue
499+
// The name of variable is intentionally mentioned in comment to make this code snippet relevant for possible changes in future
500+
total += ns.pendingIncomingSigShares.Size();
501+
}
502+
if (total >= MAX_PENDING_SIG_SHARES_TOTAL) {
503+
LogPrint(BCLog::LLMQ_SIGS, "CSigSharesManager::%s -- global pending sig shares cap reached (%d), dropping sigShare. node=%d\n",
504+
__func__, MAX_PENDING_SIG_SHARES_TOTAL, nodeId);
505+
return false;
506+
}
507+
return nodeState.pendingIncomingSigShares.Add(sigShare.GetKey(), sigShare);
508+
}
509+
478510
bool CSigSharesManager::CollectPendingSigSharesToVerify(
479-
size_t maxUniqueSessions, std::unordered_map<NodeId, std::vector<CSigShare>>& retSigShares,
511+
size_t maxShares, std::unordered_map<NodeId, std::vector<CSigShare>>& retSigShares,
480512
std::unordered_map<std::pair<Consensus::LLMQType, uint256>, CQuorumCPtr, StaticSaltedHasher>& retQuorums)
481513
{
482514
bool more_work{false};
@@ -487,16 +519,19 @@ bool CSigSharesManager::CollectPendingSigSharesToVerify(
487519
return false;
488520
}
489521

490-
// This will iterate node states in random order and pick one sig share at a time. This avoids processing
491-
// of large batches at once from the same node while other nodes also provided shares. If we wouldn't do this,
492-
// other nodes would be able to poison us with a large batch with N-1 valid shares and the last one being
493-
// invalid, making batch verification fail and revert to per-share verification, which in turn would slow down
494-
// the whole verification process
495-
std::unordered_set<std::pair<NodeId, uint256>, StaticSaltedHasher> uniqueSignHashes;
522+
// Iterate node states in random order and pick one sig share at a time. This ensures no single peer can
523+
// dominate a batch and that a large flood from one peer cannot poison batch verification (an N-1 valid /
524+
// 1 invalid batch would fall back to per-share verification and slow the whole pipeline).
525+
//
526+
// The batch is bounded by the number of shares actually added (maxShares), not by the count of unique
527+
// (nodeId, signHash) sessions. Bounding by sessions could otherwise let a single session inflate the
528+
// batch to the full pending-share cap, and, together with the in-flight batch cap, keep tens of thousands
529+
// of shares outside the pending accounting.
530+
size_t sharesAdded{0};
496531
IterateNodesRandom(
497532
nodeStates,
498533
[&]() {
499-
return uniqueSignHashes.size() < maxUniqueSessions;
534+
return sharesAdded < maxShares;
500535
// TODO: remove NO_THREAD_SAFETY_ANALYSIS
501536
// using here template IterateNodesRandom makes impossible to use lock annotation
502537
},
@@ -508,8 +543,8 @@ bool CSigSharesManager::CollectPendingSigSharesToVerify(
508543

509544
AssertLockHeld(cs);
510545
if (const bool alreadyHave = this->sigShares.Has(sigShare.GetKey()); !alreadyHave) {
511-
uniqueSignHashes.emplace(nodeId, sigShare.GetSignHash());
512546
retSigShares[nodeId].emplace_back(sigShare);
547+
++sharesAdded;
513548
}
514549
ns.pendingIncomingSigShares.Erase(sigShare.GetKey());
515550
return !ns.pendingIncomingSigShares.Empty();
@@ -1426,6 +1461,7 @@ void CSigSharesManager::MarkAsBanned(NodeId nodeId)
14261461
sigSharesRequested.Erase(k);
14271462
});
14281463
nodeState.requestedSigShares.Clear();
1464+
nodeState.pendingIncomingSigShares.Clear();
14291465
nodeState.banned = true;
14301466
}
14311467

0 commit comments

Comments
 (0)