fix: memoize governance vote signature checks to bound govsync cost - #7518
fix: memoize governance vote signature checks to bound govsync cost#7518PastaPastaPasta wants to merge 1 commit into
Conversation
|
Warning Review limit reached
Next review available in: 34 minutes You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (6)
WalkthroughGovernance votes now memoize ECDSA and BLS signature-verification results using vote content and verification-key data. Vote synchronization iterates stored votes without copying them. New tests cover request quotas, peer-port isolation, repeated checks, cache invalidation, key changes, and separate ECDSA and BLS cache entries. Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant GetSyncableVoteInvs
participant CGovernanceObjectVoteFile
participant CGovernanceVote
GetSyncableVoteInvs->>CGovernanceObjectVoteFile: iterate stored votes by const reference
CGovernanceObjectVoteFile-->>GetSyncableVoteInvs: provide stored vote
GetSyncableVoteInvs->>CGovernanceVote: check signature
CGovernanceVote-->>GetSyncableVoteInvs: return cached or computed verdict
Suggested reviewers: 🚥 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 |
|
✅ Final review complete — no blockers (commit 3b03544) |
| // Iterate the stored votes in place: CheckSignature memoises its verdict on | ||
| // the vote instance, and a GetVotes() copy would discard that memo, so every | ||
| // walk would pay a fresh ECDSA recovery or BLS pairing per vote. | ||
| for (const auto& vote : fileVotes.GetVoteList()) { |
There was a problem hiding this comment.
fileVotes.GetVotes copies; fileVotes.GetVoteList takes a reference; so when we later do vote.IsValid; when using GetVotes, nothing is cached, w/ GetVoteList it's cached (as my understanding)
There was a problem hiding this comment.
🧹 Nitpick comments (2)
src/governance/vote.cpp (2)
186-206: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winAvoid recomputing
GetSignatureHash()twice on a cache miss.
GetSignatureHash()re-serializes and hashes the whole vote object.CheckSignature(const CBLSPublicKey&)calls it once to buildcache_keyand a second time to pass intoVerifyInsecure. This duplicate work runs on every fresh verification (new vote or new key), on the mainnet BLS path used for ordinary masternode voting. Since this PR's purpose is to remove redundant signature-related work, cache the hash once and reuse it.♻️ Proposed fix
bool CGovernanceVote::CheckSignature(const CBLSPublicKey& pubKey) const { - const uint256 cache_key{SignatureCacheKey(pubKey, GetSignatureHash(), vchSig)}; + const uint256 sigHash{GetSignatureHash()}; + const uint256 cache_key{SignatureCacheKey(pubKey, sigHash, vchSig)}; if (m_sig_checked && m_sig_check_key == cache_key) { return m_sig_valid; } g_governance_vote_signature_checks.fetch_add(1, std::memory_order_relaxed); CBLSSignature sig; sig.SetBytes(vchSig, false); - const bool valid{sig.VerifyInsecure(pubKey, GetSignatureHash(), false)}; + const bool valid{sig.VerifyInsecure(pubKey, sigHash, false)};🤖 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/governance/vote.cpp` around lines 186 - 206, Update CGovernanceVote::CheckSignature so GetSignatureHash() is evaluated once on a cache miss, store that hash in a local variable, and reuse it both when constructing cache_key and when calling VerifyInsecure. Preserve the existing cache-hit behavior and signature validation flow.
155-184: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winSame redundant
GetSignatureHash()call in the TESTNET branch.For consistency with the fix suggested for
CheckSignature(const CBLSPublicKey&), apply the same hash reuse here forCHashSigner::VerifyHash. Impact is smaller since this branch only runs on testnet, but the fix is the same one-line change.♻️ Proposed fix
bool CGovernanceVote::CheckSignature(const CKeyID& keyID) const { - const uint256 cache_key{SignatureCacheKey(keyID, GetSignatureHash(), vchSig)}; + const uint256 sigHash{GetSignatureHash()}; + const uint256 cache_key{SignatureCacheKey(keyID, sigHash, vchSig)}; if (m_sig_checked && m_sig_check_key == cache_key) { return m_sig_valid; } @@ if (Params().NetworkIDString() == CBaseChainParams::TESTNET) { - valid = CHashSigner::VerifyHash(GetSignatureHash(), keyID, vchSig, strError); + valid = CHashSigner::VerifyHash(sigHash, keyID, vchSig, strError);🤖 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/governance/vote.cpp` around lines 155 - 184, Update CGovernanceVote::CheckSignature(const CKeyID&) so the TESTNET branch computes GetSignatureHash() once and reuses that value when calling CHashSigner::VerifyHash, matching the existing hash-reuse fix for the BLS overload.
🤖 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/governance/vote.cpp`:
- Around line 186-206: Update CGovernanceVote::CheckSignature so
GetSignatureHash() is evaluated once on a cache miss, store that hash in a local
variable, and reuse it both when constructing cache_key and when calling
VerifyInsecure. Preserve the existing cache-hit behavior and signature
validation flow.
- Around line 155-184: Update CGovernanceVote::CheckSignature(const CKeyID&) so
the TESTNET branch computes GetSignatureHash() once and reuses that value when
calling CHashSigner::VerifyHash, matching the existing hash-reuse fix for the
BLS overload.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: a92fbd0d-4564-47ce-b5c6-f20d3f0d5ca9
📒 Files selected for processing (7)
src/Makefile.test.includesrc/governance/governance.cppsrc/governance/net_governance.cppsrc/governance/vote.cppsrc/governance/vote.hsrc/governance/votedb.hsrc/test/governance_vote_sync_tests.cpp
3414191 to
7f40ff7
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ef602678a9
ℹ️ 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".
| HashWriter ss{}; | ||
| ss << key << sigHash << vchSig; |
There was a problem hiding this comment.
Serialize BLS cache keys using the verification scheme
During a BLS activation or reorg where bls_legacy_scheme changes, ss << key serializes CBLSPublicKey using that mutable global, while the cached operation is always VerifyInsecure(..., false). Legacy serialization of a key can equal basic serialization of its negation, so if an operator key rotates from P to -P across such a boundary, a valid verdict cached for P can be returned for -P, allowing the old vote to survive revalidation and be advertised despite failing verification. Serialize the fingerprint key explicitly with the non-legacy scheme used by verification.
AGENTS.md reference: AGENTS.md:L169-L170
Useful? React with 👍 / 👎.
thepastaclaw
left a comment
There was a problem hiding this comment.
Preliminary review — Codex only
The final tree implements the governance vote signature memo safely and leaves fulfilled-request keying unchanged, with relevant regression coverage. However, the commit stack deliberately includes a failing test, an unsafe signature-cache implementation, and a network-throttle change later reverted after breaking functional tests, so the history must be rewritten before merge.
Validated blockers were found in the Codex precheck. Sonnet 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)
🔴 1 blocking | 🟡 1 suggestion(s)
2 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:7647f98>`:
- [BLOCKING] <commit:7647f98>:1: Rewrite the known-red throttle-and-revert sequence
The stack is not bisectable. Commit 7647f983a51 explicitly adds a test that its message says fails until the next commit. Commit 00b3b2bcadb then makes fulfilled requests port-agnostic, which 747cbdc8bd6 later reverts because it causes honest peers sharing an address to accumulate misbehavior scores and makes feature_governance.py fail. The same intermediate commit caches signature verdicts using only the verification key, allowing signed-content mutation to reuse a stale verdict until cde8a4668a0 repairs it. Rewrite the stack so the signature memo is introduced with its final content-sensitive fingerprint and tests, remove the netfulfilledman.cpp change/revert, and introduce any retained port-sensitivity test directly in passing form. Preserve the rejected throttle rationale in the final commit message or test comments rather than permanent reverted history.
In `<commit:43eefde>`:
- [SUGGESTION] <commit:43eefde>:1: Fold cleanup of newly introduced comments and vote traversal API
Commit 43eefdea957 only rewrites or removes comments introduced earlier in this stack, including deleting the net_governance.cpp explanation added by 747cbdc8bd6. Similarly, 7f40ff728c3 replaces GetVoteList(), which this stack had just introduced, with the final ForEachVote() API. Fold these review-time corrections into the commits that introduce the signature memo and stored-vote traversal so permanent history contains the final documentation and API directly.
ef60267 to
23e8e5f
Compare
GetSyncableVoteInvs walks every stored vote for a requested object and calls IsValid(), which unconditionally re-runs full cryptographic verification: ECDSA recovery for proposal funding votes, or a BLS pairing for operator-key votes. Those signatures were already verified at acceptance, so the work is pure waste. It runs on the message-handler thread while holding cs_store, driven by an unauthenticated MNGOVERNANCESYNC, and the existing quota is keyed on a CService that includes the ephemeral source port, so reconnecting bypasses it. Memoize the verdict, keyed on the verification key, GetSignatureHash() and the signature bytes. GetSignatureHash() is a SerializeHash over exactly the fields GetSignatureString() signs, so mutating a covered field invalidates the memo. The BLS key is fingerprinted with the non-legacy scheme that VerifyInsecure actually uses, rather than via operator<< and the mutable bls_legacy_scheme global: a legacy encoding of P can equal the basic encoding of -P, so serializing under the global would let a verdict cached for one key be served for the other across an activation or reorg. An intermediate revision also throttled govsync by IP; that was dropped deliberately, because keying on IP alone plus a misbehaviour score would let an attacker get NAT-colocated honest peers discouraged. The residual per-request cost is still O(votes) under cs_store with no working rate limit; a per-CNode request budget is still needed and is not attempted here.
23e8e5f to
3b03544
Compare
thepastaclaw
left a comment
There was a problem hiding this comment.
Final validation — Codex + Sonnet
The rewritten one-commit range resolves both prior history findings and directly introduces the final content-sensitive signature memo and callback-based vote traversal. The implementation is sound, but the fixed-scheme BLS key encoding protects a subtle security invariant that is not covered by the new regression tests.
Review provenance
- Codex reviewers:
gpt-5.6-sol— general (completed),gpt-5.6-sol— dash-core-commit-history (completed) - Verifier:
gpt-5.6-sol— final_verifier (fallback) - Sonnet reviewers:
claude-sonnet-5— general (failed),claude-sonnet-5— dash-core-commit-history (failed),claude-sonnet-5— general (completed),claude-sonnet-5— dash-core-commit-history (completed)
🟡 1 suggestion(s)
🤖 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/governance/vote.cpp`:
- [SUGGESTION] src/governance/vote.cpp:41-43: Add a regression for cross-scheme BLS fingerprint collisions
The explicit non-legacy key encoding prevents a valid cached verdict from being reused for a different point after `bls_legacy_scheme` changes, but the new tests do not exercise this invariant. For a public key whose legacy sign bit is set, parsing its legacy bytes under the basic scheme produces the opposite point, and `basic(-P)` has the same bytes as `legacy(P)`. A future refactor from `ToBytes(false)` back to generic serialization would therefore let a verdict cached for P under the legacy global scheme be returned for -P under the basic scheme. Add a regression that selects such a key, warms a valid verdict for P while the global scheme is legacy, parses `P.ToBytes(true)` as a basic public key, switches the global scheme to basic, and verifies that the opposite key has no memoized verdict and fails verification. Restore the global scheme on test exit.
| const auto key_bytes = key.ToBytes(/*specificLegacyScheme=*/false); | ||
| ss.write(MakeByteSpan(key_bytes)); | ||
| ss << sigHash << vchSig; |
There was a problem hiding this comment.
🟡 Suggestion: Add a regression for cross-scheme BLS fingerprint collisions
The explicit non-legacy key encoding prevents a valid cached verdict from being reused for a different point after bls_legacy_scheme changes, but the new tests do not exercise this invariant. For a public key whose legacy sign bit is set, parsing its legacy bytes under the basic scheme produces the opposite point, and basic(-P) has the same bytes as legacy(P). A future refactor from ToBytes(false) back to generic serialization would therefore let a verdict cached for P under the legacy global scheme be returned for -P under the basic scheme. Add a regression that selects such a key, warms a valid verdict for P while the global scheme is legacy, parses P.ToBytes(true) as a basic public key, switches the global scheme to basic, and verifies that the opposite key has no memoized verdict and fails verification. Restore the global scheme on test exit.
source: ['codex']
Issue being fixed or feature implemented
CGovernanceManager::GetSyncableVoteInvs()walks every stored vote for a requested object and callsCGovernanceVote::IsValid(), which unconditionally re-runs full cryptographic verification viaCheckSignature()- ECDSA recovery for proposal funding votes, or a BLS pairing (~1 ms) for operator-key votes. Those signatures were already verified at acceptance inCGovernanceObject::ProcessVote, so the work is pure waste.The walk is driven by the P2P
MNGOVERNANCESYNChandler and runs on the single message-handler thread while holdingcs_storeandgovobj.cs. On a mainnet trigger with thousands of masternode votes this is seconds of pairing work per request, stalling all P2P message processing.Any unauthenticated peer can trigger it once the victim is synced. The existing
CNetFulfilledRequestManagerquota does not help: it is keyed onpeer.addr, aCServicethat includes the ephemeral source port, and it is per-object, so it is bypassed by reconnecting or by cycling through other known objects.What was done?
CGovernanceVote, keyed on the verification key,GetSignatureHash()and the signature bytes.GetSignatureHash()is aSerializeHashover exactly the fields thatGetSignatureString()signs, so the memo cannot be bypassed by mutating a covered field, and key-type confusion is not possible.CGovernanceObjectVoteFile::GetVoteList()soGetSyncableVoteInvsiterates live votes instead of copies.On the reverted commit: an intermediate commit in this branch throttled govsync by IP and is deliberately reverted by the last commit. Keying the throttle on IP alone, combined with a misbehaviour score, would let one attacker get NAT-colocated honest peers discouraged. That trade was not worth it, and the history is kept so the reasoning is visible rather than looking like an oversight.
Known remaining gap: the residual per-request cost is still O(votes) under
cs_store- aSerializeHash, a bloomcontainsand a masternode lookup per vote - with no working rate limit, and a cold cache after restart pays full price once. A proper request-rate or work budget onMNGOVERNANCESYNC, keyed perCNoderather than perCService, is still needed and is not attempted here.How Has This Been Tested?
The first commit adds a regression test proving the govsync fulfilled-request throttle is port-keyed, ordered before the fix.
Full build and test validation is delegated to CI on this PR; the changes were not built locally.
Breaking Changes
None.
Checklist: