Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
c686ce8
consensus: add shared collateral template script and new special tx t…
PastaPastaPasta Jul 9, 2026
89e7b27
evo: extend extended-address ProRegTx payload with collateral shares
PastaPastaPasta Jul 9, 2026
813fc12
evo: shared masternode consensus rules, state, and special transactions
PastaPastaPasta Jul 9, 2026
8653afa
masternode: split shared masternode rewards by share amounts
PastaPastaPasta Jul 9, 2026
a680ae3
mempool: conflict tracking and eviction for shared masternode transac…
PastaPastaPasta Jul 9, 2026
60fb33a
policy: relay carve-outs for the shared collateral template
PastaPastaPasta Jul 9, 2026
b87e835
filters: match shared masternode fields in bloom and compact filters
PastaPastaPasta Jul 9, 2026
e8dbc90
rpc: shared masternode registration, dissolution and update commands
PastaPastaPasta Jul 9, 2026
9d984c9
test: unit and functional coverage for decentralized masternode shares
PastaPastaPasta Jul 9, 2026
d424344
doc: release notes for decentralized masternode shares
PastaPastaPasta Jul 9, 2026
88a2c0f
rpc: preflight shared registration terms and document standby dissolu…
PastaPastaPasta Jul 9, 2026
b56f01e
evo: disallow same-block registration and dissolution of a shared mas…
PastaPastaPasta Aug 1, 2026
28d360f
rpc: fail clearly when special transaction inputs cannot be signed at…
PastaPastaPasta Aug 1, 2026
2230232
test: filter matching and reorg coverage for shared masternodes
PastaPastaPasta Aug 1, 2026
23a575a
test: cover shared masternode revival and pre-activation rejection
PastaPastaPasta Aug 1, 2026
2ddc406
rpc, gui: surface shared masternodes in payee displays and wallet fil…
PastaPastaPasta Aug 1, 2026
5f3ad89
evo: cap dissolution fees and unilateral penalty overpayment
PastaPastaPasta Aug 17, 2026
77a2c13
evo: enforce the ProUpShareTx signature size statelessly
PastaPastaPasta Aug 17, 2026
cbe1bde
miner: recheck the shared-collateral covenant for every packaged tran…
PastaPastaPasta Aug 17, 2026
131565e
rpc: guard shared_sign against unnoticed dissolution time locks
PastaPastaPasta Aug 17, 2026
31ac6e0
test: backfill DIP test-list coverage for shared masternodes
PastaPastaPasta Aug 17, 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
59 changes: 59 additions & 0 deletions doc/release-notes-7437.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# 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
`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 @@ -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
50 changes: 48 additions & 2 deletions src/evo/core_write.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,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 @@ -199,7 +203,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 @@ -243,6 +251,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 +401,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()) {
// 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
31 changes: 29 additions & 2 deletions src/evo/dmnstate.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ 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()
? strprintf("shares(%s), earlyPeriodBlocks=%d, earlyPenalty=%d",
ShareListToString(shares), nEarlyPeriodBlocks, nEarlyPenalty)
: PayoutListToString(GetOwnerPayouts(*this));
if (ExtractDestination(scriptOperatorPayout, dest)) {
operatorPayoutAddress = EncodeDestination(dest);
}
Expand All @@ -31,6 +33,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 +96,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