diff --git a/src/Makefile.am b/src/Makefile.am index 5137d940baef..8a4ade852050 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -931,6 +931,7 @@ libbitcoin_common_a_SOURCES = \ deploymentinfo.cpp \ evo/core_write.cpp \ evo/netinfo.cpp \ + evo/providertx_util.cpp \ external_signer.cpp \ governance/common.cpp \ governance/core_write.cpp \ @@ -1200,6 +1201,7 @@ dash_chainstate_SOURCES = \ evo/netinfo.cpp \ evo/mnhftx.cpp \ evo/providertx.cpp \ + evo/providertx_util.cpp \ evo/simplifiedmns.cpp \ evo/smldiff.cpp \ evo/specialtx.cpp \ diff --git a/src/common/bloom.cpp b/src/common/bloom.cpp index 9d39f35c12e3..4b0c4c334f19 100644 --- a/src/common/bloom.cpp +++ b/src/common/bloom.cpp @@ -132,10 +132,13 @@ bool CBloomFilter::CheckSpecialTransactionMatchesAndUpdate(const CTransaction &t switch(tx.nType) { case(TRANSACTION_PROVIDER_REGISTER): { if (const auto opt_proTx = GetTxPayload(tx)) { + const auto owner_payouts = GetOwnerPayouts(opt_proTx->nVersion, opt_proTx->scriptPayout, opt_proTx->payouts); + const bool found_payout = std::any_of(owner_payouts.begin(), owner_payouts.end(), + [&](const auto& payout) { return 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 +163,11 @@ bool CBloomFilter::CheckSpecialTransactionMatchesAndUpdate(const CTransaction &t if (const auto opt_proTx = GetTxPayload(tx)) { if(contains(opt_proTx->proTxHash)) return true; + const auto owner_payouts = GetOwnerPayouts(opt_proTx->nVersion, opt_proTx->scriptPayout, opt_proTx->payouts); + const bool found_payout = std::any_of(owner_payouts.begin(), owner_payouts.end(), + [&](const auto& payout) { return 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..3fc879c6d2cd 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", "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..d098690fd1ba 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,69 @@ template uint16_t GetMaxFromDeployment(gsl::not_null is_basic_override); } // namespace ProTxVersion +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 ((!keyIDOwner.IsNull() && 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, + bool check_payout_collateral_reuse, TxValidationState& state) +{ + if (collateral_dest == CTxDestination(PKHash(keyIDOwner)) || + collateral_dest == CTxDestination(PKHash(keyIDVoting))) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-collateral-reuse"); + } + + if (check_payout_collateral_reuse) { + for (const auto& payout : payouts) { + CTxDestination payout_dest; + if (ExtractDestination(payout.scriptPayout, payout_dest) && payout_dest == collateral_dest) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-payee-reuse"); + } + } + } + return true; +} + template bool IsNetInfoTriviallyValid(const ProTx& proTx, TxValidationState& state) { @@ -86,10 +154,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 +169,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 +182,10 @@ 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); - } + CTxDestination dest; + const std::string strPayout = nVersion >= ProTxVersion::MultiPayout + ? PayoutListToString(payouts) + : (ExtractDestination(scriptPayout, dest) ? EncodeDestination(dest) : HexStr(scriptPayout)); s += strPayout + "|"; s += strprintf("%d", nOperatorReward) + "|"; @@ -146,11 +200,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 +221,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 +273,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..a8c24d34b80b 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,47 @@ 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; + + CScript scriptPayout; + uint16_t reward{MAX_REWARD}; + + CMasternodePayoutShare() = default; + CMasternodePayoutShare(CScript script_payout, uint16_t reward) : + scriptPayout(std::move(script_payout)), + reward(reward) + { + } + + SERIALIZE_METHODS(CMasternodePayoutShare, obj) + { + READWRITE(obj.scriptPayout, obj.reward); + } + + 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, + bool check_payout_collateral_reuse, TxValidationState& state); +[[nodiscard]] std::string PayoutListToString(const MasternodePayoutShares& payouts); +[[nodiscard]] UniValue PayoutListToJson(const MasternodePayoutShares& payouts); + class CProRegTx { public: @@ -75,6 +123,7 @@ class CProRegTx CKeyID keyIDVoting; uint16_t nOperatorReward{0}; CScript scriptPayout; + MasternodePayoutShares payouts; uint256 inputsHash; // replay protection std::vector vchSig; @@ -84,7 +133,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 +147,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 +212,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 +263,7 @@ class CProUpRegTx CBLSLazyPublicKey pubKeyOperator; CKeyID keyIDVoting; CScript scriptPayout; + MasternodePayoutShares payouts; uint256 inputsHash; // replay protection std::vector vchSig; @@ -211,7 +273,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 +281,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 +339,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/providertx_util.cpp b/src/evo/providertx_util.cpp new file mode 100644 index 000000000000..0d963b4fb6d6 --- /dev/null +++ b/src/evo/providertx_util.cpp @@ -0,0 +1,56 @@ +// Copyright (c) 2018-2025 The Dash Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include + +#include +#include