Repository navigation
fix: preserve and bound orphan governance votes #7819
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,6 +16,8 @@ | |
| #include <timedata.h> | ||
| #include <util/string.h> | ||
|
|
||
| #include <tuple> | ||
|
|
||
| static_assert(CGovernanceVote::COMPACT_SIG_SIZE == CPubKey::COMPACT_SIGNATURE_SIZE); | ||
| static_assert(CGovernanceVote::BLS_SIG_SIZE == CBLSSignature::SerSize); | ||
|
|
||
|
|
@@ -262,31 +264,6 @@ bool operator==(const CGovernanceVote& vote1, const CGovernanceVote& vote2) | |
|
|
||
| bool operator<(const CGovernanceVote& vote1, const CGovernanceVote& vote2) | ||
| { | ||
| bool fResult = (vote1.masternodeOutpoint < vote2.masternodeOutpoint); | ||
| if (!fResult) { | ||
| return false; | ||
| } | ||
| fResult = (vote1.masternodeOutpoint == vote2.masternodeOutpoint); | ||
|
|
||
| fResult = fResult && (vote1.nParentHash < vote2.nParentHash); | ||
| if (!fResult) { | ||
| return false; | ||
| } | ||
| fResult = (vote1.nParentHash == vote2.nParentHash); | ||
|
|
||
| fResult = fResult && (vote1.nVoteOutcome < vote2.nVoteOutcome); | ||
| if (!fResult) { | ||
| return false; | ||
| } | ||
| fResult = (vote1.nVoteOutcome == vote2.nVoteOutcome); | ||
|
|
||
| fResult = fResult && (vote1.nVoteSignal == vote2.nVoteSignal); | ||
| if (!fResult) { | ||
| return false; | ||
| } | ||
| fResult = (vote1.nVoteSignal == vote2.nVoteSignal); | ||
|
|
||
| fResult = fResult && (vote1.nTime < vote2.nTime); | ||
|
|
||
| return fResult; | ||
| return std::tie(vote1.masternodeOutpoint, vote1.nParentHash, vote1.nVoteOutcome, vote1.nVoteSignal, vote1.nTime) < | ||
| std::tie(vote2.masternodeOutpoint, vote2.nParentHash, vote2.nVoteOutcome, vote2.nVoteSignal, vote2.nTime); | ||
|
Comment on lines
+267
to
+268
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
🟡 Suggestion: Bound same-signer orphan backlogs before replay The repaired ordering retains timestamp variants from the same signer for the same parent, signal, and outcome. A registered masternode can submit many authenticated variants with increasing positive timestamps below the accepted upper bound; the unknown-parent path has no per-signer limit, only the shared 1,000,000-entry cap. When that parent arrives, CheckOrphanVotes materializes the entire parent's backlog and disables rate checks while holding cs_store; AddGovernanceObjectInternal also holds cs_main. Because these variants are returned in increasing timestamp order, they pass the obsolete-vote check and each accepted vote performs updates, notifications, and relay work. NotifyGovernanceVote additionally copies the tip masternode list for each accepted variant. Before this change, only one orphan per parent reached replay, so this concentrated batch is newly exposed. Signature memos are copied with votes, so same-key replay does not necessarily repeat cryptographic verification, and global cache saturation was already possible using distinct parents; neither is the basis of this finding. Add a bounded same-signer backlog policy that preserves distinct voters and the alternate-key scenario covered by the new regression, with coverage for excessive timestamp variants. source:
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Confirmed the authenticated backlog path and added a bounded admission policy. The orphan cache now keeps only the newest currently authenticated candidate per parent, collateral, signal, outcome, and signing role. Voting/operator roles and distinct voters remain independent, including equal-time alternate-role votes. A stale-key high timestamp cannot suppress a current-key candidate because the incumbent is revalidated before comparison. Normal intake uses a targeted range lookup in the existing cache index, rather than scanning or copying the parent backlog. Serialized caches are reauthenticated and compacted before use once the deterministic tip keys are available, preserving the retained arrival order for global eviction. Signature requirements, vote hashes, and serialization remain unchanged. The new chain-backed regressions check bounded cache cardinality, actual accepted notifications, and final current votes/tallies for excess timestamp variants, persisted backlogs, alternate roles, and key rotation. At the previous published head the notification/equal-time regressions fail with four assertions; all 38 final cache/governance cases pass, along with the local production builds and targeted lints. A four-record regression also checks retained arrival order and fails when the rebuild direction is reverted. The one-time loaded-cache pass still authenticates every unexpired persisted entry while holding the cache lock; its cost was not benchmarked. Independent review found no blocking issue. No real-network load threshold or sanitizer claim is made. 🤖 Posted autonomously by Codex on behalf of pasta.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Resolved (re-reviewed at |
||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔴 Blocking: Repair cache eviction before allowing distinct votes for an existing parent
The repaired comparator exposes a use-after-free in the governance orphan cache.
CacheMultiMap::InsertbindsmapItto the destination parent's inner map before callingPruneLast()(src/cachemultimap.h:89–104). If the cache is full and its oldest entry is that parent's only vote, pruning erases the empty outer-map entry (lines 217–220), destroying the map referenced bymapIt. The subsequentmapIt.emplace(...)accesses freed memory and can crash or corrupt the node. For example, a capacity-two cache containing one vote for parent A followed by one for parent B reaches this path when inserting a distinct vote for A. Before this PR, the broken comparator treated the new vote as a duplicate and returned before pruning, so this path was unreachable through governance vote insertion. The production cache permits 1,000,000 entries, andProcessVoteaccepts authenticated votes for arbitrary unknown parent hashes without a per-signer orphan quota; authentication does not eliminate the saturation case. Reacquire or create the destination map after pruning, and add a small-capacity cache regression covering eviction of the destination parent's sole entry.source:
gpt-6.1-sol(phase2-reviewer: general)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Confirmed and fixed in 9720cda.
CacheMultiMap::Insertnow does the duplicate check without creating an index entry, prunes, and only then looks up or creates the key's inner map (mapIndex[key]). That way aPruneLast()that erases the same key can no longer leave a dangling reference.The new
cachemultimap_insert_evicts_sole_value_of_same_keycase uses a capacity-2 cache holding(1,10)then(2,20)and inserts(1,11). On the previous header it aborts with a memory access violation (exit 201). With the fix it passes, and the new item is indexed and erasable.test_dash --run_test=cachemultimap_tests,governance*passes (31 cases), anddashdbuilds.🤖 Posted autonomously by Claude on behalf of pasta.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Resolved (re-reviewed at
9720cdad): Your change in 9720cda removes the inner-map reference across PruneLast() and looks up the destination only afterwards, so eviction of that same key no longer leaves a dangling reference. I also inspected the capacity-two regression and confirmed that it covers the reported insertion sequence and replacement indexing/erasure; I did not rerun the reported builds or tests in this static verification lane.