perf: check quorum activity before materializing the quorum on QSIGREC - #7531
perf: check quorum activity before materializing the quorum on QSIGREC#7531PastaPastaPasta wants to merge 1 commit into
Conversation
|
🕓 Ready for review — 10 ahead in queue (commit dcdc0fa) |
Walkthrough
Estimated code review effort: 2 (Simple) | ~10 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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.
Inline comments:
In `@src/llmq/signing.cpp`:
- Around line 371-375: Correct the rationale comment above the IsQuorumActive
gate: state that it is bounded to scanning/materializing only recent
keepOldConnections quorums, while GetQuorum may reconstruct the arbitrary
historical quorum supplied by the peer. Preserve the explanation that inactive
hashes must not trigger that historical reconstruction, and remove the
inaccurate claim that ScanQuorums only scans an existing cached set.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: c8cb580d-63d3-4291-b192-2f3973380fc2
📒 Files selected for processing (1)
src/llmq/signing.cpp
thepastaclaw
left a comment
There was a problem hiding this comment.
Final validation — Codex + Sonnet
This PR reorders two checks in CSigningManager::VerifyAndProcessRecoveredSig so the cheap IsQuorumActive gate runs before the expensive GetQuorum lookup, preventing an unsolicited QSIGREC naming an inactive/historical quorum hash from forcing arbitrary historical quorum reconstruction. I verified the reordering is correct and safe: NetSigning::ProcessMessage already validates the llmq type before calling this function, and BuildQuorumFromCommitment unconditionally inserts into mapQuorumsCache on every build, so the subsequent GetQuorum call is very likely (though not unconditionally guaranteed under LRU eviction) a cache hit. The only issue found is a documentation nitpick: the new comments slightly overstate the caching guarantee of ScanQuorums/GetQuorum, since scanQuorumsCache and mapQuorumsCache are separate LRUs and a cache-hit return path in ScanQuorums does not always freshly repopulate mapQuorumsCache. This does not affect correctness of the optimization, only comment precision.
Review provenance
- Codex reviewers:
gpt-5.6-sol— general (completed),gpt-5.6-sol— dash-core-commit-history (completed) - Verifier:
claude-sonnet-5— final-verifier - Sonnet reviewers:
claude-sonnet-5— general (completed),claude-sonnet-5— dash-core-commit-history (completed)
💬 1 nitpick(s)
| @@ -383,6 +383,17 @@ void CSigningManager::VerifyAndProcessRecoveredSig(NodeId from, std::shared_ptr< | |||
| return; | |||
| } | |||
|
|
|||
| // Once IsQuorumActive has passed, quorumHash is one of the few quorums ScanQuorums | |||
| // just materialised and cached, so this is a cache hit rather than a rebuild. | |||
| auto quorum = qman.GetQuorum(llmq_type, quorum_hash); | |||
There was a problem hiding this comment.
💬 Nitpick: Comments overstate ScanQuorums/GetQuorum caching guarantee
The new comments state that IsQuorumActive "only scans the small cached set of recent quorums" and that once it passes, the subsequent GetQuorum call is "a cache hit rather than a rebuild." This isn't quite accurate: ScanQuorums (called by IsQuorumActive) does more than scan a cache — on a scanQuorumsCache miss it calls GetMinedCommitmentsUntilBlock/GetMinedCommitmentsIndexedUntilBlock and materializes up to keepOldConnections quorums via GetQuorum/BuildQuorumFromCommitment. Also, scanQuorumsCache and mapQuorumsCache are separate LRU caches (the former in quorumsman.h keyed by block hash and holding CQuorumCPtr vectors, the latter keyed by quorum hash and holding CQuorumPtr): when ScanQuorums returns a hit from scanQuorumsCache without touching mapQuorumsCache, the quorums it returns may have already aged out of mapQuorumsCache's LRU. The subsequent GetQuorum call would then rebuild via BuildQuorumFromCommitment rather than hitting the cache. This doesn't break the optimization's safety property — the rebuild in that fallback case is still bounded to one of the last few active quorums, not an attacker-chosen historical one — but the comments should describe a bounded-cost guarantee rather than an unconditional cache-hit guarantee.
| // Cheap gate first. IsQuorumActive bounds work to scanning/materializing the | |
| // keepOldConnections most recent quorums. GetQuorum, by contrast, can rebuild the | |
| // arbitrary historical quorum named by the peer on a cache miss, so reject | |
| // inactive hashes before allowing that work. Caller (NetSigning) has already | |
| // rejected unknown llmq types. | |
| if (!IsQuorumActive(llmq_type, qman, quorum_hash)) { | |
| return; | |
| } | |
| // It's important to only skip seen *valid* sig shares here. See comment for CBatchedSigShare | |
| // We don't receive recovered sigs in batches, but we do batched verification per node on these | |
| if (db.HasRecoveredSigForHash(recoveredSig->GetHash())) { | |
| return; | |
| } | |
| // Now that IsQuorumActive restricted quorum_hash to one of the last few active | |
| // quorums, this GetQuorum call is usually a cache hit, and in the rare miss case | |
| // is a cheap rebuild bounded to a recent quorum rather than an arbitrary one. | |
| auto quorum = qman.GetQuorum(llmq_type, quorum_hash); |
source: ['codex']
…QSIGREC VerifyAndProcessRecoveredSig called GetQuorum before IsQuorumActive. GetQuorum takes the peer-supplied quorum hash and rebuilds arbitrary historical mined commitments (DMN list replay plus member selection) on a cache miss, whereas IsQuorumActive is bounded to the keepOldConnections most recent quorums at the tip. An unsolicited QSIGREC naming an inactive quorum hash therefore forced the expensive path before the cheap gate could reject it. Swap the order so the cheap gate runs first. Once IsQuorumActive passes, the hash is one of those recent quorums, so the subsequent GetQuorum is usually a cache hit and at worst rebuilds a recent quorum rather than an arbitrary historical one. A null quorum there is no longer peer-controlled, so it is logged without a misbehaviour score.
87753a0 to
dcdc0fa
Compare
Issue being fixed or feature implemented
CSigningManager::VerifyAndProcessRecoveredSigcalledqman.GetQuorum(...)beforeIsQuorumActive(...).Those two have very different costs.
IsQuorumActiveis bounded to thekeepOldConnectionsmost recent quorums at the tip, and that set is shared and cached across callers.GetQuorumtakes the peer-supplied hash and can rebuild an arbitrary historical mined commitment on a cache miss — a deterministic masternode list replay plus member selection. An unsolicitedQSIGRECnaming an inactive quorum hash therefore forced the expensive path before the cheap gate had a chance to reject it.This was split out of #7519, where it had been bundled with unrelated QGETDATA work.
What was done?
Swap the order so the cheap gate runs first. Once
IsQuorumActivepasses, the hash is one of the recent quorumsScanQuorumscovers, so the subsequentGetQuorumis usually served from cache.ScanQuorumsandGetQuorumkeep separate LRUs, so a rebuild there is still possible, but only of a recent quorum — never of the arbitrary historical one a peer could otherwise name.A null quorum after that point is no longer peer-controlled — it means the quorum was reported active but is no longer materializable, e.g. after a reorg — so it is logged without a misbehaviour score.
The caller (
NetSigning) has already rejected unknown LLMQ types before this point, so the reordering does not widen what reachesIsQuorumActive.How Has This Been Tested?
Compiles cleanly. This is a reordering of two existing checks with no behavioural change for valid input, so it is covered by the existing QSIGREC paths in the functional suite. Full validation is delegated to CI.
Breaking Changes
None.
Checklist: