55#include < bls/bls.h>
66#include < consensus/amount.h>
77#include < evo/deterministicmns.h>
8+ #include < evo/providertx.h>
9+ #include < evo/specialtx.h>
810#include < governance/governance.h>
911#include < governance/object.h>
1012#include < governance/vote.h>
@@ -74,6 +76,24 @@ CGovernanceObject MakeFailedTrigger(const COutPoint& mn_collateral, const CBLSSe
7476 return govobj;
7577}
7678
79+ struct GovernanceVoteNotifications final : CValidationInterface {
80+ mutable Mutex cs;
81+ std::vector<uint256> hashes GUARDED_BY (cs);
82+
83+ void NotifyGovernanceVote (const std::shared_ptr<CDeterministicMNList>&,
84+ const std::shared_ptr<const CGovernanceVote>& vote) override EXCLUSIVE_LOCKS_REQUIRED(!cs)
85+ {
86+ LOCK (cs);
87+ hashes.push_back (vote->GetHash ());
88+ }
89+
90+ std::vector<uint256> GetHashes () const EXCLUSIVE_LOCKS_REQUIRED(!cs)
91+ {
92+ LOCK (cs);
93+ return hashes;
94+ }
95+ };
96+
7797// A chain with one registered masternode whose voting (ECDSA) and operator (BLS) keys are known
7898// to the test, so votes can be signed for real and CGovernanceVote::CheckSignature is actually
7999// reached. Without a populated masternode list every CGovernanceVote::IsValid() call would
@@ -83,6 +103,7 @@ struct GovernanceVoteSetup : public TestChainSetup {
83103 CBLSSecretKey mn_operator_key;
84104 COutPoint mn_collateral;
85105 SimpleUTXOMap utxos;
106+ std::shared_ptr<GovernanceVoteNotifications> vote_notifications;
86107 // ! Fixed so a proposal can be rebuilt bit-for-bit (and keep its hash) once its fee tx is known.
87108 int64_t proposal_time{0 };
88109 std::string proposal_payment_address;
@@ -135,6 +156,8 @@ struct GovernanceVoteSetup : public TestChainSetup {
135156 *m_node.sbman , *m_node.dmnman ,
136157 *m_node.mn_sync );
137158 BOOST_REQUIRE (m_node.govman ->LoadCache (/* load_cache=*/ false ));
159+ vote_notifications = std::make_shared<GovernanceVoteNotifications>();
160+ RegisterSharedValidationInterface (vote_notifications);
138161
139162 proposal_time = TicksSinceEpoch<std::chrono::seconds>(GetAdjustedTime ());
140163 CKey payment_key;
@@ -144,6 +167,11 @@ struct GovernanceVoteSetup : public TestChainSetup {
144167
145168 void TearDown ()
146169 {
170+ if (vote_notifications) {
171+ SyncWithValidationInterfaceQueue ();
172+ UnregisterSharedValidationInterface (vote_notifications);
173+ vote_notifications.reset ();
174+ }
147175 // govman holds a reference to node.sbman, so it must go first (matches PrepareShutdown
148176 // ordering in init.cpp).
149177 m_node.govman .reset ();
@@ -320,6 +348,129 @@ BOOST_AUTO_TEST_CASE(orphan_funding_votes_from_multiple_masternodes_are_applied)
320348 BOOST_CHECK (govman.GetOrphanVoteObjectHashes ().empty ());
321349}
322350
351+ BOOST_AUTO_TEST_CASE (orphan_timestamp_variants_replay_only_the_newest_vote)
352+ {
353+ auto & govman = *m_node.govman ;
354+ const uint256 parent_hash{MakeProposal (uint256{}).GetHash ()};
355+ uint256 newest_hash;
356+ for (const int offset : {5 , 1 , 4 , 3 , 2 , 5 }) {
357+ CGovernanceVote vote{MakeVote (parent_hash, VOTE_SIGNAL_FUNDING , VOTE_OUTCOME_YES , std::chrono::seconds{offset})};
358+ SignWithVotingKey (vote, mn_voting_key);
359+ if (offset == 5 ) newest_hash = vote.GetHash ();
360+ CGovernanceException exception;
361+ uint256 hash_to_request;
362+ BOOST_CHECK (!govman.ProcessVote (vote, exception, hash_to_request));
363+ BOOST_CHECK_EQUAL (exception.GetNodePenalty (), 0 );
364+ }
365+
366+ const uint256 collateral_hash{ConfirmProposalCollateral (parent_hash)};
367+ CGovernanceObject proposal{MakeProposal (collateral_hash)};
368+ govman.AddGovernanceObject (proposal, /* peer_str=*/ " " );
369+ SyncWithValidationInterfaceQueue ();
370+ BOOST_CHECK (vote_notifications->GetHashes () == std::vector<uint256>{newest_hash});
371+ const auto stored{govman.FindConstGovernanceObject (parent_hash)};
372+ BOOST_REQUIRE (stored != nullptr );
373+ BOOST_CHECK_EQUAL (stored->GetAbsoluteYesCount (tip_mn_list (), VOTE_SIGNAL_FUNDING ), 1 );
374+ const auto votes{govman.GetCurrentVotes (parent_hash, mn_collateral)};
375+ BOOST_REQUIRE_EQUAL (votes.size (), 1U );
376+ BOOST_CHECK_EQUAL (votes.front ().GetHash (), newest_hash);
377+ }
378+
379+ BOOST_AUTO_TEST_CASE (newer_operator_orphan_does_not_replace_voting_key_orphan)
380+ {
381+ auto & govman = *m_node.govman ;
382+ const uint256 parent_hash{MakeProposal (uint256{}).GetHash ()};
383+ CGovernanceVote operator_vote{MakeVote (parent_hash, VOTE_SIGNAL_FUNDING , VOTE_OUTCOME_YES , 30s)};
384+ SignWithOperatorKey (operator_vote, mn_operator_key);
385+ CGovernanceVote voting_vote{MakeVote (parent_hash, VOTE_SIGNAL_FUNDING , VOTE_OUTCOME_YES , 1s)};
386+ SignWithVotingKey (voting_vote, mn_voting_key);
387+ for (const auto & vote : {operator_vote, voting_vote, voting_vote}) {
388+ CGovernanceException exception;
389+ uint256 hash_to_request;
390+ BOOST_CHECK (!govman.ProcessVote (vote, exception, hash_to_request));
391+ BOOST_CHECK_EQUAL (exception.GetNodePenalty (), 0 );
392+ }
393+ const uint256 collateral_hash{ConfirmProposalCollateral (parent_hash)};
394+ CGovernanceObject proposal{MakeProposal (collateral_hash)};
395+ govman.AddGovernanceObject (proposal, /* peer_str=*/ " " );
396+ SyncWithValidationInterfaceQueue ();
397+ BOOST_CHECK (vote_notifications->GetHashes () == std::vector<uint256>{voting_vote.GetHash ()});
398+ const auto votes{govman.GetCurrentVotes (parent_hash, mn_collateral)};
399+ BOOST_REQUIRE_EQUAL (votes.size (), 1U );
400+ BOOST_CHECK_EQUAL (votes.front ().GetHash (), voting_vote.GetHash ());
401+ }
402+
403+ BOOST_AUTO_TEST_CASE (equal_time_operator_orphan_does_not_hide_voting_key_orphan)
404+ {
405+ auto & govman = *m_node.govman ;
406+ const uint256 parent_hash{MakeProposal (uint256{}).GetHash ()};
407+ CGovernanceVote operator_vote{MakeVote (parent_hash, VOTE_SIGNAL_FUNDING , VOTE_OUTCOME_YES )};
408+ SignWithOperatorKey (operator_vote, mn_operator_key);
409+ CGovernanceVote voting_vote{operator_vote};
410+ SignWithVotingKey (voting_vote, mn_voting_key);
411+ for (const auto & vote : {operator_vote, voting_vote}) {
412+ CGovernanceException exception;
413+ uint256 hash_to_request;
414+ BOOST_CHECK (!govman.ProcessVote (vote, exception, hash_to_request));
415+ BOOST_CHECK_EQUAL (exception.GetNodePenalty (), 0 );
416+ }
417+ const uint256 collateral_hash{ConfirmProposalCollateral (parent_hash)};
418+ CGovernanceObject proposal{MakeProposal (collateral_hash)};
419+ govman.AddGovernanceObject (proposal, /* peer_str=*/ " " );
420+ SyncWithValidationInterfaceQueue ();
421+ BOOST_CHECK (vote_notifications->GetHashes () == std::vector<uint256>{voting_vote.GetHash ()});
422+ const auto votes{govman.GetCurrentVotes (parent_hash, mn_collateral)};
423+ BOOST_REQUIRE_EQUAL (votes.size (), 1U );
424+ BOOST_CHECK_EQUAL (votes.front ().GetHash (), voting_vote.GetHash ());
425+ }
426+
427+ BOOST_AUTO_TEST_CASE (old_key_orphan_does_not_suppress_a_current_key_vote)
428+ {
429+ // Before DIP3 enforcement, registrar updates must keep the owner and voting keys equal.
430+ MineBlocks (500 - tip_mn_list ().GetHeight ());
431+ auto & govman = *m_node.govman ;
432+ const uint256 parent_hash{MakeProposal (uint256{}).GetHash ()};
433+ CGovernanceVote old_vote{MakeVote (parent_hash, VOTE_SIGNAL_FUNDING , VOTE_OUTCOME_YES , 30s)};
434+ SignWithVotingKey (old_vote, mn_voting_key);
435+ CGovernanceException exception;
436+ uint256 hash_to_request;
437+ BOOST_CHECK (!govman.ProcessVote (old_vote, exception, hash_to_request));
438+ BOOST_CHECK_EQUAL (exception.GetNodePenalty (), 0 );
439+
440+ CKey new_voting_key;
441+ new_voting_key.MakeNewKey (true );
442+ CProUpRegTx update;
443+ update.nVersion = ProTxVersion::GetMax (!bls::bls_legacy_scheme, /* is_extended_addr=*/ false );
444+ update.proTxHash = mn_collateral.hash ;
445+ update.pubKeyOperator .Set (mn_operator_key.GetPublicKey (), update.nVersion == ProTxVersion::LegacyBLS);
446+ update.keyIDVoting = new_voting_key.GetPubKey ().GetID ();
447+ update.scriptPayout = payout_script ();
448+ CMutableTransaction tx;
449+ tx.nVersion = 3 ;
450+ tx.nType = TRANSACTION_PROVIDER_UPDATE_REGISTRAR ;
451+ const auto spent{FundTransaction (*m_node.chainman , tx, utxos, payout_script (), 1 * COIN )};
452+ update.inputsHash = CalcTxInputsHash (CTransaction{tx});
453+ BOOST_REQUIRE (CHashSigner::SignHash (SerializeHash (update), mn_voting_key, update.vchSig ));
454+ SetTxPayload (tx, update);
455+ SignTransaction (tx, spent, coinbaseKey);
456+ MineBlock ({tx});
457+ BOOST_REQUIRE (!old_vote.IsValid (tip_mn_list (), /* useVotingKey=*/ true ));
458+
459+ CGovernanceVote current_vote{MakeVote (parent_hash, VOTE_SIGNAL_FUNDING , VOTE_OUTCOME_YES , 1s)};
460+ SignWithVotingKey (current_vote, new_voting_key);
461+ BOOST_REQUIRE (current_vote.IsValid (tip_mn_list (), /* useVotingKey=*/ true ));
462+ BOOST_CHECK (!govman.ProcessVote (current_vote, exception, hash_to_request));
463+ BOOST_CHECK_EQUAL (exception.GetNodePenalty (), 0 );
464+ const uint256 collateral_hash{ConfirmProposalCollateral (parent_hash)};
465+ CGovernanceObject proposal{MakeProposal (collateral_hash)};
466+ govman.AddGovernanceObject (proposal, /* peer_str=*/ " " );
467+ SyncWithValidationInterfaceQueue ();
468+ BOOST_CHECK (vote_notifications->GetHashes () == std::vector<uint256>{current_vote.GetHash ()});
469+ const auto votes{govman.GetCurrentVotes (parent_hash, mn_collateral)};
470+ BOOST_REQUIRE_EQUAL (votes.size (), 1U );
471+ BOOST_CHECK_EQUAL (votes.front ().GetHash (), current_vote.GetHash ());
472+ }
473+
323474// Votes are only counted if they carry a signature from a registered masternode.
324475BOOST_AUTO_TEST_CASE (unsigned_and_unknown_masternode_votes_are_rejected)
325476{
0 commit comments