diff --git a/doc/release-notes-7600.md b/doc/release-notes-7600.md new file mode 100644 index 000000000000..323608c15252 --- /dev/null +++ b/doc/release-notes-7600.md @@ -0,0 +1,16 @@ +RPC changes +----------- + +- Normal and Evo `protx` registration and maintenance commands now share a + typed provider-transaction implementation with other wallet frontends. RPC + names and successful result formats are unchanged. Fixed: when a wallet + cannot completely sign the inputs it selected (e.g. `protx register_submit` + run in a different wallet than the one that prepared the registration), + the command now fails with a clear wallet error naming the problem instead + of reporting success with a partially signed transaction or attempting a + broadcast that failed mempool acceptance with a bare `-26` error. The + external-signing workflow (`protx register_prepare` followed by + `protx register_submit`) is unchanged. + `protx update_service` on a masternode whose state does not yield a usable + default fee source now returns an explicit "specify feeSourceAddress" + parameter error instead of an internal error. (#7600) diff --git a/src/Makefile.am b/src/Makefile.am index fb1ffde2529a..0cf8ce802fdb 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -232,6 +232,7 @@ BITCOIN_CORE_H = \ evo/mnhftx.h \ evo/netinfo.h \ evo/providertx.h \ + evo/providertx_service.h \ evo/simplifiedmns.h \ evo/smldiff.h \ evo/specialtx.h \ @@ -281,6 +282,7 @@ BITCOIN_CORE_H = \ interfaces/init.h \ interfaces/ipc.h \ interfaces/node.h \ + interfaces/providertx.h \ interfaces/wallet.h \ kernel/blockmanager_opts.h \ kernel/chain.h \ @@ -539,6 +541,7 @@ libbitcoin_node_a_SOURCES = \ evo/mnauth.cpp \ evo/mnhftx.cpp \ evo/providertx.cpp \ + evo/providertx_service.cpp \ evo/simplifiedmns.cpp \ evo/smldiff.cpp \ evo/specialtx.cpp \ diff --git a/src/evo/providertx.cpp b/src/evo/providertx.cpp index 9ccf89abfc61..ef133a9a5cb5 100644 --- a/src/evo/providertx.cpp +++ b/src/evo/providertx.cpp @@ -79,30 +79,105 @@ bool IsPayoutListKeySafe(const MasternodePayoutShares& payouts, const CTxDestina return true; } -template -bool IsNetInfoTriviallyValid(const ProTx& proTx, TxValidationState& state) +static bool IsNetInfoTriviallyValid(const std::shared_ptr& net_info, MnType type, TxValidationState& state) { - if (!proTx.netInfo->HasEntries(NetInfoPurpose::CORE_P2P)) { + if (!net_info->HasEntries(NetInfoPurpose::CORE_P2P)) { // Mandatory for all nodes return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-netinfo-empty"); } - if (proTx.nType == MnType::Regular) { + if (type == MnType::Regular) { // Regular nodes shouldn't populate Platform-specific fields - if (proTx.netInfo->HasEntries(NetInfoPurpose::PLATFORM_HTTPS) || - proTx.netInfo->HasEntries(NetInfoPurpose::PLATFORM_P2P)) { + if (net_info->HasEntries(NetInfoPurpose::PLATFORM_HTTPS) || net_info->HasEntries(NetInfoPurpose::PLATFORM_P2P)) { return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-netinfo-bad"); } } - if (proTx.netInfo->CanStorePlatform() && proTx.nType == MnType::Evo) { + if (net_info->CanStorePlatform() && type == MnType::Evo) { // Platform fields are mandatory for EvoNodes - if (!proTx.netInfo->HasEntries(NetInfoPurpose::PLATFORM_HTTPS) || - !proTx.netInfo->HasEntries(NetInfoPurpose::PLATFORM_P2P)) { + if (!net_info->HasEntries(NetInfoPurpose::PLATFORM_HTTPS) || !net_info->HasEntries(NetInfoPurpose::PLATFORM_P2P)) { return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-netinfo-empty"); } } return true; } +static bool CheckNetInfo(const NetInfoInterface& net_info, TxValidationState& state) +{ + switch (net_info.Validate()) { + case NetInfoStatus::BadAddress: + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-netinfo-addr"); + case NetInfoStatus::BadPort: + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-netinfo-port"); + case NetInfoStatus::BadType: + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-netinfo-addr-type"); + case NetInfoStatus::NotRoutable: + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-netinfo-addr-unroutable"); + case NetInfoStatus::Malformed: + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-netinfo-bad"); + case NetInfoStatus::Success: + return true; + case NetInfoStatus::BadInput: + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-netinfo-entry"); + case NetInfoStatus::Duplicate: + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-dup-netinfo-entry"); + case NetInfoStatus::MaxLimit: + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-netinfo-maxlimit"); + } + assert(false); +} + +bool CheckProviderNetworkFields(const std::shared_ptr& net_info, MnType type, uint16_t version, + const uint160* platform_node_id, uint16_t platform_p2p_port, + uint16_t platform_http_port, bool allow_empty, TxValidationState& state) +{ + if (!net_info || net_info->CanStorePlatform() != (version >= ProTxVersion::ExtAddr)) { + return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-protx-netinfo-version"); + } + if (net_info->IsEmpty()) { + if (!allow_empty) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-netinfo-empty"); + } + } else { + if (!IsNetInfoTriviallyValid(net_info, type, state) || !CheckNetInfo(*net_info, state)) { + return false; + } + } + + if (type != MnType::Evo) return true; + if (platform_node_id && platform_node_id->IsNull()) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-platform-nodeid"); + } + if (version >= ProTxVersion::ExtAddr) { + if (platform_p2p_port != 0) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-platform-p2p-port"); + } + if (platform_http_port != 0) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-platform-http-port"); + } + return true; + } + + if (::IsNodeOnMainnet()) { + if (platform_p2p_port != ::MainParams().GetDefaultPlatformP2PPort()) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-platform-p2p-port"); + } + if (platform_http_port != ::MainParams().GetDefaultPlatformHTTPPort()) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-platform-http-port"); + } + } + if (platform_p2p_port == ::MainParams().GetDefaultPort()) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-platform-p2p-port"); + } + if (platform_http_port == ::MainParams().GetDefaultPort()) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-platform-http-port"); + } + + const uint16_t core_port{net_info->GetPrimary().GetPort()}; + if (platform_p2p_port == platform_http_port || platform_p2p_port == core_port || platform_http_port == core_port) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-platform-dup-ports"); + } + return true; +} + bool CProRegTx::IsTriviallyValid(TxValidationState& state) const { if (nVersion == 0 || nVersion > ProTxVersion::ExtAddr) { @@ -129,7 +204,7 @@ bool CProRegTx::IsTriviallyValid(TxValidationState& state) const if (netInfo->CanStorePlatform() != (nVersion >= ProTxVersion::ExtAddr)) { return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-protx-netinfo-version"); } - if (!netInfo->IsEmpty() && !IsNetInfoTriviallyValid(*this, state)) { + if (!netInfo->IsEmpty() && !IsNetInfoTriviallyValid(netInfo, nType, state)) { // pass the state returned by the function above return false; } @@ -199,7 +274,7 @@ bool CProUpServTx::IsTriviallyValid(TxValidationState& state) const if (netInfo->IsEmpty()) { return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-netinfo-empty"); } - if (!IsNetInfoTriviallyValid(*this, state)) { + if (!IsNetInfoTriviallyValid(netInfo, nType, state)) { // pass the state returned by the function above return false; } diff --git a/src/evo/providertx.h b/src/evo/providertx.h index d27713e74ca7..f38c3bacc059 100644 --- a/src/evo/providertx.h +++ b/src/evo/providertx.h @@ -58,6 +58,12 @@ template [[nodiscard]] std::string PayoutListToString(const MasternodePayoutShares& payouts); [[nodiscard]] UniValue PayoutListToJson(const MasternodePayoutShares& payouts); +/** Validate all provider network fields using the same rules as special transaction validation. + * Pass nullptr for platform_node_id when validating endpoint input separately from the rest of a payload. */ +[[nodiscard]] bool CheckProviderNetworkFields(const std::shared_ptr& net_info, MnType type, + uint16_t version, const uint160* platform_node_id, uint16_t platform_p2p_port, + uint16_t platform_http_port, bool allow_empty, TxValidationState& state); + class CProRegTx { public: diff --git a/src/evo/providertx_service.cpp b/src/evo/providertx_service.cpp new file mode 100644 index 000000000000..accdb7d25be3 --- /dev/null +++ b/src/evo/providertx_service.cpp @@ -0,0 +1,824 @@ +// Copyright (c) 2026 The Dash Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include