From dead905150da2e99642a05ea4098680ab8a89db0 Mon Sep 17 00:00:00 2001 From: Konstantin Akimov Date: Sat, 27 Jun 2026 01:23:04 +0700 Subject: [PATCH 01/16] refactor: removed unused CMasternodePayoutShare ctor --- src/evo/providertx.h | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/evo/providertx.h b/src/evo/providertx.h index a8c24d34b80b..43de6324fc78 100644 --- a/src/evo/providertx.h +++ b/src/evo/providertx.h @@ -73,13 +73,6 @@ class CMasternodePayoutShare 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); From aa1b33fbc9a6f6961ab1f7446e39a6b0d952c90e Mon Sep 17 00:00:00 2001 From: Konstantin Akimov Date: Sat, 27 Jun 2026 01:26:46 +0700 Subject: [PATCH 02/16] refactor: rename CMasternodePayoutShare to MasternodePayoutShare accordingly code style --- src/evo/providertx.cpp | 4 ++-- src/evo/providertx.h | 6 +++--- src/evo/providertx_util.cpp | 2 +- src/masternode/payments.cpp | 2 +- src/rpc/evo.cpp | 2 +- src/test/evo_deterministicmns_tests.cpp | 4 ++-- src/test/evo_trivialvalidation.cpp | 2 +- 7 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/evo/providertx.cpp b/src/evo/providertx.cpp index d098690fd1ba..32f7f7f72bad 100644 --- a/src/evo/providertx.cpp +++ b/src/evo/providertx.cpp @@ -60,7 +60,7 @@ bool IsPayoutListTriviallyValid(const MasternodePayoutShares& payouts, const CKe 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) { + if (payout.reward < MasternodePayoutShare::MIN_REWARD || payout.reward > MasternodePayoutShare::MAX_REWARD) { return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-payout-reward"); } total_reward += payout.reward; @@ -82,7 +82,7 @@ bool IsPayoutListTriviallyValid(const MasternodePayoutShares& payouts, const CKe } } - if (total_reward != CMasternodePayoutShare::MAX_REWARD) { + if (total_reward != MasternodePayoutShare::MAX_REWARD) { return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-payout-reward-sum"); } return true; diff --git a/src/evo/providertx.h b/src/evo/providertx.h index 43de6324fc78..233220cdf932 100644 --- a/src/evo/providertx.h +++ b/src/evo/providertx.h @@ -64,7 +64,7 @@ template std::optional is_basic_override = std::nullopt); } // namespace ProTxVersion -class CMasternodePayoutShare +class MasternodePayoutShare { public: static constexpr uint16_t MIN_REWARD = 100; @@ -73,7 +73,7 @@ class CMasternodePayoutShare CScript scriptPayout; uint16_t reward{MAX_REWARD}; - SERIALIZE_METHODS(CMasternodePayoutShare, obj) + SERIALIZE_METHODS(MasternodePayoutShare, obj) { READWRITE(obj.scriptPayout, obj.reward); } @@ -85,7 +85,7 @@ class CMasternodePayoutShare friend bool operator!=(const CMasternodePayoutShare& a, const CMasternodePayoutShare& b) { return !(a == b); } }; -using MasternodePayoutShares = std::vector; +using MasternodePayoutShares = std::vector; [[nodiscard]] MasternodePayoutShares LegacyPayoutAsList(const CScript& script_payout); [[nodiscard]] MasternodePayoutShares GetOwnerPayouts(uint16_t nVersion, const CScript& script_payout, diff --git a/src/evo/providertx_util.cpp b/src/evo/providertx_util.cpp index 0d963b4fb6d6..a85c34570243 100644 --- a/src/evo/providertx_util.cpp +++ b/src/evo/providertx_util.cpp @@ -17,7 +17,7 @@ MasternodePayoutShares LegacyPayoutAsList(const CScript& script_payout) { - return {{script_payout, CMasternodePayoutShare::MAX_REWARD}}; + return {{script_payout, MasternodePayoutShare::MAX_REWARD}}; } MasternodePayoutShares GetOwnerPayouts(const uint16_t nVersion, const CScript& script_payout, diff --git a/src/masternode/payments.cpp b/src/masternode/payments.cpp index f3f090e80942..da628351d131 100644 --- a/src/masternode/payments.cpp +++ b/src/masternode/payments.cpp @@ -144,7 +144,7 @@ CAmount GetMasternodePayment(int nHeight, CAmount blockValue, const Consensus::P 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) / CMasternodePayoutShare::MAX_REWARD; + : (masternodeReward * owner_payouts[i].reward) / MasternodePayoutShare::MAX_REWARD; paid_owner_reward += payout_amount; if (payout_amount > 0) { voutMasternodePaymentsRet.emplace_back(payout_amount, owner_payouts[i].scriptPayout); diff --git a/src/rpc/evo.cpp b/src/rpc/evo.cpp index 59ff1c0e7cc4..446d629cf302 100644 --- a/src/rpc/evo.cpp +++ b/src/rpc/evo.cpp @@ -325,7 +325,7 @@ static MasternodePayoutShares ParsePayouts(const UniValue& value, const std::str if (!IsValidDestination(first_dest)) { throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("invalid payout address: %s", value.get_str())); } - payouts.emplace_back(GetScriptForDestination(first_dest), CMasternodePayoutShare::MAX_REWARD); + payouts.emplace_back(GetScriptForDestination(first_dest), MasternodePayoutShare::MAX_REWARD); } return payouts; } diff --git a/src/test/evo_deterministicmns_tests.cpp b/src/test/evo_deterministicmns_tests.cpp index 3af64daa6347..0defc92787a9 100644 --- a/src/test/evo_deterministicmns_tests.cpp +++ b/src/test/evo_deterministicmns_tests.cpp @@ -552,7 +552,7 @@ void FuncProUpRegTxV4OnLegacyRejected(TestChainSetup& setup) proTx.proTxHash = proTxHash; proTx.pubKeyOperator.Set(operator_key.GetPublicKey(), bls::bls_legacy_scheme.load()); proTx.keyIDVoting = owner_key.GetPubKey().GetID(); - proTx.payouts = {{GenerateRandomAddress(), CMasternodePayoutShare::MAX_REWARD}}; + proTx.payouts = {{GenerateRandomAddress(), MasternodePayoutShare::MAX_REWARD}}; CMutableTransaction tx; tx.nVersion = 3; @@ -612,7 +612,7 @@ void FuncProUpRegTxV2CannotBypassV4PayoutCollateralReuse(TestChainSetup& setup) pro_reg.keyIDOwner = owner_key.GetPubKey().GetID(); pro_reg.pubKeyOperator.Set(operator_key.GetPublicKey(), bls::bls_legacy_scheme.load()); pro_reg.keyIDVoting = owner_key.GetPubKey().GetID(); - pro_reg.payouts = {{script_payout, CMasternodePayoutShare::MAX_REWARD}}; + pro_reg.payouts = {{script_payout, MasternodePayoutShare::MAX_REWARD}}; for (size_t i = 0; i < tx_collateral.vout.size(); ++i) { if (tx_collateral.vout[i].nValue == dmn_types::Regular.collat_amount) { pro_reg.collateralOutpoint = COutPoint(tx_collateral.GetHash(), i); diff --git a/src/test/evo_trivialvalidation.cpp b/src/test/evo_trivialvalidation.cpp index 1e47c3f9dfa2..0580b5312d27 100644 --- a/src/test/evo_trivialvalidation.cpp +++ b/src/test/evo_trivialvalidation.cpp @@ -174,7 +174,7 @@ BOOST_AUTO_TEST_CASE(multipayout_list_validation) CheckPayouts(too_many, owner_id, voting_id, "bad-protx-payouts-count"); CheckPayouts({{payout1, 99}, {payout2, 9901}}, owner_id, voting_id, "bad-protx-payout-reward"); - CheckPayouts({{payout1, CMasternodePayoutShare::MAX_REWARD + 1}}, owner_id, voting_id, "bad-protx-payout-reward"); + CheckPayouts({{payout1, MasternodePayoutShare::MAX_REWARD + 1}}, owner_id, voting_id, "bad-protx-payout-reward"); CheckPayouts({{payout1, 5000}, {payout2, 4999}}, owner_id, voting_id, "bad-protx-payout-reward-sum"); CheckPayouts({{payout1, 5000}, {payout1, 5000}}, owner_id, voting_id, "bad-protx-payee-dup"); CheckPayouts({{CScript() << OP_RETURN, 10000}}, owner_id, voting_id, "bad-protx-payee"); From da60a45ab464711bb144fbf346c9a4fa61267a7c Mon Sep 17 00:00:00 2001 From: Konstantin Akimov Date: Sat, 27 Jun 2026 01:23:45 +0700 Subject: [PATCH 03/16] refactor: let compiler to auto-generate operator== and != for MasternodePayoutShare --- src/evo/providertx.h | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/evo/providertx.h b/src/evo/providertx.h index 233220cdf932..08230f9e2a60 100644 --- a/src/evo/providertx.h +++ b/src/evo/providertx.h @@ -78,11 +78,8 @@ class MasternodePayoutShare 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); } + bool operator==(const MasternodePayoutShare& b) const = default; + bool operator!=(const MasternodePayoutShare& b) const = default; }; using MasternodePayoutShares = std::vector; From 857f1da7a594681043faccf05f2de64e710af201 Mon Sep 17 00:00:00 2001 From: Konstantin Akimov Date: Sat, 27 Jun 2026 01:21:32 +0700 Subject: [PATCH 04/16] fix!: merge v3 and v4 protx versions so far as they introduced by the same fork --- src/evo/core_write.cpp | 8 ++++---- src/evo/dmnstate.h | 2 +- src/evo/providertx.cpp | 9 ++++----- src/evo/providertx.h | 19 +++++++------------ src/evo/providertx_util.cpp | 2 +- src/evo/specialtxman.cpp | 15 ++++++--------- src/rpc/evo.cpp | 16 ++++++++-------- src/test/evo_deterministicmns_tests.cpp | 18 +++++++++--------- .../feature_masternode_payout_shares.py | 10 +++++----- test/functional/rpc_netinfo.py | 3 +-- 10 files changed, 46 insertions(+), 56 deletions(-) diff --git a/src/evo/core_write.cpp b/src/evo/core_write.cpp index 53056d0b9a69..c668f65b0dc5 100644 --- a/src/evo/core_write.cpp +++ b/src/evo/core_write.cpp @@ -260,7 +260,7 @@ UniValue CDeterministicMNState::ToJson(MnType nType) const } CTxDestination dest; - if (nVersion >= ProTxVersion::MultiPayout) { + if (nVersion >= ProTxVersion::ExtAddr) { obj.pushKV("payouts", PayoutListToJson(payouts)); } else if (ExtractDestination(scriptPayout, dest)) { obj.pushKV("payoutAddress", EncodeDestination(dest)); @@ -335,7 +335,7 @@ UniValue CProRegTx::ToJson() const ret.pushKV("addresses", GetNetInfoWithLegacyFields(*this, nType)); ret.pushKV("ownerAddress", EncodeDestination(PKHash(keyIDOwner))); ret.pushKV("votingAddress", EncodeDestination(PKHash(keyIDVoting))); - if (nVersion >= ProTxVersion::MultiPayout) { + if (nVersion >= ProTxVersion::ExtAddr) { ret.pushKV("payouts", PayoutListToJson(payouts)); } else if (CTxDestination dest; ExtractDestination(scriptPayout, dest)) { ret.pushKV("payoutAddress", EncodeDestination(dest)); @@ -373,7 +373,7 @@ UniValue CProUpRegTx::ToJson() const ret.pushKV("version", nVersion); ret.pushKV("proTxHash", proTxHash.ToString()); ret.pushKV("votingAddress", EncodeDestination(PKHash(keyIDVoting))); - if (nVersion >= ProTxVersion::MultiPayout) { + if (nVersion >= ProTxVersion::ExtAddr) { ret.pushKV("payouts", PayoutListToJson(payouts)); } else if (CTxDestination dest; ExtractDestination(scriptPayout, dest)) { ret.pushKV("payoutAddress", EncodeDestination(dest)); @@ -582,7 +582,7 @@ UniValue CSimplifiedMNListEntry::ToJson(bool extended) const if (extended) { CTxDestination dest; - if (nVersion >= ProTxVersion::MultiPayout) { + if (nVersion >= ProTxVersion::ExtAddr) { obj.pushKV("payouts", PayoutListToJson(payouts)); } else if (ExtractDestination(scriptPayout, dest)) { obj.pushKV("payoutAddress", EncodeDestination(dest)); diff --git a/src/evo/dmnstate.h b/src/evo/dmnstate.h index dfab76d1e25b..7513e6080934 100644 --- a/src/evo/dmnstate.h +++ b/src/evo/dmnstate.h @@ -101,7 +101,7 @@ class CDeterministicMNState obj.keyIDVoting, NetInfoSerWrapper(const_cast&>(obj.netInfo), obj.nVersion >= ProTxVersion::ExtAddr)); - if (obj.nVersion >= ProTxVersion::MultiPayout) { + if (obj.nVersion >= ProTxVersion::ExtAddr) { READWRITE(obj.payouts); } else { READWRITE(obj.scriptPayout); diff --git a/src/evo/providertx.cpp b/src/evo/providertx.cpp index 32f7f7f72bad..60d846eb57e5 100644 --- a/src/evo/providertx.cpp +++ b/src/evo/providertx.cpp @@ -22,14 +22,13 @@ 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>}; + constexpr bool is_extaddr_eligible{std::is_same_v, CProRegTx> || std::is_same_v, CProUpServTx> || + 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 ? is_v24_active : false, - is_multipayout_eligible ? is_v24_active : false); + is_extaddr_eligible ? is_v24_active : false); } template uint16_t GetMaxFromDeployment(gsl::not_null pindexPrev, const ChainstateManager& chainman, @@ -183,7 +182,7 @@ std::string CProRegTx::MakeSignString() const // We only include the important stuff in the string form... CTxDestination dest; - const std::string strPayout = nVersion >= ProTxVersion::MultiPayout + const std::string strPayout = nVersion >= ProTxVersion::ExtAddr ? PayoutListToString(payouts) : (ExtractDestination(scriptPayout, dest) ? EncodeDestination(dest) : HexStr(scriptPayout)); diff --git a/src/evo/providertx.h b/src/evo/providertx.h index 08230f9e2a60..4613be179998 100644 --- a/src/evo/providertx.h +++ b/src/evo/providertx.h @@ -32,17 +32,12 @@ 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, - const bool is_multi_payout = false) +[[nodiscard]] constexpr uint16_t GetMax(const bool is_basic_scheme_active, const bool is_extended_addr) { 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 @@ -123,7 +118,7 @@ class CProRegTx obj.nVersion ); if (obj.nVersion == 0 || - obj.nVersion > ProTxVersion::GetMax(/*is_basic_scheme_active=*/true, /*is_extended_addr=*/true, /*is_multi_payout=*/true)) { + obj.nVersion > ProTxVersion::GetMax(/*is_basic_scheme_active=*/true, /*is_extended_addr=*/true)) { // unknown version, bail out early return; } @@ -139,7 +134,7 @@ class CProRegTx obj.keyIDVoting, obj.nOperatorReward ); - if (obj.nVersion >= ProTxVersion::MultiPayout) { + if (obj.nVersion >= ProTxVersion::ExtAddr) { uint8_t payouts_count{0}; SER_WRITE(obj, payouts_count = static_cast(obj.payouts.size())); READWRITE(payouts_count); @@ -202,7 +197,7 @@ class CProUpServTx obj.nVersion ); if (obj.nVersion == 0 || - obj.nVersion > ProTxVersion::GetMax(/*is_basic_scheme_active=*/true, /*is_extended_addr=*/true, /*is_multi_payout=*/true)) { + obj.nVersion > ProTxVersion::GetMax(/*is_basic_scheme_active=*/true, /*is_extended_addr=*/true)) { // unknown version, bail out early return; } @@ -263,7 +258,7 @@ class CProUpRegTx obj.nVersion ); if (obj.nVersion == 0 || - obj.nVersion > ProTxVersion::GetMax(/*is_basic_scheme_active=*/true, /*is_extended_addr=*/true, /*is_multi_payout=*/true)) { + obj.nVersion > ProTxVersion::GetMax(/*is_basic_scheme_active=*/true, /*is_extended_addr=*/true)) { // unknown version, bail out early return; } @@ -273,7 +268,7 @@ class CProUpRegTx CBLSLazyPublicKeyVersionWrapper(const_cast(obj.pubKeyOperator), (obj.nVersion == ProTxVersion::LegacyBLS)), obj.keyIDVoting ); - if (obj.nVersion >= ProTxVersion::MultiPayout) { + if (obj.nVersion >= ProTxVersion::ExtAddr) { uint8_t payouts_count{0}; SER_WRITE(obj, payouts_count = static_cast(obj.payouts.size())); READWRITE(payouts_count); @@ -329,7 +324,7 @@ class CProUpRevTx obj.nVersion ); if (obj.nVersion == 0 || - obj.nVersion > ProTxVersion::GetMax(/*is_basic_scheme_active=*/true, /*is_extended_addr=*/true, /*is_multi_payout=*/true)) { + obj.nVersion > ProTxVersion::GetMax(/*is_basic_scheme_active=*/true, /*is_extended_addr=*/true)) { // unknown version, bail out early return; } diff --git a/src/evo/providertx_util.cpp b/src/evo/providertx_util.cpp index a85c34570243..db9215108847 100644 --- a/src/evo/providertx_util.cpp +++ b/src/evo/providertx_util.cpp @@ -23,7 +23,7 @@ MasternodePayoutShares LegacyPayoutAsList(const CScript& script_payout) MasternodePayoutShares GetOwnerPayouts(const uint16_t nVersion, const CScript& script_payout, const MasternodePayoutShares& payouts) { - return nVersion >= ProTxVersion::MultiPayout ? payouts : LegacyPayoutAsList(script_payout); + return nVersion >= ProTxVersion::ExtAddr ? payouts : LegacyPayoutAsList(script_payout); } std::string PayoutListToString(const MasternodePayoutShares& payouts) diff --git a/src/evo/specialtxman.cpp b/src/evo/specialtxman.cpp index bc68f76664f1..6bf250f09334 100644 --- a/src/evo/specialtxman.cpp +++ b/src/evo/specialtxman.cpp @@ -488,8 +488,8 @@ bool CSpecialTxProcessor::RebuildListFromBlock(const CBlock& block, gsl::not_nul if (operator_changed) { newState->pubKeyOperator.SetLegacy(target_version == ProTxVersion::LegacyBLS); } - if (target_version >= ProTxVersion::MultiPayout) { - newState->payouts = opt_proTx->nVersion >= ProTxVersion::MultiPayout + if (target_version >= ProTxVersion::ExtAddr) { + newState->payouts = opt_proTx->nVersion >= ProTxVersion::ExtAddr ? opt_proTx->payouts : LegacyPayoutAsList(opt_proTx->scriptPayout); newState->scriptPayout.clear(); @@ -517,7 +517,7 @@ bool CSpecialTxProcessor::RebuildListFromBlock(const CBlock& block, gsl::not_nul auto newState = std::make_shared(*dmn->pdmnState); const uint16_t old_version{static_cast(newState->nVersion)}; newState->ResetOperatorFields(); - if (old_version >= ProTxVersion::MultiPayout && !SetStateVersion(*newState, old_version, dmn->nType, state)) { + if (old_version >= ProTxVersion::ExtAddr && !SetStateVersion(*newState, old_version, dmn->nType, state)) { return false; } newState->BanIfNotBanned(nHeight); @@ -1011,13 +1011,10 @@ static bool IsVersionChangeValid(gsl::not_null pindexPrev, c return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-protx-version-upgrade"); } - if (tx_type != TRANSACTION_PROVIDER_UPDATE_SERVICE && tx_version == ProTxVersion::ExtAddr) { + if (tx_type != TRANSACTION_PROVIDER_UPDATE_REGISTRAR && tx_type != TRANSACTION_PROVIDER_UPDATE_SERVICE && tx_version == ProTxVersion::ExtAddr) { // 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; } @@ -1095,7 +1092,7 @@ bool CheckProRegTx(const CTransaction& tx, gsl::not_null pin // this check applies to internal and external collateral, but internal collaterals are not necessarily a P2PKH if (!IsPayoutListKeySafe(GetOwnerPayouts(opt_ptx->nVersion, opt_ptx->scriptPayout, opt_ptx->payouts), collateralTxDest, opt_ptx->keyIDOwner, opt_ptx->keyIDVoting, - opt_ptx->nVersion >= ProTxVersion::MultiPayout, state)) return false; + opt_ptx->nVersion >= ProTxVersion::ExtAddr, state)) return false; if (pindexPrev) { auto mnList = dmnman.GetListForBlock(pindexPrev); @@ -1272,7 +1269,7 @@ bool CheckProUpRegTx(const CTransaction& tx, gsl::not_null p return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-protx-collateral-dest"); } const bool check_payout_collateral_reuse{ - std::max(dmn->pdmnState->nVersion, opt_ptx->nVersion) >= ProTxVersion::MultiPayout}; + std::max(dmn->pdmnState->nVersion, opt_ptx->nVersion) >= ProTxVersion::ExtAddr}; if (!IsPayoutListKeySafe(owner_payouts, collateralTxDest, dmn->pdmnState->keyIDOwner, opt_ptx->keyIDVoting, check_payout_collateral_reuse, state)) return false; diff --git a/src/rpc/evo.cpp b/src/rpc/evo.cpp index 446d629cf302..25e841f9522e 100644 --- a/src/rpc/evo.cpp +++ b/src/rpc/evo.cpp @@ -152,8 +152,8 @@ static RPCArg GetRpcArg(const std::string& strParamName) }, {"payoutAddress_register", {"payoutAddress", RPCArg::Type::ARR, RPCArg::Optional::NO, - "The Dash address to use for masternode reward payments, or for v4 provider transactions, " - "an array of payout shares.", + "The Dash address to use for masternode reward payments, or after v24 activation, " + "an array of payout shares. Not compatible with legacy bls operator key.", { {"", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED, "", { @@ -165,8 +165,8 @@ static RPCArg GetRpcArg(const std::string& strParamName) }, {"payoutAddress_update", {"payoutAddress", RPCArg::Type::ARR, RPCArg::Optional::NO, - "The Dash address to use for masternode reward payments, or for v4 provider transactions, " - "an array of payout shares.\n" + "The Dash address to use for masternode reward payments, or after v24 activation, " + "an array of payout shares. Not compatible with legacy bls operator key.\n" "If set to an empty string, the currently active payout address is reused.", { {"", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED, "", @@ -837,8 +837,8 @@ static UniValue protx_register_common_wrapper(const JSONRPCRequest& request, ptx.nOperatorReward = operatorReward; CTxDestination payoutDest; - if (request.params[paramIdx + 5].isArray() && ptx.nVersion < ProTxVersion::MultiPayout) { - throw JSONRPCError(RPC_INVALID_PARAMETER, "payouts array requires provider transaction version 4"); + if (request.params[paramIdx + 5].isArray() && ptx.nVersion < ProTxVersion::ExtAddr) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "payouts array requires provider transaction version 3"); } ptx.payouts = ParsePayouts(request.params[paramIdx + 5], "payouts", payoutDest); @@ -1266,8 +1266,8 @@ static RPCHelpMan protx_update_registrar_wrapper(const bool specific_legacy_bls_ CTxDestination payoutDest; 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"); + if (ptx.nVersion < ProTxVersion::ExtAddr) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "payouts array requires provider transaction version 3"); } ptx.payouts = ParsePayouts(request.params[3], "payouts", payoutDest); ptx.scriptPayout = ptx.payouts.front().scriptPayout; diff --git a/src/test/evo_deterministicmns_tests.cpp b/src/test/evo_deterministicmns_tests.cpp index 0defc92787a9..5d541385d008 100644 --- a/src/test/evo_deterministicmns_tests.cpp +++ b/src/test/evo_deterministicmns_tests.cpp @@ -516,7 +516,7 @@ void FuncProUpRegTxVersionHandlingBeforeV24(TestChainSetup& setup) BOOST_CHECK(dmn->pdmnState->pubKeyOperator.Get() == operator_key_legacy.GetPublicKey()); }; -void FuncProUpRegTxV4OnLegacyRejected(TestChainSetup& setup) +void FuncProUpRegTxV3OnLegacyRejected(TestChainSetup& setup) { auto& chainman = *Assert(setup.m_node.chainman.get()); auto& dmnman = *Assert(setup.m_node.dmnman); @@ -548,7 +548,7 @@ void FuncProUpRegTxV4OnLegacyRejected(TestChainSetup& setup) BOOST_REQUIRE_EQUAL(dmnman.GetListAtChainTip().GetMN(proTxHash)->pdmnState->nVersion, ProTxVersion::LegacyBLS); CProUpRegTx proTx; - proTx.nVersion = ProTxVersion::MultiPayout; + proTx.nVersion = ProTxVersion::ExtAddr; proTx.proTxHash = proTxHash; proTx.pubKeyOperator.Set(operator_key.GetPublicKey(), bls::bls_legacy_scheme.load()); proTx.keyIDVoting = owner_key.GetPubKey().GetID(); @@ -573,7 +573,7 @@ void FuncProUpRegTxV4OnLegacyRejected(TestChainSetup& setup) BOOST_CHECK_EQUAL(val_state.GetRejectReason(), "bad-protx-version-upgrade"); }; -void FuncProUpRegTxV2CannotBypassV4PayoutCollateralReuse(TestChainSetup& setup) +void FuncProUpRegTxV2CannotBypassV3PayoutCollateralReuse(TestChainSetup& setup) { auto& chainman = *Assert(setup.m_node.chainman.get()); auto& dmnman = *Assert(setup.m_node.dmnman); @@ -606,7 +606,7 @@ void FuncProUpRegTxV2CannotBypassV4PayoutCollateralReuse(TestChainSetup& setup) dmnman.UpdatedBlockTip(chainman.ActiveChain().Tip()); CProRegTx pro_reg; - pro_reg.nVersion = ProTxVersion::MultiPayout; + pro_reg.nVersion = ProTxVersion::ExtAddr; pro_reg.netInfo = NetInfoInterface::MakeNetInfo(pro_reg.nVersion); BOOST_CHECK_EQUAL(pro_reg.netInfo->AddEntry(NetInfoPurpose::CORE_P2P, "1.1.1.1:9999"), NetInfoStatus::Success); pro_reg.keyIDOwner = owner_key.GetPubKey().GetID(); @@ -635,7 +635,7 @@ void FuncProUpRegTxV2CannotBypassV4PayoutCollateralReuse(TestChainSetup& setup) auto dmn = dmnman.GetListAtChainTip().GetMN(proTxHash); BOOST_REQUIRE(dmn); - BOOST_CHECK_EQUAL(dmn->pdmnState->nVersion, ProTxVersion::MultiPayout); + BOOST_CHECK_EQUAL(dmn->pdmnState->nVersion, ProTxVersion::ExtAddr); BOOST_CHECK_EQUAL(dmn->pdmnState->payouts.size(), 1U); BOOST_CHECK(dmn->pdmnState->payouts.front().scriptPayout == script_payout); @@ -1256,16 +1256,16 @@ BOOST_AUTO_TEST_CASE(proupreg_version_handling_before_v24) FuncProUpRegTxVersionHandlingBeforeV24(setup); } -BOOST_AUTO_TEST_CASE(proupreg_v4_on_legacy_rejected) +BOOST_AUTO_TEST_CASE(proupreg_v3_on_legacy_rejected) { TestChainV24SignalBeforeV19Setup setup; - FuncProUpRegTxV4OnLegacyRejected(setup); + FuncProUpRegTxV3OnLegacyRejected(setup); } -BOOST_AUTO_TEST_CASE(proupreg_v2_cannot_bypass_v4_payout_collateral_reuse) +BOOST_AUTO_TEST_CASE(proupreg_v2_cannot_bypass_v3_payout_collateral_reuse) { TestChainV24SignalBeforeV19Setup setup; - FuncProUpRegTxV2CannotBypassV4PayoutCollateralReuse(setup); + FuncProUpRegTxV2CannotBypassV3PayoutCollateralReuse(setup); } BOOST_AUTO_TEST_CASE(dip3_protx_legacy) diff --git a/test/functional/feature_masternode_payout_shares.py b/test/functional/feature_masternode_payout_shares.py index c8a0617145c3..35e6467f6f2e 100755 --- a/test/functional/feature_masternode_payout_shares.py +++ b/test/functional/feature_masternode_payout_shares.py @@ -133,11 +133,11 @@ def run_test(self): mn.set_params(proTxHash=protx_hash) raw = node.getrawtransaction(protx_hash, 1) - assert_equal(raw["proRegTx"]["version"], 4) + assert_equal(raw["proRegTx"]["version"], 3) assert_equal(payout_address_rewards(raw["proRegTx"]["payouts"]), payouts) info = node.protx("info", protx_hash) - assert_equal(info["state"]["version"], 4) + assert_equal(info["state"]["version"], 3) assert_equal(payout_address_rewards(info["state"]["payouts"]), payouts) assert "payoutAddress" not in info["state"] assert_equal(node.masternodelist("payee")[f"{mn.collateral_txid}-{mn.collateral_vout}"], f"{payout1}, {payout2}") @@ -201,7 +201,7 @@ def shortpay_payout2(coinbase): self.generate(node, 1, sync_fun=self.no_op) update_raw = node.getrawtransaction(update_hash, 1) - assert_equal(update_raw["proUpRegTx"]["version"], 4) + assert_equal(update_raw["proUpRegTx"]["version"], 3) assert_equal(payout_address_rewards(update_raw["proUpRegTx"]["payouts"]), updated_payouts) assert_equal(payout_address_rewards(node.protx("info", protx_hash)["state"]["payouts"]), updated_payouts) @@ -221,7 +221,7 @@ def shortpay_payout2(coinbase): self.generate(node, 1, sync_fun=self.no_op) info = node.protx("info", protx_hash) - assert_equal(info["state"]["version"], 4) + assert_equal(info["state"]["version"], 3) assert_equal(payout_address_rewards(info["state"]["payouts"]), updated_payouts) gbt_payees = [p for p in node.getblocktemplate()["masternode"] if p["script"] != "6a"] @@ -262,7 +262,7 @@ def shortpay_payout2(coinbase): self.generate(node, 1, sync_fun=self.no_op) info = node.protx("info", protx_hash) - assert_equal(info["state"]["version"], 4) + assert_equal(info["state"]["version"], 3) assert_equal(payout_address_rewards(info["state"]["payouts"]), updated_payouts) diff --git a/test/functional/rpc_netinfo.py b/test/functional/rpc_netinfo.py index d02d7e2bf811..2888095c35f8 100755 --- a/test/functional/rpc_netinfo.py +++ b/test/functional/rpc_netinfo.py @@ -34,7 +34,6 @@ # See ProTxVersion in src/evo/providertx.h PROTXVER_BASIC = 2 PROTXVER_EXTADDR = 3 -PROTXVER_MULTIPAYOUT = 4 # Sample domains DOMAINS_CLR = [ @@ -528,7 +527,7 @@ def empty_common(grt_dict): # Validate that masternode uses the current v24 extended-address-capable payload version grt_proregtx = self.node_simple.getrawtransaction(self.node_two.mn.proTxHash, True)['proRegTx'] - assert_equal(grt_proregtx['version'], PROTXVER_MULTIPAYOUT) + assert_equal(grt_proregtx['version'], PROTXVER_EXTADDR) # Test reporting for extended addresses empty_common(grt_proregtx) From 12a9ceda9e34842b3641c743898097a9b6cc24a5 Mon Sep 17 00:00:00 2001 From: Konstantin Akimov Date: Tue, 7 Jul 2026 12:23:33 +0700 Subject: [PATCH 05/16] fix: empty payouts after v4->v3 merge --- src/evo/specialtxman.cpp | 6 ++++++ src/node/interfaces.cpp | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/evo/specialtxman.cpp b/src/evo/specialtxman.cpp index 6bf250f09334..0a858ce30307 100644 --- a/src/evo/specialtxman.cpp +++ b/src/evo/specialtxman.cpp @@ -45,6 +45,12 @@ static bool SetStateVersion(CDeterministicMNState& state_mn, uint16_t nVersion, BlockValidationState& state) { const bool needs_extended = nVersion >= ProTxVersion::ExtAddr; + // Before the early-return: ProUpServ pre-sets nVersion, so migrate scriptPayout into the ExtAddr + // payout list here or it would be skipped. + if (needs_extended && state_mn.payouts.empty() && !state_mn.scriptPayout.empty()) { + state_mn.payouts = LegacyPayoutAsList(state_mn.scriptPayout); + state_mn.scriptPayout.clear(); + } if (state_mn.nVersion == nVersion && state_mn.netInfo->CanStorePlatform() == needs_extended) { return true; } diff --git a/src/node/interfaces.cpp b/src/node/interfaces.cpp index 385ee4aefa78..4d9e6c759f2f 100644 --- a/src/node/interfaces.cpp +++ b/src/node/interfaces.cpp @@ -123,7 +123,7 @@ class MnEntryImpl : public MnEntry MnEntry{dmn}, m_dmn{Assert(dmn)}, m_script_payouts{GetOwnerPayoutScripts(*m_dmn)}, - m_script_payout{m_script_payouts.front()} + m_script_payout{m_script_payouts.empty() ? CScript() : m_script_payouts.front()} { } ~MnEntryImpl() = default; From 013378c798df7f35515dcdb5d5e5d9e03148b525 Mon Sep 17 00:00:00 2001 From: Konstantin Akimov Date: Tue, 30 Jun 2026 22:08:59 +0700 Subject: [PATCH 06/16] refactor: simplify calls of GetOwnerPayouts by templating class --- src/common/bloom.cpp | 4 ++-- src/evo/deterministicmns.cpp | 2 +- src/evo/dmnstate.cpp | 2 +- src/evo/providertx.cpp | 8 ++++---- src/evo/providertx.h | 5 +++-- src/evo/providertx_util.cpp | 11 ++++++++--- src/evo/specialtx_filter.cpp | 4 ++-- src/evo/specialtxman.cpp | 4 ++-- src/masternode/payments.cpp | 3 +-- src/node/interfaces.cpp | 7 +++---- src/rpc/evo.cpp | 8 ++++---- src/rpc/masternode.cpp | 5 ++--- 12 files changed, 33 insertions(+), 30 deletions(-) diff --git a/src/common/bloom.cpp b/src/common/bloom.cpp index 4b0c4c334f19..b9a17600b6be 100644 --- a/src/common/bloom.cpp +++ b/src/common/bloom.cpp @@ -132,7 +132,7 @@ 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 auto owner_payouts = GetOwnerPayouts(*opt_proTx); 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) || @@ -163,7 +163,7 @@ 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 auto owner_payouts = GetOwnerPayouts(*opt_proTx); 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) || diff --git a/src/evo/deterministicmns.cpp b/src/evo/deterministicmns.cpp index df22b9198362..20db7a1f08ec 100644 --- a/src/evo/deterministicmns.cpp +++ b/src/evo/deterministicmns.cpp @@ -45,7 +45,7 @@ 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, GetOwnerPayouts(state.nVersion, state.scriptPayout, state.payouts), + state.scriptPayout, GetOwnerPayouts(state), state.scriptOperatorPayout, state.nVersion, nType); } diff --git a/src/evo/dmnstate.cpp b/src/evo/dmnstate.cpp index b384966cb9b7..de22a18c8ede 100644 --- a/src/evo/dmnstate.cpp +++ b/src/evo/dmnstate.cpp @@ -16,7 +16,7 @@ std::string CDeterministicMNState::ToString() const if (ExtractDestination(scriptPayout, dest)) { payoutAddress = EncodeDestination(dest); } - const auto owner_payouts = GetOwnerPayouts(nVersion, scriptPayout, payouts); + const auto owner_payouts = GetOwnerPayouts(*this); const std::string payoutList = PayoutListToString(owner_payouts); if (ExtractDestination(scriptOperatorPayout, dest)) { operatorPayoutAddress = EncodeDestination(dest); diff --git a/src/evo/providertx.cpp b/src/evo/providertx.cpp index 60d846eb57e5..898319c0423e 100644 --- a/src/evo/providertx.cpp +++ b/src/evo/providertx.cpp @@ -153,7 +153,7 @@ 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"); } - const auto owner_payouts = GetOwnerPayouts(nVersion, scriptPayout, payouts); + const auto owner_payouts = GetOwnerPayouts(*this); 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"); @@ -199,7 +199,7 @@ std::string CProRegTx::MakeSignString() const std::string CProRegTx::ToString() const { - const std::string payee = PayoutListToString(GetOwnerPayouts(nVersion, scriptPayout, payouts)); + const std::string payee = PayoutListToString(GetOwnerPayouts(*this)); 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", @@ -272,13 +272,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 (!IsPayoutListTriviallyValid(GetOwnerPayouts(nVersion, scriptPayout, payouts), CKeyID{}, keyIDVoting, state)) return false; + if (!IsPayoutListTriviallyValid(GetOwnerPayouts(*this), CKeyID{}, keyIDVoting, state)) return false; return true; } std::string CProUpRegTx::ToString() const { - const std::string payee = PayoutListToString(GetOwnerPayouts(nVersion, scriptPayout, payouts)); + const std::string payee = PayoutListToString(GetOwnerPayouts(*this)); 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 4613be179998..e0c6aa37ea92 100644 --- a/src/evo/providertx.h +++ b/src/evo/providertx.h @@ -80,8 +80,9 @@ class MasternodePayoutShare using MasternodePayoutShares = std::vector; [[nodiscard]] MasternodePayoutShares LegacyPayoutAsList(const CScript& script_payout); -[[nodiscard]] MasternodePayoutShares GetOwnerPayouts(uint16_t nVersion, const CScript& script_payout, - const MasternodePayoutShares& payouts); +template +[[nodiscard]] MasternodePayoutShares GetOwnerPayouts(const T& protx); + [[nodiscard]] bool IsPayoutListTriviallyValid(const MasternodePayoutShares& payouts, const CKeyID& keyIDOwner, const CKeyID& keyIDVoting, TxValidationState& state); [[nodiscard]] bool IsPayoutListKeySafe(const MasternodePayoutShares& payouts, const CTxDestination& collateral_dest, diff --git a/src/evo/providertx_util.cpp b/src/evo/providertx_util.cpp index db9215108847..478799ba1da7 100644 --- a/src/evo/providertx_util.cpp +++ b/src/evo/providertx_util.cpp @@ -3,6 +3,7 @@ // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include +#include #include #include