-
Notifications
You must be signed in to change notification settings - Fork 1.2k
fix: fetch orphan-vote parents via the request tracker instead of broadcasting #7526
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
f0a46cf
45467b7
7bf1403
44b9656
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 |
|---|---|---|
|
|
@@ -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}); | ||
|
|
@@ -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()) { | ||
|
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 the same orphan vote is later received from a second peer—particularly after the first peer's parent request timed out— 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()); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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). */ | ||
|
|
@@ -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); | ||
|
|
@@ -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, | ||
|
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 another peer's inventory filter also contains this hash, every candidate is registered with AGENTS.md reference: AGENTS.md:L165-L175 Useful? React with 👍 / 👎. |
||
| current_time + (overloaded ? OVERLOADED_PEER_OBJECT_DELAY : 0us)); | ||
| } | ||
| } | ||
| } | ||
|
Comment on lines
+2396
to
2455
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. 🎯 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.cppRepository: 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
🤖 Prompt for AI Agents |
||
|
|
@@ -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 | ||
|
|
||
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.
On upgrades that load an existing
governance.dat, this constructor limit is overwritten whenCacheMultiMap::Unserializerestores its serializednMaxSize. Because the serialization version remainsCGovernanceManager-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. EnforceMAX_ORPHAN_VOTESafter loading, including pruning any excess retained entries, rather than relying only on the constructor.AGENTS.md reference: AGENTS.md:L166-L175
Useful? React with 👍 / 👎.