Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
9d7436b
consensus: add shared collateral template script and new special tx t…
PastaPastaPasta Jul 9, 2026
9dc4b76
evo: extend extended-address ProRegTx payload with collateral shares
PastaPastaPasta Jul 9, 2026
5597ef1
evo: shared masternode consensus rules, state, and special transactions
PastaPastaPasta Jul 9, 2026
e45437d
masternode: split shared masternode rewards by share amounts
PastaPastaPasta Jul 9, 2026
44f4b81
mempool: conflict tracking and eviction for shared masternode transac…
PastaPastaPasta Jul 9, 2026
79ad37c
policy: relay carve-outs for the shared collateral template
PastaPastaPasta Jul 9, 2026
d8ab525
filters: match shared masternode fields in bloom and compact filters
PastaPastaPasta Jul 9, 2026
2ed8a6f
rpc: shared masternode registration, dissolution and update commands
PastaPastaPasta Jul 9, 2026
da887ff
test: unit and functional coverage for decentralized masternode shares
PastaPastaPasta Jul 9, 2026
de1b4e9
doc: release notes for decentralized masternode shares
PastaPastaPasta Jul 9, 2026
8e7c463
rpc: preflight shared registration terms and document standby dissolu…
PastaPastaPasta Jul 9, 2026
3fd25d0
evo: disallow same-block registration and dissolution of a shared mas…
PastaPastaPasta Aug 1, 2026
a08f9b9
rpc: fail clearly when special transaction inputs cannot be signed at…
PastaPastaPasta Aug 1, 2026
a816174
test: filter matching and reorg coverage for shared masternodes
PastaPastaPasta Aug 1, 2026
14309f8
test: cover shared masternode revival and pre-activation rejection
PastaPastaPasta Aug 1, 2026
6b01878
rpc, gui: surface shared masternodes in payee displays and wallet fil…
PastaPastaPasta Aug 1, 2026
f67b521
evo: cap dissolution fees and unilateral penalty overpayment
PastaPastaPasta Aug 17, 2026
fcd1dc7
evo: enforce the ProUpShareTx signature size statelessly
PastaPastaPasta Aug 17, 2026
16af0ff
miner: recheck the shared-collateral covenant for every packaged tran…
PastaPastaPasta Aug 17, 2026
4d91abe
rpc: guard shared_sign against unnoticed dissolution time locks
PastaPastaPasta Aug 17, 2026
0066781
test: backfill DIP test-list coverage for shared masternodes
PastaPastaPasta Aug 17, 2026
0fe0dde
fix: adapt shared masternode code after rebase
PastaPastaPasta Sep 7, 2026
4d7882c
fix: close shared masternode ownership and mempool gaps
PastaPastaPasta Sep 7, 2026
d7a5fd9
refactor: simplify shared masternode payout helpers
PastaPastaPasta Sep 7, 2026
f977eea
style: normalize shared transaction bloom checks
PastaPastaPasta Sep 7, 2026
865e72d
test: cover shared masternodes across participant wallets
PastaPastaPasta Sep 7, 2026
5e58f10
test: reject adversarial shared masternode transactions
PastaPastaPasta Sep 7, 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
75 changes: 75 additions & 0 deletions doc/release-notes-7437.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# 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. The transaction fee is capped at 1000000 duffs
and a unilateral dissolution may not pay bonuses beyond the configured
early penalty, bounding what a stolen share owner key can drain from its
own share.
- **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. It refuses a
dissolution carrying a lock time or non-final sequence unless
`allowTimeLocks` is set.
- `protx shared_combine` combines collected signatures and optionally submits.
- `protx dissolve` creates, signs and submits a unilateral ProDisTx (or, with

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rpc names are inconsistent.

register_shared_prepare - in the middle
shared_sign, shared_combine - at the beginning
update_share, update_shared_registrar_prepare - end
dissolve / dissove_prepare <- not even in the middle.

Consider unifying it to:

sharedprotx register_prepare
sharedprotx sign
sharedprotx combine
sharedprotx dissolve
sharedprotx dissolve_prepare
sharedprotx update_share
sharedprotx update_registrar_prepare

Or something similar

`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.

Updated RPCs
------------

- `masternodelist` and `masternode list` report comma-separated share owner
addresses in `owneraddress` for shared masternodes. The `json` and `recent`
modes can be filtered by any share owner address. Shared registrations and
masternode state omit the singular `ownerAddress` field in `protx` and decoded
transaction output; each participant's owner address is in `shares`. (#7437)

GUI changes
-----------

- The owned-masternode filter includes shared masternodes when the wallet holds
a participant's refund destination, including when rewards go to a different
wallet. (#7437)
1 change: 1 addition & 0 deletions src/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ BITCOIN_CORE_H = \
evo/netinfo.h \
evo/providertx.h \
evo/providertx_service.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 @@ -117,6 +117,7 @@ BITCOIN_TESTS =\
test/evo_mnauth_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 @@ -191,6 +191,11 @@ static bool CheckAssetUnlockTxImpl(const BlockManager& blockman, VerifySig&& ver
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
58 changes: 54 additions & 4 deletions src/evo/core_write.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,9 @@ UniValue CDeterministicMNState::ToJson(MnType nType) const
obj.pushKV("PoSeRevivedHeight", nPoSeRevivedHeight);
obj.pushKV("PoSeBanHeight", nPoSeBanHeight);
obj.pushKV("revocationReason", nRevocationReason);
obj.pushKV("ownerAddress", EncodeDestination(PKHash(keyIDOwner)));
if (!IsShared()) {
obj.pushKV("ownerAddress", EncodeDestination(PKHash(keyIDOwner)));
}
obj.pushKV("votingAddress", EncodeDestination(PKHash(keyIDVoting)));
if (nType == MnType::Evo) {
obj.pushKV("platformNodeID", platformNodeID.ToString());
Expand All @@ -174,7 +176,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 All @@ -197,9 +203,15 @@ UniValue CProRegTx::ToJson() const
ret.pushKV("service", GetDeprecatedServiceField(*this));
}
ret.pushKV("addresses", GetNetInfoWithLegacyFields(*this, nType));
ret.pushKV("ownerAddress", EncodeDestination(PKHash(keyIDOwner)));
if (!IsShared()) {
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 @@ -243,6 +255,41 @@ UniValue CProUpRevTx::ToJson() const
return ret;
}

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;
}

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;
}

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;
}

UniValue CProUpServTx::ToJson() const
{
UniValue ret(UniValue::VOBJ);
Expand Down Expand Up @@ -358,6 +405,9 @@ UniValue CSimplifiedMNListEntry::ToJson(bool extended) const
} else if (ExtractDestination(scriptPayout, dest)) {
obj.pushKV("payoutAddress", EncodeDestination(dest));
}
if (!shares.empty()) {
obj.pushKV("shares", ShareListToJson(shares));
}
if (ExtractDestination(scriptOperatorPayout, dest)) {
obj.pushKV("operatorPayoutAddress", EncodeDestination(dest));
}
Expand Down
27 changes: 24 additions & 3 deletions src/evo/deterministicmns.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,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),
state.scriptPayout, GetOwnerPayouts(state), state.shares,
state.scriptOperatorPayout, state.nVersion, nType);
}

Expand Down Expand Up @@ -432,7 +432,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()) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can shares be changed? If so, CDeterministicMNList::UpdateMN should be updated as well.

// 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 @@ -589,7 +601,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
29 changes: 27 additions & 2 deletions src/evo/dmnstate.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ std::string CDeterministicMNState::ToString() const
if (ExtractDestination(scriptPayout, dest)) {
payoutAddress = EncodeDestination(dest);
}
const auto owner_payouts = GetOwnerPayouts(*this);
const std::string payoutList = PayoutListToString(owner_payouts);
const std::string payoutList = IsShared() ? PayoutListToString(shares, nEarlyPeriodBlocks, nEarlyPenalty)
: PayoutListToString(GetOwnerPayouts(*this));
if (ExtractDestination(scriptOperatorPayout, dest)) {
operatorPayoutAddress = EncodeDestination(dest);
}
Expand All @@ -31,6 +31,22 @@ std::string CDeterministicMNState::ToString() const
EncodeDestination(PKHash(keyIDVoting)), netInfo->ToString(), payoutAddress, payoutList, operatorPayoutAddress);
}

std::vector<CScript> CDeterministicMNState::GetOwnerRewardScripts() const
{
std::vector<CScript> ret;
if (IsShared()) {
ret.reserve(shares.size());
for (const auto& share : shares) {
ret.emplace_back(share.RewardScript());
}
} else {
for (const auto& payout : GetOwnerPayouts(*this)) {
ret.emplace_back(payout.scriptPayout);
}
}
return ret;
}

UniValue CDeterministicMNStateDiff::ToJson(MnType nType) const
{
UniValue obj(UniValue::VOBJ);
Expand Down Expand Up @@ -78,6 +94,15 @@ UniValue CDeterministicMNStateDiff::ToJson(MnType nType) const
if (fields & Field_payouts) {
obj.pushKV("payouts", PayoutListToJson(state.payouts));
}
if (fields & Field_shares) {
obj.pushKV("shares", ShareListToJson(state.shares));
}
if (fields & Field_nEarlyPeriodBlocks) {
obj.pushKV("earlyPeriodBlocks", static_cast<int64_t>(state.nEarlyPeriodBlocks));
}
if (fields & Field_nEarlyPenalty) {
obj.pushKV("earlyPenalty", state.nEarlyPenalty);
}
if (fields & Field_scriptOperatorPayout) {
CTxDestination dest;
if (ExtractDestination(state.scriptOperatorPayout, dest)) {
Expand Down
Loading
Loading