Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 12 additions & 16 deletions src/governance/governance.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ GovernanceStore::GovernanceStore() :
mapObjects(),
mapErasedGovernanceObjects(),
cmapInvalidVotes(MAX_CACHE_SIZE),
cmmapOrphanVotes(MAX_CACHE_SIZE),
cmmapOrphanVotes(MAX_ORPHAN_VOTES),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Reapply the orphan-cache limit after deserialization

On upgrades that load an existing governance.dat, this constructor limit is overwritten when CacheMultiMap::Unserialize restores its serialized nMaxSize. Because the serialization version remains CGovernanceManager-Version-16, existing files contain the old 1,000,000-entry limit, so nearly every upgraded node continues accepting that many orphan votes despite this change. Enforce MAX_ORPHAN_VOTES after loading, including pruning any excess retained entries, rather than relying only on the constructor.

AGENTS.md reference: AGENTS.md:L166-L175

Useful? React with 👍 / 👎.

mapLastMasternodeObject(),
lastMNListForVotingKeys(std::make_shared<CDeterministicMNList>())
{
Expand Down Expand Up @@ -407,6 +407,11 @@ void CGovernanceManager::CheckAndRemove()

ScopedLockBool guard(cs_store, fRateChecksEnabled, false);

// Drop orphan votes whose parent never arrived. Votes for an object that did arrive are
// consumed by CheckOrphanVotes() at that point, so anything still here is either waiting or
// dead; this is the only thing that removes the latter.
ExpireOrphanVotes();

// Clean up any expired or invalid triggers
m_superblocks.Clean(nCachedBlockHeight);

Expand Down Expand Up @@ -1087,13 +1092,11 @@ void CGovernanceManager::UpdatedBlockTip(const CBlockIndex* pindex)
m_superblocks.ExecuteBestSuperblock(m_dmnman.GetListAtChainTip(), pindex->nHeight);
}

std::vector<uint256> CGovernanceManager::GetOrphanVoteObjectHashes()
void CGovernanceManager::ExpireOrphanVotes()
{
LOCK(cs_store);
AssertLockHeld(cs_store);

const auto now{Now<NodeSeconds>()};

// Clean up expired orphan votes
const vote_cmm_t::list_t& items = cmmapOrphanVotes.GetItemList();
for (auto it = items.begin(); it != items.end();) {
auto prevIt = it;
Expand All @@ -1102,18 +1105,11 @@ std::vector<uint256> CGovernanceManager::GetOrphanVoteObjectHashes()
cmmapOrphanVotes.Erase(prevIt->key, prevIt->value);
}
}
}

// Get hashes of objects we don't have yet
std::vector<uint256> vecHashesFiltered;
std::vector<uint256> vecHashes;
cmmapOrphanVotes.GetKeys(vecHashes);
for (const uint256& nHash : vecHashes) {
if (mapObjects.find(nHash) == mapObjects.end()) {
vecHashesFiltered.push_back(nHash);
}
}

return vecHashesFiltered;
size_t CGovernanceManager::GetOrphanVoteCount() const
{
return WITH_LOCK(cs_store, return cmmapOrphanVotes.GetSize());
}

void CGovernanceManager::RemoveInvalidVotes()
Expand Down
23 changes: 21 additions & 2 deletions src/governance/governance.h
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,13 @@ class GovernanceStore
using txout_m_t = std::map<COutPoint, last_object_rec>;
using vote_cmm_t = CacheMultiMap<uint256, governance::OrphanVote>;

public:
/** Bound for the orphan-vote cache, which is filled from the network by any peer with a parent
* object we do not have. Orphans are short-lived recovery state for votes that outran their
* object during relay, so this only has to cover objects genuinely in flight, not the whole
* governance set. MAX_CACHE_SIZE would allow ~750 MB of peer-supplied data here. */
static constexpr int MAX_ORPHAN_VOTES = 1000;

protected:
static constexpr int MAX_CACHE_SIZE = 1000000;
static const std::string SERIALIZATION_VERSION_STRING;
Expand Down Expand Up @@ -232,6 +239,14 @@ class GovernanceStore
>> mapObjects
>> mapLastMasternodeObject
>> *lastMNListForVotingKeys;

// CacheMultiMap serializes its own capacity, so a file written before MAX_ORPHAN_VOTES
// existed restores the old one and the bound would apply to fresh nodes only. Orphan votes
// are a ten-minute recovery window that the restart has already invalidated, so drop what
// was read and reassert the bound; the field stays in the stream to keep the on-disk format
// unchanged. Clear() does not touch the capacity.
cmmapOrphanVotes.Clear();
cmmapOrphanVotes.SetMaxSize(MAX_ORPHAN_VOTES);
}

void Clear()
Expand Down Expand Up @@ -363,8 +378,8 @@ class CGovernanceManager : public GovernanceStore
// Used by NetGovernance
std::vector<CInv> FetchRelayInventory() EXCLUSIVE_LOCKS_REQUIRED(!cs_relay);
void CheckAndRemove() EXCLUSIVE_LOCKS_REQUIRED(!cs_store);
/** Get hashes of governance objects for which we have orphan votes. Also cleans up expired orphans. */
[[nodiscard]] std::vector<uint256> GetOrphanVoteObjectHashes() EXCLUSIVE_LOCKS_REQUIRED(!cs_store);
/** Number of orphan votes currently held, so the MAX_ORPHAN_VOTES bound can be asserted. */
[[nodiscard]] size_t GetOrphanVoteCount() const EXCLUSIVE_LOCKS_REQUIRED(!cs_store);
std::pair<std::vector<uint256>, std::vector<uint256>> FetchGovernanceObjectVotes(
size_t peers_per_hash_max, int64_t now, std::map<uint256, std::map<CService, int64_t>>& map_asked_recently) const
EXCLUSIVE_LOCKS_REQUIRED(!cs_store);
Expand Down Expand Up @@ -415,6 +430,10 @@ class CGovernanceManager : public GovernanceStore
void CheckOrphanVotes(CGovernanceObject& govobj)
EXCLUSIVE_LOCKS_REQUIRED(cs_store, !cs_relay);

/** Drop orphan votes whose parent object never arrived within GOVERNANCE_ORPHAN_EXPIRATION_TIME. */
void ExpireOrphanVotes()
EXCLUSIVE_LOCKS_REQUIRED(cs_store);

void RebuildIndexes()
EXCLUSIVE_LOCKS_REQUIRED(cs_store);

Expand Down
30 changes: 9 additions & 21 deletions src/governance/net_governance.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -45,23 +45,9 @@ void NetGovernance::Schedule(CScheduler& scheduler)
[this]() -> void {
if (!m_node_sync.IsSynced()) return;

// Request governance objects for orphan votes
auto vecOrphanHashes = m_gov_manager.GetOrphanVoteObjectHashes();
if (!vecOrphanHashes.empty()) {
LogPrint(BCLog::GOBJECT, "NetGovernance::Schedule -- requesting %d orphan objects\n",
vecOrphanHashes.size());
const CConnman::NodesSnapshot snap{m_connman, CConnman::FullyConnectedOnly};
for (const uint256& nHash : vecOrphanHashes) {
for (CNode* pnode : snap.Nodes()) {
if (!pnode->CanRelay()) continue;
CNetMsgMaker msgMaker(pnode->GetCommonVersion());
CBloomFilter filter; // Empty filter - we want the object, not votes
m_connman.PushMessage(pnode, msgMaker.Make(NetMsgType::MNGOVERNANCESYNC, nHash, filter));
}
}
}

// CHECK AND REMOVE - REPROCESS GOVERNANCE OBJECTS
// Also expires orphan votes whose parent object never arrived. Fetching those parents
// is driven by the object request tracker from ProcessMessage(), not from here.
m_gov_manager.CheckAndRemove();
},
std::chrono::minutes{5});
Expand Down Expand Up @@ -262,11 +248,13 @@ void NetGovernance::ProcessMessage(CNode& peer, const std::string& msg_type, CDa
// m_peer_manager->PeerRelayInv(CInv{MSG_GOVERNANCE_OBJECT_VOTE, nHash});
} else {
LogPrint(BCLog::GOBJECT, "MNGOVERNANCEOBJECTVOTE -- Rejected vote, error = %s\n", exception.what());
if (hashToRequest != uint256()) {
// Orphan vote - request the missing governance object
CNetMsgMaker msgMaker(peer.GetCommonVersion());
CBloomFilter filter; // Empty filter - we just want the object, not votes
m_connman.PushMessage(&peer, msgMaker.Make(NetMsgType::MNGOVERNANCESYNC, hashToRequest, filter));
if (!hashToRequest.IsNull()) {

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 Re-seed cached orphan parents from later relays

When the same orphan vote is later received from a second peer—particularly after the first peer's parent request timed out—ProcessVote rejects the duplicate cache insertion and leaves hashToRequest null, so this condition skips PeerAskPeersForObject and never registers the second peer as a fallback. Because this change also removes the periodic all-peer orphan sweep, the parent can remain unavailable until an unrelated object announcement or full governance resync, despite the second peer providing the same evidence that motivated preferring the first peer. Return the cached orphan's parent for later relays, or otherwise register each relaying peer while the orphan remains pending.

AGENTS.md reference: AGENTS.md:L165-L175

Useful? React with 👍 / 👎.

// Orphan vote: fetch the parent object through the request tracker, which owns
// GETDATA scheduling, per-peer in-flight limits, expiry and fallback to another
// peer. Ask this peer first -- holding a vote for the object is evidence it has
// the object, and it may never have announced the object to us.
m_peer_manager->PeerAskPeersForObject(CInv{MSG_GOVERNANCE_OBJECT, hashToRequest},
peer.GetId());
}
if ((exception.GetNodePenalty() != 0) && m_node_sync.IsSynced()) {
m_peer_manager->PeerMisbehaving(peer.GetId(), exception.GetNodePenalty());
Expand Down
2 changes: 1 addition & 1 deletion src/instantsend/net_instantsend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -414,7 +414,7 @@ void NetInstantSend::ProcessInstantSendLock(NodeId from, const uint256& hash, co
m_peer_manager->PeerRelayInvFiltered(inv, *tx);
} else {
m_peer_manager->PeerRelayInvFiltered(inv, islock->txid);
m_peer_manager->PeerAskPeersForTransaction(islock->txid);
m_peer_manager->PeerAskPeersForObject(CInv{MSG_TX, islock->txid});
}
}

Expand Down
64 changes: 45 additions & 19 deletions src/net_processing.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ using node::fReindex;
/** Maximum number of in-flight object requests from a peer. It is not a hard limit, but the
* threshold at which point the OVERLOADED_PEER_OBJECT_DELAY kicks in. */
static constexpr int32_t MAX_PEER_OBJECT_REQUEST_IN_FLIGHT = 100;
/** How many peers to ask for an object we want but were never offered (see AskPeersForObject).
* Small on purpose: the request tracker retries and falls back to the next candidate on expiry, so
* this is the width of the initial attempt, not the number of chances to obtain the object. */
static constexpr size_t MAX_PEERS_TO_ASK_FOR_OBJECT = 4;
/** Maximum number of announced objects from a peer.
* Unlike Bitcoin, this is not reduced to 5000: governance vote sync legitimately announces up to
* MAX_INV_SZ objects from a single peer (see CGovernanceManager). */
Expand Down Expand Up @@ -644,15 +648,17 @@ class PeerManagerImpl final : public PeerManager
void PeerRelayDSQ(const CCoinJoinQueue& queue) override EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex);
void PeerRelayTransaction(const uint256& txid) override EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex);
void PeerRelayRecoveredSig(const llmq::CRecoveredSig& sig, bool proactive_relay) override EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex);
void PeerAskPeersForTransaction(const uint256& txid) override EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex);
void PeerAskPeersForObject(const CInv& inv, NodeId prefer_first) override
EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex, !cs_main);
size_t PeerGetRequestedObjectCount(NodeId nodeid) const override EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex, ::cs_main);
void PeerPostProcessMessage(MessageProcessingResult&& ret) override EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex);

private:
void _RelayTransaction(const uint256& txid) EXCLUSIVE_LOCKS_REQUIRED(cs_main, !m_peer_mutex);

/** Ask peers that have a transaction in their inventory to relay it to us. */
void AskPeersForTransaction(const uint256& txid) EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex);
/** Ask peers that have the object in their inventory to relay it to us, plus prefer_first. */
void AskPeersForObject(const CInv& inv, NodeId prefer_first)
EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex, !cs_main);

/** Relay inventories to peers that find it relevant */
void RelayInvFiltered(const CInv& inv, const CTransaction& relatedTx) EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex);
Expand Down Expand Up @@ -2387,43 +2393,63 @@ void PeerManagerImpl::SendPings()
for(auto& it : m_peer_map) it.second->m_ping_queued = true;
}

void PeerManagerImpl::AskPeersForTransaction(const uint256& txid)
void PeerManagerImpl::AskPeersForObject(const CInv& inv, NodeId prefer_first)
{
std::vector<PeerRef> peersToAsk;
peersToAsk.reserve(4);
peersToAsk.reserve(MAX_PEERS_TO_ASK_FOR_OBJECT);

{
READ_LOCK(m_peer_mutex);
// A peer that holds the object without having announced it is not in any inventory filter,
// so it can only be reached by being named. Ask it first: it is the one candidate we have
// positive evidence for.
if (prefer_first != -1) {
if (auto it = m_peer_map.find(prefer_first); it != m_peer_map.end()) {
peersToAsk.emplace_back(it->second);
}
}
// TODO consider prioritizing MNs again, once that flag is moved into Peer
for (const auto& [_, peer] : m_peer_map) {
if (peersToAsk.size() >= 4) {
if (peersToAsk.size() >= MAX_PEERS_TO_ASK_FOR_OBJECT) {
break;
}
if (IsInvInFilter(*peer, txid)) {
if (peer->m_id == prefer_first) {
continue;
}
if (IsInvInFilter(*peer, inv.hash)) {
peersToAsk.emplace_back(peer);
}
}
}
{
LOCK(cs_main);
const auto current_time{GetTime<std::chrono::microseconds>()};
// Register a fresh, preferred (undelayed) MSG_TX announcement from each peer we intend to
// ask, so the transaction is requested ASAP. We deliberately do not forget existing
// announcements for this txid: any live candidate/request from another peer must survive as
// a fallback, and there is nothing to "unstick" -- the tracker deletes a txid's COMPLETED
// announcements automatically once no live one remains, so a completed entry only lingers
// while some peer is still being tried. If a peer here already has an announcement,
// ReceivedInv is a no-op and the existing one (in flight or queued) keeps its place.
// Register a fresh, preferred (undelayed) announcement from each peer we intend to ask, so
// the object is requested ASAP. We deliberately do not forget existing announcements for
// this hash: any live candidate/request from another peer must survive as a fallback, and
// there is nothing to "unstick" -- the tracker deletes a hash's COMPLETED announcements
// automatically once no live one remains, so a completed entry only lingers while some peer
// is still being tried. If a peer here already has an announcement, ReceivedInv is a no-op
// and the existing one (in flight or queued) keeps its place.
for (PeerRef& peer : peersToAsk) {
// The peer may have been disconnected (and its tracker state wiped by DisconnectedPeer)
// after we collected it above but before we took cs_main. Registering an announcement
// for a gone peer would leave a candidate that is never requested and could block the
// live fallback peers, so skip it.
if (State(peer->m_id) == nullptr) continue;
LogPrintf("PeerManagerImpl::%s -- txid=%s: asking other peer %d for correct TX\n", __func__,
txid.ToString(), peer->m_id);
// Obey the same per-peer accounting AddObjectAnnouncement applies to announcements the
// peer sent us. A synthetic announcement is still an entry the peer's behaviour can
// cause us to create -- a peer that keeps naming objects we do not have would otherwise
// grow its tracker footprint without limit.
if (m_object_request.Count(peer->m_id) >= MAX_PEER_OBJECT_ANNOUNCEMENTS) continue;
const bool overloaded = m_object_request.CountInFlight(peer->m_id) >= MAX_PEER_OBJECT_REQUEST_IN_FLIGHT;
LogPrint(BCLog::NET, "PeerManagerImpl::%s -- %s: asking peer %d\n", __func__, inv.ToString(),
peer->m_id);

m_object_request.ReceivedInv(peer->m_id, CInv(MSG_TX, txid), /*preferred=*/true, current_time);
// Preferred and otherwise undelayed: unlike a peer-initiated announcement, we asked for
// this one and want it as soon as the peer's in-flight budget allows.
m_object_request.ReceivedInv(peer->m_id, inv, /*preferred=*/true,

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 Prioritize the preferred peer in the tracker

When another peer's inventory filter also contains this hash, every candidate is registered with preferred=true; TxRequestTracker then selects the candidate with the highest randomized priority, not the first inserted candidate. Thus prefer_first does not actually ask the orphan-vote relayer first, and a stale or malicious alternate announcement can delay the parent fetch by the 60-second governance-object request interval. Give the named peer higher tracker priority than the fallback candidates and cover the multi-candidate case in a focused test.

AGENTS.md reference: AGENTS.md:L165-L175

Useful? React with 👍 / 👎.

current_time + (overloaded ? OVERLOADED_PEER_OBJECT_DELAY : 0us));
}
}
}
Comment on lines +2396 to 2455

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect IsInvInFilter's implementation to determine whether it is
# scoped to the tx-specific known-inventory filter or generalizes across CInv types.
ast-grep run --pattern 'bool PeerManagerImpl::IsInvInFilter($$$) {
  $$$
}' --lang cpp src/net_processing.cpp

rg -n -B2 -A15 'bool PeerManagerImpl::IsInvInFilter' src/net_processing.cpp

Repository: dashpay/dash

Length of output: 761


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== declarations =="
rg -n -B3 -A5 'IsInvInFilter|AskPeersForObject|PeerAskPeersForObject|m_object_request.*RequestedInv|m_object_request.*ReceivedInv|m_tx_inventory_known_filter|TxRelay' src/net_processing.cpp src/net_processing.h src/net.h src/net.h-common.h 2>/dev/null | head -n 240

echo
echo "== call sites with surrounding context =="
rg -n -B6 -A8 'AskPeersForObject\(|PeerAskPeersForObject\(' src test -g '*.cpp' -g '*.h' -g '*.c' -g '*.cc' | head -n 240

echo
echo "== CInv enum usage relevant to object inventory =="
rg -n 'MSG_[A-Z_]+|SendTransaction|PushInventory|Inventory|TxFrom|TxTo|m_tx_inventory_known_filter\.insert|\.add\(' src/net_processing.cpp src/validation.cpp src/policy | sed -n '1,220p'

Repository: dashpay/dash

Length of output: 15683


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== declaration and relevant declarations =="
rg -n -B4 -A8 'IsInvInFilter|AskPeersForObject|PeerAskPeersForObject|m_object_request|TxRelay|m_tx_inventory_known_filter' src/net_processing.cpp src/net_processing.h src/net.h src/net.h-common.h 2>/dev/null | sed -n '1,260p'

Repository: dashpay/dash

Length of output: 19362


Scope AskPeersForObject candidate discovery or extend it beyond transactions.

IsInvInFilter() checks Peer::TxRelay::m_tx_inventory_known_filter, while PushInv() only inserts non-tx inventories (governance/IS/etc.) into vInventoryOtherToSend. For non-tx CInv types, the fallback candidate loop can only discover prefer_first, so the public contract ("Candidates are peers known to have the hash") does not hold. Restrict the docstring for non-tx invs without prefer_first, or track non-tx inventory similarly and include it here.

🤖 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/net_processing.cpp` around lines 2396 - 2446, Update AskPeersForObject’s
candidate discovery to cover non-transaction CInv types as well, since
IsInvInFilter only reflects transaction inventory knowledge. Track or otherwise
consult peers’ known non-transaction inventory (including entries populated by
PushInv) when building peersToAsk, while preserving prefer_first prioritization
and the existing request limits.

Expand Down Expand Up @@ -6782,9 +6808,9 @@ void PeerManagerImpl::PeerRelayTransaction(const uint256& txid)
RelayTransaction(txid);
}

void PeerManagerImpl::PeerAskPeersForTransaction(const uint256& txid)
void PeerManagerImpl::PeerAskPeersForObject(const CInv& inv, NodeId prefer_first)
{
AskPeersForTransaction(txid);
AskPeersForObject(inv, prefer_first);
}

size_t PeerManagerImpl::PeerGetRequestedObjectCount(NodeId nodeid) const
Expand Down
13 changes: 12 additions & 1 deletion src/net_processing.h
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,18 @@ class PeerManagerInternal
virtual void PeerRelayTransaction(const uint256& txid) = 0;
virtual void PeerRelayDSQ(const CCoinJoinQueue& queue) = 0;
virtual void PeerRelayRecoveredSig(const llmq::CRecoveredSig& sig, bool proactive_relay) = 0;
virtual void PeerAskPeersForTransaction(const uint256& txid) = 0;
/** Ask a few peers for an object we want but have not been offered, by registering a synthetic
* announcement with the request tracker. The tracker then owns the fetch: GETDATA scheduling,
* per-peer in-flight limits, expiry, and fallback to the next candidate.
*
* Candidates are prefer_first, if set, plus peers whose known-inventory filter already contains
* the hash. That filter is only consulted for peers that enabled transaction relay, so for an
* object type carried outside transaction relay -- and for any object nobody has announced to
* us -- prefer_first may be the only candidate. Pass it whenever a specific peer demonstrably
* has the object without having announced it, such as one that sent a vote naming this parent.
*
* Requires ::cs_main is NOT held. */
virtual void PeerAskPeersForObject(const CInv& inv, NodeId prefer_first = -1) = 0;
virtual size_t PeerGetRequestedObjectCount(NodeId nodeid) const = 0;
virtual void PeerPostProcessMessage(MessageProcessingResult&& ret) = 0;
};
Expand Down
Loading
Loading