Skip to content

fix: stop unvalidated governance orphan-vote amplification - #7517

Closed
PastaPastaPasta wants to merge 1 commit into
dashpay:developfrom
PastaPastaPasta:sec/u006
Closed

fix: stop unvalidated governance orphan-vote amplification#7517
PastaPastaPasta wants to merge 1 commit into
dashpay:developfrom
PastaPastaPasta:sec/u006

Conversation

@PastaPastaPasta

Copy link
Copy Markdown
Member

Issue being fixed or feature implemented

CGovernanceManager::ProcessVote() inserted a vote into cmmapOrphanVotes as soon as mapObjects.find(vote.GetParentHash()) missed - before any masternode-membership or signature check, and with a zero misbehaviour penalty. The cache is sized MAX_CACHE_SIZE = 1'000'000 and keyed by the attacker-chosen parent hash, and CacheMultiMap stores each vote twice, so a peer can park roughly 600 MB of unvalidated data there.

The larger problem is fan-out. GetOrphanVoteObjectHashes() returned every orphan key uncapped, and NetGovernance::Schedule() sends one MNGOVERNANCESYNC per key per connected peer every 5 minutes, ignoring fPauseSend. The orphan map was also serialised into governance.dat, so a flood survived restart and re-drove the fan-out on boot.

This is reachable by any unauthenticated P2P peer; the only gate is the standard announce-then-request tracker. No masternode, quorum membership or RPC access is required.

What was done?

  • Require a tip-list masternode and a valid voting-or-operator signature before a vote may enter the orphan cache. UpdateHash()/GetSignatureHash() cover nParentHash, so votes cannot be repointed at fresh parent hashes without masternode keys.
  • Bound the orphan cache with MAX_ORPHAN_VOTES = 1000.
  • Sample orphan-object requests randomly (100 per tick) instead of requesting every key, and skip peers with a paused send buffer.
  • Stop persisting orphan votes to governance.dat.

Note that the added signature verification now runs under cs_store; this is bounded by the new discouragement score on invalid votes.

How Has This Been Tested?

The first commit adds a regression test demonstrating that unvalidated votes fill the orphan cache, ordered before the fix.

Full build and test validation is delegated to CI on this PR; the changes were not built locally.

Breaking Changes

governance.dat is bumped to v17. Upgrading nodes will discard their existing governance cache and re-sync governance data on first start. No consensus or P2P protocol change.

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • 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

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@PastaPastaPasta, you've reached your PR review limit, so we couldn't start this review.

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 @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7dde798e-8ea0-421a-99c5-014831135885

📥 Commits

Reviewing files that changed from the base of the PR and between f1dde51 and 1f9db83.

📒 Files selected for processing (6)
  • src/Makefile.test.include
  • src/governance/governance.cpp
  • src/governance/governance.h
  • src/governance/net_governance.cpp
  • src/test/governance_inv_tests.cpp
  • src/test/governance_orphan_vote_tests.cpp

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@thepastaclaw

thepastaclaw commented Aug 2, 2026

Copy link
Copy Markdown

⛔ Blockers found — Sonnet deferred (commit 1f9db83)
Canonical validated blockers: 1

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3dfcfc8403

ℹ️ 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".

Comment on lines +1179 to +1180
Shuffle(vecHashesFiltered.begin(), vecHashesFiltered.end(), FastRandomContext());
vecHashesFiltered.resize(MAX_ORPHAN_OBJECT_REQUESTS_PER_TICK);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Retry retained orphans before they expire

When more than 100 signed orphan parents are cached and the immediate request to the announcing peer fails, random sampling does not prevent starvation: Schedule() runs every 5 minutes while each orphan expires after 10 minutes, so a cache of 1,000 gives each parent only one or two 10% chances of being retried through other peers before removal. Thus roughly 81–90% of those parents may never be requested again, whereas the previous scheduler retried every retained orphan; use rotating batches or otherwise ensure coverage within the expiration window.

AGENTS.md reference: AGENTS.md:L157-L175

Useful? React with 👍 / 👎.

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Preliminary review — Codex only

The orphan-cache bounds and pre-insertion validation address the original unauthenticated cache-filling vector, but cached orphan votes are not recognized before the newly added cryptographic checks. A peer can therefore replay a valid orphan vote and repeatedly force ECDSA/BLS verification while holding the governance-store lock; the corrective follow-up commit should also be folded into the implementation commit for a hygienic history.

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)

1 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 `src/governance/governance.cpp`:
- [BLOCKING] src/governance/governance.cpp:860-861: Cached orphan replays repeatedly perform signature verification
  The known-vote checks at the start of `ProcessVote()` cover only `cmapVoteToObject` and `cmapInvalidVotes`; duplicate orphan detection does not occur until `cmmapOrphanVotes.Insert()` at line 882, after the ECDSA/BLS checks shown here. The inventory path has the same gap because `ConfirmInventoryRequest()` and `HaveVoteForHash()` do not consult cached orphan votes. After each payload, `PeerConsumeObjectRequest()` consumes the request-tracker entry, so the same peer can announce the hash again, receive another GETDATA, and resend the vote. Each replay then performs cryptographic verification while `cs_store` is held, receives no penalty, and is rejected only by the late duplicate insertion. The 1,000-entry cache bound does not limit this revalidation rate. Maintain a hash-index of cached orphan votes and treat those hashes as known before signature verification and inventory retrieval; add a regression test that replays an already cached valid orphan and confirms the signature-validation path is not entered again.

In `<commit:3dfcfc84037>`:
- [SUGGESTION] <commit:3dfcfc84037>:1: Squash the corrective review follow-up into the main fix
  Commit `3dfcfc84037` changes the same implementation introduced by `be6a351ed0e`: it removes the transient penalty for valid orphan relays, replaces biased fixed-prefix truncation with random sampling, and repairs the accompanying tests. `CONTRIBUTING.md` identifies commits that repeatedly change the same lines as fixup commits that may need squashing. Fold this follow-up into `be6a351ed0e` so the permanent implementation commit does not temporarily penalize honest relays or starve higher-sorting orphan parents.

Comment on lines +860 to +861
const bool sig_ok = vote.CheckSignature(dmn->pdmnState->keyIDVoting) ||
vote.CheckSignature(dmn->pdmnState->pubKeyOperator.Get());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Blocking: Cached orphan replays repeatedly perform signature verification

The known-vote checks at the start of ProcessVote() cover only cmapVoteToObject and cmapInvalidVotes; duplicate orphan detection does not occur until cmmapOrphanVotes.Insert() at line 882, after the ECDSA/BLS checks shown here. The inventory path has the same gap because ConfirmInventoryRequest() and HaveVoteForHash() do not consult cached orphan votes. After each payload, PeerConsumeObjectRequest() consumes the request-tracker entry, so the same peer can announce the hash again, receive another GETDATA, and resend the vote. Each replay then performs cryptographic verification while cs_store is held, receives no penalty, and is rejected only by the late duplicate insertion. The 1,000-entry cache bound does not limit this revalidation rate. Maintain a hash-index of cached orphan votes and treat those hashes as known before signature verification and inventory retrieval; add a regression test that replays an already cached valid orphan and confirms the signature-validation path is not entered again.

source: ['codex']

ProcessVote inserted a vote into cmmapOrphanVotes as soon as the parent object lookup missed, before any masternode-membership or signature check and with a zero misbehaviour penalty. The cache holds a million entries keyed by an attacker-chosen parent hash, so a peer could park hundreds of megabytes of unvalidated data. Worse, every orphan key produced one MNGOVERNANCESYNC per connected peer every 5 minutes, ignoring fPauseSend, and the map was persisted to governance.dat so a flood survived restart and re-drove the fan-out on boot. Any unauthenticated peer can do this.

Require a tip-list masternode and a valid voting-or-operator signature before a vote may enter the orphan cache; UpdateHash()/GetSignatureHash() cover nParentHash, so votes cannot be repointed at fresh parent hashes without masternode keys. Bound the cache, sample orphan-object requests instead of requesting every key, skip peers with a paused send buffer, and stop persisting orphan votes.

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Preliminary review — Codex only

Carried-forward prior findings: the cached-orphan replay blocker remains valid at the current head, while the commit-history squash suggestion is fixed by the rewritten single-commit stack. The existing retry-coverage suggestion also remains valid; the latest delta introduces no genuinely new findings.
Source: Codex general reviewer gpt-5.6-sol; Codex dash-core-commit-history reviewer gpt-5.6-sol; Codex verifier gpt-5.6-sol. The openclaw-agent coordinator is orchestration-only.

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)

1 carried-forward finding(s) already raised on this PR; not re-posting as new inline comments.

🤖 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/governance.cpp`:
- [SUGGESTION] src/governance/governance.cpp:1178-1180: Retry retained orphans before they expire
  `NetGovernance::Schedule()` first runs after five minutes and repeats every five minutes, while orphan votes expire after ten minutes. With 1,000 retained parent hashes and a random sample of 100, each parent typically gets only one or two 10% chances to enter the scheduled batch, leaving approximately 81–90% without any scheduled retry before expiration. The immediate request targets only the announcing peer, so if that peer does not provide the parent, most retained orphans can expire without querying another peer. Use coverage-tracked rotating batches, a shorter retry interval, a longer expiration period, or another bounded mechanism that covers retained parents before expiration.

Comment on lines +1178 to +1180
if (vecHashesFiltered.size() > MAX_ORPHAN_OBJECT_REQUESTS_PER_TICK) {
Shuffle(vecHashesFiltered.begin(), vecHashesFiltered.end(), FastRandomContext());
vecHashesFiltered.resize(MAX_ORPHAN_OBJECT_REQUESTS_PER_TICK);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: Retry retained orphans before they expire

NetGovernance::Schedule() first runs after five minutes and repeats every five minutes, while orphan votes expire after ten minutes. With 1,000 retained parent hashes and a random sample of 100, each parent typically gets only one or two 10% chances to enter the scheduled batch, leaving approximately 81–90% without any scheduled retry before expiration. The immediate request targets only the announcing peer, so if that peer does not provide the parent, most retained orphans can expire without querying another peer. Use coverage-tracked rotating batches, a shorter retry interval, a longer expiration period, or another bounded mechanism that covers retained parents before expiration.

source: ['codex']

@PastaPastaPasta

Copy link
Copy Markdown
Member Author

superseded by #7526 + #7527.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants