diff --git a/src/activemasternode.cpp b/src/activemasternode.cpp index ab82b9681953..6e06808efb2c 100644 --- a/src/activemasternode.cpp +++ b/src/activemasternode.cpp @@ -7,7 +7,6 @@ #include "addrman.h" #include "evo/providertx.h" -#include "evo/deterministicmns.h" #include "masternode-sync.h" #include "masternode.h" #include "masternodeconfig.h" @@ -67,10 +66,38 @@ OperationResult CActiveDeterministicMasternodeManager::SetOperatorKey(const std: return OperationResult(true); } +OperationResult CActiveDeterministicMasternodeManager::GetOperatorKey(CKey& key, CKeyID& keyID, CDeterministicMNCPtr& dmn) const +{ + if (!IsReady()) { + return errorOut("Active masternode not ready"); + } + dmn = deterministicMNManager->GetListAtChainTip().GetValidMN(info.proTxHash); + if (!dmn) { + return errorOut(strprintf("Active masternode %s not registered or PoSe banned", info.proTxHash.ToString())); + } + if (info.keyIDOperator != dmn->pdmnState->keyIDOperator) { + return errorOut("Active masternode operator key changed or revoked"); + } + // return keys + key = info.keyOperator; + keyID = info.keyIDOperator; + return OperationResult(true); +} + void CActiveDeterministicMasternodeManager::Init() { - if (!fMasterNode || !deterministicMNManager->IsDIP3Enforced()) + // set masternode arg if called from RPC + if (!fMasterNode) { + gArgs.ForceSetArg("-masternode", "1"); + fMasterNode = true; + } + + if (!deterministicMNManager->IsDIP3Enforced()) { + state = MASTERNODE_ERROR; + strError = "Evo upgrade is not active yet."; + LogPrintf("%s -- ERROR: %s\n", __func__, strError); return; + } LOCK(cs_main); @@ -109,6 +136,8 @@ void CActiveDeterministicMasternodeManager::Init() LogPrintf("%s: proTxHash=%s, proTx=%s\n", __func__, dmn->proTxHash.ToString(), dmn->ToString()); + info.proTxHash = dmn->proTxHash; + if (info.service != dmn->pdmnState->addr) { state = MASTERNODE_ERROR; strError = strprintf("Local address %s does not match the address from ProTx (%s)", @@ -132,7 +161,6 @@ void CActiveDeterministicMasternodeManager::Init() } } - info.proTxHash = dmn->proTxHash; state = MASTERNODE_READY; } @@ -260,6 +288,7 @@ void CActiveMasternode::ManageStatus() return; } + // !TODO: Legacy masternodes - remove after enforcement LogPrint(BCLog::MASTERNODE, "CActiveMasternode::ManageStatus() - Begin\n"); // If a DMN has been registered with same collateral, disable me. @@ -427,3 +456,33 @@ void CActiveMasternode::GetKeys(CKey& _privKeyMasternode, CPubKey& _pubKeyMaster _privKeyMasternode = privKeyMasternode; _pubKeyMasternode = pubKeyMasternode; } + +bool GetActiveMasternodeKeys(CKey& key, CKeyID& keyID, CTxIn& vin) +{ + if (activeMasternodeManager != nullptr) { + // deterministic mn + CDeterministicMNCPtr dmn; + auto res = activeMasternodeManager->GetOperatorKey(key, keyID, dmn); + if (!res) { + LogPrint(BCLog::MNBUDGET,"%s: %s\n", __func__, res.getError()); + return false; + } + vin = CTxIn(dmn->collateralOutpoint); + return true; + } + + // legacy mn + if (activeMasternode.vin == nullopt) { + LogPrint(BCLog::MNBUDGET,"%s: Active Masternode not initialized\n", __func__); + return false; + } + if (activeMasternode.GetStatus() != ACTIVE_MASTERNODE_STARTED) { + LogPrint(BCLog::MNBUDGET,"%s: MN not started (%s)\n", __func__, activeMasternode.GetStatusMessage()); + return false; + } + CPubKey mnPubKey; + activeMasternode.GetKeys(key, mnPubKey); + keyID = mnPubKey.GetID(); + vin = *activeMasternode.vin; + return true; +} diff --git a/src/activemasternode.h b/src/activemasternode.h index 1990b94fad45..5a70af467210 100644 --- a/src/activemasternode.h +++ b/src/activemasternode.h @@ -8,6 +8,7 @@ #include "init.h" #include "key.h" +#include "evo/deterministicmns.h" #include "masternode.h" #include "net.h" #include "operationresult.h" @@ -60,6 +61,9 @@ class CActiveDeterministicMasternodeManager : public CValidationInterface void Reset(masternode_state_t _state); // Sets the Deterministic Masternode Operator's private/public key OperationResult SetOperatorKey(const std::string& strMNOperatorPrivKey); + // If the active masternode is ready, and the keyID matches with the registered one, + // return private key, keyID, and pointer to dmn. + OperationResult GetOperatorKey(CKey& key, CKeyID& keyID, CDeterministicMNCPtr& dmn) const; void SetNullProTx() { info.proTxHash = UINT256_ZERO; } const CActiveMasternodeInfo* GetInfo() const { return &info; } @@ -112,4 +116,7 @@ class CActiveMasternode void GetKeys(CKey& privKeyMasternode, CPubKey& pubKeyMasternode); }; +// Compatibility code: get keys for either legacy or deterministic masternode +bool GetActiveMasternodeKeys(CKey& key, CKeyID& keyID, CTxIn& vin); + #endif diff --git a/src/budget/budgetmanager.cpp b/src/budget/budgetmanager.cpp index dbffb3b1ffdc..3abdd8e61b78 100644 --- a/src/budget/budgetmanager.cpp +++ b/src/budget/budgetmanager.cpp @@ -5,6 +5,8 @@ #include "budget/budgetmanager.h" +#include "consensus/validation.h" +#include "evo/deterministicmns.h" #include "masternode-sync.h" #include "masternodeman.h" #include "net_processing.h" @@ -473,10 +475,6 @@ void CBudgetManager::VoteOnFinalizedBudgets() LogPrint(BCLog::MNBUDGET,"%s: Not a masternode\n", __func__); return; } - if (activeMasternode.vin == nullopt) { - LogPrint(BCLog::MNBUDGET,"%s: Active Masternode not initialized\n", __func__); - return; - } // Do this 1 in 4 blocks -- spread out the voting activity // -- this function is only called every fourteenth block, so this is really 1 in 56 blocks @@ -485,6 +483,12 @@ void CBudgetManager::VoteOnFinalizedBudgets() return; } + // Get the active masternode (operator) key + CKey mnKey; CKeyID mnKeyID; CTxIn mnVin; + if (!GetActiveMasternodeKeys(mnKey, mnKeyID, mnVin)) { + return; + } + std::vector vBudget = GetBudget(); if (vBudget.empty()) { LogPrint(BCLog::MNBUDGET,"%s: No proposal can be finalized\n", __func__); @@ -516,15 +520,10 @@ void CBudgetManager::VoteOnFinalizedBudgets() } } - // Get masternode keys - CPubKey pubKeyMasternode; - CKey keyMasternode; - activeMasternode.GetKeys(keyMasternode, pubKeyMasternode); - // Sign finalized budgets for (const uint256& budgetHash: vBudgetHashes) { - CFinalizedBudgetVote vote(*(activeMasternode.vin), budgetHash); - if (!vote.Sign(keyMasternode, pubKeyMasternode.GetID())) { + CFinalizedBudgetVote vote(mnVin, budgetHash); + if (!vote.Sign(mnKey, mnKeyID)) { LogPrintf("%s: Failure to sign budget %s", __func__, budgetHash.ToString()); continue; } @@ -533,7 +532,7 @@ void CBudgetManager::VoteOnFinalizedBudgets() LogPrintf("%s: Error submitting vote - %s\n", __func__, strError); continue; } - LogPrint(BCLog::MNBUDGET,"%s: new finalized budget vote signed: %s\n", __func__, vote.GetHash().ToString()); + LogPrint(BCLog::MNBUDGET, "%s: new finalized budget vote signed: %s\n", __func__, vote.GetHash().ToString()); AddSeenFinalizedBudgetVote(vote); vote.Relay(); } @@ -800,10 +799,18 @@ void CBudgetManager::RemoveStaleVotesOnProposal(CBudgetProposal* prop) auto it = prop->mapVotes.begin(); while (it != prop->mapVotes.end()) { - CMasternode* pmn = mnodeman.Find(it->first); - (*it).second.SetValid(pmn && pmn->IsEnabled()); + auto mnList = deterministicMNManager->GetListAtChainTip(); + auto dmn = mnList.GetMNByCollateral(it->first); + if (dmn) { + (*it).second.SetValid(mnList.IsMNValid(dmn)); + } else { + // -- Legacy System (!TODO: remove after enforcement) -- + CMasternode* pmn = mnodeman.Find(it->first); + (*it).second.SetValid(pmn && pmn->IsEnabled()); + } ++it; } + LogPrint(BCLog::MNBUDGET, "Cleaned proposal votes for %s. After: YES=%d, NO=%d\n", prop->GetName(), prop->GetYeas(), prop->GetNays()); } @@ -816,8 +823,15 @@ void CBudgetManager::RemoveStaleVotesOnFinalBudget(CFinalizedBudget* fbud) auto it = fbud->mapVotes.begin(); while (it != fbud->mapVotes.end()) { - CMasternode* pmn = mnodeman.Find(it->first); - (*it).second.SetValid(pmn && pmn->IsEnabled()); + auto mnList = deterministicMNManager->GetListAtChainTip(); + auto dmn = mnList.GetMNByCollateral(it->first); + if (dmn) { + (*it).second.SetValid(mnList.IsMNValid(dmn)); + } else { + // -- Legacy System (!TODO: remove after enforcement) -- + CMasternode* pmn = mnodeman.Find(it->first); + (*it).second.SetValid(pmn && pmn->IsEnabled()); + } ++it; } LogPrint(BCLog::MNBUDGET, "Cleaned finalized budget votes for [%s (%s)]. After: %d\n", @@ -959,46 +973,85 @@ int CBudgetManager::ProcessProposal(CBudgetProposal& proposal) return 0; } -int CBudgetManager::ProcessProposalVote(CBudgetVote& vote, CNode* pfrom) +bool CBudgetManager::ProcessProposalVote(CBudgetVote& vote, CNode* pfrom, CValidationState& state) { - if (HaveSeenProposalVote(vote.GetHash())) { - masternodeSync.AddedBudgetItem(vote.GetHash()); - return 0; + const uint256& voteID = vote.GetHash(); + + if (HaveSeenProposalVote(voteID)) { + masternodeSync.AddedBudgetItem(voteID); + return false; } const CTxIn& voteVin = vote.GetVin(); + + // See if this vote was signed with a deterministic masternode + std::string err; + auto mnList = deterministicMNManager->GetListAtChainTip(); + auto dmn = mnList.GetMNByCollateral(voteVin.prevout); + if (dmn) { + const std::string& mn_protx_id = dmn->proTxHash.ToString(); + + if (!vote.CheckSignature(dmn->pdmnState->keyIDVoting)) { + err = strprintf("invalid mvote sig from dmn: %s", mn_protx_id); + return state.DoS(100, false, REJECT_INVALID, "bad-mvote-sig", false, err); + } + + AddSeenProposalVote(vote); + + if (!mnList.IsMNValid(dmn)) { + err = strprintf("masternode (%s) not valid or PoSe banned", mn_protx_id); + return state.DoS(0, false, REJECT_INVALID, "bad-mvote", false, err); + } + + if (!UpdateProposal(vote, pfrom, err)) { + return state.DoS(0, false, REJECT_INVALID, "bad-mvote", false, strprintf("%s (%s)", err, mn_protx_id)); + } + + vote.Relay(); + masternodeSync.AddedBudgetItem(voteID); + LogPrint(BCLog::MNBUDGET, "mvote - new vote (%s) for proposal %s from dmn %s\n", + voteID.ToString(), vote.GetProposalHash().ToString(), mn_protx_id); + return true; + } + + // -- Legacy System (!TODO: remove after enforcement) -- + CMasternode* pmn = mnodeman.Find(voteVin.prevout); if (!pmn) { - LogPrint(BCLog::MNBUDGET, "mvote - unknown masternode - vin: %s\n", voteVin.ToString()); + err = strprintf("unknown masternode - vin: %s", voteVin.prevout.ToString()); mnodeman.AskForMN(pfrom, voteVin); - return 0; + return state.DoS(0, false, REJECT_INVALID, "bad-mvote", false, err); } AddSeenProposalVote(vote); if (!vote.CheckSignature(pmn->pubKeyMasternode.GetID())) { if (masternodeSync.IsSynced()) { - LogPrintf("mvote - signature invalid\n"); - return 20; + err = strprintf("signature from masternode %s invalid", voteVin.prevout.ToString()); + return state.DoS(20, false, REJECT_INVALID, "bad-fbvote", false, err); } // it could just be a non-synced masternode mnodeman.AskForMN(pfrom, voteVin); - return 0; + return false; } - std::string strError; - if (pmn->IsEnabled() && UpdateProposal(vote, pfrom, strError)) { - vote.Relay(); - masternodeSync.AddedBudgetItem(vote.GetHash()); - LogPrint(BCLog::MNBUDGET, "mvote - new budget vote for budget %s - %s\n", vote.GetProposalHash().ToString(), vote.GetHash().ToString()); - } else { - LogPrint(BCLog::MNBUDGET, "mvote error: %s", strError); + if (!pmn->IsEnabled()) { + return state.DoS(0, false, REJECT_INVALID, "bad-mvote", false, "masternode not valid"); } - return 0; + if (!UpdateProposal(vote, pfrom, err)) { + return state.DoS(0, false, REJECT_INVALID, "bad-mvote", false, err); + } + + vote.Relay(); + masternodeSync.AddedBudgetItem(voteID); + LogPrint(BCLog::MNBUDGET, "mvote - new vote (%s) for proposal %s from dmn %s\n", + voteID.ToString(), vote.GetProposalHash().ToString(), voteVin.prevout.ToString()); + return true; } int CBudgetManager::ProcessFinalizedBudget(CFinalizedBudget& finalbudget) { + const uint256& nHash = finalbudget.GetHash(); if (HaveFinalizedBudget(nHash)) { masternodeSync.AddedBudgetItem(nHash); @@ -1016,42 +1069,78 @@ int CBudgetManager::ProcessFinalizedBudget(CFinalizedBudget& finalbudget) return 0; } -int CBudgetManager::ProcessFinalizedBudgetVote(CFinalizedBudgetVote& vote, CNode* pfrom) +bool CBudgetManager::ProcessFinalizedBudgetVote(CFinalizedBudgetVote& vote, CNode* pfrom, CValidationState& state) { - if (HaveSeenFinalizedBudgetVote(vote.GetHash())) { - masternodeSync.AddedBudgetItem(vote.GetHash()); - return 0; + const uint256& voteID = vote.GetHash(); + + if (HaveSeenFinalizedBudgetVote(voteID)) { + masternodeSync.AddedBudgetItem(voteID); + return false; } const CTxIn& voteVin = vote.GetVin(); + + // See if this vote was signed with a deterministic masternode + std::string err; + auto mnList = deterministicMNManager->GetListAtChainTip(); + auto dmn = mnList.GetMNByCollateral(voteVin.prevout); + if (dmn) { + const std::string& mn_protx_id = dmn->proTxHash.ToString(); + + if (!vote.CheckSignature(dmn->pdmnState->keyIDOperator)) { + err = strprintf("invalid fbvote sig from dmn: %s", mn_protx_id); + return state.DoS(100, false, REJECT_INVALID, "bad-fbvote-sig", false, err); + } + + AddSeenFinalizedBudgetVote(vote); + + if (!mnList.IsMNValid(dmn)) { + err = strprintf("masternode (%s) not valid or PoSe banned", mn_protx_id); + return state.DoS(0, false, REJECT_INVALID, "bad-fbvote", false, err); + } + if (!UpdateFinalizedBudget(vote, pfrom, err)) { + return state.DoS(0, false, REJECT_INVALID, "bad-fbvote", false, strprintf("%s (%s)", err, mn_protx_id)); + } + + vote.Relay(); + masternodeSync.AddedBudgetItem(voteID); + LogPrint(BCLog::MNBUDGET, "fbvote - new vote (%s) for budget %s from dmn %s\n", + voteID.ToString(), vote.GetBudgetHash().ToString(), mn_protx_id); + return true; + } + + // -- Legacy System (!TODO: remove after enforcement) -- CMasternode* pmn = mnodeman.Find(voteVin.prevout); if (!pmn) { - LogPrint(BCLog::MNBUDGET, "fbvote - unknown masternode - vin: %s\n", voteVin.prevout.hash.ToString()); + err = strprintf("unknown masternode - vin: %s", voteVin.prevout.ToString()); mnodeman.AskForMN(pfrom, voteVin); - return 0; + return state.DoS(0, false, REJECT_INVALID, "bad-fbvote", false, err); } AddSeenFinalizedBudgetVote(vote); if (!vote.CheckSignature(pmn->pubKeyMasternode.GetID())) { if (masternodeSync.IsSynced()) { - LogPrint(BCLog::MNBUDGET, "fbvote - signature from masternode %s invalid\n", HexStr(pmn->pubKeyMasternode)); - return 20; + err = strprintf("signature from masternode %s invalid", voteVin.prevout.ToString()); + return state.DoS(20, false, REJECT_INVALID, "bad-fbvote", false, err); } // it could just be a non-synced masternode mnodeman.AskForMN(pfrom, voteVin); - return 0; + return false; } - std::string strError; - if (UpdateFinalizedBudget(vote, pfrom, strError)) { - vote.Relay(); - masternodeSync.AddedBudgetItem(vote.GetHash()); - LogPrint(BCLog::MNBUDGET, "fbvote - new finalized budget vote - %s from masternode %s\n", vote.GetHash().ToString(), HexStr(pmn->pubKeyMasternode)); - } else { - LogPrint(BCLog::MNBUDGET, "fbvote - rejected finalized budget vote - %s from masternode %s - %s\n", vote.GetHash().ToString(), HexStr(pmn->pubKeyMasternode), strError); + if (!pmn->IsEnabled()) { + return state.DoS(0, false, REJECT_INVALID, "bad-fbvote", false, "masternode not valid"); } - return 0; + if (!UpdateFinalizedBudget(vote, pfrom, err)) { + return state.DoS(0, false, REJECT_INVALID, "bad-fbvote", false, err); + } + + vote.Relay(); + masternodeSync.AddedBudgetItem(voteID); + LogPrint(BCLog::MNBUDGET, "fbvote - new vote (%s) for budget %s from mn %s\n", + voteID.ToString(), vote.GetBudgetHash().ToString(), voteVin.prevout.ToString()); + return true; } void CBudgetManager::ProcessMessage(CNode* pfrom, std::string& strCommand, CDataStream& vRecv) @@ -1089,7 +1178,15 @@ int CBudgetManager::ProcessMessageInner(CNode* pfrom, std::string& strCommand, C CBudgetVote vote; vRecv >> vote; vote.SetValid(true); - return ProcessProposalVote(vote, pfrom); + CValidationState state; + if (!ProcessProposalVote(vote, pfrom, state)) { + int nDos = 0; + if(state.IsInvalid(nDos) && nDos > 0) { + LogPrint(BCLog::NET, "%s: %s\n", __func__, FormatStateMessage(state)); + return nDos; + } + } + return 0; } if (strCommand == NetMsgType::FINALBUDGET) { @@ -1105,7 +1202,15 @@ int CBudgetManager::ProcessMessageInner(CNode* pfrom, std::string& strCommand, C CFinalizedBudgetVote vote; vRecv >> vote; vote.SetValid(true); - return ProcessFinalizedBudgetVote(vote, pfrom); + CValidationState state; + if (!ProcessFinalizedBudgetVote(vote, pfrom, state)) { + int nDos = 0; + if(state.IsInvalid(nDos) && nDos > 0) { + LogPrint(BCLog::NET, "%s: %s\n", __func__, FormatStateMessage(state)); + return nDos; + } + } + return 0; } // nothing was done diff --git a/src/budget/budgetmanager.h b/src/budget/budgetmanager.h index add1de973cee..eb1cbc3a7f5d 100644 --- a/src/budget/budgetmanager.h +++ b/src/budget/budgetmanager.h @@ -9,6 +9,8 @@ #include "budget/budgetproposal.h" #include "budget/finalizedbudget.h" +class CValidationState; + // // Budget Manager : Contains all proposals for the budget // @@ -97,9 +99,10 @@ class CBudgetManager int ProcessBudgetVoteSync(const uint256& nProp, CNode* pfrom); int ProcessProposal(CBudgetProposal& proposal); - int ProcessProposalVote(CBudgetVote& proposal, CNode* pfrom); int ProcessFinalizedBudget(CFinalizedBudget& finalbudget); - int ProcessFinalizedBudgetVote(CFinalizedBudgetVote& vote, CNode* pfrom); + + bool ProcessProposalVote(CBudgetVote& proposal, CNode* pfrom, CValidationState& state); + bool ProcessFinalizedBudgetVote(CFinalizedBudgetVote& vote, CNode* pfrom, CValidationState& state); // functions returning a pointer in the map. Need cs_proposals/cs_budgets locked from the caller CBudgetProposal* FindProposal(const uint256& nHash); diff --git a/src/masternode-payments.cpp b/src/masternode-payments.cpp index 1f2715ea2fb0..5c029749bfc9 100644 --- a/src/masternode-payments.cpp +++ b/src/masternode-payments.cpp @@ -4,6 +4,7 @@ // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include "masternode-payments.h" + #include "addrman.h" #include "chainparams.h" #include "evo/deterministicmns.h" @@ -164,24 +165,9 @@ std::string CMasternodePaymentWinner::GetStrMessage() const bool CMasternodePaymentWinner::IsValid(CNode* pnode, std::string& strError) { - CMasternode* pmn = mnodeman.Find(vinMasternode.prevout); - - if (!pmn) { - strError = strprintf("Unknown Masternode %s", vinMasternode.prevout.hash.ToString()); - LogPrint(BCLog::MASTERNODE,"CMasternodePaymentWinner::IsValid - %s\n", strError); - mnodeman.AskForMN(pnode, vinMasternode); - return false; - } - - if (pmn->protocolVersion < ActiveProtocol()) { - strError = strprintf("Masternode protocol too old %d - req %d", pmn->protocolVersion, ActiveProtocol()); - LogPrint(BCLog::MASTERNODE,"CMasternodePaymentWinner::IsValid - %s\n", strError); - return false; - } - int n = mnodeman.GetMasternodeRank(vinMasternode, nBlockHeight - 100); - if (n > MNPAYMENTS_SIGNATURES_TOTAL) { + if (n < 1 || n > MNPAYMENTS_SIGNATURES_TOTAL) { //It's common to have masternodes mistakenly think they are in the top 10 // We don't want to print all of these messages, or punish them unless they're way off if (n > MNPAYMENTS_SIGNATURES_TOTAL * 2) { @@ -338,9 +324,12 @@ bool CMasternodePayments::GetLegacyMasternodeTxOut(int nHeight, std::vectorpubKeyCollateralAddress.GetID()); + payee = winningNode->GetPayeeScript(); } else { LogPrint(BCLog::MASTERNODE,"CreateNewBlock: Failed to detect masternode to pay\n"); return false; @@ -414,6 +403,12 @@ void CMasternodePayments::ProcessMessageMasternodePayments(CNode* pfrom, std::st if (fLiteMode) return; //disable all Masternode related functionality + // Skip after legacy obsolete. !TODO: remove when transition to DMN is complete + if (deterministicMNManager->LegacyMNObsolete()) { + LogPrint(BCLog::MASTERNODE, "mnw - skip obsolete message %s\n", strCommand); + return; + } + if (strCommand == NetMsgType::GETMNWINNERS) { //Masternode Payments Request Sync if (fLiteMode) return; //disable all Masternode related functionality @@ -471,23 +466,34 @@ void CMasternodePayments::ProcessMessageMasternodePayments(CNode* pfrom, std::st return; } - const CMasternode* pmn = mnodeman.Find(winner.vinMasternode.prevout); - if (!pmn || !winner.CheckSignature(pmn->pubKeyMasternode.GetID())) { + // See if this winner was signed with a dmn or a legacy masternode + bool fDeterministic{false}; + Optional mnKeyID = nullopt; + auto mnList = deterministicMNManager->GetListAtChainTip(); + auto dmn = mnList.GetMNByCollateral(winner.vinMasternode.prevout); + if (dmn) { + fDeterministic = true; + mnKeyID = Optional(dmn->pdmnState->keyIDOperator); + } else { + const CMasternode* pmn = mnodeman.Find(winner.vinMasternode.prevout); + if (pmn) { + mnKeyID = Optional(pmn->pubKeyMasternode.GetID()); + } + } + + if (mnKeyID == nullopt || !winner.CheckSignature(*mnKeyID)) { if (masternodeSync.IsSynced()) { LogPrintf("CMasternodePayments::ProcessMessageMasternodePayments() : mnw - invalid signature\n"); LOCK(cs_main); Misbehaving(pfrom->GetId(), 20); } // it could just be a non-synced masternode - mnodeman.AskForMN(pfrom, winner.vinMasternode); + if (!fDeterministic) { + mnodeman.AskForMN(pfrom, winner.vinMasternode); + } return; } - CTxDestination address1; - ExtractDestination(winner.payee, address1); - - // LogPrint(BCLog::MASTERNODE, "mnw - winning vote - Addr %s Height %d bestHeight %d - %s\n", address2.ToString().c_str(), winner.nBlockHeight, nHeight, winner.vinMasternode.prevout.ToStringShort()); - if (masternodePayments.AddWinningMasternode(winner)) { winner.Relay(); masternodeSync.AddedMasternodeWinner(winner.GetHash()); @@ -513,9 +519,7 @@ bool CMasternodePayments::IsScheduled(const CMasternode& mn, int nNotBlockHeight int nHeight = mnodeman.GetBestHeight(); - CScript mnpayee; - mnpayee = GetScriptForDestination(mn.pubKeyCollateralAddress.GetID()); - + const CScript& mnpayee = mn.GetPayeeScript(); CScript payee; for (int64_t h = nHeight; h <= nHeight + 8; h++) { if (h == nNotBlockHeight) continue; @@ -553,6 +557,9 @@ bool CMasternodePayments::AddWinningMasternode(CMasternodePaymentWinner& winnerI } } + CTxDestination addr; + ExtractDestination(winnerIn.payee, addr); + LogPrint(BCLog::MASTERNODE, "mnw - Adding winner %s for block %d\n", EncodeDestination(addr), winnerIn.nBlockHeight); mapMasternodeBlocks[winnerIn.nBlockHeight].AddPayee(winnerIn.payee, 1); return true; @@ -688,58 +695,60 @@ void CMasternodePayments::CleanPaymentList(int mnCount, int nHeight) } } -bool CMasternodePayments::ProcessBlock(int nBlockHeight) +void CMasternodePayments::ProcessBlock(int nBlockHeight) { - if (!fMasterNode) return false; + // No more mnw messages after transition to DMN + if (deterministicMNManager->LegacyMNObsolete()) { + return; + } + if (!fMasterNode) return; - if (activeMasternode.vin == nullopt) - return error("%s: Active Masternode not initialized.", __func__); + // Get the active masternode (operator) key + CKey mnKey; CKeyID mnKeyID; CTxIn mnVin; + if (!GetActiveMasternodeKeys(mnKey, mnKeyID, mnVin)) { + return; + } //reference node - hybrid mode - int n = mnodeman.GetMasternodeRank(*(activeMasternode.vin), nBlockHeight - 100); + int n = mnodeman.GetMasternodeRank(mnVin, nBlockHeight - 100); if (n == -1) { LogPrint(BCLog::MASTERNODE, "CMasternodePayments::ProcessBlock - Unknown Masternode\n"); - return false; + return; } if (n > MNPAYMENTS_SIGNATURES_TOTAL) { LogPrint(BCLog::MASTERNODE, "CMasternodePayments::ProcessBlock - Masternode not in the top %d (%d)\n", MNPAYMENTS_SIGNATURES_TOTAL, n); - return false; + return; } - if (nBlockHeight <= nLastBlockHeight) return false; + if (nBlockHeight <= nLastBlockHeight) return; if (g_budgetman.IsBudgetPaymentBlock(nBlockHeight)) { //is budget payment block -- handled by the budgeting software - return false; + return; } - CMasternodePaymentWinner newWinner(*(activeMasternode.vin), nBlockHeight); // pay to the oldest MN that still had no payment but its input is old enough and it was active long enough int nCount = 0; MasternodeRef pmn = mnodeman.GetNextMasternodeInQueueForPayment(nBlockHeight, true, nCount); if (pmn == nullptr) { LogPrint(BCLog::MASTERNODE,"%s: Failed to find masternode to pay\n", __func__); - return false; + return; } - const CScript& payee = GetScriptForDestination(pmn->pubKeyCollateralAddress.GetID()); - newWinner.AddPayee(payee); - - CPubKey pubKeyMasternode; CKey keyMasternode; - activeMasternode.GetKeys(keyMasternode, pubKeyMasternode); - if (!newWinner.Sign(keyMasternode, pubKeyMasternode.GetID())) { + CMasternodePaymentWinner newWinner(mnVin, nBlockHeight); + newWinner.AddPayee(pmn->GetPayeeScript()); + if (!newWinner.Sign(mnKey, mnKeyID)) { LogPrintf("%s: Failed to sign masternode winner\n", __func__); - return false; + return; } if (!AddWinningMasternode(newWinner)) { - return false; + return; } newWinner.Relay(); nLastBlockHeight = nBlockHeight; - return true; } void CMasternodePayments::Sync(CNode* node, int nCountNeeded) diff --git a/src/masternode-payments.h b/src/masternode-payments.h index a231841a4d33..db90697df0c1 100644 --- a/src/masternode-payments.h +++ b/src/masternode-payments.h @@ -177,7 +177,7 @@ class CMasternodePaymentWinner : public CSignedMessage payee() {} - CMasternodePaymentWinner(CTxIn vinIn, int nHeight) : + CMasternodePaymentWinner(const CTxIn& vinIn, int nHeight): CSignedMessage(), vinMasternode(vinIn), nBlockHeight(nHeight), @@ -255,7 +255,7 @@ class CMasternodePayments } bool AddWinningMasternode(CMasternodePaymentWinner& winner); - bool ProcessBlock(int nBlockHeight); + void ProcessBlock(int nBlockHeight); void Sync(CNode* node, int nCountNeeded); void CleanPaymentList(int mnCount, int nHeight); diff --git a/src/masternode-sync.cpp b/src/masternode-sync.cpp index 7fcec9276c9b..072542bfa98c 100644 --- a/src/masternode-sync.cpp +++ b/src/masternode-sync.cpp @@ -6,6 +6,7 @@ // clang-format off #include "activemasternode.h" #include "budget/budgetmanager.h" +#include "evo/deterministicmns.h" #include "masternode-sync.h" #include "masternode-payments.h" #include "masternode.h" @@ -160,28 +161,37 @@ bool CMasternodeSync::IsBudgetFinEmpty() return sumBudgetItemFin == 0 && countBudgetItemFin > 0; } -void CMasternodeSync::GetNextAsset() +int CMasternodeSync::GetNextAsset(int currentAsset) { - switch (RequestedMasternodeAssets) { + if (currentAsset > MASTERNODE_SYNC_FINISHED) { + LogPrintf("%s - invalid asset %d\n", __func__, currentAsset); + return MASTERNODE_SYNC_FAILED; + } + switch (currentAsset) { case (MASTERNODE_SYNC_INITIAL): - case (MASTERNODE_SYNC_FAILED): // should never be used here actually, use Reset() instead - ClearFulfilledRequest(); - RequestedMasternodeAssets = MASTERNODE_SYNC_SPORKS; - break; + case (MASTERNODE_SYNC_FAILED): + return MASTERNODE_SYNC_SPORKS; case (MASTERNODE_SYNC_SPORKS): - RequestedMasternodeAssets = MASTERNODE_SYNC_LIST; - break; + return deterministicMNManager->LegacyMNObsolete() ? MASTERNODE_SYNC_BUDGET : MASTERNODE_SYNC_LIST; case (MASTERNODE_SYNC_LIST): - RequestedMasternodeAssets = MASTERNODE_SYNC_MNW; - break; + return deterministicMNManager->LegacyMNObsolete() ? MASTERNODE_SYNC_BUDGET : MASTERNODE_SYNC_MNW; case (MASTERNODE_SYNC_MNW): - RequestedMasternodeAssets = MASTERNODE_SYNC_BUDGET; - break; + return MASTERNODE_SYNC_BUDGET; case (MASTERNODE_SYNC_BUDGET): - LogPrintf("CMasternodeSync::GetNextAsset - Sync has finished\n"); - RequestedMasternodeAssets = MASTERNODE_SYNC_FINISHED; - break; + default: + return MASTERNODE_SYNC_FINISHED; + } +} + +void CMasternodeSync::SwitchToNextAsset() +{ + const int nextAsset = GetNextAsset(RequestedMasternodeAssets); + if (nextAsset == MASTERNODE_SYNC_FINISHED) { + LogPrintf("%s - Sync has finished\n", __func__); + } else if (nextAsset == MASTERNODE_SYNC_FAILED) { + ClearFulfilledRequest(); } + RequestedMasternodeAssets = nextAsset; RequestedMasternodeAttempt = 0; nAssetSyncStarted = GetTime(); } @@ -280,12 +290,15 @@ void CMasternodeSync::Process() LogPrint(BCLog::MASTERNODE, "CMasternodeSync::Process() - tick %d RequestedMasternodeAssets %d\n", tick, RequestedMasternodeAssets); - if (RequestedMasternodeAssets == MASTERNODE_SYNC_INITIAL) GetNextAsset(); + if (RequestedMasternodeAssets == MASTERNODE_SYNC_INITIAL) SwitchToNextAsset(); // sporks synced but blockchain is not, wait until we're almost at a recent block to continue if (!IsBlockchainSynced() && RequestedMasternodeAssets > MASTERNODE_SYNC_SPORKS) return; + // Skip after legacy obsolete. !TODO: remove when transition to DMN is complete + bool fLegacyMnObsolete = deterministicMNManager->LegacyMNObsolete(); + CMasternodeSync* sync = this; // New sync architecture, regtest only for now. @@ -297,12 +310,12 @@ void CMasternodeSync::Process() } // Mainnet sync - g_connman->ForEachNodeContinueIf([sync](CNode* pnode){ - return sync->SyncWithNode(pnode); + g_connman->ForEachNodeContinueIf([sync, fLegacyMnObsolete](CNode* pnode){ + return sync->SyncWithNode(pnode, fLegacyMnObsolete); }); } -bool CMasternodeSync::SyncWithNode(CNode* pnode) +bool CMasternodeSync::SyncWithNode(CNode* pnode, bool fLegacyMnObsolete) { CNetMsgMaker msgMaker(pnode->GetSendVersion()); @@ -312,16 +325,21 @@ bool CMasternodeSync::SyncWithNode(CNode* pnode) pnode->FulfilledRequest("getspork"); g_connman->PushMessage(pnode, msgMaker.Make(NetMsgType::GETSPORKS)); //get current network sporks - if (RequestedMasternodeAttempt >= 2) GetNextAsset(); + if (RequestedMasternodeAttempt >= 2) SwitchToNextAsset(); RequestedMasternodeAttempt++; return false; } if (pnode->nVersion >= ActiveProtocol()) { if (RequestedMasternodeAssets == MASTERNODE_SYNC_LIST) { + if (fLegacyMnObsolete) { + SwitchToNextAsset(); + return false; + } + LogPrint(BCLog::MASTERNODE, "CMasternodeSync::Process() - lastMasternodeList %lld (GetTime() - MASTERNODE_SYNC_TIMEOUT) %lld\n", lastMasternodeList, GetTime() - MASTERNODE_SYNC_TIMEOUT); if (lastMasternodeList > 0 && lastMasternodeList < GetTime() - MASTERNODE_SYNC_TIMEOUT * 2 && RequestedMasternodeAttempt >= MASTERNODE_SYNC_THRESHOLD) { //hasn't received a new item in the last five seconds, so we'll move to the - GetNextAsset(); + SwitchToNextAsset(); return false; } @@ -338,7 +356,7 @@ bool CMasternodeSync::SyncWithNode(CNode* pnode) lastFailure = GetTime(); nCountFailures++; } else { - GetNextAsset(); + SwitchToNextAsset(); } return false; } @@ -351,8 +369,13 @@ bool CMasternodeSync::SyncWithNode(CNode* pnode) } if (RequestedMasternodeAssets == MASTERNODE_SYNC_MNW) { + if (fLegacyMnObsolete) { + SwitchToNextAsset(); + return false; + } + if (lastMasternodeWinner > 0 && lastMasternodeWinner < GetTime() - MASTERNODE_SYNC_TIMEOUT * 2 && RequestedMasternodeAttempt >= MASTERNODE_SYNC_THRESHOLD) { //hasn't received a new item in the last five seconds, so we'll move to the - GetNextAsset(); + SwitchToNextAsset(); return false; } @@ -369,7 +392,7 @@ bool CMasternodeSync::SyncWithNode(CNode* pnode) lastFailure = GetTime(); nCountFailures++; } else { - GetNextAsset(); + SwitchToNextAsset(); } return false; } @@ -386,7 +409,7 @@ bool CMasternodeSync::SyncWithNode(CNode* pnode) // We'll start rejecting votes if we accidentally get set as synced too soon if (lastBudgetItem > 0 && lastBudgetItem < GetTime() - MASTERNODE_SYNC_TIMEOUT * 2 && RequestedMasternodeAttempt >= MASTERNODE_SYNC_THRESHOLD) { // Hasn't received a new item in the last five seconds, so we'll move to the - GetNextAsset(); + SwitchToNextAsset(); // Try to activate our masternode if possible activeMasternode.ManageStatus(); @@ -397,7 +420,7 @@ bool CMasternodeSync::SyncWithNode(CNode* pnode) if (lastBudgetItem == 0 && (RequestedMasternodeAttempt >= MASTERNODE_SYNC_THRESHOLD * 3 || GetTime() - nAssetSyncStarted > MASTERNODE_SYNC_TIMEOUT * 5)) { // maybe there is no budgets at all, so just finish syncing - GetNextAsset(); + SwitchToNextAsset(); activeMasternode.ManageStatus(); return false; } diff --git a/src/masternode-sync.h b/src/masternode-sync.h index 41b5dd0c083d..ea7b8add4d89 100644 --- a/src/masternode-sync.h +++ b/src/masternode-sync.h @@ -77,7 +77,7 @@ class CMasternodeSync void AddedMasternodeList(const uint256& hash); void AddedMasternodeWinner(const uint256& hash); void AddedBudgetItem(const uint256& hash); - void GetNextAsset(); + void SwitchToNextAsset(); std::string GetSyncStatus(); void ProcessMessage(CNode* pfrom, std::string& strCommand, CDataStream& vRecv); bool IsBudgetFinEmpty(); @@ -90,7 +90,7 @@ class CMasternodeSync * If it returns false, the Process() step is complete. * Otherwise Process() calls it again for a different node. */ - bool SyncWithNode(CNode* pnode); + bool SyncWithNode(CNode* pnode, bool fLegacyMnObsolete); bool IsSynced(); bool NotCompleted(); bool IsSporkListSynced(); @@ -108,6 +108,7 @@ class CMasternodeSync // Tier two sync node state // map of nodeID --> TierTwoPeerData std::map peersSyncState; + static int GetNextAsset(int currentAsset); void SyncRegtest(CNode* pnode); diff --git a/src/masternode.cpp b/src/masternode.cpp index a668f1008fb8..1c20bc17dd45 100644 --- a/src/masternode.cpp +++ b/src/masternode.cpp @@ -18,8 +18,8 @@ #define MASTERNODE_MIN_MNP_SECONDS_REGTEST 90 #define MASTERNODE_MIN_MNB_SECONDS_REGTEST 25 #define MASTERNODE_PING_SECONDS_REGTEST 25 -#define MASTERNODE_EXPIRATION_SECONDS_REGTEST 180 -#define MASTERNODE_REMOVAL_SECONDS_REGTEST 200 +#define MASTERNODE_EXPIRATION_SECONDS_REGTEST 12 * 60 +#define MASTERNODE_REMOVAL_SECONDS_REGTEST 13 * 60 #define MASTERNODE_MIN_CONFIRMATIONS 15 #define MASTERNODE_MIN_MNP_SECONDS (10 * 60) @@ -77,6 +77,7 @@ CMasternode::CMasternode() : protocolVersion = PROTOCOL_VERSION; nScanningErrorCount = 0; nLastScanningErrorBlockHeight = 0; + mnPayeeScript.clear(); } CMasternode::CMasternode(const CMasternode& other) : @@ -92,6 +93,23 @@ CMasternode::CMasternode(const CMasternode& other) : protocolVersion = other.protocolVersion; nScanningErrorCount = other.nScanningErrorCount; nLastScanningErrorBlockHeight = other.nLastScanningErrorBlockHeight; + mnPayeeScript = other.mnPayeeScript; +} + +CMasternode::CMasternode(const CDeterministicMNCPtr& dmn, int64_t registeredTime, const uint256& registeredHash) : + CSignedMessage() +{ + LOCK(cs); + vin = CTxIn(dmn->collateralOutpoint); + addr = dmn->pdmnState->addr; + pubKeyCollateralAddress = CPubKey(); + pubKeyMasternode = CPubKey(); + sigTime = registeredTime; + lastPing = CMasternodePing(vin, registeredHash, registeredTime); + protocolVersion = PROTOCOL_VERSION; + nScanningErrorCount = 0; + nLastScanningErrorBlockHeight = 0; + mnPayeeScript = dmn->pdmnState->scriptPayout; } uint256 CMasternode::GetSignatureHash() const @@ -660,3 +678,11 @@ void CMasternodePing::Relay() CInv inv(MSG_MASTERNODE_PING, GetHash()); g_connman->RelayInv(inv); } + +MasternodeRef MakeMasternodeRefForDMN(const CDeterministicMNCPtr& dmn) +{ + // create legacy masternode for DMN + int refHeight = std::max(dmn->pdmnState->nRegisteredHeight, dmn->pdmnState->nPoSeRevivedHeight); + const CBlockIndex* pindex = WITH_LOCK(cs_main, return mapBlockIndex.at(chainActive[refHeight]->GetBlockHash()); ); + return std::make_shared(CMasternode(dmn, pindex->GetBlockTime(), pindex->GetBlockHash())); +} diff --git a/src/masternode.h b/src/masternode.h index 048826fb393a..33977f3bedd5 100644 --- a/src/masternode.h +++ b/src/masternode.h @@ -22,6 +22,11 @@ class CMasternode; class CMasternodeBroadcast; class CMasternodePing; +typedef std::shared_ptr MasternodeRef; + +class CDeterministicMN; +typedef std::shared_ptr CDeterministicMNCPtr; + int MasternodeMinPingSeconds(); int MasternodeBroadcastSeconds(); int MasternodeCollateralMinConf(); @@ -116,6 +121,9 @@ class CMasternode : public CSignedMessage explicit CMasternode(); CMasternode(const CMasternode& other); + // Initialize from DMN. Used by the compatibility code. + CMasternode(const CDeterministicMNCPtr& dmn, int64_t registeredTime, const uint256& registeredHash); + // override CSignedMessage functions uint256 GetSignatureHash() const override; std::string GetStrMessage() const override; @@ -233,6 +241,20 @@ class CMasternode : public CSignedMessage /// Is the input associated with collateral public key? (and there is 10000 PIV - checking if valid masternode) bool IsInputAssociatedWithPubkey() const; + + /* + * This is used only by the compatibility code for DMN, which don't share the public key (but the keyid). + * Used by the payment-logic to include the necessary information in a temporary MasternodeRef object + * (which is not indexed in the maps of the legacy manager). + * A non-empty mnPayeeScript identifies this object as a "deterministic" masternode. + * Note: this is the single payout for the masternode (if the dmn is configured to pay a portion of the reward + * to the operator, this is done only after the disabling of the legacy system). + */ + CScript mnPayeeScript{}; + CScript GetPayeeScript() const { + return mnPayeeScript.empty() ? GetScriptForDestination(pubKeyCollateralAddress.GetID()) + : mnPayeeScript; + } }; @@ -281,4 +303,8 @@ class CMasternodeBroadcast : public CMasternode static bool CheckDefaultPort(CService service, std::string& strErrorRet, const std::string& strContext); }; +// Temporary function used for payment compatibility code. +// Returns a shared pointer to a masternode object initialized from a DMN. +MasternodeRef MakeMasternodeRefForDMN(const CDeterministicMNCPtr& dmn); + #endif diff --git a/src/masternodeman.cpp b/src/masternodeman.cpp index fd38d3e3873a..bf954a92c49e 100644 --- a/src/masternodeman.cpp +++ b/src/masternodeman.cpp @@ -226,8 +226,6 @@ void CMasternodeMan::AskForMN(CNode* pnode, const CTxIn& vin) int CMasternodeMan::CheckAndRemove(bool forceExpiredRemoval) { - LOCK(cs); - // Skip after legacy obsolete. !TODO: remove when transition to DMN is complete if (deterministicMNManager->LegacyMNObsolete()) { LogPrint(BCLog::MASTERNODE, "Removing all legacy mn due to SPORK 21\n"); @@ -235,6 +233,8 @@ int CMasternodeMan::CheckAndRemove(bool forceExpiredRemoval) return 0; } + LOCK(cs); + //remove inactive and outdated (or replaced by DMN) auto it = mapMasternodes.begin(); while (it != mapMasternodes.end()) { @@ -500,6 +500,12 @@ static bool canScheduleMN(bool fFilterSigTime, const MasternodeRef& mn, int minP // MasternodeRef CMasternodeMan::GetNextMasternodeInQueueForPayment(int nBlockHeight, bool fFilterSigTime, int& nCount, const CBlockIndex* pChainTip) const { + // Skip after legacy obsolete. !TODO: remove when transition to DMN is complete + if (deterministicMNManager->LegacyMNObsolete()) { + LogPrintf("%s: ERROR - called after legacy system disabled\n", __func__); + return nullptr; + } + AssertLockNotHeld(cs_main); const CBlockIndex* BlockReading = (pChainTip == nullptr ? GetChainTip() : pChainTip); if (!BlockReading) return nullptr; @@ -507,14 +513,19 @@ MasternodeRef CMasternodeMan::GetNextMasternodeInQueueForPayment(int nBlockHeigh MasternodeRef pBestMasternode = nullptr; std::vector > vecMasternodeLastPaid; + CDeterministicMNList mnList; + if (deterministicMNManager->IsDIP3Enforced()) { + mnList = deterministicMNManager->GetListAtChainTip(); + } + /* Make a vector with all of the last paid times */ int minProtocol = ActiveProtocol(); - int nMnCount{0}; + int nMnCount = mnList.GetValidMNsCount(); { LOCK(cs); - nMnCount = CountEnabled(); + nMnCount += CountEnabled(); for (const auto& it : mapMasternodes) { if (!it.second->IsEnabled()) continue; if (canScheduleMN(fFilterSigTime, it.second, minProtocol, nMnCount, nBlockHeight)) { @@ -522,6 +533,13 @@ MasternodeRef CMasternodeMan::GetNextMasternodeInQueueForPayment(int nBlockHeigh } } } + // Add deterministic masternodes to the vector + mnList.ForEachMN(true, [&](const CDeterministicMNCPtr& dmn) { + const MasternodeRef mn = MakeMasternodeRefForDMN(dmn); + if (canScheduleMN(fFilterSigTime, mn, minProtocol, nMnCount, nBlockHeight)) { + vecMasternodeLastPaid.emplace_back(SecondsSincePayment(mn, BlockReading), mn); + } + }); nCount = (int)vecMasternodeLastPaid.size(); @@ -537,7 +555,7 @@ MasternodeRef CMasternodeMan::GetNextMasternodeInQueueForPayment(int nBlockHeigh // -- (chance per block * chances before IsScheduled will fire) int nTenthNetwork = nMnCount / 10; int nCountTenth = 0; - arith_uint256 nHigh; + arith_uint256 nHigh = ARITH_UINT256_ZERO; const uint256& hash = GetHashAtHeight(nBlockHeight - 101); for (const auto& s: vecMasternodeLastPaid) { const MasternodeRef pmn = s.second; @@ -554,7 +572,7 @@ MasternodeRef CMasternodeMan::GetNextMasternodeInQueueForPayment(int nBlockHeigh return pBestMasternode; } -MasternodeRef CMasternodeMan::GetCurrentMasterNode(int nHeight, const uint256& hash) const +MasternodeRef CMasternodeMan::GetCurrentMasterNode(const uint256& hash) const { int minProtocol = ActiveProtocol(); int64_t score = 0; @@ -573,6 +591,21 @@ MasternodeRef CMasternodeMan::GetCurrentMasterNode(int nHeight, const uint256& h } } + // scan also dmns + if (deterministicMNManager->IsDIP3Enforced()) { + auto mnList = deterministicMNManager->GetListAtChainTip(); + mnList.ForEachMN(true, [&](const CDeterministicMNCPtr& dmn) { + const MasternodeRef mn = MakeMasternodeRefForDMN(dmn); + // calculate the score of the masternode + const int64_t n = mn->CalculateScore(hash).GetCompact(false); + // determine the winner + if (n > score) { + score = n; + winner = mn; + } + }); + } + return winner; } @@ -584,7 +617,7 @@ std::vector> CMasternodeMan::GetMnScores(int nLast for (int nHeight = nChainHeight - nLast; nHeight < nChainHeight + 20; nHeight++) { const uint256& hash = GetHashAtHeight(nHeight - 101); - MasternodeRef winner = GetCurrentMasterNode(nHeight, hash); + MasternodeRef winner = GetCurrentMasterNode(hash); if (winner) { ret.emplace_back(winner, nHeight); } @@ -620,6 +653,15 @@ int CMasternodeMan::GetMasternodeRank(const CTxIn& vin, int64_t nBlockHeight) co } } + // scan also dmns + if (deterministicMNManager->IsDIP3Enforced()) { + auto mnList = deterministicMNManager->GetListAtChainTip(); + mnList.ForEachMN(true, [&](const CDeterministicMNCPtr& dmn) { + const MasternodeRef mn = MakeMasternodeRefForDMN(dmn); + vecMasternodeScores.emplace_back(mn->CalculateScore(hash).GetCompact(false), mn->vin); + }); + } + sort(vecMasternodeScores.rbegin(), vecMasternodeScores.rend(), CompareScoreMN()); int rank = 0; @@ -644,13 +686,21 @@ std::vector> CMasternodeMan::GetMasternodeRank // scan for winner for (const auto& it : mapMasternodes) { const MasternodeRef mn = it.second; - if (!mn->IsEnabled()) { - vecMasternodeScores.emplace_back(9999, mn); - } else { - vecMasternodeScores.emplace_back(mn->CalculateScore(hash).GetCompact(false), mn); - } + const uint32_t score = mn->IsEnabled() ? mn->CalculateScore(hash).GetCompact(false) : 9999; + + vecMasternodeScores.emplace_back(score, mn); } } + // scan also dmns + if (deterministicMNManager->IsDIP3Enforced()) { + auto mnList = deterministicMNManager->GetListAtChainTip(); + mnList.ForEachMN(false, [&](const CDeterministicMNCPtr& dmn) { + const MasternodeRef mn = MakeMasternodeRefForDMN(dmn); + const uint32_t score = mnList.IsMNValid(dmn) ? mn->CalculateScore(hash).GetCompact(false) : 9999; + + vecMasternodeScores.emplace_back(score, mn); + }); + } sort(vecMasternodeScores.rbegin(), vecMasternodeScores.rend(), CompareScoreMN()); return vecMasternodeScores; } @@ -720,12 +770,6 @@ int CMasternodeMan::ProcessMNPing(CNode* pfrom, CMasternodePing& mnp) int CMasternodeMan::ProcessGetMNList(CNode* pfrom, CTxIn& vin) { - // Skip after legacy obsolete. !TODO: remove when transition to DMN is complete - if (deterministicMNManager->LegacyMNObsolete()) { - LogPrint(BCLog::MASTERNODE, "dseg - skip obsolete message\n"); - return 0; - } - if (vin.IsNull()) { //only should ask for this once //local network bool isLocal = (pfrom->addr.IsRFC1918() || pfrom->addr.IsLocal()); @@ -844,7 +888,7 @@ void CMasternodeMan::UpdateMasternodeList(CMasternodeBroadcast& mnb) mapSeenMasternodeBroadcast.emplace(mnb.GetHash(), mnb); masternodeSync.AddedMasternodeList(mnb.GetHash()); - LogPrint(BCLog::MASTERNODE,"CMasternodeMan::UpdateMasternodeList() -- masternode=%s\n", mnb.vin.prevout.ToString()); + LogPrint(BCLog::MASTERNODE,"%s -- masternode=%s\n", __func__, mnb.vin.prevout.ToString()); CMasternode* pmn = Find(mnb.vin.prevout); if (pmn == NULL) { @@ -874,8 +918,7 @@ int64_t CMasternodeMan::GetLastPaid(const MasternodeRef& mn, const CBlockIndex* { if (BlockReading == nullptr) return false; - CScript mnpayee; - mnpayee = GetScriptForDestination(mn->pubKeyCollateralAddress.GetID()); + const CScript& mnpayee = mn->GetPayeeScript(); CHashWriter ss(SER_GETHASH, PROTOCOL_VERSION); ss << mn->vin; diff --git a/src/masternodeman.h b/src/masternodeman.h index 1859304df0ee..9bc24a98428d 100644 --- a/src/masternodeman.h +++ b/src/masternodeman.h @@ -53,8 +53,6 @@ class CMasternodeDB ReadResult Read(CMasternodeMan& mnodemanToLoad); }; -// -typedef std::shared_ptr MasternodeRef; class CMasternodeMan { @@ -146,8 +144,8 @@ class CMasternodeMan /// Find an entry in the masternode list that is next to be paid MasternodeRef GetNextMasternodeInQueueForPayment(int nBlockHeight, bool fFilterSigTime, int& nCount, const CBlockIndex* pChainTip = nullptr) const; - /// Get the current winner for this block - MasternodeRef GetCurrentMasterNode(int nHeight, const uint256& hash) const; + /// Get the winner for this block hash + MasternodeRef GetCurrentMasterNode(const uint256& hash) const; /// vector of pairs std::vector> GetMnScores(int nLast) const; diff --git a/src/messagesigner.cpp b/src/messagesigner.cpp index 46df3e55b824..18e2d3a5e00d 100644 --- a/src/messagesigner.cpp +++ b/src/messagesigner.cpp @@ -19,7 +19,7 @@ bool CMessageSigner::GetKeysFromSecret(const std::string& strSecret, CKey& keyRe return false; pubkeyRet = keyRet.GetPubKey(); - return true; + return pubkeyRet.IsValid(); } bool CMessageSigner::GetKeysFromSecret(const std::string& strSecret, CKey& keyRet, CKeyID& keyIDRet) diff --git a/src/net_processing.cpp b/src/net_processing.cpp index a978b71b14e9..ae31ea4d6bbf 100644 --- a/src/net_processing.cpp +++ b/src/net_processing.cpp @@ -8,6 +8,7 @@ #include "budget/budgetmanager.h" #include "chain.h" +#include "evo/deterministicmns.h" #include "masternodeman.h" #include "masternode-payments.h" #include "masternode-sync.h" @@ -854,7 +855,8 @@ bool static PushTierTwoGetDataRequest(const CInv& inv, } } - if (inv.type == MSG_MASTERNODE_WINNER) { + // !TODO: remove when transition to DMN is complete + if (inv.type == MSG_MASTERNODE_WINNER && !deterministicMNManager->LegacyMNObsolete()) { if (masternodePayments.mapMasternodePayeeVotes.count(inv.hash)) { CDataStream ss(SER_NETWORK, PROTOCOL_VERSION); ss.reserve(1000); @@ -892,7 +894,8 @@ bool static PushTierTwoGetDataRequest(const CInv& inv, } } - if (inv.type == MSG_MASTERNODE_ANNOUNCE) { + // !TODO: remove when transition to DMN is complete + if (inv.type == MSG_MASTERNODE_ANNOUNCE && !deterministicMNManager->LegacyMNObsolete()) { if (mnodeman.mapSeenMasternodeBroadcast.count(inv.hash)) { CDataStream ss(SER_NETWORK, PROTOCOL_VERSION); ss.reserve(1000); @@ -902,7 +905,8 @@ bool static PushTierTwoGetDataRequest(const CInv& inv, } } - if (inv.type == MSG_MASTERNODE_PING) { + // !TODO: remove when transition to DMN is complete + if (inv.type == MSG_MASTERNODE_PING && !deterministicMNManager->LegacyMNObsolete()) { if (mnodeman.mapSeenMasternodePing.count(inv.hash)) { CDataStream ss(SER_NETWORK, PROTOCOL_VERSION); ss.reserve(1000); diff --git a/src/rpc/budget.cpp b/src/rpc/budget.cpp index 7fbdf7d4d1f9..0287afabea6e 100644 --- a/src/rpc/budget.cpp +++ b/src/rpc/budget.cpp @@ -8,6 +8,7 @@ #include "db.h" #include "init.h" #include "budget/budgetmanager.h" +#include "evo/deterministicmns.h" #include "masternode-payments.h" #include "masternode-sync.h" #include "masternodeconfig.h" @@ -217,7 +218,7 @@ UniValue submitbudget(const JSONRPCRequest& request) return proposal.GetHash().ToString(); } -UniValue packRetStatus(const std::string& nodeType, const std::string& result, const std::string& error) +static UniValue packRetStatus(const std::string& nodeType, const std::string& result, const std::string& error) { UniValue statusObj(UniValue::VOBJ); statusObj.pushKV("node", nodeType); @@ -226,78 +227,109 @@ UniValue packRetStatus(const std::string& nodeType, const std::string& result, c return statusObj; } -UniValue packErrorRetStatus(const std::string& nodeType, const std::string& error) +static UniValue packErrorRetStatus(const std::string& nodeType, const std::string& error) { return packRetStatus(nodeType, "failed", error); } -bool voteProposal(CPubKey& pubKeyMasternode, CKey& keyMasternode, const std::string& mnAlias, - const uint256& propHash, const CBudgetVote::VoteDirection& nVote, - UniValue& resultsObj) +static UniValue packVoteReturnValue(const UniValue& details, int success, int failed) { - CMasternode* pmn = mnodeman.Find(pubKeyMasternode); - if (!pmn) { - resultsObj.push_back(packErrorRetStatus(mnAlias, "Can't find masternode by pubkey")); - return false; - } - - CBudgetVote vote(pmn->vin, propHash, nVote); - if (!vote.Sign(keyMasternode, pubKeyMasternode.GetID())) { - resultsObj.push_back(packErrorRetStatus(mnAlias, "Failure to sign.")); - return false; - } - - std::string strError; - if (!g_budgetman.AddAndRelayProposalVote(vote, strError)) { - resultsObj.push_back(packErrorRetStatus(mnAlias, strError)); - return false; - } - - resultsObj.push_back(packRetStatus(mnAlias, "success", "")); - return true; + UniValue returnObj(UniValue::VOBJ); + returnObj.pushKV("overall", strprintf("Voted successfully %d time(s) and failed %d time(s).", success, failed)); + returnObj.pushKV("detail", details); + return returnObj; } -bool voteProposalMasternodeEntry(const CMasternodeConfig::CMasternodeEntry& mne, - const uint256& propHash, const CBudgetVote::VoteDirection& nVote, - UniValue& resultsObj) { - CPubKey pubKeyMasternode; - CKey keyMasternode; +// key, alias and collateral outpoint of a masternode. Struct used to sign proposal/budget votes +struct MnKeyData +{ + std::string mnAlias; + const COutPoint* collateralOut; + CKey key; + + MnKeyData(const std::string& _mnAlias, const COutPoint* _collateralOut, const CKey& _key): + mnAlias(_mnAlias), + collateralOut(_collateralOut), + key(_key) + {} +}; - if (!CMessageSigner::GetKeysFromSecret(mne.getPrivKey(), keyMasternode, pubKeyMasternode)) { - resultsObj.push_back( - packErrorRetStatus(mne.getAlias(), "Masternode signing error, could not set key correctly.")); - return false; +typedef std::list mnKeyList; + +static UniValue voteProposal(const uint256& propHash, const CBudgetVote::VoteDirection& nVote, + const mnKeyList& mnKeys, UniValue resultsObj, int failed) +{ + int success = 0; + for (const auto& k : mnKeys) { + CBudgetVote vote(CTxIn(*k.collateralOut), propHash, nVote); + if (!vote.Sign(k.key, k.key.GetPubKey().GetID())) { + resultsObj.push_back(packErrorRetStatus(k.mnAlias, "Failure to sign.")); + failed++; + continue; + } + CValidationState state; + if (!g_budgetman.ProcessProposalVote(vote, nullptr, state)) { + resultsObj.push_back(packErrorRetStatus(k.mnAlias, FormatStateMessage(state))); + failed++; + continue; + } + resultsObj.push_back(packRetStatus(k.mnAlias, "success", "")); + success++; } - return voteProposal(pubKeyMasternode, keyMasternode, mne.getAlias(), propHash, nVote, resultsObj); + return packVoteReturnValue(resultsObj, success, failed); } -UniValue packVoteReturnValue(const UniValue& details, int success, int failed) +static UniValue voteFinalBudget(const uint256& budgetHash, + const mnKeyList& mnKeys, UniValue resultsObj, int failed) { - UniValue returnObj(UniValue::VOBJ); - returnObj.pushKV("overall", strprintf("Voted successfully %d time(s) and failed %d time(s).", success, failed)); - returnObj.pushKV("detail", details); - return returnObj; + int success = 0; + for (const auto& k : mnKeys) { + CFinalizedBudgetVote vote(CTxIn(*k.collateralOut), budgetHash); + if (!vote.Sign(k.key, k.key.GetPubKey().GetID())) { + resultsObj.push_back(packErrorRetStatus(k.mnAlias, "Failure to sign.")); + failed++; + continue; + } + CValidationState state; + if (!g_budgetman.ProcessFinalizedBudgetVote(vote, nullptr, state)) { + resultsObj.push_back(packErrorRetStatus(k.mnAlias, FormatStateMessage(state))); + failed++; + continue; + } + resultsObj.push_back(packRetStatus(k.mnAlias, "success", "")); + success++; + } + + return packVoteReturnValue(resultsObj, success, failed); } -UniValue mnBudgetVoteInner(Optional mnAliasFilter, const uint256& propHash, - const CBudgetVote::VoteDirection& nVote) +// Legacy masternodes +static mnKeyList getMNKeys(const Optional& mnAliasFilter, + UniValue& resultsObj, int& failed) { - UniValue resultsObj(UniValue::VARR); - int success = 0; - int failed = 0; + mnKeyList mnKeys; for (const CMasternodeConfig::CMasternodeEntry& mne : masternodeConfig.getEntries()) { if (mnAliasFilter && *mnAliasFilter != mne.getAlias()) continue; - if (!voteProposalMasternodeEntry(mne, propHash, nVote, resultsObj)) { + CKey mnKey; CPubKey mnPubKey; + const std::string& mnAlias = mne.getAlias(); + if (!CMessageSigner::GetKeysFromSecret(mne.getPrivKey(), mnKey, mnPubKey)) { + resultsObj.push_back(packErrorRetStatus(mnAlias, "Could not get key from masternode.conf")); + failed++; + continue; + } + CMasternode* pmn = mnodeman.Find(mnPubKey); + if (!pmn) { + resultsObj.push_back(packErrorRetStatus(mnAlias, "Can't find masternode by pubkey")); failed++; - } else { - success++; + continue; } + mnKeys.emplace_back(mnAlias, &pmn->vin.prevout, mnKey); } - return packVoteReturnValue(resultsObj, success, failed); + return mnKeys; } -UniValue mnLocalBudgetVoteInner(const uint256& propHash, const CBudgetVote::VoteDirection& nVote) +static mnKeyList getMNKeysForActiveMasternode(UniValue& resultsObj) { // local node must be a masternode if (!fMasterNode) @@ -306,17 +338,114 @@ UniValue mnLocalBudgetVoteInner(const uint256& propHash, const CBudgetVote::Vote if (activeMasternode.vin == nullopt) throw JSONRPCError(RPC_MISC_ERROR, _("Active Masternode not initialized.")); - UniValue returnObj(UniValue::VOBJ); + CKey mnKey; CPubKey mnPubKey; + activeMasternode.GetKeys(mnKey, mnPubKey); + CMasternode* pmn = mnodeman.Find(mnPubKey); + if (!pmn) { + resultsObj.push_back(packErrorRetStatus("local", "Can't find masternode by pubkey")); + return mnKeyList(); + } + + return {MnKeyData("local", &pmn->vin.prevout, mnKey)}; +} + +// Deterministic masternodes +static mnKeyList getDMNKeys(CWallet* const pwallet, const Optional& mnAliasFilter, bool fFinal, UniValue& resultsObj, int& failed) +{ + if (!pwallet) { + throw JSONRPCError(RPC_IN_WARMUP, "Wallet (with voting key) not found."); + } + + auto mnList = deterministicMNManager->GetListAtChainTip(); + + CDeterministicMNCPtr mnFilter{nullptr}; + if (mnAliasFilter) { + // vote with a single masternode (identified by ProTx) + const uint256& proTxHash = ParseHashV(*mnAliasFilter, "ProTX transaction hash"); + mnFilter = mnList.GetValidMN(proTxHash); + if (!mnFilter) { + resultsObj.push_back(packErrorRetStatus(*mnAliasFilter, "Invalid or unknown proTxHash")); + failed++; + return mnKeyList(); + } + } + + LOCK(pwallet->cs_wallet); + EnsureWalletIsUnlocked(pwallet); + + mnKeyList mnKeys; + mnList.ForEachMN(true, [&](const CDeterministicMNCPtr& dmn) { + bool filtered = mnFilter && dmn->proTxHash == mnFilter->proTxHash; + if (!mnFilter || filtered) { + const CKeyID& mnKeyID = fFinal ? dmn->pdmnState->keyIDOperator : dmn->pdmnState->keyIDVoting; + CKey mnKey; + if (pwallet->GetKey(mnKeyID, mnKey)) { + mnKeys.emplace_back(dmn->proTxHash.ToString(), &dmn->collateralOutpoint, mnKey); + } else if (filtered) { + resultsObj.push_back(packErrorRetStatus(*mnAliasFilter, strprintf( + "Private key for voting address %s not known by this wallet", EncodeDestination(mnKeyID)))); + failed++; + } + } + }); + + return mnKeys; +} + +static mnKeyList getDMNKeysForActiveMasternode(UniValue& resultsObj) +{ + // local node must be a masternode + if (!activeMasternodeManager) + throw JSONRPCError(RPC_MISC_ERROR, _("This is not a deterministic masternode. 'local' option disabled.")); + + CKey dmnKey; CKeyID dmnKeyID; CDeterministicMNCPtr dmn; + auto res = activeMasternodeManager->GetOperatorKey(dmnKey, dmnKeyID, dmn); + if (!res) { + resultsObj.push_back(packErrorRetStatus("local", res.getError())); + return {}; + } + + return {MnKeyData("local", &dmn->collateralOutpoint, dmnKey)}; +} + +// vote on proposal (finalized budget, if fFinal=true) with all possible keys or a single mn (mnAliasFilter) +static UniValue mnBudgetVoteInner(CWallet* const pwallet, bool fLegacyMN, const uint256& budgetHash, bool fFinal, + const CBudgetVote::VoteDirection& nVote, const Optional& mnAliasFilter) +{ + UniValue resultsObj(UniValue::VARR); + int failed = 0; + + mnKeyList mnKeys = fLegacyMN ? getMNKeys(mnAliasFilter, resultsObj, failed) + : getDMNKeys(pwallet, mnAliasFilter, fFinal, resultsObj, failed); + + if (mnKeys.empty()) { + return packVoteReturnValue(resultsObj, 0, failed); + } + + return (fFinal ? voteFinalBudget(budgetHash, mnKeys, resultsObj, failed) + : voteProposal(budgetHash, nVote, mnKeys, resultsObj, failed)); +} + +// vote on proposal (finalized budget, if fFinal=true) with the active local masternode +// Note: for DMNs only finalized budget voting is allowed with the operator key +// (proposal voting requires the voting key) +static UniValue mnLocalBudgetVoteInner(bool fLegacyMN, const uint256& budgetHash, bool fFinal, + const CBudgetVote::VoteDirection& nVote) +{ UniValue resultsObj(UniValue::VARR); - // Get MN keys - CPubKey pubKeyMasternode; - CKey keyMasternode; - activeMasternode.GetKeys(keyMasternode, pubKeyMasternode); - bool ret = voteProposal(pubKeyMasternode, keyMasternode, "local", propHash, nVote, resultsObj); - return packVoteReturnValue(resultsObj, ret, !ret); + + mnKeyList mnKeys = fLegacyMN ? getMNKeysForActiveMasternode(resultsObj) + : getDMNKeysForActiveMasternode(resultsObj); + + if (mnKeys.empty()) { + return packVoteReturnValue(resultsObj, 0, 1); + } + + return (fFinal ? voteFinalBudget(budgetHash, mnKeys, resultsObj, 0) + : voteProposal(budgetHash, nVote, mnKeys, resultsObj, 0)); } -CBudgetVote::VoteDirection parseVote(const std::string& strVote) +static CBudgetVote::VoteDirection parseVote(const std::string& strVote) { if (strVote != "yes" && strVote != "no") throw JSONRPCError(RPC_MISC_ERROR, "You can only vote 'yes' or 'no'"); CBudgetVote::VoteDirection nVote = CBudgetVote::VOTE_ABSTAIN; @@ -337,17 +466,21 @@ UniValue mnbudgetvote(const JSONRPCRequest& request) if (strCommand == "vote-alias") strCommand = "alias"; } + CWallet * const pwallet = GetWalletForJSONRPCRequest(request); + if (request.fHelp || (request.params.size() == 3 && (strCommand != "local" && strCommand != "many")) || (request.params.size() == 4 && strCommand != "alias") || - request.params.size() > 4 || request.params.size() < 3) + request.params.size() > 5 || request.params.size() < 3) throw std::runtime_error( - "mnbudgetvote \"local|many|alias\" \"votehash\" \"yes|no\" ( \"alias\" )\n" + "mnbudgetvote \"local|many|alias\" \"votehash\" \"yes|no\" ( \"alias\" legacy )\n" "\nVote on a budget proposal\n" + "\nAfter V6 enforcement, the deterministic masternode system is used by default. Set the \"legacy\" parameter to true to vote with legacy masternodes." "\nArguments:\n" "1. \"mode\" (string, required) The voting mode. 'local' for voting directly from a masternode, 'many' for voting with a MN controller and casting the same vote for each MN, 'alias' for voting with a MN controller and casting a vote for a single MN\n" "2. \"votehash\" (string, required) The vote hash for the proposal\n" "3. \"votecast\" (string, required) Your vote. 'yes' to vote for the proposal, 'no' to vote against\n" - "4. \"alias\" (string, required for 'alias' mode) The MN alias to cast a vote for.\n" + "4. \"alias\" (string, required for 'alias' mode) The MN alias to cast a vote for (for deterministic masternodes it's the hash of the proTx transaction).\n" + "5. \"legacy\" (boolean, optional, default=false) Use the legacy masternode system after deterministic masternodes enforcement.\n" "\nResult:\n" "{\n" @@ -363,20 +496,30 @@ UniValue mnbudgetvote(const JSONRPCRequest& request) "}\n" "\nExamples:\n" + - HelpExampleCli("mnbudgetvote", "\"local\" \"ed2f83cedee59a91406f5f47ec4d60bf5a7f9ee6293913c82976bd2d3a658041\" \"yes\"") + - HelpExampleRpc("mnbudgetvote", "\"local\" \"ed2f83cedee59a91406f5f47ec4d60bf5a7f9ee6293913c82976bd2d3a658041\" \"yes\"")); + HelpExampleCli("mnbudgetvote", "\"alias\" \"ed2f83cedee59a91406f5f47ec4d60bf5a7f9ee6293913c82976bd2d3a658041\" \"yes\" \"4f9de28fca1f0574a217c5d3c59cc51125ec671de82a2f80b6ceb69673115041\"") + + HelpExampleRpc("mnbudgetvote", "\"alias\" \"ed2f83cedee59a91406f5f47ec4d60bf5a7f9ee6293913c82976bd2d3a658041\" \"yes\" \"4f9de28fca1f0574a217c5d3c59cc51125ec671de82a2f80b6ceb69673115041\"")); const uint256& hash = ParseHashV(request.params[1], "parameter 1"); CBudgetVote::VoteDirection nVote = parseVote(request.params[2].get_str()); + bool fLegacyMN = !deterministicMNManager->IsDIP3Enforced() || (request.params.size() > 4 && request.params[4].get_bool()); + if (strCommand == "local") { - return mnLocalBudgetVoteInner(hash, nVote); + if (!fLegacyMN) { + throw JSONRPCError(RPC_MISC_ERROR, _("\"local\" vote is no longer available with DMNs. Use \"alias\" from the wallet with the voting key.")); + } + return mnLocalBudgetVoteInner(true, hash, false, nVote); + } + + // DMN require wallet with voting key + if (!fLegacyMN && !EnsureWalletIsAvailable(pwallet, false)) { + return NullUniValue; } bool isAlias = false; if (strCommand == "many" || (isAlias = strCommand == "alias")) { Optional mnAlias = isAlias ? Optional(request.params[3].get_str()) : nullopt; - return mnBudgetVoteInner(mnAlias, hash, nVote); + return mnBudgetVoteInner(pwallet, fLegacyMN, hash, false, nVote, mnAlias); } return NullUniValue; @@ -633,8 +776,10 @@ UniValue mnfinalbudget(const JSONRPCRequest& request) if (request.params.size() >= 1) strCommand = request.params[0].get_str(); + CWallet * const pwallet = GetWalletForJSONRPCRequest(request); + if (request.fHelp || - (strCommand != "suggest" && strCommand != "vote-many" && strCommand != "vote" && strCommand != "show" && strCommand != "getvotes")) + (strCommand != "vote-many" && strCommand != "vote" && strCommand != "show" && strCommand != "getvotes")) throw std::runtime_error( "mnfinalbudget \"command\"... ( \"passphrase\" )\n" "\nVote or show current budgets\n" @@ -645,111 +790,20 @@ UniValue mnfinalbudget(const JSONRPCRequest& request) " show - Show existing finalized budgets\n" " getvotes - Get vote information for each finalized budget\n"); - if (strCommand == "vote-many") { - if (request.params.size() != 2) - throw std::runtime_error("Correct usage is 'mnfinalbudget vote-many BUDGET_HASH'"); - - std::string strHash = request.params[1].get_str(); - uint256 hash(uint256S(strHash)); - - int success = 0; - int failed = 0; - - UniValue resultsObj(UniValue::VOBJ); - - for (CMasternodeConfig::CMasternodeEntry mne : masternodeConfig.getEntries()) { - std::vector vchMasterNodeSignature; - std::string strMasterNodeSignMessage; - - CPubKey pubKeyCollateralAddress; - CKey keyCollateralAddress; - CPubKey pubKeyMasternode; - CKey keyMasternode; - - UniValue statusObj(UniValue::VOBJ); - - if (!CMessageSigner::GetKeysFromSecret(mne.getPrivKey(), keyMasternode, pubKeyMasternode)) { - failed++; - statusObj.pushKV("result", "failed"); - statusObj.pushKV("errorMessage", "Masternode signing error, could not set key correctly."); - resultsObj.pushKV(mne.getAlias(), statusObj); - continue; - } - - CMasternode* pmn = mnodeman.Find(pubKeyMasternode); - if (pmn == NULL) { - failed++; - statusObj.pushKV("result", "failed"); - statusObj.pushKV("errorMessage", "Can't find masternode by pubkey"); - resultsObj.pushKV(mne.getAlias(), statusObj); - continue; - } - - - CFinalizedBudgetVote vote(pmn->vin, hash); - if (!vote.Sign(keyMasternode, pubKeyMasternode.GetID())) { - failed++; - statusObj.pushKV("result", "failed"); - statusObj.pushKV("errorMessage", "Failure to sign."); - resultsObj.pushKV(mne.getAlias(), statusObj); - continue; - } - - std::string strError = ""; - if (g_budgetman.UpdateFinalizedBudget(vote, NULL, strError)) { - g_budgetman.AddSeenFinalizedBudgetVote(vote); - vote.Relay(); - success++; - statusObj.pushKV("result", "success"); - } else { - failed++; - statusObj.pushKV("result", strError.c_str()); - } - - resultsObj.pushKV(mne.getAlias(), statusObj); - } - - UniValue returnObj(UniValue::VOBJ); - returnObj.pushKV("overall", strprintf("Voted successfully %d time(s) and failed %d time(s).", success, failed)); - returnObj.pushKV("detail", resultsObj); - - return returnObj; - } - - if (strCommand == "vote") { - if (!fMasterNode) - throw JSONRPCError(RPC_MISC_ERROR, _("This is not a masternode. 'local' option disabled.")); - - if (activeMasternode.vin == nullopt) - throw JSONRPCError(RPC_MISC_ERROR, _("Active Masternode not initialized.")); - - if (request.params.size() != 2) - throw std::runtime_error("Correct usage is 'mnfinalbudget vote BUDGET_HASH'"); - - std::string strHash = request.params[1].get_str(); - uint256 hash(uint256S(strHash)); - - CPubKey pubKeyMasternode; CKey keyMasternode; - activeMasternode.GetKeys(keyMasternode, pubKeyMasternode); - - CMasternode* pmn = mnodeman.Find(activeMasternode.vin->prevout); - if (pmn == NULL) { - return "Failure to find masternode in list : " + activeMasternode.vin->ToString(); + if (strCommand == "vote-many" || strCommand == "vote") { + if (request.params.size() < 2 || request.params.size() > 3) { + throw std::runtime_error(strprintf("Correct usage is 'mnfinalbudget %s BUDGET_HASH (fLegacy)'", strCommand)); } + const uint256& hash = ParseHashV(request.params[1], "BUDGET_HASH"); + bool fLegacyMN = !deterministicMNManager->IsDIP3Enforced() || (request.params.size() > 2 && request.params[2].get_bool()); - CFinalizedBudgetVote vote(*(activeMasternode.vin), hash); - if (!vote.Sign(keyMasternode, pubKeyMasternode.GetID())) { - return "Failure to sign."; + // DMN require wallet with operator keys for vote-many + if (!fLegacyMN && strCommand == "vote-many" && !EnsureWalletIsAvailable(pwallet, false)) { + return NullUniValue; } - std::string strError = ""; - if (g_budgetman.UpdateFinalizedBudget(vote, NULL, strError)) { - g_budgetman.AddSeenFinalizedBudgetVote(vote); - vote.Relay(); - return "success"; - } else { - return "Error voting : " + strError; - } + return (strCommand == "vote-many" ? mnBudgetVoteInner(pwallet, fLegacyMN, hash, true, CBudgetVote::VOTE_YES, nullopt) + : mnLocalBudgetVoteInner(fLegacyMN, hash, true, CBudgetVote::VOTE_YES)); } if (strCommand == "show") { diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index b70e6ae48f80..e31a6e1ea58d 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -120,6 +120,7 @@ static const CRPCConvertParam vRPCConvertParams[] = { { "importpubkey", 2 }, { "importmulti", 0 }, { "importmulti", 1 }, + { "initmasternode", 2}, { "exportsaplingkey", 1 }, { "importsaplingkey", 2 }, { "importsaplingviewingkey", 2 }, @@ -144,6 +145,7 @@ static const CRPCConvertParam vRPCConvertParams[] = { // TODO: remove this and switch over to proper arg parsing in rpc/masternode.cpp for the second argument //{"startmasternode", 1}, { "startmasternode", 3 }, + { "mnbudgetvote", 4 }, { "mnvoteraw", 1 }, { "mnvoteraw", 4 }, { "setstakesplitthreshold", 0 }, diff --git a/src/rpc/masternode.cpp b/src/rpc/masternode.cpp index a91bf2812b30..e1fcc5be4903 100644 --- a/src/rpc/masternode.cpp +++ b/src/rpc/masternode.cpp @@ -55,13 +55,14 @@ UniValue mnping(const JSONRPCRequest& request) UniValue initmasternode(const JSONRPCRequest& request) { - if (request.fHelp || (request.params.empty() || request.params.size() > 2)) { + if (request.fHelp || (request.params.size() < 2|| request.params.size() > 3)) { throw std::runtime_error( - "initmasternode ( \"masternodePrivKey\" \"masternodeAddr\" )\n" + "initmasternode \"masternodePrivKey\" \"masternodeAddr\" ( deterministic )\n" "\nInitialize masternode on demand if it's not already initialized.\n" "\nArguments:\n" "1. masternodePrivKey (string, required) The masternode private key.\n" "2. masternodeAddr (string, required) The IP:Port of this masternode.\n" + "3. deterministic (boolean, optional, default=false) Init as DMN.\n" "\nResult:\n" " success (string) if the masternode initialization succeeded.\n" @@ -73,6 +74,21 @@ UniValue initmasternode(const JSONRPCRequest& request) std::string _strMasterNodePrivKey = request.params[0].get_str(); std::string _strMasterNodeAddr = request.params[1].get_str(); + bool fDeterministic = request.params.size() > 2 && request.params[2].get_bool(); + if (fDeterministic) { + if (!activeMasternodeManager) { + activeMasternodeManager = new CActiveDeterministicMasternodeManager(); + RegisterValidationInterface(activeMasternodeManager); + } + auto res = activeMasternodeManager->SetOperatorKey(_strMasterNodePrivKey); + if (!res) throw std::runtime_error(res.getError()); + activeMasternodeManager->Init(); + if (activeMasternodeManager->GetState() == CActiveDeterministicMasternodeManager::MASTERNODE_ERROR) { + throw std::runtime_error(activeMasternodeManager->GetStatus()); + } + return "success"; + } + // legacy auto res = initMasternode(_strMasterNodePrivKey, _strMasterNodeAddr, false); if (!res) throw std::runtime_error(res.getError()); return "success"; @@ -134,13 +150,14 @@ UniValue listmasternodes(const JSONRPCRequest& request) "\nResult:\n" "[\n" " {\n" - " \"rank\": n, (numeric) Masternode Rank (or 0 if not enabled)\n" - " \"txhash\": \"hash\", (string) Collateral transaction hash\n" - " \"outidx\": n, (numeric) Collateral transaction output index\n" - " \"pubkey\": \"key\", (string) Masternode public key used for message broadcasting\n" - " \"status\": s, (string) Status (ENABLED/EXPIRED/REMOVE/etc)\n" - " \"addr\": \"addr\", (string) Masternode PIVX address\n" - " \"version\": v, (numeric) Masternode protocol version\n" + " \"rank\": n, (numeric) Masternode Rank (or 0 if not enabled)\n" + " \"type\": \"legacy\"|\"deterministic\", (string) type of masternode\n" + " \"txhash\": \"hash\", (string) Collateral transaction hash\n" + " \"outidx\": n, (numeric) Collateral transaction output index\n" + " \"pubkey\": \"key\", (string) Masternode public key used for message broadcasting\n" + " \"status\": s, (string) Status (ENABLED/EXPIRED/REMOVE/etc)\n" + " \"addr\": \"addr\", (string) Masternode PIVX address\n" + " \"version\": v, (numeric) Masternode protocol version\n" " \"lastseen\": ttt, (numeric) The time in seconds since epoch (Jan 1 1970 GMT) of the last seen\n" " \"activetime\": ttt, (numeric) The time in seconds since epoch (Jan 1 1970 GMT) masternode has been active\n" " \"lastpaid\": ttt, (numeric) The time in seconds since epoch (Jan 1 1970 GMT) masternode was last paid\n" @@ -155,24 +172,24 @@ UniValue listmasternodes(const JSONRPCRequest& request) const std::string& strFilter = request.params.size() > 0 ? request.params[0].get_str() : ""; UniValue ret(UniValue::VARR); - auto mnList = deterministicMNManager->GetListAtChainTip(); - mnList.ForEachMN(false, [&](const CDeterministicMNCPtr& dmn) { - UniValue obj(UniValue::VOBJ); - dmn->ToJson(obj); - bool fEnabled = dmn->pdmnState->nPoSeBanHeight == -1; - if (filterMasternode(obj, strFilter, fEnabled)) { - // Added for backward compatibility. can be removed later. - obj.pushKV("txhash", obj["proTxHash"].get_str()); - - obj.pushKV("status", fEnabled ? "ENABLED" : "POSE_BANNED"); - ret.push_back(obj); - } - }); + if (deterministicMNManager->LegacyMNObsolete()) { + auto mnList = deterministicMNManager->GetListAtChainTip(); + mnList.ForEachMN(false, [&](const CDeterministicMNCPtr& dmn) { + UniValue obj(UniValue::VOBJ); + dmn->ToJson(obj); + bool fEnabled = dmn->pdmnState->nPoSeBanHeight == -1; + if (filterMasternode(obj, strFilter, fEnabled)) { + ret.push_back(obj); + } + }); + return ret; + } // Legacy masternodes (!TODO: remove when transition to dmn is complete) const CBlockIndex* chainTip = GetChainTip(); if (!chainTip) return "[]"; int nHeight = chainTip->nHeight; + auto mnList = deterministicMNManager->GetListAtChainTip(); std::vector> vMasternodeRanks = mnodeman.GetMasternodeRanks(nHeight); for (int pos=0; pos < (int) vMasternodeRanks.size(); pos++) { @@ -180,6 +197,26 @@ UniValue listmasternodes(const JSONRPCRequest& request) UniValue obj(UniValue::VOBJ); const CMasternode& mn = *(s.second); + if (!mn.mnPayeeScript.empty()) { + // Deterministic masternode + auto dmn = mnList.GetMNByCollateral(mn.vin.prevout); + if (dmn) { + UniValue obj(UniValue::VOBJ); + dmn->ToJson(obj); + bool fEnabled = dmn->pdmnState->nPoSeBanHeight == -1; + if (filterMasternode(obj, strFilter, fEnabled)) { + // Added for backward compatibility with legacy masternodes + obj.pushKV("type", "deterministic"); + obj.pushKV("txhash", obj["proTxHash"].get_str()); + obj.pushKV("addr", obj["dmnstate"]["payoutAddress"].get_str()); + obj.pushKV("status", fEnabled ? "ENABLED" : "POSE_BANNED"); + obj.pushKV("rank", fEnabled ? pos : 0); + ret.push_back(obj); + } + } + continue; + } + std::string strVin = mn.vin.prevout.ToStringShort(); std::string strTxHash = mn.vin.prevout.hash.ToString(); uint32_t oIdx = mn.vin.prevout.n; @@ -197,10 +234,11 @@ UniValue listmasternodes(const JSONRPCRequest& request) std::string strNetwork = GetNetworkName(node.GetNetwork()); obj.pushKV("rank", (strStatus == "ENABLED" ? pos : 0)); + obj.pushKV("type", "legacy"); obj.pushKV("network", strNetwork); obj.pushKV("txhash", strTxHash); obj.pushKV("outidx", (uint64_t)oIdx); - obj.pushKV("pubkey", HexStr(mn.pubKeyMasternode)); + obj.pushKV("pubkey", EncodeDestination(mn.pubKeyMasternode.GetID())); obj.pushKV("status", strStatus); obj.pushKV("addr", EncodeDestination(mn.pubKeyCollateralAddress.GetID())); obj.pushKV("version", mn.protocolVersion); @@ -490,8 +528,9 @@ UniValue startmasternode(const JSONRPCRequest& request) pwallet->Lock(); if(!found) { - statusObj.pushKV("success", false); - statusObj.pushKV("error_message", "Could not find alias in config. Verify with listmasternodeconf."); + statusObj.pushKV("alias", alias); + statusObj.pushKV("result", "failed"); + statusObj.pushKV("error", "Could not find alias in config. Verify with listmasternodeconf."); } return statusObj; @@ -629,7 +668,7 @@ UniValue getmasternodestatus (const JSONRPCRequest& request) "getmasternodestatus\n" "\nPrint masternode status\n" - "\nResult:\n" + "\nResult (if legacy masternode):\n" "{\n" " \"txhash\": \"xxxx\", (string) Collateral transaction hash\n" " \"outputidx\": n, (numeric) Collateral transaction output index number\n" @@ -638,6 +677,11 @@ UniValue getmasternodestatus (const JSONRPCRequest& request) " \"status\": \"xxxx\", (string) Masternode status\n" " \"message\": \"xxxx\" (string) Masternode status message\n" "}\n" + "\n" + "\nResult (if deterministic masternode):\n" + "{\n" + "... !TODO ...\n" + "}\n" "\nExamples:\n" + HelpExampleCli("getmasternodestatus", "") + HelpExampleRpc("getmasternodestatus", "")); @@ -645,8 +689,33 @@ UniValue getmasternodestatus (const JSONRPCRequest& request) if (!fMasterNode) throw JSONRPCError(RPC_MISC_ERROR, _("This is not a masternode.")); - if (activeMasternode.vin == nullopt) + bool fLegacyMN = (activeMasternode.vin != nullopt); + bool fDeterministicMN = (activeMasternodeManager != nullptr); + + if (!fLegacyMN && !fDeterministicMN) { throw JSONRPCError(RPC_MISC_ERROR, _("Active Masternode not initialized.")); + } + + if (fDeterministicMN) { + if (!deterministicMNManager->IsDIP3Enforced()) { + // this should never happen as ProTx transactions are not accepted yet + throw JSONRPCError(RPC_MISC_ERROR, _("Deterministic masternodes are not enforced yet")); + } + const CActiveMasternodeInfo* amninfo = activeMasternodeManager->GetInfo(); + UniValue mnObj(UniValue::VOBJ); + auto dmn = deterministicMNManager->GetListAtChainTip().GetMNByOperatorKey(amninfo->keyIDOperator); + if (dmn) { + dmn->ToJson(mnObj); + } + mnObj.pushKV("netaddr", amninfo->service.ToString()); + mnObj.pushKV("status", activeMasternodeManager->GetStatus()); + return mnObj; + } + + // Legacy code !TODO: remove when transition to DMN is complete + if (deterministicMNManager->LegacyMNObsolete()) { + throw JSONRPCError(RPC_MISC_ERROR, _("Legacy Masternode is obsolete.")); + } CMasternode* pmn = mnodeman.Find(activeMasternode.vin->prevout); diff --git a/src/rpc/rpcevo.cpp b/src/rpc/rpcevo.cpp index a593bcae1a4d..4c348f812b1f 100644 --- a/src/rpc/rpcevo.cpp +++ b/src/rpc/rpcevo.cpp @@ -207,7 +207,6 @@ static CKeyID ParsePubKeyIDFromAddress(const std::string& strAddress) template static void FundSpecialTx(CWallet* pwallet, CMutableTransaction& tx, SpecialTxPayload& payload) { - assert(pwallet != nullptr); SetTxPayload(tx, payload); static CTxOut dummyTxOut(0, CScript() << OP_RETURN); diff --git a/src/tiertwo_networksync.cpp b/src/tiertwo_networksync.cpp index ce946550cee5..0183a5ec7f61 100644 --- a/src/tiertwo_networksync.cpp +++ b/src/tiertwo_networksync.cpp @@ -4,11 +4,11 @@ #include "masternode-sync.h" -#include "spork.h" // for sporkManager -#include "masternodeman.h" // for mnodeman +#include "masternodeman.h" // for mnodeman #include "netmessagemaker.h" -#include "net_processing.h" // for Misbehaving -#include "streams.h" // for CDataStream +#include "net_processing.h" // for Misbehaving +#include "spork.h" // for sporkManager +#include "streams.h" // for CDataStream // Update in-flight message status if needed @@ -71,7 +71,7 @@ bool CMasternodeSync::MessageDispatcher(CNode* pfrom, std::string& strCommand, C return true; } // All good, Update in-flight message status if needed - if (!UpdatePeerSyncState(pfrom->GetId(), NetMsgType::GETSPORKS, MASTERNODE_SYNC_LIST)) { + if (!UpdatePeerSyncState(pfrom->GetId(), NetMsgType::GETSPORKS, GetNextAsset(MASTERNODE_SYNC_SPORKS))) { // This could happen because of the message thread is requesting the sporks alone.. // So.. for now, can just update the peer status and move it to the next state if the end message arrives if (spork.nSporkID == SPORK_INVALID) { @@ -96,17 +96,17 @@ bool CMasternodeSync::MessageDispatcher(CNode* pfrom, std::string& strCommand, C // this means we will receive no further communication on the first sync switch (nItemID) { case MASTERNODE_SYNC_LIST: { - UpdatePeerSyncState(pfrom->GetId(), NetMsgType::GETMNLIST, MASTERNODE_SYNC_MNW); + UpdatePeerSyncState(pfrom->GetId(), NetMsgType::GETMNLIST, GetNextAsset(nItemID)); return true; } case MASTERNODE_SYNC_MNW: { - UpdatePeerSyncState(pfrom->GetId(), NetMsgType::GETMNWINNERS, MASTERNODE_SYNC_BUDGET); + UpdatePeerSyncState(pfrom->GetId(), NetMsgType::GETMNWINNERS, GetNextAsset(nItemID)); return true; } case MASTERNODE_SYNC_BUDGET_PROP: { // TODO: This could be a MASTERNODE_SYNC_BUDGET_FIN as well, possibly should decouple the finalization budget sync // from the MASTERNODE_SYNC_BUDGET_PROP (both are under the BUDGETVOTESYNC message) - UpdatePeerSyncState(pfrom->GetId(), NetMsgType::BUDGETVOTESYNC, MASTERNODE_SYNC_FINISHED); + UpdatePeerSyncState(pfrom->GetId(), NetMsgType::BUDGETVOTESYNC, GetNextAsset(nItemID)); return true; } case MASTERNODE_SYNC_BUDGET_FIN: { diff --git a/test/functional/test_framework/messages.py b/test/functional/test_framework/messages.py index ffd5d42bfe4a..2b8e684f4c95 100644 --- a/test/functional/test_framework/messages.py +++ b/test/functional/test_framework/messages.py @@ -1372,3 +1372,25 @@ def serialize(self): r += self.block_transactions.serialize(with_witness=True) return r + +# PIVX Classes +class Masternode(object): + def __init__(self, idx, owner_addr, operator_addr, voting_addr, ipport, payout_addr, operator_key): + self.idx = idx + self.owner = owner_addr + self.operator = operator_addr + self.voting = voting_addr + self.ipport = ipport + self.payee = payout_addr + self.operator_key = operator_key + self.proTx = None + self.collateral = None + + def __repr__(self): + return "Masternode(idx=%d, owner=%s, operator=%s, voting=%s, ip=%s, payee=%s, opkey=%s, protx=%s, collateral=%s)" % ( + self.idx, str(self.owner), str(self.operator), str(self.voting), str(self.ipport), + str(self.payee), str(self.operator_key), str(self.proTx), str(self.collateral) + ) + + def __str__(self): + return self.__repr__() diff --git a/test/functional/test_framework/test_framework.py b/test/functional/test_framework/test_framework.py index 64d362bd6112..3350ba1b83d5 100755 --- a/test/functional/test_framework/test_framework.py +++ b/test/functional/test_framework/test_framework.py @@ -47,18 +47,20 @@ connect_nodes, connect_nodes_clique, disconnect_nodes, + get_collateral_vout, + lock_utxo, Decimal, DEFAULT_FEE, get_datadir_path, hex_str_to_bytes, bytes_to_hex_str, initialize_datadir, + create_new_dmn, p2p_port, set_node_times, SPORK_ACTIVATION_TIME, SPORK_DEACTIVATION_TIME, - vZC_DENOMS, - wait_until, + satoshi_round ) class TestStatus(Enum): @@ -1048,9 +1050,10 @@ def controller_start_masternode(self, mnOwner, masternodeAlias): def send_pings(self, mnodes): for node in mnodes: - sent = node.mnping()["sent"] - if sent != "YES" and "Too early to send Masternode Ping" not in sent: - raise AssertionError("Unable to send ping: \"sent\" = %s" % sent) + try: + node.mnping()["sent"] + except: + pass time.sleep(1) @@ -1068,6 +1071,68 @@ def stake_and_ping(self, node_id, num_blocks, with_ping_mns=[]): if len(with_ping_mns) > 0: self.send_pings(with_ping_mns) + # !TODO: remove after obsoleting legacy system + def setupDMN(self, + mnOwner, + miner, + mnRemotePos, + strType, # "fund"|"internal"|"external" + outpoint=None): # COutPoint, only for "external" + self.log.info("Creating%s proRegTx for deterministic masternode..." % ( + " and funding" if strType == "fFund" else "")) + collateralAdd = mnOwner.getnewaddress("dmn") + ipport = "127.0.0.1:" + str(p2p_port(mnRemotePos)) + ownerAdd = mnOwner.getnewaddress("dmn_owner") + operatorAdd = mnOwner.getnewaddress("dmn_operator") + operatorKey = mnOwner.dumpprivkey(operatorAdd) + votingAdd = mnOwner.getnewaddress("dmn_voting") + if strType == "fund": + # send to the owner the collateral tx cost + some dust for the ProReg and fee + fundingTxId = miner.sendtoaddress(collateralAdd, Decimal('101')) + # confirm and verify reception + self.stake_and_sync(self.nodes.index(miner), 1) + assert_greater_than(mnOwner.getrawtransaction(fundingTxId, 1)["confirmations"], 0) + # create and send the ProRegTx funding the collateral + proTxId = mnOwner.protx_register_fund(collateralAdd, ipport, ownerAdd, + operatorAdd, votingAdd, collateralAdd) + elif strType == "internal": + mnOwner.getnewaddress("dust") + # send to the owner the collateral tx cost + some dust for the ProReg and fee + collateralTxId = miner.sendtoaddress(collateralAdd, Decimal('100')) + miner.sendtoaddress(collateralAdd, Decimal('1')) + # confirm and verify reception + self.stake_and_sync(self.nodes.index(miner), 1) + json_tx = mnOwner.getrawtransaction(collateralTxId, True) + collateralTxId_n = -1 + for o in json_tx["vout"]: + if o["value"] == Decimal('100'): + collateralTxId_n = o["n"] + break + assert_greater_than(collateralTxId_n, -1) + assert_greater_than(json_tx["confirmations"], 0) + proTxId = mnOwner.protx_register(collateralTxId, collateralTxId_n, ipport, ownerAdd, + operatorAdd, votingAdd, collateralAdd) + elif strType == "external": + self.log.info("Setting up ProRegTx with collateral externally-signed...") + # send the tx from the miner + payoutAdd = mnOwner.getnewaddress("payout") + register_res = miner.protx_register_prepare(outpoint.hash, outpoint.n, ipport, ownerAdd, + operatorAdd, votingAdd, payoutAdd) + self.log.info("ProTx prepared") + message_to_sign = register_res["signMessage"] + collateralAdd = register_res["collateralAddress"] + signature = mnOwner.signmessage(collateralAdd, message_to_sign) + self.log.info("ProTx signed") + proTxId = miner.protx_register_submit(register_res["tx"], signature) + else: + raise Exception("Type %s not available" % strType) + + self.sync_mempools([mnOwner, miner]) + # confirm and verify inclusion in list + self.stake_and_sync(self.nodes.index(miner), 1) + assert_greater_than(self.nodes[mnRemotePos].getrawtransaction(proTxId, 1)["confirmations"], 0) + assert proTxId in self.nodes[mnRemotePos].protx_list(False) + return proTxId, operatorKey def setupMasternode(self, mnOwner, @@ -1105,9 +1170,152 @@ def setupMasternode(self, # lock collateral mnOwner.lockunspent(False, [{"txid": collateralTxId, "vout": collateralTxId_n}]) - # return the collateral id - return collateralTxId + # return the collateral outpoint + return COutPoint(collateralTxId, collateralTxId_n) + +### ---------------------- +### ----- DMN setup ------ +### ---------------------- + + def connect_to_all(self, nodePos): + for i in range(self.num_nodes): + if i != nodePos and self.nodes[i] is not None: + connect_nodes(self.nodes[i], nodePos) + + def assert_equal_for_all(self, expected, func_name, *args): + def not_found(): + raise Exception("function %s not found!" % func_name) + + assert_equal([getattr(x, func_name, not_found)(*args) for x in self.nodes], + [expected] * self.num_nodes) + + """ + Create a ProReg tx, which has the collateral as one of its outputs + """ + def protx_register_fund(self, miner, controller, dmn, collateral_addr, lock=True): + # send to the owner the collateral tx + some dust for the ProReg and fee + funding_txid = miner.sendtoaddress(collateral_addr, Decimal('101')) + # confirm and verify reception + miner.generate(1) + self.sync_blocks([miner, controller]) + assert_greater_than(controller.getrawtransaction(funding_txid, True)["confirmations"], 0) + # create and send the ProRegTx funding the collateral + dmn.proTx = controller.protx_register_fund(collateral_addr, dmn.ipport, dmn.owner, + dmn.operator, dmn.voting, dmn.payee) + dmn.collateral = COutPoint(int(dmn.proTx, 16), + get_collateral_vout(controller.getrawtransaction(dmn.proTx, True))) + if lock: + lock_utxo(controller, dmn.collateral) + + """ + Create a ProReg tx, which references an 100 PIV UTXO as collateral. + The controller node owns the collateral and creates the ProReg tx. + """ + def protx_register(self, miner, controller, dmn, collateral_addr, fLock): + # send to the owner the exact collateral tx amount + funding_txid = miner.sendtoaddress(collateral_addr, Decimal('100')) + # send another output to be used for the fee of the proReg tx + miner.sendtoaddress(collateral_addr, Decimal('1')) + # confirm and verify reception + miner.generate(1) + self.sync_blocks([miner, controller]) + json_tx = controller.getrawtransaction(funding_txid, True) + assert_greater_than(json_tx["confirmations"], 0) + # create and send the ProRegTx + dmn.collateral = COutPoint(int(funding_txid, 16), get_collateral_vout(json_tx)) + dmn.proTx = controller.protx_register(funding_txid, dmn.collateral.n, dmn.ipport, dmn.owner, + dmn.operator, dmn.voting, dmn.payee) + if fLock: + lock_utxo(controller, dmn.collateral) + + """ + Create a ProReg tx, referencing a collateral signed externally (eg. HW wallets). + Here the controller node owns the collateral (and signs), but the miner creates the ProReg tx. + """ + def protx_register_ext(self, miner, controller, dmn, outpoint, fSubmit, fLock): + # send to the owner the collateral tx if the outpoint is not specified + if outpoint is None: + funding_txid = miner.sendtoaddress(controller.getnewaddress("collateral"), Decimal('100')) + # confirm and verify reception + miner.generate(1) + self.sync_blocks([miner, controller]) + json_tx = controller.getrawtransaction(funding_txid, True) + assert_greater_than(json_tx["confirmations"], 0) + outpoint = COutPoint(int(funding_txid, 16), get_collateral_vout(json_tx)) + dmn.collateral = outpoint + # Prepare the message to be signed externally by the owner of the collateral (the controller) + reg_tx = miner.protx_register_prepare("%064x" % outpoint.hash, outpoint.n, dmn.ipport, dmn.owner, + dmn.operator, dmn.voting, dmn.payee) + sig = controller.signmessage(reg_tx["collateralAddress"], reg_tx["signMessage"]) + if fSubmit: + if fLock: + lock_utxo(controller, dmn.collateral) + dmn.proTx = miner.protx_register_submit(reg_tx["tx"], sig) + else: + return reg_tx["tx"], sig + + """ Create and register new deterministic masternode + :param idx: (int) index of the (remote) node in self.nodes + miner_idx: (int) index of the miner in self.nodes + controller_idx: (int) index of the controller in self.nodes + strType: (string) "fund"|"internal"|"external" + payout_addr: (string) payee address. If not specified, reuse the collateral address. + outpoint: (COutPoint) collateral outpoint to be used with "external". + It must be owned by the controller (proTx is sent from the miner). + If not provided, a new utxo is created, sending it from the miner. + op_addr_and_key: (list of strings) List with two entries, operator address (0) and private key (1). + If not provided, a new address-key pair is generated. + fLock: (boolean) lock the collateral output + :return: dmn: (Masternode) the deterministic masternode object + """ + def register_new_dmn(self, idx, miner_idx, controller_idx, strType, + payout_addr=None, outpoint=None, op_addr_and_key=None, fLock=True): + # Prepare remote node + assert idx != miner_idx + assert idx != controller_idx + miner_node = self.nodes[miner_idx] + controller_node = self.nodes[controller_idx] + mn_node = self.nodes[idx] + + # Generate ip and addresses/keys + collateral_addr = controller_node.getnewaddress("mncollateral-%d" % idx) + if payout_addr is None: + payout_addr = collateral_addr + dmn = create_new_dmn(idx, controller_node, payout_addr, op_addr_and_key) + + # Create ProRegTx + self.log.info("Creating%s proRegTx for deterministic masternode idx=%d..." % ( + " and funding" if strType == "fund" else "", idx)) + if strType == "fund": + self.protx_register_fund(miner_node, controller_node, dmn, collateral_addr, fLock) + elif strType == "internal": + self.protx_register(miner_node, controller_node, dmn, collateral_addr, fLock) + elif strType == "external": + self.protx_register_ext(miner_node, controller_node, dmn, outpoint, True, fLock) + else: + raise Exception("Type %s not available" % strType) + time.sleep(1) + self.sync_mempools([miner_node, controller_node]) + + # confirm and verify inclusion in list + miner_node.generate(1) + self.sync_blocks(self.nodes) + assert_greater_than(mn_node.getrawtransaction(dmn.proTx, 1)["confirmations"], 0) + assert dmn.proTx in mn_node.protx_list(False) + return dmn + + def check_mn_list_on_node(self, idx, mns): + mnlist = self.nodes[idx].listmasternodes() + if len(mnlist) != len(mns): + raise Exception("Invalid mn list on node %d:\n%s\nExpected:%s" % (idx, str(mnlist), str(mns))) + protxs = [x["proTxHash"] for x in mnlist] + for mn in mns: + if mn.proTx not in protxs: + raise Exception("ProTx for mn %d (%s) not found in the list of node %d", mn.idx, mn.proTx, idx) + + +### ------------------------------------------------------ class SkipTest(Exception): """This exception is raised to skip a test""" @@ -1118,17 +1326,12 @@ def __init__(self, message): ''' PivxTestFramework extensions ''' - +# !TODO: remove after obsoleting legacy system class PivxTier2TestFramework(PivxTestFramework): def set_test_params(self): self.setup_clean_chain = True - self.num_nodes = 5 - self.extra_args = [[], - ["-listen", "-externalip=127.0.0.1"], - [], - ["-listen", "-externalip=127.0.0.1"], - ["-sporkkey=932HEevBSujW2ud7RfB1YF91AFygbBRQj3de3LyaCRqNzKKgWXi"]] + self.num_nodes = 6 self.enable_mocktime() self.ownerOnePos = 0 @@ -1136,6 +1339,12 @@ def set_test_params(self): self.ownerTwoPos = 2 self.remoteTwoPos = 3 self.minerPos = 4 + self.remoteDMN1Pos = 5 + + self.extra_args = [["-nuparams=v5_shield:249", "-nuparams=v6_evo:250"]] * self.num_nodes + for i in [self.remoteOnePos, self.remoteTwoPos, self.remoteDMN1Pos]: + self.extra_args[i] += ["-listen", "-externalip=127.0.0.1"] + self.extra_args[self.minerPos].append("-sporkkey=932HEevBSujW2ud7RfB1YF91AFygbBRQj3de3LyaCRqNzKKgWXi") self.masternodeOneAlias = "mnOne" self.masternodeTwoAlias = "mntwo" @@ -1143,22 +1352,25 @@ def set_test_params(self): self.mnOnePrivkey = "9247iC59poZmqBYt9iDh9wDam6v9S1rW5XekjLGyPnDhrDkP4AK" self.mnTwoPrivkey = "92Hkebp3RHdDidGZ7ARgS4orxJAGyFUPDXNqtsYsiwho1HGVRbF" - # Updated in setup_2_masternodes_network() to be called at the start of run_test + # Updated in setup_3_masternodes_network() to be called at the start of run_test self.ownerOne = None # self.nodes[self.ownerOnePos] self.remoteOne = None # self.nodes[self.remoteOnePos] self.ownerTwo = None # self.nodes[self.ownerTwoPos] self.remoteTwo = None # self.nodes[self.remoteTwoPos] self.miner = None # self.nodes[self.minerPos] - self.mnOneTxHash = "" - self.mnTwoTxHash = "" + self.remoteDMN1 = None # self.nodes[self.remoteDMN1Pos] + self.mnOneCollateral = COutPoint() + self.mnTwoCollateral = COutPoint() + self.proRegTx1 = None # hash of provider-register-tx def send_3_pings(self): + mns = [self.remoteOne, self.remoteTwo] self.advance_mocktime(30) - self.send_pings([self.remoteOne, self.remoteTwo]) - self.stake(1, [self.remoteOne, self.remoteTwo]) + self.send_pings(mns) + self.stake(1, mns) self.advance_mocktime(30) - self.send_pings([self.remoteOne, self.remoteTwo]) + self.send_pings(mns) time.sleep(2) def stake(self, num_blocks, with_ping_mns=[]): @@ -1167,12 +1379,12 @@ def stake(self, num_blocks, with_ping_mns=[]): def controller_start_all_masternodes(self): self.controller_start_masternode(self.ownerOne, self.masternodeOneAlias) self.controller_start_masternode(self.ownerTwo, self.masternodeTwoAlias) - self.wait_until_mn_preenabled(self.mnOneTxHash, 40) - self.wait_until_mn_preenabled(self.mnTwoTxHash, 40) + self.wait_until_mn_preenabled(self.mnOneCollateral.hash, 40) + self.wait_until_mn_preenabled(self.mnTwoCollateral.hash, 40) self.log.info("masternodes started, waiting until both get enabled..") self.send_3_pings() - self.wait_until_mn_enabled(self.mnOneTxHash, 120, [self.remoteOne, self.remoteTwo]) - self.wait_until_mn_enabled(self.mnTwoTxHash, 120, [self.remoteOne, self.remoteTwo]) + self.wait_until_mn_enabled(self.mnOneCollateral.hash, 120, [self.remoteOne, self.remoteTwo]) + self.wait_until_mn_enabled(self.mnTwoCollateral.hash, 120, [self.remoteOne, self.remoteTwo]) self.log.info("masternodes enabled and running properly!") def advance_mocktime_and_stake(self, secs_to_add): @@ -1180,26 +1392,27 @@ def advance_mocktime_and_stake(self, secs_to_add): self.mocktime = self.generate_pos(self.minerPos, self.mocktime) time.sleep(2) - def setup_2_masternodes_network(self): + def setup_3_masternodes_network(self): self.ownerOne = self.nodes[self.ownerOnePos] self.remoteOne = self.nodes[self.remoteOnePos] self.ownerTwo = self.nodes[self.ownerTwoPos] self.remoteTwo = self.nodes[self.remoteTwoPos] self.miner = self.nodes[self.minerPos] - ownerOneDir = os.path.join(self.options.tmpdir, "node0") - ownerTwoDir = os.path.join(self.options.tmpdir, "node2") + self.remoteDMN1 = self.nodes[self.remoteDMN1Pos] + ownerOneDir = os.path.join(self.options.tmpdir, "node%d" % self.ownerOnePos) + ownerTwoDir = os.path.join(self.options.tmpdir, "node%d" % self.ownerTwoPos) - self.log.info("generating 259 blocks..") + self.log.info("generating 256 blocks..") # First mine 250 PoW blocks for i in range(250): self.mocktime = self.generate_pow(self.minerPos, self.mocktime) self.sync_blocks() # Then start staking - self.stake(9) + self.stake(6) self.log.info("masternodes setup..") # setup first masternode node, corresponding to nodeOne - self.mnOneTxHash = self.setupMasternode( + self.mnOneCollateral = self.setupMasternode( self.ownerOne, self.miner, self.masternodeOneAlias, @@ -1207,13 +1420,20 @@ def setup_2_masternodes_network(self): self.remoteOnePos, self.mnOnePrivkey) # setup second masternode node, corresponding to nodeTwo - self.mnTwoTxHash = self.setupMasternode( + self.mnTwoCollateral = self.setupMasternode( self.ownerTwo, self.miner, self.masternodeTwoAlias, os.path.join(ownerTwoDir, "regtest"), self.remoteTwoPos, self.mnTwoPrivkey) + # setup deterministic masternode + self.proRegTx1, self.dmn1Privkey = self.setupDMN( + self.ownerOne, + self.miner, + self.remoteDMN1Pos, + "fund" + ) self.log.info("masternodes setup completed, initializing them..") @@ -1225,6 +1445,7 @@ def setup_2_masternodes_network(self): remoteTwoPort = p2p_port(self.remoteTwoPos) self.remoteOne.initmasternode(self.mnOnePrivkey, "127.0.0.1:"+str(remoteOnePort)) self.remoteTwo.initmasternode(self.mnTwoPrivkey, "127.0.0.1:"+str(remoteTwoPort)) + self.remoteDMN1.initmasternode(self.dmn1Privkey, "", True) # wait until mnsync complete on all nodes self.stake(1) @@ -1233,3 +1454,16 @@ def setup_2_masternodes_network(self): # Now everything is set, can start both masternodes self.controller_start_all_masternodes() + + def spend_collateral(self, mnOwner, collateralOutpoint, miner): + send_value = satoshi_round(100 - 0.001) + inputs = [{'txid': collateralOutpoint.hash, 'vout': collateralOutpoint.n}] + outputs = {} + outputs[mnOwner.getnewaddress()] = float(send_value) + rawtx = mnOwner.createrawtransaction(inputs, outputs) + signedtx = mnOwner.signrawtransaction(rawtx) + txid = miner.sendrawtransaction(signedtx['hex']) + self.sync_mempools() + self.log.info("Collateral spent in %s" % txid) + self.send_pings([self.remoteTwo]) + self.stake(1, [self.remoteTwo]) diff --git a/test/functional/test_framework/util.py b/test/functional/test_framework/util.py index 74d805085f99..9006cebb57ef 100644 --- a/test/functional/test_framework/util.py +++ b/test/functional/test_framework/util.py @@ -16,7 +16,7 @@ from subprocess import CalledProcessError import time -from . import coverage +from . import coverage, messages from .authproxy import AuthServiceProxy, JSONRPCException logger = logging.getLogger("TestFramework.utils") @@ -581,3 +581,38 @@ def get_coinstake_address(node, expected_utxos=None): addrs = [a for a in set(addrs) if addrs.count(a) == expected_utxos] assert(len(addrs) > 0) return addrs[0] + +# Deterministic masternodes + +def lock_utxo(node, outpoint): + node.lockunspent(False, [{"txid": "%064x" % outpoint.hash, "vout": outpoint.n}]) + +def get_collateral_vout(json_tx): + funding_txidn = -1 + for o in json_tx["vout"]: + if o["value"] == Decimal('100'): + funding_txidn = o["n"] + break + assert_greater_than(funding_txidn, -1) + return funding_txidn + +# owner and voting keys are created from controller node. +# operator key and address are created, if operator_addr_and_key is None. +def create_new_dmn(idx, controller, payout_addr, operator_addr_and_key): + ipport = "127.0.0.1:" + str(p2p_port(idx)) + owner_addr = controller.getnewaddress("mnowner-%d" % idx) + voting_addr = controller.getnewaddress("mnvoting-%d" % idx) + if operator_addr_and_key is None: + operator_addr = controller.getnewaddress("mnoperator-%d" % idx) + operator_key = controller.dumpprivkey(operator_addr) + else: + operator_addr = operator_addr_and_key[0] + operator_key = operator_addr_and_key[1] + return messages.Masternode(idx, owner_addr, operator_addr, voting_addr, ipport, payout_addr, operator_key) + +def spend_mn_collateral(spender, dmn): + inputs = [{"txid": "%064x" % dmn.collateral.hash, "vout": dmn.collateral.n}] + outputs = {spender.getnewaddress(): Decimal('99.99')} + sig_res = spender.signrawtransaction(spender.createrawtransaction(inputs, outputs)) + assert_equal(sig_res['complete'], True) + return spender.sendrawtransaction(sig_res['hex']) diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 7ba290508061..905a95593f9f 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -142,6 +142,7 @@ TIERTWO_SCRIPTS = [ # Longest test should go first, to favor running tests in parallel 'tiertwo_governance_sync_basic.py', + 'tiertwo_mn_compatibility.py', 'tiertwo_masternode_activation.py', 'tiertwo_masternode_ping.py', ] diff --git a/test/functional/tiertwo_governance_sync_basic.py b/test/functional/tiertwo_governance_sync_basic.py index 3ff5412cfa93..0383afed882e 100755 --- a/test/functional/tiertwo_governance_sync_basic.py +++ b/test/functional/tiertwo_governance_sync_basic.py @@ -4,10 +4,11 @@ # file COPYING or https://www.opensource.org/licenses/mit-license.php. from test_framework.test_framework import PivxTier2TestFramework +from test_framework.messages import COutPoint from test_framework.util import ( assert_equal, assert_true, - Decimal, + satoshi_round, ) import time @@ -23,6 +24,24 @@ class MasternodeGovernanceBasicTest(PivxTier2TestFramework): + def check_mns_status_legacy(self, node, txhash): + status = node.getmasternodestatus() + assert_equal(status["txhash"], txhash) + assert_equal(status["message"], "Masternode successfully started") + + def check_mns_status(self, node, txhash): + status = node.getmasternodestatus() + assert_equal(status["proTxHash"], txhash) + assert_equal(status["dmnstate"]["PoSePenalty"], 0) + assert_equal(status["status"], "Ready") + + def check_mn_list(self, node, txHashSet): + # check masternode list from node + mnlist = node.listmasternodes() + assert_equal(len(mnlist), 3) + foundHashes = set([mn["txhash"] for mn in mnlist if mn["txhash"] in txHashSet]) + assert_equal(len(foundHashes), len(txHashSet)) + def check_budget_finalization_sync(self, votesCount, status): for i in range(0, len(self.nodes)): node = self.nodes[i] @@ -49,7 +68,7 @@ def check_proposal_existence(self, proposalName, proposalHash): assert(len(proposals) > 0) assert_equal(proposals[0]["Hash"], proposalHash) - def check_vote_existence(self, proposalName, mnCollateralHash, voteType): + def check_vote_existence(self, proposalName, mnCollateralHash, voteType, voteValid): for i in range(0, len(self.nodes)): node = self.nodes[i] votesInfo = node.getbudgetvotes(proposalName) @@ -58,6 +77,7 @@ def check_vote_existence(self, proposalName, mnCollateralHash, voteType): for voteInfo in votesInfo: if (voteInfo["mnId"].split("-")[0] == mnCollateralHash) : assert_equal(voteInfo["Vote"], voteType) + assert_equal(voteInfo["fValid"], voteValid) found = True assert_true(found, "Error checking vote existence in node " + str(i)) @@ -96,7 +116,18 @@ def check_budgetprojection(self, expected): def run_test(self): self.enable_mocktime() - self.setup_2_masternodes_network() + self.setup_3_masternodes_network() + txHashSet = set([self.mnOneCollateral.hash, self.mnTwoCollateral.hash, self.proRegTx1]) + # check mn list from miner + self.check_mn_list(self.miner, txHashSet) + + # check status of masternodes + self.check_mns_status_legacy(self.remoteOne, self.mnOneCollateral.hash) + self.log.info("MN1 active") + self.check_mns_status_legacy(self.remoteTwo, self.mnTwoCollateral.hash) + self.log.info("MN2 active") + self.check_mns_status(self.remoteDMN1, self.proRegTx1) + self.log.info("DMN1 active") # Prepare the proposal self.log.info("preparing budget proposal..") @@ -147,24 +178,35 @@ def run_test(self): self.stake(7, [self.remoteOne, self.remoteTwo]) # now let's vote for the proposal with the first MN - self.log.info("broadcasting votes for the proposal now..") - voteResult = self.ownerOne.mnbudgetvote("alias", proposalHash, "yes", self.masternodeOneAlias) + self.log.info("Voting with MN1...") + voteResult = self.ownerOne.mnbudgetvote("alias", proposalHash, "yes", self.masternodeOneAlias, True) assert_equal(voteResult["detail"][0]["result"], "success") # check that the vote was accepted everywhere self.stake(1, [self.remoteOne, self.remoteTwo]) - self.check_vote_existence(firstProposalName, self.mnOneTxHash, "YES") + self.check_vote_existence(firstProposalName, self.mnOneCollateral.hash, "YES", True) self.log.info("all good, MN1 vote accepted everywhere!") # now let's vote for the proposal with the second MN - voteResult = self.ownerTwo.mnbudgetvote("alias", proposalHash, "yes", self.masternodeTwoAlias) + self.log.info("Voting with MN2...") + voteResult = self.ownerTwo.mnbudgetvote("alias", proposalHash, "yes", self.masternodeTwoAlias, True) assert_equal(voteResult["detail"][0]["result"], "success") # check that the vote was accepted everywhere self.stake(1, [self.remoteOne, self.remoteTwo]) - self.check_vote_existence(firstProposalName, self.mnTwoTxHash, "YES") + self.check_vote_existence(firstProposalName, self.mnTwoCollateral.hash, "YES", True) self.log.info("all good, MN2 vote accepted everywhere!") + # now let's vote for the proposal with the first DMN + self.log.info("Voting with DMN1...") + voteResult = self.ownerOne.mnbudgetvote("alias", proposalHash, "yes", self.proRegTx1) + assert_equal(voteResult["detail"][0]["result"], "success") + + # check that the vote was accepted everywhere + self.stake(1, [self.remoteOne, self.remoteTwo]) + self.check_vote_existence(firstProposalName, self.proRegTx1, "YES", True) + self.log.info("all good, DMN1 vote accepted everywhere!") + # Now check the budget blockStart = nextSuperBlockHeight blockEnd = blockStart + firstProposalCycles * 145 @@ -174,8 +216,8 @@ def run_test(self): expected_budget = [ self.get_proposal_obj(firstProposalName, firstProposalLink, proposalHash, proposalFeeTxId, blockStart, blockEnd, firstProposalCycles, RemainingPaymentCount, firstProposalAddress, 1, - 2, 0, 0, Decimal(str(TotalPayment)), Decimal(str(firstProposalAmountPerCycle)), - True, True, Decimal(str(Allotted)), Decimal(str(Allotted))) + 3, 0, 0, satoshi_round(TotalPayment), satoshi_round(firstProposalAmountPerCycle), + True, True, satoshi_round(Allotted), satoshi_round(Allotted)) ] self.check_budgetprojection(expected_budget) @@ -199,12 +241,19 @@ def run_test(self): self.log.info("budget finalization synced!, now voting for the budget finalization..") - self.ownerOne.mnfinalbudget("vote-many", budgetFinHash) - self.ownerTwo.mnfinalbudget("vote-many", budgetFinHash) + voteResult = self.ownerOne.mnfinalbudget("vote-many", budgetFinHash, True) + assert_equal(voteResult["detail"][0]["result"], "success") + self.log.info("Remote One voted successfully.") + voteResult = self.ownerTwo.mnfinalbudget("vote-many", budgetFinHash, True) + assert_equal(voteResult["detail"][0]["result"], "success") + self.log.info("Remote Two voted successfully.") + voteResult = self.remoteDMN1.mnfinalbudget("vote", budgetFinHash) + assert_equal(voteResult["detail"][0]["result"], "success") + self.log.info("DMN voted successfully.") self.stake(2, [self.remoteOne, self.remoteTwo]) self.log.info("checking finalization votes..") - self.check_budget_finalization_sync(2, "OK") + self.check_budget_finalization_sync(3, "OK") self.stake(8, [self.remoteOne, self.remoteTwo]) addrInfo = self.miner.listreceivedbyaddress(0, False, False, firstProposalAddress) @@ -216,9 +265,28 @@ def run_test(self): expected_budget[0]["RemainingPaymentCount"] -= 1 self.check_budgetprojection(expected_budget) + self.stake(1, [self.remoteOne, self.remoteTwo]) + + # now let's verify that votes expire properly. + # Drop one MN and one DMN + self.log.info("expiring MN1..") + self.spend_collateral(self.ownerOne, self.mnOneCollateral, self.miner) + self.wait_until_mn_vinspent(self.mnOneCollateral.hash, 30, [self.remoteTwo]) + self.stake(15, [self.remoteTwo]) # create blocks to remove staled votes + time.sleep(2) # wait a little bit + self.check_vote_existence(firstProposalName, self.mnOneCollateral.hash, "YES", False) + self.log.info("MN1 vote expired after collateral spend, all good") + self.log.info("expiring DMN1..") + lm = self.ownerOne.listmasternodes(self.proRegTx1)[0] + self.spend_collateral(self.ownerOne, COutPoint(lm["collateralHash"], lm["collateralIndex"]), self.miner) + self.wait_until_mn_vinspent(self.proRegTx1, 30, [self.remoteTwo]) + self.stake(15, [self.remoteTwo]) # create blocks to remove staled votes + time.sleep(2) # wait a little bit + self.check_vote_existence(firstProposalName, self.proRegTx1, "YES", False) + self.log.info("DMN vote expired after collateral spend, all good") if __name__ == '__main__': - MasternodeGovernanceBasicTest().main() \ No newline at end of file + MasternodeGovernanceBasicTest().main() diff --git a/test/functional/tiertwo_masternode_activation.py b/test/functional/tiertwo_masternode_activation.py index ec95325cac99..47ce2480c67b 100755 --- a/test/functional/tiertwo_masternode_activation.py +++ b/test/functional/tiertwo_masternode_activation.py @@ -5,10 +5,8 @@ from test_framework.test_framework import PivxTier2TestFramework from test_framework.util import ( - assert_equal, connect_nodes_clique, disconnect_nodes, - satoshi_round, wait_until, ) @@ -43,27 +41,11 @@ def reconnect_and_restart_masternodes(self): self.wait_until_mnsync_finished() self.controller_start_all_masternodes() - def spend_collateral(self): - mnCollateralOutput = self.ownerOne.getmasternodeoutputs()[0] - assert_equal(mnCollateralOutput["txhash"], self.mnOneTxHash) - mnCollateralOutputIndex = mnCollateralOutput["outputidx"] - send_value = satoshi_round(100 - 0.001) - inputs = [{'txid' : self.mnOneTxHash, 'vout' : mnCollateralOutputIndex}] - outputs = {} - outputs[self.ownerOne.getnewaddress()] = float(send_value) - rawtx = self.ownerOne.createrawtransaction(inputs, outputs) - signedtx = self.ownerOne.signrawtransaction(rawtx) - txid = self.miner.sendrawtransaction(signedtx['hex']) - self.sync_mempools() - self.log.info("Collateral spent in %s" % txid) - self.send_pings([self.remoteTwo]) - self.stake(1, [self.remoteTwo]) - # Similar to base class wait_until_mn_status but skipping the disconnected nodes def wait_until_mn_expired(self, _timeout, removed=False): collaterals = { - self.remoteOnePos: self.mnOneTxHash, - self.remoteTwoPos: self.mnTwoTxHash + self.remoteOnePos: self.mnOneCollateral.hash, + self.remoteTwoPos: self.mnTwoCollateral.hash } for k in collaterals: for i in range(self.num_nodes): @@ -86,11 +68,11 @@ def wait_until_mn_expired(self, _timeout, removed=False): def run_test(self): self.enable_mocktime() - self.setup_2_masternodes_network() + self.setup_3_masternodes_network() # check masternode expiration self.log.info("testing expiration now.") - expiration_time = 180 # regtest expiration time + expiration_time = 12 * 60 # regtest expiration time self.log.info("disconnect remote and move time %d seconds in the future..." % expiration_time) self.disconnect_remotes() self.advance_mocktime_and_stake(expiration_time) @@ -99,7 +81,7 @@ def run_test(self): # check masternode removal self.log.info("testing removal now.") - removal_time = 200 # regtest removal time + removal_time = 13 * 60 # regtest removal time self.advance_mocktime_and_stake(removal_time - expiration_time) self.wait_until_mn_expired(30, removed=True) self.log.info("masternodes removed successfully") @@ -108,11 +90,11 @@ def run_test(self): self.reconnect_and_restart_masternodes() self.advance_mocktime(30) self.log.info("spending the collateral now..") - self.spend_collateral() + self.spend_collateral(self.ownerOne, self.mnOneCollateral, self.miner) self.sync_blocks() self.log.info("checking mn status..") time.sleep(3) # wait a little bit - self.wait_until_mn_vinspent(self.mnOneTxHash, 30, [self.remoteTwo]) + self.wait_until_mn_vinspent(self.mnOneCollateral.hash, 30, [self.remoteTwo]) self.log.info("masternode list updated successfully, vin spent") diff --git a/test/functional/tiertwo_mn_compatibility.py b/test/functional/tiertwo_mn_compatibility.py new file mode 100755 index 000000000000..fa407cb62760 --- /dev/null +++ b/test/functional/tiertwo_mn_compatibility.py @@ -0,0 +1,210 @@ +#!/usr/bin/env python3 +# Copyright (c) 2021 The PIVX developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or https://www.opensource.org/licenses/mit-license.php. + +from test_framework.test_framework import PivxTier2TestFramework +from test_framework.util import ( + assert_equal, + connect_nodes, +) + +from decimal import Decimal + +""" +Test checking compatibility code between MN and DMN +""" + +class MasternodeCompatibilityTest(PivxTier2TestFramework): + + def set_test_params(self): + self.setup_clean_chain = True + self.num_nodes = 7 + self.enable_mocktime() + + self.minerPos = 0 + self.ownerOnePos = self.ownerTwoPos = 1 + self.remoteOnePos = 2 + self.remoteTwoPos = 3 + self.remoteDMN1Pos = 4 + self.remoteDMN2Pos = 5 + self.remoteDMN3Pos = 6 + + self.masternodeOneAlias = "mnOne" + self.masternodeTwoAlias = "mntwo" + + self.extra_args = [["-nuparams=v5_shield:249", "-nuparams=v6_evo:250"]] * self.num_nodes + for i in [self.remoteOnePos, self.remoteTwoPos, self.remoteDMN1Pos, self.remoteDMN2Pos, self.remoteDMN3Pos]: + self.extra_args[i] += ["-listen", "-externalip=127.0.0.1"] + self.extra_args[self.minerPos].append("-sporkkey=932HEevBSujW2ud7RfB1YF91AFygbBRQj3de3LyaCRqNzKKgWXi") + + self.mnOnePrivkey = "9247iC59poZmqBYt9iDh9wDam6v9S1rW5XekjLGyPnDhrDkP4AK" + self.mnTwoPrivkey = "92Hkebp3RHdDidGZ7ARgS4orxJAGyFUPDXNqtsYsiwho1HGVRbF" + + self.miner = None + self.ownerOne = self.ownerTwo = None + self.remoteOne = None + self.remoteTwo = None + self.remoteDMN1 = None + self.remoteDMN2 = None + self.remoteDMN3 = None + + def check_mns_status_legacy(self, node, txhash): + status = node.getmasternodestatus() + assert_equal(status["txhash"], txhash) + assert_equal(status["message"], "Masternode successfully started") + + def check_mns_status(self, node, txhash): + status = node.getmasternodestatus() + assert_equal(status["proTxHash"], txhash) + assert_equal(status["dmnstate"]["PoSePenalty"], 0) + assert_equal(status["status"], "Ready") + + """ + Checks the block at specified height (it must be a v10 block). + Returns the address of the mn paid (in the coinbase), and the json coinstake tx + """ + def get_block_mnwinner(self, height): + blk = self.miner.getblock(self.miner.getblockhash(height), True) + assert_equal(blk['height'], height) + assert_equal(blk['version'], 10) + cbase_tx = self.miner.getrawtransaction(blk['tx'][0], True) + assert_equal(len(cbase_tx['vin']), 1) + cbase_script = height.to_bytes(1 + height // 256, byteorder="little") + cbase_script = len(cbase_script).to_bytes(1, byteorder="little") + cbase_script + bytearray(1) + assert_equal(cbase_tx['vin'][0]['coinbase'], cbase_script.hex()) + assert_equal(len(cbase_tx['vout']), 1) + assert_equal(cbase_tx['vout'][0]['value'], Decimal("3.0")) + return cbase_tx['vout'][0]['scriptPubKey']['addresses'][0], self.miner.getrawtransaction(blk['tx'][1], True) + + def check_mn_list(self, node, txHashSet): + # check masternode list from node + mnlist = node.listmasternodes() + if len(mnlist) != len(txHashSet): + raise Exception(str(mnlist)) + foundHashes = set([mn["txhash"] for mn in mnlist if mn["txhash"] in txHashSet]) + if len(foundHashes) != len(txHashSet): + raise Exception(str(mnlist)) + for x in mnlist: + self.mn_addresses[x["txhash"]] = x["addr"] + + def run_test(self): + self.mn_addresses = {} + self.enable_mocktime() + self.setup_3_masternodes_network() + + # add two more nodes to the network + self.remoteDMN2 = self.nodes[self.remoteDMN2Pos] + self.remoteDMN3 = self.nodes[self.remoteDMN3Pos] + # add more direct connections to the miner + connect_nodes(self.miner, 2) + connect_nodes(self.remoteTwo, 0) + connect_nodes(self.remoteDMN2, 0) + self.sync_all() + + # check mn list from miner + txHashSet = set([self.mnOneCollateral.hash, self.mnTwoCollateral.hash, self.proRegTx1]) + self.check_mn_list(self.miner, txHashSet) + + # check status of masternodes + self.check_mns_status_legacy(self.remoteOne, self.mnOneCollateral.hash) + self.log.info("MN1 active. Pays %s" % self.mn_addresses[self.mnOneCollateral.hash]) + self.check_mns_status_legacy(self.remoteTwo, self.mnTwoCollateral.hash) + self.log.info("MN2 active Pays %s" % self.mn_addresses[self.mnTwoCollateral.hash]) + self.check_mns_status(self.remoteDMN1, self.proRegTx1) + self.log.info("DMN1 active Pays %s" % self.mn_addresses[self.proRegTx1]) + + # Create another DMN, this time without funding the collateral. + # ProTx references another transaction in the owner's wallet + self.proRegTx2, self.dmn2Privkey = self.setupDMN( + self.ownerOne, + self.miner, + self.remoteDMN2Pos, + "internal" + ) + self.remoteDMN2.initmasternode(self.dmn2Privkey, "", True) + + # check list and status + txHashSet.add(self.proRegTx2) + self.check_mn_list(self.miner, txHashSet) + self.check_mns_status(self.remoteDMN2, self.proRegTx2) + self.log.info("DMN2 active Pays %s" % self.mn_addresses[self.proRegTx2]) + + # Check block version and coinbase payment + blk_count = self.miner.getblockcount() + self.log.info("Checking block version and coinbase payment...") + payee, cstake_tx = self.get_block_mnwinner(blk_count) + if payee not in [self.mn_addresses[k] for k in self.mn_addresses]: + raise Exception("payee %s not found in expected list %s" % (payee, str(self.mn_addresses))) + assert_equal(len(cstake_tx['vin']), 1) + assert_equal(len(cstake_tx['vout']), 2) + assert_equal(cstake_tx['vout'][1]['value'], Decimal("497.0")) # 250 + 250 - 3 + self.log.info("Block at height %d checks out" % blk_count) + + # Now create a DMN, reusing the collateral output of a legacy MN + self.log.info("Creating a DMN reusing the collateral of a legacy MN...") + self.proRegTx3, self.dmn3Privkey = self.setupDMN( + self.ownerOne, + self.miner, + self.remoteDMN3Pos, + "external", + self.mnOneCollateral, + ) + # The remote node is shutting down the pinging service + self.send_3_pings() + + self.remoteDMN3.initmasternode(self.dmn3Privkey, "", True) + + # The legacy masternode must no longer be in the list + # and the DMN must have taken its place + txHashSet.remove(self.mnOneCollateral.hash) + txHashSet.add(self.proRegTx3) + for node in self.nodes: + self.check_mn_list(node, txHashSet) + self.log.info("Masternode list correctly updated by all nodes.") + self.check_mns_status(self.remoteDMN3, self.proRegTx3) + self.log.info("DMN3 active Pays %s" % self.mn_addresses[self.proRegTx3]) + + # Now try to start a legacy MN with a collateral used by a DMN + self.log.info("Now trying to start a legacy MN with a collateral of a DMN...") + self.controller_start_masternode(self.ownerOne, self.masternodeOneAlias) + self.send_3_pings() + + # the masternode list hasn't changed + for node in self.nodes: + self.check_mn_list(node, txHashSet) + self.log.info("Masternode list correctly unchanged in all nodes.") + + # stake 30 blocks, sync tiertwo data, and check winners + self.log.info("Staking 30 blocks...") + self.stake(30, [self.remoteTwo]) + self.sync_blocks() + self.wait_until_mnsync_finished() + + # check projection + self.log.info("Checking winners...") + winners = set([x['winner']['address'] for x in self.miner.getmasternodewinners() + if x['winner']['address'] != "Unknown"]) + # all except mn1 must be scheduled + mn_addresses = set([self.mn_addresses[k] for k in self.mn_addresses + if k != self.mnOneCollateral.hash]) + assert_equal(winners, mn_addresses) + + # check mns paid in the last 20 blocks + self.log.info("Checking masternodes paid...") + blk_count = self.miner.getblockcount() + mn_payments = {} # dict address --> payments count + for i in range(blk_count - 20 + 1, blk_count + 1): + winner, _ = self.get_block_mnwinner(i) + if winner not in mn_payments: + mn_payments[winner] = 0 + mn_payments[winner] += 1 + # two full 10-blocks schedule: all mns must be paid at least twice + assert_equal(len(mn_payments), len(mn_addresses)) + assert all([x >= 2 for x in mn_payments.values()]) + self.log.info("All good.") + + + +if __name__ == '__main__': + MasternodeCompatibilityTest().main()