diff --git a/doc/release-notes-5039.md b/doc/release-notes-5039.md new file mode 100644 index 000000000000..78b137b51089 --- /dev/null +++ b/doc/release-notes-5039.md @@ -0,0 +1,18 @@ +Added RPCs +-------- + +The following RPCs were added: `protx register_hpmn`, `protx register_fund_hpmn`, `protx register_prepare_hpmn` and `protx update_service_hpmn`. +These HPMN RPCs correspond to the standard masternode RPCs but have the following additional mandatory arguments: `platformNodeID`, `platformP2PPort` and `platformHTTPPort`. +- `platformNodeID`: Platform P2P node ID, derived from P2P public key. +- `platformP2PPort`: TCP port of Dash Platform peer-to-peer communication between nodes (network byte order). +- `platformHTTPPort`: TCP port of Platform HTTP/API interface (network byte order). +Notes: +- `platformNodeID` must be unique across the network. +- `platformP2PPort`, `platformHTTPPort` and the Core port must be distinct. + + +Updated RPCs +-------- + +The RPC's `gobject getcurrentvotes` reply is enriched by adding the vote weight at the end of each line. Possible values are 1 or 4. Example: +`"7cb20c883c6093b8489f795b3ec0aad0d9c2c2821610ae9ed938baaf42fec66d": "277e6345359071410ab691c21a3a16f8f46c9229c2f8ec8f028c9a95c0f1c0e7-1:1670019339:yes:funding:4"` \ No newline at end of file diff --git a/src/Makefile.am b/src/Makefile.am index 6e32c294e2e0..09073e0fd7d3 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -165,6 +165,7 @@ BITCOIN_CORE_H = \ cuckoocache.h \ ctpl_stl.h \ cxxtimer.hpp \ + evo/dmn_types.h \ evo/cbtx.h \ evo/deterministicmns.h \ evo/dmnstate.h \ diff --git a/src/chainparams.cpp b/src/chainparams.cpp index c9dde2425983..93c6b05e97bf 100644 --- a/src/chainparams.cpp +++ b/src/chainparams.cpp @@ -271,6 +271,8 @@ class CMainParams : public CChainParams { pchMessageStart[2] = 0x6b; pchMessageStart[3] = 0xbd; nDefaultPort = 9999; + nDefaultPlatformP2PPort = 26656; + nDefaultPlatformHTTPPort = 443; nPruneAfterHeight = 100000; m_assumed_blockchain_size = 45; m_assumed_chain_state_size = 1; @@ -502,6 +504,8 @@ class CTestNetParams : public CChainParams { pchMessageStart[2] = 0xca; pchMessageStart[3] = 0xff; nDefaultPort = 19999; + nDefaultPlatformP2PPort = 22000; + nDefaultPlatformHTTPPort = 22001; nPruneAfterHeight = 1000; m_assumed_blockchain_size = 4; m_assumed_chain_state_size = 1; @@ -710,6 +714,8 @@ class CDevNetParams : public CChainParams { pchMessageStart[2] = 0xff; pchMessageStart[3] = 0xce; nDefaultPort = 19799; + nDefaultPlatformP2PPort = 22100; + nDefaultPlatformHTTPPort = 22101; nPruneAfterHeight = 1000; m_assumed_blockchain_size = 0; m_assumed_chain_state_size = 0; @@ -960,6 +966,8 @@ class CRegTestParams : public CChainParams { pchMessageStart[2] = 0xb7; pchMessageStart[3] = 0xdc; nDefaultPort = 19899; + nDefaultPlatformP2PPort = 22200; + nDefaultPlatformHTTPPort = 22201; nPruneAfterHeight = 1000; m_assumed_blockchain_size = 0; m_assumed_chain_state_size = 0; @@ -1027,10 +1035,11 @@ class CRegTestParams : public CChainParams { AddLLMQ(Consensus::LLMQType::LLMQ_TEST_INSTANTSEND); AddLLMQ(Consensus::LLMQType::LLMQ_TEST_V17); AddLLMQ(Consensus::LLMQType::LLMQ_TEST_DIP0024); + AddLLMQ(Consensus::LLMQType::LLMQ_TEST_PLATFORM); consensus.llmqTypeChainLocks = Consensus::LLMQType::LLMQ_TEST; consensus.llmqTypeInstantSend = Consensus::LLMQType::LLMQ_TEST_INSTANTSEND; consensus.llmqTypeDIP0024InstantSend = Consensus::LLMQType::LLMQ_TEST_DIP0024; - consensus.llmqTypePlatform = Consensus::LLMQType::LLMQ_TEST; + consensus.llmqTypePlatform = Consensus::LLMQType::LLMQ_TEST_PLATFORM; consensus.llmqTypeMnhf = Consensus::LLMQType::LLMQ_TEST; UpdateLLMQTestParametersFromArgs(args, Consensus::LLMQType::LLMQ_TEST); diff --git a/src/chainparams.h b/src/chainparams.h index 18377207f683..a0c8c7275fa3 100644 --- a/src/chainparams.h +++ b/src/chainparams.h @@ -61,6 +61,8 @@ class CChainParams const Consensus::Params& GetConsensus() const { return consensus; } const CMessageHeader::MessageStartChars& MessageStart() const { return pchMessageStart; } uint16_t GetDefaultPort() const { return nDefaultPort; } + uint16_t GetDefaultPlatformP2PPort() const { return nDefaultPlatformP2PPort; } + uint16_t GetDefaultPlatformHTTPPort() const { return nDefaultPlatformHTTPPort; } const CBlock& GenesisBlock() const { return genesis; } const CBlock& DevNetGenesisBlock() const { return devnetGenesis; } @@ -145,6 +147,8 @@ class CChainParams std::vector vSporkAddresses; int nMinSporkKeys; bool fBIP9CheckMasternodesUpgraded; + uint16_t nDefaultPlatformP2PPort; + uint16_t nDefaultPlatformHTTPPort; void AddLLMQ(Consensus::LLMQType llmqType); }; diff --git a/src/evo/deterministicmns.cpp b/src/evo/deterministicmns.cpp index 2b9770191df9..f58ef238004d 100644 --- a/src/evo/deterministicmns.cpp +++ b/src/evo/deterministicmns.cpp @@ -3,12 +3,13 @@ // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include +#include #include -#include +#include #include +#include #include #include -#include #include #include @@ -24,8 +25,8 @@ #include -static const std::string DB_LIST_SNAPSHOT = "dmn_S"; -static const std::string DB_LIST_DIFF = "dmn_D"; +static const std::string DB_LIST_SNAPSHOT = "dmn_S2"; +static const std::string DB_LIST_DIFF = "dmn_D2"; std::unique_ptr deterministicMNManager; @@ -49,6 +50,7 @@ void CDeterministicMN::ToJson(UniValue& obj) const UniValue stateObj; pdmnState->ToJson(stateObj); + obj.pushKV("type", std::string(GetMnType(nType).description)); obj.pushKV("proTxHash", proTxHash.ToString()); obj.pushKV("collateralHash", collateralOutpoint.hash.ToString()); obj.pushKV("collateralIndex", (int)collateralOutpoint.n); @@ -175,15 +177,35 @@ static bool CompareByLastPaid(const CDeterministicMN* _a, const CDeterministicMN return CompareByLastPaid(*_a, *_b); } -CDeterministicMNCPtr CDeterministicMNList::GetMNPayee() const +CDeterministicMNCPtr CDeterministicMNList::GetMNPayee(const CBlockIndex* pIndex) const { if (mnMap.size() == 0) { return nullptr; } - CDeterministicMNCPtr best; + bool isv19Active = llmq::utils::IsV19Active(pIndex); + // Starting from v19 and until v20 (Platform release), HPMN will be rewarded 4 blocks in a row + // TODO: Skip this code once v20 is active + CDeterministicMNCPtr best = nullptr; + if (isv19Active) { + ForEachMNShared(true, [&](const CDeterministicMNCPtr& dmn) { + if (dmn->pdmnState->nLastPaidHeight == nHeight) { + // We found the last MN Payee. + // If the last payee is a HPMN, we need to check its consecutive payments and pay him again if needed + if (dmn->nType == MnType::HighPerformance.index && dmn->pdmnState->nConsecutivePayments < MnType::HighPerformance.voting_weight) { + best = dmn; + } + } + }); + + if (best != nullptr) return best; + + // Note: If the last payee was a regular MN or if the payee is a HPMN that was removed from the mnList then that's fine. + // We can proceed with classic MN payee selection + } + ForEachMNShared(true, [&](const CDeterministicMNCPtr& dmn) { - if (!best || CompareByLastPaid(dmn.get(), best.get())) { + if (best == nullptr || CompareByLastPaid(dmn.get(), best.get())) { best = dmn; } }); @@ -213,9 +235,9 @@ std::vector CDeterministicMNList::GetProjectedMNPayees(int return result; } -std::vector CDeterministicMNList::CalculateQuorum(size_t maxSize, const uint256& modifier) const +std::vector CDeterministicMNList::CalculateQuorum(size_t maxSize, const uint256& modifier, const bool onlyHighPerformanceMasternodes) const { - auto scores = CalculateScores(modifier); + auto scores = CalculateScores(modifier, onlyHighPerformanceMasternodes); // sort is descending order std::sort(scores.rbegin(), scores.rend(), [](const std::pair& a, const std::pair& b) { @@ -235,7 +257,7 @@ std::vector CDeterministicMNList::CalculateQuorum(size_t m return result; } -std::vector> CDeterministicMNList::CalculateScores(const uint256& modifier) const +std::vector> CDeterministicMNList::CalculateScores(const uint256& modifier, const bool onlyHighPerformanceMasternodes) const { std::vector> scores; scores.reserve(GetAllMNsCount()); @@ -245,6 +267,10 @@ std::vector> CDeterministicMNList // future quorums return; } + if (onlyHighPerformanceMasternodes) { + if (dmn->nType != MnType::HighPerformance.index) + return; + } // calculate sha256(sha256(proTxHash, confirmedHash), modifier) per MN // Please note that this is not a double-sha256 but a single-sha256 // The first part is already precalculated (confirmedHashWithProRegTxHash) @@ -351,10 +377,11 @@ CDeterministicMNListDiff CDeterministicMNList::BuildDiff(const CDeterministicMNL CSimplifiedMNListDiff CDeterministicMNList::BuildSimplifiedDiff(const CDeterministicMNList& to, bool extended) const { + bool v19active = llmq::utils::IsV19Active(::ChainActive().Tip()); CSimplifiedMNListDiff diffRet; diffRet.baseBlockHash = blockHash; diffRet.blockHash = to.blockHash; - diffRet.nVersion = llmq::utils::IsV19Active(::ChainActive().Tip()) ? CSimplifiedMNListDiff::BASIC_BLS_VERSION : CSimplifiedMNListDiff::LEGACY_BLS_VERSION; + diffRet.nVersion = v19active ? CSimplifiedMNListDiff::BASIC_BLS_VERSION : CSimplifiedMNListDiff::LEGACY_BLS_VERSION; to.ForEachMN(false, [&](const auto& toPtr) { auto fromPtr = GetMN(toPtr.proTxHash); @@ -444,6 +471,14 @@ void CDeterministicMNList::AddMN(const CDeterministicMNCPtr& dmn, bool fBumpTota dmn->proTxHash.ToString(), dmn->pdmnState->pubKeyOperator.Get().ToString()))); } + if (dmn->nType == MnType::HighPerformance.index) { + if (!AddUniqueProperty(*dmn, dmn->pdmnState->platformNodeID)) { + mnUniquePropertyMap = mnUniquePropertyMapSaved; + throw(std::runtime_error(strprintf("%s: Can't add a masternode %s with a duplicate platformNodeID=%s", __func__, + dmn->proTxHash.ToString(), dmn->pdmnState->platformNodeID.ToString()))); + } + } + mnMap = mnMap.set(dmn->proTxHash, dmn); mnInternalIdMap = mnInternalIdMap.set(dmn->GetInternalId(), dmn->proTxHash); if (fBumpTotalCount) { @@ -477,6 +512,13 @@ void CDeterministicMNList::UpdateMN(const CDeterministicMN& oldDmn, const std::s throw(std::runtime_error(strprintf("%s: Can't update a masternode %s with a duplicate pubKeyOperator=%s", __func__, oldDmn.proTxHash.ToString(), pdmnState->pubKeyOperator.Get().ToString()))); } + if (dmn->nType == MnType::HighPerformance.index) { + if (!UpdateUniqueProperty(*dmn, oldState->platformNodeID, dmn->pdmnState->platformNodeID)) { + mnUniquePropertyMap = mnUniquePropertyMapSaved; + throw(std::runtime_error(strprintf("%s: Can't update a masternode %s with a duplicate platformNodeID=%s", __func__, + dmn->proTxHash.ToString(), dmn->pdmnState->platformNodeID.ToString()))); + } + } mnMap = mnMap.set(oldDmn.proTxHash, dmn); } @@ -530,6 +572,14 @@ void CDeterministicMNList::RemoveMN(const uint256& proTxHash) proTxHash.ToString(), dmn->pdmnState->pubKeyOperator.Get().ToString()))); } + if (dmn->nType == MnType::HighPerformance.index) { + if (!DeleteUniqueProperty(*dmn, dmn->pdmnState->platformNodeID)) { + mnUniquePropertyMap = mnUniquePropertyMapSaved; + throw(std::runtime_error(strprintf("%s: Can't delete a masternode %s with a duplicate platformNodeID=%s", __func__, + dmn->proTxHash.ToString(), dmn->pdmnState->platformNodeID.ToString()))); + } + } + mnMap = mnMap.erase(proTxHash); mnInternalIdMap = mnInternalIdMap.erase(dmn->GetInternalId()); } @@ -658,7 +708,7 @@ bool CDeterministicMNManager::BuildNewListFromBlock(const CBlock& block, const C newList.SetBlockHash(uint256()); // we can't know the final block hash, so better not return a (invalid) block hash newList.SetHeight(nHeight); - auto payee = oldList.GetMNPayee(); + auto payee = oldList.GetMNPayee(pindexPrev); // we iterate the oldList here and update the newList // this is only valid as long these have not diverged at this point, which is the case as long as we don't add @@ -695,7 +745,11 @@ bool CDeterministicMNManager::BuildNewListFromBlock(const CBlock& block, const C return _state.Invalid(ValidationInvalidReason::CONSENSUS, false, REJECT_INVALID, "bad-protx-payload"); } - auto dmn = std::make_shared(newList.GetTotalRegisteredCount()); + if (proTx.nType == MnType::HighPerformance.index && !llmq::utils::IsV19Active(pindexPrev)) { + return _state.Invalid(ValidationInvalidReason::CONSENSUS, false, REJECT_INVALID, "bad-protx-payload"); + } + + auto dmn = std::make_shared(newList.GetTotalRegisteredCount(), proTx.nType == MnType::HighPerformance.index); dmn->proTxHash = tx.GetHash(); // collateralOutpoint is either pointing to an external collateral or to the ProRegTx itself @@ -706,7 +760,8 @@ bool CDeterministicMNManager::BuildNewListFromBlock(const CBlock& block, const C } Coin coin; - if (!proTx.collateralOutpoint.hash.IsNull() && (!view.GetCoin(dmn->collateralOutpoint, coin) || coin.IsSpent() || coin.out.nValue != 1000 * COIN)) { + CAmount expectedCollateral = GetMnType(proTx.nType).collat_amount; + if (!proTx.collateralOutpoint.hash.IsNull() && (!view.GetCoin(dmn->collateralOutpoint, coin) || coin.IsSpent() || coin.out.nValue != expectedCollateral)) { // should actually never get to this point as CheckProRegTx should have handled this case. // We do this additional check nevertheless to be 100% sure return _state.Invalid(ValidationInvalidReason::CONSENSUS, false, REJECT_INVALID, "bad-protx-collateral"); @@ -753,6 +808,10 @@ bool CDeterministicMNManager::BuildNewListFromBlock(const CBlock& block, const C return _state.Invalid(ValidationInvalidReason::CONSENSUS, false, REJECT_INVALID, "bad-protx-payload"); } + if (proTx.nType == MnType::HighPerformance.index && !llmq::utils::IsV19Active(pindexPrev)) { + return _state.Invalid(ValidationInvalidReason::CONSENSUS, false, REJECT_INVALID, "bad-protx-payload"); + } + if (newList.HasUniqueProperty(proTx.addr) && newList.GetUniquePropertyMN(proTx.addr)->proTxHash != proTx.proTxHash) { return _state.Invalid(ValidationInvalidReason::CONSENSUS, false, REJECT_DUPLICATE, "bad-protx-dup-addr"); } @@ -761,10 +820,21 @@ bool CDeterministicMNManager::BuildNewListFromBlock(const CBlock& block, const C if (!dmn) { return _state.Invalid(ValidationInvalidReason::CONSENSUS, false, REJECT_INVALID, "bad-protx-hash"); } + if (proTx.nType == MnType::HighPerformance.index && dmn->nType != MnType::HighPerformance.index) { + return _state.Invalid(ValidationInvalidReason::CONSENSUS, false, REJECT_INVALID, "bad-protx-type"); + } + if (proTx.nType == MnType::Regular.index && dmn->nType != MnType::Regular.index) { + return _state.Invalid(ValidationInvalidReason::CONSENSUS, false, REJECT_INVALID, "bad-protx-type"); + } + auto newState = std::make_shared(*dmn->pdmnState); newState->addr = proTx.addr; newState->scriptOperatorPayout = proTx.scriptOperatorPayout; - + if (proTx.nType == MnType::HighPerformance.index) { + newState->platformNodeID = proTx.platformNodeID; + newState->platformP2PPort = proTx.platformP2PPort; + newState->platformHTTPPort = proTx.platformHTTPPort; + } if (newState->IsBanned()) { // only revive when all keys are set if (newState->pubKeyOperator.Get().IsValid() && !newState->keyIDVoting.IsNull() && !newState->keyIDOwner.IsNull()) { @@ -869,11 +939,43 @@ bool CDeterministicMNManager::BuildNewListFromBlock(const CBlock& block, const C // The payee for the current block was determined by the previous block's list, but it might have disappeared in the // current block. We still pay that MN one last time, however. if (payee && newList.HasMN(payee->proTxHash)) { - auto newState = std::make_shared(*newList.GetMN(payee->proTxHash)->pdmnState); + auto dmn = newList.GetMN(payee->proTxHash); + auto newState = std::make_shared(*dmn->pdmnState); newState->nLastPaidHeight = nHeight; + // Starting from v19 and until v20, HPMN will be paid 4 blocks in a row + // No need to check if v19 is active, since HPMN ProRegTx are allowed only after v19 activation + // TODO: Skip this code once v20 is active + // Note: If the payee wasn't found in the current block that's fine + if (dmn->nType == MnType::HighPerformance.index) { + ++newState->nConsecutivePayments; + if (debugLogs) { + LogPrintf("CDeterministicMNManager::%s -- MN %s is a HPMN, bumping nConsecutivePayments to %d\n", + __func__, dmn->proTxHash.ToString(), newState->nConsecutivePayments); + } + } newList.UpdateMN(payee->proTxHash, newState); + if (debugLogs) { + dmn = newList.GetMN(payee->proTxHash); + LogPrintf("CDeterministicMNManager::%s -- MN %s, nConsecutivePayments=%d\n", + __func__, dmn->proTxHash.ToString(), dmn->pdmnState->nConsecutivePayments); + } } + // reset nConsecutivePayments on non-paid HPMNs + auto newList2 = newList; + newList2.ForEachMN(false, [&](auto& dmn) { + if (payee != nullptr && dmn.proTxHash == payee->proTxHash) return; + if (dmn.nType != MnType::HighPerformance.index) return; + if (dmn.pdmnState->nConsecutivePayments == 0) return; + if (debugLogs) { + LogPrintf("CDeterministicMNManager::%s -- MN %s, reset nConsecutivePayments %d->0\n", + __func__, dmn.proTxHash.ToString(), dmn.pdmnState->nConsecutivePayments); + } + auto newState = std::make_shared(*dmn.pdmnState); + newState->nConsecutivePayments = 0; + newList.UpdateMN(dmn.proTxHash, newState); + }); + mnListRet = std::move(newList); return true; @@ -1013,7 +1115,10 @@ bool CDeterministicMNManager::IsProTxWithCollateral(const CTransactionRef& tx, u if (proTx.collateralOutpoint.n >= tx->vout.size() || proTx.collateralOutpoint.n != n) { return false; } - if (tx->vout[n].nValue != 1000 * COIN) { + + const CAmount expectedCollateral = GetMnType(proTx.nType).collat_amount; + + if (tx->vout[n].nValue != expectedCollateral) { return false; } return true; @@ -1074,6 +1179,110 @@ void CDeterministicMNManager::CleanupCache(int nHeight) } } +void CDeterministicMNManager::MigrateDiff(CDBBatch& batch, const CBlockIndex* pindexNext, const CDeterministicMNList& curMNList, CDeterministicMNList& newMNList) +{ + static const std::string DB_OLD_LIST_DIFF = "dmn_D"; + + CDataStream diff_data(SER_DISK, CLIENT_VERSION); + if (!m_evoDb.GetRawDB().ReadDataStream(std::make_pair(DB_OLD_LIST_DIFF, pindexNext->GetBlockHash()), diff_data)) { + newMNList = curMNList; + newMNList.SetBlockHash(pindexNext->GetBlockHash()); + newMNList.SetHeight(pindexNext->nHeight); + return; + } + + CDeterministicMNListDiff mndiff; + mndiff.Unserialize(diff_data, CDeterministicMN::CURRENT_MN_FORMAT); + + // At this point, all masternodes included in the diff are set as regular masternodes by default. + // We can then write them in evoDB as the new serialisation format includes the type + + batch.Write(std::make_pair(DB_LIST_DIFF, pindexNext->GetBlockHash()), mndiff); + + // applies added/removed MNs + newMNList = curMNList.ApplyDiff(pindexNext, mndiff); +} + +bool CDeterministicMNManager::MigrateDBIfNeeded() +{ + static const std::string DB_OLD_LIST_SNAPSHOT = "dmn_S"; + static const std::string DB_OLD_LIST_DIFF = "dmn_D"; + static const std::string DB_OLD_BEST_BLOCK = "b_b2"; + + LOCK(cs_main); + + LogPrintf("CDeterministicMNManager::%s -- upgrading DB to migrate MN type\n", __func__); + + if (::ChainActive().Tip() == nullptr) { + // should have no records + LogPrintf("CDeterministicMNManager::%s -- Chain empty. evoDB:%d.\n", __func__, m_evoDb.IsEmpty()); + return m_evoDb.IsEmpty(); + } + + if (m_evoDb.GetRawDB().Exists(EVODB_BEST_BLOCK)) { + LogPrintf("CDeterministicMNManager::%s -- migration already done. skipping.\n", __func__); + return true; + } + + // Removing the old EVODB_BEST_BLOCK value early results in older version to crash immediately, even if the upgrade + // process is cancelled in-between. But if the new version sees that the old EVODB_BEST_BLOCK is already removed, + // then we must assume that the upgrade process was already running before but was interrupted. + if (::ChainActive().Height() > 1 && !m_evoDb.GetRawDB().Exists(DB_OLD_BEST_BLOCK)) { + LogPrintf("CDeterministicMNManager::%s -- previous migration attempt failed.\n", __func__); + return false; + } + m_evoDb.GetRawDB().Erase(DB_OLD_BEST_BLOCK); + + if (::ChainActive().Height() < Params().GetConsensus().DIP0003Height) { + // not reached DIP3 height yet, so no upgrade needed + LogPrintf("CDeterministicMNManager::%s -- migration not needed. dip3 not reached\n", __func__); + auto dbTx = m_evoDb.BeginTransaction(); + m_evoDb.WriteBestBlock(::ChainActive().Tip()->GetBlockHash()); + dbTx->Commit(); + return true; + } + + CDBBatch batch(m_evoDb.GetRawDB()); + + CDeterministicMNList curMNList; + curMNList.SetHeight(Params().GetConsensus().DIP0003Height - 1); + curMNList.SetBlockHash(::ChainActive()[Params().GetConsensus().DIP0003Height - 1]->GetBlockHash()); + + for (const auto nHeight : irange::range(Params().GetConsensus().DIP0003Height, ::ChainActive().Height() + 1)) { + auto pindex = ::ChainActive()[nHeight]; + + CDeterministicMNList newMNList; + MigrateDiff(batch, pindex, curMNList, newMNList); + + if ((nHeight % DISK_SNAPSHOT_PERIOD) == 0) { + batch.Write(std::make_pair(DB_LIST_SNAPSHOT, pindex->GetBlockHash()), newMNList); + m_evoDb.GetRawDB().WriteBatch(batch); + batch.Clear(); + } + + curMNList = newMNList; + } + + m_evoDb.GetRawDB().WriteBatch(batch); + + // Writing EVODB_BEST_BLOCK (which is b_b3 now) marks the DB as upgraded + auto dbTx = m_evoDb.BeginTransaction(); + m_evoDb.WriteBestBlock(::ChainActive().Tip()->GetBlockHash()); + dbTx->Commit(); + + LogPrintf("CDeterministicMNManager::%s -- done migrating\n", __func__); + + m_evoDb.GetRawDB().Erase(DB_OLD_LIST_DIFF); + m_evoDb.GetRawDB().Erase(DB_OLD_LIST_SNAPSHOT); + + LogPrintf("CDeterministicMNManager::%s -- done cleaning old data\n", __func__); + + m_evoDb.GetRawDB().CompactFull(); + + LogPrintf("CDeterministicMNManager::%s -- done compacting database\n", __func__); + + return true; +} template static bool CheckService(const ProTx& proTx, CValidationState& state) @@ -1101,6 +1310,44 @@ static bool CheckService(const ProTx& proTx, CValidationState& state) return true; } +template +static bool CheckPlatformFields(const ProTx& proTx, CValidationState& state) +{ + if (proTx.platformNodeID.IsNull()) { + return state.Invalid(ValidationInvalidReason::TX_BAD_SPECIAL, false, REJECT_INVALID, "bad-protx-platform-nodeid"); + } + + static int mainnetPlatformP2PPort = CreateChainParams(CBaseChainParams::MAIN)->GetDefaultPlatformP2PPort(); + if (Params().NetworkIDString() == CBaseChainParams::MAIN) { + if (proTx.platformP2PPort != mainnetPlatformP2PPort) { + return state.Invalid(ValidationInvalidReason::TX_BAD_SPECIAL, false, REJECT_INVALID, "bad-protx-platform-p2p-port"); + } + } + + static int mainnetPlatformHTTPPort = CreateChainParams(CBaseChainParams::MAIN)->GetDefaultPlatformHTTPPort(); + if (Params().NetworkIDString() == CBaseChainParams::MAIN) { + if (proTx.platformHTTPPort != mainnetPlatformHTTPPort) { + return state.Invalid(ValidationInvalidReason::TX_BAD_SPECIAL, false, REJECT_INVALID, "bad-protx-platform-http-port"); + } + } + + static int mainnetDefaultP2PPort = CreateChainParams(CBaseChainParams::MAIN)->GetDefaultPort(); + if (proTx.platformP2PPort == mainnetDefaultP2PPort) { + return state.Invalid(ValidationInvalidReason::TX_BAD_SPECIAL, false, REJECT_INVALID, "bad-protx-platform-p2p-port"); + } + if (proTx.platformHTTPPort == mainnetDefaultP2PPort) { + return state.Invalid(ValidationInvalidReason::TX_BAD_SPECIAL, false, REJECT_INVALID, "bad-protx-platform-http-port"); + } + + if (proTx.platformP2PPort == proTx.platformHTTPPort || + proTx.platformP2PPort == proTx.addr.GetPort() || + proTx.platformHTTPPort == proTx.addr.GetPort()) { + return state.Invalid(ValidationInvalidReason::TX_BAD_SPECIAL, false, REJECT_INVALID, "bad-protx-platform-dup-ports"); + } + + return true; +} + template static bool CheckHashSig(const ProTx& proTx, const PKHash& pkhash, CValidationState& state) { @@ -1152,13 +1399,21 @@ bool CheckProRegTx(const CTransaction& tx, const CBlockIndex* pindexPrev, CValid return false; } + if (ptx.nType == MnType::HighPerformance.index) { + if (!CheckPlatformFields(ptx, state)) { + return false; + } + } + CTxDestination collateralTxDest; const PKHash *keyForPayloadSig = nullptr; COutPoint collateralOutpoint; + CAmount expectedCollateral = GetMnType(ptx.nType).collat_amount; + if (!ptx.collateralOutpoint.hash.IsNull()) { Coin coin; - if (!view.GetCoin(ptx.collateralOutpoint, coin) || coin.IsSpent() || coin.out.nValue != 1000 * COIN) { + if (!view.GetCoin(ptx.collateralOutpoint, coin) || coin.IsSpent() || coin.out.nValue != expectedCollateral) { return state.Invalid(ValidationInvalidReason::TX_BAD_SPECIAL, false, REJECT_INVALID, "bad-protx-collateral"); } @@ -1178,7 +1433,7 @@ bool CheckProRegTx(const CTransaction& tx, const CBlockIndex* pindexPrev, CValid if (ptx.collateralOutpoint.n >= tx.vout.size()) { return state.Invalid(ValidationInvalidReason::TX_BAD_SPECIAL, false, REJECT_INVALID, "bad-protx-collateral-index"); } - if (tx.vout[ptx.collateralOutpoint.n].nValue != 1000 * COIN) { + if (tx.vout[ptx.collateralOutpoint.n].nValue != expectedCollateral) { return state.Invalid(ValidationInvalidReason::TX_BAD_SPECIAL, false, REJECT_INVALID, "bad-protx-collateral"); } @@ -1255,6 +1510,12 @@ bool CheckProUpServTx(const CTransaction& tx, const CBlockIndex* pindexPrev, CVa return false; } + if (ptx.nType == MnType::HighPerformance.index) { + if (!CheckPlatformFields(ptx, state)) { + return false; + } + } + if (pindexPrev) { auto mnList = deterministicMNManager->GetListForBlock(pindexPrev); auto mn = mnList.GetMN(ptx.proTxHash); diff --git a/src/evo/deterministicmns.h b/src/evo/deterministicmns.h index 1de7edc8cce1..701a07fa3e03 100644 --- a/src/evo/deterministicmns.h +++ b/src/evo/deterministicmns.h @@ -8,8 +8,9 @@ #include #include -#include #include +#include +#include #include #include #include @@ -40,18 +41,17 @@ class CDeterministicMN uint64_t internalId{std::numeric_limits::max()}; public: + static constexpr uint16_t CURRENT_MN_FORMAT = 0; + static constexpr uint16_t MN_TYPE_FORMAT = 1; + CDeterministicMN() = delete; // no default constructor, must specify internalId - explicit CDeterministicMN(uint64_t _internalId) : internalId(_internalId) + explicit CDeterministicMN(uint64_t _internalId, bool highPerformanceMasternode = false) : + internalId(_internalId), + nType(highPerformanceMasternode ? MnType::HighPerformance.index : MnType::Regular.index) { // only non-initial values assert(_internalId != std::numeric_limits::max()); } - // TODO: can be removed in a future version - CDeterministicMN(CDeterministicMN mn, uint64_t _internalId) : CDeterministicMN(std::move(mn)) { - // only non-initial values - assert(_internalId != std::numeric_limits::max()); - internalId = _internalId; - } template CDeterministicMN(deserialize_type, Stream& s) @@ -62,30 +62,47 @@ class CDeterministicMN uint256 proTxHash; COutPoint collateralOutpoint; uint16_t nOperatorReward{0}; + uint16_t nType{MnType::Regular.index}; std::shared_ptr pdmnState; template - inline void SerializationOp(Stream& s, Operation ser_action, bool oldFormat) + inline void SerializationOp(Stream& s, Operation ser_action, const uint8_t format_version) { READWRITE(proTxHash); - if (!oldFormat) { - READWRITE(VARINT(internalId)); - } + READWRITE(VARINT(internalId)); READWRITE(collateralOutpoint); READWRITE(nOperatorReward); - READWRITE(pdmnState); + // We need to read CDeterministicMNState using the old format only when called with CURRENT_MN_FORMAT on Unserialize() + // Serialisation (writing) will be done always using new format + if (ser_action.ForRead() && format_version == CURRENT_MN_FORMAT) { + CDeterministicMNState_Oldformat old_state; + READWRITE(old_state); + pdmnState = std::make_shared(old_state); + } else { + READWRITE(pdmnState); + } + // We need to read/write nType if: + // format_version is set to MN_TYPE_FORMAT (For writing (serialisation) it is always the case) Needed for the MNLISTDIFF Migration in evoDB + // We can't know if we are serialising for the Disk or for the Network here (s.GetType() is not accessible) + // Therefore if s.GetVersion() == CLIENT_VERSION -> Then we know we are serialising for the Disk + // Otherwise, we can safely check with protocol versioning logic so we won't break old clients + if (format_version >= MN_TYPE_FORMAT && (s.GetVersion() == CLIENT_VERSION || s.GetVersion() >= DMN_TYPE_PROTO_VERSION)) { + READWRITE(nType); + } else { + nType = MnType::Regular.index; + } } template void Serialize(Stream& s) const { - const_cast(this)->SerializationOp(s, CSerActionSerialize(), false); + const_cast(this)->SerializationOp(s, CSerActionSerialize(), MN_TYPE_FORMAT); } - template - void Unserialize(Stream& s, bool oldFormat = false) + template + void Unserialize(Stream& s, const uint8_t format_version = MN_TYPE_FORMAT) { - SerializationOp(s, CSerActionUnserialize(), oldFormat); + SerializationOp(s, CSerActionUnserialize(), format_version); } [[nodiscard]] uint64_t GetInternalId() const; @@ -208,6 +225,11 @@ class CDeterministicMNList return ranges::count_if(mnMap, [](const auto& p){ return IsMNValid(*p.second); }); } + [[nodiscard]] size_t GetAllHPMNsCount() const + { + return ranges::count_if(mnMap, [](const auto& p) { return p.second->nType == MnType::HighPerformance.index; }); + } + /** * Execute a callback on all masternodes in the mnList. This will pass a reference * of each masternode to the callback function. This should be preferred over ForEachMNShared. @@ -286,7 +308,7 @@ class CDeterministicMNList [[nodiscard]] CDeterministicMNCPtr GetValidMNByCollateral(const COutPoint& collateralOutpoint) const; [[nodiscard]] CDeterministicMNCPtr GetMNByService(const CService& service) const; [[nodiscard]] CDeterministicMNCPtr GetMNByInternalId(uint64_t internalId) const; - [[nodiscard]] CDeterministicMNCPtr GetMNPayee() const; + [[nodiscard]] CDeterministicMNCPtr GetMNPayee(const CBlockIndex* pIndex) const; /** * Calculates the projected MN payees for the next *count* blocks. The result is not guaranteed to be correct @@ -302,8 +324,8 @@ class CDeterministicMNList * @param modifier * @return */ - [[nodiscard]] std::vector CalculateQuorum(size_t maxSize, const uint256& modifier) const; - [[nodiscard]] std::vector> CalculateScores(const uint256& modifier) const; + [[nodiscard]] std::vector CalculateQuorum(size_t maxSize, const uint256& modifier, const bool onlyHighPerformanceMasternodes = false) const; + [[nodiscard]] std::vector> CalculateScores(const uint256& modifier, const bool onlyHighPerformanceMasternodes) const; /** * Calculates the maximum penalty which is allowed at the height of this MN list. It is dynamic and might change @@ -448,18 +470,29 @@ class CDeterministicMNListDiff } } - template - void Unserialize(Stream& s) + template + void Unserialize(Stream& s, const uint8_t format_version = CDeterministicMN::MN_TYPE_FORMAT) { updatedMNs.clear(); removedMns.clear(); size_t tmp; uint64_t tmp2; - s >> addedMNs; + tmp = ReadCompactSize(s); + for (size_t i = 0; i < tmp; i++) { + CDeterministicMN mn(0); + // Unserialise CDeterministicMN using CURRENT_MN_FORMAT and set it's type to the default value TYPE_REGULAR_MASTERNODE + // It will be later written with format MN_TYPE_FORMAT which includes the type field. + mn.Unserialize(s, format_version); + auto dmn = std::make_shared(mn); + addedMNs.push_back(dmn); + } tmp = ReadCompactSize(s); for (size_t i = 0; i < tmp; i++) { CDeterministicMNStateDiff diff; + // CDeterministicMNState hold new fields {nConsecutivePayments, platformNodeID, platformP2PPort, platformHTTPPort} but no migration is needed here since: + // CDeterministicMNStateDiff is always serialised using a bitmask. + // Because the new field have a new bit guide value then we are good to continue tmp2 = ReadVarInt(s); s >> diff; updatedMNs.emplace(tmp2, std::move(diff)); @@ -527,6 +560,8 @@ class CDeterministicMNManager bool IsDIP3Enforced(int nHeight = -1); + void MigrateDiff(CDBBatch& batch, const CBlockIndex* pindexNext, const CDeterministicMNList& curMNList, CDeterministicMNList& newMNList); + bool MigrateDBIfNeeded(); void DoMaintenance(); diff --git a/src/evo/dmn_types.h b/src/evo/dmn_types.h new file mode 100644 index 000000000000..b69cb9ced323 --- /dev/null +++ b/src/evo/dmn_types.h @@ -0,0 +1,45 @@ +// Copyright (c) 2023 The Dash Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_EVO_DMN_TYPES_H +#define BITCOIN_EVO_DMN_TYPES_H + +#include +#include +#include + +class CDeterministicMNType +{ +public: + uint8_t index; + int32_t voting_weight; + CAmount collat_amount; + std::string_view description; +}; + +namespace MnType { +constexpr auto Regular = CDeterministicMNType{ + .index = 0, + .voting_weight = 1, + .collat_amount = 1000 * COIN, + .description = "Regular", +}; +constexpr auto HighPerformance = CDeterministicMNType{ + .index = 1, + .voting_weight = 4, + .collat_amount = 4000 * COIN, + .description = "HighPerformance", +}; +} // namespace MnType + +constexpr const auto& GetMnType(int index) +{ + switch (index) { + case 0: return MnType::Regular; + case 1: return MnType::HighPerformance; + default: assert(false); + } +} + +#endif // BITCOIN_EVO_DMN_TYPES_H diff --git a/src/evo/dmnstate.cpp b/src/evo/dmnstate.cpp index c793fed55ded..267c3148da59 100644 --- a/src/evo/dmnstate.cpp +++ b/src/evo/dmnstate.cpp @@ -39,6 +39,7 @@ void CDeterministicMNState::ToJson(UniValue& obj) const obj.pushKV("service", addr.ToStringIPPort(false)); obj.pushKV("registeredHeight", nRegisteredHeight); obj.pushKV("lastPaidHeight", nLastPaidHeight); + obj.pushKV("consecutivePayments", nConsecutivePayments); obj.pushKV("PoSePenalty", nPoSePenalty); obj.pushKV("PoSeRevivedHeight", nPoSeRevivedHeight); obj.pushKV("PoSeBanHeight", nPoSeBanHeight); diff --git a/src/evo/dmnstate.h b/src/evo/dmnstate.h index 634ca2f5aa2c..e772a4e89226 100644 --- a/src/evo/dmnstate.h +++ b/src/evo/dmnstate.h @@ -25,6 +25,53 @@ namespace llmq class CFinalCommitment; } // namespace llmq +// TODO: To remove this in the future +class CDeterministicMNState_Oldformat +{ +private: + int nPoSeBanHeight{-1}; + + friend class CDeterministicMNStateDiff; + friend class CDeterministicMNState; + +public: + int nRegisteredHeight{-1}; + int nLastPaidHeight{0}; + int nPoSePenalty{0}; + int nPoSeRevivedHeight{-1}; + uint16_t nRevocationReason{CProUpRevTx::REASON_NOT_SPECIFIED}; + uint256 confirmedHash; + uint256 confirmedHashWithProRegTxHash; + CKeyID keyIDOwner; + CBLSLazyPublicKey pubKeyOperator; + CKeyID keyIDVoting; + CService addr; + CScript scriptPayout; + CScript scriptOperatorPayout; + +public: + CDeterministicMNState_Oldformat() = default; + + SERIALIZE_METHODS(CDeterministicMNState_Oldformat, obj) + { + READWRITE( + obj.nRegisteredHeight, + obj.nLastPaidHeight, + obj.nPoSePenalty, + obj.nPoSeRevivedHeight, + obj.nPoSeBanHeight, + obj.nRevocationReason, + obj.confirmedHash, + obj.confirmedHashWithProRegTxHash, + obj.keyIDOwner, + obj.pubKeyOperator, + obj.keyIDVoting, + obj.addr, + obj.scriptPayout, + obj.scriptOperatorPayout); + } +}; + class CDeterministicMNState { private: @@ -35,6 +82,7 @@ class CDeterministicMNState public: int nRegisteredHeight{-1}; int nLastPaidHeight{0}; + int nConsecutivePayments{0}; int nPoSePenalty{0}; int nPoSeRevivedHeight{-1}; uint16_t nRevocationReason{CProUpRevTx::REASON_NOT_SPECIFIED}; @@ -52,16 +100,38 @@ class CDeterministicMNState CScript scriptPayout; CScript scriptOperatorPayout; + uint160 platformNodeID{}; + uint16_t platformP2PPort{0}; + uint16_t platformHTTPPort{0}; + public: CDeterministicMNState() = default; explicit CDeterministicMNState(const CProRegTx& proTx) : - keyIDOwner(proTx.keyIDOwner), - keyIDVoting(proTx.keyIDVoting), - addr(proTx.addr), - scriptPayout(proTx.scriptPayout) + keyIDOwner(proTx.keyIDOwner), + keyIDVoting(proTx.keyIDVoting), + addr(proTx.addr), + scriptPayout(proTx.scriptPayout), + platformNodeID(proTx.platformNodeID), + platformP2PPort(proTx.platformP2PPort), + platformHTTPPort(proTx.platformHTTPPort) { pubKeyOperator.Set(proTx.pubKeyOperator); } + explicit CDeterministicMNState(const CDeterministicMNState_Oldformat& s) : + nPoSeBanHeight(s.nPoSeBanHeight), + nRegisteredHeight(s.nRegisteredHeight), + nLastPaidHeight(s.nLastPaidHeight), + nPoSePenalty(s.nPoSePenalty), + nPoSeRevivedHeight(s.nPoSeRevivedHeight), + nRevocationReason(s.nRevocationReason), + confirmedHash(s.confirmedHash), + confirmedHashWithProRegTxHash(s.confirmedHashWithProRegTxHash), + keyIDOwner(s.keyIDOwner), + pubKeyOperator(s.pubKeyOperator), + keyIDVoting(s.keyIDVoting), + addr(s.addr), + scriptPayout(s.scriptPayout), + scriptOperatorPayout(s.scriptOperatorPayout) {} template CDeterministicMNState(deserialize_type, Stream& s) { @@ -71,21 +141,24 @@ class CDeterministicMNState SERIALIZE_METHODS(CDeterministicMNState, obj) { READWRITE( - obj.nRegisteredHeight, - obj.nLastPaidHeight, - obj.nPoSePenalty, - obj.nPoSeRevivedHeight, - obj.nPoSeBanHeight, - obj.nRevocationReason, - obj.confirmedHash, - obj.confirmedHashWithProRegTxHash, - obj.keyIDOwner, - obj.pubKeyOperator, - obj.keyIDVoting, - obj.addr, - obj.scriptPayout, - obj.scriptOperatorPayout - ); + obj.nRegisteredHeight, + obj.nLastPaidHeight, + obj.nConsecutivePayments, + obj.nPoSePenalty, + obj.nPoSeRevivedHeight, + obj.nPoSeBanHeight, + obj.nRevocationReason, + obj.confirmedHash, + obj.confirmedHashWithProRegTxHash, + obj.keyIDOwner, + obj.pubKeyOperator, + obj.keyIDVoting, + obj.addr, + obj.scriptPayout, + obj.scriptOperatorPayout, + obj.platformNodeID, + obj.platformP2PPort, + obj.platformHTTPPort); } void ResetOperatorFields() @@ -94,6 +167,7 @@ class CDeterministicMNState addr = CService(); scriptOperatorPayout = CScript(); nRevocationReason = CProUpRevTx::REASON_NOT_SPECIFIED; + platformNodeID = uint160(); } void BanIfNotBanned(int height) { @@ -133,37 +207,45 @@ class CDeterministicMNStateDiff { public: enum Field : uint32_t { - Field_nRegisteredHeight = 0x0001, - Field_nLastPaidHeight = 0x0002, - Field_nPoSePenalty = 0x0004, - Field_nPoSeRevivedHeight = 0x0008, - Field_nPoSeBanHeight = 0x0010, - Field_nRevocationReason = 0x0020, - Field_confirmedHash = 0x0040, - Field_confirmedHashWithProRegTxHash = 0x0080, - Field_keyIDOwner = 0x0100, - Field_pubKeyOperator = 0x0200, - Field_keyIDVoting = 0x0400, - Field_addr = 0x0800, - Field_scriptPayout = 0x1000, - Field_scriptOperatorPayout = 0x2000, + Field_nRegisteredHeight = 0x0001, + Field_nLastPaidHeight = 0x0002, + Field_nPoSePenalty = 0x0004, + Field_nPoSeRevivedHeight = 0x0008, + Field_nPoSeBanHeight = 0x0010, + Field_nRevocationReason = 0x0020, + Field_confirmedHash = 0x0040, + Field_confirmedHashWithProRegTxHash = 0x0080, + Field_keyIDOwner = 0x0100, + Field_pubKeyOperator = 0x0200, + Field_keyIDVoting = 0x0400, + Field_addr = 0x0800, + Field_scriptPayout = 0x1000, + Field_scriptOperatorPayout = 0x2000, + Field_nConsecutivePayments = 0x4000, + Field_platformNodeID = 0x8000, + Field_platformP2PPort = 0x10000, + Field_platformHTTPPort = 0x20000, }; -#define DMN_STATE_DIFF_ALL_FIELDS \ - DMN_STATE_DIFF_LINE(nRegisteredHeight) \ - DMN_STATE_DIFF_LINE(nLastPaidHeight) \ - DMN_STATE_DIFF_LINE(nPoSePenalty) \ - DMN_STATE_DIFF_LINE(nPoSeRevivedHeight) \ - DMN_STATE_DIFF_LINE(nPoSeBanHeight) \ - DMN_STATE_DIFF_LINE(nRevocationReason) \ - DMN_STATE_DIFF_LINE(confirmedHash) \ +#define DMN_STATE_DIFF_ALL_FIELDS \ + DMN_STATE_DIFF_LINE(nRegisteredHeight) \ + DMN_STATE_DIFF_LINE(nLastPaidHeight) \ + DMN_STATE_DIFF_LINE(nPoSePenalty) \ + DMN_STATE_DIFF_LINE(nPoSeRevivedHeight) \ + DMN_STATE_DIFF_LINE(nPoSeBanHeight) \ + DMN_STATE_DIFF_LINE(nRevocationReason) \ + DMN_STATE_DIFF_LINE(confirmedHash) \ DMN_STATE_DIFF_LINE(confirmedHashWithProRegTxHash) \ - DMN_STATE_DIFF_LINE(keyIDOwner) \ - DMN_STATE_DIFF_LINE(pubKeyOperator) \ - DMN_STATE_DIFF_LINE(keyIDVoting) \ - DMN_STATE_DIFF_LINE(addr) \ - DMN_STATE_DIFF_LINE(scriptPayout) \ - DMN_STATE_DIFF_LINE(scriptOperatorPayout) + DMN_STATE_DIFF_LINE(keyIDOwner) \ + DMN_STATE_DIFF_LINE(pubKeyOperator) \ + DMN_STATE_DIFF_LINE(keyIDVoting) \ + DMN_STATE_DIFF_LINE(addr) \ + DMN_STATE_DIFF_LINE(scriptPayout) \ + DMN_STATE_DIFF_LINE(scriptOperatorPayout) \ + DMN_STATE_DIFF_LINE(nConsecutivePayments) \ + DMN_STATE_DIFF_LINE(platformNodeID) \ + DMN_STATE_DIFF_LINE(platformP2PPort) \ + DMN_STATE_DIFF_LINE(platformHTTPPort) public: uint32_t fields{0}; diff --git a/src/evo/evodb.h b/src/evo/evodb.h index 5758fe36d976..9334603f24f3 100644 --- a/src/evo/evodb.h +++ b/src/evo/evodb.h @@ -11,7 +11,8 @@ // "b_b" was used in the initial version of deterministic MN storage // "b_b2" was used after compact diffs were introduced -static const std::string EVODB_BEST_BLOCK = "b_b2"; +// "b_b3" was used after masternode type introduction in evoDB +static const std::string EVODB_BEST_BLOCK = "b_b3"; class CEvoDB; diff --git a/src/evo/providertx.cpp b/src/evo/providertx.cpp index edb28c20aedb..4d1139f9a2ab 100644 --- a/src/evo/providertx.cpp +++ b/src/evo/providertx.cpp @@ -2,6 +2,7 @@ // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. +#include #include #include @@ -14,7 +15,7 @@ maybe_error CProRegTx::IsTriviallyValid(bool is_bls_legacy_scheme) const if (nVersion == 0 || nVersion > GetVersion(is_bls_legacy_scheme)) { return {ValidationInvalidReason::CONSENSUS, "bad-protx-version"}; } - if (nType != 0) { + if (nType != MnType::Regular.index && nType != MnType::HighPerformance.index) { return {ValidationInvalidReason::CONSENSUS, "bad-protx-type"}; } if (nMode != 0) { @@ -78,8 +79,8 @@ std::string CProRegTx::ToString() const payee = EncodeDestination(dest); } - return strprintf("CProRegTx(nVersion=%d, collateralOutpoint=%s, addr=%s, nOperatorReward=%f, ownerAddress=%s, pubKeyOperator=%s, votingAddress=%s, scriptPayout=%s)", - nVersion, collateralOutpoint.ToStringShort(), addr.ToString(), (double)nOperatorReward / 100, EncodeDestination(PKHash(keyIDOwner)), pubKeyOperator.ToString(nVersion == LEGACY_BLS_VERSION), EncodeDestination(PKHash(keyIDVoting)), payee); + return strprintf("CProRegTx(nVersion=%d, nType=%d, collateralOutpoint=%s, addr=%s, nOperatorReward=%f, ownerAddress=%s, pubKeyOperator=%s, votingAddress=%s, scriptPayout=%s, platformNodeID=%s, platformP2PPort=%d, platformHTTPPort=%d)", + nVersion, nType, collateralOutpoint.ToStringShort(), addr.ToString(), (double)nOperatorReward / 100, EncodeDestination(PKHash(keyIDOwner)), pubKeyOperator.ToString(nVersion == LEGACY_BLS_VERSION), EncodeDestination(PKHash(keyIDVoting)), payee, platformNodeID.ToString(), platformP2PPort, platformHTTPPort); } maybe_error CProUpServTx::IsTriviallyValid(bool is_bls_legacy_scheme) const @@ -99,8 +100,8 @@ std::string CProUpServTx::ToString() const payee = EncodeDestination(dest); } - return strprintf("CProUpServTx(nVersion=%d, proTxHash=%s, addr=%s, operatorPayoutAddress=%s)", - nVersion, proTxHash.ToString(), addr.ToString(), payee); + return strprintf("CProUpServTx(nVersion=%d, nType=%d, proTxHash=%s, addr=%s, operatorPayoutAddress=%s, platformNodeID=%s, platformP2PPort=%d, platformHTTPPort=%d)", + nVersion, nType, proTxHash.ToString(), addr.ToString(), payee, platformNodeID.ToString(), platformP2PPort, platformHTTPPort); } maybe_error CProUpRegTx::IsTriviallyValid(bool is_bls_legacy_scheme) const diff --git a/src/evo/providertx.h b/src/evo/providertx.h index e8f84ec376fe..341ac1a12ce3 100644 --- a/src/evo/providertx.h +++ b/src/evo/providertx.h @@ -10,6 +10,7 @@ #include #include +#include #include #include #include @@ -33,10 +34,13 @@ class CProRegTx } uint16_t nVersion{LEGACY_BLS_VERSION}; // message version - uint16_t nType{0}; // only 0 supported for now + uint16_t nType{MnType::Regular.index}; uint16_t nMode{0}; // only 0 supported for now COutPoint collateralOutpoint{uint256(), (uint32_t)-1}; // if hash is null, we refer to a ProRegTx output CService addr; + uint160 platformNodeID{}; + uint16_t platformP2PPort{0}; + uint16_t platformHTTPPort{0}; CKeyID keyIDOwner; CBLSPublicKey pubKeyOperator; CKeyID keyIDVoting; @@ -66,6 +70,12 @@ class CProRegTx obj.scriptPayout, obj.inputsHash ); + if (obj.nVersion == BASIC_BLS_VERSION && obj.nType == MnType::HighPerformance.index) { + READWRITE( + obj.platformNodeID, + obj.platformP2PPort, + obj.platformHTTPPort); + } if (!(s.GetType() & SER_GETHASH)) { READWRITE(obj.vchSig); } @@ -82,6 +92,7 @@ class CProRegTx obj.clear(); obj.setObject(); obj.pushKV("version", nVersion); + obj.pushKV("type", nType); obj.pushKV("collateralHash", collateralOutpoint.hash.ToString()); obj.pushKV("collateralIndex", (int)collateralOutpoint.n); obj.pushKV("service", addr.ToString(false)); @@ -94,7 +105,11 @@ class CProRegTx } obj.pushKV("pubKeyOperator", pubKeyOperator.ToString(nVersion == LEGACY_BLS_VERSION)); obj.pushKV("operatorReward", (double)nOperatorReward / 100); - + if (nType == MnType::HighPerformance.index) { + obj.pushKV("platformNodeID", platformNodeID.ToString()); + obj.pushKV("platformP2PPort", platformP2PPort); + obj.pushKV("platformHTTPPort", platformHTTPPort); + } obj.pushKV("inputsHash", inputsHash.ToString()); } @@ -114,8 +129,12 @@ class CProUpServTx } uint16_t nVersion{LEGACY_BLS_VERSION}; // message version + uint16_t nType{MnType::Regular.index}; uint256 proTxHash; CService addr; + uint160 platformNodeID{}; + uint16_t platformP2PPort{0}; + uint16_t platformHTTPPort{0}; CScript scriptOperatorPayout; uint256 inputsHash; // replay protection CBLSSignature sig; @@ -129,12 +148,22 @@ class CProUpServTx // unknown version, bail out early return; } + if (obj.nVersion == BASIC_BLS_VERSION) { + READWRITE( + obj.nType); + } READWRITE( obj.proTxHash, obj.addr, obj.scriptOperatorPayout, obj.inputsHash ); + if (obj.nVersion == BASIC_BLS_VERSION && obj.nType == MnType::HighPerformance.index) { + READWRITE( + obj.platformNodeID, + obj.platformP2PPort, + obj.platformHTTPPort); + } if (!(s.GetType() & SER_GETHASH)) { READWRITE( CBLSSignatureVersionWrapper(const_cast(obj.sig), (obj.nVersion == LEGACY_BLS_VERSION), true) @@ -149,12 +178,18 @@ class CProUpServTx obj.clear(); obj.setObject(); obj.pushKV("version", nVersion); + obj.pushKV("type", nType); obj.pushKV("proTxHash", proTxHash.ToString()); obj.pushKV("service", addr.ToString(false)); CTxDestination dest; if (ExtractDestination(scriptOperatorPayout, dest)) { obj.pushKV("operatorPayoutAddress", EncodeDestination(dest)); } + if (nType == MnType::HighPerformance.index) { + obj.pushKV("platformNodeID", platformNodeID.ToString()); + obj.pushKV("platformP2PPort", platformP2PPort); + obj.pushKV("platformHTTPPort", platformHTTPPort); + } obj.pushKV("inputsHash", inputsHash.ToString()); } diff --git a/src/evo/simplifiedmns.cpp b/src/evo/simplifiedmns.cpp index 926a75eddd73..97447ef7365d 100644 --- a/src/evo/simplifiedmns.cpp +++ b/src/evo/simplifiedmns.cpp @@ -30,7 +30,10 @@ CSimplifiedMNListEntry::CSimplifiedMNListEntry(const CDeterministicMN& dmn) : keyIDVoting(dmn.pdmnState->keyIDVoting), isValid(!dmn.pdmnState->IsBanned()), scriptPayout(dmn.pdmnState->scriptPayout), - scriptOperatorPayout(dmn.pdmnState->scriptOperatorPayout) + scriptOperatorPayout(dmn.pdmnState->scriptOperatorPayout), + nType(dmn.nType), + platformHTTPPort(dmn.pdmnState->platformHTTPPort), + platformNodeID(dmn.pdmnState->platformNodeID) { } @@ -53,8 +56,8 @@ std::string CSimplifiedMNListEntry::ToString() const operatorPayoutAddress = EncodeDestination(dest); } - return strprintf("CSimplifiedMNListEntry(proRegTxHash=%s, confirmedHash=%s, service=%s, pubKeyOperator=%s, votingAddress=%s, isValid=%d, payoutAddress=%s, operatorPayoutAddress=%s)", - proRegTxHash.ToString(), confirmedHash.ToString(), service.ToString(false), pubKeyOperator.Get().ToString(), EncodeDestination(PKHash(keyIDVoting)), isValid, payoutAddress, operatorPayoutAddress); + return strprintf("CSimplifiedMNListEntry(nType=%d, proRegTxHash=%s, confirmedHash=%s, service=%s, pubKeyOperator=%s, votingAddress=%s, isValid=%d, payoutAddress=%s, operatorPayoutAddress=%s, platformHTTPPort=%d, platformNodeID=%s)", + nType, proRegTxHash.ToString(), confirmedHash.ToString(), service.ToString(false), pubKeyOperator.Get().ToString(), EncodeDestination(PKHash(keyIDVoting)), isValid, payoutAddress, operatorPayoutAddress, platformHTTPPort, platformNodeID.ToString()); } void CSimplifiedMNListEntry::ToJson(UniValue& obj, bool extended) const @@ -68,6 +71,11 @@ void CSimplifiedMNListEntry::ToJson(UniValue& obj, bool extended) const obj.pushKV("votingAddress", EncodeDestination(PKHash(keyIDVoting))); obj.pushKV("isValid", isValid); obj.pushKV("nVersion", nVersion); + obj.pushKV("nType", nType); + if (nType == MnType::HighPerformance.index) { + obj.pushKV("platformHTTPPort", platformHTTPPort); + obj.pushKV("platformNodeID", platformNodeID.ToString()); + } if (!extended) return; @@ -80,6 +88,7 @@ void CSimplifiedMNListEntry::ToJson(UniValue& obj, bool extended) const } } +// TODO: Invistigate if we can delete this constructor CSimplifiedMNList::CSimplifiedMNList(const std::vector& smlEntries) { mnList.resize(smlEntries.size()); diff --git a/src/evo/simplifiedmns.h b/src/evo/simplifiedmns.h index 95cb4aa3df8d..9cb9718932ab 100644 --- a/src/evo/simplifiedmns.h +++ b/src/evo/simplifiedmns.h @@ -6,6 +6,8 @@ #define BITCOIN_EVO_SIMPLIFIEDMNS_H #include +#include +#include #include #include #include @@ -32,6 +34,9 @@ class CSimplifiedMNListEntry CBLSLazyPublicKey pubKeyOperator; CKeyID keyIDVoting; bool isValid{false}; + uint16_t nType{MnType::Regular.index}; + uint16_t platformHTTPPort{0}; + uint160 platformNodeID{}; CScript scriptPayout; // mem-only CScript scriptOperatorPayout; // mem-only uint16_t nVersion{LEGACY_BLS_VERSION}; // mem-only @@ -47,7 +52,10 @@ class CSimplifiedMNListEntry pubKeyOperator == rhs.pubKeyOperator && keyIDVoting == rhs.keyIDVoting && isValid == rhs.isValid && - nVersion == rhs.nVersion; + nVersion == rhs.nVersion && + nType == rhs.nType && + platformHTTPPort == rhs.platformHTTPPort && + platformNodeID == rhs.platformNodeID; } bool operator!=(const CSimplifiedMNListEntry& rhs) const @@ -65,6 +73,13 @@ class CSimplifiedMNListEntry obj.keyIDVoting, obj.isValid ); + if (obj.nVersion == BASIC_BLS_VERSION) { + READWRITE(obj.nType); + if (obj.nType == MnType::HighPerformance.index) { + READWRITE(obj.platformHTTPPort); + READWRITE(obj.platformNodeID); + } + } } uint256 CalcHash() const; diff --git a/src/governance/object.cpp b/src/governance/object.cpp index ee11c42c9759..5bb57ff08bae 100644 --- a/src/governance/object.cpp +++ b/src/governance/object.cpp @@ -619,6 +619,8 @@ bool CGovernanceObject::IsCollateralValid(std::string& strError, bool& fMissingC int CGovernanceObject::CountMatchingVotes(vote_signal_enum_t eVoteSignalIn, vote_outcome_enum_t eVoteOutcomeIn) const { + auto mnList = deterministicMNManager->GetListAtChainTip(); + LOCK(cs); int nCount = 0; @@ -626,7 +628,10 @@ int CGovernanceObject::CountMatchingVotes(vote_signal_enum_t eVoteSignalIn, vote const vote_rec_t& recVote = votepair.second; auto it2 = recVote.mapInstances.find(eVoteSignalIn); if (it2 != recVote.mapInstances.end() && it2->second.eOutcome == eVoteOutcomeIn) { - ++nCount; + // 4x times weight vote for HPMN owners. + // No need to check if v19 is active since no HPMN are allowed to register before v19s + auto dmn = mnList.GetMNByCollateral(votepair.first); + if (dmn != nullptr) nCount += GetMnType(dmn->nType).voting_weight; } } return nCount; diff --git a/src/governance/vote.cpp b/src/governance/vote.cpp index 516acbedabf6..f5dd9beed9a1 100644 --- a/src/governance/vote.cpp +++ b/src/governance/vote.cpp @@ -2,6 +2,7 @@ // Distributed under the MIT/X11 software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. +#include #include #include @@ -110,11 +111,15 @@ CGovernanceVote::CGovernanceVote(const COutPoint& outpointMasternodeIn, const ui std::string CGovernanceVote::ToString() const { + auto mnList = deterministicMNManager->GetListAtChainTip(); + auto dmn = mnList.GetMNByCollateral(masternodeOutpoint); + int voteWeight = dmn != nullptr ? GetMnType(dmn->nType).voting_weight : 0; std::ostringstream ostr; ostr << masternodeOutpoint.ToStringShort() << ":" << nTime << ":" << CGovernanceVoting::ConvertOutcomeToString(GetOutcome()) << ":" - << CGovernanceVoting::ConvertSignalToString(GetSignal()); + << CGovernanceVoting::ConvertSignalToString(GetSignal()) << ":" + << voteWeight; return ostr.str(); } diff --git a/src/init.cpp b/src/init.cpp index 818ac65f1cc3..f38216ee061d 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -2161,6 +2161,11 @@ bool AppInitMain(const CoreContext& context, NodeContext& node, interfaces::Bloc break; // out of the chainstate activation do-while } + if (!deterministicMNManager->MigrateDBIfNeeded()) { + strLoadError = _("Error upgrading evo database"); + break; + } + if (!llmq::quorumBlockProcessor->UpgradeDB()) { strLoadError = _("Error upgrading evo database"); break; diff --git a/src/llmq/params.h b/src/llmq/params.h index 1a2d9029071b..ff6bc7d1c548 100644 --- a/src/llmq/params.h +++ b/src/llmq/params.h @@ -30,8 +30,9 @@ enum class LLMQType : uint8_t { LLMQ_TEST_V17 = 102, // 3 members, 2 (66%) threshold, one per hour. Params might differ when -llmqtestparams is used // for testing only - LLMQ_TEST_DIP0024 = 103, // 4 members, 2 (66%) threshold, one per hour. Params might differ when -llmqtestparams is used + LLMQ_TEST_DIP0024 = 103, // 4 members, 2 (66%) threshold, one per hour. Params might differ when -llmqtestparams is used LLMQ_TEST_INSTANTSEND = 104, // 3 members, 2 (66%) threshold, one per hour. Params might differ when -llmqtestinstantsendparams is used + LLMQ_TEST_PLATFORM = 106, // 4 members, 2 (66%) threshold, one per hour. // for devnets only. rotated version (v2) for devnets LLMQ_DEVNET_DIP0024 = 105 // 8 members, 4 (50%) threshold, one per hour. Params might differ when -llmqdevnetparams is used @@ -106,7 +107,7 @@ struct LLMQParams { }; -static constexpr std::array available_llmqs = { +static constexpr std::array available_llmqs = { /** * llmq_test @@ -208,6 +209,31 @@ static constexpr std::array available_llmqs = { .recoveryMembers = 3, }, + /** + * llmq_test_platform + * This quorum is only used for testing + * + */ + LLMQParams{ + .type = LLMQType::LLMQ_TEST_PLATFORM, + .name = "llmq_test_platform", + .useRotation = false, + .size = 4, + .minSize = 3, + .threshold = 2, + + .dkgInterval = 24, // DKG cycle + .dkgPhaseBlocks = 2, + .dkgMiningWindowStart = 10, // signingActiveQuorumCount + dkgPhaseBlocks * 5 = after finalization + .dkgMiningWindowEnd = 18, + .dkgBadVotesThreshold = 2, + + .signingActiveQuorumCount = 2, // just a few ones to allow easier testing + + .keepOldConnections = 4, + .recoveryMembers = 3, + }, + /** * llmq_devnet * This quorum is only used for testing on devnets diff --git a/src/llmq/utils.cpp b/src/llmq/utils.cpp index af3f5524b4af..01d5b48e09c9 100644 --- a/src/llmq/utils.cpp +++ b/src/llmq/utils.cpp @@ -131,7 +131,7 @@ std::vector ComputeQuorumMembers(Consensus::LLMQType llmqT { auto allMns = deterministicMNManager->GetListForBlock(pQuorumBaseBlockIndex); auto modifier = ::SerializeHash(std::make_pair(llmqType, pQuorumBaseBlockIndex->GetBlockHash())); - return allMns.CalculateQuorum(GetLLMQParams(llmqType).size, modifier); + return allMns.CalculateQuorum(GetLLMQParams(llmqType).size, modifier, IsLLMQTypeHPMNOnly(llmqType)); } std::vector> ComputeQuorumMembersByQuarterRotation(Consensus::LLMQType llmqType, const CBlockIndex* pCycleQuorumBaseBlockIndex) @@ -673,6 +673,11 @@ bool IsInstantSendLLMQTypeShared() return false; } +bool IsLLMQTypeHPMNOnly(Consensus::LLMQType llmqType) +{ + return Params().GetConsensus().llmqTypePlatform == llmqType; +} + uint256 DeterministicOutboundConnection(const uint256& proTxHash1, const uint256& proTxHash2) { // We need to deterministically select who is going to initiate the connection. The naive way would be to simply @@ -930,6 +935,7 @@ bool IsQuorumTypeEnabledInternal(Consensus::LLMQType llmqType, const CQuorumMana break; } case Consensus::LLMQType::LLMQ_TEST: + case Consensus::LLMQType::LLMQ_TEST_PLATFORM: case Consensus::LLMQType::LLMQ_400_60: case Consensus::LLMQType::LLMQ_400_85: break; diff --git a/src/llmq/utils.h b/src/llmq/utils.h index a29a35f62ed2..587781336865 100644 --- a/src/llmq/utils.h +++ b/src/llmq/utils.h @@ -85,6 +85,7 @@ Consensus::LLMQType GetInstantSendLLMQType(bool deterministic); bool IsDIP0024Active(const CBlockIndex* pindex); bool IsV19Active(const CBlockIndex* pindex); const CBlockIndex* V19ActivationIndex(const CBlockIndex* pindex); +static bool IsLLMQTypeHPMNOnly(Consensus::LLMQType llmqType); /// Returns the state of `-llmq-data-recovery` bool QuorumDataRecoveryEnabled(); diff --git a/src/masternode/payments.cpp b/src/masternode/payments.cpp index e3ab3cc058ac..9e3fb01becd9 100644 --- a/src/masternode/payments.cpp +++ b/src/masternode/payments.cpp @@ -277,7 +277,7 @@ bool CMasternodePayments::GetBlockTxOuts(int nBlockHeight, CAmount blockReward, voutMasternodePaymentsRet.clear(); const CBlockIndex* pindex = WITH_LOCK(cs_main, return ::ChainActive()[nBlockHeight - 1]); - auto dmnPayee = deterministicMNManager->GetListForBlock(pindex).GetMNPayee(); + auto dmnPayee = deterministicMNManager->GetListForBlock(pindex).GetMNPayee(pindex); if (!dmnPayee) { return false; } diff --git a/src/rpc/evo.cpp b/src/rpc/evo.cpp index 10983deca993..1fe27e0e99c0 100644 --- a/src/rpc/evo.cpp +++ b/src/rpc/evo.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -23,8 +24,8 @@ #include #include #include -#include #include +#include #include #ifdef ENABLE_WALLET @@ -138,6 +139,18 @@ static RPCArg GetRpcArg(const std::string& strParamName) "It has to match the private key which is later used when voting on proposals.\n" "If set to an empty string, the currently active voting key address is reused."} }, + {"platformNodeID", + {"platformNodeID", RPCArg::Type::STR, RPCArg::Optional::NO, + "Platform P2P node ID, derived from P2P public key."} + }, + {"platformP2PPort", + {"platformP2PPort", RPCArg::Type::NUM, RPCArg::Optional::NO, + "TCP port of Dash Platform peer-to-peer communication between nodes (network byte order)."} + }, + {"platformHTTPPort", + {"platformHTTPPort", RPCArg::Type::NUM, RPCArg::Optional::NO, + "TCP port of Platform HTTP/API interface (network byte order)."} + }, }; auto it = mapParamHelp.find(strParamName); @@ -177,6 +190,11 @@ static CBLSSecretKey ParseBLSSecretKey(const std::string& hexKey, const std::str return secKey; } +static bool ValidatePlatformPort(const int32_t port) +{ + return port >= 1 && port <= std::numeric_limits::max(); +} + #ifdef ENABLE_WALLET template @@ -433,18 +451,130 @@ static void protx_register_submit_help(const JSONRPCRequest& request) }, }.Check(request); } -static UniValue protx_register_wrapper(const JSONRPCRequest& request, - const bool specific_legacy_bls_scheme, - const bool isExternalRegister, - const bool isFundRegister, - const bool isPrepareRegister) + +static void protx_register_fund_hpmn_help(const JSONRPCRequest& request) +{ + RPCHelpMan{ + "protx register_fund_hpmn", + "\nCreates, funds and sends a ProTx to the network. The resulting transaction will move 4000 Dash\n" + "to the address specified by collateralAddress and will then function as the collateral of your\n" + "HPMN.\n" + "A few of the limitations you see in the arguments are temporary and might be lifted after DIP3\n" + "is fully deployed.\n" + + HELP_REQUIRING_PASSPHRASE, + { + GetRpcArg("collateralAddress"), + GetRpcArg("ipAndPort"), + GetRpcArg("ownerAddress"), + GetRpcArg("operatorPubKey_register"), + GetRpcArg("votingAddress_register"), + GetRpcArg("operatorReward"), + GetRpcArg("payoutAddress_register"), + GetRpcArg("platformNodeID"), + GetRpcArg("platformP2PPort"), + GetRpcArg("platformHTTPPort"), + GetRpcArg("fundAddress"), + GetRpcArg("submit"), + }, + { + RPCResult{"if \"submit\" is not set or set to true", + RPCResult::Type::STR_HEX, "txid", "The transaction id"}, + RPCResult{"if \"submit\" is set to false", + RPCResult::Type::STR_HEX, "hex", "The serialized signed ProTx in hex format"}, + }, + RPCExamples{ + HelpExampleCli("protx", "register_fund_hpmn \"XrVhS9LogauRJGJu2sHuryjhpuex4RNPSb\" 1000 \"1.2.3.4:1234\" \"Xt9AMWaYSz7tR7Uo7gzXA3m4QmeWgrR3rr\" \"93746e8731c57f87f79b3620a7982924e2931717d49540a85864bd543de11c43fb868fd63e501a1db37e19ed59ae6db4\" \"Xt9AMWaYSz7tR7Uo7gzXA3m4QmeWgrR3rr\" 0 \"XrVhS9LogauRJGJu2sHuryjhpuex4RNPSb\" \"f2dbd9b0a1f541a7c44d34a58674d0262f5feca5\" 22821 22822")}, + }.Check(request); +} + +static void protx_register_hpmn_help(const JSONRPCRequest& request) +{ + RPCHelpMan{ + "protx register_hpmn", + "\nSame as \"protx register_fund_hpmn\", but with an externally referenced collateral.\n" + "The collateral is specified through \"collateralHash\" and \"collateralIndex\" and must be an unspent\n" + "transaction output spendable by this wallet. It must also not be used by any other masternode.\n" + + HELP_REQUIRING_PASSPHRASE, + { + GetRpcArg("collateralHash"), + GetRpcArg("collateralIndex"), + GetRpcArg("ipAndPort"), + GetRpcArg("ownerAddress"), + GetRpcArg("operatorPubKey_register"), + GetRpcArg("votingAddress_register"), + GetRpcArg("operatorReward"), + GetRpcArg("payoutAddress_register"), + GetRpcArg("platformNodeID"), + GetRpcArg("platformP2PPort"), + GetRpcArg("platformHTTPPort"), + GetRpcArg("feeSourceAddress"), + GetRpcArg("submit"), + }, + { + RPCResult{"if \"submit\" is not set or set to true", + RPCResult::Type::STR_HEX, "txid", "The transaction id"}, + RPCResult{"if \"submit\" is set to false", + RPCResult::Type::STR_HEX, "hex", "The serialized signed ProTx in hex format"}, + }, + RPCExamples{ + HelpExampleCli("protx", "register_hpmn \"0123456701234567012345670123456701234567012345670123456701234567\" 0 \"1.2.3.4:1234\" \"Xt9AMWaYSz7tR7Uo7gzXA3m4QmeWgrR3rr\" \"93746e8731c57f87f79b3620a7982924e2931717d49540a85864bd543de11c43fb868fd63e501a1db37e19ed59ae6db4\" \"Xt9AMWaYSz7tR7Uo7gzXA3m4QmeWgrR3rr\" 0 \"XrVhS9LogauRJGJu2sHuryjhpuex4RNPSb\" \"f2dbd9b0a1f541a7c44d34a58674d0262f5feca5\" 22821 22822")}, + }.Check(request); +} + +static void protx_register_prepare_hpmn_help(const JSONRPCRequest& request) +{ + RPCHelpMan{ + "protx register_prepare_hpmn", + "\nCreates an unsigned ProTx and a message that must be signed externally\n" + "with the private key that corresponds to collateralAddress to prove collateral ownership.\n" + "The prepared transaction will also contain inputs and outputs to cover fees.\n", + { + GetRpcArg("collateralHash"), + GetRpcArg("collateralIndex"), + GetRpcArg("ipAndPort"), + GetRpcArg("ownerAddress"), + GetRpcArg("operatorPubKey_register"), + GetRpcArg("votingAddress_register"), + GetRpcArg("operatorReward"), + GetRpcArg("payoutAddress_register"), + GetRpcArg("platformNodeID"), + GetRpcArg("platformP2PPort"), + GetRpcArg("platformHTTPPort"), + GetRpcArg("feeSourceAddress"), + }, + RPCResult{ + RPCResult::Type::OBJ, "", "", { + {RPCResult::Type::STR_HEX, "tx", "The serialized unsigned ProTx in hex format"}, + {RPCResult::Type::STR_HEX, "collateralAddress", "The collateral address"}, + {RPCResult::Type::STR_HEX, "signMessage", "The string message that needs to be signed with the collateral key"}, + }}, + RPCExamples{HelpExampleCli("protx", "register_prepare_hpmn \"0123456701234567012345670123456701234567012345670123456701234567\" 0 \"1.2.3.4:1234\" \"Xt9AMWaYSz7tR7Uo7gzXA3m4QmeWgrR3rr\" \"93746e8731c57f87f79b3620a7982924e2931717d49540a85864bd543de11c43fb868fd63e501a1db37e19ed59ae6db4\" \"Xt9AMWaYSz7tR7Uo7gzXA3m4QmeWgrR3rr\" 0 \"XrVhS9LogauRJGJu2sHuryjhpuex4RNPSb\" \"f2dbd9b0a1f541a7c44d34a58674d0262f5feca5\" 22821 22822")}, + }.Check(request); +} + +static UniValue protx_register_common_wrapper(const JSONRPCRequest& request, + const bool specific_legacy_bls_scheme, + const bool isExternalRegister, + const bool isFundRegister, + const bool isPrepareRegister, + const bool isHPMNrequested) { - if (isFundRegister && (request.fHelp || (request.params.size() < 7 || request.params.size() > 9))) { - protx_register_fund_help(request); - } else if (isExternalRegister && (request.fHelp || (request.params.size() < 8 || request.params.size() > 10))) { - protx_register_help(request); - } else if (isPrepareRegister && (request.fHelp || (request.params.size() != 8 && request.params.size() != 9))) { - protx_register_prepare_help(request); + if (isHPMNrequested) { + if (isFundRegister && (request.fHelp || (request.params.size() < 10 || request.params.size() > 12))) { + protx_register_fund_hpmn_help(request); + } else if (isExternalRegister && (request.fHelp || (request.params.size() < 11 || request.params.size() > 13))) { + protx_register_hpmn_help(request); + } else if (isPrepareRegister && (request.fHelp || (request.params.size() != 11 && request.params.size() != 12))) { + protx_register_prepare_hpmn_help(request); + } + } else { + if (isFundRegister && (request.fHelp || (request.params.size() < 7 || request.params.size() > 9))) { + protx_register_fund_help(request); + } else if (isExternalRegister && (request.fHelp || (request.params.size() < 8 || request.params.size() > 10))) { + protx_register_help(request); + } else if (isPrepareRegister && (request.fHelp || (request.params.size() != 8 && request.params.size() != 9))) { + protx_register_prepare_help(request); + } } std::shared_ptr const wallet = GetWalletForJSONRPCRequest(request); @@ -454,19 +584,25 @@ static UniValue protx_register_wrapper(const JSONRPCRequest& request, EnsureWalletIsUnlocked(wallet.get()); } - size_t paramIdx = 0; + bool isV19active = llmq::utils::IsV19Active(WITH_LOCK(cs_main, return ::ChainActive().Tip();)); + if (isHPMNrequested && !isV19active) { + throw JSONRPCError(RPC_INVALID_REQUEST, "HPMN aren't allowed yet"); + } - CAmount collateralAmount = 1000 * COIN; + size_t paramIdx = 0; CMutableTransaction tx; tx.nVersion = 3; tx.nType = TRANSACTION_PROVIDER_REGISTER; CProRegTx ptx; - if (specific_legacy_bls_scheme) + if (specific_legacy_bls_scheme && !isHPMNrequested) { ptx.nVersion = CProRegTx::LEGACY_BLS_VERSION; - else - ptx.nVersion = CProRegTx::GetVersion(llmq::utils::IsV19Active(::ChainActive().Tip())); + } else { + ptx.nVersion = CProRegTx::GetVersion(isV19active); + } + auto mn_type = isHPMNrequested ? MnType::HighPerformance : MnType::Regular; + ptx.nType = mn_type.index; if (isFundRegister) { CTxDestination collateralDest = DecodeDestination(request.params[paramIdx].get_str()); @@ -475,7 +611,8 @@ static UniValue protx_register_wrapper(const JSONRPCRequest& request, } CScript collateralScript = GetScriptForDestination(collateralDest); - CTxOut collateralTxOut(collateralAmount, collateralScript); + CAmount fundCollateral = mn_type.collat_amount; + CTxOut collateralTxOut(fundCollateral, collateralScript); tx.vout.emplace_back(collateralTxOut); paramIdx++; @@ -501,7 +638,7 @@ static UniValue protx_register_wrapper(const JSONRPCRequest& request, } ptx.keyIDOwner = ParsePubKeyIDFromAddress(request.params[paramIdx + 1].get_str(), "owner address"); - CBLSPublicKey pubKeyOperator = ParseBLSPubKey(request.params[paramIdx + 2].get_str(), "operator BLS address", specific_legacy_bls_scheme); + CBLSPublicKey pubKeyOperator = ParseBLSPubKey(request.params[paramIdx + 2].get_str(), "operator BLS address", specific_legacy_bls_scheme && !isHPMNrequested); CKeyID keyIDVoting = ptx.keyIDOwner; if (request.params[paramIdx + 3].get_str() != "") { @@ -522,6 +659,27 @@ static UniValue protx_register_wrapper(const JSONRPCRequest& request, throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("invalid payout address: %s", request.params[paramIdx + 5].get_str())); } + if (isHPMNrequested) { + if (!IsHex(request.params[paramIdx + 6].get_str())) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "platformNodeID must be hexadecimal string"); + } + ptx.platformNodeID.SetHex(request.params[paramIdx + 6].get_str()); + + int32_t requestedPlatformP2PPort = ParseInt32V(request.params[paramIdx + 7].get_str(), "platformP2PPort"); + if (!ValidatePlatformPort(requestedPlatformP2PPort)) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "platformP2PPort must be a valid port [1-65535]"); + } + ptx.platformP2PPort = static_cast(requestedPlatformP2PPort); + + int32_t requestedPlatformHTTPPort = ParseInt32V(request.params[paramIdx + 8].get_str(), "platformHTTPPort"); + if (!ValidatePlatformPort(requestedPlatformHTTPPort)) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "platformHTTPPort must be a valid port [1-65535]"); + } + ptx.platformHTTPPort = static_cast(requestedPlatformHTTPPort); + + paramIdx += 3; + } + ptx.pubKeyOperator = pubKeyOperator; ptx.keyIDVoting = keyIDVoting; ptx.scriptPayout = GetScriptForDestination(payoutDest); @@ -547,9 +705,10 @@ static UniValue protx_register_wrapper(const JSONRPCRequest& request, } if (isFundRegister) { + CAmount fundCollateral = mn_type.collat_amount; uint32_t collateralIndex = (uint32_t) -1; for (uint32_t i = 0; i < tx.vout.size(); i++) { - if (tx.vout[i].nValue == collateralAmount) { + if (tx.vout[i].nValue == fundCollateral) { collateralIndex = i; break; } @@ -601,12 +760,20 @@ static UniValue protx_register_wrapper(const JSONRPCRequest& request, } } +static UniValue protx_register_hpmn(const JSONRPCRequest& request) +{ + bool isExternalRegister = request.strMethod == "protxregister_hpmn"; + bool isFundRegister = request.strMethod == "protxregister_fund_hpmn"; + bool isPrepareRegister = request.strMethod == "protxregister_prepare_hpmn"; + return protx_register_common_wrapper(request, false, isExternalRegister, isFundRegister, isPrepareRegister, true); +} + static UniValue protx_register(const JSONRPCRequest& request) { bool isExternalRegister = request.strMethod == "protxregister"; bool isFundRegister = request.strMethod == "protxregister_fund"; bool isPrepareRegister = request.strMethod == "protxregister_prepare"; - return protx_register_wrapper(request, false, isExternalRegister, isFundRegister, isPrepareRegister); + return protx_register_common_wrapper(request, false, isExternalRegister, isFundRegister, isPrepareRegister, false); } static UniValue protx_register_legacy(const JSONRPCRequest& request) @@ -614,10 +781,9 @@ static UniValue protx_register_legacy(const JSONRPCRequest& request) bool isExternalRegister = request.strMethod == "protxregister_legacy"; bool isFundRegister = request.strMethod == "protxregister_fund_legacy"; bool isPrepareRegister = request.strMethod == "protxregister_prepare_legacy"; - return protx_register_wrapper(request, true, isExternalRegister, isFundRegister, isPrepareRegister); + return protx_register_common_wrapper(request, true, isExternalRegister, isFundRegister, isPrepareRegister, false); } -// handles register, register_prepare and register_fund in one method static UniValue protx_register_submit(const JSONRPCRequest& request) { protx_register_submit_help(request); @@ -671,17 +837,52 @@ static void protx_update_service_help(const JSONRPCRequest& request) }.Check(request); } -static UniValue protx_update_service(const JSONRPCRequest& request) +static void protx_update_service_hpmn_help(const JSONRPCRequest& request) { - protx_update_service_help(request); + RPCHelpMan{ + "protx update_service_hpmn", + "\nCreates and sends a ProUpServTx to the network. This will update the IP address and the Platform fields\n" + "of a HPMN.\n" + "If this is done for a HPMN that got PoSe-banned, the ProUpServTx will also revive this HPMN.\n" + + HELP_REQUIRING_PASSPHRASE, + { + GetRpcArg("proTxHash"), + GetRpcArg("ipAndPort"), + GetRpcArg("operatorKey"), + GetRpcArg("platformNodeID"), + GetRpcArg("platformP2PPort"), + GetRpcArg("platformHTTPPort"), + GetRpcArg("operatorPayoutAddress"), + GetRpcArg("feeSourceAddress"), + }, + RPCResult{ + RPCResult::Type::STR_HEX, "txid", "The transaction id"}, + RPCExamples{ + HelpExampleCli("protx", "update_service_hpmn \"0123456701234567012345670123456701234567012345670123456701234567\" \"1.2.3.4:1234\" \"5a2e15982e62f1e0b7cf9783c64cf7e3af3f90a52d6c40f6f95d624c0b1621cd\" \"f2dbd9b0a1f541a7c44d34a58674d0262f5feca5\" 22821 22822")}, + }.Check(request); +} + +static UniValue protx_update_service_common_wrapper(const JSONRPCRequest& request, const bool isHPMNrequested) +{ + if (isHPMNrequested) { + protx_update_service_hpmn_help(request); + } else { + protx_update_service_help(request); + } std::shared_ptr const wallet = GetWalletForJSONRPCRequest(request); if (!wallet) return NullUniValue; EnsureWalletIsUnlocked(wallet.get()); + bool isV19active = llmq::utils::IsV19Active(WITH_LOCK(cs_main, return ::ChainActive().Tip();)); + if (isHPMNrequested && !isV19active) { + throw JSONRPCError(RPC_INVALID_REQUEST, "HPMN aren't allowed yet"); + } + CProUpServTx ptx; ptx.nVersion = CProUpServTx::GetVersion(llmq::utils::IsV19Active(::ChainActive().Tip())); + ptx.nType = isHPMNrequested ? MnType::HighPerformance.index : MnType::Regular.index; ptx.proTxHash = ParseHashV(request.params[0], "proTxHash"); if (!Lookup(request.params[1].get_str().c_str(), ptx.addr, Params().GetDefaultPort(), false)) { @@ -690,10 +891,37 @@ static UniValue protx_update_service(const JSONRPCRequest& request) CBLSSecretKey keyOperator = ParseBLSSecretKey(request.params[2].get_str(), "operatorKey"); + size_t paramIdx = 3; + if (isHPMNrequested) { + if (!IsHex(request.params[paramIdx].get_str())) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "platformNodeID must be hexadecimal string"); + } + ptx.platformNodeID.SetHex(request.params[paramIdx].get_str()); + + int32_t requestedPlatformP2PPort = ParseInt32V(request.params[paramIdx + 1].get_str(), "platformP2PPort"); + if (!ValidatePlatformPort(requestedPlatformP2PPort)) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "platformP2PPort must be a valid port [1-65535]"); + } + ptx.platformP2PPort = static_cast(requestedPlatformP2PPort); + + int32_t requestedPlatformHTTPPort = ParseInt32V(request.params[paramIdx + 2].get_str(), "platformHTTPPort"); + if (!ValidatePlatformPort(requestedPlatformHTTPPort)) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "platformHTTPPort must be a valid port [1-65535]"); + } + ptx.platformHTTPPort = static_cast(requestedPlatformHTTPPort); + + paramIdx += 3; + } + auto dmn = deterministicMNManager->GetListAtChainTip().GetMN(ptx.proTxHash); if (!dmn) { throw std::runtime_error(strprintf("masternode with proTxHash %s not found", ptx.proTxHash.ToString())); } + if (isHPMNrequested && dmn->nType != MnType::HighPerformance.index) { + throw std::runtime_error(strprintf("masternode with proTxHash %s is not a HPMN", ptx.proTxHash.ToString())); + } else if (!isHPMNrequested && dmn->nType == MnType::HighPerformance.index) { + throw std::runtime_error(strprintf("masternode with proTxHash %s is a HPMN", ptx.proTxHash.ToString())); + } if (keyOperator.GetPublicKey() != dmn->pdmnState->pubKeyOperator.Get()) { throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("the operator key does not belong to the registered public key")); @@ -704,13 +932,13 @@ static UniValue protx_update_service(const JSONRPCRequest& request) tx.nType = TRANSACTION_PROVIDER_UPDATE_SERVICE; // param operatorPayoutAddress - if (!request.params[3].isNull()) { - if (request.params[3].get_str().empty()) { + if (!request.params[paramIdx].isNull()) { + if (request.params[paramIdx].get_str().empty()) { ptx.scriptOperatorPayout = dmn->pdmnState->scriptOperatorPayout; } else { - CTxDestination payoutDest = DecodeDestination(request.params[3].get_str()); + CTxDestination payoutDest = DecodeDestination(request.params[paramIdx].get_str()); if (!IsValidDestination(payoutDest)) { - throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("invalid operator payout address: %s", request.params[3].get_str())); + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("invalid operator payout address: %s", request.params[paramIdx].get_str())); } ptx.scriptOperatorPayout = GetScriptForDestination(payoutDest); } @@ -721,10 +949,10 @@ static UniValue protx_update_service(const JSONRPCRequest& request) CTxDestination feeSource; // param feeSourceAddress - if (!request.params[4].isNull()) { - feeSource = DecodeDestination(request.params[4].get_str()); + if (!request.params[paramIdx + 1].isNull()) { + feeSource = DecodeDestination(request.params[paramIdx + 1].get_str()); if (!IsValidDestination(feeSource)) - throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, std::string("Invalid Dash address: ") + request.params[4].get_str()); + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, std::string("Invalid Dash address: ") + request.params[paramIdx + 1].get_str()); } else { if (ptx.scriptOperatorPayout != CScript()) { // use operator reward address as default source for fees @@ -1228,7 +1456,8 @@ static UniValue protx_diff(const JSONRPCRequest& request) [[ noreturn ]] static void protx_help() { - RPCHelpMan{"protx", + RPCHelpMan{ + "protx", "Set of commands to execute ProTx related actions.\n" "To get help on individual commands, use \"help protx command\".\n" "\nAvailable commands:\n" @@ -1236,6 +1465,9 @@ static UniValue protx_diff(const JSONRPCRequest& request) " register - Create and send ProTx to network\n" " register_fund - Fund, create and send ProTx to network\n" " register_prepare - Create an unsigned ProTx\n" + " register_hpmn - Create and send ProTx to network for a HPMN\n" + " register_fund_hpmn - Fund, create and send ProTx to network for a HPMN\n" + " register_prepare_hpmn - Create an unsigned ProTx for a HPMN\n" " register_legacy - Create a ProTx by parsing BLS using the legacy scheme and send it to network\n" " register_fund_legacy - Fund and create a ProTx by parsing BLS using the legacy scheme, then send it to network\n" " register_prepare_legacy - Create an unsigned ProTx by parsing BLS using the legacy scheme\n" @@ -1245,6 +1477,7 @@ static UniValue protx_diff(const JSONRPCRequest& request) " info - Return information about a ProTx\n" #ifdef ENABLE_WALLET " update_service - Create and send ProUpServTx to network\n" + " update_service_hpmn - Create and send ProUpServTx to network for a HPMN\n" " update_registrar - Create and send ProUpRegTx to network\n" " revoke - Create and send ProUpRevTx to network\n" #endif @@ -1265,12 +1498,16 @@ static UniValue protx(const JSONRPCRequest& request) #ifdef ENABLE_WALLET if (command == "protxregister" || command == "protxregister_fund" || command == "protxregister_prepare") { return protx_register(new_request); + } else if (command == "protxregister_hpmn" || command == "protxregister_fund_hpmn" || command == "protxregister_prepare_hpmn") { + return protx_register_hpmn(new_request); } else if (command == "protxregister_legacy" || command == "protxregister_fund_legacy" || command == "protxregister_prepare_legacy") { return protx_register_legacy(new_request); } else if (command == "protxregister_submit") { return protx_register_submit(new_request); } else if (command == "protxupdate_service") { - return protx_update_service(new_request); + return protx_update_service_common_wrapper(new_request, false); + } else if (command == "protxupdate_service_hpmn") { + return protx_update_service_common_wrapper(new_request, true); } else if (command == "protxupdate_registrar") { return protx_update_registrar(new_request); } else if (command == "protxupdate_registrar_legacy") { diff --git a/src/rpc/masternode.cpp b/src/rpc/masternode.cpp index 01b848656757..365f26611252 100644 --- a/src/rpc/masternode.cpp +++ b/src/rpc/masternode.cpp @@ -336,7 +336,8 @@ static UniValue masternode_winners(const JSONRPCRequest& request) int nStartHeight = std::max(nChainTipHeight - nCount, 1); for (int h = nStartHeight; h <= nChainTipHeight; h++) { - auto payee = deterministicMNManager->GetListForBlock(pindexTip->GetAncestor(h - 1)).GetMNPayee(); + const CBlockIndex* pIndex = pindexTip->GetAncestor(h - 1); + auto payee = deterministicMNManager->GetListForBlock(pIndex).GetMNPayee(pIndex); std::string strPayments = GetRequiredPaymentsString(h, payee); if (strFilter != "" && strPayments.find(strFilter) == std::string::npos) continue; obj.pushKV(strprintf("%d", h), strPayments); @@ -462,7 +463,7 @@ static UniValue masternode_payments(const JSONRPCRequest& request) } // NOTE: we use _previous_ block to find a payee for the current one - const auto dmnPayee = deterministicMNManager->GetListForBlock(pindex->pprev).GetMNPayee(); + const auto dmnPayee = deterministicMNManager->GetListForBlock(pindex->pprev).GetMNPayee(pindex->pprev); protxObj.pushKV("proTxHash", dmnPayee == nullptr ? "" : dmnPayee->proTxHash.ToString()); protxObj.pushKV("amount", payedPerMasternode); protxObj.pushKV("payees", payeesArr); @@ -666,7 +667,14 @@ static UniValue masternodelist(const JSONRPCRequest& request) objMN.pushKV("address", dmn.pdmnState->addr.ToString()); objMN.pushKV("payee", payeeStr); objMN.pushKV("status", dmnToStatus(dmn)); + objMN.pushKV("type", std::string(GetMnType(dmn.nType).description)); + if (dmn.nType == MnType::HighPerformance.index) { + objMN.pushKV("platformNodeID", dmn.pdmnState->platformNodeID.ToString()); + objMN.pushKV("platformP2PPort", dmn.pdmnState->platformP2PPort); + objMN.pushKV("platformHTTPPort", dmn.pdmnState->platformHTTPPort); + } objMN.pushKV("pospenaltyscore", dmn.pdmnState->nPoSePenalty); + objMN.pushKV("consecutivePayments", dmn.pdmnState->nConsecutivePayments); objMN.pushKV("lastpaidtime", dmnToLastPaidTime(dmn)); objMN.pushKV("lastpaidblock", dmn.pdmnState->nLastPaidHeight); objMN.pushKV("owneraddress", EncodeDestination(PKHash(dmn.pdmnState->keyIDOwner))); diff --git a/src/test/block_reward_reallocation_tests.cpp b/src/test/block_reward_reallocation_tests.cpp index bb19c25c741b..0626ccbec412 100644 --- a/src/test/block_reward_reallocation_tests.cpp +++ b/src/test/block_reward_reallocation_tests.cpp @@ -132,7 +132,7 @@ static CMutableTransaction CreateProRegTx(const CTxMemPool& mempool, SimpleUTXOM CMutableTransaction tx; tx.nVersion = 3; tx.nType = TRANSACTION_PROVIDER_REGISTER; - FundTransaction(tx, utxos, scriptPayout, 1000 * COIN); + FundTransaction(tx, utxos, scriptPayout, MnType::Regular.collat_amount); proTx.inputsHash = CalcTxInputsHash(CTransaction(tx)); SetTxPayload(tx, proTx); SignTransaction(mempool, tx, coinbaseKey); diff --git a/src/test/evo_deterministicmns_tests.cpp b/src/test/evo_deterministicmns_tests.cpp index 62dd6cf992c7..807028d73229 100644 --- a/src/test/evo_deterministicmns_tests.cpp +++ b/src/test/evo_deterministicmns_tests.cpp @@ -110,7 +110,7 @@ static CMutableTransaction CreateProRegTx(const CTxMemPool& mempool, SimpleUTXOM CMutableTransaction tx; tx.nVersion = 3; tx.nType = TRANSACTION_PROVIDER_REGISTER; - FundTransaction(tx, utxos, scriptPayout, 1000 * COIN, coinbaseKey); + FundTransaction(tx, utxos, scriptPayout, MnType::Regular.collat_amount, coinbaseKey); proTx.inputsHash = CalcTxInputsHash(CTransaction(tx)); SetTxPayload(tx, proTx); SignTransaction(mempool, tx, coinbaseKey); @@ -322,7 +322,7 @@ void FuncDIP3Protx(TestChainSetup& setup) // check MN reward payments for (size_t i = 0; i < 20; i++) { - auto dmnExpectedPayee = deterministicMNManager->GetListAtChainTip().GetMNPayee(); + auto dmnExpectedPayee = deterministicMNManager->GetListAtChainTip().GetMNPayee(::ChainActive().Tip()); CBlock block = setup.CreateAndProcessBlock({}, setup.coinbaseKey); deterministicMNManager->UpdatedBlockTip(::ChainActive().Tip()); @@ -380,7 +380,7 @@ void FuncDIP3Protx(TestChainSetup& setup) // test that the revoked MN does not get paid anymore for (size_t i = 0; i < 20; i++) { - auto dmnExpectedPayee = deterministicMNManager->GetListAtChainTip().GetMNPayee(); + auto dmnExpectedPayee = deterministicMNManager->GetListAtChainTip().GetMNPayee(::ChainActive().Tip()); BOOST_ASSERT(dmnExpectedPayee->proTxHash != dmnHashes[0]); CBlock block = setup.CreateAndProcessBlock({}, setup.coinbaseKey); @@ -428,7 +428,7 @@ void FuncDIP3Protx(TestChainSetup& setup) // test that the revived MN gets payments again bool foundRevived = false; for (size_t i = 0; i < 20; i++) { - auto dmnExpectedPayee = deterministicMNManager->GetListAtChainTip().GetMNPayee(); + auto dmnExpectedPayee = deterministicMNManager->GetListAtChainTip().GetMNPayee(::ChainActive().Tip()); if (dmnExpectedPayee->proTxHash == dmnHashes[0]) { foundRevived = true; } @@ -468,7 +468,7 @@ void FuncTestMempoolReorg(TestChainSetup& setup) // Create a MN with an external collateral CMutableTransaction tx_collateral; - FundTransaction(tx_collateral, utxos, scriptCollateral, 1000 * COIN, setup.coinbaseKey); + FundTransaction(tx_collateral, utxos, scriptCollateral, MnType::Regular.collat_amount, setup.coinbaseKey); SignTransaction(*(setup.m_node.mempool), tx_collateral, setup.coinbaseKey); auto block = std::make_shared(setup.CreateBlock({tx_collateral}, setup.coinbaseKey)); @@ -486,7 +486,7 @@ void FuncTestMempoolReorg(TestChainSetup& setup) payload.scriptPayout = scriptPayout; for (size_t i = 0; i < tx_collateral.vout.size(); ++i) { - if (tx_collateral.vout[i].nValue == 1000 * COIN) { + if (tx_collateral.vout[i].nValue == MnType::Regular.collat_amount) { payload.collateralOutpoint = COutPoint(tx_collateral.GetHash(), i); break; } @@ -495,7 +495,7 @@ void FuncTestMempoolReorg(TestChainSetup& setup) CMutableTransaction tx_reg; tx_reg.nVersion = 3; tx_reg.nType = TRANSACTION_PROVIDER_REGISTER; - FundTransaction(tx_reg, utxos, scriptPayout, 1000 * COIN, setup.coinbaseKey); + FundTransaction(tx_reg, utxos, scriptPayout, MnType::Regular.collat_amount, setup.coinbaseKey); payload.inputsHash = CalcTxInputsHash(CTransaction(tx_reg)); CMessageSigner::SignMessage(payload.MakeSignString(), payload.vchSig, collateralKey); SetTxPayload(tx_reg, payload); @@ -555,7 +555,7 @@ void FuncTestMempoolDualProregtx(TestChainSetup& setup) payload.scriptPayout = scriptPayout; for (size_t i = 0; i < tx_reg1.vout.size(); ++i) { - if (tx_reg1.vout[i].nValue == 1000 * COIN) { + if (tx_reg1.vout[i].nValue == MnType::Regular.collat_amount) { payload.collateralOutpoint = COutPoint(tx_reg1.GetHash(), i); break; } @@ -564,7 +564,7 @@ void FuncTestMempoolDualProregtx(TestChainSetup& setup) CMutableTransaction tx_reg2; tx_reg2.nVersion = 3; tx_reg2.nType = TRANSACTION_PROVIDER_REGISTER; - FundTransaction(tx_reg2, utxos, scriptPayout, 1000 * COIN, setup.coinbaseKey); + FundTransaction(tx_reg2, utxos, scriptPayout, MnType::Regular.collat_amount, setup.coinbaseKey); payload.inputsHash = CalcTxInputsHash(CTransaction(tx_reg2)); CMessageSigner::SignMessage(payload.MakeSignString(), payload.vchSig, collateralKey); SetTxPayload(tx_reg2, payload); @@ -599,7 +599,7 @@ void FuncVerifyDB(TestChainSetup& setup) // Create a MN with an external collateral CMutableTransaction tx_collateral; - FundTransaction(tx_collateral, utxos, scriptCollateral, 1000 * COIN, setup.coinbaseKey); + FundTransaction(tx_collateral, utxos, scriptCollateral, MnType::Regular.collat_amount, setup.coinbaseKey); SignTransaction(*(setup.m_node.mempool), tx_collateral, setup.coinbaseKey); auto block = std::make_shared(setup.CreateBlock({tx_collateral}, setup.coinbaseKey)); @@ -617,7 +617,7 @@ void FuncVerifyDB(TestChainSetup& setup) payload.scriptPayout = scriptPayout; for (size_t i = 0; i < tx_collateral.vout.size(); ++i) { - if (tx_collateral.vout[i].nValue == 1000 * COIN) { + if (tx_collateral.vout[i].nValue == MnType::Regular.collat_amount) { payload.collateralOutpoint = COutPoint(tx_collateral.GetHash(), i); break; } @@ -626,7 +626,7 @@ void FuncVerifyDB(TestChainSetup& setup) CMutableTransaction tx_reg; tx_reg.nVersion = 3; tx_reg.nType = TRANSACTION_PROVIDER_REGISTER; - FundTransaction(tx_reg, utxos, scriptPayout, 1000 * COIN, setup.coinbaseKey); + FundTransaction(tx_reg, utxos, scriptPayout, MnType::Regular.collat_amount, setup.coinbaseKey); payload.inputsHash = CalcTxInputsHash(CTransaction(tx_reg)); CMessageSigner::SignMessage(payload.MakeSignString(), payload.vchSig, collateralKey); SetTxPayload(tx_reg, payload); diff --git a/src/version.h b/src/version.h index 1ded6e1ba363..456b44d78423 100644 --- a/src/version.h +++ b/src/version.h @@ -11,7 +11,7 @@ */ -static const int PROTOCOL_VERSION = 70226; +static const int PROTOCOL_VERSION = 70227; //! initial proto version, to be increased after version/verack negotiation static const int INIT_PROTO_VERSION = 209; @@ -50,6 +50,9 @@ static const int BLS_SCHEME_PROTO_VERSION = 70225; //! DSQ and DSTX started using protx hash in this version static const int COINJOIN_PROTX_HASH_PROTO_VERSION = 70226; +//! Masternode type was introduced in this version +static const int DMN_TYPE_PROTO_VERSION = 70227; + // Make sure that none of the values above collide with `ADDRV2_FORMAT`. #endif // BITCOIN_VERSION_H diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 3544b7e639dd..f411cd3b62ac 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -2535,7 +2535,7 @@ void CWallet::AvailableCoins(std::vector &vCoins, bool fOnlySafe, const if (CCoinJoin::IsCollateralAmount(pcoin->tx->vout[i].nValue)) continue; // do not use collateral amounts found = !CCoinJoin::IsDenominatedAmount(pcoin->tx->vout[i].nValue); } else if(nCoinType == CoinType::ONLY_MASTERNODE_COLLATERAL) { - found = pcoin->tx->vout[i].nValue == 1000*COIN; + found = pcoin->tx->vout[i].nValue == MnType::Regular.collat_amount || pcoin->tx->vout[i].nValue == MnType::HighPerformance.collat_amount; } else if(nCoinType == CoinType::ONLY_COINJOIN_COLLATERAL) { found = CCoinJoin::IsCollateralAmount(pcoin->tx->vout[i].nValue); } else { @@ -3046,7 +3046,7 @@ std::vector CWallet::SelectCoinsGroupedByAddresses(bool fSkipD if(fAnonymizable) { // ignore collaterals if(CCoinJoin::IsCollateralAmount(wtx.tx->vout[i].nValue)) continue; - if(fMasternodeMode && wtx.tx->vout[i].nValue == 1000*COIN) continue; + if (fMasternodeMode && (wtx.tx->vout[i].nValue == MnType::Regular.collat_amount || wtx.tx->vout[i].nValue == MnType::HighPerformance.collat_amount)) continue; // ignore outputs that are 10 times smaller then the smallest denomination // otherwise they will just lead to higher fee / lower priority if(wtx.tx->vout[i].nValue <= nSmallestDenom/10) continue; diff --git a/test/functional/feature_llmq_hpmn.py b/test/functional/feature_llmq_hpmn.py new file mode 100755 index 000000000000..98a1d3e6196b --- /dev/null +++ b/test/functional/feature_llmq_hpmn.py @@ -0,0 +1,212 @@ +#!/usr/bin/env python3 +# Copyright (c) 2015-2022 The Dash Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +''' +feature_llmq_hpmn.py + +Checks HPMNs + +''' +from _decimal import Decimal +import random + +from test_framework.script import hash160 +from test_framework.test_framework import DashTestFramework +from test_framework.util import ( + assert_equal, p2p_port +) + + +def extract_quorum_members(quorum_info): + return [d['proTxHash'] for d in quorum_info["members"]] + +class LLMQHPMNTest(DashTestFramework): + def set_test_params(self): + self.set_dash_test_params(5, 4, fast_dip3_enforcement=True, hpmn_count=7) + self.set_dash_llmq_test_params(4, 4) + + def run_test(self): + # Connect all nodes to node1 so that we always have the whole network connected + # Otherwise only masternode connections will be established between nodes, which won't propagate TXs/blocks + # Usually node0 is the one that does this, but in this test we isolate it multiple times + + for i in range(len(self.nodes)): + if i != 0: + self.connect_nodes(i, 0) + + self.activate_dip8() + + self.nodes[0].sporkupdate("SPORK_17_QUORUM_DKG_ENABLED", 0) + self.wait_for_sporks_same() + + self.mine_quorum(llmq_type_name='llmq_test', llmq_type=100) + + self.log.info("Test that HPMN registration is rejected before v19") + self.test_hpmn_is_rejected_before_v19() + + self.activate_v19(expected_activation_height=900) + self.log.info("Activated v19 at height:" + str(self.nodes[0].getblockcount())) + + self.move_to_next_cycle() + self.log.info("Cycle H height:" + str(self.nodes[0].getblockcount())) + self.move_to_next_cycle() + self.log.info("Cycle H+C height:" + str(self.nodes[0].getblockcount())) + self.move_to_next_cycle() + self.log.info("Cycle H+2C height:" + str(self.nodes[0].getblockcount())) + + (quorum_info_i_0, quorum_info_i_1) = self.mine_cycle_quorum(llmq_type_name='llmq_test_dip0024', llmq_type=103) + + hpmn_protxhash_list = list() + for i in range(5): + hpmn_info = self.dynamically_add_masternode(hpmn=True) + hpmn_protxhash_list.append(hpmn_info.proTxHash) + self.nodes[0].generate(8) + self.sync_blocks(self.nodes) + self.test_hpmn_update_service(hpmn_info) + + self.log.info("Test llmq_platform are formed only with HPMNs") + for i in range(3): + quorum_i_hash = self.mine_quorum(llmq_type_name='llmq_test_platform', llmq_type=106) + self.test_quorum_members_are_high_performance(quorum_i_hash, llmq_type=106) + + self.log.info("Test that HPMNs are present in MN list") + self.test_hpmn_protx_are_in_mnlist(hpmn_protxhash_list) + + self.log.info("Test that HPMNs are paid 4x blocks in a row") + self.test_hpmmn_payements(window_analysis=256) + + self.log.info(self.nodes[0].masternodelist()) + + return + + def test_hpmmn_payements(self, window_analysis): + current_hpmn = None + consecutive_paymments = 0 + for i in range(0, window_analysis): + payee = self.get_mn_payee_for_block(self.nodes[0].getbestblockhash()) + if payee is not None and payee.hpmn: + if current_hpmn is not None and payee.proTxHash == current_hpmn.proTxHash: + # same HPMN + assert consecutive_paymments > 0 + consecutive_paymments += 1 + consecutive_paymments_rpc = self.nodes[0].protx('info', current_hpmn.proTxHash)['state']['consecutivePayments'] + assert_equal(consecutive_paymments, consecutive_paymments_rpc) + else: + # new HPMN + if current_hpmn is not None: + # make sure the old one was paid 4 times in a row + assert_equal(consecutive_paymments, 4) + consecutive_paymments_rpc = self.nodes[0].protx('info', current_hpmn.proTxHash)['state']['consecutivePayments'] + # old HPMN should have its nConsecutivePayments reset to 0 + assert_equal(consecutive_paymments_rpc, 0) + current_hpmn = payee + consecutive_paymments = 1 + consecutive_paymments_rpc = self.nodes[0].protx('info', current_hpmn.proTxHash)['state']['consecutivePayments'] + assert_equal(consecutive_paymments, consecutive_paymments_rpc) + else: + # not a HPMN + if current_hpmn is not None: + # make sure the old one was paid 4 times in a row + assert_equal(consecutive_paymments, 4) + consecutive_paymments_rpc = self.nodes[0].protx('info', current_hpmn.proTxHash)['state']['consecutivePayments'] + # old HPMN should have its nConsecutivePayments reset to 0 + assert_equal(consecutive_paymments_rpc, 0) + current_hpmn = None + consecutive_paymments = 0 + + self.nodes[0].generate(1) + if i % 8 == 0: + self.sync_blocks() + + def get_mn_payee_for_block(self, block_hash): + mn_payee_info = self.nodes[0].masternode("payments", block_hash)[0] + mn_payee_protx = mn_payee_info['masternodes'][0]['proTxHash'] + + mninfos_online = self.mninfo.copy() + for mn_info in mninfos_online: + if mn_info.proTxHash == mn_payee_protx: + return mn_info + return None + + def test_quorum_members_are_high_performance(self, quorum_hash, llmq_type): + quorum_info = self.nodes[0].quorum("info", llmq_type, quorum_hash) + quorum_members = extract_quorum_members(quorum_info) + mninfos_online = self.mninfo.copy() + for qm in quorum_members: + found = False + for mn in mninfos_online: + if mn.proTxHash == qm: + assert_equal(mn.hpmn, True) + found = True + break + assert_equal(found, True) + + def test_hpmn_protx_are_in_mnlist(self, hpmn_protx_list): + mn_list = self.nodes[0].masternodelist() + for hpmn_protx in hpmn_protx_list: + found = False + for mn in mn_list: + if mn_list.get(mn)['proTxHash'] == hpmn_protx: + found = True + assert_equal(mn_list.get(mn)['type'], "HighPerformance") + assert_equal(found, True) + + def test_hpmn_is_rejected_before_v19(self): + bls = self.nodes[0].bls('generate') + collateral_address = self.nodes[0].getnewaddress() + funds_address = self.nodes[0].getnewaddress() + owner_address = self.nodes[0].getnewaddress() + voting_address = self.nodes[0].getnewaddress() + reward_address = self.nodes[0].getnewaddress() + + collateral_amount = 4000 + collateral_txid = self.nodes[0].sendtoaddress(collateral_address, collateral_amount) + # send to same address to reserve some funds for fees + self.nodes[0].sendtoaddress(funds_address, 1) + collateral_vout = 0 + self.nodes[0].generate(1) + self.sync_all(self.nodes) + + rawtx = self.nodes[0].getrawtransaction(collateral_txid, 1) + for txout in rawtx['vout']: + if txout['value'] == Decimal(collateral_amount): + collateral_vout = txout['n'] + break + assert collateral_vout is not None + + ipAndPort = '127.0.0.1:%d' % p2p_port(len(self.nodes)) + operatorReward = len(self.nodes) + + self.nodes[0].generate(1) + + protx_success = False + try: + self.nodes[0].protx('register_hpmn', collateral_txid, collateral_vout, ipAndPort, owner_address, bls['public'], voting_address, operatorReward, reward_address, funds_address, True) + protx_success = True + except: + self.log.info("protx_hpmn rejected") + assert_equal(protx_success, False) + + def test_hpmn_update_service(self, hpmn_info): + funds_address = self.nodes[0].getnewaddress() + operator_reward_address = self.nodes[0].getnewaddress() + + # For the sake of the test, generate random nodeid, p2p and http platform values + rnd = random.randint(1000, 65000) + platform_node_id = hash160(b'%d' % rnd).hex() + platform_p2p_port = '%d' % (rnd + 1) + platform_http_port = '%d' % (rnd + 2) + + self.nodes[0].sendtoaddress(funds_address, 1) + self.nodes[0].generate(1) + self.sync_all(self.nodes) + + self.nodes[0].protx('update_service_hpmn', hpmn_info.proTxHash, hpmn_info.addr, hpmn_info.keyOperator, platform_node_id, platform_p2p_port, platform_http_port, operator_reward_address, funds_address) + self.nodes[0].generate(1) + self.sync_all(self.nodes) + self.log.info("Updated HPMN %s: platformNodeID=%s, platformP2PPort=%s, platformHTTPPort=%s" % (hpmn_info.proTxHash, platform_node_id, platform_p2p_port, platform_http_port)) + +if __name__ == '__main__': + LLMQHPMNTest().main() diff --git a/test/functional/feature_new_quorum_type_activation.py b/test/functional/feature_new_quorum_type_activation.py index 3535b72fc490..d70aa78a4bef 100755 --- a/test/functional/feature_new_quorum_type_activation.py +++ b/test/functional/feature_new_quorum_type_activation.py @@ -24,17 +24,17 @@ def run_test(self): self.nodes[0].generate(9) assert_equal(get_bip9_status(self.nodes[0], 'dip0020')['status'], 'started') ql = self.nodes[0].quorum("list") - assert_equal(len(ql), 2) + assert_equal(len(ql), 3) assert "llmq_test_v17" not in ql self.nodes[0].generate(10) assert_equal(get_bip9_status(self.nodes[0], 'dip0020')['status'], 'locked_in') ql = self.nodes[0].quorum("list") - assert_equal(len(ql), 2) + assert_equal(len(ql), 3) assert "llmq_test_v17" not in ql self.nodes[0].generate(10) assert_equal(get_bip9_status(self.nodes[0], 'dip0020')['status'], 'active') ql = self.nodes[0].quorum("list") - assert_equal(len(ql), 3) + assert_equal(len(ql), 4) assert "llmq_test_v17" in ql diff --git a/test/functional/rpc_platform_filter.py b/test/functional/rpc_platform_filter.py index 82c45be46f87..5dc0437fffe0 100755 --- a/test/functional/rpc_platform_filter.py +++ b/test/functional/rpc_platform_filter.py @@ -87,8 +87,8 @@ def test_command(method, params, auth, expexted_status, should_not_match=False): test_command("getblockhash", [0], rpcuser_authpair_platform, 200) test_command("getblockcount", [], rpcuser_authpair_platform, 200) test_command("getbestchainlock", [], rpcuser_authpair_platform, 500) - test_command("quorum", ["sign", 100], rpcuser_authpair_platform, 500) - test_command("quorum", ["sign", 100, "0000000000000000000000000000000000000000000000000000000000000000", + test_command("quorum", ["sign", 106], rpcuser_authpair_platform, 500) + test_command("quorum", ["sign", 106, "0000000000000000000000000000000000000000000000000000000000000000", "0000000000000000000000000000000000000000000000000000000000000001"], rpcuser_authpair_platform, 200) test_command("quorum", ["verify"], rpcuser_authpair_platform, 500) diff --git a/test/functional/test_framework/test_framework.py b/test/functional/test_framework/test_framework.py index 348f509338b8..cf4826b75484 100755 --- a/test/functional/test_framework/test_framework.py +++ b/test/functional/test_framework/test_framework.py @@ -7,6 +7,7 @@ import configparser import copy +from _decimal import Decimal from enum import Enum import logging import argparse @@ -32,6 +33,7 @@ ser_compact_size, ser_string, ) +from .script import hash160 from .test_node import TestNode from .mininode import NetworkThread from .util import ( @@ -49,7 +51,7 @@ set_timeout_scale, satoshi_round, wait_until, - get_chain_folder, + get_chain_folder, rpc_port, ) @@ -469,6 +471,75 @@ def add_nodes(self, num_nodes, extra_args=None, *, rpchost=None, binary=None): use_valgrind=self.options.valgrind, )) + def add_dynamically_node(self, extra_args=None, *, rpchost=None, binary=None): + if self.bind_to_localhost_only: + extra_confs = [["bind=127.0.0.1"]] + else: + extra_confs = [[]] + if extra_args is None: + extra_args = [[]] + if binary is None: + binary = [self.options.bitcoind] + assert_equal(len(extra_confs), 1) + assert_equal(len(binary), 1) + old_num_nodes = len(self.nodes) + + p0 = old_num_nodes + p1 = get_datadir_path(self.options.tmpdir, old_num_nodes) + p2 = self.extra_args_from_options + p3 = self.chain + p4 = rpchost + p5 = self.rpc_timeout + p6 = self.options.timeout_factor + p7 = binary[0] + p8 = self.options.bitcoincli + p9 = self.mocktime + p10 = self.options.coveragedir + p11 = self.options.tmpdir + p12 = extra_confs[0] + p13 = extra_args + p14 = self.options.usecli + p15 = self.options.perf + p16 = self.options.valgrind + + t_node = TestNode(p0, p1, p2, chain=p3, rpchost=p4, timewait=p5, timeout_factor=p6, bitcoind=p7, bitcoin_cli=p8, mocktime=p9, coverage_dir=p10, cwd=p11, extra_conf=p12, extra_args=p13, use_cli=p14, start_perf=p15, use_valgrind=p16) + self.nodes.append(t_node) + return t_node + + def dynamically_initialize_datadir(self, chain, node_p2p_port, node_rpc_port): + data_dir = get_datadir_path(self.options.tmpdir, len(self.nodes)) + if not os.path.isdir(data_dir): + os.makedirs(data_dir) + # Translate chain name to config name + if chain == 'testnet3': + chain_name_conf_arg = 'testnet' + chain_name_conf_section = 'test' + chain_name_conf_arg_value = '1' + elif chain == 'devnet': + chain_name_conf_arg = 'devnet' + chain_name_conf_section = 'devnet' + chain_name_conf_arg_value = 'devnet1' + else: + chain_name_conf_arg = chain + chain_name_conf_section = chain + chain_name_conf_arg_value = '1' + with open(os.path.join(data_dir, "dash.conf"), 'w', encoding='utf8') as f: + f.write("{}={}\n".format(chain_name_conf_arg, chain_name_conf_arg_value)) + f.write("[{}]\n".format(chain_name_conf_section)) + f.write("port=" + str(node_p2p_port) + "\n") + f.write("rpcport=" + str(node_rpc_port) + "\n") + f.write("server=1\n") + f.write("debug=1\n") + f.write("keypool=1\n") + f.write("discover=0\n") + f.write("listenonion=0\n") + f.write("printtoconsole=0\n") + f.write("upnp=0\n") + f.write("natpmp=0\n") + f.write("shrinkdebugfile=0\n") + os.makedirs(os.path.join(data_dir, 'stderr'), exist_ok=True) + os.makedirs(os.path.join(data_dir, 'stdout'), exist_ok=True) + def start_node(self, i, *args, **kwargs): """Start a dashd""" @@ -826,10 +897,10 @@ def is_zmq_compiled(self): MASTERNODE_COLLATERAL = 1000 - +HIGHPERFORMANCE_MASTERNODE_COLLATERAL = 4000 class MasternodeInfo: - def __init__(self, proTxHash, ownerAddr, votingAddr, pubKeyOperator, keyOperator, collateral_address, collateral_txid, collateral_vout): + def __init__(self, proTxHash, ownerAddr, votingAddr, pubKeyOperator, keyOperator, collateral_address, collateral_txid, collateral_vout, addr, hpmn=False): self.proTxHash = proTxHash self.ownerAddr = ownerAddr self.votingAddr = votingAddr @@ -838,6 +909,8 @@ def __init__(self, proTxHash, ownerAddr, votingAddr, pubKeyOperator, keyOperator self.collateral_address = collateral_address self.collateral_txid = collateral_txid self.collateral_vout = collateral_vout + self.addr = addr + self.hpmn = hpmn class DashTestFramework(BitcoinTestFramework): @@ -852,8 +925,9 @@ def run_test(self): """Tests must override this method to define test logic""" raise NotImplementedError - def set_dash_test_params(self, num_nodes, masterodes_count, extra_args=None, fast_dip3_enforcement=False): + def set_dash_test_params(self, num_nodes, masterodes_count, extra_args=None, fast_dip3_enforcement=False, hpmn_count=0): self.mn_count = masterodes_count + self.hpmn_count = hpmn_count self.num_nodes = num_nodes self.mninfo = [] self.setup_clean_chain = True @@ -953,29 +1027,100 @@ def create_simple_node(self): for i in range(0, idx): self.connect_nodes(i, idx) + def dynamically_add_masternode(self, hpmn=False): + mn_idx = len(self.nodes) + + node_p2p_port = p2p_port(mn_idx) + node_rpc_port = rpc_port(mn_idx) + + created_mn_info = self.dynamically_prepare_masternode(mn_idx, node_p2p_port, hpmn) + + self.dynamically_initialize_datadir(self.nodes[0].chain,node_p2p_port, node_rpc_port) + node_info = self.add_dynamically_node(self.extra_args[1]) + + args = ['-masternodeblsprivkey=%s' % created_mn_info.keyOperator] + node_info.extra_args + self.start_node(mn_idx, args) + + for mn_info in self.mninfo: + if mn_info.proTxHash == created_mn_info.proTxHash: + mn_info.nodeIx = mn_idx + mn_info.node = self.nodes[mn_idx] + + self.connect_nodes(mn_idx, 0) + + self.wait_for_sporks_same() + self.sync_blocks(self.nodes) + force_finish_mnsync(self.nodes[mn_idx]) + + self.log.info("Successfully started and synced proTx:"+str(created_mn_info.proTxHash)) + return created_mn_info + + def dynamically_prepare_masternode(self, idx, node_p2p_port, hpmn=False): + bls = self.nodes[0].bls('generate') + collateral_address = self.nodes[0].getnewaddress() + funds_address = self.nodes[0].getnewaddress() + owner_address = self.nodes[0].getnewaddress() + voting_address = self.nodes[0].getnewaddress() + reward_address = self.nodes[0].getnewaddress() + + platform_node_id = hash160(b'%d' % node_p2p_port).hex() + platform_p2p_port = '%d' % (node_p2p_port + 101) if hpmn else '' + platform_http_port = '%d' % (node_p2p_port + 102) if hpmn else '' + + collateral_amount = 4000 if hpmn else 1000 + collateral_txid = self.nodes[0].sendtoaddress(collateral_address, collateral_amount) + # send to same address to reserve some funds for fees + self.nodes[0].sendtoaddress(funds_address, 1) + collateral_vout = 0 + self.nodes[0].generate(1) + self.sync_all(self.nodes) + + rawtx = self.nodes[0].getrawtransaction(collateral_txid, 1) + for txout in rawtx['vout']: + if txout['value'] == Decimal(collateral_amount): + collateral_vout = txout['n'] + break + assert collateral_vout is not None + + ipAndPort = '127.0.0.1:%d' % node_p2p_port + operatorReward = idx + + self.nodes[0].generate(1) + register_rpc = 'register_hpmn' if hpmn else 'register' + protx_result = self.nodes[0].protx(register_rpc, collateral_txid, collateral_vout, ipAndPort, owner_address, bls['public'], voting_address, operatorReward, reward_address, platform_node_id, platform_p2p_port, platform_http_port, funds_address, True) + self.nodes[0].generate(1) + self.sync_all(self.nodes) + mn_info = MasternodeInfo(protx_result, owner_address, voting_address, bls['public'], bls['secret'], collateral_address, collateral_txid, collateral_vout, ipAndPort, hpmn) + self.mninfo.append(mn_info) + + mn_type_str = "HPMN" if hpmn else "MN" + self.log.info("Prepared %s %d: collateral_txid=%s, collateral_vout=%d, protxHash=%s" % (mn_type_str, idx, collateral_txid, collateral_vout, protx_result)) + return mn_info + def prepare_masternodes(self): self.log.info("Preparing %d masternodes" % self.mn_count) rewardsAddr = self.nodes[0].getnewaddress() for idx in range(0, self.mn_count): - self.prepare_masternode(idx, rewardsAddr) + self.prepare_masternode(idx, rewardsAddr, False) self.sync_all() - def prepare_masternode(self, idx, rewardsAddr=None): + def prepare_masternode(self, idx, rewardsAddr=None, hpmn=False): register_fund = (idx % 2) == 0 bls = self.nodes[0].bls('generate') address = self.nodes[0].getnewaddress() + collateral_amount = HIGHPERFORMANCE_MASTERNODE_COLLATERAL if hpmn else MASTERNODE_COLLATERAL txid = None - txid = self.nodes[0].sendtoaddress(address, MASTERNODE_COLLATERAL) + txid = self.nodes[0].sendtoaddress(address, collateral_amount) collateral_vout = 0 if not register_fund: txraw = self.nodes[0].getrawtransaction(txid, True) for vout_idx in range(0, len(txraw["vout"])): vout = txraw["vout"][vout_idx] - if vout["value"] == MASTERNODE_COLLATERAL: + if vout["value"] == collateral_amount: collateral_vout = vout_idx self.nodes[0].lockunspent(False, [{'txid': txid, 'vout': collateral_vout}]) @@ -1007,16 +1152,16 @@ def prepare_masternode(self, idx, rewardsAddr=None): else: proTxHash = self.nodes[0].sendrawtransaction(protx_result) - if operatorReward > 0: self.nodes[0].generate(1) operatorPayoutAddress = self.nodes[0].getnewaddress() self.nodes[0].protx('update_service', proTxHash, ipAndPort, bls['secret'], operatorPayoutAddress, address) - self.mninfo.append(MasternodeInfo(proTxHash, ownerAddr, votingAddr, bls['public'], bls['secret'], address, txid, collateral_vout)) + self.mninfo.append(MasternodeInfo(proTxHash, ownerAddr, votingAddr, bls['public'], bls['secret'], address, txid, collateral_vout, ipAndPort, hpmn)) # self.sync_all() - self.log.info("Prepared masternode %d: collateral_txid=%s, collateral_vout=%d, protxHash=%s" % (idx, txid, collateral_vout, proTxHash)) + mn_type_str = "HPMN" if hpmn else "MN" + self.log.info("Prepared %s %d: collateral_txid=%s, collateral_vout=%d, protxHash=%s" % (mn_type_str, idx, txid, collateral_vout, proTxHash)) def remove_masternode(self, idx): mn = self.mninfo[idx] @@ -1084,12 +1229,20 @@ def start_masternode(self, mninfo, extra_args=None): mninfo.node = self.nodes[mninfo.nodeIdx] force_finish_mnsync(mninfo.node) + def dynamically_start_masternode(self, mnidx, extra_args=None): + args = [] + if extra_args is not None: + args += extra_args + self.start_node(mnidx, extra_args=args) + force_finish_mnsync(self.nodes[mnidx]) + def setup_network(self): self.log.info("Creating and starting controller node") self.add_nodes(1, extra_args=[self.extra_args[0]]) self.start_node(0) self.import_deterministic_coinbase_privkeys() - required_balance = MASTERNODE_COLLATERAL * self.mn_count + 1 + required_balance = HIGHPERFORMANCE_MASTERNODE_COLLATERAL * self.hpmn_count + required_balance += MASTERNODE_COLLATERAL * (self.mn_count - self.hpmn_count) + 1 self.log.info("Generating %d coins" % required_balance) while self.nodes[0].getbalance() < required_balance: self.bump_mocktime(1) @@ -1248,6 +1401,7 @@ def wait_for_best_chainlock(self, node, block_hash, timeout=15): def wait_for_sporks_same(self, timeout=30): def check_sporks_same(): + self.bump_mocktime(1) sporks = self.nodes[0].spork('show') return all(node.spork('show') == sporks for node in self.nodes[1:]) wait_until(check_sporks_same, timeout=timeout, sleep=0.5) diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 7ec534660fdd..474da55260cb 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -111,6 +111,7 @@ 'feature_llmq_chainlocks.py', # NOTE: needs dash_hash to pass 'feature_llmq_rotation.py', # NOTE: needs dash_hash to pass 'feature_llmq_connections.py', # NOTE: needs dash_hash to pass + 'feature_llmq_hpmn.py', # NOTE: needs dash_hash to pass 'feature_llmq_simplepose.py', # NOTE: needs dash_hash to pass 'feature_llmq_is_cl_conflicts.py', # NOTE: needs dash_hash to pass 'feature_llmq_is_migration.py', # NOTE: needs dash_hash to pass