-
Notifications
You must be signed in to change notification settings - Fork 1.2k
fix: stop unvalidated governance orphan-vote amplification #7517
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
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 |
|---|---|---|
|
|
@@ -19,14 +19,18 @@ | |
| #include <net.h> | ||
| #include <node/interface_ui.h> | ||
| #include <protocol.h> | ||
| #include <random.h> | ||
| #include <timedata.h> | ||
| #include <util/check.h> | ||
| #include <util/time.h> | ||
| #include <validationinterface.h> | ||
|
|
||
| #include <algorithm> | ||
| #include <ranges> | ||
|
|
||
| const std::string GovernanceStore::SERIALIZATION_VERSION_STRING = "CGovernanceManager-Version-16"; | ||
| // Version 17 drops cmmapOrphanVotes from the on-disk format so an unauthenticated | ||
| // orphan flood cannot survive restart. | ||
| const std::string GovernanceStore::SERIALIZATION_VERSION_STRING = "CGovernanceManager-Version-17"; | ||
|
|
||
| namespace { | ||
| constexpr std::chrono::seconds GOVERNANCE_DELETION_DELAY{10min}; | ||
|
|
@@ -64,7 +68,7 @@ GovernanceStore::GovernanceStore() : | |
| mapObjects(), | ||
| mapErasedGovernanceObjects(), | ||
| cmapInvalidVotes(MAX_CACHE_SIZE), | ||
| cmmapOrphanVotes(MAX_CACHE_SIZE), | ||
| cmmapOrphanVotes(MAX_ORPHAN_VOTES), | ||
| mapLastMasternodeObject(), | ||
| lastMNListForVotingKeys(std::make_shared<CDeterministicMNList>()) | ||
| { | ||
|
|
@@ -819,10 +823,61 @@ bool CGovernanceManager::ProcessVote(const CGovernanceVote& vote, CGovernanceExc | |
| return false; | ||
| } | ||
|
|
||
| const auto tip_mn_list = m_dmnman.GetListAtChainTip(); | ||
| auto it = mapObjects.find(nHashGovobj); | ||
| if (it == mapObjects.end()) { | ||
| // Validate before orphan caching so an unauthenticated peer cannot fill | ||
| // cmmapOrphanVotes with attacker-chosen parent hashes. | ||
| // Match the cheap structural checks in CGovernanceVote::IsValid, then | ||
| // require a tip-list masternode and a verifiable signature. | ||
| const auto max_time{std::chrono::time_point_cast<std::chrono::seconds>(GetAdjustedTime() + MAX_TIME_FUTURE_DEVIATION)}; | ||
| if (vote.Time() > max_time) { | ||
| std::string msg{strprintf("CGovernanceManager::%s -- vote is too far ahead of current time, hash = %s", | ||
| __func__, nHashVote.ToString())}; | ||
| LogPrint(BCLog::GOBJECT, "%s\n", msg); | ||
| exception = CGovernanceException(msg, GOVERNANCE_EXCEPTION_TEMPORARY_ERROR, 20); | ||
| return false; | ||
| } | ||
| if (vote.GetSignal() <= VOTE_SIGNAL_NONE || vote.GetSignal() >= VOTE_SIGNAL_UNKNOWN || | ||
| vote.GetOutcome() <= VOTE_OUTCOME_NONE || vote.GetOutcome() >= VOTE_OUTCOME_UNKNOWN) { | ||
| std::string msg{strprintf("CGovernanceManager::%s -- invalid vote signal/outcome, hash = %s", | ||
| __func__, nHashVote.ToString())}; | ||
| LogPrint(BCLog::GOBJECT, "%s\n", msg); | ||
| exception = CGovernanceException(msg, GOVERNANCE_EXCEPTION_PERMANENT_ERROR, 20); | ||
| return false; | ||
| } | ||
| auto dmn = tip_mn_list.GetMNByCollateral(vote.GetMasternodeOutpoint()); | ||
| if (!dmn) { | ||
| std::string msg{strprintf("CGovernanceManager::%s -- Unknown Masternode - %s, governance object hash = %s", | ||
| __func__, vote.GetMasternodeOutpoint().ToStringShort(), nHashGovobj.ToString())}; | ||
| LogPrint(BCLog::GOBJECT, "%s\n", msg); | ||
| // Match CGovernanceObject::ProcessVote scoring so repeated injection is banned. | ||
| exception = CGovernanceException(msg, GOVERNANCE_EXCEPTION_PERMANENT_ERROR, 20); | ||
| return false; | ||
| } | ||
| // FUNDING votes on proposals may use the voting key; everything else uses | ||
| // the operator BLS key. Parent type is unknown here, so try both. | ||
| const bool sig_ok = vote.CheckSignature(dmn->pdmnState->keyIDVoting) || | ||
| vote.CheckSignature(dmn->pdmnState->pubKeyOperator.Get()); | ||
| if (!sig_ok) { | ||
| std::string msg{strprintf("CGovernanceManager::%s -- Invalid vote signature, MN outpoint = %s, " | ||
| "governance object hash = %s, vote hash = %s", | ||
| __func__, vote.GetMasternodeOutpoint().ToStringShort(), nHashGovobj.ToString(), nHashVote.ToString())}; | ||
| LogPrint(BCLog::GOBJECT, "%s\n", msg); | ||
| exception = CGovernanceException(msg, GOVERNANCE_EXCEPTION_PERMANENT_ERROR, 20); | ||
| return false; | ||
| } | ||
|
|
||
| std::string msg{strprintf("CGovernanceManager::%s -- Unknown parent object %s, MN outpoint = %s", __func__, | ||
| nHashGovobj.ToString(), vote.GetMasternodeOutpoint().ToStringShort())}; | ||
| // Keep the penalty at zero. Reaching this point means the vote carries a valid | ||
| // signature from a tip-list masternode, so the only remaining reason we cannot | ||
| // apply it is that the parent object has not arrived yet - a benign relay race | ||
| // that happens routinely during governance sync. The peer that forwarded it is | ||
| // typically an honest relay, and misbehavior scores never decay, so scoring here | ||
| // would discourage honest peers after DISCOURAGEMENT_THRESHOLD/penalty races. | ||
| // The flood is bounded by the validation gate above plus MAX_ORPHAN_VOTES, not | ||
| // by ban scoring. | ||
| exception = CGovernanceException(msg, GOVERNANCE_EXCEPTION_WARNING); | ||
| if (cmmapOrphanVotes.Insert(nHashGovobj, governance::OrphanVote{vote, Now<NodeSeconds>() + GOVERNANCE_ORPHAN_EXPIRATION_TIME})) { | ||
| hashToRequest = nHashGovobj; // Caller should request this object | ||
|
|
@@ -839,7 +894,7 @@ bool CGovernanceManager::ProcessVote(const CGovernanceVote& vote, CGovernanceExc | |
| return false; | ||
| } | ||
|
|
||
| bool fOk = govobj.ProcessVote(m_mn_metaman, fRateChecksEnabled, m_dmnman.GetListAtChainTip(), vote, exception); | ||
| bool fOk = govobj.ProcessVote(m_mn_metaman, fRateChecksEnabled, tip_mn_list, vote, exception); | ||
| if (fOk) { | ||
| fOk = cmapVoteToObject.Insert(nHashVote, it->second); | ||
| } else if (exception.GetType() == GOVERNANCE_EXCEPTION_PERMANENT_ERROR && exception.GetNodePenalty() == 20) { | ||
|
|
@@ -1103,7 +1158,9 @@ std::vector<uint256> CGovernanceManager::GetOrphanVoteObjectHashes() | |
| } | ||
| } | ||
|
|
||
| // Get hashes of objects we don't have yet | ||
| // Get hashes of objects we don't have yet, capped so the scheduler fan-out | ||
| // stays O(MAX_ORPHAN_OBJECT_REQUESTS_PER_TICK × peers) rather than | ||
| // O(orphans × peers). | ||
| std::vector<uint256> vecHashesFiltered; | ||
| std::vector<uint256> vecHashes; | ||
| cmmapOrphanVotes.GetKeys(vecHashes); | ||
|
|
@@ -1113,6 +1170,16 @@ std::vector<uint256> CGovernanceManager::GetOrphanVoteObjectHashes() | |
| } | ||
| } | ||
|
|
||
| // Sample randomly rather than truncating: GetKeys() returns ascending uint256 | ||
| // order, so a fixed prefix would request the same numerically-lowest hashes | ||
| // every tick and starve every other orphan until it expires — a parent whose | ||
| // hash sorts high would never be requested at all. Matches the shuffle used | ||
| // for the governance object-vote sync in CSyncManager::RequestGovernanceData(). | ||
| if (vecHashesFiltered.size() > MAX_ORPHAN_OBJECT_REQUESTS_PER_TICK) { | ||
| Shuffle(vecHashesFiltered.begin(), vecHashesFiltered.end(), FastRandomContext()); | ||
| vecHashesFiltered.resize(MAX_ORPHAN_OBJECT_REQUESTS_PER_TICK); | ||
|
Comment on lines
+1179
to
+1180
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.
When more than 100 signed orphan parents are cached and the immediate request to the announcing peer fails, random sampling does not prevent starvation: AGENTS.md reference: AGENTS.md:L157-L175 Useful? React with 👍 / 👎.
Comment on lines
+1178
to
+1180
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: Retry retained orphans before they expire
source: ['codex'] |
||
| } | ||
|
|
||
| return vecHashesFiltered; | ||
| } | ||
|
|
||
|
|
||
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: Cached orphan replays repeatedly perform signature verification
The known-vote checks at the start of
ProcessVote()cover onlycmapVoteToObjectandcmapInvalidVotes; duplicate orphan detection does not occur untilcmmapOrphanVotes.Insert()at line 882, after the ECDSA/BLS checks shown here. The inventory path has the same gap becauseConfirmInventoryRequest()andHaveVoteForHash()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 whilecs_storeis 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']