Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions doc/release-notes-7353.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
Notable changes
===============

Consensus / Masternodes
-----------------------

- DIP-0026 (Multi-Party Payouts) is implemented behind a new version-bits deployment, `v25`
(EHF, version bit 13), which additionally requires `v24` (extended addresses) to be active.
Once `v25` activates, a masternode can split its owner-side block reward on-chain among up to
32 payees instead of a single owner payout, using a new version 4 ProRegTx/ProUpRegTx. All
pre-activation behavior is unchanged: version 4 transactions are rejected until `v25` is active,
and the serialized forms for existing (pre-v4) masternodes are byte-for-byte identical. The
coinbase splits the owner reward across the payees by basis points (floor with a deterministic
one-duff remainder), summing to the owner reward exactly. (#7353)

RPC
---

- `protx register`, `protx register_fund`, `protx register_prepare` and `protx update_registrar`
now accept the `payoutAddress` argument either as a single address (unchanged) or, once DIP-0026
(`v25`) is active, as a JSON object mapping each payout address to its share in basis points
(1-10000), e.g. `{"XaddrA": 6000, "XaddrB": 4000}`. The shares must be unique and sum to exactly
10000. Over dash-cli the object is passed as a quoted JSON string. (#7353)

- `protx info`, `protx list` and `masternodelist` now report a `payoutShares` array for version 4
masternodes; the single `payoutAddress` field is omitted for those entries. Output for pre-v4
masternodes is unchanged. (#7353)
1 change: 1 addition & 0 deletions src/Makefile.test.include
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ BITCOIN_TESTS =\
test/evo_islock_tests.cpp \
test/evo_mnhf_tests.cpp \
test/evo_netinfo_tests.cpp \
test/evo_providertx_tests.cpp \
test/evo_simplifiedmns_tests.cpp \
test/evo_trivialvalidation.cpp \
test/evo_utils_tests.cpp \
Expand Down
36 changes: 36 additions & 0 deletions src/chainparams.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,15 @@ class CMainParams : public CChainParams {
consensus.vDeployments[Consensus::DEPLOYMENT_V24].nFalloffCoeff = 5; // this corresponds to 10 periods
consensus.vDeployments[Consensus::DEPLOYMENT_V24].useEHF = true;

consensus.vDeployments[Consensus::DEPLOYMENT_V25].bit = 13;
consensus.vDeployments[Consensus::DEPLOYMENT_V25].nStartTime = Consensus::BIP9Deployment::NEVER_ACTIVE; // TODO
consensus.vDeployments[Consensus::DEPLOYMENT_V25].nTimeout = Consensus::BIP9Deployment::NO_TIMEOUT; // TODO
consensus.vDeployments[Consensus::DEPLOYMENT_V25].nWindowSize = 4032;
consensus.vDeployments[Consensus::DEPLOYMENT_V25].nThresholdStart = 3226; // 80% of 4032
consensus.vDeployments[Consensus::DEPLOYMENT_V25].nThresholdMin = 2420; // 60% of 4032
consensus.vDeployments[Consensus::DEPLOYMENT_V25].nFalloffCoeff = 5; // this corresponds to 10 periods
consensus.vDeployments[Consensus::DEPLOYMENT_V25].useEHF = true;

consensus.nMinimumChainWork = uint256S("0x00000000000000000000000000000000000000000000b9040746437784aaec47"); // 2471728
consensus.defaultAssumeValid = uint256S("0x000000000000001a19ad7270422a00f86123ea94e0b295a3a796d6861bd7b032"); // 2471728

Expand Down Expand Up @@ -419,6 +428,15 @@ class CTestNetParams : public CChainParams {
consensus.vDeployments[Consensus::DEPLOYMENT_V24].nFalloffCoeff = 5; // this corresponds to 10 periods
consensus.vDeployments[Consensus::DEPLOYMENT_V24].useEHF = true;

consensus.vDeployments[Consensus::DEPLOYMENT_V25].bit = 13;
consensus.vDeployments[Consensus::DEPLOYMENT_V25].nStartTime = Consensus::BIP9Deployment::NEVER_ACTIVE; // TODO
consensus.vDeployments[Consensus::DEPLOYMENT_V25].nTimeout = Consensus::BIP9Deployment::NO_TIMEOUT;
consensus.vDeployments[Consensus::DEPLOYMENT_V25].nWindowSize = 100;
consensus.vDeployments[Consensus::DEPLOYMENT_V25].nThresholdStart = 80; // 80% of 100
consensus.vDeployments[Consensus::DEPLOYMENT_V25].nThresholdMin = 60; // 60% of 100
consensus.vDeployments[Consensus::DEPLOYMENT_V25].nFalloffCoeff = 5; // this corresponds to 10 periods
consensus.vDeployments[Consensus::DEPLOYMENT_V25].useEHF = true;

consensus.nMinimumChainWork = uint256S("0x000000000000000000000000000000000000000000000000036c8f738da818d2"); // 1400000
consensus.defaultAssumeValid = uint256S("0x000000541a23f9db7411cddbe50f9f1ebd4aa7108ebdcad62214753f648c0239"); // 1400000

Expand Down Expand Up @@ -594,6 +612,15 @@ class CDevNetParams : public CChainParams {
consensus.vDeployments[Consensus::DEPLOYMENT_V24].nFalloffCoeff = 5; // this corresponds to 10 periods
consensus.vDeployments[Consensus::DEPLOYMENT_V24].useEHF = true;

consensus.vDeployments[Consensus::DEPLOYMENT_V25].bit = 13;
consensus.vDeployments[Consensus::DEPLOYMENT_V25].nStartTime = Consensus::BIP9Deployment::NEVER_ACTIVE; // TODO
consensus.vDeployments[Consensus::DEPLOYMENT_V25].nTimeout = Consensus::BIP9Deployment::NO_TIMEOUT;
consensus.vDeployments[Consensus::DEPLOYMENT_V25].nWindowSize = 120;
consensus.vDeployments[Consensus::DEPLOYMENT_V25].nThresholdStart = 96; // 80% of 120
consensus.vDeployments[Consensus::DEPLOYMENT_V25].nThresholdMin = 72; // 60% of 120
consensus.vDeployments[Consensus::DEPLOYMENT_V25].nFalloffCoeff = 5; // this corresponds to 10 periods
consensus.vDeployments[Consensus::DEPLOYMENT_V25].useEHF = true;

consensus.nMinimumChainWork = uint256{};
consensus.defaultAssumeValid = uint256{};

Expand Down Expand Up @@ -831,6 +858,15 @@ class CRegTestParams : public CChainParams {
consensus.vDeployments[Consensus::DEPLOYMENT_V24].nFalloffCoeff = 5; // this corresponds to 10 periods
consensus.vDeployments[Consensus::DEPLOYMENT_V24].useEHF = true;

consensus.vDeployments[Consensus::DEPLOYMENT_V25].bit = 13;
consensus.vDeployments[Consensus::DEPLOYMENT_V25].nStartTime = 0;
consensus.vDeployments[Consensus::DEPLOYMENT_V25].nTimeout = Consensus::BIP9Deployment::NO_TIMEOUT;
consensus.vDeployments[Consensus::DEPLOYMENT_V25].nWindowSize = 250;
consensus.vDeployments[Consensus::DEPLOYMENT_V25].nThresholdStart = 250 / 5 * 4; // 80% of window size
consensus.vDeployments[Consensus::DEPLOYMENT_V25].nThresholdMin = 250 / 5 * 3; // 60% of window size
consensus.vDeployments[Consensus::DEPLOYMENT_V25].nFalloffCoeff = 5; // this corresponds to 10 periods
consensus.vDeployments[Consensus::DEPLOYMENT_V25].useEHF = true;

consensus.nMinimumChainWork = uint256{};
consensus.defaultAssumeValid = uint256{};

Expand Down
1 change: 1 addition & 0 deletions src/consensus/params.h
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ constexpr bool ValidDeployment(BuriedDeployment dep) { return dep <= DEPLOYMENT_
enum DeploymentPos : uint16_t {
DEPLOYMENT_TESTDUMMY,
DEPLOYMENT_V24, // Deployment of doubling withdrawal limit, extended addresses
DEPLOYMENT_V25, // Deployment of DIP0026 multi-party masternode payouts
// NOTE: Also add new deployments to VersionBitsDeploymentInfo in deploymentinfo.cpp
MAX_VERSION_BITS_DEPLOYMENTS
};
Expand Down
4 changes: 4 additions & 0 deletions src/deploymentinfo.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ const struct VBDeploymentInfo VersionBitsDeploymentInfo[Consensus::MAX_VERSION_B
/*.name =*/"v24",
/*.gbt_force =*/true,
},
{
/*.name =*/"v25",
/*.gbt_force =*/true,
},
};

std::string DeploymentName(Consensus::BuriedDeployment dep)
Expand Down
46 changes: 46 additions & 0 deletions src/evo/core_write.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,21 @@ RPCResult GetRpcResult(const std::string& key, bool optional, const std::string&
__FILE__, __LINE__, __func__);
}

// DIP0026: the multi-party payout shares array, present only for v4 (MultiPayout) entities.
RPCResult GetPayoutSharesResult()
{
return {RPCResult::Type::ARR, "payoutShares", /*optional=*/true,
"DIP0026 multi-party payout shares (present only for v4 masternodes/transactions)",
{
{RPCResult::Type::OBJ, "", "",
{
{RPCResult::Type::STR, "payoutAddress", /*optional=*/true, "Dash address for this payout share"},
{RPCResult::Type::STR_HEX, "payoutScript", /*optional=*/true, "Payout script, if no address could be extracted"},
{RPCResult::Type::NUM, "payoutShareReward", "Reward share in basis points (1-10000)"},
}},
}};
}

RPCResult CAssetLockPayload::GetJsonHelp(const std::string& key, bool optional)
{
return {RPCResult::Type::OBJ, key, optional, key.empty() ? "" : "The asset lock special transaction",
Expand Down Expand Up @@ -212,6 +227,7 @@ RPCResult CDeterministicMNState::GetJsonHelp(const std::string& key, bool option
GetRpcResult("platformP2PPort", /*optional=*/true),
GetRpcResult("platformHTTPPort", /*optional=*/true),
GetRpcResult("payoutAddress", /*optional=*/true),
GetPayoutSharesResult(),
GetRpcResult("pubKeyOperator"),
GetRpcResult("operatorPayoutAddress", /*optional=*/true),
}};
Expand Down Expand Up @@ -246,6 +262,11 @@ UniValue CDeterministicMNState::ToJson(MnType nType) const
if (ExtractDestination(scriptPayout, dest)) {
obj.pushKV("payoutAddress", EncodeDestination(dest));
}
if (nVersion >= ProTxVersion::MultiPayout) {
UniValue shares(UniValue::VARR);
for (const auto& share : payoutShares) shares.push_back(share.ToJson());
obj.pushKV("payoutShares", shares);
}
obj.pushKV("pubKeyOperator", pubKeyOperator.ToString());
if (ExtractDestination(scriptOperatorPayout, dest)) {
obj.pushKV("operatorPayoutAddress", EncodeDestination(dest));
Expand All @@ -270,6 +291,7 @@ RPCResult CDeterministicMNStateDiff::GetJsonHelp(const std::string& key, bool op
GetRpcResult("ownerAddress", /*optional=*/true),
GetRpcResult("votingAddress", /*optional=*/true),
GetRpcResult("payoutAddress", /*optional=*/true),
GetPayoutSharesResult(),
GetRpcResult("operatorPayoutAddress", /*optional=*/true),
GetRpcResult("pubKeyOperator", /*optional=*/true),
GetRpcResult("platformNodeID", /*optional=*/true),
Expand All @@ -292,6 +314,7 @@ RPCResult CProRegTx::GetJsonHelp(const std::string& key, bool optional)
GetRpcResult("ownerAddress"),
GetRpcResult("votingAddress"),
GetRpcResult("payoutAddress", /*optional=*/true),
GetPayoutSharesResult(),
GetRpcResult("pubKeyOperator"),
GetRpcResult("operatorReward"),
GetRpcResult("platformNodeID", /*optional=*/true),
Expand All @@ -301,6 +324,18 @@ RPCResult CProRegTx::GetJsonHelp(const std::string& key, bool optional)
}};
}

UniValue PayoutShare::ToJson() const
{
UniValue obj(UniValue::VOBJ);
if (CTxDestination dest; ExtractDestination(scriptPayout, dest)) {
obj.pushKV("payoutAddress", EncodeDestination(dest));
} else {
obj.pushKV("payoutScript", HexStr(scriptPayout));
}
obj.pushKV("payoutShareReward", payoutShareReward);
return obj;
}

UniValue CProRegTx::ToJson() const
{
UniValue ret(UniValue::VOBJ);
Expand All @@ -317,6 +352,11 @@ UniValue CProRegTx::ToJson() const
if (CTxDestination dest; ExtractDestination(scriptPayout, dest)) {
ret.pushKV("payoutAddress", EncodeDestination(dest));
}
if (nVersion >= ProTxVersion::MultiPayout) {
UniValue shares(UniValue::VARR);
for (const auto& share : payoutShares) shares.push_back(share.ToJson());
ret.pushKV("payoutShares", shares);
}
ret.pushKV("pubKeyOperator", pubKeyOperator.ToString());
ret.pushKV("operatorReward", (double)nOperatorReward / 100);
if (nType == MnType::Evo) {
Expand All @@ -338,6 +378,7 @@ RPCResult CProUpRegTx::GetJsonHelp(const std::string& key, bool optional)
GetRpcResult("proTxHash"),
GetRpcResult("votingAddress"),
GetRpcResult("payoutAddress", /*optional=*/true),
GetPayoutSharesResult(),
GetRpcResult("pubKeyOperator"),
GetRpcResult("inputsHash"),
}};
Expand All @@ -352,6 +393,11 @@ UniValue CProUpRegTx::ToJson() const
if (CTxDestination dest; ExtractDestination(scriptPayout, dest)) {
ret.pushKV("payoutAddress", EncodeDestination(dest));
}
if (nVersion >= ProTxVersion::MultiPayout) {
UniValue shares(UniValue::VARR);
for (const auto& share : payoutShares) shares.push_back(share.ToJson());
ret.pushKV("payoutShares", shares);
}
ret.pushKV("pubKeyOperator", pubKeyOperator.ToString());
ret.pushKV("inputsHash", inputsHash.ToString());
return ret;
Expand Down
5 changes: 5 additions & 0 deletions src/evo/dmnstate.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ UniValue CDeterministicMNStateDiff::ToJson(MnType nType) const
obj.pushKV("payoutAddress", EncodeDestination(dest));
}
}
if (fields & Field_payoutShares) {
UniValue shares(UniValue::VARR);
for (const auto& share : state.payoutShares) shares.push_back(share.ToJson());
obj.pushKV("payoutShares", shares);
}
if (fields & Field_scriptOperatorPayout) {
CTxDestination dest;
if (ExtractDestination(state.scriptOperatorPayout, dest)) {
Expand Down
34 changes: 29 additions & 5 deletions src/evo/dmnstate.h
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ class CDeterministicMNState
CBLSLazyPublicKey pubKeyOperator;
CKeyID keyIDVoting;
std::shared_ptr<NetInfoInterface> netInfo{nullptr};
CScript scriptPayout;
CScript scriptPayout; // used for nVersion < MultiPayout
std::vector<PayoutShare> payoutShares; // DIP0026, used for nVersion >= MultiPayout
CScript scriptOperatorPayout;

uint160 platformNodeID{};
Expand All @@ -72,6 +73,7 @@ class CDeterministicMNState
keyIDVoting(proTx.keyIDVoting),
netInfo(proTx.netInfo),
scriptPayout(proTx.scriptPayout),
payoutShares(proTx.payoutShares),
platformNodeID(proTx.platformNodeID),
platformP2PPort(proTx.platformP2PPort),
platformHTTPPort(proTx.platformHTTPPort)
Expand All @@ -98,8 +100,15 @@ class CDeterministicMNState
READWRITE(
obj.keyIDVoting,
NetInfoSerWrapper(const_cast<std::shared_ptr<NetInfoInterface>&>(obj.netInfo),
obj.nVersion >= ProTxVersion::ExtAddr),
obj.scriptPayout,
obj.nVersion >= ProTxVersion::ExtAddr));
// DIP0026: v4+ stores an array of payout shares in place of the single scriptPayout.
// Pre-v4 serialization is byte-for-byte unchanged.
if (obj.nVersion < ProTxVersion::MultiPayout) {
READWRITE(obj.scriptPayout);
} else {
READWRITE(obj.payoutShares);
}
READWRITE(
obj.scriptOperatorPayout,
obj.platformNodeID);
if (obj.nVersion < ProTxVersion::ExtAddr) {
Expand All @@ -111,7 +120,11 @@ class CDeterministicMNState

void ResetOperatorFields()
{
nVersion = ProTxVersion::LegacyBLS;
// DIP0026: the multi-party payout is an owner-side property and must survive an operator
// revocation. Keep nVersion at MultiPayout when payout shares are present so they are
// still serialized; the (empty) netInfo is rebuilt as the matching extended type. Reset
// to LegacyBLS as before when there are no shares.
nVersion = payoutShares.empty() ? ProTxVersion::LegacyBLS : ProTxVersion::MultiPayout;
pubKeyOperator = CBLSLazyPublicKey();
netInfo = NetInfoInterface::MakeNetInfo(nVersion);
scriptOperatorPayout = CScript();
Expand Down Expand Up @@ -147,6 +160,15 @@ class CDeterministicMNState
h.Finalize(confirmedHashWithProRegTxHash.begin());
}

// Uniform view of the owner-side payout regardless of version: for nVersion >= MultiPayout
// returns the stored shares; for older versions synthesizes a single full share from
// scriptPayout. Downstream payout code (masternode/payments) uses this for one code path.
[[nodiscard]] std::vector<PayoutShare> GetPayoutShares() const
{
if (nVersion >= ProTxVersion::MultiPayout) return payoutShares;
return {PayoutShare{scriptPayout, PayoutShare::TOTAL_BASIS_POINTS}};
}

public:
std::string ToString() const;
[[nodiscard]] static RPCResult GetJsonHelp(const std::string& key, bool optional);
Expand Down Expand Up @@ -176,6 +198,7 @@ class CDeterministicMNStateDiff
Field_platformNodeID = 0x10000,
Field_platformP2PPort = 0x20000,
Field_platformHTTPPort = 0x40000,
Field_payoutShares = 0x80000, // DIP0026; appended last to keep existing diff bits stable
};

private:
Expand Down Expand Up @@ -207,7 +230,8 @@ class CDeterministicMNStateDiff
DMN_STATE_MEMBER(nConsecutivePayments),
DMN_STATE_MEMBER(platformNodeID),
DMN_STATE_MEMBER(platformP2PPort),
DMN_STATE_MEMBER(platformHTTPPort)
DMN_STATE_MEMBER(platformHTTPPort),
DMN_STATE_MEMBER(payoutShares)
);
#undef DMN_STATE_MEMBER

Expand Down
Loading
Loading