-
Notifications
You must be signed in to change notification settings - Fork 1.2k
fix: require a valid masternode signature before caching an orphan governance vote #7527
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 |
|---|---|---|
|
|
@@ -801,6 +801,8 @@ bool CGovernanceManager::ProcessVote(const CGovernanceVote& vote, CGovernanceExc | |
| AssertLockNotHeld(cs_store); | ||
| hashToRequest = uint256{}; | ||
|
|
||
| const auto tip_mn_list{m_dmnman.GetListAtChainTip()}; | ||
|
|
||
| LOCK(cs_store); | ||
| uint256 nHashVote = vote.GetHash(); | ||
| uint256 nHashGovobj = vote.GetParentHash(); | ||
|
|
@@ -821,8 +823,19 @@ bool CGovernanceManager::ProcessVote(const CGovernanceVote& vote, CGovernanceExc | |
|
|
||
| auto it = mapObjects.find(nHashGovobj); | ||
| if (it == mapObjects.end()) { | ||
| // The parent object is unknown, so the vote signal cannot be mapped to a key type the way | ||
| // CGovernanceObject::ProcessVote does it (see onlyVotingKeyAllowed there). Accept either key. | ||
| if (!vote.IsValid(tip_mn_list, /*useVotingKey=*/true) && !vote.IsValid(tip_mn_list, /*useVotingKey=*/false)) { | ||
|
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.
For orphan votes using AGENTS.md reference: AGENTS.md:L159-L166 Useful? React with 👍 / 👎. 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 a peer announces an orphan vote containing a real public masternode outpoint but a garbage signature, the voting-key check fails and the second call reaches AGENTS.md reference: AGENTS.md:L159-L166 Useful? React with 👍 / 👎. |
||
| std::string msg{strprintf("CGovernanceManager::%s -- Invalid vote for unknown parent object %s, MN outpoint = %s, vote hash = %s", | ||
| __func__, nHashGovobj.ToString(), vote.GetMasternodeOutpoint().ToStringShort(), 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())}; | ||
| // No penalty: the vote is signed by a masternode, it just arrived before its parent object, | ||
| // which routinely happens during governance sync. Misbehaviour scores never decay. | ||
| 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 +852,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) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,7 @@ | |
| #include <governance/governance.h> | ||
| #include <governance/net_governance.h> | ||
| #include <governance/object.h> | ||
| #include <masternode/meta.h> | ||
| #include <masternode/sync.h> | ||
| #include <net.h> | ||
| #include <net_processing.h> | ||
|
|
@@ -394,7 +395,12 @@ BOOST_AUTO_TEST_CASE(governance_votes_require_peer_announcement_or_request) | |
| m_node.peerman->InitializeNode(*second_announcing_peer, NODE_NETWORK); | ||
| m_node.peerman->InitializeNode(*unsolicited_peer, NODE_NETWORK); | ||
|
|
||
| auto& connman = static_cast<ConnmanTestMsg&>(*m_node.connman); | ||
| // The vote below carries an unknown masternode outpoint, so CGovernanceManager::ProcessVote | ||
| // rejects it with a penalty of 20. Only a peer that passes the announce-then-request gate | ||
| // reaches ProcessVote at all, which makes the score the observable for the gate itself. | ||
| // Penalties are applied only once fully synced. | ||
| m_node.mn_sync->SwitchToNextAsset(); | ||
| BOOST_REQUIRE(m_node.mn_sync->IsSynced()); | ||
|
|
||
| const CGovernanceVote vote{MakeGovernanceVote(uint256S("31"))}; | ||
| const CInv vote_inv{MSG_GOVERNANCE_OBJECT_VOTE, vote.GetHash()}; | ||
|
|
@@ -406,20 +412,17 @@ BOOST_AUTO_TEST_CASE(governance_votes_require_peer_announcement_or_request) | |
| ProcessInv(*m_node.peerman, *announcing_peer, vote_inv); | ||
| ProcessInv(*m_node.peerman, *second_announcing_peer, vote_inv); | ||
|
|
||
| connman.FlushSendBuffer(*announcing_peer); | ||
| ProcessGovernanceVote(net_gov, *announcing_peer, vote); | ||
| BOOST_CHECK_EQUAL(CountQueuedMessages(*announcing_peer, NetMsgType::MNGOVERNANCESYNC), 1U); | ||
| AssertMisbehaviorScore(*m_node.peerman, *announcing_peer, 0); | ||
| BOOST_CHECK( | ||
| !WITH_LOCK(::cs_main, return m_node.peerman->PeerConsumeObjectRequest(announcing_peer->GetId(), vote_inv))); | ||
| AssertMisbehaviorScore(*m_node.peerman, *announcing_peer, 20); | ||
|
|
||
| connman.FlushSendBuffer(*second_announcing_peer); | ||
| ProcessGovernanceVote(net_gov, *second_announcing_peer, vote); | ||
| // Second announcer: the gate accepts (it independently announced the vote) and consumes | ||
| // its per-peer request entry. A rejected vote would return before the gate consumes and | ||
| // leave the entry intact, so this proves the accept path independently of the (deduped) | ||
| // orphan-request side effect. | ||
| // Consumption is per-peer: the second announcer's own entry is accepted and consumed, | ||
| // independent of the first peer's already-consumed entry. | ||
| BOOST_CHECK(!WITH_LOCK(::cs_main, | ||
| return m_node.peerman->PeerConsumeObjectRequest(second_announcing_peer->GetId(), vote_inv))); | ||
| AssertMisbehaviorScore(*m_node.peerman, *second_announcing_peer, 0); | ||
| AssertMisbehaviorScore(*m_node.peerman, *second_announcing_peer, 20); | ||
|
|
||
| m_node.peerman->FinalizeNode(*announcing_peer); | ||
| m_node.peerman->FinalizeNode(*second_announcing_peer); | ||
|
|
@@ -442,7 +445,6 @@ BOOST_AUTO_TEST_CASE(governance_vote_authorization_survives_unsynced_drop) | |
|
|
||
| auto peer{MakeGovernanceInvPeer(/*id=*/31)}; | ||
| m_node.peerman->InitializeNode(*peer, NODE_NETWORK); | ||
| auto& connman = static_cast<ConnmanTestMsg&>(*m_node.connman); | ||
|
|
||
| const CGovernanceVote vote{MakeGovernanceVote(uint256S("41"))}; | ||
| const CInv vote_inv{MSG_GOVERNANCE_OBJECT_VOTE, vote.GetHash()}; | ||
|
|
@@ -454,21 +456,98 @@ BOOST_AUTO_TEST_CASE(governance_vote_authorization_survives_unsynced_drop) | |
| // Not synced: delivering the vote is dropped at the sync gate and must NOT consume the request. | ||
| m_node.mn_sync->Reset(/*fForce=*/true, /*fNotifyReset=*/false); | ||
| BOOST_REQUIRE(!m_node.mn_sync->IsBlockchainSynced()); | ||
| connman.FlushSendBuffer(*peer); | ||
| ProcessGovernanceVote(net_gov, *peer, vote); | ||
| BOOST_CHECK_EQUAL(CountQueuedMessages(*peer, NetMsgType::MNGOVERNANCESYNC), 0U); | ||
| AssertMisbehaviorScore(*m_node.peerman, *peer, 0); | ||
|
|
||
| // Back in sync, the retransmit is still authorized and reaches ProcessVote, which rejects the | ||
| // unknown masternode outpoint with a penalty of 20. Had the unsynced drop consumed the request, | ||
| // the gate would now reject the vote as unrequested and return before ProcessVote, leaving the | ||
| // score at 0. | ||
| while (!m_node.mn_sync->IsSynced()) { | ||
| m_node.mn_sync->SwitchToNextAsset(); | ||
| } | ||
| ProcessGovernanceVote(net_gov, *peer, vote); | ||
| AssertMisbehaviorScore(*m_node.peerman, *peer, 20); | ||
|
|
||
| m_node.peerman->FinalizeNode(*peer); | ||
| chainstate.ResetIbd(); | ||
| } | ||
|
|
||
| // A vote whose parent object is unknown must prove masternode authorship before it is cached. | ||
| BOOST_AUTO_TEST_CASE(orphan_votes_require_a_valid_masternode_signature) | ||
| { | ||
| LOCK(NetEventsInterface::g_msgproc_mutex); | ||
|
|
||
| // Back in sync, the retransmit is still authorized: ProcessVote runs and (orphan parent) | ||
| // requests the missing object. Had the unsynced drop consumed the request, the gate would now | ||
| // reject the vote as unrequested and send no MNGOVERNANCESYNC. | ||
| TestChainState& chainstate = | ||
| *static_cast<TestChainState*>(&m_node.chainman->ActiveChainstate()); | ||
| chainstate.JumpOutOfIbd(); | ||
|
|
||
| // Penalties are applied only once fully synced. | ||
| m_node.mn_sync->SwitchToNextAsset(); | ||
| BOOST_REQUIRE(m_node.mn_sync->IsBlockchainSynced()); | ||
| BOOST_REQUIRE(m_node.mn_sync->IsSynced()); | ||
|
|
||
| NetGovernance net_gov(m_node.peerman.get(), *m_node.govman, *m_node.mn_sync, | ||
| *m_node.netfulfilledman, *m_node.connman); | ||
|
|
||
| auto peer{MakeGovernanceInvPeer(/*id=*/41)}; | ||
| m_node.peerman->InitializeNode(*peer, NODE_NETWORK); | ||
| auto& connman = static_cast<ConnmanTestMsg&>(*m_node.connman); | ||
|
|
||
| // The tip masternode list is empty in this setup, so no vote can name a known collateral. | ||
| const CGovernanceVote vote{MakeGovernanceVote(uint256S("51"))}; | ||
| const CInv vote_inv{MSG_GOVERNANCE_OBJECT_VOTE, vote.GetHash()}; | ||
|
|
||
| ProcessInv(*m_node.peerman, *peer, vote_inv); | ||
| connman.FlushSendBuffer(*peer); | ||
| ProcessGovernanceVote(net_gov, *peer, vote); | ||
| BOOST_CHECK_EQUAL(CountQueuedMessages(*peer, NetMsgType::MNGOVERNANCESYNC), 1U); | ||
|
|
||
| BOOST_CHECK(m_node.govman->GetOrphanVoteObjectHashes().empty()); | ||
| BOOST_CHECK_EQUAL(CountQueuedMessages(*peer, NetMsgType::MNGOVERNANCESYNC), 0U); | ||
| AssertMisbehaviorScore(*m_node.peerman, *peer, 20); | ||
|
Comment on lines
+496
to
+506
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: New orphan-gate signature check has no test exercising an actual signature
source: ['codex'] |
||
|
|
||
| m_node.peerman->FinalizeNode(*peer); | ||
| chainstate.ResetIbd(); | ||
| } | ||
|
|
||
| // The same unauthenticated vote must cost the sender the same whether or not its parent object | ||
| // happens to have arrived first. | ||
| BOOST_AUTO_TEST_CASE(invalid_vote_is_scored_alike_with_and_without_a_parent_object) | ||
| { | ||
| LOCK(NetEventsInterface::g_msgproc_mutex); | ||
|
|
||
| TestChainState& chainstate = | ||
| *static_cast<TestChainState*>(&m_node.chainman->ActiveChainstate()); | ||
| chainstate.JumpOutOfIbd(); | ||
|
|
||
| m_node.mn_sync->SwitchToNextAsset(); | ||
| BOOST_REQUIRE(m_node.mn_sync->IsSynced()); | ||
| // The known-object path runs CGovernanceObject::ProcessVote, which asserts metaman.IsValid(). | ||
| BOOST_REQUIRE(m_node.mn_metaman->LoadCache(/*load_cache=*/false)); | ||
|
|
||
| NetGovernance net_gov(m_node.peerman.get(), *m_node.govman, *m_node.mn_sync, | ||
| *m_node.netfulfilledman, *m_node.connman); | ||
|
|
||
| auto orphan_peer{MakeGovernanceInvPeer(/*id=*/51)}; | ||
| auto known_parent_peer{MakeGovernanceInvPeer(/*id=*/52)}; | ||
| m_node.peerman->InitializeNode(*orphan_peer, NODE_NETWORK); | ||
| m_node.peerman->InitializeNode(*known_parent_peer, NODE_NETWORK); | ||
|
|
||
| const CGovernanceObject govobj{MakeGovernanceObject(GetTime<std::chrono::seconds>().count(), uint256S("61"))}; | ||
| const CGovernanceVote orphan_vote{MakeGovernanceVote(uint256S("62"))}; | ||
| const CGovernanceVote known_parent_vote{MakeGovernanceVote(govobj.GetHash())}; | ||
|
|
||
| ProcessInv(*m_node.peerman, *orphan_peer, CInv{MSG_GOVERNANCE_OBJECT_VOTE, orphan_vote.GetHash()}); | ||
| ProcessGovernanceVote(net_gov, *orphan_peer, orphan_vote); | ||
| AssertMisbehaviorScore(*m_node.peerman, *orphan_peer, 20); | ||
|
|
||
| m_node.govman->AddGovernanceObjectForTesting(govobj); | ||
| ProcessInv(*m_node.peerman, *known_parent_peer, CInv{MSG_GOVERNANCE_OBJECT_VOTE, known_parent_vote.GetHash()}); | ||
| ProcessGovernanceVote(net_gov, *known_parent_peer, known_parent_vote); | ||
| AssertMisbehaviorScore(*m_node.peerman, *known_parent_peer, 20); | ||
|
|
||
| m_node.peerman->FinalizeNode(*orphan_peer); | ||
| m_node.peerman->FinalizeNode(*known_parent_peer); | ||
| chainstate.ResetIbd(); | ||
| } | ||
|
|
||
| BOOST_AUTO_TEST_SUITE_END() | ||
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.
🟡 Suggestion: Orphan-vote gate accepts the voting key for signals that always require the operator key
CGovernanceObject::ProcessVote (object.cpp:455) only allows the voting-key signature when
type == PROPOSAL && signal == VOTE_SIGNAL_FUNDING; for VALID, DELETE, and ENDORSED it requires the operator key regardless of the object's type. The new orphan gate here tries both keys unconditionally for every signal, so a vote signed only with the voting key (a credential that's meant to carry much lower trust — it's routinely delegated to third parties) can pass the gate, get cached, and trigger a real MNGOVERNANCESYNC request for a non-funding signal, even though that exact vote is guaranteed to fail once the parent object actually arrives and CGovernanceObject::ProcessVote runs the correct key check. The commit message frames this as unavoidable because "which [key] applies depends on the parent object's type and the vote signal," but that's only true for FUNDING — for every other signal the key is always the operator key irrespective of type, so the ambiguity (and the two-key fallback) should be restricted to the FUNDING signal.source: ['codex']