Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
2860d23
consensus: add shared collateral template script and new special tx t…
PastaPastaPasta Jul 9, 2026
59d7512
evo: extend extended-address ProRegTx payload with collateral shares
PastaPastaPasta Jul 9, 2026
d56c85b
evo: shared masternode consensus rules, state, and special transactions
PastaPastaPasta Jul 9, 2026
4c003c5
masternode: split shared masternode rewards by share amounts
PastaPastaPasta Jul 9, 2026
93539fc
mempool: conflict tracking and eviction for shared masternode transac…
PastaPastaPasta Jul 9, 2026
2ef43b4
policy: relay carve-outs for the shared collateral template
PastaPastaPasta Jul 9, 2026
0aef863
filters: match shared masternode fields in bloom and compact filters
PastaPastaPasta Jul 9, 2026
f724e47
rpc: shared masternode registration, dissolution and update commands
PastaPastaPasta Jul 9, 2026
04b23c2
test: unit and functional coverage for decentralized masternode shares
PastaPastaPasta Jul 9, 2026
fb121cb
doc: release notes for decentralized masternode shares
PastaPastaPasta Jul 9, 2026
3c5bcad
rpc: preflight shared registration terms and document standby dissolu…
PastaPastaPasta Jul 9, 2026
c048336
evo: disallow same-block registration and dissolution of a shared mas…
PastaPastaPasta Aug 1, 2026
be39090
rpc: fail clearly when special transaction inputs cannot be signed at…
PastaPastaPasta Aug 1, 2026
4b079ef
test: filter matching and reorg coverage for shared masternodes
PastaPastaPasta Aug 1, 2026
3da9193
test: cover shared masternode revival and pre-activation rejection
PastaPastaPasta Aug 1, 2026
f4878a3
rpc, gui: surface shared masternodes in payee displays and wallet fil…
PastaPastaPasta Aug 1, 2026
55d67cd
evo: cap dissolution fees and unilateral penalty overpayment
PastaPastaPasta Aug 17, 2026
562287f
evo: enforce the ProUpShareTx signature size statelessly
PastaPastaPasta Aug 17, 2026
e2d6d72
miner: recheck the shared-collateral covenant for every packaged tran…
PastaPastaPasta Aug 17, 2026
896e192
rpc: guard shared_sign against unnoticed dissolution time locks
PastaPastaPasta Aug 17, 2026
5a27443
test: backfill DIP test-list coverage for shared masternodes
PastaPastaPasta Aug 17, 2026
7b3754a
fix: close shared masternode ownership and mempool gaps
PastaPastaPasta Sep 7, 2026
6ad9302
refactor: simplify shared masternode payout helpers
PastaPastaPasta Sep 7, 2026
7318a26
test: cover shared masternodes across participant wallets
PastaPastaPasta Sep 7, 2026
a23c27b
test: reject adversarial shared masternode transactions
PastaPastaPasta Sep 7, 2026
bf7cc7a
fix(qt): classify shared masternode updates in transaction history
PastaPastaPasta Sep 10, 2026
a7a2c50
fix: require explicit shared masternode reward scripts
PastaPastaPasta Sep 10, 2026
0cddf15
test: repair pre-activation gate check after requiring explicit rewar…
PastaPastaPasta Sep 11, 2026
df1fc9e
evo: reject a nonzero early penalty without an early period
PastaPastaPasta Sep 11, 2026
1b96c46
rpc: reject unilateral dissolutions in protx shared_combine
PastaPastaPasta Sep 11, 2026
5f75ed2
rpc: warn about time locks on shared registrations in protx shared_sign
PastaPastaPasta Sep 11, 2026
5d0d9ce
evo: fail loudly when a share or signature count exceeds the wire field
PastaPastaPasta Sep 11, 2026
38faee2
evo: use share reward scripts as the default fee source for shared ma…
PastaPastaPasta Sep 11, 2026
fbccdc0
rpc: group shared masternode commands in help
PastaPastaPasta Sep 11, 2026
23a51f0
rpc: prefix shared masternode commands consistently
PastaPastaPasta Sep 11, 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
78 changes: 78 additions & 0 deletions doc/release-notes-7437.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# 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 early-period penalty must be below the smallest
share and must be zero when no early period is configured.
- 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 shared_register_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
registration or dissolution carrying an unsatisfied lock time or a relative
input lock unless `allowTimeLocks` is set.
- `protx shared_combine` combines collected signatures and optionally submits.
A dissolution combined here requires a signature from every share; unilateral
dissolutions come fully signed from `protx shared_dissolve`.
- `protx shared_dissolve` creates, signs and submits a unilateral ProDisTx (or, with
`submit=false`, returns hex suitable for offline standby storage).
- `protx shared_dissolve_prepare` builds an unsigned unanimous ProDisTx.
- `protx shared_update_share` updates one share's reward address. To restore rewards to the immutable refund address, pass that address explicitly; empty reward scripts are not valid in update transactions.
- `protx shared_update_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;
}
Comment thread
PastaPastaPasta marked this conversation as resolved.
}
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; 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 @@ -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),
state.scriptPayout, GetOwnerPayouts(state), state.shares,
state.scriptOperatorPayout, state.nVersion, nType);
}

Expand Down 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()) {
Comment thread
PastaPastaPasta marked this conversation as resolved.
// 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
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