Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
db6c30f
consensus: add shared collateral template script and new special tx t…
PastaPastaPasta Jul 9, 2026
8ff0261
evo: extend extended-address ProRegTx payload with collateral shares
PastaPastaPasta Jul 9, 2026
ac616bc
evo: shared masternode consensus rules, state, and special transactions
PastaPastaPasta Jul 9, 2026
7cad2a6
masternode: split shared masternode rewards by share amounts
PastaPastaPasta Jul 9, 2026
1ff331d
mempool: conflict tracking and eviction for shared masternode transac…
PastaPastaPasta Jul 9, 2026
912a0b3
policy: relay carve-outs for the shared collateral template
PastaPastaPasta Jul 9, 2026
6b04a73
filters: match shared masternode fields in bloom and compact filters
PastaPastaPasta Jul 9, 2026
8b3b59b
rpc: shared masternode registration, dissolution and update commands
PastaPastaPasta Jul 9, 2026
2e5df74
test: unit and functional coverage for decentralized masternode shares
PastaPastaPasta Jul 9, 2026
2f6e09d
doc: release notes for decentralized masternode shares
PastaPastaPasta Jul 9, 2026
a87101c
evo: apply shared masternode dissolution in the collateral-spend phase
PastaPastaPasta Jul 9, 2026
1a30041
rpc: preflight shared registration terms and document standby dissolu…
PastaPastaPasta Jul 9, 2026
ef371ed
evo: commit the signature count into the ProDisTx signing digest
PastaPastaPasta Jul 10, 2026
c340701
mempool: guard removeProTxKeyChangedConflicts against already-removed…
PastaPastaPasta Jul 10, 2026
0c15a43
evo: disallow same-block registration and dissolution of a shared mas…
PastaPastaPasta Aug 1, 2026
31936df
rpc: fail clearly when special transaction inputs cannot be signed at…
PastaPastaPasta Aug 1, 2026
0e5941d
test: filter matching and reorg coverage for shared masternodes
PastaPastaPasta Aug 1, 2026
3a827db
test: cover shared masternode revival and pre-activation rejection
PastaPastaPasta Aug 1, 2026
56c13c7
rpc, gui: surface shared masternodes in payee displays and wallet fil…
PastaPastaPasta Aug 1, 2026
e9e86aa
rpc: reject already-signed dissolutions in protx shared_sign
PastaPastaPasta Aug 1, 2026
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
54 changes: 54 additions & 0 deletions doc/release-notes-7437.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Decentralized Masternode Shares

This release implements the Decentralized Masternode Shares DIP, activating
together with DIP-0026 multi-party payouts as part of the v24 hard fork
(`DEPLOYMENT_V24`). Before activation there is no behavior change.

## Consensus changes (active with v24)

- A version 3 (extended addresses) ProRegTx may carry a collateral share table: 2 to 8 participants
fund the masternode collateral atomically in one registration, each recording
an immutable amount, refund script and share owner key, plus an updatable
reward script. Every participant consents by signing a digest that binds the
exact funding inputs, all outputs, the share table, the penalty terms and the
registrar configuration.
- The shared collateral is paid to the 7-byte template script
`04445348437551` (`0x04 "DSHC" OP_DROP OP_TRUE`). From activation, an output
paying this exact script is valid only as the collateral of a valid shared
registration, and spending such an output is valid only via a ProDisTx.
Template outputs mined before activation become permanently unspendable.
- Three new special transaction types:
- **ProDisTx (type 10)** dissolves a shared masternode, refunding every
participant's principal to its immutable refund script. Exactly one
signature (unilateral, penalized during the configured early period) or one
per share (unanimous, penalty-free). Validity is monotone: a ProDisTx that
is valid at some height is valid at every later height, which makes offline
"standby dissolutions" safe.
- **ProUpShareTx (type 11)** lets one share owner update their reward script.
- **ProUpSharedRegTx (type 12)** updates the operator key and/or voting key
with a signature from every share owner. A plain ProUpRegTx is invalid for
shared masternodes.
- The owner reward of a shared masternode is split across the share table
proportionally to the recorded contributions (sequential floor, remainder to
the last entry), paying each share's reward script (or its refund script when
none is set). Operator rewards are unchanged.
- Withdrawal (asset unlock) transactions may not pay the template script.

## Relay policy changes

- The template output relays only as the declared collateral output of a shared
registration, and a template prevout is accepted only inside a ProDisTx; both
remain nonstandard everywhere else.

## New RPCs

- `protx register_shared_prepare` builds an unsigned shared registration from a
caller-supplied funding transaction.
- `protx shared_sign` signs a shared registration, dissolution or shared
registrar update with every share owner key the wallet holds.
- `protx shared_combine` combines collected signatures and optionally submits.
- `protx dissolve` creates, signs and submits a unilateral ProDisTx (or, with
`submit=false`, returns hex suitable for offline standby storage).
- `protx dissolve_prepare` builds an unsigned unanimous ProDisTx.
- `protx update_share` updates one share's reward address.
- `protx update_shared_registrar_prepare` builds an unsigned ProUpSharedRegTx.
1 change: 1 addition & 0 deletions src/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ BITCOIN_CORE_H = \
evo/mnhftx.h \
evo/netinfo.h \
evo/providertx.h \
evo/sharedcollateral.h \
evo/simplifiedmns.h \
evo/smldiff.h \
evo/specialtx.h \
Expand Down
1 change: 1 addition & 0 deletions src/Makefile.test.include
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ BITCOIN_TESTS =\
test/evo_islock_tests.cpp \
test/evo_mnhf_tests.cpp \
test/evo_netinfo_tests.cpp \
test/evo_sharedmn_tests.cpp \
test/evo_simplifiedmns_tests.cpp \
test/evo_trivialvalidation.cpp \
test/evo_utils_tests.cpp \
Expand Down
40 changes: 39 additions & 1 deletion src/common/bloom.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -135,10 +135,16 @@ bool CBloomFilter::CheckSpecialTransactionMatchesAndUpdate(const CTransaction &t
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); });
const bool found_share = std::any_of(opt_proTx->shares.begin(), opt_proTx->shares.end(),
[&](const auto& share) {
return CheckScript(share.scriptRefund) ||
CheckScript(share.RewardScript()) ||
contains(share.keyIDOwner);
});
if(contains(opt_proTx->collateralOutpoint) ||
contains(opt_proTx->keyIDOwner) ||
contains(opt_proTx->keyIDVoting) ||
found_payout) {
found_payout || found_share) {
if ((nFlags & BLOOM_UPDATE_MASK) == BLOOM_UPDATE_ALL)
insert(tx.GetHash());
return true;
Expand Down Expand Up @@ -182,6 +188,38 @@ bool CBloomFilter::CheckSpecialTransactionMatchesAndUpdate(const CTransaction &t
}
return false;
}
case(TRANSACTION_PROVIDER_DISSOLVE): {
// the refund payments are literal transaction outputs, matched by the generic output loop
if (const auto opt_proTx = GetTxPayload<CProDisTx>(tx)) {
if(contains(opt_proTx->proTxHash))
return true;
}
return false;
}
case(TRANSACTION_PROVIDER_UPDATE_SHARE): {
if (const auto opt_proTx = GetTxPayload<CProUpShareTx>(tx)) {
if(contains(opt_proTx->proTxHash))
return true;
if(CheckScript(opt_proTx->scriptReward)) {
if ((nFlags & BLOOM_UPDATE_MASK) == BLOOM_UPDATE_ALL)
insert(opt_proTx->proTxHash);
return true;
}
}
return false;
}
case(TRANSACTION_PROVIDER_UPDATE_SHARED_REGISTRAR): {
if (const auto opt_proTx = GetTxPayload<CProUpSharedRegTx>(tx)) {
if(contains(opt_proTx->proTxHash))
return true;
if(contains(opt_proTx->keyIDVoting)) {
if ((nFlags & BLOOM_UPDATE_MASK) == BLOOM_UPDATE_ALL)
insert(opt_proTx->proTxHash);
return true;
}
}
return false;
}
case(TRANSACTION_ASSET_LOCK): {
// inputs of Asset Lock transactions are standard. But some outputs are special
if (const auto opt_assetlockTx = GetTxPayload<CAssetLockPayload>(tx)) {
Expand Down
12 changes: 12 additions & 0 deletions src/core_write.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,18 @@ void TxToUniv(const CTransaction& tx, const uint256& block_hash, UniValue& entry
if (const auto opt_assetUnlockTx = GetTxPayload<CAssetUnlockPayload>(tx)) {
entry.pushKV("assetUnlockTx", opt_assetUnlockTx->ToJson());
}
} else if (tx.nType == TRANSACTION_PROVIDER_DISSOLVE) {
if (const auto opt_proTx = GetTxPayload<CProDisTx>(tx)) {
entry.pushKV("proDisTx", opt_proTx->ToJson());
}
} else if (tx.nType == TRANSACTION_PROVIDER_UPDATE_SHARE) {
if (const auto opt_proTx = GetTxPayload<CProUpShareTx>(tx)) {
entry.pushKV("proUpShareTx", opt_proTx->ToJson());
}
} else if (tx.nType == TRANSACTION_PROVIDER_UPDATE_SHARED_REGISTRAR) {
if (const auto opt_proTx = GetTxPayload<CProUpSharedRegTx>(tx)) {
entry.pushKV("proUpSharedRegTx", opt_proTx->ToJson());
}
}

if (have_undo) {
Expand Down
5 changes: 5 additions & 0 deletions src/evo/assetlocktx.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,11 @@ bool CheckAssetUnlockTx(const BlockManager& blockman, const llmq::CQuorumManager
return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-assetunlocktx-too-many-outs");
}

// A withdrawal paying the shared-collateral template is rejected by the generic template
// creation rule (CheckSharedCollateralTemplateOutputs), which is gated on v24. It is
// deliberately NOT re-checked here: this function has no v24 activation status available, and
// an unconditional rejection would diverge from pre-activation consensus and split the chain.

const auto opt_assetUnlockTx = GetTxPayload<CAssetUnlockPayload>(tx);
if (!opt_assetUnlockTx) {
return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-assetunlocktx-payload");
Expand Down
107 changes: 105 additions & 2 deletions src/evo/core_write.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ const std::map<std::string, RPCResult> RPCRESULT_MAP{{
RESULT_MAP_ENTRY("collateralHash", RPCResult::Type::STR_HEX, "Collateral transaction hash"),
RESULT_MAP_ENTRY("collateralIndex", RPCResult::Type::NUM, "Collateral transaction output index"),
RESULT_MAP_ENTRY("consecutivePayments", RPCResult::Type::NUM, "Consecutive payments masternode has received in payment cycle"),
RESULT_MAP_ENTRY("earlyPenalty", RPCResult::Type::NUM, "Penalty in duffs for unilateral dissolution during the early period"),
RESULT_MAP_ENTRY("earlyPeriodBlocks", RPCResult::Type::NUM, "Length in blocks of the early period during which unilateral dissolution is penalized"),
RESULT_MAP_ENTRY("height", RPCResult::Type::NUM, "Block height"),
RESULT_MAP_ENTRY("inputsHash", RPCResult::Type::STR_HEX, "Hash of all the outpoints of the transaction inputs"),
RESULT_MAP_ENTRY("lastPaidHeight", RPCResult::Type::NUM, "Height masternode was last paid"),
Expand Down Expand Up @@ -77,6 +79,19 @@ const std::map<std::string, RPCResult> RPCRESULT_MAP{{
RESULT_MAP_ENTRY("registeredHeight", RPCResult::Type::NUM, "Height masternode was registered"),
RESULT_MAP_ENTRY("revocationReason", RPCResult::Type::NUM, "Reason for ProUpRegTx revocation"),
RESULT_MAP_ENTRY("service", RPCResult::Type::STR, "IP address and port of the masternode (DEPRECATED, returned only if config option -deprecatedrpc=service is passed)"),
{"shares",
{RPCResult::Type::ARR, "shares", "Collateral shares of a shared masternode",
{
{RPCResult::Type::OBJ, "", "",
{
{RPCResult::Type::NUM, "amount", "Collateral contribution in duffs"},
{RPCResult::Type::STR, "refundAddress", "Dash address the principal is refunded to at dissolution"},
{RPCResult::Type::STR_HEX, "refundScript", "Refund scriptPubKey"},
{RPCResult::Type::STR, "rewardAddress", "Dash address this share's owner rewards are paid to"},
{RPCResult::Type::STR_HEX, "rewardScript", "Reward scriptPubKey"},
{RPCResult::Type::STR, "ownerAddress", "Dash address of the share owner key"},
}},
}}},
RESULT_MAP_ENTRY("type", RPCResult::Type::NUM, "Masternode type"),
RESULT_MAP_ENTRY("type_str", RPCResult::Type::STR, "Masternode type (human-readable string)"),
RESULT_MAP_ENTRY("version", RPCResult::Type::NUM, "Special transaction version"),
Expand Down Expand Up @@ -229,6 +244,9 @@ RPCResult CDeterministicMNState::GetJsonHelp(const std::string& key, bool option
GetRpcResult("platformHTTPPort", /*optional=*/true),
GetRpcResult("payoutAddress", /*optional=*/true),
GetRpcResult("payouts", /*optional=*/true),
GetRpcResult("shares", /*optional=*/true),
GetRpcResult("earlyPeriodBlocks", /*optional=*/true),
GetRpcResult("earlyPenalty", /*optional=*/true),
GetRpcResult("pubKeyOperator"),
GetRpcResult("operatorPayoutAddress", /*optional=*/true),
}};
Expand Down Expand Up @@ -260,7 +278,11 @@ UniValue CDeterministicMNState::ToJson(MnType nType) const
}

CTxDestination dest;
if (nVersion >= ProTxVersion::ExtAddr) {
if (IsShared()) {
obj.pushKV("shares", ShareListToJson(shares));
obj.pushKV("earlyPeriodBlocks", static_cast<int64_t>(nEarlyPeriodBlocks));
obj.pushKV("earlyPenalty", nEarlyPenalty);
} else if (nVersion >= ProTxVersion::ExtAddr) {
obj.pushKV("payouts", PayoutListToJson(payouts));
} else if (ExtractDestination(scriptPayout, dest)) {
obj.pushKV("payoutAddress", EncodeDestination(dest));
Expand Down Expand Up @@ -290,6 +312,9 @@ RPCResult CDeterministicMNStateDiff::GetJsonHelp(const std::string& key, bool op
GetRpcResult("votingAddress", /*optional=*/true),
GetRpcResult("payoutAddress", /*optional=*/true),
GetRpcResult("payouts", /*optional=*/true),
GetRpcResult("shares", /*optional=*/true),
GetRpcResult("earlyPeriodBlocks", /*optional=*/true),
GetRpcResult("earlyPenalty", /*optional=*/true),
GetRpcResult("operatorPayoutAddress", /*optional=*/true),
GetRpcResult("pubKeyOperator", /*optional=*/true),
GetRpcResult("platformNodeID", /*optional=*/true),
Expand All @@ -313,6 +338,9 @@ RPCResult CProRegTx::GetJsonHelp(const std::string& key, bool optional)
GetRpcResult("votingAddress"),
GetRpcResult("payoutAddress", /*optional=*/true),
GetRpcResult("payouts", /*optional=*/true),
GetRpcResult("shares", /*optional=*/true),
GetRpcResult("earlyPeriodBlocks", /*optional=*/true),
GetRpcResult("earlyPenalty", /*optional=*/true),
GetRpcResult("pubKeyOperator"),
GetRpcResult("operatorReward"),
GetRpcResult("platformNodeID", /*optional=*/true),
Expand All @@ -335,7 +363,11 @@ 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::ExtAddr) {
if (IsShared()) {
ret.pushKV("shares", ShareListToJson(shares));
ret.pushKV("earlyPeriodBlocks", static_cast<int64_t>(nEarlyPeriodBlocks));
ret.pushKV("earlyPenalty", nEarlyPenalty);
} else if (nVersion >= ProTxVersion::ExtAddr) {
ret.pushKV("payouts", PayoutListToJson(payouts));
} else if (CTxDestination dest; ExtractDestination(scriptPayout, dest)) {
ret.pushKV("payoutAddress", EncodeDestination(dest));
Expand Down Expand Up @@ -404,6 +436,77 @@ UniValue CProUpRevTx::ToJson() const
return ret;
}

RPCResult CProDisTx::GetJsonHelp(const std::string& key, bool optional)
{
return {RPCResult::Type::OBJ, key, optional, key.empty() ? "" : "The shared masternode dissolution special transaction",
{
GetRpcResult("version"),
GetRpcResult("proTxHash"),
{RPCResult::Type::NUM, "actorIndex", "Index into the share table of the participant paying the penalty (if any) and the fee"},
{RPCResult::Type::NUM, "sigCount", "Number of signatures: 1 = unilateral, one per share = unanimous"},
}};
}

UniValue CProDisTx::ToJson() const
{
UniValue ret(UniValue::VOBJ);
ret.pushKV("version", nVersion);
ret.pushKV("proTxHash", proTxHash.ToString());
ret.pushKV("actorIndex", actorIndex);
ret.pushKV("sigCount", static_cast<uint64_t>(vchSigs.size()));
return ret;
}

RPCResult CProUpShareTx::GetJsonHelp(const std::string& key, bool optional)
{
return {RPCResult::Type::OBJ, key, optional, key.empty() ? "" : "The shared masternode share update special transaction",
{
GetRpcResult("version"),
GetRpcResult("proTxHash"),
{RPCResult::Type::NUM, "shareIndex", "Index into the share table of the share being updated"},
{RPCResult::Type::STR, "rewardAddress", /*optional=*/true, "New Dash address for this share's owner rewards; omitted when reverting to the refund script"},
GetRpcResult("inputsHash"),
}};
}

UniValue CProUpShareTx::ToJson() const
{
UniValue ret(UniValue::VOBJ);
ret.pushKV("version", nVersion);
ret.pushKV("proTxHash", proTxHash.ToString());
ret.pushKV("shareIndex", shareIndex);
if (CTxDestination dest; !scriptReward.empty() && ExtractDestination(scriptReward, dest)) {
ret.pushKV("rewardAddress", EncodeDestination(dest));
}
ret.pushKV("inputsHash", inputsHash.ToString());
return ret;
}

RPCResult CProUpSharedRegTx::GetJsonHelp(const std::string& key, bool optional)
{
return {RPCResult::Type::OBJ, key, optional, key.empty() ? "" : "The shared masternode registrar update special transaction",
{
GetRpcResult("version"),
GetRpcResult("proTxHash"),
GetRpcResult("votingAddress"),
GetRpcResult("pubKeyOperator"),
GetRpcResult("inputsHash"),
{RPCResult::Type::NUM, "sigCount", "Number of signatures; must equal the share count"},
}};
}

UniValue CProUpSharedRegTx::ToJson() const
{
UniValue ret(UniValue::VOBJ);
ret.pushKV("version", nVersion);
ret.pushKV("proTxHash", proTxHash.ToString());
ret.pushKV("votingAddress", EncodeDestination(PKHash(keyIDVoting)));
ret.pushKV("pubKeyOperator", pubKeyOperator.ToString());
ret.pushKV("inputsHash", inputsHash.ToString());
ret.pushKV("sigCount", static_cast<uint64_t>(vchSigs.size()));
return ret;
}

RPCResult CProUpServTx::GetJsonHelp(const std::string& key, bool optional)
{
return {RPCResult::Type::OBJ, key, optional, key.empty() ? "" : "The masternode update service special transaction",
Expand Down
25 changes: 23 additions & 2 deletions src/evo/deterministicmns.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -431,7 +431,19 @@ void CDeterministicMNList::AddMN(const CDeterministicMNCPtr& dmn, bool fBumpTota
strprintf("%s: Can't add a masternode %s with invalid address", __func__, dmn->proTxHash.ToString()));
}
}
if (!AddUniqueProperty(*dmn, dmn->pdmnState->keyIDOwner)) {
if (dmn->pdmnState->IsShared()) {
// A shared masternode has a null keyIDOwner; each share owner key takes its place. Share
// owner keys deliberately land in the same uniqueness namespace as keyIDOwner
// (GetUniquePropertyHash is an untagged SerializeHash of the value), which is what makes
// owner-key reuse between shared and non-shared masternodes impossible in both directions.
for (const auto& share : dmn->pdmnState->shares) {
if (!AddUniqueProperty(*dmn, share.keyIDOwner)) {
mnUniquePropertyMap = mnUniquePropertyMapSaved;
throw(std::runtime_error(strprintf("%s: Can't add a masternode %s with a duplicate share ownerKeyID=%s", __func__,
dmn->proTxHash.ToString(), EncodeDestination(PKHash(share.keyIDOwner)))));
}
}
} else if (!AddUniqueProperty(*dmn, dmn->pdmnState->keyIDOwner)) {
mnUniquePropertyMap = mnUniquePropertyMapSaved;
throw(std::runtime_error(strprintf("%s: Can't add a masternode %s with a duplicate keyIDOwner=%s", __func__,
dmn->proTxHash.ToString(), EncodeDestination(PKHash(dmn->pdmnState->keyIDOwner)))));
Expand Down Expand Up @@ -588,7 +600,16 @@ void CDeterministicMNList::RemoveMN(const uint256& proTxHash)
dmn->proTxHash.ToString()));
}
}
if (!DeleteUniqueProperty(*dmn, dmn->pdmnState->keyIDOwner)) {
if (dmn->pdmnState->IsShared()) {
// Shared masternodes have a null keyIDOwner; the share owner keys were registered instead
for (const auto& share : dmn->pdmnState->shares) {
if (!DeleteUniqueProperty(*dmn, share.keyIDOwner)) {
mnUniquePropertyMap = mnUniquePropertyMapSaved;
throw(std::runtime_error(strprintf("%s: Can't delete a masternode %s with a share ownerKeyID=%s", __func__,
proTxHash.ToString(), EncodeDestination(PKHash(share.keyIDOwner)))));
}
}
} else if (!DeleteUniqueProperty(*dmn, dmn->pdmnState->keyIDOwner)) {
mnUniquePropertyMap = mnUniquePropertyMapSaved;
throw(std::runtime_error(strprintf("%s: Can't delete a masternode %s with a keyIDOwner=%s", __func__,
proTxHash.ToString(), EncodeDestination(PKHash(dmn->pdmnState->keyIDOwner)))));
Expand Down
Loading
Loading