diff --git a/doc/release-notes-7594.md b/doc/release-notes-7594.md new file mode 100644 index 000000000000..af1fd81b9e1c --- /dev/null +++ b/doc/release-notes-7594.md @@ -0,0 +1,8 @@ +Wallet +------ + +* Mnemonic-backed wallets can now derive and recover masternode operator BLS + keys using the DashSync-compatible `m/9'/coin'/3'/3'/index` path. A successful + registration can record the public key's derivation index without storing the + operator secret separately; the recovery phrase remains the backup. Existing + operator keys and wallets without a BIP39 recovery phrase are unchanged. diff --git a/doc/release-notes-7600.md b/doc/release-notes-7600.md new file mode 100644 index 000000000000..be0ec57ada90 --- /dev/null +++ b/doc/release-notes-7600.md @@ -0,0 +1,8 @@ +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. Transactions whose wallet + inputs cannot be signed completely now fail with a wallet error instead of + returning or attempting to broadcast a partially signed transaction. (#7600) diff --git a/src/Makefile.am b/src/Makefile.am index fb1ffde2529a..132e6c7e34b5 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 \ @@ -280,7 +281,9 @@ BITCOIN_CORE_H = \ interfaces/handler.h \ interfaces/init.h \ interfaces/ipc.h \ + interfaces/masternode_operator.h \ interfaces/node.h \ + interfaces/providertx.h \ interfaces/wallet.h \ kernel/blockmanager_opts.h \ kernel/chain.h \ @@ -476,6 +479,7 @@ BITCOIN_CORE_H = \ wallet/hdchain.h \ wallet/ismine.h \ wallet/load.h \ + wallet/masternode_operator.h \ wallet/receive.h \ wallet/rpc/util.h \ wallet/rpc/wallet.h \ @@ -539,6 +543,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/Makefile.test.include b/src/Makefile.test.include index 427909b8e12f..f3ef0cee4e4d 100644 --- a/src/Makefile.test.include +++ b/src/Makefile.test.include @@ -224,6 +224,7 @@ if ENABLE_WALLET BITCOIN_TESTS += \ wallet/test/bip39_tests.cpp \ wallet/test/coinjoin_tests.cpp \ + wallet/test/masternode_operator_tests.cpp \ wallet/test/psbt_wallet_tests.cpp \ wallet/test/spend_tests.cpp \ wallet/test/wallet_tests.cpp \ diff --git a/src/bls/bls.cpp b/src/bls/bls.cpp index 49981ee9190f..8d5b1b83703c 100644 --- a/src/bls/bls.cpp +++ b/src/bls/bls.cpp @@ -8,6 +8,7 @@ #ifndef BUILD_BITCOIN_INTERNAL #include +#include #endif #include @@ -73,6 +74,7 @@ void CBLSSecretKey::MakeNewKey() } catch (...) { } } + memory_cleanse(buf, sizeof(buf)); fValid = true; cachedHash.SetNull(); } 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..aea8ddd921bf --- /dev/null +++ b/src/evo/providertx_service.cpp @@ -0,0 +1,818 @@ +// 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