diff --git a/doc/release-notes-7437.md b/doc/release-notes-7437.md new file mode 100644 index 000000000000..7d98d2d42fdb --- /dev/null +++ b/doc/release-notes-7437.md @@ -0,0 +1,53 @@ +# 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 4 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 inside a ProRegTx, 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. diff --git a/src/Makefile.am b/src/Makefile.am index 4b446675291e..106b38fb5c9b 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -227,6 +227,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 \ diff --git a/src/Makefile.test.include b/src/Makefile.test.include index e2f1782e5d42..521f5167184f 100644 --- a/src/Makefile.test.include +++ b/src/Makefile.test.include @@ -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 \ diff --git a/src/common/bloom.cpp b/src/common/bloom.cpp index 4b0c4c334f19..d2ba500dbb65 100644 --- a/src/common/bloom.cpp +++ b/src/common/bloom.cpp @@ -135,10 +135,16 @@ bool CBloomFilter::CheckSpecialTransactionMatchesAndUpdate(const CTransaction &t const auto owner_payouts = GetOwnerPayouts(opt_proTx->nVersion, opt_proTx->scriptPayout, opt_proTx->payouts); 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; @@ -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(tx)) { + if(contains(opt_proTx->proTxHash)) + return true; + } + return false; + } + case(TRANSACTION_PROVIDER_UPDATE_SHARE): { + if (const auto opt_proTx = GetTxPayload(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(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(tx)) { diff --git a/src/core_write.cpp b/src/core_write.cpp index a7ab773fdc22..7c6d3aee9f3b 100644 --- a/src/core_write.cpp +++ b/src/core_write.cpp @@ -324,6 +324,18 @@ void TxToUniv(const CTransaction& tx, const uint256& block_hash, UniValue& entry if (const auto opt_assetUnlockTx = GetTxPayload(tx)) { entry.pushKV("assetUnlockTx", opt_assetUnlockTx->ToJson()); } + } else if (tx.nType == TRANSACTION_PROVIDER_DISSOLVE) { + if (const auto opt_proTx = GetTxPayload(tx)) { + entry.pushKV("proDisTx", opt_proTx->ToJson()); + } + } else if (tx.nType == TRANSACTION_PROVIDER_UPDATE_SHARE) { + if (const auto opt_proTx = GetTxPayload(tx)) { + entry.pushKV("proUpShareTx", opt_proTx->ToJson()); + } + } else if (tx.nType == TRANSACTION_PROVIDER_UPDATE_SHARED_REGISTRAR) { + if (const auto opt_proTx = GetTxPayload(tx)) { + entry.pushKV("proUpSharedRegTx", opt_proTx->ToJson()); + } } if (have_undo) { diff --git a/src/evo/assetlocktx.cpp b/src/evo/assetlocktx.cpp index cba0bdc55534..636488aa26c8 100644 --- a/src/evo/assetlocktx.cpp +++ b/src/evo/assetlocktx.cpp @@ -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(tx); if (!opt_assetUnlockTx) { return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-assetunlocktx-payload"); diff --git a/src/evo/core_write.cpp b/src/evo/core_write.cpp index 53056d0b9a69..fa41dd74da55 100644 --- a/src/evo/core_write.cpp +++ b/src/evo/core_write.cpp @@ -41,6 +41,8 @@ const std::map 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"), @@ -77,6 +79,19 @@ const std::map 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"), @@ -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), }}; @@ -260,7 +278,11 @@ UniValue CDeterministicMNState::ToJson(MnType nType) const } CTxDestination dest; - if (nVersion >= ProTxVersion::MultiPayout) { + if (IsShared()) { + obj.pushKV("shares", ShareListToJson(shares)); + obj.pushKV("earlyPeriodBlocks", static_cast(nEarlyPeriodBlocks)); + obj.pushKV("earlyPenalty", nEarlyPenalty); + } else if (nVersion >= ProTxVersion::MultiPayout) { obj.pushKV("payouts", PayoutListToJson(payouts)); } else if (ExtractDestination(scriptPayout, dest)) { obj.pushKV("payoutAddress", EncodeDestination(dest)); @@ -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), @@ -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), @@ -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::MultiPayout) { + if (IsShared()) { + ret.pushKV("shares", ShareListToJson(shares)); + ret.pushKV("earlyPeriodBlocks", static_cast(nEarlyPeriodBlocks)); + ret.pushKV("earlyPenalty", nEarlyPenalty); + } else if (nVersion >= ProTxVersion::MultiPayout) { ret.pushKV("payouts", PayoutListToJson(payouts)); } else if (CTxDestination dest; ExtractDestination(scriptPayout, dest)) { ret.pushKV("payoutAddress", EncodeDestination(dest)); @@ -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(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(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", diff --git a/src/evo/deterministicmns.cpp b/src/evo/deterministicmns.cpp index df22b9198362..911ce32822b3 100644 --- a/src/evo/deterministicmns.cpp +++ b/src/evo/deterministicmns.cpp @@ -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))))); @@ -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))))); diff --git a/src/evo/dmnstate.cpp b/src/evo/dmnstate.cpp index b384966cb9b7..8afb00406706 100644 --- a/src/evo/dmnstate.cpp +++ b/src/evo/dmnstate.cpp @@ -16,8 +16,10 @@ std::string CDeterministicMNState::ToString() const if (ExtractDestination(scriptPayout, dest)) { payoutAddress = EncodeDestination(dest); } - const auto owner_payouts = GetOwnerPayouts(nVersion, scriptPayout, payouts); - 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(nVersion, scriptPayout, payouts)); if (ExtractDestination(scriptOperatorPayout, dest)) { operatorPayoutAddress = EncodeDestination(dest); } @@ -78,6 +80,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(state.nEarlyPeriodBlocks)); + } + if (fields & Field_nEarlyPenalty) { + obj.pushKV("earlyPenalty", state.nEarlyPenalty); + } if (fields & Field_scriptOperatorPayout) { CTxDestination dest; if (ExtractDestination(state.scriptOperatorPayout, dest)) { diff --git a/src/evo/dmnstate.h b/src/evo/dmnstate.h index dfab76d1e25b..9b2570c6d32a 100644 --- a/src/evo/dmnstate.h +++ b/src/evo/dmnstate.h @@ -58,6 +58,9 @@ class CDeterministicMNState std::shared_ptr netInfo{nullptr}; CScript scriptPayout; MasternodePayoutShares payouts; + CollateralShares shares; // non-empty = shared masternode (joinSigs are not kept in state) + uint32_t nEarlyPeriodBlocks{0}; + CAmount nEarlyPenalty{0}; CScript scriptOperatorPayout; uint160 platformNodeID{}; @@ -74,6 +77,9 @@ class CDeterministicMNState netInfo(proTx.netInfo), scriptPayout(proTx.scriptPayout), payouts(proTx.payouts), + shares(proTx.shares), + nEarlyPeriodBlocks(proTx.nEarlyPeriodBlocks), + nEarlyPenalty(proTx.nEarlyPenalty), platformNodeID(proTx.platformNodeID), platformP2PPort(proTx.platformP2PPort), platformHTTPPort(proTx.platformHTTPPort) @@ -102,7 +108,7 @@ class CDeterministicMNState NetInfoSerWrapper(const_cast&>(obj.netInfo), obj.nVersion >= ProTxVersion::ExtAddr)); if (obj.nVersion >= ProTxVersion::MultiPayout) { - READWRITE(obj.payouts); + READWRITE(obj.payouts, obj.shares, obj.nEarlyPeriodBlocks, obj.nEarlyPenalty); } else { READWRITE(obj.scriptPayout); } @@ -145,6 +151,8 @@ class CDeterministicMNState nPoSeBanHeight = -1; nPoSeRevivedHeight = nRevivedHeight; } + /** Whether this is a shared masternode (DIP: decentralized masternode shares) */ + [[nodiscard]] bool IsShared() const { return !shares.empty(); } void UpdateConfirmedHash(const uint256& _proTxHash, const uint256& _confirmedHash) { confirmedHash = _confirmedHash; @@ -184,6 +192,9 @@ class CDeterministicMNStateDiff Field_platformP2PPort = 0x20000, Field_platformHTTPPort = 0x40000, Field_payouts = 0x80000, + Field_shares = 0x100000, + Field_nEarlyPeriodBlocks = 0x200000, + Field_nEarlyPenalty = 0x400000, }; private: @@ -212,6 +223,9 @@ class CDeterministicMNStateDiff DMN_STATE_MEMBER(netInfo), DMN_STATE_MEMBER(scriptPayout), DMN_STATE_MEMBER(payouts), + DMN_STATE_MEMBER(shares), + DMN_STATE_MEMBER(nEarlyPeriodBlocks), + DMN_STATE_MEMBER(nEarlyPenalty), DMN_STATE_MEMBER(scriptOperatorPayout), DMN_STATE_MEMBER(nConsecutivePayments), DMN_STATE_MEMBER(platformNodeID), diff --git a/src/evo/providertx.cpp b/src/evo/providertx.cpp index d098690fd1ba..fbab424f7282 100644 --- a/src/evo/providertx.cpp +++ b/src/evo/providertx.cpp @@ -5,9 +5,11 @@ #include #include +#include #include #include +#include #include #include #include @@ -88,6 +90,82 @@ bool IsPayoutListTriviallyValid(const MasternodePayoutShares& payouts, const CKe return true; } +bool IsShareListTriviallyValid(const CollateralShares& shares, + const std::vector>& join_sigs, + uint32_t early_period_blocks, CAmount early_penalty, CAmount required_collateral, + const CKeyID& keyIDVoting, TxValidationState& state) +{ + if (shares.size() < CProRegTx::MIN_SHARES || shares.size() > CProRegTx::MAX_SHARES) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-shares-count"); + } + if (join_sigs.size() != shares.size()) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-shares-sig-count"); + } + for (const auto& sig : join_sigs) { + if (sig.size() != CPubKey::COMPACT_SIGNATURE_SIZE) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-shares-sig-size"); + } + } + if (early_period_blocks > CProRegTx::MAX_EARLY_PERIOD_BLOCKS) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-shares-early-period"); + } + + CAmount total_amount{0}; + CAmount min_amount{std::numeric_limits::max()}; + std::set seen_owner_keys; + std::set seen_refund_scripts; + for (const auto& share : shares) { + // Bounding each amount by the required collateral first makes the sum overflow-safe + if (share.amount < CCollateralShare::MIN_AMOUNT || share.amount > required_collateral) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-shares-amount"); + } + total_amount += share.amount; + min_amount = std::min(min_amount, share.amount); + + if (share.keyIDOwner.IsNull()) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-shares-key-null"); + } + if (!seen_owner_keys.emplace(share.keyIDOwner).second) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-shares-dup-key"); + } + if (!seen_refund_scripts.emplace(share.scriptRefund).second) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-shares-dup-refund"); + } + + for (const CScript* script : {&share.scriptRefund, &share.scriptReward}) { + if (script == &share.scriptReward && script->empty()) { + // An empty reward script means "use the refund script" + continue; + } + if (sharedcollateral::IsSharedCollateralScript(*script)) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-shares-payee-template"); + } + if (!IsValidPayoutScript(*script)) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-shares-payee"); + } + CTxDestination dest; + if (!ExtractDestination(*script, dest)) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-shares-payee-dest"); + } + if (dest == CTxDestination(PKHash(keyIDVoting))) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-shares-payee-reuse"); + } + for (const auto& other : shares) { + if (dest == CTxDestination(PKHash(other.keyIDOwner))) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-shares-payee-reuse"); + } + } + } + } + if (total_amount != required_collateral) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-shares-amount-sum"); + } + if (early_penalty < 0 || early_penalty >= min_amount) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-shares-penalty"); + } + return true; +} + bool IsPayoutListKeySafe(const MasternodePayoutShares& payouts, const CTxDestination& collateral_dest, const CKeyID& keyIDOwner, const CKeyID& keyIDVoting, bool check_payout_collateral_reuse, TxValidationState& state) @@ -148,14 +226,47 @@ bool CProRegTx::IsTriviallyValid(gsl::not_null pindexPrev, c return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-protx-mode"); } - if (keyIDOwner.IsNull() || !pubKeyOperator.Get().IsValid() || keyIDVoting.IsNull()) { + if (IsShared()) { + if (nVersion < ProTxVersion::MultiPayout) { + return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-protx-version"); + } + if (nType != MnType::Regular) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-shares-evo"); + } + // The collateral must be internal; the funding inputs and outputs are covered by the consent digest + if (!collateralOutpoint.hash.IsNull()) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-shares-external"); + } + // The share owner keys replace the owner key; owner rewards derive from the share table + if (!keyIDOwner.IsNull()) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-shares-owner-key"); + } + if (!payouts.empty()) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-shares-payouts"); + } + } else { + if (!vchJoinSigs.empty() || nEarlyPeriodBlocks != 0 || nEarlyPenalty != 0) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-shares-empty-fields"); + } + if (keyIDOwner.IsNull()) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-key-null"); + } + } + if (!pubKeyOperator.Get().IsValid() || keyIDVoting.IsNull()) { return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-key-null"); } if (pubKeyOperator.IsLegacy() != (nVersion == ProTxVersion::LegacyBLS)) { return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-operator-pubkey"); } - const auto owner_payouts = GetOwnerPayouts(nVersion, scriptPayout, payouts); - if (!IsPayoutListTriviallyValid(owner_payouts, keyIDOwner, keyIDVoting, state)) return false; + if (IsShared()) { + if (!IsShareListTriviallyValid(shares, vchJoinSigs, nEarlyPeriodBlocks, nEarlyPenalty, + GetMnType(nType).collat_amount, keyIDVoting, state)) { + return false; + } + } else { + const auto owner_payouts = GetOwnerPayouts(nVersion, scriptPayout, payouts); + if (!IsPayoutListTriviallyValid(owner_payouts, keyIDOwner, keyIDVoting, state)) return false; + } if (netInfo->CanStorePlatform() != (nVersion >= ProTxVersion::ExtAddr)) { return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-protx-netinfo-version"); } @@ -176,6 +287,37 @@ bool CProRegTx::IsTriviallyValid(gsl::not_null pindexPrev, c return true; } +uint256 CProRegTx::MakeSharedRegConsentHash(const CTransaction& tx) const +{ + // Per the decentralized masternode shares DIP the consent digest binds every participant to + // the exact funding inputs, all outputs (including the collateral output and every change + // output), the full share table, the penalty terms and the registrar configuration. It + // deliberately does not rely on the sighash modes of funding-input signatures. + CHashWriter hw(SER_GETHASH, CLIENT_VERSION); + hw << std::string("DashSharedMNReg"); + hw << nVersion; + hw << tx.nVersion; + hw << tx.nType; + hw << tx.nLockTime; + hw << CalcTxInputsHash(tx); + hw << CalcTxOutputsHash(tx); + hw << nType; + hw << nMode; + hw << NetInfoSerWrapper(const_cast&>(netInfo), + nVersion >= ProTxVersion::ExtAddr); + hw << keyIDVoting; + hw << CBLSLazyPublicKeyVersionWrapper(const_cast(pubKeyOperator), + nVersion == ProTxVersion::LegacyBLS); + hw << nOperatorReward; + hw << static_cast(shares.size()); + for (const auto& share : shares) { + hw << share; + } + hw << nEarlyPeriodBlocks; + hw << nEarlyPenalty; + return hw.GetHash(); +} + std::string CProRegTx::MakeSignString() const { std::string s; @@ -200,7 +342,9 @@ std::string CProRegTx::MakeSignString() const std::string CProRegTx::ToString() const { - const std::string payee = PayoutListToString(GetOwnerPayouts(nVersion, scriptPayout, payouts)); + const std::string payee = IsShared() ? strprintf("shares(%s), earlyPeriodBlocks=%d, earlyPenalty=%d", + ShareListToString(shares), nEarlyPeriodBlocks, nEarlyPenalty) + : PayoutListToString(GetOwnerPayouts(nVersion, scriptPayout, payouts)); return strprintf("CProRegTx(nVersion=%d, nType=%d, collateralOutpoint=%s, netInfo=%s, nOperatorReward=%f, " "ownerAddress=%s, pubKeyOperator=%s, votingAddress=%s, scriptPayout=%s, platformNodeID=%s%s)\n", @@ -305,3 +449,121 @@ std::string CProUpRevTx::ToString() const return strprintf("CProUpRevTx(nVersion=%d, proTxHash=%s, nReason=%d)", nVersion, proTxHash.ToString(), nReason); } + +uint256 CProDisTx::MakeSignHash(const CTransaction& tx, uint8_t sig_count) const +{ + // The digest commits to the transaction's actual input(s) and outputs directly; the payload + // deliberately carries no inputsHash/outputsHash copies. It also commits to the signature + // count, which selects the dissolution mode, so a third party cannot reinterpret a penalty-free + // unanimous dissolution as a unilateral one (or vice versa) by dropping/adding signatures. + // Together with the empty-scriptSig rule and the low-S requirement this pins every free byte of + // a ProDisTx, so its txid is non-malleable by third parties. + CHashWriter hw(SER_GETHASH, CLIENT_VERSION); + hw << std::string("DashSharedMNDissolve"); + hw << nVersion; + hw << tx.nVersion; + hw << tx.nType; + hw << tx.nLockTime; + for (const auto& in : tx.vin) { + hw << in.prevout; + } + for (const auto& in : tx.vin) { + hw << in.nSequence; + } + for (const auto& out : tx.vout) { + hw << out; + } + hw << proTxHash; + hw << actorIndex; + hw << sig_count; + return hw.GetHash(); +} + +bool CProDisTx::IsTriviallyValid(gsl::not_null pindexPrev, const ChainstateManager& chainman, + TxValidationState& state) const +{ + if (!DeploymentActiveAfter(pindexPrev, chainman, Consensus::DEPLOYMENT_V24)) { + return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-prodis-too-early"); + } + if (nVersion == 0 || nVersion > CURRENT_VERSION) { + return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-prodis-version"); + } + if (vchSigs.empty() || vchSigs.size() > CProRegTx::MAX_SHARES) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-prodis-sig-count"); + } + for (const auto& sig : vchSigs) { + if (sig.size() != CPubKey::COMPACT_SIGNATURE_SIZE) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-prodis-sig-size"); + } + } + return true; +} + +std::string CProDisTx::ToString() const +{ + return strprintf("CProDisTx(nVersion=%d, proTxHash=%s, actorIndex=%d, sigCount=%d)", + nVersion, proTxHash.ToString(), actorIndex, vchSigs.size()); +} + +bool CProUpShareTx::IsTriviallyValid(gsl::not_null pindexPrev, const ChainstateManager& chainman, + TxValidationState& state) const +{ + if (!DeploymentActiveAfter(pindexPrev, chainman, Consensus::DEPLOYMENT_V24)) { + return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-proupshare-too-early"); + } + if (nVersion == 0 || nVersion > CURRENT_VERSION) { + return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-proupshare-version"); + } + if (!scriptReward.empty()) { + if (sharedcollateral::IsSharedCollateralScript(scriptReward)) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-proupshare-payee-template"); + } + if (!IsValidPayoutScript(scriptReward)) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-proupshare-payee"); + } + } + return true; +} + +std::string CProUpShareTx::ToString() const +{ + CTxDestination dest; + const std::string reward = scriptReward.empty() + ? "refund" + : (ExtractDestination(scriptReward, dest) ? EncodeDestination(dest) : HexStr(scriptReward)); + return strprintf("CProUpShareTx(nVersion=%d, proTxHash=%s, shareIndex=%d, rewardAddress=%s)", + nVersion, proTxHash.ToString(), shareIndex, reward); +} + +bool CProUpSharedRegTx::IsTriviallyValid(gsl::not_null pindexPrev, + const ChainstateManager& chainman, TxValidationState& state) const +{ + if (!DeploymentActiveAfter(pindexPrev, chainman, Consensus::DEPLOYMENT_V24)) { + return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-proupsharedreg-too-early"); + } + if (nVersion == 0 || nVersion > CURRENT_VERSION) { + return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-proupsharedreg-version"); + } + if (!pubKeyOperator.Get().IsValid() || keyIDVoting.IsNull()) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-key-null"); + } + if (pubKeyOperator.IsLegacy()) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-operator-pubkey"); + } + if (vchSigs.empty() || vchSigs.size() > CProRegTx::MAX_SHARES) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-proupsharedreg-sig-count"); + } + for (const auto& sig : vchSigs) { + if (sig.size() != CPubKey::COMPACT_SIGNATURE_SIZE) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-proupsharedreg-sig-size"); + } + } + return true; +} + +std::string CProUpSharedRegTx::ToString() const +{ + return strprintf("CProUpSharedRegTx(nVersion=%d, proTxHash=%s, pubKeyOperator=%s, votingAddress=%s, sigCount=%d)", + nVersion, proTxHash.ToString(), pubKeyOperator.ToString(), EncodeDestination(PKHash(keyIDVoting)), + vchSigs.size()); +} diff --git a/src/evo/providertx.h b/src/evo/providertx.h index a8c24d34b80b..8de436a9e032 100644 --- a/src/evo/providertx.h +++ b/src/evo/providertx.h @@ -94,6 +94,62 @@ class CMasternodePayoutShare using MasternodePayoutShares = std::vector; +/** Serializes a 65-byte compact recoverable ECDSA signature without a length prefix */ +struct CompactSignatureFormatter { + template + void Ser(Stream& s, const V& v) + { + if (v.size() != CPubKey::COMPACT_SIGNATURE_SIZE) { + throw std::ios_base::failure("compact signature size mismatch"); + } + s.write(AsBytes(Span{v})); + } + template + void Unser(Stream& s, V& v) + { + v.resize(CPubKey::COMPACT_SIGNATURE_SIZE); + s.read(AsWritableBytes(Span{v})); + } +}; + +/** One participant's contribution to a shared masternode collateral */ +class CCollateralShare +{ +public: + static constexpr CAmount MIN_AMOUNT{100 * COIN}; + + CAmount amount{0}; + CScript scriptRefund; + CScript scriptReward; //!< empty means "use scriptRefund" + CKeyID keyIDOwner; + + CCollateralShare() = default; + CCollateralShare(CAmount amount, CScript script_refund, CScript script_reward, CKeyID key_id_owner) : + amount(amount), + scriptRefund(std::move(script_refund)), + scriptReward(std::move(script_reward)), + keyIDOwner(key_id_owner) + { + } + + SERIALIZE_METHODS(CCollateralShare, obj) + { + READWRITE(obj.amount, obj.scriptRefund, obj.scriptReward, obj.keyIDOwner); + } + + /** The script this share's portion of owner rewards is paid to */ + const CScript& RewardScript() const { return scriptReward.empty() ? scriptRefund : scriptReward; } + + friend bool operator==(const CCollateralShare& a, const CCollateralShare& b) + { + return a.amount == b.amount && a.scriptRefund == b.scriptRefund && a.scriptReward == b.scriptReward && + a.keyIDOwner == b.keyIDOwner; + } + friend bool operator!=(const CCollateralShare& a, const CCollateralShare& b) { return !(a == b); } +}; + +using CollateralShares = std::vector; + [[nodiscard]] MasternodePayoutShares LegacyPayoutAsList(const CScript& script_payout); [[nodiscard]] MasternodePayoutShares GetOwnerPayouts(uint16_t nVersion, const CScript& script_payout, const MasternodePayoutShares& payouts); @@ -105,11 +161,26 @@ using MasternodePayoutShares = std::vector; [[nodiscard]] std::string PayoutListToString(const MasternodePayoutShares& payouts); [[nodiscard]] UniValue PayoutListToJson(const MasternodePayoutShares& payouts); +[[nodiscard]] bool IsShareListTriviallyValid(const CollateralShares& shares, + const std::vector>& join_sigs, + uint32_t early_period_blocks, CAmount early_penalty, + CAmount required_collateral, const CKeyID& keyIDVoting, + TxValidationState& state); +[[nodiscard]] std::string ShareListToString(const CollateralShares& shares); +[[nodiscard]] UniValue ShareListToJson(const CollateralShares& shares); +/** Split an amount across shares proportionally to their collateral amounts: sequential floor, + * remainder to the last entry. The result always sums to total exactly. */ +[[nodiscard]] std::vector SplitAmountByShares(CAmount total, const CollateralShares& shares); + class CProRegTx { public: static constexpr auto SPECIALTX_TYPE = TRANSACTION_PROVIDER_REGISTER; + static constexpr uint8_t MIN_SHARES{2}; + static constexpr uint8_t MAX_SHARES{8}; + static constexpr uint32_t MAX_EARLY_PERIOD_BLOCKS{420480}; // approx. two years at 2.5-minute blocks + uint16_t nVersion{ProTxVersion::LegacyBLS}; // message version MnType nType{MnType::Regular}; uint16_t nMode{0}; // only 0 supported for now @@ -124,6 +195,10 @@ class CProRegTx uint16_t nOperatorReward{0}; CScript scriptPayout; MasternodePayoutShares payouts; + CollateralShares shares; // non-empty = shared masternode registration + std::vector> vchJoinSigs; // one consent signature per share, in share order + uint32_t nEarlyPeriodBlocks{0}; + CAmount nEarlyPenalty{0}; uint256 inputsHash; // replay protection std::vector vchSig; @@ -157,6 +232,18 @@ class CProRegTx for (auto& payout : obj.payouts) { READWRITE(payout); } + uint8_t shares_count{0}; + SER_WRITE(obj, shares_count = static_cast(obj.shares.size())); + READWRITE(shares_count); + SER_READ(obj, obj.shares.resize(shares_count)); + for (auto& share : obj.shares) { + READWRITE(share); + } + SER_READ(obj, obj.vchJoinSigs.resize(shares_count)); + for (auto& sig : obj.vchJoinSigs) { + READWRITE(Using(sig)); + } + READWRITE(obj.nEarlyPeriodBlocks, obj.nEarlyPenalty); } else { READWRITE(obj.scriptPayout); } @@ -177,6 +264,14 @@ class CProRegTx } } + /** Whether this is a shared masternode registration (DIP: decentralized masternode shares) */ + [[nodiscard]] bool IsShared() const { return !shares.empty(); } + + /** The digest each share owner signs (joinSigs) to consent to a shared registration. It binds + * every participant to the exact funding inputs, all outputs, the full share table, the + * penalty terms and the registrar configuration of the containing transaction. */ + [[nodiscard]] uint256 MakeSharedRegConsentHash(const CTransaction& tx) const; + // When signing with the collateral key, we don't sign the hash but a generated message instead // This is needed for HW wallet support which can only sign text messages as of now std::string MakeSignString() const; @@ -364,6 +459,117 @@ class CProUpRevTx TxValidationState& state) const; }; +class CProDisTx +{ +public: + static constexpr auto SPECIALTX_TYPE = TRANSACTION_PROVIDER_DISSOLVE; + static constexpr uint16_t CURRENT_VERSION = 1; + + uint16_t nVersion{CURRENT_VERSION}; + uint256 proTxHash; + uint16_t actorIndex{0}; + // Exactly one signature (unilateral, by shares[actorIndex]) or one per share in share order + // (unanimous); the signature count defines the mode + std::vector> vchSigs; + + SERIALIZE_METHODS(CProDisTx, obj) + { + READWRITE(obj.nVersion, obj.proTxHash, obj.actorIndex); + uint8_t sig_count{0}; + SER_WRITE(obj, sig_count = static_cast(obj.vchSigs.size())); + READWRITE(sig_count); + SER_READ(obj, obj.vchSigs.resize(sig_count)); + for (auto& sig : obj.vchSigs) { + READWRITE(Using(sig)); + } + } + + /** The digest every dissolution signature commits to. It covers the transaction's actual + * input(s) and outputs directly, plus the signature count, which selects the mode + * (1 = unilateral, sharesCount = unanimous). Committing the count is what stops a third party + * from reinterpreting a penalty-free unanimous dissolution as a unilateral one (or vice + * versa) by dropping/adding signatures, which would change the txid. Callers pass the count + * the transaction will carry; verification passes the actual vchSigs.size(). */ + [[nodiscard]] uint256 MakeSignHash(const CTransaction& tx, uint8_t sig_count) const; + + std::string ToString() const; + + [[nodiscard]] static RPCResult GetJsonHelp(const std::string& key, bool optional); + [[nodiscard]] UniValue ToJson() const; + + bool IsTriviallyValid(gsl::not_null pindexPrev, const ChainstateManager& chainman, + TxValidationState& state) const; +}; + +class CProUpShareTx +{ +public: + static constexpr auto SPECIALTX_TYPE = TRANSACTION_PROVIDER_UPDATE_SHARE; + static constexpr uint16_t CURRENT_VERSION = 1; + + uint16_t nVersion{CURRENT_VERSION}; + uint256 proTxHash; + uint16_t shareIndex{0}; + CScript scriptReward; //!< empty means "use the refund script" + uint256 inputsHash; // replay protection + std::vector vchSig; + + SERIALIZE_METHODS(CProUpShareTx, obj) + { + READWRITE(obj.nVersion, obj.proTxHash, obj.shareIndex, obj.scriptReward, obj.inputsHash); + if (!(s.GetType() & SER_GETHASH)) { + READWRITE(obj.vchSig); + } + } + + std::string ToString() const; + + [[nodiscard]] static RPCResult GetJsonHelp(const std::string& key, bool optional); + [[nodiscard]] UniValue ToJson() const; + + bool IsTriviallyValid(gsl::not_null pindexPrev, const ChainstateManager& chainman, + TxValidationState& state) const; +}; + +class CProUpSharedRegTx +{ +public: + static constexpr auto SPECIALTX_TYPE = TRANSACTION_PROVIDER_UPDATE_SHARED_REGISTRAR; + static constexpr uint16_t CURRENT_VERSION = 1; + + uint16_t nVersion{CURRENT_VERSION}; + uint256 proTxHash; + CBLSLazyPublicKey pubKeyOperator; + CKeyID keyIDVoting; + uint256 inputsHash; // replay protection + // One signature per share, in share order; a shared registrar update requires unanimity + std::vector> vchSigs; + + SERIALIZE_METHODS(CProUpSharedRegTx, obj) + { + READWRITE(obj.nVersion, obj.proTxHash, + CBLSLazyPublicKeyVersionWrapper(const_cast(obj.pubKeyOperator), /*legacy=*/false), + obj.keyIDVoting, obj.inputsHash); + if (!(s.GetType() & SER_GETHASH)) { + uint8_t sig_count{0}; + SER_WRITE(obj, sig_count = static_cast(obj.vchSigs.size())); + READWRITE(sig_count); + SER_READ(obj, obj.vchSigs.resize(sig_count)); + for (auto& sig : obj.vchSigs) { + READWRITE(Using(sig)); + } + } + } + + std::string ToString() const; + + [[nodiscard]] static RPCResult GetJsonHelp(const std::string& key, bool optional); + [[nodiscard]] UniValue ToJson() const; + + bool IsTriviallyValid(gsl::not_null pindexPrev, const ChainstateManager& chainman, + TxValidationState& state) const; +}; + template static bool CheckInputsHash(const CTransaction& tx, const ProTx& proTx, TxValidationState& state) { diff --git a/src/evo/providertx_util.cpp b/src/evo/providertx_util.cpp index 0d963b4fb6d6..583930279db1 100644 --- a/src/evo/providertx_util.cpp +++ b/src/evo/providertx_util.cpp @@ -4,6 +4,7 @@ #include +#include #include #include