From 8c28f30ad649d8d889b2ef846103282fee3e86ff Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Thu, 22 May 2025 18:03:32 +0000 Subject: [PATCH 1/9] rpc: spin-off `ipAndPort` and `platform{HTTP,P2P}Port` setters to util --- src/Makefile.am | 2 ++ src/rpc/evo.cpp | 40 +++------------------- src/rpc/evo_util.cpp | 56 +++++++++++++++++++++++++++++++ src/rpc/evo_util.h | 16 +++++++++ test/util/data/non-backported.txt | 1 + 5 files changed, 80 insertions(+), 35 deletions(-) create mode 100644 src/rpc/evo_util.cpp create mode 100644 src/rpc/evo_util.h diff --git a/src/Makefile.am b/src/Makefile.am index daa4a2568587..8603d8ed9cf6 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -300,6 +300,7 @@ BITCOIN_CORE_H = \ randomenv.h \ rpc/blockchain.h \ rpc/client.h \ + rpc/evo_util.h \ rpc/index_util.h \ rpc/mempool.h \ rpc/mining.h \ @@ -800,6 +801,7 @@ libbitcoin_common_a_SOURCES = \ policy/policy.cpp \ protocol.cpp \ psbt.cpp \ + rpc/evo_util.cpp \ rpc/rawtransaction_util.cpp \ rpc/util.cpp \ saltedhasher.cpp \ diff --git a/src/rpc/evo.cpp b/src/rpc/evo.cpp index d1f7e7afdf55..9340afc86190 100644 --- a/src/rpc/evo.cpp +++ b/src/rpc/evo.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -230,11 +231,6 @@ static CBLSPublicKey ParseBLSPubKey(const std::string& hexKey, const std::string return pubKey; } -static bool ValidatePlatformPort(const int32_t port) -{ - return port >= 1 && port <= std::numeric_limits::max(); -} - template static void FundSpecialTx(CWallet& wallet, CMutableTransaction& tx, const SpecialTxPayload& payload, const CTxDestination& fundDest) EXCLUSIVE_LOCKS_REQUIRED(!wallet.cs_wallet) @@ -713,11 +709,7 @@ static UniValue protx_register_common_wrapper(const JSONRPCRequest& request, paramIdx += 2; } - if (!request.params[paramIdx].get_str().empty()) { - if (auto entryRet = ptx.netInfo->AddEntry(request.params[paramIdx].get_str()); entryRet != NetInfoStatus::Success) { - throw std::runtime_error(strprintf("%s (%s)", NISToString(entryRet), request.params[paramIdx].get_str())); - } - } + ProcessNetInfoCore(ptx, request.params[paramIdx], /*optional=*/true); ptx.keyIDOwner = ParsePubKeyIDFromAddress(request.params[paramIdx + 1].get_str(), "owner address"); ptx.pubKeyOperator.Set(ParseBLSPubKey(request.params[paramIdx + 2].get_str(), "operator BLS address", use_legacy), use_legacy); @@ -749,17 +741,7 @@ static UniValue protx_register_common_wrapper(const JSONRPCRequest& request, } ptx.platformNodeID.SetHex(request.params[paramIdx + 6].get_str()); - int32_t requestedPlatformP2PPort = ParseInt32V(request.params[paramIdx + 7], "platformP2PPort"); - if (!ValidatePlatformPort(requestedPlatformP2PPort)) { - throw JSONRPCError(RPC_INVALID_PARAMETER, "platformP2PPort must be a valid port [1-65535]"); - } - ptx.platformP2PPort = static_cast(requestedPlatformP2PPort); - - int32_t requestedPlatformHTTPPort = ParseInt32V(request.params[paramIdx + 8], "platformHTTPPort"); - if (!ValidatePlatformPort(requestedPlatformHTTPPort)) { - throw JSONRPCError(RPC_INVALID_PARAMETER, "platformHTTPPort must be a valid port [1-65535]"); - } - ptx.platformHTTPPort = static_cast(requestedPlatformHTTPPort); + ProcessNetInfoPlatform(ptx, request.params[paramIdx + 7], request.params[paramIdx + 8]); paramIdx += 3; } @@ -1018,9 +1000,7 @@ static UniValue protx_update_service_common_wrapper(const JSONRPCRequest& reques /*is_basic_override=*/dmn->pdmnState->nVersion > ProTxVersion::LegacyBLS); ptx.netInfo = NetInfoInterface::MakeNetInfo(ptx.nVersion); - if (auto entryRet = ptx.netInfo->AddEntry(request.params[1].get_str()); entryRet != NetInfoStatus::Success) { - throw std::runtime_error(strprintf("%s (%s)", NISToString(entryRet), request.params[1].get_str())); - } + ProcessNetInfoCore(ptx, request.params[1], /*optional=*/false); CBLSSecretKey keyOperator = ParseBLSSecretKey(request.params[2].get_str(), "operatorKey"); @@ -1031,17 +1011,7 @@ static UniValue protx_update_service_common_wrapper(const JSONRPCRequest& reques } ptx.platformNodeID.SetHex(request.params[paramIdx].get_str()); - int32_t requestedPlatformP2PPort = ParseInt32V(request.params[paramIdx + 1], "platformP2PPort"); - if (!ValidatePlatformPort(requestedPlatformP2PPort)) { - throw JSONRPCError(RPC_INVALID_PARAMETER, "platformP2PPort must be a valid port [1-65535]"); - } - ptx.platformP2PPort = static_cast(requestedPlatformP2PPort); - - int32_t requestedPlatformHTTPPort = ParseInt32V(request.params[paramIdx + 2], "platformHTTPPort"); - if (!ValidatePlatformPort(requestedPlatformHTTPPort)) { - throw JSONRPCError(RPC_INVALID_PARAMETER, "platformHTTPPort must be a valid port [1-65535]"); - } - ptx.platformHTTPPort = static_cast(requestedPlatformHTTPPort); + ProcessNetInfoPlatform(ptx, request.params[paramIdx + 1], request.params[paramIdx + 2]); paramIdx += 3; } diff --git a/src/rpc/evo_util.cpp b/src/rpc/evo_util.cpp new file mode 100644 index 000000000000..c477961db259 --- /dev/null +++ b/src/rpc/evo_util.cpp @@ -0,0 +1,56 @@ +// Copyright (c) 2025 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 + +template +void ProcessNetInfoCore(T1& ptx, const UniValue& input, const bool optional) +{ + CHECK_NONFATAL(ptx.netInfo); + + if (!input.isStr()) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid param for ipAndPort, must be string"); + } + if (!optional && input.get_str().empty()) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Empty param for ipAndPort not allowed"); + } + if (!input.get_str().empty()) { + if (auto entryRet = ptx.netInfo->AddEntry(input.get_str()); entryRet != NetInfoStatus::Success) { + throw JSONRPCError(RPC_INVALID_PARAMETER, + strprintf("Error setting ipAndPort to '%s' (%s)", input.get_str(), NISToString(entryRet))); + } + } +} +template void ProcessNetInfoCore(CProRegTx& ptx, const UniValue& input, const bool optional); +template void ProcessNetInfoCore(CProUpServTx& ptx, const UniValue& input, const bool optional); + +template +void ProcessNetInfoPlatform(T1& ptx, const UniValue& input_p2p, const UniValue& input_http) +{ + CHECK_NONFATAL(ptx.netInfo); + + auto process_field = [](uint16_t& target, const UniValue& input, const std::string& field_name) { + if (!input.isNum() && !input.isStr()) { + throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Invalid param for %s, must be number", field_name)); + } + if (int32_t port{ParseInt32V(input, field_name)}; port >= 1 && port <= std::numeric_limits::max()) { + target = static_cast(port); + } else { + throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("%s must be a valid port [1-65535]", field_name)); + } + }; + process_field(ptx.platformP2PPort, input_p2p, "platformP2PPort"); + process_field(ptx.platformHTTPPort, input_http, "platformHTTPPort"); +} +template void ProcessNetInfoPlatform(CProRegTx& ptx, const UniValue& input_p2p, const UniValue& input_http); +template void ProcessNetInfoPlatform(CProUpServTx& ptx, const UniValue& input_p2p, const UniValue& input_http); diff --git a/src/rpc/evo_util.h b/src/rpc/evo_util.h new file mode 100644 index 000000000000..de06acc36028 --- /dev/null +++ b/src/rpc/evo_util.h @@ -0,0 +1,16 @@ +// Copyright (c) 2025 The Dash Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_RPC_EVO_UTIL_H +#define BITCOIN_RPC_EVO_UTIL_H + +class UniValue; + +template +void ProcessNetInfoCore(T1& ptx, const UniValue& input, const bool optional); + +template +void ProcessNetInfoPlatform(T1& ptx, const UniValue& input_p2p, const UniValue& input_http); + +#endif // BITCOIN_RPC_EVO_UTIL_H diff --git a/test/util/data/non-backported.txt b/test/util/data/non-backported.txt index a12d04314384..d7567f67035d 100644 --- a/test/util/data/non-backported.txt +++ b/test/util/data/non-backported.txt @@ -24,6 +24,7 @@ src/qt/governancelist.* src/qt/masternodelist.* src/rpc/coinjoin.cpp src/rpc/evo.cpp +src/rpc/evo_util.* src/rpc/governance.cpp src/rpc/masternode.cpp src/rpc/quorums.cpp From 4fd4e0ebf1029db82eabf8a9e4b2ac92c3ed52dd Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Fri, 11 Jul 2025 18:02:43 +0000 Subject: [PATCH 2/9] rpc: allow `ipAndPort` to accept multiple entries with arrays --- src/rpc/evo.cpp | 16 ++++-- src/rpc/evo_util.cpp | 50 +++++++++++++++---- .../feature_dip3_deterministicmns.py | 2 +- .../test_framework/test_framework.py | 18 +++---- 4 files changed, 62 insertions(+), 24 deletions(-) diff --git a/src/rpc/evo.cpp b/src/rpc/evo.cpp index 9340afc86190..131f558bfb92 100644 --- a/src/rpc/evo.cpp +++ b/src/rpc/evo.cpp @@ -93,13 +93,19 @@ static RPCArg GetRpcArg(const std::string& strParamName) "The private key belonging to this address must be known in your wallet."} }, {"ipAndPort", - {"ipAndPort", RPCArg::Type::STR, RPCArg::Optional::NO, - "IP and port in the form \"IP:PORT\". Must be unique on the network.\n" - "Can be set to an empty string, which will require a ProUpServTx afterwards."} + {"ipAndPort", RPCArg::Type::ARR, RPCArg::Optional::NO, + "Array of addresses in the form \"ADDR:PORT\". Must be unique on the network.\n" + "Can be set to an empty string, which will require a ProUpServTx afterwards.", + { + {"address", RPCArg::Type::STR, RPCArg::Optional::NO, ""}, + }} }, {"ipAndPort_update", - {"ipAndPort", RPCArg::Type::STR, RPCArg::Optional::NO, - "IP and port in the form \"IP:PORT\". Must be unique on the network."} + {"ipAndPort", RPCArg::Type::ARR, RPCArg::Optional::NO, + "Array of addresses in the form \"ADDR:PORT\". Must be unique on the network.", + { + {"address", RPCArg::Type::STR, RPCArg::Optional::NO, ""}, + }} }, {"operatorKey", {"operatorKey", RPCArg::Type::STR, RPCArg::Optional::NO, diff --git a/src/rpc/evo_util.cpp b/src/rpc/evo_util.cpp index c477961db259..611e1db5b680 100644 --- a/src/rpc/evo_util.cpp +++ b/src/rpc/evo_util.cpp @@ -18,18 +18,50 @@ void ProcessNetInfoCore(T1& ptx, const UniValue& input, const bool optional) { CHECK_NONFATAL(ptx.netInfo); - if (!input.isStr()) { - throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid param for ipAndPort, must be string"); - } - if (!optional && input.get_str().empty()) { - throw JSONRPCError(RPC_INVALID_PARAMETER, "Empty param for ipAndPort not allowed"); - } - if (!input.get_str().empty()) { - if (auto entryRet = ptx.netInfo->AddEntry(input.get_str()); entryRet != NetInfoStatus::Success) { + if (input.isStr()) { + const std::string& entry = input.get_str(); + if (entry.empty()) { + if (!optional) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Empty param for ipAndPort not allowed"); + } + return; // Nothing to do + } + if (auto entryRet = ptx.netInfo->AddEntry(entry); entryRet != NetInfoStatus::Success) { throw JSONRPCError(RPC_INVALID_PARAMETER, - strprintf("Error setting ipAndPort to '%s' (%s)", input.get_str(), NISToString(entryRet))); + strprintf("Error setting ipAndPort[0] to '%s' (%s)", entry, NISToString(entryRet))); + } + return; // Parsing complete + } + + if (input.isArray()) { + const UniValue& entries = input.get_array(); + if (entries.empty()) { + if (!optional) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Empty params for ipAndPort not allowed"); + } + return; // Nothing to do } + for (size_t idx{0}; idx < entries.size(); idx++) { + const UniValue& entry_uv{entries[idx]}; + if (!entry_uv.isStr()) { + throw JSONRPCError(RPC_INVALID_PARAMETER, + strprintf("Invalid param for ipAndPort[%d], must be string", idx)); + } + const std::string& entry = entry_uv.get_str(); + if (entry.empty()) { + throw JSONRPCError(RPC_INVALID_PARAMETER, + strprintf("Invalid param for ipAndPort[%d], cannot be empty string", idx)); + } + if (auto entryRet = ptx.netInfo->AddEntry(entry); entryRet != NetInfoStatus::Success) { + throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Error setting ipAndPort[%d] to '%s' (%s)", idx, + entry, NISToString(entryRet))); + } + } + return; // Parsing complete } + + // Invalid input + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid param for ipAndPort, must be string or array"); } template void ProcessNetInfoCore(CProRegTx& ptx, const UniValue& input, const bool optional); template void ProcessNetInfoCore(CProUpServTx& ptx, const UniValue& input, const bool optional); diff --git a/test/functional/feature_dip3_deterministicmns.py b/test/functional/feature_dip3_deterministicmns.py index 698a3f39765f..53321e3fc751 100755 --- a/test/functional/feature_dip3_deterministicmns.py +++ b/test/functional/feature_dip3_deterministicmns.py @@ -272,7 +272,7 @@ def update_mn_payee(self, mn: MasternodeInfo, payee): def test_protx_update_service(self, mn: MasternodeInfo): self.nodes[0].sendtoaddress(mn.fundsAddr, 0.001) - mn.update_service(self.nodes[0], submit=True, ipAndPort=f'127.0.0.2:{mn.nodePort}') + mn.update_service(self.nodes[0], submit=True, ipAndPort=[f'127.0.0.2:{mn.nodePort}']) self.generate(self.nodes[0], 1) for node in self.nodes: protx_info = node.protx('info', mn.proTxHash) diff --git a/test/functional/test_framework/test_framework.py b/test/functional/test_framework/test_framework.py index d6dbdfd8c487..dd4fa1e4e032 100755 --- a/test/functional/test_framework/test_framework.py +++ b/test/functional/test_framework/test_framework.py @@ -23,7 +23,7 @@ import time from concurrent.futures import ThreadPoolExecutor -from typing import List, Optional +from typing import List, Optional, Union from .address import ADDRESS_BCRT1_P2SH_OP_TRUE from .authproxy import JSONRPCException from test_framework.masternodes import check_banned, check_punished @@ -1216,7 +1216,7 @@ def get_node(self, test: BitcoinTestFramework) -> TestNode: return test.nodes[self.nodeIdx] def register(self, node: TestNode, submit: bool, collateral_txid: Optional[str] = None, collateral_vout: Optional[int] = None, - ipAndPort: Optional[str] = None, ownerAddr: Optional[str] = None, pubKeyOperator: Optional[str] = None, votingAddr: Optional[str] = None, + ipAndPort: Union[str, List[str], None] = None, ownerAddr: Optional[str] = None, pubKeyOperator: Optional[str] = None, votingAddr: Optional[str] = None, operator_reward: Optional[int] = None, rewards_address: Optional[str] = None, fundsAddr: Optional[str] = None, platform_node_id: Optional[str] = None, platform_p2p_port: Optional[int] = None, platform_http_port: Optional[int] = None, expected_assert_code: Optional[int] = None, expected_assert_msg: Optional[str] = None) -> Optional[str]: @@ -1236,7 +1236,7 @@ def register(self, node: TestNode, submit: bool, collateral_txid: Optional[str] args = [ collateral_txid or self.collateral_txid, collateral_vout or self.collateral_vout, - ipAndPort or f'127.0.0.1:{self.nodePort}', + ipAndPort or [f'127.0.0.1:{self.nodePort}'], ownerAddr or self.ownerAddr, pubKeyOperator or self.pubKeyOperator, votingAddr or self.votingAddr, @@ -1271,7 +1271,7 @@ def register(self, node: TestNode, submit: bool, collateral_txid: Optional[str] return ret - def register_fund(self, node: TestNode, submit: bool, collateral_address: Optional[str] = None, ipAndPort: Optional[str] = None, + def register_fund(self, node: TestNode, submit: bool, collateral_address: Optional[str] = None, ipAndPort: Union[str, List[str], None] = None, ownerAddr: Optional[str] = None, pubKeyOperator: Optional[str] = None, votingAddr: Optional[str] = None, operator_reward: Optional[int] = None, rewards_address: Optional[str] = None, fundsAddr: Optional[str] = None, platform_node_id: Optional[str] = None, platform_p2p_port: Optional[int] = None, platform_http_port: Optional[int] = None, @@ -1299,7 +1299,7 @@ def register_fund(self, node: TestNode, submit: bool, collateral_address: Option # Common arguments shared between regular masternodes and EvoNodes args = [ collateral_address or self.collateral_address, - ipAndPort or f'127.0.0.1:{self.nodePort}', + ipAndPort or [f'127.0.0.1:{self.nodePort}'], ownerAddr or self.ownerAddr, pubKeyOperator or self.pubKeyOperator, votingAddr or self.votingAddr, @@ -1410,7 +1410,7 @@ def update_registrar(self, node: TestNode, submit: bool, pubKeyOperator: Optiona return ret - def update_service(self, node: TestNode, submit: bool, ipAndPort: Optional[str] = None, platform_node_id: Optional[str] = None, platform_p2p_port: Optional[int] = None, + def update_service(self, node: TestNode, submit: bool, ipAndPort: Union[str, List[str], None] = None, platform_node_id: Optional[str] = None, platform_p2p_port: Optional[int] = None, platform_http_port: Optional[int] = None, address_operator: Optional[str] = None, fundsAddr: Optional[str] = None, expected_assert_code: Optional[int] = None, expected_assert_msg: Optional[str] = None) -> Optional[str]: if (expected_assert_code and not expected_assert_msg) or (not expected_assert_code and expected_assert_msg): @@ -1442,7 +1442,7 @@ def update_service(self, node: TestNode, submit: bool, ipAndPort: Optional[str] # Common arguments shared between regular masternodes and EvoNodes args = [ self.proTxHash, - ipAndPort or f'127.0.0.1:{self.nodePort}', + ipAndPort or [f'127.0.0.1:{self.nodePort}'], self.keyOperator, ] address_funds = fundsAddr or self.fundsAddr @@ -1643,7 +1643,7 @@ def dynamically_prepare_masternode(self, idx, node_p2p_port, evo=False, rnd=None mn.bury_tx(self, genIdx=0, txid=collateral_txid, depth=1) collateral_vout = mn.get_collateral_vout(self.nodes[0], collateral_txid) - ipAndPort = '127.0.0.1:%d' % node_p2p_port + ipAndPort = ['127.0.0.1:%d' % node_p2p_port] operatorReward = idx # platform_node_id, platform_p2p_port and platform_http_port are ignored for regular masternodes @@ -1709,7 +1709,7 @@ def prepare_masternode(self, idx): self.nodes[0].sendtoaddress(mn.fundsAddr, 0.001) port = p2p_port(len(self.nodes) + idx) - ipAndPort = '127.0.0.1:%d' % port + ipAndPort = ['127.0.0.1:%d' % port] operatorReward = idx submit = (idx % 4) < 2 From 33e5f6aff24c87bd2c84bd832a1a3f03c915e1c0 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Wed, 9 Jul 2025 12:35:41 +0000 Subject: [PATCH 3/9] refactor: s/ipAndPort/coreP2PAddrs/g --- src/rpc/evo.cpp | 24 +++++++++---------- src/rpc/evo_util.cpp | 14 +++++------ .../feature_dip3_deterministicmns.py | 2 +- .../test_framework/test_framework.py | 24 +++++++++---------- 4 files changed, 32 insertions(+), 32 deletions(-) diff --git a/src/rpc/evo.cpp b/src/rpc/evo.cpp index 131f558bfb92..a470253691c5 100644 --- a/src/rpc/evo.cpp +++ b/src/rpc/evo.cpp @@ -92,16 +92,16 @@ static RPCArg GetRpcArg(const std::string& strParamName) "If not specified, payoutAddress is the one that is going to be used.\n" "The private key belonging to this address must be known in your wallet."} }, - {"ipAndPort", - {"ipAndPort", RPCArg::Type::ARR, RPCArg::Optional::NO, + {"coreP2PAddrs", + {"coreP2PAddrs", RPCArg::Type::ARR, RPCArg::Optional::NO, "Array of addresses in the form \"ADDR:PORT\". Must be unique on the network.\n" "Can be set to an empty string, which will require a ProUpServTx afterwards.", { {"address", RPCArg::Type::STR, RPCArg::Optional::NO, ""}, }} }, - {"ipAndPort_update", - {"ipAndPort", RPCArg::Type::ARR, RPCArg::Optional::NO, + {"coreP2PAddrs_update", + {"coreP2PAddrs", RPCArg::Type::ARR, RPCArg::Optional::NO, "Array of addresses in the form \"ADDR:PORT\". Must be unique on the network.", { {"address", RPCArg::Type::STR, RPCArg::Optional::NO, ""}, @@ -403,7 +403,7 @@ static RPCHelpMan protx_register_fund_wrapper(const bool legacy) + HELP_REQUIRING_PASSPHRASE, { GetRpcArg("collateralAddress"), - GetRpcArg("ipAndPort"), + GetRpcArg("coreP2PAddrs"), GetRpcArg("ownerAddress"), legacy ? GetRpcArg("operatorPubKey_register_legacy") : GetRpcArg("operatorPubKey_register"), GetRpcArg("votingAddress_register"), @@ -454,7 +454,7 @@ static RPCHelpMan protx_register_wrapper(bool legacy) { GetRpcArg("collateralHash"), GetRpcArg("collateralIndex"), - GetRpcArg("ipAndPort"), + GetRpcArg("coreP2PAddrs"), GetRpcArg("ownerAddress"), legacy ? GetRpcArg("operatorPubKey_register_legacy") : GetRpcArg("operatorPubKey_register"), GetRpcArg("votingAddress_register"), @@ -506,7 +506,7 @@ static RPCHelpMan protx_register_prepare_wrapper(const bool legacy) { GetRpcArg("collateralHash"), GetRpcArg("collateralIndex"), - GetRpcArg("ipAndPort"), + GetRpcArg("coreP2PAddrs"), GetRpcArg("ownerAddress"), legacy ? GetRpcArg("operatorPubKey_register_legacy") : GetRpcArg("operatorPubKey_register"), GetRpcArg("votingAddress_register"), @@ -557,7 +557,7 @@ static RPCHelpMan protx_register_fund_evo() HELP_REQUIRING_PASSPHRASE, { GetRpcArg("collateralAddress"), - GetRpcArg("ipAndPort"), + GetRpcArg("coreP2PAddrs"), GetRpcArg("ownerAddress"), GetRpcArg("operatorPubKey_register"), GetRpcArg("votingAddress_register"), @@ -596,7 +596,7 @@ static RPCHelpMan protx_register_evo() { GetRpcArg("collateralHash"), GetRpcArg("collateralIndex"), - GetRpcArg("ipAndPort"), + GetRpcArg("coreP2PAddrs"), GetRpcArg("ownerAddress"), GetRpcArg("operatorPubKey_register"), GetRpcArg("votingAddress_register"), @@ -634,7 +634,7 @@ static RPCHelpMan protx_register_prepare_evo() { GetRpcArg("collateralHash"), GetRpcArg("collateralIndex"), - GetRpcArg("ipAndPort"), + GetRpcArg("coreP2PAddrs"), GetRpcArg("ownerAddress"), GetRpcArg("operatorPubKey_register"), GetRpcArg("votingAddress_register"), @@ -919,7 +919,7 @@ static RPCHelpMan protx_update_service() + HELP_REQUIRING_PASSPHRASE, { GetRpcArg("proTxHash"), - GetRpcArg("ipAndPort_update"), + GetRpcArg("coreP2PAddrs_update"), GetRpcArg("operatorKey"), GetRpcArg("operatorPayoutAddress"), GetRpcArg("feeSourceAddress"), @@ -952,7 +952,7 @@ static RPCHelpMan protx_update_service_evo() HELP_REQUIRING_PASSPHRASE, { GetRpcArg("proTxHash"), - GetRpcArg("ipAndPort_update"), + GetRpcArg("coreP2PAddrs_update"), GetRpcArg("operatorKey"), GetRpcArg("platformNodeID"), GetRpcArg("platformP2PPort"), diff --git a/src/rpc/evo_util.cpp b/src/rpc/evo_util.cpp index 611e1db5b680..a054beacb6b3 100644 --- a/src/rpc/evo_util.cpp +++ b/src/rpc/evo_util.cpp @@ -22,13 +22,13 @@ void ProcessNetInfoCore(T1& ptx, const UniValue& input, const bool optional) const std::string& entry = input.get_str(); if (entry.empty()) { if (!optional) { - throw JSONRPCError(RPC_INVALID_PARAMETER, "Empty param for ipAndPort not allowed"); + throw JSONRPCError(RPC_INVALID_PARAMETER, "Empty param for coreP2PAddrs not allowed"); } return; // Nothing to do } if (auto entryRet = ptx.netInfo->AddEntry(entry); entryRet != NetInfoStatus::Success) { throw JSONRPCError(RPC_INVALID_PARAMETER, - strprintf("Error setting ipAndPort[0] to '%s' (%s)", entry, NISToString(entryRet))); + strprintf("Error setting coreP2PAddrs[0] to '%s' (%s)", entry, NISToString(entryRet))); } return; // Parsing complete } @@ -37,7 +37,7 @@ void ProcessNetInfoCore(T1& ptx, const UniValue& input, const bool optional) const UniValue& entries = input.get_array(); if (entries.empty()) { if (!optional) { - throw JSONRPCError(RPC_INVALID_PARAMETER, "Empty params for ipAndPort not allowed"); + throw JSONRPCError(RPC_INVALID_PARAMETER, "Empty params for coreP2PAddrs not allowed"); } return; // Nothing to do } @@ -45,15 +45,15 @@ void ProcessNetInfoCore(T1& ptx, const UniValue& input, const bool optional) const UniValue& entry_uv{entries[idx]}; if (!entry_uv.isStr()) { throw JSONRPCError(RPC_INVALID_PARAMETER, - strprintf("Invalid param for ipAndPort[%d], must be string", idx)); + strprintf("Invalid param for coreP2PAddrs[%d], must be string", idx)); } const std::string& entry = entry_uv.get_str(); if (entry.empty()) { throw JSONRPCError(RPC_INVALID_PARAMETER, - strprintf("Invalid param for ipAndPort[%d], cannot be empty string", idx)); + strprintf("Invalid param for coreP2PAddrs[%d], cannot be empty string", idx)); } if (auto entryRet = ptx.netInfo->AddEntry(entry); entryRet != NetInfoStatus::Success) { - throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Error setting ipAndPort[%d] to '%s' (%s)", idx, + throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Error setting coreP2PAddrs[%d] to '%s' (%s)", idx, entry, NISToString(entryRet))); } } @@ -61,7 +61,7 @@ void ProcessNetInfoCore(T1& ptx, const UniValue& input, const bool optional) } // Invalid input - throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid param for ipAndPort, must be string or array"); + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid param for coreP2PAddrs, must be string or array"); } template void ProcessNetInfoCore(CProRegTx& ptx, const UniValue& input, const bool optional); template void ProcessNetInfoCore(CProUpServTx& ptx, const UniValue& input, const bool optional); diff --git a/test/functional/feature_dip3_deterministicmns.py b/test/functional/feature_dip3_deterministicmns.py index 53321e3fc751..1d909af4a8b2 100755 --- a/test/functional/feature_dip3_deterministicmns.py +++ b/test/functional/feature_dip3_deterministicmns.py @@ -272,7 +272,7 @@ def update_mn_payee(self, mn: MasternodeInfo, payee): def test_protx_update_service(self, mn: MasternodeInfo): self.nodes[0].sendtoaddress(mn.fundsAddr, 0.001) - mn.update_service(self.nodes[0], submit=True, ipAndPort=[f'127.0.0.2:{mn.nodePort}']) + mn.update_service(self.nodes[0], submit=True, coreP2PAddrs=[f'127.0.0.2:{mn.nodePort}']) self.generate(self.nodes[0], 1) for node in self.nodes: protx_info = node.protx('info', mn.proTxHash) diff --git a/test/functional/test_framework/test_framework.py b/test/functional/test_framework/test_framework.py index dd4fa1e4e032..eb1f8d6e1b22 100755 --- a/test/functional/test_framework/test_framework.py +++ b/test/functional/test_framework/test_framework.py @@ -1216,7 +1216,7 @@ def get_node(self, test: BitcoinTestFramework) -> TestNode: return test.nodes[self.nodeIdx] def register(self, node: TestNode, submit: bool, collateral_txid: Optional[str] = None, collateral_vout: Optional[int] = None, - ipAndPort: Union[str, List[str], None] = None, ownerAddr: Optional[str] = None, pubKeyOperator: Optional[str] = None, votingAddr: Optional[str] = None, + coreP2PAddrs: Union[str, List[str], None] = None, ownerAddr: Optional[str] = None, pubKeyOperator: Optional[str] = None, votingAddr: Optional[str] = None, operator_reward: Optional[int] = None, rewards_address: Optional[str] = None, fundsAddr: Optional[str] = None, platform_node_id: Optional[str] = None, platform_p2p_port: Optional[int] = None, platform_http_port: Optional[int] = None, expected_assert_code: Optional[int] = None, expected_assert_msg: Optional[str] = None) -> Optional[str]: @@ -1236,7 +1236,7 @@ def register(self, node: TestNode, submit: bool, collateral_txid: Optional[str] args = [ collateral_txid or self.collateral_txid, collateral_vout or self.collateral_vout, - ipAndPort or [f'127.0.0.1:{self.nodePort}'], + coreP2PAddrs or [f'127.0.0.1:{self.nodePort}'], ownerAddr or self.ownerAddr, pubKeyOperator or self.pubKeyOperator, votingAddr or self.votingAddr, @@ -1271,7 +1271,7 @@ def register(self, node: TestNode, submit: bool, collateral_txid: Optional[str] return ret - def register_fund(self, node: TestNode, submit: bool, collateral_address: Optional[str] = None, ipAndPort: Union[str, List[str], None] = None, + def register_fund(self, node: TestNode, submit: bool, collateral_address: Optional[str] = None, coreP2PAddrs: Union[str, List[str], None] = None, ownerAddr: Optional[str] = None, pubKeyOperator: Optional[str] = None, votingAddr: Optional[str] = None, operator_reward: Optional[int] = None, rewards_address: Optional[str] = None, fundsAddr: Optional[str] = None, platform_node_id: Optional[str] = None, platform_p2p_port: Optional[int] = None, platform_http_port: Optional[int] = None, @@ -1299,7 +1299,7 @@ def register_fund(self, node: TestNode, submit: bool, collateral_address: Option # Common arguments shared between regular masternodes and EvoNodes args = [ collateral_address or self.collateral_address, - ipAndPort or [f'127.0.0.1:{self.nodePort}'], + coreP2PAddrs or [f'127.0.0.1:{self.nodePort}'], ownerAddr or self.ownerAddr, pubKeyOperator or self.pubKeyOperator, votingAddr or self.votingAddr, @@ -1410,7 +1410,7 @@ def update_registrar(self, node: TestNode, submit: bool, pubKeyOperator: Optiona return ret - def update_service(self, node: TestNode, submit: bool, ipAndPort: Union[str, List[str], None] = None, platform_node_id: Optional[str] = None, platform_p2p_port: Optional[int] = None, + def update_service(self, node: TestNode, submit: bool, coreP2PAddrs: Union[str, List[str], None] = None, platform_node_id: Optional[str] = None, platform_p2p_port: Optional[int] = None, platform_http_port: Optional[int] = None, address_operator: Optional[str] = None, fundsAddr: Optional[str] = None, expected_assert_code: Optional[int] = None, expected_assert_msg: Optional[str] = None) -> Optional[str]: if (expected_assert_code and not expected_assert_msg) or (not expected_assert_code and expected_assert_msg): @@ -1442,7 +1442,7 @@ def update_service(self, node: TestNode, submit: bool, ipAndPort: Union[str, Lis # Common arguments shared between regular masternodes and EvoNodes args = [ self.proTxHash, - ipAndPort or [f'127.0.0.1:{self.nodePort}'], + coreP2PAddrs or [f'127.0.0.1:{self.nodePort}'], self.keyOperator, ] address_funds = fundsAddr or self.fundsAddr @@ -1643,11 +1643,11 @@ def dynamically_prepare_masternode(self, idx, node_p2p_port, evo=False, rnd=None mn.bury_tx(self, genIdx=0, txid=collateral_txid, depth=1) collateral_vout = mn.get_collateral_vout(self.nodes[0], collateral_txid) - ipAndPort = ['127.0.0.1:%d' % node_p2p_port] + coreP2PAddrs = ['127.0.0.1:%d' % node_p2p_port] operatorReward = idx # platform_node_id, platform_p2p_port and platform_http_port are ignored for regular masternodes - protx_result = mn.register(self.nodes[0], submit=True, collateral_txid=collateral_txid, collateral_vout=collateral_vout, ipAndPort=ipAndPort, operator_reward=operatorReward, + protx_result = mn.register(self.nodes[0], submit=True, collateral_txid=collateral_txid, collateral_vout=collateral_vout, coreP2PAddrs=coreP2PAddrs, operator_reward=operatorReward, platform_node_id=platform_node_id, platform_p2p_port=platform_p2p_port, platform_http_port=platform_http_port) assert protx_result is not None @@ -1709,16 +1709,16 @@ def prepare_masternode(self, idx): self.nodes[0].sendtoaddress(mn.fundsAddr, 0.001) port = p2p_port(len(self.nodes) + idx) - ipAndPort = ['127.0.0.1:%d' % port] + coreP2PAddrs = ['127.0.0.1:%d' % port] operatorReward = idx submit = (idx % 4) < 2 if register_fund: - protx_result = mn.register_fund(self.nodes[0], submit=submit, ipAndPort=ipAndPort, operator_reward=operatorReward) + protx_result = mn.register_fund(self.nodes[0], submit=submit, coreP2PAddrs=coreP2PAddrs, operator_reward=operatorReward) else: self.generate(self.nodes[0], 1, sync_fun=self.no_op) - protx_result = mn.register(self.nodes[0], submit=submit, collateral_txid=txid, collateral_vout=collateral_vout, ipAndPort=ipAndPort, + protx_result = mn.register(self.nodes[0], submit=submit, collateral_txid=txid, collateral_vout=collateral_vout, coreP2PAddrs=coreP2PAddrs, operator_reward=operatorReward) if submit: proTxHash = protx_result @@ -1730,7 +1730,7 @@ def prepare_masternode(self, idx): if operatorReward > 0: self.generate(self.nodes[0], 1, sync_fun=self.no_op) operatorPayoutAddress = self.nodes[0].getnewaddress() - mn.update_service(self.nodes[0], submit=True, ipAndPort=ipAndPort, address_operator=operatorPayoutAddress) + mn.update_service(self.nodes[0], submit=True, coreP2PAddrs=coreP2PAddrs, address_operator=operatorPayoutAddress) self.mninfo.append(mn) self.log.info("Prepared MN %d: collateral_txid=%s, collateral_vout=%d, protxHash=%s" % (idx, txid, collateral_vout, proTxHash)) From d9247ad34fc19b473b3d2958a7077368beea0c5c Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Sun, 18 May 2025 13:16:47 +0000 Subject: [PATCH 4/9] rpc: add new key "addresses" to allow reporting multiple entries --- contrib/seeds/makeseeds.py | 2 +- src/coinjoin/client.cpp | 1 + src/evo/core_write.cpp | 2 ++ src/evo/dmnstate.cpp | 2 ++ src/evo/netinfo.cpp | 14 ++++++++++++++ src/evo/netinfo.h | 4 ++++ src/evo/simplifiedmns.cpp | 1 + src/rpc/coinjoin.cpp | 6 ++++++ src/rpc/masternode.cpp | 1 + src/rpc/quorums.cpp | 1 + test/functional/feature_dip3_deterministicmns.py | 4 ++-- test/functional/rpc_quorum.py | 2 +- 12 files changed, 36 insertions(+), 4 deletions(-) diff --git a/contrib/seeds/makeseeds.py b/contrib/seeds/makeseeds.py index 314fb6ea1610..72dc8fb6a3c9 100755 --- a/contrib/seeds/makeseeds.py +++ b/contrib/seeds/makeseeds.py @@ -164,7 +164,7 @@ def main(): mns = filtermulticollateraladdress(mns) mns = filtermultipayoutaddress(mns) # Extract IPs - ips = [parseip(mn['state']['service']) for mn in mns] + ips = [parseip(mn['state']['addresses'][0]) for mn in mns] for onion in onions: parsed = parseip(onion) if parsed is not None: diff --git a/src/coinjoin/client.cpp b/src/coinjoin/client.cpp index d5f5185e4b28..8ad93018bc97 100644 --- a/src/coinjoin/client.cpp +++ b/src/coinjoin/client.cpp @@ -1884,6 +1884,7 @@ void CCoinJoinClientSession::GetJsonInfo(UniValue& obj) const obj.pushKV("protxhash", mixingMasternode->proTxHash.ToString()); obj.pushKV("outpoint", mixingMasternode->collateralOutpoint.ToStringShort()); obj.pushKV("service", mixingMasternode->pdmnState->netInfo->GetPrimary().ToStringAddrPort()); + obj.pushKV("addresses", mixingMasternode->pdmnState->netInfo->ToJson()); } obj.pushKV("denomination", ValueFromAmount(CoinJoin::DenominationToAmount(nSessionDenom))); obj.pushKV("state", GetStateString()); diff --git a/src/evo/core_write.cpp b/src/evo/core_write.cpp index 91ffc5c2270f..4a44e716ae44 100644 --- a/src/evo/core_write.cpp +++ b/src/evo/core_write.cpp @@ -68,6 +68,7 @@ ret.pushKV("collateralHash", collateralOutpoint.hash.ToString()); ret.pushKV("collateralIndex", (int)collateralOutpoint.n); ret.pushKV("service", netInfo->GetPrimary().ToStringAddrPort()); + ret.pushKV("addresses", netInfo->ToJson()); ret.pushKV("ownerAddress", EncodeDestination(PKHash(keyIDOwner))); ret.pushKV("votingAddress", EncodeDestination(PKHash(keyIDVoting))); if (CTxDestination dest; ExtractDestination(scriptPayout, dest)) { @@ -115,6 +116,7 @@ ret.pushKV("type", ToUnderlying(nType)); ret.pushKV("proTxHash", proTxHash.ToString()); ret.pushKV("service", netInfo->GetPrimary().ToStringAddrPort()); + ret.pushKV("addresses", netInfo->ToJson()); if (CTxDestination dest; ExtractDestination(scriptOperatorPayout, dest)) { ret.pushKV("operatorPayoutAddress", EncodeDestination(dest)); } diff --git a/src/evo/dmnstate.cpp b/src/evo/dmnstate.cpp index 70a1c139c9c9..8553efd6dd26 100644 --- a/src/evo/dmnstate.cpp +++ b/src/evo/dmnstate.cpp @@ -39,6 +39,7 @@ UniValue CDeterministicMNState::ToJson(MnType nType) const UniValue obj(UniValue::VOBJ); obj.pushKV("version", nVersion); obj.pushKV("service", netInfo->GetPrimary().ToStringAddrPort()); + obj.pushKV("addresses", netInfo->ToJson()); obj.pushKV("registeredHeight", nRegisteredHeight); obj.pushKV("lastPaidHeight", nLastPaidHeight); obj.pushKV("consecutivePayments", nConsecutivePayments); @@ -73,6 +74,7 @@ UniValue CDeterministicMNStateDiff::ToJson(MnType nType) const } if (fields & Field_netInfo) { obj.pushKV("service", state.netInfo->GetPrimary().ToStringAddrPort()); + obj.pushKV("addresses", state.netInfo->ToJson()); } if (fields & Field_nRegisteredHeight) { obj.pushKV("registeredHeight", state.nRegisteredHeight); diff --git a/src/evo/netinfo.cpp b/src/evo/netinfo.cpp index 76c67404a750..43b3c1fce5ca 100644 --- a/src/evo/netinfo.cpp +++ b/src/evo/netinfo.cpp @@ -11,6 +11,8 @@ #include #include +#include + namespace { static std::unique_ptr g_main_params{nullptr}; static std::once_flag g_main_params_flag; @@ -33,6 +35,13 @@ bool MatchCharsFilter(std::string_view input, std::string_view filter) } } // anonymous namespace +UniValue ArrFromService(const CService& addr) +{ + UniValue obj(UniValue::VARR); + obj.push_back(addr.ToStringAddrPort()); + return obj; +} + bool NetInfoEntry::operator==(const NetInfoEntry& rhs) const { if (m_type != rhs.m_type) return false; @@ -227,6 +236,11 @@ NetInfoStatus MnNetInfo::Validate() const return ValidateService(GetPrimary()); } +UniValue MnNetInfo::ToJson() const +{ + return ArrFromService(GetPrimary()); +} + std::string MnNetInfo::ToString() const { // Extra padding to account for padding done by the calling function. diff --git a/src/evo/netinfo.h b/src/evo/netinfo.h index 84bb5b5d39c0..9c09b8805b37 100644 --- a/src/evo/netinfo.h +++ b/src/evo/netinfo.h @@ -13,6 +13,8 @@ class CService; +class UniValue; + enum class NetInfoStatus : uint8_t { // Managing entries BadInput, @@ -141,6 +143,7 @@ class NetInfoInterface virtual bool CanStorePlatform() const = 0; virtual bool IsEmpty() const = 0; virtual NetInfoStatus Validate() const = 0; + virtual UniValue ToJson() const = 0; virtual std::string ToString() const = 0; virtual void Clear() = 0; @@ -197,6 +200,7 @@ class MnNetInfo final : public NetInfoInterface bool IsEmpty() const override { return m_addr.IsEmpty(); } bool CanStorePlatform() const override { return false; } NetInfoStatus Validate() const override; + UniValue ToJson() const override; std::string ToString() const override; void Clear() override { m_addr.Clear(); } diff --git a/src/evo/simplifiedmns.cpp b/src/evo/simplifiedmns.cpp index d6eda245d3b4..27ab4f16ce21 100644 --- a/src/evo/simplifiedmns.cpp +++ b/src/evo/simplifiedmns.cpp @@ -81,6 +81,7 @@ UniValue CSimplifiedMNListEntry::ToJson(bool extended) const obj.pushKV("proRegTxHash", proRegTxHash.ToString()); obj.pushKV("confirmedHash", confirmedHash.ToString()); obj.pushKV("service", netInfo->GetPrimary().ToStringAddrPort()); + obj.pushKV("addresses", netInfo->ToJson()); obj.pushKV("pubKeyOperator", pubKeyOperator.ToString()); obj.pushKV("votingAddress", EncodeDestination(PKHash(keyIDVoting))); obj.pushKV("isValid", isValid); diff --git a/src/rpc/coinjoin.cpp b/src/rpc/coinjoin.cpp index 15a36d3c67dc..e5a49d92a8de 100644 --- a/src/rpc/coinjoin.cpp +++ b/src/rpc/coinjoin.cpp @@ -436,6 +436,12 @@ static RPCHelpMan getcoinjoininfo() {RPCResult::Type::STR_HEX, "protxhash", "The ProTxHash of the masternode"}, {RPCResult::Type::STR_HEX, "outpoint", "The outpoint of the masternode"}, {RPCResult::Type::STR, "service", "The IP address and port of the masternode"}, + {RPCResult::Type::ARR, "addresses", "Network addresses of the masternode", + { + { + {RPCResult::Type::STR, "address", ""}, + } + }}, {RPCResult::Type::NUM, "denomination", "The denomination of the mixing session in " + CURRENCY_UNIT + ""}, {RPCResult::Type::STR_HEX, "state", "Current state of the mixing session"}, {RPCResult::Type::NUM, "entries_count", "The number of entries in the mixing session"}, diff --git a/src/rpc/masternode.cpp b/src/rpc/masternode.cpp index 7248938326d0..709b33aae19a 100644 --- a/src/rpc/masternode.cpp +++ b/src/rpc/masternode.cpp @@ -624,6 +624,7 @@ static RPCHelpMan masternodelist_helper(bool is_composite) UniValue objMN(UniValue::VOBJ); objMN.pushKV("proTxHash", dmn.proTxHash.ToString()); objMN.pushKV("address", dmn.pdmnState->netInfo->GetPrimary().ToStringAddrPort()); + objMN.pushKV("addresses", dmn.pdmnState->netInfo->ToJson()); objMN.pushKV("payee", payeeStr); objMN.pushKV("status", dmnToStatus(dmn)); objMN.pushKV("type", std::string(GetMnType(dmn.nType).description)); diff --git a/src/rpc/quorums.cpp b/src/rpc/quorums.cpp index 64ceaa059c3b..39c28256745d 100644 --- a/src/rpc/quorums.cpp +++ b/src/rpc/quorums.cpp @@ -207,6 +207,7 @@ static UniValue BuildQuorumInfo(const llmq::CQuorumBlockProcessor& quorum_block_ UniValue mo(UniValue::VOBJ); mo.pushKV("proTxHash", dmn->proTxHash.ToString()); mo.pushKV("service", dmn->pdmnState->netInfo->GetPrimary().ToStringAddrPort()); + mo.pushKV("addresses", dmn->pdmnState->netInfo->ToJson()); mo.pushKV("pubKeyOperator", dmn->pdmnState->pubKeyOperator.ToString()); mo.pushKV("valid", quorum->qc->validMembers[i]); if (quorum->qc->validMembers[i]) { diff --git a/test/functional/feature_dip3_deterministicmns.py b/test/functional/feature_dip3_deterministicmns.py index 1d909af4a8b2..7494f6536d1e 100755 --- a/test/functional/feature_dip3_deterministicmns.py +++ b/test/functional/feature_dip3_deterministicmns.py @@ -277,8 +277,8 @@ def test_protx_update_service(self, mn: MasternodeInfo): for node in self.nodes: protx_info = node.protx('info', mn.proTxHash) mn_list = node.masternode('list') - assert_equal(protx_info['state']['service'], '127.0.0.2:%d' % mn.nodePort) - assert_equal(mn_list['%s-%d' % (mn.collateral_txid, mn.collateral_vout)]['address'], '127.0.0.2:%d' % mn.nodePort) + assert_equal(protx_info['state']['addresses'][0], '127.0.0.2:%d' % mn.nodePort) + assert_equal(mn_list['%s-%d' % (mn.collateral_txid, mn.collateral_vout)]['addresses'][0], '127.0.0.2:%d' % mn.nodePort) # undo mn.update_service(self.nodes[0], submit=True) diff --git a/test/functional/rpc_quorum.py b/test/functional/rpc_quorum.py index 40da55c4a776..202f0374c9d3 100755 --- a/test/functional/rpc_quorum.py +++ b/test/functional/rpc_quorum.py @@ -28,7 +28,7 @@ def run_test(self): mn: MasternodeInfo = self.mninfo[idx] for member in quorum_info["members"]: if member["proTxHash"] == mn.proTxHash: - assert_equal(member["service"], f'127.0.0.1:{mn.nodePort}') + assert_equal(member['addresses'][0], f'127.0.0.1:{mn.nodePort}') if __name__ == '__main__': RPCMasternodeTest().main() From e0d2a81fa5a294cc04f2b209eb692297738ef5be Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Wed, 9 Jul 2025 11:55:59 +0000 Subject: [PATCH 5/9] rpc: deprecate key "service" replaced by "addresses" IsDeprecatedRPCEnabled() is defined in RPC code but we construct UniValue objects outside of RPC code that are impacted. They are linked in binaries that do not include RPC logic. So, we need to implement a function that's functionally identical, IsServiceDeprecatedRPCEnabled(). --- src/coinjoin/client.cpp | 4 +++- src/evo/core_write.cpp | 8 ++++++-- src/evo/dmnstate.cpp | 8 ++++++-- src/evo/netinfo.cpp | 6 ++++++ src/evo/netinfo.h | 3 +++ src/evo/simplifiedmns.cpp | 9 ++++++--- src/rpc/coinjoin.cpp | 2 +- src/rpc/masternode.cpp | 4 +++- src/rpc/quorums.cpp | 4 +++- 9 files changed, 37 insertions(+), 11 deletions(-) diff --git a/src/coinjoin/client.cpp b/src/coinjoin/client.cpp index 8ad93018bc97..e5125a017a4f 100644 --- a/src/coinjoin/client.cpp +++ b/src/coinjoin/client.cpp @@ -1883,7 +1883,9 @@ void CCoinJoinClientSession::GetJsonInfo(UniValue& obj) const assert(mixingMasternode->pdmnState); obj.pushKV("protxhash", mixingMasternode->proTxHash.ToString()); obj.pushKV("outpoint", mixingMasternode->collateralOutpoint.ToStringShort()); - obj.pushKV("service", mixingMasternode->pdmnState->netInfo->GetPrimary().ToStringAddrPort()); + if (m_wallet->chain().rpcEnableDeprecated("service")) { + obj.pushKV("service", mixingMasternode->pdmnState->netInfo->GetPrimary().ToStringAddrPort()); + } obj.pushKV("addresses", mixingMasternode->pdmnState->netInfo->ToJson()); } obj.pushKV("denomination", ValueFromAmount(CoinJoin::DenominationToAmount(nSessionDenom))); diff --git a/src/evo/core_write.cpp b/src/evo/core_write.cpp index 4a44e716ae44..28ba23342f0c 100644 --- a/src/evo/core_write.cpp +++ b/src/evo/core_write.cpp @@ -67,7 +67,9 @@ ret.pushKV("type", ToUnderlying(nType)); ret.pushKV("collateralHash", collateralOutpoint.hash.ToString()); ret.pushKV("collateralIndex", (int)collateralOutpoint.n); - ret.pushKV("service", netInfo->GetPrimary().ToStringAddrPort()); + if (IsServiceDeprecatedRPCEnabled()) { + ret.pushKV("service", netInfo->GetPrimary().ToStringAddrPort()); + } ret.pushKV("addresses", netInfo->ToJson()); ret.pushKV("ownerAddress", EncodeDestination(PKHash(keyIDOwner))); ret.pushKV("votingAddress", EncodeDestination(PKHash(keyIDVoting))); @@ -115,7 +117,9 @@ ret.pushKV("version", nVersion); ret.pushKV("type", ToUnderlying(nType)); ret.pushKV("proTxHash", proTxHash.ToString()); - ret.pushKV("service", netInfo->GetPrimary().ToStringAddrPort()); + if (IsServiceDeprecatedRPCEnabled()) { + ret.pushKV("service", netInfo->GetPrimary().ToStringAddrPort()); + } ret.pushKV("addresses", netInfo->ToJson()); if (CTxDestination dest; ExtractDestination(scriptOperatorPayout, dest)) { ret.pushKV("operatorPayoutAddress", EncodeDestination(dest)); diff --git a/src/evo/dmnstate.cpp b/src/evo/dmnstate.cpp index 8553efd6dd26..6734c9671a27 100644 --- a/src/evo/dmnstate.cpp +++ b/src/evo/dmnstate.cpp @@ -38,7 +38,9 @@ UniValue CDeterministicMNState::ToJson(MnType nType) const { UniValue obj(UniValue::VOBJ); obj.pushKV("version", nVersion); - obj.pushKV("service", netInfo->GetPrimary().ToStringAddrPort()); + if (IsServiceDeprecatedRPCEnabled()) { + obj.pushKV("service", netInfo->GetPrimary().ToStringAddrPort()); + } obj.pushKV("addresses", netInfo->ToJson()); obj.pushKV("registeredHeight", nRegisteredHeight); obj.pushKV("lastPaidHeight", nLastPaidHeight); @@ -73,7 +75,9 @@ UniValue CDeterministicMNStateDiff::ToJson(MnType nType) const obj.pushKV("version", state.nVersion); } if (fields & Field_netInfo) { - obj.pushKV("service", state.netInfo->GetPrimary().ToStringAddrPort()); + if (IsServiceDeprecatedRPCEnabled()) { + obj.pushKV("service", state.netInfo->GetPrimary().ToStringAddrPort()); + } obj.pushKV("addresses", state.netInfo->ToJson()); } if (fields & Field_nRegisteredHeight) { diff --git a/src/evo/netinfo.cpp b/src/evo/netinfo.cpp index 43b3c1fce5ca..03a258c4f777 100644 --- a/src/evo/netinfo.cpp +++ b/src/evo/netinfo.cpp @@ -42,6 +42,12 @@ UniValue ArrFromService(const CService& addr) return obj; } +bool IsServiceDeprecatedRPCEnabled() +{ + const auto args = gArgs.GetArgs("-deprecatedrpc"); + return std::find(args.begin(), args.end(), "service") != args.end(); +} + bool NetInfoEntry::operator==(const NetInfoEntry& rhs) const { if (m_type != rhs.m_type) return false; diff --git a/src/evo/netinfo.h b/src/evo/netinfo.h index 9c09b8805b37..27f2ce932895 100644 --- a/src/evo/netinfo.h +++ b/src/evo/netinfo.h @@ -53,6 +53,9 @@ constexpr std::string_view NISToString(const NetInfoStatus code) assert(false); } +/* Identical to IsDeprecatedRPCEnabled("service"). For use outside of RPC code. */ +bool IsServiceDeprecatedRPCEnabled(); + class NetInfoEntry { public: diff --git a/src/evo/simplifiedmns.cpp b/src/evo/simplifiedmns.cpp index 27ab4f16ce21..d88d74380004 100644 --- a/src/evo/simplifiedmns.cpp +++ b/src/evo/simplifiedmns.cpp @@ -4,15 +4,16 @@ #include -#include #include #include +#include #include +#include +#include #include #include #include #include -#include #include #include @@ -80,7 +81,9 @@ UniValue CSimplifiedMNListEntry::ToJson(bool extended) const obj.pushKV("nType", ToUnderlying(nType)); obj.pushKV("proRegTxHash", proRegTxHash.ToString()); obj.pushKV("confirmedHash", confirmedHash.ToString()); - obj.pushKV("service", netInfo->GetPrimary().ToStringAddrPort()); + if (IsServiceDeprecatedRPCEnabled()) { + obj.pushKV("service", netInfo->GetPrimary().ToStringAddrPort()); + } obj.pushKV("addresses", netInfo->ToJson()); obj.pushKV("pubKeyOperator", pubKeyOperator.ToString()); obj.pushKV("votingAddress", EncodeDestination(PKHash(keyIDVoting))); diff --git a/src/rpc/coinjoin.cpp b/src/rpc/coinjoin.cpp index e5a49d92a8de..2a2605ca032d 100644 --- a/src/rpc/coinjoin.cpp +++ b/src/rpc/coinjoin.cpp @@ -435,7 +435,7 @@ static RPCHelpMan getcoinjoininfo() { {RPCResult::Type::STR_HEX, "protxhash", "The ProTxHash of the masternode"}, {RPCResult::Type::STR_HEX, "outpoint", "The outpoint of the masternode"}, - {RPCResult::Type::STR, "service", "The IP address and port of the masternode"}, + {RPCResult::Type::STR, "service", "The IP address and port of the masternode (DEPRECATED, returned only if config option -deprecatedrpc=service is passed)"}, {RPCResult::Type::ARR, "addresses", "Network addresses of the masternode", { { diff --git a/src/rpc/masternode.cpp b/src/rpc/masternode.cpp index 709b33aae19a..354c532a8bcf 100644 --- a/src/rpc/masternode.cpp +++ b/src/rpc/masternode.cpp @@ -623,7 +623,9 @@ static RPCHelpMan masternodelist_helper(bool is_composite) strOutpoint.find(strFilter) == std::string::npos) return; UniValue objMN(UniValue::VOBJ); objMN.pushKV("proTxHash", dmn.proTxHash.ToString()); - objMN.pushKV("address", dmn.pdmnState->netInfo->GetPrimary().ToStringAddrPort()); + if (IsDeprecatedRPCEnabled("service")) { + objMN.pushKV("address", dmn.pdmnState->netInfo->GetPrimary().ToStringAddrPort()); + } objMN.pushKV("addresses", dmn.pdmnState->netInfo->ToJson()); objMN.pushKV("payee", payeeStr); objMN.pushKV("status", dmnToStatus(dmn)); diff --git a/src/rpc/quorums.cpp b/src/rpc/quorums.cpp index 39c28256745d..c8bfd6cd0128 100644 --- a/src/rpc/quorums.cpp +++ b/src/rpc/quorums.cpp @@ -206,7 +206,9 @@ static UniValue BuildQuorumInfo(const llmq::CQuorumBlockProcessor& quorum_block_ const auto& dmn = quorum->members[i]; UniValue mo(UniValue::VOBJ); mo.pushKV("proTxHash", dmn->proTxHash.ToString()); - mo.pushKV("service", dmn->pdmnState->netInfo->GetPrimary().ToStringAddrPort()); + if (IsDeprecatedRPCEnabled("service")) { + mo.pushKV("service", dmn->pdmnState->netInfo->GetPrimary().ToStringAddrPort()); + } mo.pushKV("addresses", dmn->pdmnState->netInfo->ToJson()); mo.pushKV("pubKeyOperator", dmn->pdmnState->pubKeyOperator.ToString()); mo.pushKV("valid", quorum->qc->validMembers[i]); From a60c39acdcb8c4d267fbf852d37af92b9237b3d1 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Wed, 9 Jul 2025 12:38:56 +0000 Subject: [PATCH 6/9] test: add functional test for `addresses` and deprecated `service` field --- test/functional/rpc_netinfo.py | 240 +++++++++++++++++++++++++++++++++ test/functional/test_runner.py | 1 + 2 files changed, 241 insertions(+) create mode 100755 test/functional/rpc_netinfo.py diff --git a/test/functional/rpc_netinfo.py b/test/functional/rpc_netinfo.py new file mode 100755 index 000000000000..0ca871b2c503 --- /dev/null +++ b/test/functional/rpc_netinfo.py @@ -0,0 +1,240 @@ +#!/usr/bin/env python3 +# Copyright (c) 2025 The Dash Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test network information fields across RPCs.""" + +from test_framework.util import ( + assert_equal +) +from test_framework.script import ( + hash160 +) +from test_framework.test_framework import ( + BitcoinTestFramework, + MasternodeInfo, + p2p_port +) +from test_framework.test_node import TestNode + +from _decimal import Decimal +from random import randint + +# See CRegTestParams in src/chainparams.cpp +DEFAULT_PORT_PLATFORM_P2P = 22200 +DEFAULT_PORT_PLATFORM_HTTP = 22201 + +class Node: + mn: MasternodeInfo + node: TestNode + platform_nodeid: str = "" + + def __init__(self, node: TestNode, is_evo: bool): + self.mn = MasternodeInfo(evo=is_evo, legacy=False) + self.mn.generate_addresses(node) + self.mn.set_node(node.index) + self.mn.set_params(nodePort=p2p_port(node.index)) + self.node = node + + def generate_collateral(self, test: BitcoinTestFramework): + assert self.mn.nodeIdx is not None + + while self.node.getbalance() < self.mn.get_collateral_value(): + test.bump_mocktime(1) + test.generate(self.node, 10, sync_fun=test.no_op) + + collateral_txid = self.node.sendmany("", {self.mn.collateral_address: self.mn.get_collateral_value(), self.mn.fundsAddr: 1}) + self.mn.bury_tx(test, self.mn.nodeIdx, collateral_txid, 1) + collateral_vout = self.mn.get_collateral_vout(self.node, collateral_txid) + self.mn.set_params(collateral_txid=collateral_txid, collateral_vout=collateral_vout) + + def is_mn_visible(self, _protx_hash = None) -> bool: + protx_hash = _protx_hash or self.mn.proTxHash + mn_list = self.node.masternodelist() + mn_visible = False + for mn_entry in mn_list: + dmn = mn_list.get(mn_entry) + if dmn['proTxHash'] == protx_hash: + assert_equal(dmn['type'], "Evo" if self.mn.evo else "Regular") + mn_visible = True + return mn_visible + + def register_mn(self, test: BitcoinTestFramework, submit: bool, addrs_core_p2p, addrs_platform_p2p = None, addrs_platform_http = None) -> str: + assert self.mn.nodeIdx is not None + + if self.mn.evo and (not addrs_platform_http or not addrs_platform_p2p): + raise AssertionError("EvoNode but platformP2PPort and platformHTTPPort not specified") + + # Evonode-specific fields are ignored if regular masternode + self.platform_nodeid = hash160(b'%d' % randint(1, 65535)).hex() + protx_output = self.mn.register(self.node, submit=submit, coreP2PAddrs=addrs_core_p2p, operator_reward=0, + platform_node_id=self.platform_nodeid, platform_p2p_port=addrs_platform_p2p, + platform_http_port=addrs_platform_http) + assert protx_output is not None + + if not submit: + return "" + + # Bury ProTx transaction and check if masternode is online + self.mn.set_params(proTxHash=protx_output, operator_reward=0) + self.mn.bury_tx(test, self.mn.nodeIdx, protx_output, 1) + assert_equal(self.is_mn_visible(), True) + + test.log.debug(f"Registered {'Evo' if self.mn.evo else 'regular'} masternode with collateral_txid={self.mn.collateral_txid}, " + f"collateral_vout={self.mn.collateral_vout}, provider_txid={self.mn.proTxHash}") + + test.restart_node(self.mn.nodeIdx, extra_args=self.node.extra_args + [f'-masternodeblsprivkey={self.mn.keyOperator}']) + return self.mn.proTxHash + + def update_mn(self, test: BitcoinTestFramework, addrs_core_p2p, addrs_platform_p2p = None, addrs_platform_http = None) -> str: + assert self.mn.nodeIdx is not None + + if self.mn.evo and (not addrs_platform_http or not addrs_platform_p2p): + raise AssertionError("EvoNode but platformP2PPort and platformHTTPPort not specified") + + # Evonode-specific fields are ignored if regular masternode + protx_output = self.mn.update_service(self.node, submit=True, coreP2PAddrs=addrs_core_p2p, platform_node_id=self.platform_nodeid, + platform_p2p_port=addrs_platform_p2p, platform_http_port=addrs_platform_http) + assert protx_output is not None + + self.mn.bury_tx(test, self.mn.nodeIdx, protx_output, 1) + assert_equal(self.is_mn_visible(), True) + + test.log.debug(f"Updated {'Evo' if self.mn.evo else 'regular'} masternode with collateral_txid={self.mn.collateral_txid}, " + f"collateral_vout={self.mn.collateral_vout}, provider_txid={self.mn.proTxHash}") + return protx_output + + def destroy_mn(self, test: BitcoinTestFramework): + assert self.mn.nodeIdx is not None + + # Get UTXO from address used to pay fees, generate new addresses + address_funds_unspent = self.node.listunspent(0, 99999, [self.mn.fundsAddr])[0] + address_funds_value = address_funds_unspent['amount'] + self.mn.generate_addresses(self.node, True) + + # Create transaction to spend old collateral and fee change + raw_tx = self.node.createrawtransaction([ + { 'txid': self.mn.collateral_txid, 'vout': self.mn.collateral_vout }, + { 'txid': address_funds_unspent['txid'], 'vout': address_funds_unspent['vout'] } + ], [ + {self.mn.collateral_address: float(self.mn.get_collateral_value())}, + {self.mn.fundsAddr: float(address_funds_value - Decimal(0.001))} + ]) + raw_tx = self.node.signrawtransactionwithwallet(raw_tx)['hex'] + + # Send that transaction, resulting txid is new collateral + new_collateral_txid = self.node.sendrawtransaction(raw_tx) + self.mn.bury_tx(test, self.mn.nodeIdx, new_collateral_txid, 1) + new_collateral_vout = self.mn.get_collateral_vout(self.node, new_collateral_txid) + self.mn.set_params(proTxHash="", collateral_txid=new_collateral_txid, collateral_vout=new_collateral_vout) + + # Old masternode entry should be dead + assert_equal(self.is_mn_visible(self.mn.proTxHash), False) + test.log.debug(f"Destroyed {'Evo' if self.mn.evo else 'regular'} masternode with collateral_txid={self.mn.collateral_txid}, " + f"collateral_vout={self.mn.collateral_vout}, provider_txid={self.mn.proTxHash}") + + test.restart_node(self.mn.nodeIdx, extra_args=self.node.extra_args) + +class NetInfoTest(BitcoinTestFramework): + def set_test_params(self): + self.num_nodes = 2 + self.extra_args = [ + ["-dip3params=2:2"], + ["-deprecatedrpc=service", "-dip3params=2:2"] + ] + + def skip_test_if_missing_module(self): + self.skip_if_no_wallet() + + def check_netinfo_fields(self, val, core_p2p_port: int): + assert_equal(val[0], f"127.0.0.1:{core_p2p_port}") + + def run_test(self): + self.node_evo: Node = Node(self.nodes[0], True) + self.node_evo.generate_collateral(self) + + self.node_simple: TestNode = self.nodes[1] + + # netInfo is represented with JSON in CProRegTx, CProUpServTx, CDeterministicMNState and CSimplifiedMNListEntry, + # so we need to test calls that rely on these underlying implementations. Start by collecting RPC responses. + self.log.info("Collect JSON RPC responses from node") + + # CProRegTx::ToJson() <- TxToUniv() <- TxToJSON() <- getrawtransaction + proregtx_hash = self.node_evo.register_mn(self, True, f"127.0.0.1:{self.node_evo.mn.nodePort}", DEFAULT_PORT_PLATFORM_P2P, DEFAULT_PORT_PLATFORM_HTTP) + proregtx_rpc = self.node_evo.node.getrawtransaction(proregtx_hash, True) + + # CDeterministicMNState::ToJson() <- CDeterministicMN::pdmnState <- masternode_status + masternode_status = self.node_evo.node.masternode('status') + + # Generate deprecation-disabled response to avoid having to re-create a masternode again later on + self.restart_node(self.node_evo.mn.nodeIdx, extra_args=self.node_evo.node.extra_args + + [f'-masternodeblsprivkey={self.node_evo.mn.keyOperator}', '-deprecatedrpc=service']) + self.connect_nodes(self.node_evo.mn.nodeIdx, self.node_simple.index) # Needed as restarts don't reconnect nodes + masternode_status_depr = self.node_evo.node.masternode('status') + + # Stop actively running the masternode so we can issue a CProUpServTx (and enable the deprecation) + self.restart_node(self.node_evo.mn.nodeIdx, extra_args=self.node_evo.node.extra_args) + self.connect_nodes(self.node_evo.mn.nodeIdx, self.node_simple.index) # Needed as restarts don't reconnect nodes + + # CProUpServTx::ToJson() <- TxToUniv() <- TxToJSON() <- getrawtransaction + proupservtx_hash = self.node_evo.update_mn(self, f"127.0.0.1:{self.node_evo.mn.nodePort+1}", DEFAULT_PORT_PLATFORM_P2P, DEFAULT_PORT_PLATFORM_HTTP) + proupservtx_rpc = self.node_evo.node.getrawtransaction(proupservtx_hash, True) + + # We need to update *twice*, the first time to incorrect values and the second time, back to correct values. + # This is to make sure that the fields we need to check against are reflected in the diff. + proupservtx_hash = self.node_evo.update_mn(self, f"127.0.0.1:{self.node_evo.mn.nodePort}", DEFAULT_PORT_PLATFORM_P2P, DEFAULT_PORT_PLATFORM_HTTP) + proupservtx_rpc = self.node_evo.node.getrawtransaction(proupservtx_hash, True) + + # CSimplifiedMNListEntry::ToJson() <- CSimplifiedMNListDiff::mnList <- CSimplifiedMNListDiff::ToJson() <- protx_diff + masternode_active_height: int = masternode_status['dmnState']['registeredHeight'] + protx_diff_rpc = self.node_evo.node.protx('diff', masternode_active_height - 1, masternode_active_height) + + # CDeterministicMNStateDiff::ToJson() <- CDeterministicMNListDiff::updatedMns <- protx_listdiff + proupservtx_height = proupservtx_rpc['height'] + protx_listdiff_rpc = self.node_evo.node.protx('listdiff', proupservtx_height - 1, proupservtx_height) + + self.log.info("Test RPCs return an 'addresses' field") + assert "addresses" in proregtx_rpc['proRegTx'].keys() + assert "addresses" in masternode_status['dmnState'].keys() + assert "addresses" in proupservtx_rpc['proUpServTx'].keys() + assert "addresses" in protx_diff_rpc['mnList'][0].keys() + assert "addresses" in protx_listdiff_rpc['updatedMNs'][0][proregtx_hash].keys() + + self.log.info("Test 'addresses' report correctly") + self.check_netinfo_fields(proregtx_rpc['proRegTx']['addresses'], self.node_evo.mn.nodePort) + self.check_netinfo_fields(masternode_status['dmnState']['addresses'], self.node_evo.mn.nodePort) + self.check_netinfo_fields(proupservtx_rpc['proUpServTx']['addresses'], self.node_evo.mn.nodePort) + self.check_netinfo_fields(protx_diff_rpc['mnList'][0]['addresses'], self.node_evo.mn.nodePort) + self.check_netinfo_fields(protx_listdiff_rpc['updatedMNs'][0][proregtx_hash]['addresses'], self.node_evo.mn.nodePort) + + self.log.info("Test RPCs by default no longer return a 'service' field") + assert "service" not in proregtx_rpc['proRegTx'].keys() + assert "service" not in masternode_status['dmnState'].keys() + assert "service" not in proupservtx_rpc['proUpServTx'].keys() + assert "service" not in protx_diff_rpc['mnList'][0].keys() + assert "service" not in protx_listdiff_rpc['updatedMNs'][0][proregtx_hash].keys() + # "service" in "masternode status" is exempt from the deprecation as the primary address is + # relevant on the host node as opposed to expressing payload information in most other RPCs. + assert "service" in masternode_status.keys() + + self.node_evo.destroy_mn(self) # Shut down previous masternode + self.connect_nodes(self.node_evo.mn.nodeIdx, self.node_simple.index) # Needed as restarts don't reconnect nodes + + self.log.info("Collect RPC responses from node with -deprecatedrpc=service") + + # Re-use chain activity from earlier + proregtx_rpc = self.node_simple.getrawtransaction(proregtx_hash, True) + proupservtx_rpc = self.node_simple.getrawtransaction(proupservtx_hash, True) + protx_diff_rpc = self.node_simple.protx('diff', masternode_active_height - 1, masternode_active_height) + masternode_status = masternode_status_depr # Pull in response generated from earlier + protx_listdiff_rpc = self.node_simple.protx('listdiff', proupservtx_height - 1, proupservtx_height) + + self.log.info("Test RPCs return 'service' with -deprecatedrpc=service") + assert "service" in proregtx_rpc['proRegTx'].keys() + assert "service" in masternode_status['dmnState'].keys() + assert "service" in proupservtx_rpc['proUpServTx'].keys() + assert "service" in protx_diff_rpc['mnList'][0].keys() + assert "service" in protx_listdiff_rpc['updatedMNs'][0][proregtx_hash].keys() + +if __name__ == "__main__": + NetInfoTest().main() diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 963d281ed235..1f1bae53c762 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -233,6 +233,7 @@ 'p2p_addrfetch.py', 'rpc_net.py --v1transport', 'rpc_net.py --v2transport', + 'rpc_netinfo.py', 'wallet_keypool.py --legacy-wallet', 'wallet_keypool_hd.py --legacy-wallet', 'wallet_keypool_hd.py --descriptors', From 859ce5ccfda8fe3ae7f1020fa571ade40499f776 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Wed, 9 Jul 2025 13:33:37 +0000 Subject: [PATCH 7/9] test: validate common conditions for input validation --- test/functional/rpc_netinfo.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/test/functional/rpc_netinfo.py b/test/functional/rpc_netinfo.py index 0ca871b2c503..f7fead452247 100755 --- a/test/functional/rpc_netinfo.py +++ b/test/functional/rpc_netinfo.py @@ -155,6 +155,35 @@ def run_test(self): self.node_simple: TestNode = self.nodes[1] + self.log.info("Test input validation for masternode address fields") + self.test_validation_common() + + self.log.info("Test output masternode address fields for consistency") + self.test_deprecation() + + def test_validation_common(self): + # Arrays of addresses with invalid inputs get refused + self.node_evo.register_mn(self, False, [[f"127.0.0.1:{self.node_evo.mn.nodePort}"]], + DEFAULT_PORT_PLATFORM_P2P, DEFAULT_PORT_PLATFORM_HTTP, + -8, "Invalid param for coreP2PAddrs[0], must be string") + self.node_evo.register_mn(self, False, [f"127.0.0.1:{self.node_evo.mn.nodePort}", ""], + DEFAULT_PORT_PLATFORM_P2P, DEFAULT_PORT_PLATFORM_HTTP, + -8, "Invalid param for coreP2PAddrs[1], cannot be empty string") + self.node_evo.register_mn(self, False, [f"127.0.0.1:{self.node_evo.mn.nodePort}", self.node_evo.mn.nodePort], + DEFAULT_PORT_PLATFORM_P2P, DEFAULT_PORT_PLATFORM_HTTP, + -8, "Invalid param for coreP2PAddrs[1], must be string") + + # platformP2PPort and platformHTTPPort must be within acceptable range (i.e. a valid port number) + self.node_evo.register_mn(self, False, f"127.0.0.1:{self.node_evo.mn.nodePort}", "0", DEFAULT_PORT_PLATFORM_HTTP, + -8, "platformP2PPort must be a valid port [1-65535]") + self.node_evo.register_mn(self, False, f"127.0.0.1:{self.node_evo.mn.nodePort}", "65536", DEFAULT_PORT_PLATFORM_HTTP, + -8, "platformP2PPort must be a valid port [1-65535]") + self.node_evo.register_mn(self, False, f"127.0.0.1:{self.node_evo.mn.nodePort}", DEFAULT_PORT_PLATFORM_P2P, "0", + -8, "platformHTTPPort must be a valid port [1-65535]") + self.node_evo.register_mn(self, False, f"127.0.0.1:{self.node_evo.mn.nodePort}", DEFAULT_PORT_PLATFORM_P2P, "65536", + -8, "platformHTTPPort must be a valid port [1-65535]") + + def test_deprecation(self): # netInfo is represented with JSON in CProRegTx, CProUpServTx, CDeterministicMNState and CSimplifiedMNListEntry, # so we need to test calls that rely on these underlying implementations. Start by collecting RPC responses. self.log.info("Collect JSON RPC responses from node") From 5c564c2649b49a70f719c8e2669d81a9448429f6 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Wed, 9 Jul 2025 13:34:59 +0000 Subject: [PATCH 8/9] test: check pre-fork conditions for ProRegTx and ProUpServTx preparation --- test/functional/rpc_netinfo.py | 37 ++++++++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/test/functional/rpc_netinfo.py b/test/functional/rpc_netinfo.py index f7fead452247..ba1951b820d7 100755 --- a/test/functional/rpc_netinfo.py +++ b/test/functional/rpc_netinfo.py @@ -20,6 +20,8 @@ from _decimal import Decimal from random import randint +# See CMainParams in src/chainparams.cpp +DEFAULT_PORT_MAINNET_CORE_P2P = 9999 # See CRegTestParams in src/chainparams.cpp DEFAULT_PORT_PLATFORM_P2P = 22200 DEFAULT_PORT_PLATFORM_HTTP = 22201 @@ -59,7 +61,7 @@ def is_mn_visible(self, _protx_hash = None) -> bool: mn_visible = True return mn_visible - def register_mn(self, test: BitcoinTestFramework, submit: bool, addrs_core_p2p, addrs_platform_p2p = None, addrs_platform_http = None) -> str: + def register_mn(self, test: BitcoinTestFramework, submit: bool, addrs_core_p2p, addrs_platform_p2p = None, addrs_platform_http = None, code = None, msg = None) -> str: assert self.mn.nodeIdx is not None if self.mn.evo and (not addrs_platform_http or not addrs_platform_p2p): @@ -69,11 +71,16 @@ def register_mn(self, test: BitcoinTestFramework, submit: bool, addrs_core_p2p, self.platform_nodeid = hash160(b'%d' % randint(1, 65535)).hex() protx_output = self.mn.register(self.node, submit=submit, coreP2PAddrs=addrs_core_p2p, operator_reward=0, platform_node_id=self.platform_nodeid, platform_p2p_port=addrs_platform_p2p, - platform_http_port=addrs_platform_http) - assert protx_output is not None + platform_http_port=addrs_platform_http, expected_assert_code=code, expected_assert_msg=msg) - if not submit: + # If we expected error, make sure the transaction didn't succeed + if code and msg: + assert protx_output is None return "" + else: + assert protx_output is not None + if not submit: + return "" # Bury ProTx transaction and check if masternode is online self.mn.set_params(proTxHash=protx_output, operator_reward=0) @@ -157,6 +164,7 @@ def run_test(self): self.log.info("Test input validation for masternode address fields") self.test_validation_common() + self.test_validation_legacy() self.log.info("Test output masternode address fields for consistency") self.test_deprecation() @@ -183,6 +191,27 @@ def test_validation_common(self): self.node_evo.register_mn(self, False, f"127.0.0.1:{self.node_evo.mn.nodePort}", DEFAULT_PORT_PLATFORM_P2P, "65536", -8, "platformHTTPPort must be a valid port [1-65535]") + def test_validation_legacy(self): + # Using mainnet P2P port gets refused + self.node_evo.register_mn(self, False, f"127.0.0.1:{DEFAULT_PORT_MAINNET_CORE_P2P}", + DEFAULT_PORT_PLATFORM_P2P, DEFAULT_PORT_PLATFORM_HTTP, + -8, f"Error setting coreP2PAddrs[0] to '127.0.0.1:{DEFAULT_PORT_MAINNET_CORE_P2P}' (invalid port)") + + # Arrays of addresses are recognized by coreP2PAddrs (but get refused for too many entries) + self.node_evo.register_mn(self, False, [f"127.0.0.1:{self.node_evo.mn.nodePort}", f"127.0.0.2:{self.node_evo.mn.nodePort}"], + DEFAULT_PORT_PLATFORM_P2P, DEFAULT_PORT_PLATFORM_HTTP, + -8, f"Error setting coreP2PAddrs[1] to '127.0.0.2:{self.node_evo.mn.nodePort}' (too many entries)") + + # platformP2PPort and platformHTTPPort doesn't accept non-numeric inputs + self.node_evo.register_mn(self, False, f"127.0.0.1:{self.node_evo.mn.nodePort}", f"127.0.0.1:{DEFAULT_PORT_PLATFORM_P2P}", DEFAULT_PORT_PLATFORM_HTTP, + -8, f"platformP2PPort must be a 32bit integer (not '127.0.0.1:{DEFAULT_PORT_PLATFORM_P2P}')") + self.node_evo.register_mn(self, False, f"127.0.0.1:{self.node_evo.mn.nodePort}", [f"127.0.0.1:{DEFAULT_PORT_PLATFORM_P2P}"], DEFAULT_PORT_PLATFORM_HTTP, + -8, "Invalid param for platformP2PPort, must be number") + self.node_evo.register_mn(self, False, f"127.0.0.1:{self.node_evo.mn.nodePort}", DEFAULT_PORT_PLATFORM_P2P, f"127.0.0.1:{DEFAULT_PORT_PLATFORM_HTTP}", + -8, f"platformHTTPPort must be a 32bit integer (not '127.0.0.1:{DEFAULT_PORT_PLATFORM_HTTP}')") + self.node_evo.register_mn(self, False, f"127.0.0.1:{self.node_evo.mn.nodePort}", DEFAULT_PORT_PLATFORM_P2P, [f"127.0.0.1:{DEFAULT_PORT_PLATFORM_HTTP}"], + -8, "Invalid param for platformHTTPPort, must be number") + def test_deprecation(self): # netInfo is represented with JSON in CProRegTx, CProUpServTx, CDeterministicMNState and CSimplifiedMNListEntry, # so we need to test calls that rely on these underlying implementations. Start by collecting RPC responses. From 7fdd642f047d7127d387e5eb84e2cd120da609f6 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Fri, 6 Jun 2025 17:37:35 +0000 Subject: [PATCH 9/9] docs: add documentation for deprecation, new field and renamed input --- doc/release-notes-6665.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 doc/release-notes-6665.md diff --git a/doc/release-notes-6665.md b/doc/release-notes-6665.md new file mode 100644 index 000000000000..efdaea2e482d --- /dev/null +++ b/doc/release-notes-6665.md @@ -0,0 +1,13 @@ +Updated RPCs +------------ + +* The input field `ipAndPort` has been renamed to `coreP2PAddrs`. + * `coreP2PAddrs` can now, in addition to accepting a string, accept an array of strings, subject to validation rules. + +* The key `service` has been deprecated for some RPCs (`decoderawtransaction`, `decodepsbt`, `getblock`, `getrawtransaction`, + `gettransaction`, `masternode status` (only for the `dmnState` key), `protx diff`, `protx listdiff`) and has been replaced + with the field `addresses`. + * The deprecated field can be re-enabled using `-deprecatedrpc=service` but is liable to be removed in future versions + of Dash Core. + * This change does not affect `masternode status` (except for the `dmnState` key) as `service` does not represent a payload + value but the external address advertised by the active masternode.