From 45047834a415d2749f9dccbb7c5b303435994a6f Mon Sep 17 00:00:00 2001 From: pasta Date: Fri, 5 Jun 2026 16:01:31 -0500 Subject: [PATCH 01/15] feat: add masternode reward shares --- src/common/bloom.cpp | 12 +- src/evo/core_write.cpp | 31 +++- src/evo/deterministicmns.cpp | 3 +- src/evo/dmnstate.cpp | 9 +- src/evo/dmnstate.h | 13 +- src/evo/providertx.cpp | 153 +++++++++++++----- src/evo/providertx.h | 91 +++++++++-- src/evo/simplifiedmns.cpp | 2 + src/evo/simplifiedmns.h | 4 +- src/evo/smldiff.cpp | 1 + src/evo/specialtx_filter.cpp | 12 +- src/evo/specialtxman.cpp | 36 ++--- src/masternode/payments.cpp | 13 +- src/node/interfaces.cpp | 8 +- src/rpc/evo.cpp | 90 +++++++++-- src/rpc/masternode.cpp | 16 +- src/test/evo_trivialvalidation.cpp | 54 +++++++ .../feature_masternode_payout_shares.py | 128 +++++++++++++++ .../test_framework/test_framework.py | 14 +- test/functional/test_runner.py | 1 + 20 files changed, 579 insertions(+), 112 deletions(-) create mode 100755 test/functional/feature_masternode_payout_shares.py diff --git a/src/common/bloom.cpp b/src/common/bloom.cpp index 9d39f35c12e3..3e3065f5ed19 100644 --- a/src/common/bloom.cpp +++ b/src/common/bloom.cpp @@ -132,10 +132,14 @@ bool CBloomFilter::CheckSpecialTransactionMatchesAndUpdate(const CTransaction &t switch(tx.nType) { case(TRANSACTION_PROVIDER_REGISTER): { if (const auto opt_proTx = GetTxPayload(tx)) { + bool found_payout = false; + for (const auto& payout : GetOwnerPayouts(opt_proTx->nVersion, opt_proTx->scriptPayout, opt_proTx->payouts)) { + found_payout = found_payout || CheckScript(payout.scriptPayout); + } if(contains(opt_proTx->collateralOutpoint) || contains(opt_proTx->keyIDOwner) || contains(opt_proTx->keyIDVoting) || - CheckScript(opt_proTx->scriptPayout)) { + found_payout) { if ((nFlags & BLOOM_UPDATE_MASK) == BLOOM_UPDATE_ALL) insert(tx.GetHash()); return true; @@ -160,8 +164,12 @@ bool CBloomFilter::CheckSpecialTransactionMatchesAndUpdate(const CTransaction &t if (const auto opt_proTx = GetTxPayload(tx)) { if(contains(opt_proTx->proTxHash)) return true; + bool found_payout = false; + for (const auto& payout : GetOwnerPayouts(opt_proTx->nVersion, opt_proTx->scriptPayout, opt_proTx->payouts)) { + found_payout = found_payout || CheckScript(payout.scriptPayout); + } if(contains(opt_proTx->keyIDVoting) || - CheckScript(opt_proTx->scriptPayout)) { + found_payout) { if ((nFlags & BLOOM_UPDATE_MASK) == BLOOM_UPDATE_ALL) insert(opt_proTx->proTxHash); return true; diff --git a/src/evo/core_write.cpp b/src/evo/core_write.cpp index 1ccfc998840b..b3aa4da07574 100644 --- a/src/evo/core_write.cpp +++ b/src/evo/core_write.cpp @@ -53,6 +53,16 @@ const std::map RPCRESULT_MAP{{ RESULT_MAP_ENTRY("outpoint", RPCResult::Type::STR_HEX,"The outpoint of the masternode"), RESULT_MAP_ENTRY("ownerAddress", RPCResult::Type::STR, "Dash address used for payee updates and proposal voting"), RESULT_MAP_ENTRY("payoutAddress", RPCResult::Type::STR, "Dash address used for masternode reward payments"), + {"payouts", + {RPCResult::Type::ARR, "payouts", "Owner masternode reward payout shares", + { + {RPCResult::Type::OBJ, "", "", + { + {RPCResult::Type::STR, "address", /*optional=*/true, "Dash address used for this owner payout"}, + {RPCResult::Type::STR_HEX, "script", "Owner payout scriptPubKey"}, + {RPCResult::Type::NUM, "reward", "Owner payout share in basis points"}, + }}, + }}}, RESULT_MAP_ENTRY("platformHTTPPort", RPCResult::Type::NUM, "TCP port of Platform HTTP API (DEPRECATED, returned only if config option -deprecatedrpc=service is passed)"), RESULT_MAP_ENTRY("platformNodeID", RPCResult::Type::STR_HEX, "Node ID derived from P2P public key for Platform P2P"), RESULT_MAP_ENTRY("platformP2PPort", RPCResult::Type::NUM, "TCP port of Platform P2P (DEPRECATED, returned only if config option -deprecatedrpc=service is passed)"), @@ -212,6 +222,7 @@ RPCResult CDeterministicMNState::GetJsonHelp(const std::string& key, bool option GetRpcResult("platformP2PPort", /*optional=*/true), GetRpcResult("platformHTTPPort", /*optional=*/true), GetRpcResult("payoutAddress", /*optional=*/true), + GetRpcResult("payouts", /*optional=*/true), GetRpcResult("pubKeyOperator"), GetRpcResult("operatorPayoutAddress", /*optional=*/true), }}; @@ -243,7 +254,9 @@ UniValue CDeterministicMNState::ToJson(MnType nType) const } CTxDestination dest; - if (ExtractDestination(scriptPayout, dest)) { + if (nVersion >= ProTxVersion::MultiPayout) { + obj.pushKV("payouts", PayoutListToJson(payouts)); + } else if (ExtractDestination(scriptPayout, dest)) { obj.pushKV("payoutAddress", EncodeDestination(dest)); } obj.pushKV("pubKeyOperator", pubKeyOperator.ToString()); @@ -270,6 +283,7 @@ RPCResult CDeterministicMNStateDiff::GetJsonHelp(const std::string& key, bool op GetRpcResult("ownerAddress", /*optional=*/true), GetRpcResult("votingAddress", /*optional=*/true), GetRpcResult("payoutAddress", /*optional=*/true), + GetRpcResult("payouts", /*optional=*/true), GetRpcResult("operatorPayoutAddress", /*optional=*/true), GetRpcResult("pubKeyOperator", /*optional=*/true), GetRpcResult("platformNodeID", /*optional=*/true), @@ -292,6 +306,7 @@ RPCResult CProRegTx::GetJsonHelp(const std::string& key, bool optional) GetRpcResult("ownerAddress"), GetRpcResult("votingAddress"), GetRpcResult("payoutAddress", /*optional=*/true), + GetRpcResult("payouts", /*optional=*/true), GetRpcResult("pubKeyOperator"), GetRpcResult("operatorReward"), GetRpcResult("platformNodeID", /*optional=*/true), @@ -314,7 +329,9 @@ UniValue CProRegTx::ToJson() const ret.pushKV("addresses", GetNetInfoWithLegacyFields(*this, nType)); ret.pushKV("ownerAddress", EncodeDestination(PKHash(keyIDOwner))); ret.pushKV("votingAddress", EncodeDestination(PKHash(keyIDVoting))); - if (CTxDestination dest; ExtractDestination(scriptPayout, dest)) { + if (nVersion >= ProTxVersion::MultiPayout) { + ret.pushKV("payouts", PayoutListToJson(payouts)); + } else if (CTxDestination dest; ExtractDestination(scriptPayout, dest)) { ret.pushKV("payoutAddress", EncodeDestination(dest)); } ret.pushKV("pubKeyOperator", pubKeyOperator.ToString()); @@ -338,6 +355,7 @@ RPCResult CProUpRegTx::GetJsonHelp(const std::string& key, bool optional) GetRpcResult("proTxHash"), GetRpcResult("votingAddress"), GetRpcResult("payoutAddress", /*optional=*/true), + GetRpcResult("payouts", /*optional=*/true), GetRpcResult("pubKeyOperator"), GetRpcResult("inputsHash"), }}; @@ -349,7 +367,9 @@ UniValue CProUpRegTx::ToJson() const ret.pushKV("version", nVersion); ret.pushKV("proTxHash", proTxHash.ToString()); ret.pushKV("votingAddress", EncodeDestination(PKHash(keyIDVoting))); - if (CTxDestination dest; ExtractDestination(scriptPayout, dest)) { + if (nVersion >= ProTxVersion::MultiPayout) { + ret.pushKV("payouts", PayoutListToJson(payouts)); + } else if (CTxDestination dest; ExtractDestination(scriptPayout, dest)) { ret.pushKV("payoutAddress", EncodeDestination(dest)); } ret.pushKV("pubKeyOperator", pubKeyOperator.ToString()); @@ -528,6 +548,7 @@ RPCResult CSimplifiedMNListEntry::GetJsonHelp(const std::string& key, bool optio GetRpcResult("platformHTTPPort", /*optional=*/true), GetRpcResult("platformNodeID", /*optional=*/true), GetRpcResult("payoutAddress", /*optional=*/true), + GetRpcResult("payouts", /*optional=*/true), GetRpcResult("operatorPayoutAddress", /*optional=*/true), }}; } @@ -555,7 +576,9 @@ UniValue CSimplifiedMNListEntry::ToJson(bool extended) const if (extended) { CTxDestination dest; - if (ExtractDestination(scriptPayout, dest)) { + if (nVersion >= ProTxVersion::MultiPayout) { + obj.pushKV("payouts", PayoutListToJson(payouts)); + } else if (ExtractDestination(scriptPayout, dest)) { obj.pushKV("payoutAddress", EncodeDestination(dest)); } if (ExtractDestination(scriptOperatorPayout, dest)) { diff --git a/src/evo/deterministicmns.cpp b/src/evo/deterministicmns.cpp index 9c70869382c3..df22b9198362 100644 --- a/src/evo/deterministicmns.cpp +++ b/src/evo/deterministicmns.cpp @@ -45,7 +45,8 @@ CSimplifiedMNListEntry CDeterministicMN::to_sml_entry() const const CDeterministicMNState& state{*pdmnState}; return CSimplifiedMNListEntry(proTxHash, state.confirmedHash, state.netInfo, state.pubKeyOperator, state.keyIDVoting, !state.IsBanned(), state.platformHTTPPort, state.platformNodeID, - state.scriptPayout, state.scriptOperatorPayout, state.nVersion, nType); + state.scriptPayout, GetOwnerPayouts(state.nVersion, state.scriptPayout, state.payouts), + state.scriptOperatorPayout, state.nVersion, nType); } std::string CDeterministicMN::ToString() const diff --git a/src/evo/dmnstate.cpp b/src/evo/dmnstate.cpp index 8d0601b5a6bf..2818f783e8f4 100644 --- a/src/evo/dmnstate.cpp +++ b/src/evo/dmnstate.cpp @@ -16,17 +16,19 @@ std::string CDeterministicMNState::ToString() const if (ExtractDestination(scriptPayout, dest)) { payoutAddress = EncodeDestination(dest); } + const auto owner_payouts = GetOwnerPayouts(nVersion, scriptPayout, payouts); + const std::string payoutList = PayoutListToString(owner_payouts); if (ExtractDestination(scriptOperatorPayout, dest)) { operatorPayoutAddress = EncodeDestination(dest); } return strprintf("CDeterministicMNState(nVersion=%d, nRegisteredHeight=%d, nLastPaidHeight=%d, nPoSePenalty=%d, " "nPoSeRevivedHeight=%d, nPoSeBanHeight=%d, nRevocationReason=%d, " - "ownerAddress=%s, pubKeyOperator=%s, votingAddress=%s, netInfo=%s, payoutAddress=%s, " + "ownerAddress=%s, pubKeyOperator=%s, votingAddress=%s, netInfo=%s, payoutAddress=%s, payouts=%s, " "operatorPayoutAddress=%s)\n", nVersion, nRegisteredHeight, nLastPaidHeight, nPoSePenalty, nPoSeRevivedHeight, nPoSeBanHeight, nRevocationReason, EncodeDestination(PKHash(keyIDOwner)), pubKeyOperator.ToString(), - EncodeDestination(PKHash(keyIDVoting)), netInfo->ToString(), payoutAddress, operatorPayoutAddress); + EncodeDestination(PKHash(keyIDVoting)), netInfo->ToString(), payoutAddress, payoutList, operatorPayoutAddress); } UniValue CDeterministicMNStateDiff::ToJson(MnType nType) const @@ -73,6 +75,9 @@ UniValue CDeterministicMNStateDiff::ToJson(MnType nType) const obj.pushKV("payoutAddress", EncodeDestination(dest)); } } + if (fields & Field_payouts) { + obj.pushKV("payouts", PayoutListToJson(state.payouts)); + } if (fields & Field_scriptOperatorPayout) { CTxDestination dest; if (ExtractDestination(state.scriptOperatorPayout, dest)) { diff --git a/src/evo/dmnstate.h b/src/evo/dmnstate.h index bb1a23f2074c..dfab76d1e25b 100644 --- a/src/evo/dmnstate.h +++ b/src/evo/dmnstate.h @@ -57,6 +57,7 @@ class CDeterministicMNState CKeyID keyIDVoting; std::shared_ptr netInfo{nullptr}; CScript scriptPayout; + MasternodePayoutShares payouts; CScript scriptOperatorPayout; uint160 platformNodeID{}; @@ -72,6 +73,7 @@ class CDeterministicMNState keyIDVoting(proTx.keyIDVoting), netInfo(proTx.netInfo), scriptPayout(proTx.scriptPayout), + payouts(proTx.payouts), platformNodeID(proTx.platformNodeID), platformP2PPort(proTx.platformP2PPort), platformHTTPPort(proTx.platformHTTPPort) @@ -98,8 +100,13 @@ class CDeterministicMNState READWRITE( obj.keyIDVoting, NetInfoSerWrapper(const_cast&>(obj.netInfo), - obj.nVersion >= ProTxVersion::ExtAddr), - obj.scriptPayout, + obj.nVersion >= ProTxVersion::ExtAddr)); + if (obj.nVersion >= ProTxVersion::MultiPayout) { + READWRITE(obj.payouts); + } else { + READWRITE(obj.scriptPayout); + } + READWRITE( obj.scriptOperatorPayout, obj.platformNodeID); if (obj.nVersion < ProTxVersion::ExtAddr) { @@ -176,6 +183,7 @@ class CDeterministicMNStateDiff Field_platformNodeID = 0x10000, Field_platformP2PPort = 0x20000, Field_platformHTTPPort = 0x40000, + Field_payouts = 0x80000, }; private: @@ -203,6 +211,7 @@ class CDeterministicMNStateDiff DMN_STATE_MEMBER(keyIDVoting), DMN_STATE_MEMBER(netInfo), DMN_STATE_MEMBER(scriptPayout), + DMN_STATE_MEMBER(payouts), DMN_STATE_MEMBER(scriptOperatorPayout), DMN_STATE_MEMBER(nConsecutivePayments), DMN_STATE_MEMBER(platformNodeID), diff --git a/src/evo/providertx.cpp b/src/evo/providertx.cpp index b2c3691aa4f5..2527279b8337 100644 --- a/src/evo/providertx.cpp +++ b/src/evo/providertx.cpp @@ -15,16 +15,21 @@ #include #include +#include + namespace ProTxVersion { template [[nodiscard]] uint16_t GetMaxFromDeployment(gsl::not_null pindexPrev, const ChainstateManager& chainman, std::optional is_basic_override) { constexpr bool is_extaddr_eligible{std::is_same_v, CProRegTx> || std::is_same_v, CProUpServTx>}; + constexpr bool is_multipayout_eligible{std::is_same_v, CProRegTx> || std::is_same_v, CProUpRegTx>}; + const bool is_v24_active{DeploymentActiveAfter(pindexPrev, chainman, Consensus::DEPLOYMENT_V24)}; return ProTxVersion::GetMax( is_basic_override ? *is_basic_override : DeploymentActiveAfter(pindexPrev, chainman.GetConsensus(), Consensus::DEPLOYMENT_V19), - is_extaddr_eligible ? DeploymentActiveAfter(pindexPrev, chainman, Consensus::DEPLOYMENT_V24) : false); + is_extaddr_eligible ? is_v24_active : false, + is_multipayout_eligible ? is_v24_active : false); } template uint16_t GetMaxFromDeployment(gsl::not_null pindexPrev, const ChainstateManager& chainman, @@ -40,6 +45,109 @@ template uint16_t GetMaxFromDeployment(gsl::not_null is_basic_override); } // namespace ProTxVersion +MasternodePayoutShares LegacyPayoutAsList(const CScript& script_payout) +{ + return {{CMasternodePayoutShare::MAX_REWARD, script_payout}}; +} + +MasternodePayoutShares GetOwnerPayouts(const uint16_t nVersion, const CScript& script_payout, + const MasternodePayoutShares& payouts) +{ + return nVersion >= ProTxVersion::MultiPayout ? payouts : LegacyPayoutAsList(script_payout); +} + +static bool IsValidPayoutScript(const CScript& script) +{ + return script.IsPayToPublicKeyHash() || script.IsPayToScriptHash(); +} + +bool IsPayoutListTriviallyValid(const MasternodePayoutShares& payouts, const CKeyID& keyIDOwner, + const CKeyID& keyIDVoting, TxValidationState& state) +{ + if (payouts.empty() || payouts.size() > 8) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-payouts-count"); + } + + uint32_t total_reward{0}; + std::set seen_scripts; + for (const auto& payout : payouts) { + if (payout.reward < CMasternodePayoutShare::MIN_REWARD || payout.reward > CMasternodePayoutShare::MAX_REWARD) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-payout-reward"); + } + total_reward += payout.reward; + + if (!IsValidPayoutScript(payout.scriptPayout)) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-payee"); + } + if (!seen_scripts.emplace(payout.scriptPayout).second) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-payee-dup"); + } + + CTxDestination payout_dest; + if (!ExtractDestination(payout.scriptPayout, payout_dest)) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-payee-dest"); + } + if (payout_dest == CTxDestination(PKHash(keyIDOwner)) || payout_dest == CTxDestination(PKHash(keyIDVoting))) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-payee-reuse"); + } + } + + if (total_reward != CMasternodePayoutShare::MAX_REWARD) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-payout-reward-sum"); + } + return true; +} + +bool IsPayoutListKeySafe(const MasternodePayoutShares& payouts, const CTxDestination& collateral_dest, + const CKeyID& keyIDOwner, const CKeyID& keyIDVoting, TxValidationState& state) +{ + if (collateral_dest == CTxDestination(PKHash(keyIDOwner)) || + collateral_dest == CTxDestination(PKHash(keyIDVoting))) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-collateral-reuse"); + } + + const auto* collateral_pkhash = std::get_if(&collateral_dest); + if (!collateral_pkhash) { + return true; + } + + for (const auto& payout : payouts) { + CTxDestination payout_dest; + if (ExtractDestination(payout.scriptPayout, payout_dest) && + payout_dest == CTxDestination(*collateral_pkhash)) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-payee-reuse"); + } + } + return true; +} + +std::string PayoutListToString(const MasternodePayoutShares& payouts) +{ + std::string ret; + for (const auto& payout : payouts) { + CTxDestination dest; + const std::string payout_str = ExtractDestination(payout.scriptPayout, dest) ? EncodeDestination(dest) : HexStr(payout.scriptPayout); + if (!ret.empty()) ret += ","; + ret += strprintf("%d:%s", payout.reward, payout_str); + } + return ret; +} + +UniValue PayoutListToJson(const MasternodePayoutShares& payouts) +{ + UniValue ret(UniValue::VARR); + for (const auto& payout : payouts) { + UniValue obj(UniValue::VOBJ); + obj.pushKV("reward", payout.reward); + if (CTxDestination dest; ExtractDestination(payout.scriptPayout, dest)) { + obj.pushKV("address", EncodeDestination(dest)); + } + obj.pushKV("script", HexStr(payout.scriptPayout)); + ret.push_back(obj); + } + return ret; +} + template bool IsNetInfoTriviallyValid(const ProTx& proTx, TxValidationState& state) { @@ -86,10 +194,9 @@ bool CProRegTx::IsTriviallyValid(gsl::not_null pindexPrev, c if (pubKeyOperator.IsLegacy() != (nVersion == ProTxVersion::LegacyBLS)) { return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-operator-pubkey"); } - if (!scriptPayout.IsPayToPublicKeyHash() && !scriptPayout.IsPayToScriptHash()) { - return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-payee"); - } - if (netInfo->CanStorePlatform() != (nVersion == ProTxVersion::ExtAddr)) { + const auto owner_payouts = GetOwnerPayouts(nVersion, scriptPayout, payouts); + if (!IsPayoutListTriviallyValid(owner_payouts, keyIDOwner, keyIDVoting, state)) return false; + if (netInfo->CanStorePlatform() != (nVersion >= ProTxVersion::ExtAddr)) { return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-protx-netinfo-version"); } if (!netInfo->IsEmpty() && !IsNetInfoTriviallyValid(*this, state)) { @@ -102,16 +209,6 @@ bool CProRegTx::IsTriviallyValid(gsl::not_null pindexPrev, c } } - CTxDestination payoutDest; - if (!ExtractDestination(scriptPayout, payoutDest)) { - // should not happen as we checked script types before - return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-payee-dest"); - } - // don't allow reuse of payout key for other keys (don't allow people to put the payee key onto an online server) - if (payoutDest == CTxDestination(PKHash(keyIDOwner)) || payoutDest == CTxDestination(PKHash(keyIDVoting))) { - return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-payee-reuse"); - } - if (nOperatorReward > 10000) { return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-operator-reward"); } @@ -125,13 +222,7 @@ std::string CProRegTx::MakeSignString() const // We only include the important stuff in the string form... - CTxDestination destPayout; - std::string strPayout; - if (ExtractDestination(scriptPayout, destPayout)) { - strPayout = EncodeDestination(destPayout); - } else { - strPayout = HexStr(scriptPayout); - } + const std::string strPayout = PayoutListToString(GetOwnerPayouts(nVersion, scriptPayout, payouts)); s += strPayout + "|"; s += strprintf("%d", nOperatorReward) + "|"; @@ -146,11 +237,7 @@ std::string CProRegTx::MakeSignString() const std::string CProRegTx::ToString() const { - CTxDestination dest; - std::string payee = "unknown"; - if (ExtractDestination(scriptPayout, dest)) { - payee = EncodeDestination(dest); - } + const std::string payee = PayoutListToString(GetOwnerPayouts(nVersion, scriptPayout, payouts)); return strprintf("CProRegTx(nVersion=%d, nType=%d, collateralOutpoint=%s, netInfo=%s, nOperatorReward=%f, " "ownerAddress=%s, pubKeyOperator=%s, votingAddress=%s, scriptPayout=%s, platformNodeID=%s%s)\n", @@ -171,7 +258,7 @@ bool CProUpServTx::IsTriviallyValid(gsl::not_null pindexPrev if (nVersion < ProTxVersion::BasicBLS && nType == MnType::Evo) { return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-protx-evo-version"); } - if (netInfo->CanStorePlatform() != (nVersion == ProTxVersion::ExtAddr)) { + if (netInfo->CanStorePlatform() != (nVersion >= ProTxVersion::ExtAddr)) { return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-protx-netinfo-version"); } if (netInfo->IsEmpty()) { @@ -223,19 +310,13 @@ bool CProUpRegTx::IsTriviallyValid(gsl::not_null pindexPrev, if (pubKeyOperator.IsLegacy() != (nVersion == ProTxVersion::LegacyBLS)) { return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-operator-pubkey"); } - if (!scriptPayout.IsPayToPublicKeyHash() && !scriptPayout.IsPayToScriptHash()) { - return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-payee"); - } + if (!IsPayoutListTriviallyValid(GetOwnerPayouts(nVersion, scriptPayout, payouts), CKeyID{}, keyIDVoting, state)) return false; return true; } std::string CProUpRegTx::ToString() const { - CTxDestination dest; - std::string payee = "unknown"; - if (ExtractDestination(scriptPayout, dest)) { - payee = EncodeDestination(dest); - } + const std::string payee = PayoutListToString(GetOwnerPayouts(nVersion, scriptPayout, payouts)); return strprintf("CProUpRegTx(nVersion=%d, proTxHash=%s, pubKeyOperator=%s, votingAddress=%s, payoutAddress=%s)", nVersion, proTxHash.ToString(), pubKeyOperator.ToString(), EncodeDestination(PKHash(keyIDVoting)), payee); diff --git a/src/evo/providertx.h b/src/evo/providertx.h index 68c6fe3c7aa2..6714c35d8c33 100644 --- a/src/evo/providertx.h +++ b/src/evo/providertx.h @@ -20,6 +20,8 @@ #include #include +#include + class CBlockIndex; class ChainstateManager; class TxValidationState; @@ -30,12 +32,17 @@ enum : uint16_t { LegacyBLS = 1, BasicBLS = 2, ExtAddr = 3, + MultiPayout = 4, }; /** Get highest permissible ProTx version based on flags set. */ -[[nodiscard]] constexpr uint16_t GetMax(const bool is_basic_scheme_active, const bool is_extended_addr) +[[nodiscard]] constexpr uint16_t GetMax(const bool is_basic_scheme_active, const bool is_extended_addr, + const bool is_multi_payout = false) { if (is_basic_scheme_active) { + if (is_multi_payout) { + return ProTxVersion::MultiPayout; + } if (is_extended_addr) { // Requires *both* forks to be active to use extended addresses. is_basic_scheme_active could // be set to false due to RPC specialization, so we must evaluate is_extended_addr *last* to @@ -57,6 +64,46 @@ template std::optional is_basic_override = std::nullopt); } // namespace ProTxVersion +class CMasternodePayoutShare +{ +public: + static constexpr uint16_t MIN_REWARD = 100; + static constexpr uint16_t MAX_REWARD = 10000; + + uint16_t reward{MAX_REWARD}; + CScript scriptPayout; + + CMasternodePayoutShare() = default; + CMasternodePayoutShare(uint16_t reward, CScript script_payout) : + reward(reward), + scriptPayout(std::move(script_payout)) + { + } + + SERIALIZE_METHODS(CMasternodePayoutShare, obj) + { + READWRITE(obj.reward, obj.scriptPayout); + } + + friend bool operator==(const CMasternodePayoutShare& a, const CMasternodePayoutShare& b) + { + return a.reward == b.reward && a.scriptPayout == b.scriptPayout; + } + friend bool operator!=(const CMasternodePayoutShare& a, const CMasternodePayoutShare& b) { return !(a == b); } +}; + +using MasternodePayoutShares = std::vector; + +[[nodiscard]] MasternodePayoutShares LegacyPayoutAsList(const CScript& script_payout); +[[nodiscard]] MasternodePayoutShares GetOwnerPayouts(uint16_t nVersion, const CScript& script_payout, + const MasternodePayoutShares& payouts); +[[nodiscard]] bool IsPayoutListTriviallyValid(const MasternodePayoutShares& payouts, const CKeyID& keyIDOwner, + const CKeyID& keyIDVoting, TxValidationState& state); +[[nodiscard]] bool IsPayoutListKeySafe(const MasternodePayoutShares& payouts, const CTxDestination& collateral_dest, + const CKeyID& keyIDOwner, const CKeyID& keyIDVoting, TxValidationState& state); +[[nodiscard]] std::string PayoutListToString(const MasternodePayoutShares& payouts); +[[nodiscard]] UniValue PayoutListToJson(const MasternodePayoutShares& payouts); + class CProRegTx { public: @@ -75,6 +122,7 @@ class CProRegTx CKeyID keyIDVoting; uint16_t nOperatorReward{0}; CScript scriptPayout; + MasternodePayoutShares payouts; uint256 inputsHash; // replay protection std::vector vchSig; @@ -84,7 +132,7 @@ class CProRegTx obj.nVersion ); if (obj.nVersion == 0 || - obj.nVersion > ProTxVersion::GetMax(/*is_basic_scheme_active=*/true, /*is_extended_addr=*/true)) { + obj.nVersion > ProTxVersion::GetMax(/*is_basic_scheme_active=*/true, /*is_extended_addr=*/true, /*is_multi_payout=*/true)) { // unknown version, bail out early return; } @@ -98,8 +146,20 @@ class CProRegTx obj.keyIDOwner, CBLSLazyPublicKeyVersionWrapper(const_cast(obj.pubKeyOperator), (obj.nVersion == ProTxVersion::LegacyBLS)), obj.keyIDVoting, - obj.nOperatorReward, - obj.scriptPayout, + obj.nOperatorReward + ); + if (obj.nVersion >= ProTxVersion::MultiPayout) { + uint8_t payouts_count{0}; + SER_WRITE(obj, payouts_count = static_cast(obj.payouts.size())); + READWRITE(payouts_count); + SER_READ(obj, obj.payouts.resize(payouts_count)); + for (auto& payout : obj.payouts) { + READWRITE(payout); + } + } else { + READWRITE(obj.scriptPayout); + } + READWRITE( obj.inputsHash ); if (obj.nType == MnType::Evo) { @@ -151,7 +211,7 @@ class CProUpServTx obj.nVersion ); if (obj.nVersion == 0 || - obj.nVersion > ProTxVersion::GetMax(/*is_basic_scheme_active=*/true, /*is_extended_addr=*/true)) { + obj.nVersion > ProTxVersion::GetMax(/*is_basic_scheme_active=*/true, /*is_extended_addr=*/true, /*is_multi_payout=*/true)) { // unknown version, bail out early return; } @@ -202,6 +262,7 @@ class CProUpRegTx CBLSLazyPublicKey pubKeyOperator; CKeyID keyIDVoting; CScript scriptPayout; + MasternodePayoutShares payouts; uint256 inputsHash; // replay protection std::vector vchSig; @@ -211,7 +272,7 @@ class CProUpRegTx obj.nVersion ); if (obj.nVersion == 0 || - obj.nVersion > ProTxVersion::GetMax(/*is_basic_scheme_active=*/true, /*is_extended_addr=*/true)) { + obj.nVersion > ProTxVersion::GetMax(/*is_basic_scheme_active=*/true, /*is_extended_addr=*/true, /*is_multi_payout=*/true)) { // unknown version, bail out early return; } @@ -219,8 +280,20 @@ class CProUpRegTx obj.proTxHash, obj.nMode, CBLSLazyPublicKeyVersionWrapper(const_cast(obj.pubKeyOperator), (obj.nVersion == ProTxVersion::LegacyBLS)), - obj.keyIDVoting, - obj.scriptPayout, + obj.keyIDVoting + ); + if (obj.nVersion >= ProTxVersion::MultiPayout) { + uint8_t payouts_count{0}; + SER_WRITE(obj, payouts_count = static_cast(obj.payouts.size())); + READWRITE(payouts_count); + SER_READ(obj, obj.payouts.resize(payouts_count)); + for (auto& payout : obj.payouts) { + READWRITE(payout); + } + } else { + READWRITE(obj.scriptPayout); + } + READWRITE( obj.inputsHash ); if (!(s.GetType() & SER_GETHASH)) { @@ -265,7 +338,7 @@ class CProUpRevTx obj.nVersion ); if (obj.nVersion == 0 || - obj.nVersion > ProTxVersion::GetMax(/*is_basic_scheme_active=*/true, /*is_extended_addr=*/true)) { + obj.nVersion > ProTxVersion::GetMax(/*is_basic_scheme_active=*/true, /*is_extended_addr=*/true, /*is_multi_payout=*/true)) { // unknown version, bail out early return; } diff --git a/src/evo/simplifiedmns.cpp b/src/evo/simplifiedmns.cpp index 911c1da8756c..da7d9f1ad97e 100644 --- a/src/evo/simplifiedmns.cpp +++ b/src/evo/simplifiedmns.cpp @@ -23,6 +23,7 @@ CSimplifiedMNListEntry::CSimplifiedMNListEntry(const uint256& proreg_tx_hash, co const CBLSLazyPublicKey& pubkey_operator, const CKeyID& keyid_voting, bool is_valid, uint16_t platform_http_port, const uint160& platform_node_id, const CScript& script_payout, + const MasternodePayoutShares& payouts_in, const CScript& script_operator_payout, uint16_t version, MnType type) : proRegTxHash(proreg_tx_hash), confirmedHash(confirmed_hash), @@ -33,6 +34,7 @@ CSimplifiedMNListEntry::CSimplifiedMNListEntry(const uint256& proreg_tx_hash, co platformHTTPPort(platform_http_port), platformNodeID(platform_node_id), scriptPayout(script_payout), + payouts(payouts_in), scriptOperatorPayout(script_operator_payout), nVersion(version), nType(type) diff --git a/src/evo/simplifiedmns.h b/src/evo/simplifiedmns.h index fca52d88fa03..52d1088e8893 100644 --- a/src/evo/simplifiedmns.h +++ b/src/evo/simplifiedmns.h @@ -36,6 +36,7 @@ class CSimplifiedMNListEntry uint16_t platformHTTPPort{0}; uint160 platformNodeID{}; CScript scriptPayout; // mem-only + MasternodePayoutShares payouts; // mem-only CScript scriptOperatorPayout; // mem-only uint16_t nVersion{ProTxVersion::LegacyBLS}; MnType nType{MnType::Regular}; @@ -45,7 +46,8 @@ class CSimplifiedMNListEntry const std::shared_ptr& net_info, const CBLSLazyPublicKey& pubkey_operator, const CKeyID& keyid_voting, bool is_valid, uint16_t platform_http_port, const uint160& platform_node_id, const CScript& script_payout, - const CScript& script_operator_payout, uint16_t version, MnType type); + const MasternodePayoutShares& payouts, const CScript& script_operator_payout, + uint16_t version, MnType type); bool operator==(const CSimplifiedMNListEntry& rhs) const { diff --git a/src/evo/smldiff.cpp b/src/evo/smldiff.cpp index 0e0366a1e3ce..3c0991ea33c4 100644 --- a/src/evo/smldiff.cpp +++ b/src/evo/smldiff.cpp @@ -134,6 +134,7 @@ CSimplifiedMNListDiff BuildSimplifiedDiff(const CDeterministicMNList& from, cons CSimplifiedMNListEntry sme1{toPtr.to_sml_entry()}; CSimplifiedMNListEntry sme2(fromPtr->to_sml_entry()); if ((sme1 != sme2) || (extended && (sme1.scriptPayout != sme2.scriptPayout || + sme1.payouts != sme2.payouts || sme1.scriptOperatorPayout != sme2.scriptOperatorPayout))) { diffRet.mnList.push_back(std::move(sme1)); } diff --git a/src/evo/specialtx_filter.cpp b/src/evo/specialtx_filter.cpp index b61173dc7587..cd11be7708f6 100644 --- a/src/evo/specialtx_filter.cpp +++ b/src/evo/specialtx_filter.cpp @@ -72,8 +72,10 @@ void ExtractSpecialTxFilterElements(const CTransaction& tx, const std::function< // Add voting key ID AddHashElement(opt_proTx->keyIDVoting, addElement); - // Add payout script - AddScriptElement(opt_proTx->scriptPayout, addElement); + // Add owner payout scripts + for (const auto& payout : GetOwnerPayouts(opt_proTx->nVersion, opt_proTx->scriptPayout, opt_proTx->payouts)) { + AddScriptElement(payout.scriptPayout, addElement); + } } break; } @@ -95,8 +97,10 @@ void ExtractSpecialTxFilterElements(const CTransaction& tx, const std::function< // Add voting key ID AddHashElement(opt_proTx->keyIDVoting, addElement); - // Add payout script - AddScriptElement(opt_proTx->scriptPayout, addElement); + // Add owner payout scripts + for (const auto& payout : GetOwnerPayouts(opt_proTx->nVersion, opt_proTx->scriptPayout, opt_proTx->payouts)) { + AddScriptElement(payout.scriptPayout, addElement); + } } break; } diff --git a/src/evo/specialtxman.cpp b/src/evo/specialtxman.cpp index 65b1e6c4e2c6..1424619a36b5 100644 --- a/src/evo/specialtxman.cpp +++ b/src/evo/specialtxman.cpp @@ -396,7 +396,14 @@ bool CSpecialTxProcessor::RebuildListFromBlock(const CBlock& block, gsl::not_nul newState->pubKeyOperator = opt_proTx->pubKeyOperator; } newState->keyIDVoting = opt_proTx->keyIDVoting; - newState->scriptPayout = opt_proTx->scriptPayout; + newState->nVersion = opt_proTx->nVersion; + if (opt_proTx->nVersion >= ProTxVersion::MultiPayout) { + newState->payouts = opt_proTx->payouts; + newState->scriptPayout.clear(); + } else { + newState->scriptPayout = opt_proTx->scriptPayout; + newState->payouts.clear(); + } newList.UpdateMN(opt_proTx->proTxHash, newState); @@ -911,6 +918,9 @@ static bool IsVersionChangeValid(gsl::not_null pindexPrev, c // Only new entries (ProRegTx) and service updates (ProUpServTx) can use ExtAddr versioning return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-protx-version-tx-type"); } + if (tx_type != TRANSACTION_PROVIDER_UPDATE_REGISTRAR && tx_version == ProTxVersion::MultiPayout) { + return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-protx-version-tx-type"); + } return true; } @@ -986,10 +996,8 @@ bool CheckProRegTx(const CTransaction& tx, gsl::not_null pin // don't allow reuse of collateral key for other keys (don't allow people to put the collateral key onto an online server) // this check applies to internal and external collateral, but internal collaterals are not necessarily a P2PKH - if (collateralTxDest == CTxDestination(PKHash(opt_ptx->keyIDOwner)) || - collateralTxDest == CTxDestination(PKHash(opt_ptx->keyIDVoting))) { - return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-collateral-reuse"); - } + if (!IsPayoutListKeySafe(GetOwnerPayouts(opt_ptx->nVersion, opt_ptx->scriptPayout, opt_ptx->payouts), + collateralTxDest, opt_ptx->keyIDOwner, opt_ptx->keyIDVoting, state)) return false; if (pindexPrev) { auto mnList = dmnman.GetListForBlock(pindexPrev); @@ -1140,12 +1148,6 @@ bool CheckProUpRegTx(const CTransaction& tx, gsl::not_null p return false; } - CTxDestination payoutDest; - if (!ExtractDestination(opt_ptx->scriptPayout, payoutDest)) { - // should not happen as we checked script types before - return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-payee-dest"); - } - auto mnList = dmnman.GetListForBlock(pindexPrev); auto dmn = mnList.GetMN(opt_ptx->proTxHash); if (!dmn) { @@ -1157,11 +1159,8 @@ bool CheckProUpRegTx(const CTransaction& tx, gsl::not_null p return false; } - // don't allow reuse of payee key for other keys (don't allow people to put the payee key onto an online server) - if (payoutDest == CTxDestination(PKHash(dmn->pdmnState->keyIDOwner)) || - payoutDest == CTxDestination(PKHash(opt_ptx->keyIDVoting))) { - return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-payee-reuse"); - } + const auto owner_payouts = GetOwnerPayouts(opt_ptx->nVersion, opt_ptx->scriptPayout, opt_ptx->payouts); + if (!IsPayoutListTriviallyValid(owner_payouts, dmn->pdmnState->keyIDOwner, opt_ptx->keyIDVoting, state)) return false; Coin coin; if (!view.GetCoin(dmn->collateralOutpoint, coin) || coin.IsSpent()) { @@ -1174,10 +1173,7 @@ bool CheckProUpRegTx(const CTransaction& tx, gsl::not_null p if (!ExtractDestination(coin.out.scriptPubKey, collateralTxDest)) { return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-protx-collateral-dest"); } - if (collateralTxDest == CTxDestination(PKHash(dmn->pdmnState->keyIDOwner)) || - collateralTxDest == CTxDestination(PKHash(opt_ptx->keyIDVoting))) { - return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-collateral-reuse"); - } + if (!IsPayoutListKeySafe(owner_payouts, collateralTxDest, dmn->pdmnState->keyIDOwner, opt_ptx->keyIDVoting, state)) return false; if (mnList.HasUniqueProperty(opt_ptx->pubKeyOperator)) { auto otherDmn = mnList.GetUniquePropertyMN(opt_ptx->pubKeyOperator); diff --git a/src/masternode/payments.cpp b/src/masternode/payments.cpp index be5635d15a27..9b6e85cbd966 100644 --- a/src/masternode/payments.cpp +++ b/src/masternode/payments.cpp @@ -71,8 +71,17 @@ CAmount PlatformShare(const CAmount reward) masternodeReward -= operatorReward; } - if (masternodeReward > 0) { - voutMasternodePaymentsRet.emplace_back(masternodeReward, dmnPayee->pdmnState->scriptPayout); + const auto owner_payouts = GetOwnerPayouts(dmnPayee->pdmnState->nVersion, dmnPayee->pdmnState->scriptPayout, + dmnPayee->pdmnState->payouts); + CAmount paid_owner_reward{0}; + for (size_t i = 0; i < owner_payouts.size(); ++i) { + const bool last = i + 1 == owner_payouts.size(); + const CAmount payout_amount = last ? masternodeReward - paid_owner_reward + : (masternodeReward * owner_payouts[i].reward) / 10000; + paid_owner_reward += payout_amount; + if (payout_amount > 0) { + voutMasternodePaymentsRet.emplace_back(payout_amount, owner_payouts[i].scriptPayout); + } } if (operatorReward > 0) { voutMasternodePaymentsRet.emplace_back(operatorReward, dmnPayee->pdmnState->scriptOperatorPayout); diff --git a/src/node/interfaces.cpp b/src/node/interfaces.cpp index d9fe93f70e41..86bd0931490d 100644 --- a/src/node/interfaces.cpp +++ b/src/node/interfaces.cpp @@ -105,6 +105,7 @@ class MnEntryImpl : public MnEntry { private: CDeterministicMNCPtr m_dmn; + mutable CScript m_script_payout; public: MnEntryImpl(const CDeterministicMNCPtr& dmn) : @@ -122,7 +123,12 @@ class MnEntryImpl : public MnEntry const CKeyID& getKeyIdOwner() const override { return m_dmn->pdmnState->keyIDOwner; } const CKeyID& getKeyIdVoting() const override { return m_dmn->pdmnState->keyIDVoting; } const COutPoint& getCollateralOutpoint() const override { return m_dmn->collateralOutpoint; } - const CScript& getScriptPayout() const override { return m_dmn->pdmnState->scriptPayout; } + const CScript& getScriptPayout() const override + { + m_script_payout = GetOwnerPayouts(m_dmn->pdmnState->nVersion, m_dmn->pdmnState->scriptPayout, + m_dmn->pdmnState->payouts).front().scriptPayout; + return m_script_payout; + } const CScript& getScriptOperatorPayout() const override { return m_dmn->pdmnState->scriptOperatorPayout; } const int32_t& getLastPaidHeight() const override { return m_dmn->pdmnState->nLastPaidHeight; } const int32_t& getPoSePenalty() const override { return m_dmn->pdmnState->nPoSePenalty; } diff --git a/src/rpc/evo.cpp b/src/rpc/evo.cpp index 735f524e808b..80ddbef61aa9 100644 --- a/src/rpc/evo.cpp +++ b/src/rpc/evo.cpp @@ -28,6 +28,8 @@ #include #include +#include + #ifdef ENABLE_WALLET #include #include @@ -254,6 +256,47 @@ static CBLSPublicKey ParseBLSPubKey(const std::string& hexKey, const std::string return pubKey; } +static MasternodePayoutShares ParsePayouts(const UniValue& value, const std::string& paramName, CTxDestination& first_dest) +{ + MasternodePayoutShares payouts; + if (value.isArray()) { + const auto& arr = value.get_array(); + if (arr.empty()) { + throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("%s must contain at least one entry", paramName)); + } + for (size_t i = 0; i < arr.size(); ++i) { + const UniValue& entry = arr[i]; + if (!entry.isObject()) { + throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("%s entries must be objects", paramName)); + } + const UniValue& address_value = entry.find_value("address"); + const UniValue& reward_value = entry.find_value("reward"); + if (!address_value.isStr() || !reward_value.isNum()) { + throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("%s entries must include address and numeric reward", paramName)); + } + CTxDestination dest = DecodeDestination(address_value.get_str()); + if (!IsValidDestination(dest)) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("invalid payout address: %s", address_value.get_str())); + } + if (payouts.empty()) { + first_dest = dest; + } + const int64_t reward = reward_value.getInt(); + if (reward < 0 || reward > std::numeric_limits::max()) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "payout reward out of range"); + } + payouts.emplace_back(static_cast(reward), GetScriptForDestination(dest)); + } + } else { + first_dest = DecodeDestination(value.get_str()); + if (!IsValidDestination(first_dest)) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("invalid payout address: %s", value.get_str())); + } + payouts.emplace_back(CMasternodePayoutShare::MAX_REWARD, GetScriptForDestination(first_dest)); + } + return payouts; +} + template static void FundSpecialTx(CWallet& wallet, CMutableTransaction& tx, const SpecialTxPayload& payload, const CTxDestination& fundDest) EXCLUSIVE_LOCKS_REQUIRED(!wallet.cs_wallet) @@ -743,10 +786,11 @@ static UniValue protx_register_common_wrapper(const JSONRPCRequest& request, } ptx.nOperatorReward = operatorReward; - CTxDestination payoutDest = DecodeDestination(request.params[paramIdx + 5].get_str()); - if (!IsValidDestination(payoutDest)) { - throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("invalid payout address: %s", request.params[paramIdx + 5].get_str())); + CTxDestination payoutDest; + if (request.params[paramIdx + 5].isArray() && ptx.nVersion < ProTxVersion::MultiPayout) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "payouts array requires provider transaction version 4"); } + ptx.payouts = ParsePayouts(request.params[paramIdx + 5], "payouts", payoutDest); if (isEvoRequested) { if (!IsHex(request.params[paramIdx + 6].get_str())) { @@ -760,7 +804,7 @@ static UniValue protx_register_common_wrapper(const JSONRPCRequest& request, } ptx.keyIDVoting = keyIDVoting; - ptx.scriptPayout = GetScriptForDestination(payoutDest); + ptx.scriptPayout = ptx.payouts.front().scriptPayout; if (action != ProTxRegisterAction::Fund) { // make sure fee calculation works @@ -1073,7 +1117,8 @@ static UniValue protx_update_service_common_wrapper(const JSONRPCRequest& reques ExtractDestination(ptx.scriptOperatorPayout, feeSource); } else { // use payout address as default source for fees - ExtractDestination(dmn->pdmnState->scriptPayout, feeSource); + const auto owner_payouts = GetOwnerPayouts(dmn->pdmnState->nVersion, dmn->pdmnState->scriptPayout, dmn->pdmnState->payouts); + ExtractDestination(owner_payouts.front().scriptPayout, feeSource); } } @@ -1149,6 +1194,7 @@ static RPCHelpMan protx_update_registrar_wrapper(const bool specific_legacy_bls_ ptx.keyIDVoting = dmn->pdmnState->keyIDVoting; ptx.scriptPayout = dmn->pdmnState->scriptPayout; + ptx.payouts = GetOwnerPayouts(dmn->pdmnState->nVersion, dmn->pdmnState->scriptPayout, dmn->pdmnState->payouts); if (!request.params[1].get_str().empty()) { // new pubkey @@ -1165,13 +1211,16 @@ static RPCHelpMan protx_update_registrar_wrapper(const bool specific_legacy_bls_ } CTxDestination payoutDest; - ExtractDestination(ptx.scriptPayout, payoutDest); - if (!request.params[3].get_str().empty()) { - payoutDest = DecodeDestination(request.params[3].get_str()); - if (!IsValidDestination(payoutDest)) { - throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("invalid payout address: %s", request.params[3].get_str())); + ExtractDestination(ptx.payouts.front().scriptPayout, payoutDest); + if (request.params[3].isArray()) { + if (ptx.nVersion < ProTxVersion::MultiPayout) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "payouts array requires provider transaction version 4"); } - ptx.scriptPayout = GetScriptForDestination(payoutDest); + ptx.payouts = ParsePayouts(request.params[3], "payouts", payoutDest); + ptx.scriptPayout = ptx.payouts.front().scriptPayout; + } else if (!request.params[3].get_str().empty()) { + ptx.payouts = ParsePayouts(request.params[3], "payouts", payoutDest); + ptx.scriptPayout = ptx.payouts.front().scriptPayout; } { @@ -1302,13 +1351,12 @@ static RPCHelpMan protx_revoke() CTxDestination txDest; ExtractDestination(dmn->pdmnState->scriptOperatorPayout, txDest); FundSpecialTx(*pwallet, tx, ptx, txDest); - } else if (dmn->pdmnState->scriptPayout != CScript()) { + } else { // Using funds from previousely specified masternode payout address CTxDestination txDest; - ExtractDestination(dmn->pdmnState->scriptPayout, txDest); + const auto owner_payouts = GetOwnerPayouts(dmn->pdmnState->nVersion, dmn->pdmnState->scriptPayout, dmn->pdmnState->payouts); + ExtractDestination(owner_payouts.front().scriptPayout, txDest); FundSpecialTx(*pwallet, tx, ptx, txDest); - } else { - throw JSONRPCError(RPC_INTERNAL_ERROR, "No payout or fee source addresses found, can't revoke"); } bool fSubmit{true}; @@ -1334,6 +1382,14 @@ static bool CheckWalletOwnsScript(const CWallet* const pwallet, const CScript& s return WITH_LOCK(pwallet->cs_wallet, return pwallet->IsMine(script)) == isminetype::ISMINE_SPENDABLE; } +static bool CheckWalletOwnsAnyPayout(const CWallet* const pwallet, const CDeterministicMNState& state) +{ + for (const auto& payout : GetOwnerPayouts(state.nVersion, state.scriptPayout, state.payouts)) { + if (CheckWalletOwnsScript(pwallet, payout.scriptPayout)) return true; + } + return false; +} + static bool CheckWalletOwnsKey(const CWallet* const pwallet, const CKeyID& keyID) { return CheckWalletOwnsScript(pwallet, GetScriptForDestination(PKHash(keyID))); } @@ -1381,7 +1437,7 @@ static UniValue BuildDMNListEntry(const CWallet* const pwallet, const CDetermini walletObj.pushKV("hasOperatorKey", false); walletObj.pushKV("hasVotingKey", hasVotingKey); walletObj.pushKV("ownsCollateral", ownsCollateral); - walletObj.pushKV("ownsPayeeScript", CheckWalletOwnsScript(pwallet, dmn.pdmnState->scriptPayout)); + walletObj.pushKV("ownsPayeeScript", CheckWalletOwnsAnyPayout(pwallet, *dmn.pdmnState)); walletObj.pushKV("ownsOperatorRewardScript", CheckWalletOwnsScript(pwallet, dmn.pdmnState->scriptOperatorPayout)); o.pushKV("wallet", walletObj); } @@ -1477,7 +1533,7 @@ static RPCHelpMan protx_list() if (setOutpts.count(dmn.collateralOutpoint) || CheckWalletOwnsKey(wallet.get(), dmn.pdmnState->keyIDOwner) || CheckWalletOwnsKey(wallet.get(), dmn.pdmnState->keyIDVoting) || - CheckWalletOwnsScript(wallet.get(), dmn.pdmnState->scriptPayout) || + CheckWalletOwnsAnyPayout(wallet.get(), *dmn.pdmnState) || CheckWalletOwnsScript(wallet.get(), dmn.pdmnState->scriptOperatorPayout)) { ret.push_back(BuildDMNListEntry(wallet.get(), dmn, mn_metaman, detailed, chainman)); } diff --git a/src/rpc/masternode.cpp b/src/rpc/masternode.cpp index 14e940b61136..9fffd08cbce7 100644 --- a/src/rpc/masternode.cpp +++ b/src/rpc/masternode.cpp @@ -228,12 +228,18 @@ static std::string GetRequiredPaymentsString(governance::SuperblockManager& supe { std::string strPayments = "Unknown"; if (payee) { - CTxDestination dest; - if (!ExtractDestination(payee->pdmnState->scriptPayout, dest)) { - NONFATAL_UNREACHABLE(); + strPayments.clear(); + for (const auto& payout : GetOwnerPayouts(payee->pdmnState->nVersion, payee->pdmnState->scriptPayout, + payee->pdmnState->payouts)) { + CTxDestination dest; + if (!ExtractDestination(payout.scriptPayout, dest)) { + NONFATAL_UNREACHABLE(); + } + if (!strPayments.empty()) strPayments += ", "; + strPayments += EncodeDestination(dest); } - strPayments = EncodeDestination(dest); if (payee->nOperatorReward != 0 && payee->pdmnState->scriptOperatorPayout != CScript()) { + CTxDestination dest; if (!ExtractDestination(payee->pdmnState->scriptOperatorPayout, dest)) { NONFATAL_UNREACHABLE(); } @@ -632,7 +638,7 @@ static RPCHelpMan masternodelist_helper(bool is_composite) } } - CScript payeeScript = dmn.pdmnState->scriptPayout; + CScript payeeScript = GetOwnerPayouts(dmn.pdmnState->nVersion, dmn.pdmnState->scriptPayout, dmn.pdmnState->payouts).front().scriptPayout; CTxDestination payeeDest; std::string payeeStr = "UNKNOWN"; if (ExtractDestination(payeeScript, payeeDest)) { diff --git a/src/test/evo_trivialvalidation.cpp b/src/test/evo_trivialvalidation.cpp index 74da999581b2..967a3f3e2e8e 100644 --- a/src/test/evo_trivialvalidation.cpp +++ b/src/test/evo_trivialvalidation.cpp @@ -11,7 +11,10 @@ #include #include #include +#include #include +#include