From 50cdc84c95ac9bf970291801833a8c47be2639ff Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Tue, 19 Aug 2025 15:54:25 +0000 Subject: [PATCH 01/27] fix: don't return invalid values with empty `MnNetInfo` If `MnNetInfo` is empty, we should be returning nothing instead of returning a CService's blank data. That behavior is reserved only for `GetPrimary()`. --- src/evo/netinfo.cpp | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/evo/netinfo.cpp b/src/evo/netinfo.cpp index 03a258c4f777..a08f78340281 100644 --- a/src/evo/netinfo.cpp +++ b/src/evo/netinfo.cpp @@ -244,13 +244,18 @@ NetInfoStatus MnNetInfo::Validate() const UniValue MnNetInfo::ToJson() const { + if (IsEmpty()) { + return UniValue{UniValue::VARR}; + } return ArrFromService(GetPrimary()); } std::string MnNetInfo::ToString() const { - // Extra padding to account for padding done by the calling function. - return strprintf("MnNetInfo()\n" - " %s\n", - m_addr.ToString()); + std::string ret{"MnNetInfo()\n"}; + if (!IsEmpty()) { + // Extra padding to account for padding done by the calling function. + ret += strprintf(" %s\n", m_addr.ToString()); + } + return ret; } From ef2fb7b25bd5037b4a3a87b34b963187cc46c481 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Sun, 10 Aug 2025 12:29:20 +0000 Subject: [PATCH 02/27] evo: drop `std::reference_wrapper` usage, make copies instead With extended addresses, the risk of dangling references is present as Clear()'ing it while another thread is iterating through GetEntries() could result in undefined behavior. Let's avoid that by making copies, the data structures aren't expensive enough to risk safety. --- src/evo/deterministicmns.cpp | 53 ++++++++++++++++-------------------- src/evo/netinfo.cpp | 9 +++--- src/evo/netinfo.h | 12 ++++---- src/evo/providertx.cpp | 4 +-- src/evo/specialtxman.cpp | 16 +++++------ src/rpc/masternode.cpp | 2 +- src/txmempool.cpp | 16 +++++------ 7 files changed, 52 insertions(+), 60 deletions(-) diff --git a/src/evo/deterministicmns.cpp b/src/evo/deterministicmns.cpp index 08468ca9c5b9..d4e37b2008c2 100644 --- a/src/evo/deterministicmns.cpp +++ b/src/evo/deterministicmns.cpp @@ -432,13 +432,12 @@ void CDeterministicMNList::AddMN(const CDeterministicMNCPtr& dmn, bool fBumpTota throw(std::runtime_error(strprintf("%s: Can't add a masternode %s with a duplicate collateralOutpoint=%s", __func__, dmn->proTxHash.ToString(), dmn->collateralOutpoint.ToStringShort()))); } - for (const NetInfoEntry& entry : dmn->pdmnState->netInfo->GetEntries()) { - if (const auto& service_opt{entry.GetAddrPort()}; service_opt.has_value()) { - const CService& service{service_opt.value()}; - if (!AddUniqueProperty(*dmn, service)) { + for (const auto& entry : dmn->pdmnState->netInfo->GetEntries()) { + if (const auto service_opt{entry.GetAddrPort()}) { + if (!AddUniqueProperty(*dmn, *service_opt)) { mnUniquePropertyMap = mnUniquePropertyMapSaved; - throw std::runtime_error(strprintf("%s: Can't add a masternode %s with a duplicate address=%s", - __func__, dmn->proTxHash.ToString(), service.ToStringAddrPort())); + throw std::runtime_error(strprintf("%s: Can't add a masternode %s with a duplicate address=%s", __func__, + dmn->proTxHash.ToString(), service_opt->ToStringAddrPort())); } } else { mnUniquePropertyMap = mnUniquePropertyMapSaved; @@ -489,21 +488,19 @@ void CDeterministicMNList::UpdateMN(const CDeterministicMN& oldDmn, const std::s // We track each individual entry in netInfo as opposed to netInfo itself (preventing us from // using UpdateUniqueProperty()), so we need to successfully purge all old entries and insert // new entries to successfully update. - for (const NetInfoEntry& old_entry : oldInfo->GetEntries()) { - if (const auto& service_opt{old_entry.GetAddrPort()}) { - const CService& service{service_opt.value()}; - if (!DeleteUniqueProperty(dmn, service)) { + for (const auto& old_entry : oldInfo->GetEntries()) { + if (const auto service_opt{old_entry.GetAddrPort()}) { + if (!DeleteUniqueProperty(dmn, *service_opt)) { return "internal error"; // This shouldn't be possible } } else { return "invalid address"; } } - for (const NetInfoEntry& new_entry : newInfo->GetEntries()) { - if (const auto& service_opt{new_entry.GetAddrPort()}) { - const CService& service{service_opt.value()}; - if (!AddUniqueProperty(dmn, service)) { - return strprintf("duplicate (%s)", service.ToStringAddrPort()); + for (const auto& new_entry : newInfo->GetEntries()) { + if (const auto service_opt{new_entry.GetAddrPort()}) { + if (!AddUniqueProperty(dmn, *service_opt)) { + return strprintf("duplicate (%s)", service_opt->ToStringAddrPort()); } } else { return "invalid address"; @@ -578,13 +575,12 @@ void CDeterministicMNList::RemoveMN(const uint256& proTxHash) throw(std::runtime_error(strprintf("%s: Can't delete a masternode %s with a collateralOutpoint=%s", __func__, proTxHash.ToString(), dmn->collateralOutpoint.ToStringShort()))); } - for (const NetInfoEntry& entry : dmn->pdmnState->netInfo->GetEntries()) { - if (const auto& service_opt{entry.GetAddrPort()}; service_opt.has_value()) { - const CService& service{service_opt.value()}; - if (!DeleteUniqueProperty(*dmn, service)) { + for (const auto& entry : dmn->pdmnState->netInfo->GetEntries()) { + if (const auto service_opt{entry.GetAddrPort()}) { + if (!DeleteUniqueProperty(*dmn, *service_opt)) { mnUniquePropertyMap = mnUniquePropertyMapSaved; throw std::runtime_error(strprintf("%s: Can't delete a masternode %s with an address=%s", __func__, - proTxHash.ToString(), service.ToStringAddrPort())); + proTxHash.ToString(), service_opt->ToStringAddrPort())); } } else { mnUniquePropertyMap = mnUniquePropertyMapSaved; @@ -1119,11 +1115,10 @@ bool CheckProRegTx(CDeterministicMNManager& dmnman, const CTransaction& tx, gsl: auto mnList = dmnman.GetListForBlock(pindexPrev); // only allow reusing of addresses when it's for the same collateral (which replaces the old MN) - for (const NetInfoEntry& entry : opt_ptx->netInfo->GetEntries()) { - if (const auto& service_opt{entry.GetAddrPort()}; service_opt.has_value()) { - const CService& service{service_opt.value()}; - if (mnList.HasUniqueProperty(service) && - mnList.GetUniquePropertyMN(service)->collateralOutpoint != collateralOutpoint) { + for (const auto& entry : opt_ptx->netInfo->GetEntries()) { + if (const auto service_opt{entry.GetAddrPort()}) { + if (mnList.HasUniqueProperty(*service_opt) && + mnList.GetUniquePropertyMN(*service_opt)->collateralOutpoint != collateralOutpoint) { return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-dup-netinfo-entry"); } } else { @@ -1202,10 +1197,10 @@ bool CheckProUpServTx(CDeterministicMNManager& dmnman, const CTransaction& tx, g } // don't allow updating to addresses already used by other MNs - for (const NetInfoEntry& entry : opt_ptx->netInfo->GetEntries()) { - if (const auto& service_opt{entry.GetAddrPort()}; service_opt.has_value()) { - const CService& service{service_opt.value()}; - if (mnList.HasUniqueProperty(service) && mnList.GetUniquePropertyMN(service)->proTxHash != opt_ptx->proTxHash) { + for (const auto& entry : opt_ptx->netInfo->GetEntries()) { + if (const auto service_opt{entry.GetAddrPort()}) { + if (mnList.HasUniqueProperty(*service_opt) && + mnList.GetUniquePropertyMN(*service_opt)->proTxHash != opt_ptx->proTxHash) { return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-dup-netinfo-entry"); } } else { diff --git a/src/evo/netinfo.cpp b/src/evo/netinfo.cpp index a08f78340281..8a628dfc9347 100644 --- a/src/evo/netinfo.cpp +++ b/src/evo/netinfo.cpp @@ -16,7 +16,6 @@ namespace { static std::unique_ptr g_main_params{nullptr}; static std::once_flag g_main_params_flag; -static const CService empty_service{}; static constexpr std::string_view SAFE_CHARS_IPV4{"1234567890."}; @@ -78,7 +77,7 @@ bool NetInfoEntry::operator<(const NetInfoEntry& rhs) const m_data, rhs.m_data); } -std::optional> NetInfoEntry::GetAddrPort() const +std::optional NetInfoEntry::GetAddrPort() const { if (const auto* data_ptr{std::get_if(&m_data)}; m_type == NetInfoType::Service && data_ptr) { ASSERT_IF_DEBUG(data_ptr->IsValid()); @@ -226,12 +225,12 @@ NetInfoList MnNetInfo::GetEntries() const return {}; } -const CService& MnNetInfo::GetPrimary() const +CService MnNetInfo::GetPrimary() const { - if (const auto& service_opt{m_addr.GetAddrPort()}) { + if (const auto service_opt{m_addr.GetAddrPort()}) { return *service_opt; } - return empty_service; + return CService{}; } NetInfoStatus MnNetInfo::Validate() const diff --git a/src/evo/netinfo.h b/src/evo/netinfo.h index 27f2ce932895..15b6c2d63c75 100644 --- a/src/evo/netinfo.h +++ b/src/evo/netinfo.h @@ -118,7 +118,7 @@ class NetInfoEntry m_data = std::monostate{}; } - std::optional> GetAddrPort() const; + std::optional GetAddrPort() const; uint16_t GetPort() const; bool IsEmpty() const { return *this == NetInfoEntry{}; } bool IsTriviallyValid() const; @@ -129,7 +129,7 @@ class NetInfoEntry template<> struct is_serializable_enum : std::true_type {}; -using NetInfoList = std::vector>; +using NetInfoList = std::vector; class NetInfoInterface { @@ -142,7 +142,7 @@ class NetInfoInterface virtual NetInfoStatus AddEntry(const std::string& service) = 0; virtual NetInfoList GetEntries() const = 0; - virtual const CService& GetPrimary() const = 0; + virtual CService GetPrimary() const = 0; virtual bool CanStorePlatform() const = 0; virtual bool IsEmpty() const = 0; virtual NetInfoStatus Validate() const = 0; @@ -176,8 +176,8 @@ class MnNetInfo final : public NetInfoInterface template void Serialize(Stream& s) const { - if (const auto& service{m_addr.GetAddrPort()}; service.has_value()) { - s << service->get(); + if (const auto service_opt{m_addr.GetAddrPort()}) { + s << *service_opt; } else { s << CService{}; } @@ -199,7 +199,7 @@ class MnNetInfo final : public NetInfoInterface NetInfoStatus AddEntry(const std::string& service) override; NetInfoList GetEntries() const override; - const CService& GetPrimary() const override; + CService GetPrimary() const override; bool IsEmpty() const override { return m_addr.IsEmpty(); } bool CanStorePlatform() const override { return false; } NetInfoStatus Validate() const override; diff --git a/src/evo/providertx.cpp b/src/evo/providertx.cpp index 02b749912143..39c36acb8647 100644 --- a/src/evo/providertx.cpp +++ b/src/evo/providertx.cpp @@ -59,7 +59,7 @@ bool CProRegTx::IsTriviallyValid(gsl::not_null pindexPrev, T if (netInfo->CanStorePlatform() != (nVersion == ProTxVersion::ExtAddr)) { return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-protx-netinfo-version"); } - for (const NetInfoEntry& entry : netInfo->GetEntries()) { + for (const auto& entry : netInfo->GetEntries()) { if (!entry.IsTriviallyValid()) { return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-netinfo-bad"); } @@ -139,7 +139,7 @@ bool CProUpServTx::IsTriviallyValid(gsl::not_null pindexPrev if (netInfo->IsEmpty()) { return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-netinfo-empty"); } - for (const NetInfoEntry& entry : netInfo->GetEntries()) { + for (const auto& entry : netInfo->GetEntries()) { if (!entry.IsTriviallyValid()) { return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-netinfo-bad"); } diff --git a/src/evo/specialtxman.cpp b/src/evo/specialtxman.cpp index c049d13db048..0e8fa765e0de 100644 --- a/src/evo/specialtxman.cpp +++ b/src/evo/specialtxman.cpp @@ -257,10 +257,9 @@ bool CSpecialTxProcessor::BuildNewListFromBlock(const CBlock& block, gsl::not_nu } } - for (const NetInfoEntry& entry : proTx.netInfo->GetEntries()) { - if (const auto& service_opt{entry.GetAddrPort()}; service_opt.has_value()) { - const CService& service{service_opt.value()}; - if (newList.HasUniqueProperty(service)) { + for (const auto& entry : proTx.netInfo->GetEntries()) { + if (const auto service_opt{entry.GetAddrPort()}) { + if (newList.HasUniqueProperty(*service_opt)) { return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "bad-protx-dup-netinfo-entry"); } } else { @@ -293,11 +292,10 @@ bool CSpecialTxProcessor::BuildNewListFromBlock(const CBlock& block, gsl::not_nu return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "bad-protx-payload"); } - for (const NetInfoEntry& entry : opt_proTx->netInfo->GetEntries()) { - if (const auto& service_opt{entry.GetAddrPort()}; service_opt.has_value()) { - const CService& service{service_opt.value()}; - if (newList.HasUniqueProperty(service) && - newList.GetUniquePropertyMN(service)->proTxHash != opt_proTx->proTxHash) { + for (const auto& entry : opt_proTx->netInfo->GetEntries()) { + if (const auto service_opt{entry.GetAddrPort()}) { + if (newList.HasUniqueProperty(*service_opt) && + newList.GetUniquePropertyMN(*service_opt)->proTxHash != opt_proTx->proTxHash) { return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "bad-protx-dup-netinfo-entry"); } } else { diff --git a/src/rpc/masternode.cpp b/src/rpc/masternode.cpp index 7f354a5ecc2b..33b96cfc6723 100644 --- a/src/rpc/masternode.cpp +++ b/src/rpc/masternode.cpp @@ -566,7 +566,7 @@ static RPCHelpMan masternodelist_helper(bool is_composite) std::string strAddress{}; if (strMode == "addr" || strMode == "full" || strMode == "info" || strMode == "json" || strMode == "recent" || strMode == "evo") { - for (const NetInfoEntry& entry : dmn.pdmnState->netInfo->GetEntries()) { + for (const auto& entry : dmn.pdmnState->netInfo->GetEntries()) { strAddress += entry.ToStringAddrPort() + " "; } if (!strAddress.empty()) strAddress.pop_back(); // Remove trailing space diff --git a/src/txmempool.cpp b/src/txmempool.cpp index 32f91fc65c06..e8324f5a6572 100644 --- a/src/txmempool.cpp +++ b/src/txmempool.cpp @@ -651,7 +651,7 @@ void CTxMemPool::addUncheckedProTx(indexed_transaction_set::iterator& newit, con if (!proTx.collateralOutpoint.hash.IsNull()) { mapProTxRefs.emplace(tx_hash, proTx.collateralOutpoint.hash); } - for (const NetInfoEntry& entry : proTx.netInfo->GetEntries()) { + for (const auto& entry : proTx.netInfo->GetEntries()) { mapProTxAddresses.emplace(entry, tx_hash); } mapProTxPubKeyIDs.emplace(proTx.keyIDOwner, tx_hash); @@ -664,7 +664,7 @@ void CTxMemPool::addUncheckedProTx(indexed_transaction_set::iterator& newit, con } else if (tx.nType == TRANSACTION_PROVIDER_UPDATE_SERVICE) { auto proTx = *Assert(GetTxPayload(tx)); mapProTxRefs.emplace(proTx.proTxHash, tx_hash); - for (const NetInfoEntry& entry : proTx.netInfo->GetEntries()) { + for (const auto& entry : proTx.netInfo->GetEntries()) { mapProTxAddresses.emplace(entry, tx_hash); } } else if (tx.nType == TRANSACTION_PROVIDER_UPDATE_REGISTRAR) { @@ -755,7 +755,7 @@ void CTxMemPool::removeUncheckedProTx(const CTransaction& tx) if (!proTx.collateralOutpoint.IsNull()) { eraseProTxRef(tx_hash, proTx.collateralOutpoint.hash); } - for (const NetInfoEntry& entry : proTx.netInfo->GetEntries()) { + for (const auto& entry : proTx.netInfo->GetEntries()) { mapProTxAddresses.erase(entry); } mapProTxPubKeyIDs.erase(proTx.keyIDOwner); @@ -765,7 +765,7 @@ void CTxMemPool::removeUncheckedProTx(const CTransaction& tx) } else if (tx.nType == TRANSACTION_PROVIDER_UPDATE_SERVICE) { auto proTx = *Assert(GetTxPayload(tx)); eraseProTxRef(proTx.proTxHash, tx_hash); - for (const NetInfoEntry& entry : proTx.netInfo->GetEntries()) { + for (const auto& entry : proTx.netInfo->GetEntries()) { mapProTxAddresses.erase(entry); } } else if (tx.nType == TRANSACTION_PROVIDER_UPDATE_REGISTRAR) { @@ -994,7 +994,7 @@ void CTxMemPool::removeProTxConflicts(const CTransaction &tx) } auto& proTx = *opt_proTx; - for (const NetInfoEntry& entry : proTx.netInfo->GetEntries()) { + for (const auto& entry : proTx.netInfo->GetEntries()) { if (mapProTxAddresses.count(entry)) { uint256 conflictHash = mapProTxAddresses[entry]; if (conflictHash != tx_hash && mapTx.count(conflictHash)) { @@ -1016,7 +1016,7 @@ void CTxMemPool::removeProTxConflicts(const CTransaction &tx) return; } - for (const NetInfoEntry& entry : opt_proTx->netInfo->GetEntries()) { + for (const auto& entry : opt_proTx->netInfo->GetEntries()) { if (mapProTxAddresses.count(entry)) { uint256 conflictHash = mapProTxAddresses[entry]; if (conflictHash != tx_hash && mapTx.count(conflictHash)) { @@ -1354,7 +1354,7 @@ bool CTxMemPool::existsProviderTxConflict(const CTransaction &tx) const { return true; // i.e. can't decode payload == conflict } auto& proTx = *opt_proTx; - for (const NetInfoEntry& entry : proTx.netInfo->GetEntries()) { + for (const auto& entry : proTx.netInfo->GetEntries()) { if (mapProTxAddresses.count(entry)) { return true; } @@ -1379,7 +1379,7 @@ bool CTxMemPool::existsProviderTxConflict(const CTransaction &tx) const { LogPrint(BCLog::MEMPOOL, "%s: ERROR: Invalid transaction payload, tx: %s\n", __func__, tx_hash.ToString()); return true; // i.e. can't decode payload == conflict } - for (const NetInfoEntry& entry : opt_proTx->netInfo->GetEntries()) { + for (const auto& entry : opt_proTx->netInfo->GetEntries()) { auto it = mapProTxAddresses.find(entry); if (it != mapProTxAddresses.end() && it->second != opt_proTx->proTxHash) { return true; From e9cac4797b3240ec3a000c2e7cb813f2355c5c66 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Sun, 14 Sep 2025 22:31:07 +0000 Subject: [PATCH 03/27] evo: introduce barebones extended addresses (`ExtNetInfo`) impl --- src/evo/deterministicmns.h | 1 + src/evo/dmnstate.cpp | 7 +- src/evo/netinfo.cpp | 116 +++++++++++++++++++++++++++++++-- src/evo/netinfo.h | 67 +++++++++++++++++-- src/evo/providertx.cpp | 20 +++--- src/evo/simplifiedmns.cpp | 9 ++- src/test/evo_netinfo_tests.cpp | 107 +++++++++++++++++++++++++----- 7 files changed, 280 insertions(+), 47 deletions(-) diff --git a/src/evo/deterministicmns.h b/src/evo/deterministicmns.h index 9c106c5fe941..db6fa318b296 100644 --- a/src/evo/deterministicmns.h +++ b/src/evo/deterministicmns.h @@ -434,6 +434,7 @@ class CDeterministicMNList #define DMNL_NO_TEMPLATE(name) \ static_assert(!std::is_same_v, name>, "GetUniquePropertyHash cannot be templated against " #name) DMNL_NO_TEMPLATE(CBLSPublicKey); + DMNL_NO_TEMPLATE(ExtNetInfo); DMNL_NO_TEMPLATE(MnNetInfo); DMNL_NO_TEMPLATE(NetInfoEntry); DMNL_NO_TEMPLATE(NetInfoInterface); diff --git a/src/evo/dmnstate.cpp b/src/evo/dmnstate.cpp index dae0db688e4e..74f346303f3f 100644 --- a/src/evo/dmnstate.cpp +++ b/src/evo/dmnstate.cpp @@ -22,12 +22,11 @@ std::string CDeterministicMNState::ToString() const return strprintf("CDeterministicMNState(nVersion=%d, nRegisteredHeight=%d, nLastPaidHeight=%d, nPoSePenalty=%d, " "nPoSeRevivedHeight=%d, nPoSeBanHeight=%d, nRevocationReason=%d, " - "ownerAddress=%s, pubKeyOperator=%s, votingAddress=%s, payoutAddress=%s, " - "operatorPayoutAddress=%s)\n" - " %s", + "ownerAddress=%s, pubKeyOperator=%s, votingAddress=%s, netInfo=%s, payoutAddress=%s, " + "operatorPayoutAddress=%s)\n", nVersion, nRegisteredHeight, nLastPaidHeight, nPoSePenalty, nPoSeRevivedHeight, nPoSeBanHeight, nRevocationReason, EncodeDestination(PKHash(keyIDOwner)), pubKeyOperator.ToString(), - EncodeDestination(PKHash(keyIDVoting)), payoutAddress, operatorPayoutAddress, netInfo->ToString()); + EncodeDestination(PKHash(keyIDVoting)), netInfo->ToString(), payoutAddress, operatorPayoutAddress); } UniValue CDeterministicMNState::ToJson(MnType nType) const diff --git a/src/evo/netinfo.cpp b/src/evo/netinfo.cpp index 8a628dfc9347..187438b8705c 100644 --- a/src/evo/netinfo.cpp +++ b/src/evo/netinfo.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -18,6 +19,7 @@ static std::unique_ptr g_main_params{nullptr}; static std::once_flag g_main_params_flag; static constexpr std::string_view SAFE_CHARS_IPV4{"1234567890."}; +static constexpr std::string_view SAFE_CHARS_IPV4_6{"abcdefABCDEF1234567890.:[]"}; bool IsNodeOnMainnet() { return Params().NetworkIDString() == CBaseChainParams::MAIN; } const CChainParams& MainParams() @@ -164,7 +166,10 @@ std::string NetInfoEntry::ToStringAddrPort() const std::shared_ptr NetInfoInterface::MakeNetInfo(const uint16_t nVersion) { - assert(nVersion > 0 && nVersion < ProTxVersion::ExtAddr); + assert(nVersion > 0); + if (nVersion >= ProTxVersion::ExtAddr) { + return std::make_shared(); + } return std::make_shared(); } @@ -251,10 +256,111 @@ UniValue MnNetInfo::ToJson() const std::string MnNetInfo::ToString() const { - std::string ret{"MnNetInfo()\n"}; - if (!IsEmpty()) { - // Extra padding to account for padding done by the calling function. - ret += strprintf(" %s\n", m_addr.ToString()); + return IsEmpty() ? "MnNetInfo()" : strprintf("MnNetInfo([%s])", m_addr.ToString()); +} + +NetInfoStatus ExtNetInfo::ProcessCandidate(const NetInfoEntry& candidate) +{ + assert(candidate.IsTriviallyValid()); + + if (m_data.size() >= MAX_ENTRIES_EXTNETINFO) { + return NetInfoStatus::MaxLimit; + } + m_data.push_back(candidate); + + return NetInfoStatus::Success; +} + +NetInfoStatus ExtNetInfo::ValidateService(const CService& service) +{ + if (!service.IsValid()) { + return NetInfoStatus::BadAddress; + } + if (!service.IsIPv4() && !service.IsIPv6()) { + return NetInfoStatus::BadType; + } + if (Params().RequireRoutableExternalIP() && !service.IsRoutable()) { + return NetInfoStatus::NotRoutable; + } + if (IsBadPort(service.GetPort()) || service.GetPort() == 0) { + return NetInfoStatus::BadPort; + } + + return NetInfoStatus::Success; +} + +NetInfoStatus ExtNetInfo::AddEntry(const std::string& input) +{ + // We don't allow assuming ports, so we set the default value to 0 so that if no port is specified + // it uses a fallback value of 0, which will return a NetInfoStatus::BadPort + std::string addr; + uint16_t port{0}; + SplitHostPort(input, port, addr); + // Contains invalid characters, unlikely to pass Lookup(), fast-fail + if (!MatchCharsFilter(addr, SAFE_CHARS_IPV4_6)) { + return NetInfoStatus::BadInput; + } + + if (auto service_opt{Lookup(addr, /*portDefault=*/port, /*fAllowLookup=*/false)}) { + const auto ret{ValidateService(*service_opt)}; + if (ret == NetInfoStatus::Success) { + return ProcessCandidate(NetInfoEntry{*service_opt}); + } + return ret; /* ValidateService() failed */ + } + return NetInfoStatus::BadInput; /* Lookup() failed */ +} + +NetInfoList ExtNetInfo::GetEntries() const +{ + return m_data; +} + +CService ExtNetInfo::GetPrimary() const +{ + if (m_data.size() >= 1) { + if (const auto& service_opt{m_data[0].GetAddrPort()}) { + return *service_opt; + } + } + return CService{}; +} + +NetInfoStatus ExtNetInfo::Validate() const +{ + if (m_data.empty()) { + return NetInfoStatus::Malformed; + } + for (const auto& entry : m_data) { + if (!entry.IsTriviallyValid()) { + // Trivially invalid NetInfoEntry, no point checking against consensus rules + return NetInfoStatus::Malformed; + } + if (const auto& service_opt{entry.GetAddrPort()}) { + if (auto ret{ValidateService(*service_opt)}; ret != NetInfoStatus::Success) { + // Stores CService underneath but doesn't pass validation rules + return ret; + } + } else { + // Doesn't store valid type underneath + return NetInfoStatus::Malformed; + } + } + return NetInfoStatus::Success; +} + +UniValue ExtNetInfo::ToJson() const +{ + UniValue ret(UniValue::VARR); + for (const auto& entry : m_data) { + ret.push_back(entry.ToStringAddrPort()); } return ret; } + +std::string ExtNetInfo::ToString() const +{ + return IsEmpty() + ? "ExtNetInfo()" + : strprintf("ExtNetInfo([%s])", Join(m_data, ", ", [](const auto& entry) { return entry.ToString(); })); +} diff --git a/src/evo/netinfo.h b/src/evo/netinfo.h index 15b6c2d63c75..53fe912d2228 100644 --- a/src/evo/netinfo.h +++ b/src/evo/netinfo.h @@ -15,6 +15,9 @@ class CService; class UniValue; +/** Maximum entries that can be stored in an ExtNetInfo */ +static constexpr uint8_t MAX_ENTRIES_EXTNETINFO{4}; + enum class NetInfoStatus : uint8_t { // Managing entries BadInput, @@ -219,6 +222,58 @@ class MnNetInfo final : public NetInfoInterface } }; +class ExtNetInfo final : public NetInfoInterface +{ +private: + /** Validate uniqueness requirements and add to object if passed */ + NetInfoStatus ProcessCandidate(const NetInfoEntry& candidate); + + /** Validate CService candidate address against ruleset */ + static NetInfoStatus ValidateService(const CService& service); + +private: + NetInfoList m_data{}; + +public: + ExtNetInfo() = default; + template + ExtNetInfo(deserialize_type, Stream& s) { s >> *this; } + + ~ExtNetInfo() = default; + + SERIALIZE_METHODS(ExtNetInfo, obj) + { + READWRITE(obj.m_data); + } + + NetInfoStatus AddEntry(const std::string& input) override; + NetInfoList GetEntries() const override; + + CService GetPrimary() const override; + bool IsEmpty() const override { return m_data.empty(); } + bool CanStorePlatform() const override + { + // TODO: Store Platform fields, reporting as true as used to differentiate + // with legacy implementation + return true; + } + NetInfoStatus Validate() const override; + UniValue ToJson() const override; + std::string ToString() const override; + + void Clear() override { m_data.clear(); } + +private: + // operator== and operator!= are defined by the parent which then leverage the child's IsEqual() override + // IsEqual() should only be called by NetInfoInterface::operator== otherwise static_cast assumption could fail + bool IsEqual(const NetInfoInterface& rhs) const override + { + ASSERT_IF_DEBUG(typeid(*this) == typeid(rhs)); + const auto& rhs_obj{static_cast(rhs)}; + return m_data == rhs_obj.m_data; + } +}; + class NetInfoSerWrapper { private: @@ -232,8 +287,6 @@ class NetInfoSerWrapper m_data{data}, m_is_extended{is_extended} { - // TODO: Remove when extended addresses implementation is added in - assert(!m_is_extended); } ~NetInfoSerWrapper() = default; @@ -241,7 +294,9 @@ class NetInfoSerWrapper template void Serialize(Stream& s) const { - if (const auto ptr{std::dynamic_pointer_cast(m_data)}) { + if (const auto ptr{std::dynamic_pointer_cast(m_data)}) { + s << *ptr; + } else if (const auto ptr{std::dynamic_pointer_cast(m_data)}) { s << *ptr; } else { // NetInfoInterface::MakeNetInfo() supplied an unexpected implementation or we didn't call it and @@ -253,7 +308,11 @@ class NetInfoSerWrapper template void Unserialize(Stream& s) { - m_data = std::make_shared(deserialize, s); + if (m_is_extended) { + m_data = std::make_shared(deserialize, s); + } else { + m_data = std::make_shared(deserialize, s); + } } }; diff --git a/src/evo/providertx.cpp b/src/evo/providertx.cpp index 39c36acb8647..c6e1fd77c9e0 100644 --- a/src/evo/providertx.cpp +++ b/src/evo/providertx.cpp @@ -115,14 +115,13 @@ std::string CProRegTx::ToString() const payee = EncodeDestination(dest); } - return strprintf("CProRegTx(nVersion=%d, nType=%d, collateralOutpoint=%s, nOperatorReward=%f, " + return strprintf("CProRegTx(nVersion=%d, nType=%d, collateralOutpoint=%s, netInfo=%s, nOperatorReward=%f, " "ownerAddress=%s, pubKeyOperator=%s, votingAddress=%s, scriptPayout=%s, platformNodeID=%s, " - "platformP2PPort=%d, platformHTTPPort=%d)\n" - " %s", - nVersion, ToUnderlying(nType), collateralOutpoint.ToStringShort(), (double)nOperatorReward / 100, - EncodeDestination(PKHash(keyIDOwner)), pubKeyOperator.ToString(), + "platformP2PPort=%d, platformHTTPPort=%d)\n", + nVersion, ToUnderlying(nType), collateralOutpoint.ToStringShort(), netInfo->ToString(), + (double)nOperatorReward / 100, EncodeDestination(PKHash(keyIDOwner)), pubKeyOperator.ToString(), EncodeDestination(PKHash(keyIDVoting)), payee, platformNodeID.ToString(), platformP2PPort, - platformHTTPPort, netInfo->ToString()); + platformHTTPPort); } bool CProUpServTx::IsTriviallyValid(gsl::not_null pindexPrev, TxValidationState& state) const @@ -156,11 +155,10 @@ std::string CProUpServTx::ToString() const payee = EncodeDestination(dest); } - return strprintf("CProUpServTx(nVersion=%d, nType=%d, proTxHash=%s, operatorPayoutAddress=%s, " - "platformNodeID=%s, platformP2PPort=%d, platformHTTPPort=%d)\n" - " %s", - nVersion, ToUnderlying(nType), proTxHash.ToString(), payee, platformNodeID.ToString(), - platformP2PPort, platformHTTPPort, netInfo->ToString()); + return strprintf("CProUpServTx(nVersion=%d, nType=%d, proTxHash=%s, netInfo=%s, operatorPayoutAddress=%s, " + "platformNodeID=%s, platformP2PPort=%d, platformHTTPPort=%d)\n", + nVersion, ToUnderlying(nType), proTxHash.ToString(), netInfo->ToString(), payee, + platformNodeID.ToString(), platformP2PPort, platformHTTPPort); } bool CProUpRegTx::IsTriviallyValid(gsl::not_null pindexPrev, TxValidationState& state) const diff --git a/src/evo/simplifiedmns.cpp b/src/evo/simplifiedmns.cpp index b84f2e98bf00..6daec5f0f14e 100644 --- a/src/evo/simplifiedmns.cpp +++ b/src/evo/simplifiedmns.cpp @@ -56,13 +56,12 @@ std::string CSimplifiedMNListEntry::ToString() const operatorPayoutAddress = EncodeDestination(dest); } - return strprintf("CSimplifiedMNListEntry(nVersion=%d, nType=%d, proRegTxHash=%s, confirmedHash=%s, " + return strprintf("CSimplifiedMNListEntry(nVersion=%d, nType=%d, proRegTxHash=%s, confirmedHash=%s, netInfo=%s, " "pubKeyOperator=%s, votingAddress=%s, isValid=%d, payoutAddress=%s, operatorPayoutAddress=%s, " - "platformHTTPPort=%d, platformNodeID=%s)\n" - " %s", + "platformHTTPPort=%d, platformNodeID=%s)\n", nVersion, ToUnderlying(nType), proRegTxHash.ToString(), confirmedHash.ToString(), - pubKeyOperator.ToString(), EncodeDestination(PKHash(keyIDVoting)), isValid, payoutAddress, - operatorPayoutAddress, platformHTTPPort, platformNodeID.ToString(), netInfo->ToString()); + netInfo->ToString(), pubKeyOperator.ToString(), EncodeDestination(PKHash(keyIDVoting)), isValid, + payoutAddress, operatorPayoutAddress, platformHTTPPort, platformNodeID.ToString()); } CSimplifiedMNList::CSimplifiedMNList(std::vector>&& smlEntries) diff --git a/src/test/evo_netinfo_tests.cpp b/src/test/evo_netinfo_tests.cpp index 50138f5c0818..f8ee87bc0e80 100644 --- a/src/test/evo_netinfo_tests.cpp +++ b/src/test/evo_netinfo_tests.cpp @@ -15,27 +15,33 @@ BOOST_FIXTURE_TEST_SUITE(evo_netinfo_tests, BasicTestingSetup) -const std::vector> vals{ +using TestVectors = + std::vector>; + +static const TestVectors vals_main{ // Address and port specified - {"1.1.1.1:9999", NetInfoStatus::Success}, - // Address specified, port should default to default P2P core - {"1.1.1.1", NetInfoStatus::Success}, - // Non-mainnet port on mainnet - {"1.1.1.1:9998", NetInfoStatus::BadPort}, + {"1.1.1.1:9999", NetInfoStatus::Success, NetInfoStatus::Success}, + // - Port should default to default P2P core with MnNetInfo + // - Ports are no longer implied with ExtNetInfo + {"1.1.1.1", NetInfoStatus::Success, NetInfoStatus::BadPort}, + // - Non-mainnet port on mainnet causes failure in MnNetInfo + // - ExtNetInfo is indifferent to choice of port unless it's a bad port which 9998 isn't + {"1.1.1.1:9998", NetInfoStatus::BadPort, NetInfoStatus::Success}, // Internal addresses not allowed on mainnet - {"127.0.0.1:9999", NetInfoStatus::NotRoutable}, + {"127.0.0.1:9999", NetInfoStatus::NotRoutable, NetInfoStatus::NotRoutable}, // Valid IPv4 formatting but invalid IPv4 address - {"0.0.0.0:9999", NetInfoStatus::BadAddress}, + {"0.0.0.0:9999", NetInfoStatus::BadAddress, NetInfoStatus::BadAddress}, // Port greater than uint16_t max - {"1.1.1.1:99999", NetInfoStatus::BadInput}, - // Only IPv4 allowed - {"[2606:4700:4700::1111]:9999", NetInfoStatus::BadInput}, + {"1.1.1.1:99999", NetInfoStatus::BadInput, NetInfoStatus::BadInput}, + // - Non-IPv4 addresses are prohibited in MnNetInfo + // - Any valid BIP155 address is allowed in ExtNetInfo + {"[2606:4700:4700::1111]:9999", NetInfoStatus::BadInput, NetInfoStatus::Success}, // Domains are not allowed - {"example.com:9999", NetInfoStatus::BadInput}, + {"example.com:9999", NetInfoStatus::BadInput, NetInfoStatus::BadInput}, // Incorrect IPv4 address - {"1.1.1.256:9999", NetInfoStatus::BadInput}, + {"1.1.1.256:9999", NetInfoStatus::BadInput, NetInfoStatus::BadInput}, // Missing address - {":9999", NetInfoStatus::BadInput}, + {":9999", NetInfoStatus::BadInput, NetInfoStatus::BadInput}, }; void ValidateGetEntries(const NetInfoList& entries, const size_t expected_size) @@ -46,10 +52,9 @@ void ValidateGetEntries(const NetInfoList& entries, const size_t expected_size) } } -BOOST_AUTO_TEST_CASE(mnnetinfo_rules) +void TestMnNetInfo(const TestVectors& vals) { - // Validate AddEntry() rules enforcement - for (const auto& [input, expected_ret] : vals) { + for (const auto& [input, expected_ret, _] : vals) { MnNetInfo netInfo; BOOST_CHECK_EQUAL(netInfo.AddEntry(input), expected_ret); if (expected_ret != NetInfoStatus::Success) { @@ -61,6 +66,27 @@ BOOST_AUTO_TEST_CASE(mnnetinfo_rules) ValidateGetEntries(netInfo.GetEntries(), /*expected_size=*/1); } } +} + +void TestExtNetInfo(const TestVectors& vals) +{ + for (const auto& [input, _, expected_ret] : vals) { + ExtNetInfo netInfo; + BOOST_CHECK_EQUAL(netInfo.AddEntry(input), expected_ret); + if (expected_ret != NetInfoStatus::Success) { + // An empty ExtNetInfo is considered malformed + BOOST_CHECK_EQUAL(netInfo.Validate(), NetInfoStatus::Malformed); + BOOST_CHECK(netInfo.GetEntries().empty()); + } else { + BOOST_CHECK_EQUAL(netInfo.Validate(), NetInfoStatus::Success); + ValidateGetEntries(netInfo.GetEntries(), /*expected_size=*/1); + } + } +} + +BOOST_AUTO_TEST_CASE(mnnetinfo_rules_main) +{ + TestMnNetInfo(vals_main); { // MnNetInfo only stores one value, overwriting prohibited @@ -71,6 +97,37 @@ BOOST_AUTO_TEST_CASE(mnnetinfo_rules) } } +BOOST_AUTO_TEST_CASE(extnetinfo_rules_main) { TestExtNetInfo(vals_main); } + +static const TestVectors vals_reg{ + // - MnNetInfo doesn't mind using port 0 + // - ExtNetInfo requires non-zero ports + {"1.1.1.1:0", NetInfoStatus::Success, NetInfoStatus::BadPort}, + // - Mainnet P2P port on non-mainnet cause failure in MnNetInfo + // - ExtNetInfo is indifferent to choice of port unless it's a bad port which 9999 isn't + {"1.1.1.1:9999", NetInfoStatus::BadPort, NetInfoStatus::Success}, + // - Non-mainnet P2P port is allowed in MnNetInfo regardless of bad port status + // - Port 22 (SSH) is below the privileged ports threshold (1023) and is therefore a bad port, disallowed in ExtNetInfo + {"1.1.1.1:22", NetInfoStatus::Success, NetInfoStatus::BadPort}, +}; + +BOOST_FIXTURE_TEST_CASE(mnnetinfo_rules_reg, RegTestingSetup) { TestMnNetInfo(vals_reg); } + +BOOST_FIXTURE_TEST_CASE(extnetinfo_rules_reg, RegTestingSetup) +{ + TestExtNetInfo(vals_reg); + + { + // ExtNetInfo can store up to 4 entries, check limit enforcement + ExtNetInfo netInfo; + for (size_t idx{1}; idx <= MAX_ENTRIES_EXTNETINFO; idx++) { + BOOST_CHECK_EQUAL(netInfo.AddEntry(strprintf("1.1.1.%d:9998", idx)), NetInfoStatus::Success); + } + BOOST_CHECK_EQUAL(netInfo.AddEntry("1.1.1.5:9998"), NetInfoStatus::MaxLimit); + ValidateGetEntries(netInfo.GetEntries(), /*expected_size=*/MAX_ENTRIES_EXTNETINFO); + } +} + BOOST_AUTO_TEST_CASE(netinfo_ser) { { @@ -217,7 +274,7 @@ BOOST_AUTO_TEST_CASE(cservice_compatible) BOOST_AUTO_TEST_CASE(interface_equality) { - // We also check for symmetry as NetInfoInterface, MnNetInfo and NetInfoEntry + // We also check for symmetry as NetInfoInterface, ExtNetInfo, MnNetInfo and NetInfoEntry // define their operator!= as the inverse of operator== std::shared_ptr ptr_lhs{nullptr}, ptr_rhs{nullptr}; @@ -236,6 +293,20 @@ BOOST_AUTO_TEST_CASE(interface_equality) // Equal initialization state, same type, differing values BOOST_CHECK_EQUAL(ptr_rhs->AddEntry("1.1.1.1:9999"), NetInfoStatus::Success); BOOST_CHECK(!util::shared_ptr_equal(ptr_lhs, ptr_rhs) && util::shared_ptr_not_equal(ptr_lhs, ptr_rhs)); + + // Equal initialization state, different type, same values + ptr_rhs = std::make_shared(); + BOOST_CHECK(ptr_lhs->IsEmpty() && ptr_rhs->IsEmpty()); + BOOST_CHECK(!util::shared_ptr_equal(ptr_lhs, ptr_rhs) && util::shared_ptr_not_equal(ptr_lhs, ptr_rhs)); + + // Equal initialization state, same type, same values + ptr_lhs = std::make_shared(); + BOOST_CHECK(ptr_lhs->IsEmpty() && ptr_rhs->IsEmpty()); + BOOST_CHECK(util::shared_ptr_equal(ptr_lhs, ptr_rhs) && !util::shared_ptr_not_equal(ptr_lhs, ptr_rhs)); + + // Equal initialization state, same type, differing values + BOOST_CHECK_EQUAL(ptr_rhs->AddEntry("1.1.1.1:9999"), NetInfoStatus::Success); + BOOST_CHECK(!util::shared_ptr_equal(ptr_lhs, ptr_rhs) && util::shared_ptr_not_equal(ptr_lhs, ptr_rhs)); } BOOST_AUTO_TEST_SUITE_END() From 56b1bb65d6f4f7f4c0baa6878ef6433fd153a392 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Thu, 10 Apr 2025 10:39:15 +0000 Subject: [PATCH 04/27] evo: introduce versioning for `ExtNetInfo` --- src/evo/netinfo.cpp | 2 +- src/evo/netinfo.h | 18 +++++++++++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/evo/netinfo.cpp b/src/evo/netinfo.cpp index 187438b8705c..4824ea0c84fe 100644 --- a/src/evo/netinfo.cpp +++ b/src/evo/netinfo.cpp @@ -328,7 +328,7 @@ CService ExtNetInfo::GetPrimary() const NetInfoStatus ExtNetInfo::Validate() const { - if (m_data.empty()) { + if (m_version == 0 || m_version > CURRENT_VERSION || m_data.empty()) { return NetInfoStatus::Malformed; } for (const auto& entry : m_data) { diff --git a/src/evo/netinfo.h b/src/evo/netinfo.h index 53fe912d2228..2f2da8538136 100644 --- a/src/evo/netinfo.h +++ b/src/evo/netinfo.h @@ -225,6 +225,9 @@ class MnNetInfo final : public NetInfoInterface class ExtNetInfo final : public NetInfoInterface { private: + /** Update if serialization or ruleset changed */ + static constexpr uint8_t CURRENT_VERSION{1}; + /** Validate uniqueness requirements and add to object if passed */ NetInfoStatus ProcessCandidate(const NetInfoEntry& candidate); @@ -232,6 +235,7 @@ class ExtNetInfo final : public NetInfoInterface static NetInfoStatus ValidateService(const CService& service); private: + uint8_t m_version{CURRENT_VERSION}; NetInfoList m_data{}; public: @@ -243,6 +247,10 @@ class ExtNetInfo final : public NetInfoInterface SERIALIZE_METHODS(ExtNetInfo, obj) { + READWRITE(obj.m_version); + if (obj.m_version == 0 || obj.m_version > CURRENT_VERSION) { + return; // Don't bother with unknown versions + } READWRITE(obj.m_data); } @@ -250,7 +258,7 @@ class ExtNetInfo final : public NetInfoInterface NetInfoList GetEntries() const override; CService GetPrimary() const override; - bool IsEmpty() const override { return m_data.empty(); } + bool IsEmpty() const override { return m_version == CURRENT_VERSION && m_data.empty(); } bool CanStorePlatform() const override { // TODO: Store Platform fields, reporting as true as used to differentiate @@ -261,7 +269,11 @@ class ExtNetInfo final : public NetInfoInterface UniValue ToJson() const override; std::string ToString() const override; - void Clear() override { m_data.clear(); } + void Clear() override + { + m_version = CURRENT_VERSION; + m_data.clear(); + } private: // operator== and operator!= are defined by the parent which then leverage the child's IsEqual() override @@ -270,7 +282,7 @@ class ExtNetInfo final : public NetInfoInterface { ASSERT_IF_DEBUG(typeid(*this) == typeid(rhs)); const auto& rhs_obj{static_cast(rhs)}; - return m_data == rhs_obj.m_data; + return m_version == rhs_obj.m_version && m_data == rhs_obj.m_data; } }; From a35d9c6b1963b559ef38dd4a85019f26cc98557b Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Sun, 14 Sep 2025 15:07:26 +0000 Subject: [PATCH 05/27] evo: prohibit entries with duplicate addresses in `ExtNetInfo` MnNetInfo cannot store duplicate entries as it can store only one entry, so it cannot emit NetInfoStatus::Duplicate. --- src/evo/deterministicmns.cpp | 1 + src/evo/netinfo.cpp | 25 +++++++++++++++++++++++++ src/evo/netinfo.h | 9 +++++++++ src/test/evo_netinfo_tests.cpp | 10 ++++++++++ 4 files changed, 45 insertions(+) diff --git a/src/evo/deterministicmns.cpp b/src/evo/deterministicmns.cpp index d4e37b2008c2..c43da27d9a10 100644 --- a/src/evo/deterministicmns.cpp +++ b/src/evo/deterministicmns.cpp @@ -910,6 +910,7 @@ static bool CheckService(const ProTx& proTx, TxValidationState& state) return true; // Shouldn't be possible during self-checks case NetInfoStatus::BadInput: + case NetInfoStatus::Duplicate: case NetInfoStatus::MaxLimit: assert(false); } // no default case, so the compiler can warn about missing cases diff --git a/src/evo/netinfo.cpp b/src/evo/netinfo.cpp index 4824ea0c84fe..c30d19d613e7 100644 --- a/src/evo/netinfo.cpp +++ b/src/evo/netinfo.cpp @@ -259,6 +259,25 @@ std::string MnNetInfo::ToString() const return IsEmpty() ? "MnNetInfo()" : strprintf("MnNetInfo([%s])", m_addr.ToString()); } +bool ExtNetInfo::HasDuplicates() const +{ + std::unordered_set known{}; + for (const NetInfoEntry& entry : m_data) { + if (auto [_, inserted] = known.insert(entry.ToStringAddr()); !inserted) { + return true; + } + } + ASSERT_IF_DEBUG(known.size() == m_data.size()); + return false; +} + +bool ExtNetInfo::IsDuplicateCandidate(const NetInfoEntry& candidate) const +{ + const std::string& candidate_str{candidate.ToStringAddr()}; + return std::any_of(m_data.begin(), m_data.end(), + [&candidate_str](const auto& entry) { return candidate_str == entry.ToStringAddr(); }); +} + NetInfoStatus ExtNetInfo::ProcessCandidate(const NetInfoEntry& candidate) { assert(candidate.IsTriviallyValid()); @@ -266,6 +285,9 @@ NetInfoStatus ExtNetInfo::ProcessCandidate(const NetInfoEntry& candidate) if (m_data.size() >= MAX_ENTRIES_EXTNETINFO) { return NetInfoStatus::MaxLimit; } + if (IsDuplicateCandidate(candidate)) { + return NetInfoStatus::Duplicate; + } m_data.push_back(candidate); return NetInfoStatus::Success; @@ -331,6 +353,9 @@ NetInfoStatus ExtNetInfo::Validate() const if (m_version == 0 || m_version > CURRENT_VERSION || m_data.empty()) { return NetInfoStatus::Malformed; } + if (HasDuplicates()) { + return NetInfoStatus::Duplicate; + } for (const auto& entry : m_data) { if (!entry.IsTriviallyValid()) { // Trivially invalid NetInfoEntry, no point checking against consensus rules diff --git a/src/evo/netinfo.h b/src/evo/netinfo.h index 2f2da8538136..9ce32f6ac879 100644 --- a/src/evo/netinfo.h +++ b/src/evo/netinfo.h @@ -21,6 +21,7 @@ static constexpr uint8_t MAX_ENTRIES_EXTNETINFO{4}; enum class NetInfoStatus : uint8_t { // Managing entries BadInput, + Duplicate, MaxLimit, // Validation @@ -44,6 +45,8 @@ constexpr std::string_view NISToString(const NetInfoStatus code) return "invalid port"; case NetInfoStatus::BadType: return "invalid address type"; + case NetInfoStatus::Duplicate: + return "duplicate"; case NetInfoStatus::NotRoutable: return "unroutable address"; case NetInfoStatus::Malformed: @@ -228,6 +231,12 @@ class ExtNetInfo final : public NetInfoInterface /** Update if serialization or ruleset changed */ static constexpr uint8_t CURRENT_VERSION{1}; + /** Returns true if there are addr:port duplicates in the object */ + bool HasDuplicates() const; + + /** Returns true if candidate is an addr:port duplicate in the object */ + bool IsDuplicateCandidate(const NetInfoEntry& candidate) const; + /** Validate uniqueness requirements and add to object if passed */ NetInfoStatus ProcessCandidate(const NetInfoEntry& candidate); diff --git a/src/test/evo_netinfo_tests.cpp b/src/test/evo_netinfo_tests.cpp index f8ee87bc0e80..eb0583c94ede 100644 --- a/src/test/evo_netinfo_tests.cpp +++ b/src/test/evo_netinfo_tests.cpp @@ -126,6 +126,16 @@ BOOST_FIXTURE_TEST_CASE(extnetinfo_rules_reg, RegTestingSetup) BOOST_CHECK_EQUAL(netInfo.AddEntry("1.1.1.5:9998"), NetInfoStatus::MaxLimit); ValidateGetEntries(netInfo.GetEntries(), /*expected_size=*/MAX_ENTRIES_EXTNETINFO); } + + { + // ExtNetInfo does not allow storing duplicates + ExtNetInfo netInfo; + BOOST_CHECK_EQUAL(netInfo.AddEntry("1.1.1.1:9998"), NetInfoStatus::Success); + // Exact duplicates are prohibited + BOOST_CHECK_EQUAL(netInfo.AddEntry("1.1.1.1:9998"), NetInfoStatus::Duplicate); + // Partial duplicates (same address, different port) are also prohibited + BOOST_CHECK_EQUAL(netInfo.AddEntry("1.1.1.1:9997"), NetInfoStatus::Duplicate); + } } BOOST_AUTO_TEST_CASE(netinfo_ser) From 4ca6542e563bf3adc6b84228e625853bf8f378cb Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Tue, 19 Aug 2025 19:00:50 +0000 Subject: [PATCH 06/27] evo: introduce the ability to store multiple lists of addresses --- contrib/seeds/makeseeds.py | 2 +- src/evo/deterministicmns.cpp | 3 + src/evo/netinfo.cpp | 127 +++++++++++++----- src/evo/netinfo.h | 72 ++++++++-- src/evo/providertx.cpp | 5 +- src/rpc/coinjoin.cpp | 10 +- src/rpc/evo_util.cpp | 4 +- src/test/block_reward_reallocation_tests.cpp | 3 +- src/test/evo_deterministicmns_tests.cpp | 14 +- src/test/evo_netinfo_tests.cpp | 84 +++++++----- src/test/evo_simplifiedmns_tests.cpp | 3 +- .../feature_dip3_deterministicmns.py | 4 +- test/functional/rpc_netinfo.py | 2 +- test/functional/rpc_quorum.py | 2 +- 14 files changed, 234 insertions(+), 101 deletions(-) diff --git a/contrib/seeds/makeseeds.py b/contrib/seeds/makeseeds.py index 72dc8fb6a3c9..b00087164b52 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']['addresses'][0]) for mn in mns] + ips = [parseip(mn['state']['addresses']['core_p2p'][0]) for mn in mns] for onion in onions: parsed = parseip(onion) if parsed is not None: diff --git a/src/evo/deterministicmns.cpp b/src/evo/deterministicmns.cpp index c43da27d9a10..22ae195cab33 100644 --- a/src/evo/deterministicmns.cpp +++ b/src/evo/deterministicmns.cpp @@ -895,6 +895,9 @@ void CDeterministicMNManager::CleanupCache(int nHeight) template static bool CheckService(const ProTx& proTx, TxValidationState& state) { + if (!proTx.netInfo->HasEntries(NetInfoPurpose::CORE_P2P)) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-netinfo-empty"); + } switch (proTx.netInfo->Validate()) { case NetInfoStatus::BadAddress: return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-netinfo-addr"); diff --git a/src/evo/netinfo.cpp b/src/evo/netinfo.cpp index c30d19d613e7..7664d330372c 100644 --- a/src/evo/netinfo.cpp +++ b/src/evo/netinfo.cpp @@ -194,9 +194,9 @@ NetInfoStatus MnNetInfo::ValidateService(const CService& service) return NetInfoStatus::Success; } -NetInfoStatus MnNetInfo::AddEntry(const std::string& input) +NetInfoStatus MnNetInfo::AddEntry(const NetInfoPurpose purpose, const std::string& input) { - if (!IsEmpty()) { + if (purpose != NetInfoPurpose::CORE_P2P || !IsEmpty()) { return NetInfoStatus::MaxLimit; } @@ -248,48 +248,61 @@ NetInfoStatus MnNetInfo::Validate() const UniValue MnNetInfo::ToJson() const { - if (IsEmpty()) { - return UniValue{UniValue::VARR}; + UniValue ret{UniValue::VOBJ}; + if (!IsEmpty()) { + ret.pushKV(PurposeToString(NetInfoPurpose::CORE_P2P).data(), ArrFromService(GetPrimary())); } - return ArrFromService(GetPrimary()); + return ret; } std::string MnNetInfo::ToString() const { - return IsEmpty() ? "MnNetInfo()" : strprintf("MnNetInfo([%s])", m_addr.ToString()); + return IsEmpty() ? "MnNetInfo()" + : strprintf("MnNetInfo(NetInfo(purpose=%s, [%s]))", PurposeToString(NetInfoPurpose::CORE_P2P), + m_addr.ToString()); } bool ExtNetInfo::HasDuplicates() const { std::unordered_set known{}; - for (const NetInfoEntry& entry : m_data) { + for (const auto& entry : m_all_entries) { if (auto [_, inserted] = known.insert(entry.ToStringAddr()); !inserted) { return true; } } - ASSERT_IF_DEBUG(known.size() == m_data.size()); + ASSERT_IF_DEBUG(known.size() == m_all_entries.size()); return false; } bool ExtNetInfo::IsDuplicateCandidate(const NetInfoEntry& candidate) const { const std::string& candidate_str{candidate.ToStringAddr()}; - return std::any_of(m_data.begin(), m_data.end(), + return std::any_of(m_all_entries.begin(), m_all_entries.end(), [&candidate_str](const auto& entry) { return candidate_str == entry.ToStringAddr(); }); } -NetInfoStatus ExtNetInfo::ProcessCandidate(const NetInfoEntry& candidate) +NetInfoStatus ExtNetInfo::ProcessCandidate(const NetInfoPurpose purpose, const NetInfoEntry& candidate) { assert(candidate.IsTriviallyValid()); - if (m_data.size() >= MAX_ENTRIES_EXTNETINFO) { - return NetInfoStatus::MaxLimit; - } if (IsDuplicateCandidate(candidate)) { return NetInfoStatus::Duplicate; } - m_data.push_back(candidate); + if (auto it{m_data.find(purpose)}; it != m_data.end()) { + // Existing entries list found, check limit + auto& [_, entries] = *it; + if (entries.size() >= MAX_ENTRIES_EXTNETINFO) { + return NetInfoStatus::MaxLimit; + } + entries.push_back(candidate); + } else { + // First entry for purpose code, create new entries list + auto [_, status] = m_data.try_emplace(purpose, std::vector({candidate})); + assert(status); // We did just check to see if our value already existed, try_emplace shouldn't fail + } + // Candidate succesfully added, update cache + m_all_entries.push_back(candidate); return NetInfoStatus::Success; } @@ -311,8 +324,12 @@ NetInfoStatus ExtNetInfo::ValidateService(const CService& service) return NetInfoStatus::Success; } -NetInfoStatus ExtNetInfo::AddEntry(const std::string& input) +NetInfoStatus ExtNetInfo::AddEntry(const NetInfoPurpose purpose, const std::string& input) { + if (!IsValidPurpose(purpose)) { + return NetInfoStatus::MaxLimit; + } + // We don't allow assuming ports, so we set the default value to 0 so that if no port is specified // it uses a fallback value of 0, which will return a NetInfoStatus::BadPort std::string addr; @@ -326,7 +343,7 @@ NetInfoStatus ExtNetInfo::AddEntry(const std::string& input) if (auto service_opt{Lookup(addr, /*portDefault=*/port, /*fAllowLookup=*/false)}) { const auto ret{ValidateService(*service_opt)}; if (ret == NetInfoStatus::Success) { - return ProcessCandidate(NetInfoEntry{*service_opt}); + return ProcessCandidate(purpose, NetInfoEntry{*service_opt}); } return ret; /* ValidateService() failed */ } @@ -335,19 +352,33 @@ NetInfoStatus ExtNetInfo::AddEntry(const std::string& input) NetInfoList ExtNetInfo::GetEntries() const { - return m_data; + // ExtNetInfo is an append-only structure, we can avoid re-calculating + // a list of all entries by maintaining a small cache. + return m_all_entries; } CService ExtNetInfo::GetPrimary() const { - if (m_data.size() >= 1) { - if (const auto& service_opt{m_data[0].GetAddrPort()}) { - return *service_opt; + if (const auto& it{m_data.find(NetInfoPurpose::CORE_P2P)}; it != m_data.end()) { + const auto& [_, entries] = *it; + // If a purpose code is in the map, there should be at least one entry + ASSERT_IF_DEBUG(!entries.empty()); + if (entries.size() >= 1) { + if (const auto& service_opt{entries[0].GetAddrPort()}) { + return *service_opt; + } } } return CService{}; } +bool ExtNetInfo::HasEntries(NetInfoPurpose purpose) const +{ + if (!IsValidPurpose(purpose)) return false; + const auto& it{m_data.find(purpose)}; + return it != m_data.end() && !it->second.empty(); +} + NetInfoStatus ExtNetInfo::Validate() const { if (m_version == 0 || m_version > CURRENT_VERSION || m_data.empty()) { @@ -356,36 +387,60 @@ NetInfoStatus ExtNetInfo::Validate() const if (HasDuplicates()) { return NetInfoStatus::Duplicate; } - for (const auto& entry : m_data) { - if (!entry.IsTriviallyValid()) { - // Trivially invalid NetInfoEntry, no point checking against consensus rules + for (const auto& [purpose, entries] : m_data) { + if (!IsValidPurpose(purpose)) { return NetInfoStatus::Malformed; } - if (const auto& service_opt{entry.GetAddrPort()}) { - if (auto ret{ValidateService(*service_opt)}; ret != NetInfoStatus::Success) { - // Stores CService underneath but doesn't pass validation rules - return ret; - } - } else { - // Doesn't store valid type underneath + if (entries.empty()) { + // Purpose if present in map must have at least one entry return NetInfoStatus::Malformed; } + for (const auto& entry : entries) { + if (!entry.IsTriviallyValid()) { + // Trivially invalid NetInfoEntry, no point checking against consensus rules + return NetInfoStatus::Malformed; + } + if (const auto& service_opt{entry.GetAddrPort()}) { + if (auto ret{ValidateService(*service_opt)}; ret != NetInfoStatus::Success) { + // Stores CService underneath but doesn't pass validation rules + return ret; + } + } else { + // Doesn't store valid type underneath + return NetInfoStatus::Malformed; + } + } } return NetInfoStatus::Success; } UniValue ExtNetInfo::ToJson() const { - UniValue ret(UniValue::VARR); - for (const auto& entry : m_data) { - ret.push_back(entry.ToStringAddrPort()); + UniValue ret(UniValue::VOBJ); + for (const auto& [purpose, entries] : m_data) { + UniValue arr(UniValue::VARR); + for (const auto& entry : entries) { + arr.push_back(entry.ToStringAddrPort()); + } + ret.pushKV(PurposeToString(purpose).data(), arr); } return ret; } std::string ExtNetInfo::ToString() const { - return IsEmpty() - ? "ExtNetInfo()" - : strprintf("ExtNetInfo([%s])", Join(m_data, ", ", [](const auto& entry) { return entry.ToString(); })); + return IsEmpty() ? "ExtNetInfo()" : strprintf("ExtNetInfo(%s)", [&]() -> std::string { + std::string ret{}; + bool first{true}; + for (const auto& [purpose, entries] : m_data) { + if (!first) { ret += ", "; } else { first = false; } + ret += strprintf("NetInfo(purpose=%s, [%s])", PurposeToString(purpose), [&]() -> std::string { + if (entries.empty()) { + return "invalid list"; + } + return Join(entries, ", ", [](const auto& entry) { return entry.ToString(); }); + }()); + } + return ret; + }()); } diff --git a/src/evo/netinfo.h b/src/evo/netinfo.h index 9ce32f6ac879..a4c8d35f0792 100644 --- a/src/evo/netinfo.h +++ b/src/evo/netinfo.h @@ -15,7 +15,7 @@ class CService; class UniValue; -/** Maximum entries that can be stored in an ExtNetInfo */ +/** Maximum entries that can be stored in an ExtNetInfo per purpose code */ static constexpr uint8_t MAX_ENTRIES_EXTNETINFO{4}; enum class NetInfoStatus : uint8_t { @@ -59,6 +59,34 @@ constexpr std::string_view NISToString(const NetInfoStatus code) assert(false); } +// A purpose corresponds to the index position in the ExtNetInfo map, entries must +// be contiguous and cannot be changed once set without a format version update +enum class NetInfoPurpose : uint8_t { + // Mandatory for masternodes + CORE_P2P = 0, +}; + +template<> struct is_serializable_enum : std::true_type {}; + +constexpr bool IsValidPurpose(const NetInfoPurpose purpose) +{ + switch (purpose) { + case NetInfoPurpose::CORE_P2P: + return true; + } // no default case, so the compiler can warn about missing cases + return false; +} + +// Warning: Used in RPC code, altering existing values is a breaking change +constexpr std::string_view PurposeToString(const NetInfoPurpose purpose) +{ + switch (purpose) { + case NetInfoPurpose::CORE_P2P: + return "core_p2p"; + } // no default case, so the compiler can warn about missing cases + return ""; +} + /* Identical to IsDeprecatedRPCEnabled("service"). For use outside of RPC code. */ bool IsServiceDeprecatedRPCEnabled(); @@ -145,11 +173,12 @@ class NetInfoInterface public: virtual ~NetInfoInterface() = default; - virtual NetInfoStatus AddEntry(const std::string& service) = 0; + virtual NetInfoStatus AddEntry(const NetInfoPurpose purpose, const std::string& service) = 0; virtual NetInfoList GetEntries() const = 0; virtual CService GetPrimary() const = 0; virtual bool CanStorePlatform() const = 0; + virtual bool HasEntries(NetInfoPurpose purpose) const = 0; virtual bool IsEmpty() const = 0; virtual NetInfoStatus Validate() const = 0; virtual UniValue ToJson() const = 0; @@ -202,10 +231,11 @@ class MnNetInfo final : public NetInfoInterface m_addr = NetInfoEntry{service}; } - NetInfoStatus AddEntry(const std::string& service) override; + NetInfoStatus AddEntry(const NetInfoPurpose purpose, const std::string& service) override; NetInfoList GetEntries() const override; CService GetPrimary() const override; + bool HasEntries(NetInfoPurpose purpose) const override { return purpose == NetInfoPurpose::CORE_P2P && !IsEmpty(); } bool IsEmpty() const override { return m_addr.IsEmpty(); } bool CanStorePlatform() const override { return false; } NetInfoStatus Validate() const override; @@ -238,14 +268,17 @@ class ExtNetInfo final : public NetInfoInterface bool IsDuplicateCandidate(const NetInfoEntry& candidate) const; /** Validate uniqueness requirements and add to object if passed */ - NetInfoStatus ProcessCandidate(const NetInfoEntry& candidate); + NetInfoStatus ProcessCandidate(const NetInfoPurpose purpose, const NetInfoEntry& candidate); /** Validate CService candidate address against ruleset */ static NetInfoStatus ValidateService(const CService& service); private: uint8_t m_version{CURRENT_VERSION}; - NetInfoList m_data{}; + std::map m_data{}; + + // memory only + NetInfoList m_all_entries{}; public: ExtNetInfo() = default; @@ -254,19 +287,37 @@ class ExtNetInfo final : public NetInfoInterface ~ExtNetInfo() = default; - SERIALIZE_METHODS(ExtNetInfo, obj) + template + void Serialize(Stream& s) const + { + s << m_version; + if (m_version == 0 || m_version > CURRENT_VERSION) { + return; // Don't bother with unknown versions + } + s << m_data; + } + + template + void Unserialize(Stream& s) { - READWRITE(obj.m_version); - if (obj.m_version == 0 || obj.m_version > CURRENT_VERSION) { + s >> m_version; + if (m_version == 0 || m_version > CURRENT_VERSION) { return; // Don't bother with unknown versions } - READWRITE(obj.m_data); + s >> m_data; + + // Regenerate internal cache + m_all_entries.clear(); + for (const auto& [_, entries] : m_data) { + m_all_entries.insert(m_all_entries.end(), entries.begin(), entries.end()); + } } - NetInfoStatus AddEntry(const std::string& input) override; + NetInfoStatus AddEntry(const NetInfoPurpose purpose, const std::string& input) override; NetInfoList GetEntries() const override; CService GetPrimary() const override; + bool HasEntries(NetInfoPurpose purpose) const override; bool IsEmpty() const override { return m_version == CURRENT_VERSION && m_data.empty(); } bool CanStorePlatform() const override { @@ -282,6 +333,7 @@ class ExtNetInfo final : public NetInfoInterface { m_version = CURRENT_VERSION; m_data.clear(); + m_all_entries.clear(); } private: diff --git a/src/evo/providertx.cpp b/src/evo/providertx.cpp index c6e1fd77c9e0..67717e41e318 100644 --- a/src/evo/providertx.cpp +++ b/src/evo/providertx.cpp @@ -59,6 +59,9 @@ bool CProRegTx::IsTriviallyValid(gsl::not_null pindexPrev, T if (netInfo->CanStorePlatform() != (nVersion == ProTxVersion::ExtAddr)) { return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-protx-netinfo-version"); } + if (!netInfo->IsEmpty() && !netInfo->HasEntries(NetInfoPurpose::CORE_P2P)) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-netinfo-empty"); + } for (const auto& entry : netInfo->GetEntries()) { if (!entry.IsTriviallyValid()) { return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-netinfo-bad"); @@ -135,7 +138,7 @@ bool CProUpServTx::IsTriviallyValid(gsl::not_null pindexPrev if (netInfo->CanStorePlatform() != (nVersion == ProTxVersion::ExtAddr)) { return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-protx-netinfo-version"); } - if (netInfo->IsEmpty()) { + if (netInfo->IsEmpty() || !netInfo->HasEntries(NetInfoPurpose::CORE_P2P)) { return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-netinfo-empty"); } for (const auto& entry : netInfo->GetEntries()) { diff --git a/src/rpc/coinjoin.cpp b/src/rpc/coinjoin.cpp index c7d29f65204c..bf2f4ea722ee 100644 --- a/src/rpc/coinjoin.cpp +++ b/src/rpc/coinjoin.cpp @@ -434,11 +434,13 @@ 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 (DEPRECATED, returned only if config option -deprecatedrpc=service is passed)"}, - {RPCResult::Type::ARR, "addresses", "Network addresses of the masternode", + {RPCResult::Type::OBJ, "addresses", "Network addresses of the masternode", { - { - {RPCResult::Type::STR, "address", ""}, - } + {RPCResult::Type::ARR, "core_p2p", /*optional=*/true, "Addresses used for protocol P2P", + { + {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"}, diff --git a/src/rpc/evo_util.cpp b/src/rpc/evo_util.cpp index a054beacb6b3..714b7bd22aeb 100644 --- a/src/rpc/evo_util.cpp +++ b/src/rpc/evo_util.cpp @@ -26,7 +26,7 @@ void ProcessNetInfoCore(T1& ptx, const UniValue& input, const bool optional) } return; // Nothing to do } - if (auto entryRet = ptx.netInfo->AddEntry(entry); entryRet != NetInfoStatus::Success) { + if (auto entryRet = ptx.netInfo->AddEntry(NetInfoPurpose::CORE_P2P, entry); entryRet != NetInfoStatus::Success) { throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Error setting coreP2PAddrs[0] to '%s' (%s)", entry, NISToString(entryRet))); } @@ -52,7 +52,7 @@ void ProcessNetInfoCore(T1& ptx, const UniValue& input, const bool optional) throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Invalid param for coreP2PAddrs[%d], cannot be empty string", idx)); } - if (auto entryRet = ptx.netInfo->AddEntry(entry); entryRet != NetInfoStatus::Success) { + if (auto entryRet = ptx.netInfo->AddEntry(NetInfoPurpose::CORE_P2P, entry); entryRet != NetInfoStatus::Success) { throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Error setting coreP2PAddrs[%d] to '%s' (%s)", idx, entry, NISToString(entryRet))); } diff --git a/src/test/block_reward_reallocation_tests.cpp b/src/test/block_reward_reallocation_tests.cpp index 9b60fd89a69e..ffb7ced72a22 100644 --- a/src/test/block_reward_reallocation_tests.cpp +++ b/src/test/block_reward_reallocation_tests.cpp @@ -118,7 +118,8 @@ static CMutableTransaction CreateProRegTx(const CChain& active_chain, const CTxM proTx.nVersion = ProTxVersion::GetMax(!bls::bls_legacy_scheme, /*is_extended_addr=*/false); proTx.netInfo = NetInfoInterface::MakeNetInfo(proTx.nVersion); proTx.collateralOutpoint.n = 0; - BOOST_CHECK_EQUAL(proTx.netInfo->AddEntry(strprintf("1.1.1.1:%d", port)), NetInfoStatus::Success); + BOOST_CHECK_EQUAL(proTx.netInfo->AddEntry(NetInfoPurpose::CORE_P2P, strprintf("1.1.1.1:%d", port)), + NetInfoStatus::Success); proTx.keyIDOwner = ownerKeyRet.GetPubKey().GetID(); proTx.pubKeyOperator.Set(operatorKeyRet.GetPublicKey(), bls::bls_legacy_scheme.load()); proTx.keyIDVoting = ownerKeyRet.GetPubKey().GetID(); diff --git a/src/test/evo_deterministicmns_tests.cpp b/src/test/evo_deterministicmns_tests.cpp index c58f32116de3..c67d4dd40725 100644 --- a/src/test/evo_deterministicmns_tests.cpp +++ b/src/test/evo_deterministicmns_tests.cpp @@ -108,7 +108,8 @@ static CMutableTransaction CreateProRegTx(const CChain& active_chain, const CTxM proTx.nVersion = ProTxVersion::GetMax(!bls::bls_legacy_scheme, /*is_extended_addr=*/false); proTx.netInfo = NetInfoInterface::MakeNetInfo(proTx.nVersion); proTx.collateralOutpoint.n = 0; - BOOST_CHECK_EQUAL(proTx.netInfo->AddEntry(strprintf("1.1.1.1:%d", port)), NetInfoStatus::Success); + BOOST_CHECK_EQUAL(proTx.netInfo->AddEntry(NetInfoPurpose::CORE_P2P, strprintf("1.1.1.1:%d", port)), + NetInfoStatus::Success); proTx.keyIDOwner = ownerKeyRet.GetPubKey().GetID(); proTx.pubKeyOperator.Set(operatorKeyRet.GetPublicKey(), bls::bls_legacy_scheme.load()); proTx.keyIDVoting = ownerKeyRet.GetPubKey().GetID(); @@ -131,7 +132,8 @@ static CMutableTransaction CreateProUpServTx(const CChain& active_chain, const C proTx.nVersion = ProTxVersion::GetMax(!bls::bls_legacy_scheme, /*is_extended_addr=*/false); proTx.netInfo = NetInfoInterface::MakeNetInfo(proTx.nVersion); proTx.proTxHash = proTxHash; - BOOST_CHECK_EQUAL(proTx.netInfo->AddEntry(strprintf("1.1.1.1:%d", port)), NetInfoStatus::Success); + BOOST_CHECK_EQUAL(proTx.netInfo->AddEntry(NetInfoPurpose::CORE_P2P, strprintf("1.1.1.1:%d", port)), + NetInfoStatus::Success); proTx.scriptOperatorPayout = scriptOperatorPayout; CMutableTransaction tx; @@ -645,7 +647,7 @@ void FuncTestMempoolReorg(TestChainSetup& setup) CProRegTx payload; payload.nVersion = ProTxVersion::GetMax(!bls::bls_legacy_scheme, /*is_extended_addr=*/false); payload.netInfo = NetInfoInterface::MakeNetInfo(payload.nVersion); - BOOST_CHECK_EQUAL(payload.netInfo->AddEntry("1.1.1.1:1"), NetInfoStatus::Success); + BOOST_CHECK_EQUAL(payload.netInfo->AddEntry(NetInfoPurpose::CORE_P2P, "1.1.1.1:1"), NetInfoStatus::Success); payload.keyIDOwner = ownerKey.GetPubKey().GetID(); payload.pubKeyOperator.Set(operatorKey.GetPublicKey(), bls::bls_legacy_scheme.load()); payload.keyIDVoting = ownerKey.GetPubKey().GetID(); @@ -721,7 +723,7 @@ void FuncTestMempoolDualProregtx(TestChainSetup& setup) CProRegTx payload; payload.nVersion = ProTxVersion::GetMax(!bls::bls_legacy_scheme, /*is_extended_addr=*/false); payload.netInfo = NetInfoInterface::MakeNetInfo(payload.nVersion); - BOOST_CHECK_EQUAL(payload.netInfo->AddEntry("1.1.1.1:2"), NetInfoStatus::Success); + BOOST_CHECK_EQUAL(payload.netInfo->AddEntry(NetInfoPurpose::CORE_P2P, "1.1.1.1:2"), NetInfoStatus::Success); payload.keyIDOwner = ownerKey.GetPubKey().GetID(); payload.pubKeyOperator.Set(operatorKey.GetPublicKey(), bls::bls_legacy_scheme.load()); payload.keyIDVoting = ownerKey.GetPubKey().GetID(); @@ -791,7 +793,7 @@ void FuncVerifyDB(TestChainSetup& setup) CProRegTx payload; payload.nVersion = ProTxVersion::GetMax(!bls::bls_legacy_scheme, /*is_extended_addr=*/false); payload.netInfo = NetInfoInterface::MakeNetInfo(payload.nVersion); - BOOST_CHECK_EQUAL(payload.netInfo->AddEntry("1.1.1.1:1"), NetInfoStatus::Success); + BOOST_CHECK_EQUAL(payload.netInfo->AddEntry(NetInfoPurpose::CORE_P2P, "1.1.1.1:1"), NetInfoStatus::Success); payload.keyIDOwner = ownerKey.GetPubKey().GetID(); payload.pubKeyOperator.Set(operatorKey.GetPublicKey(), bls::bls_legacy_scheme.load()); payload.keyIDVoting = ownerKey.GetPubKey().GetID(); @@ -855,7 +857,7 @@ static CDeterministicMNCPtr create_mock_mn(uint64_t internal_id) dmnState->keyIDVoting = ownerKey.GetPubKey().GetID(); dmnState->netInfo = NetInfoInterface::MakeNetInfo( ProTxVersion::GetMax(!bls::bls_legacy_scheme, /*is_extended_addr=*/false)); - BOOST_CHECK_EQUAL(dmnState->netInfo->AddEntry("1.1.1.1:1"), NetInfoStatus::Success); + BOOST_CHECK_EQUAL(dmnState->netInfo->AddEntry(NetInfoPurpose::CORE_P2P, "1.1.1.1:1"), NetInfoStatus::Success); auto dmn = std::make_shared(internal_id, MnType::Regular); dmn->proTxHash = GetRandHash(); diff --git a/src/test/evo_netinfo_tests.cpp b/src/test/evo_netinfo_tests.cpp index eb0583c94ede..d3f739e97c66 100644 --- a/src/test/evo_netinfo_tests.cpp +++ b/src/test/evo_netinfo_tests.cpp @@ -15,33 +15,38 @@ BOOST_FIXTURE_TEST_SUITE(evo_netinfo_tests, BasicTestingSetup) -using TestVectors = - std::vector>; +struct TestEntry { + std::pair input; + NetInfoStatus expected_ret_mn; + NetInfoStatus expected_ret_ext; +}; -static const TestVectors vals_main{ +static const std::vector vals_main{ // Address and port specified - {"1.1.1.1:9999", NetInfoStatus::Success, NetInfoStatus::Success}, + {{NetInfoPurpose::CORE_P2P, "1.1.1.1:9999"}, NetInfoStatus::Success, NetInfoStatus::Success}, // - Port should default to default P2P core with MnNetInfo // - Ports are no longer implied with ExtNetInfo - {"1.1.1.1", NetInfoStatus::Success, NetInfoStatus::BadPort}, + {{NetInfoPurpose::CORE_P2P, "1.1.1.1"}, NetInfoStatus::Success, NetInfoStatus::BadPort}, // - Non-mainnet port on mainnet causes failure in MnNetInfo // - ExtNetInfo is indifferent to choice of port unless it's a bad port which 9998 isn't - {"1.1.1.1:9998", NetInfoStatus::BadPort, NetInfoStatus::Success}, + {{NetInfoPurpose::CORE_P2P, "1.1.1.1:9998"}, NetInfoStatus::BadPort, NetInfoStatus::Success}, // Internal addresses not allowed on mainnet - {"127.0.0.1:9999", NetInfoStatus::NotRoutable, NetInfoStatus::NotRoutable}, + {{NetInfoPurpose::CORE_P2P, "127.0.0.1:9999"}, NetInfoStatus::NotRoutable, NetInfoStatus::NotRoutable}, // Valid IPv4 formatting but invalid IPv4 address - {"0.0.0.0:9999", NetInfoStatus::BadAddress, NetInfoStatus::BadAddress}, + {{NetInfoPurpose::CORE_P2P, "0.0.0.0:9999"}, NetInfoStatus::BadAddress, NetInfoStatus::BadAddress}, // Port greater than uint16_t max - {"1.1.1.1:99999", NetInfoStatus::BadInput, NetInfoStatus::BadInput}, + {{NetInfoPurpose::CORE_P2P, "1.1.1.1:99999"}, NetInfoStatus::BadInput, NetInfoStatus::BadInput}, // - Non-IPv4 addresses are prohibited in MnNetInfo // - Any valid BIP155 address is allowed in ExtNetInfo - {"[2606:4700:4700::1111]:9999", NetInfoStatus::BadInput, NetInfoStatus::Success}, + {{NetInfoPurpose::CORE_P2P, "[2606:4700:4700::1111]:9999"}, NetInfoStatus::BadInput, NetInfoStatus::Success}, // Domains are not allowed - {"example.com:9999", NetInfoStatus::BadInput, NetInfoStatus::BadInput}, + {{NetInfoPurpose::CORE_P2P, "example.com:9999"}, NetInfoStatus::BadInput, NetInfoStatus::BadInput}, // Incorrect IPv4 address - {"1.1.1.256:9999", NetInfoStatus::BadInput, NetInfoStatus::BadInput}, + {{NetInfoPurpose::CORE_P2P, "1.1.1.256:9999"}, NetInfoStatus::BadInput, NetInfoStatus::BadInput}, // Missing address - {":9999", NetInfoStatus::BadInput, NetInfoStatus::BadInput}, + {{NetInfoPurpose::CORE_P2P, ":9999"}, NetInfoStatus::BadInput, NetInfoStatus::BadInput}, + // Bad purpose code + {{static_cast(64), "1.1.1.1:9999"}, NetInfoStatus::MaxLimit, NetInfoStatus::MaxLimit}, }; void ValidateGetEntries(const NetInfoList& entries, const size_t expected_size) @@ -52,33 +57,39 @@ void ValidateGetEntries(const NetInfoList& entries, const size_t expected_size) } } -void TestMnNetInfo(const TestVectors& vals) +void TestMnNetInfo(const std::vector& vals) { for (const auto& [input, expected_ret, _] : vals) { + const auto& [purpose, addr] = input; MnNetInfo netInfo; - BOOST_CHECK_EQUAL(netInfo.AddEntry(input), expected_ret); + BOOST_CHECK_EQUAL(netInfo.AddEntry(purpose, addr), expected_ret); if (expected_ret != NetInfoStatus::Success) { // An empty MnNetInfo is considered malformed BOOST_CHECK_EQUAL(netInfo.Validate(), NetInfoStatus::Malformed); + BOOST_CHECK(!netInfo.HasEntries(purpose)); BOOST_CHECK(netInfo.GetEntries().empty()); } else { BOOST_CHECK_EQUAL(netInfo.Validate(), NetInfoStatus::Success); + BOOST_CHECK(netInfo.HasEntries(purpose)); ValidateGetEntries(netInfo.GetEntries(), /*expected_size=*/1); } } } -void TestExtNetInfo(const TestVectors& vals) +void TestExtNetInfo(const std::vector& vals) { for (const auto& [input, _, expected_ret] : vals) { + const auto& [purpose, addr] = input; ExtNetInfo netInfo; - BOOST_CHECK_EQUAL(netInfo.AddEntry(input), expected_ret); + BOOST_CHECK_EQUAL(netInfo.AddEntry(purpose, addr), expected_ret); if (expected_ret != NetInfoStatus::Success) { // An empty ExtNetInfo is considered malformed BOOST_CHECK_EQUAL(netInfo.Validate(), NetInfoStatus::Malformed); + BOOST_CHECK(!netInfo.HasEntries(purpose)); BOOST_CHECK(netInfo.GetEntries().empty()); } else { BOOST_CHECK_EQUAL(netInfo.Validate(), NetInfoStatus::Success); + BOOST_CHECK(netInfo.HasEntries(purpose)); ValidateGetEntries(netInfo.GetEntries(), /*expected_size=*/1); } } @@ -91,24 +102,25 @@ BOOST_AUTO_TEST_CASE(mnnetinfo_rules_main) { // MnNetInfo only stores one value, overwriting prohibited MnNetInfo netInfo; - BOOST_CHECK_EQUAL(netInfo.AddEntry("1.1.1.1:9999"), NetInfoStatus::Success); - BOOST_CHECK_EQUAL(netInfo.AddEntry("1.1.1.2:9999"), NetInfoStatus::MaxLimit); + BOOST_CHECK_EQUAL(netInfo.AddEntry(NetInfoPurpose::CORE_P2P, "1.1.1.1:9999"), NetInfoStatus::Success); + BOOST_CHECK_EQUAL(netInfo.AddEntry(NetInfoPurpose::CORE_P2P, "1.1.1.2:9999"), NetInfoStatus::MaxLimit); + BOOST_CHECK(netInfo.HasEntries(NetInfoPurpose::CORE_P2P)); ValidateGetEntries(netInfo.GetEntries(), /*expected_size=*/1); } } BOOST_AUTO_TEST_CASE(extnetinfo_rules_main) { TestExtNetInfo(vals_main); } -static const TestVectors vals_reg{ +static const std::vector vals_reg{ // - MnNetInfo doesn't mind using port 0 // - ExtNetInfo requires non-zero ports - {"1.1.1.1:0", NetInfoStatus::Success, NetInfoStatus::BadPort}, + {{NetInfoPurpose::CORE_P2P, "1.1.1.1:0"}, NetInfoStatus::Success, NetInfoStatus::BadPort}, // - Mainnet P2P port on non-mainnet cause failure in MnNetInfo // - ExtNetInfo is indifferent to choice of port unless it's a bad port which 9999 isn't - {"1.1.1.1:9999", NetInfoStatus::BadPort, NetInfoStatus::Success}, + {{NetInfoPurpose::CORE_P2P, "1.1.1.1:9999"}, NetInfoStatus::BadPort, NetInfoStatus::Success}, // - Non-mainnet P2P port is allowed in MnNetInfo regardless of bad port status // - Port 22 (SSH) is below the privileged ports threshold (1023) and is therefore a bad port, disallowed in ExtNetInfo - {"1.1.1.1:22", NetInfoStatus::Success, NetInfoStatus::BadPort}, + {{NetInfoPurpose::CORE_P2P, "1.1.1.1:22"}, NetInfoStatus::Success, NetInfoStatus::BadPort}, }; BOOST_FIXTURE_TEST_CASE(mnnetinfo_rules_reg, RegTestingSetup) { TestMnNetInfo(vals_reg); } @@ -118,23 +130,25 @@ BOOST_FIXTURE_TEST_CASE(extnetinfo_rules_reg, RegTestingSetup) TestExtNetInfo(vals_reg); { - // ExtNetInfo can store up to 4 entries, check limit enforcement + // ExtNetInfo can store up to 4 entries per purpose code, check limit enforcement ExtNetInfo netInfo; for (size_t idx{1}; idx <= MAX_ENTRIES_EXTNETINFO; idx++) { - BOOST_CHECK_EQUAL(netInfo.AddEntry(strprintf("1.1.1.%d:9998", idx)), NetInfoStatus::Success); + BOOST_CHECK_EQUAL(netInfo.AddEntry(NetInfoPurpose::CORE_P2P, strprintf("1.1.1.%d:9998", idx)), + NetInfoStatus::Success); } - BOOST_CHECK_EQUAL(netInfo.AddEntry("1.1.1.5:9998"), NetInfoStatus::MaxLimit); + BOOST_CHECK_EQUAL(netInfo.AddEntry(NetInfoPurpose::CORE_P2P, "1.1.1.5:9998"), NetInfoStatus::MaxLimit); + BOOST_CHECK(netInfo.HasEntries(NetInfoPurpose::CORE_P2P)); ValidateGetEntries(netInfo.GetEntries(), /*expected_size=*/MAX_ENTRIES_EXTNETINFO); } { // ExtNetInfo does not allow storing duplicates ExtNetInfo netInfo; - BOOST_CHECK_EQUAL(netInfo.AddEntry("1.1.1.1:9998"), NetInfoStatus::Success); + BOOST_CHECK_EQUAL(netInfo.AddEntry(NetInfoPurpose::CORE_P2P, "1.1.1.1:9998"), NetInfoStatus::Success); // Exact duplicates are prohibited - BOOST_CHECK_EQUAL(netInfo.AddEntry("1.1.1.1:9998"), NetInfoStatus::Duplicate); + BOOST_CHECK_EQUAL(netInfo.AddEntry(NetInfoPurpose::CORE_P2P, "1.1.1.1:9998"), NetInfoStatus::Duplicate); // Partial duplicates (same address, different port) are also prohibited - BOOST_CHECK_EQUAL(netInfo.AddEntry("1.1.1.1:9997"), NetInfoStatus::Duplicate); + BOOST_CHECK_EQUAL(netInfo.AddEntry(NetInfoPurpose::CORE_P2P, "1.1.1.1:9997"), NetInfoStatus::Duplicate); } } @@ -260,25 +274,25 @@ BOOST_AUTO_TEST_CASE(cservice_compatible) // Valid IPv4 address, valid port service = LookupNumeric("1.1.1.1", 9999); netInfo.Clear(); - BOOST_CHECK_EQUAL(netInfo.AddEntry("1.1.1.1:9999"), NetInfoStatus::Success); + BOOST_CHECK_EQUAL(netInfo.AddEntry(NetInfoPurpose::CORE_P2P, "1.1.1.1:9999"), NetInfoStatus::Success); BOOST_CHECK(CheckIfSerSame(service, netInfo)); // Valid IPv4 address, default P2P port implied service = LookupNumeric("1.1.1.1", Params().GetDefaultPort()); netInfo.Clear(); - BOOST_CHECK_EQUAL(netInfo.AddEntry("1.1.1.1"), NetInfoStatus::Success); + BOOST_CHECK_EQUAL(netInfo.AddEntry(NetInfoPurpose::CORE_P2P, "1.1.1.1"), NetInfoStatus::Success); BOOST_CHECK(CheckIfSerSame(service, netInfo)); // Lookup() failure (domains not allowed), MnNetInfo should remain empty if Lookup() failed service = CService(); netInfo.Clear(); - BOOST_CHECK_EQUAL(netInfo.AddEntry("example.com"), NetInfoStatus::BadInput); + BOOST_CHECK_EQUAL(netInfo.AddEntry(NetInfoPurpose::CORE_P2P, "example.com"), NetInfoStatus::BadInput); BOOST_CHECK(CheckIfSerSame(service, netInfo)); // Validation failure (non-IPv4 not allowed), MnNetInfo should remain empty if ValidateService() failed service = CService(); netInfo.Clear(); - BOOST_CHECK_EQUAL(netInfo.AddEntry("[2606:4700:4700::1111]:9999"), NetInfoStatus::BadInput); + BOOST_CHECK_EQUAL(netInfo.AddEntry(NetInfoPurpose::CORE_P2P, "[2606:4700:4700::1111]:9999"), NetInfoStatus::BadInput); BOOST_CHECK(CheckIfSerSame(service, netInfo)); } @@ -301,7 +315,7 @@ BOOST_AUTO_TEST_CASE(interface_equality) BOOST_CHECK(util::shared_ptr_equal(ptr_lhs, ptr_rhs) && !util::shared_ptr_not_equal(ptr_lhs, ptr_rhs)); // Equal initialization state, same type, differing values - BOOST_CHECK_EQUAL(ptr_rhs->AddEntry("1.1.1.1:9999"), NetInfoStatus::Success); + BOOST_CHECK_EQUAL(ptr_rhs->AddEntry(NetInfoPurpose::CORE_P2P, "1.1.1.1:9999"), NetInfoStatus::Success); BOOST_CHECK(!util::shared_ptr_equal(ptr_lhs, ptr_rhs) && util::shared_ptr_not_equal(ptr_lhs, ptr_rhs)); // Equal initialization state, different type, same values @@ -315,7 +329,7 @@ BOOST_AUTO_TEST_CASE(interface_equality) BOOST_CHECK(util::shared_ptr_equal(ptr_lhs, ptr_rhs) && !util::shared_ptr_not_equal(ptr_lhs, ptr_rhs)); // Equal initialization state, same type, differing values - BOOST_CHECK_EQUAL(ptr_rhs->AddEntry("1.1.1.1:9999"), NetInfoStatus::Success); + BOOST_CHECK_EQUAL(ptr_rhs->AddEntry(NetInfoPurpose::CORE_P2P, "1.1.1.1:9999"), NetInfoStatus::Success); BOOST_CHECK(!util::shared_ptr_equal(ptr_lhs, ptr_rhs) && util::shared_ptr_not_equal(ptr_lhs, ptr_rhs)); } diff --git a/src/test/evo_simplifiedmns_tests.cpp b/src/test/evo_simplifiedmns_tests.cpp index 02fef713c0e2..a3ea64683452 100644 --- a/src/test/evo_simplifiedmns_tests.cpp +++ b/src/test/evo_simplifiedmns_tests.cpp @@ -24,7 +24,8 @@ BOOST_AUTO_TEST_CASE(simplifiedmns_merkleroots) smle.proRegTxHash.SetHex(strprintf("%064x", i)); smle.confirmedHash.SetHex(strprintf("%064x", i)); - BOOST_CHECK_EQUAL(smle.netInfo->AddEntry(strprintf("%d.%d.%d.%d:%d", 0, 0, 0, i, i)), NetInfoStatus::Success); + BOOST_CHECK_EQUAL(smle.netInfo->AddEntry(NetInfoPurpose::CORE_P2P, strprintf("%d.%d.%d.%d:%d", 0, 0, 0, i, i)), + NetInfoStatus::Success); std::vector vecBytes{static_cast(i)}; vecBytes.resize(CBLSSecretKey::SerSize); diff --git a/test/functional/feature_dip3_deterministicmns.py b/test/functional/feature_dip3_deterministicmns.py index 7494f6536d1e..77a5e4768a2b 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']['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) + assert_equal(protx_info['state']['addresses']['core_p2p'][0], '127.0.0.2:%d' % mn.nodePort) + assert_equal(mn_list['%s-%d' % (mn.collateral_txid, mn.collateral_vout)]['addresses']['core_p2p'][0], '127.0.0.2:%d' % mn.nodePort) # undo mn.update_service(self.nodes[0], submit=True) diff --git a/test/functional/rpc_netinfo.py b/test/functional/rpc_netinfo.py index ba1951b820d7..cbe2129d33da 100755 --- a/test/functional/rpc_netinfo.py +++ b/test/functional/rpc_netinfo.py @@ -154,7 +154,7 @@ 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}") + assert_equal(val['core_p2p'][0], f"127.0.0.1:{core_p2p_port}") def run_test(self): self.node_evo: Node = Node(self.nodes[0], True) diff --git a/test/functional/rpc_quorum.py b/test/functional/rpc_quorum.py index 202f0374c9d3..998049cf320b 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['addresses'][0], f'127.0.0.1:{mn.nodePort}') + assert_equal(member['addresses']['core_p2p'][0], f'127.0.0.1:{mn.nodePort}') if __name__ == '__main__': RPCMasternodeTest().main() From 53f993c0c388d6f8a2b3638706ff29b742317aee Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Sun, 3 Aug 2025 13:43:59 +0000 Subject: [PATCH 07/27] evo: allow address entries to be differentiated by port --- src/evo/netinfo.cpp | 38 +++++++++++++++++++++++++++++++------- src/evo/netinfo.h | 10 ++++++++-- 2 files changed, 39 insertions(+), 9 deletions(-) diff --git a/src/evo/netinfo.cpp b/src/evo/netinfo.cpp index 7664d330372c..d75b0fc287af 100644 --- a/src/evo/netinfo.cpp +++ b/src/evo/netinfo.cpp @@ -262,11 +262,11 @@ std::string MnNetInfo::ToString() const m_addr.ToString()); } -bool ExtNetInfo::HasDuplicates() const +bool ExtNetInfo::HasAddrPortDuplicates() const { - std::unordered_set known{}; + std::set known{}; for (const auto& entry : m_all_entries) { - if (auto [_, inserted] = known.insert(entry.ToStringAddr()); !inserted) { + if (auto [_, inserted] = known.insert(entry); !inserted) { return true; } } @@ -274,10 +274,28 @@ bool ExtNetInfo::HasDuplicates() const return false; } -bool ExtNetInfo::IsDuplicateCandidate(const NetInfoEntry& candidate) const +bool ExtNetInfo::IsAddrPortDuplicate(const NetInfoEntry& candidate) const { - const std::string& candidate_str{candidate.ToStringAddr()}; return std::any_of(m_all_entries.begin(), m_all_entries.end(), + [&candidate](const auto& entry) { return candidate == entry; }); +} + +bool ExtNetInfo::HasAddrDuplicates(const NetInfoList& entries) const +{ + std::unordered_set known{}; + for (const auto& entry : entries) { + if (auto [_, inserted] = known.insert(entry.ToStringAddr()); !inserted) { + return true; + } + } + ASSERT_IF_DEBUG(known.size() == entries.size()); + return false; +} + +bool ExtNetInfo::IsAddrDuplicate(const NetInfoEntry& candidate, const NetInfoList& entries) const +{ + const std::string& candidate_str{candidate.ToStringAddr()}; + return std::any_of(entries.begin(), entries.end(), [&candidate_str](const auto& entry) { return candidate_str == entry.ToStringAddr(); }); } @@ -285,7 +303,7 @@ NetInfoStatus ExtNetInfo::ProcessCandidate(const NetInfoPurpose purpose, const N { assert(candidate.IsTriviallyValid()); - if (IsDuplicateCandidate(candidate)) { + if (IsAddrPortDuplicate(candidate)) { return NetInfoStatus::Duplicate; } if (auto it{m_data.find(purpose)}; it != m_data.end()) { @@ -294,6 +312,9 @@ NetInfoStatus ExtNetInfo::ProcessCandidate(const NetInfoPurpose purpose, const N if (entries.size() >= MAX_ENTRIES_EXTNETINFO) { return NetInfoStatus::MaxLimit; } + if (IsAddrDuplicate(candidate, entries)) { + return NetInfoStatus::Duplicate; + } entries.push_back(candidate); } else { // First entry for purpose code, create new entries list @@ -384,7 +405,7 @@ NetInfoStatus ExtNetInfo::Validate() const if (m_version == 0 || m_version > CURRENT_VERSION || m_data.empty()) { return NetInfoStatus::Malformed; } - if (HasDuplicates()) { + if (HasAddrPortDuplicates()) { return NetInfoStatus::Duplicate; } for (const auto& [purpose, entries] : m_data) { @@ -395,6 +416,9 @@ NetInfoStatus ExtNetInfo::Validate() const // Purpose if present in map must have at least one entry return NetInfoStatus::Malformed; } + if (HasAddrDuplicates(entries)) { + return NetInfoStatus::Duplicate; + } for (const auto& entry : entries) { if (!entry.IsTriviallyValid()) { // Trivially invalid NetInfoEntry, no point checking against consensus rules diff --git a/src/evo/netinfo.h b/src/evo/netinfo.h index a4c8d35f0792..d52fef7d75e5 100644 --- a/src/evo/netinfo.h +++ b/src/evo/netinfo.h @@ -262,10 +262,16 @@ class ExtNetInfo final : public NetInfoInterface static constexpr uint8_t CURRENT_VERSION{1}; /** Returns true if there are addr:port duplicates in the object */ - bool HasDuplicates() const; + bool HasAddrPortDuplicates() const; /** Returns true if candidate is an addr:port duplicate in the object */ - bool IsDuplicateCandidate(const NetInfoEntry& candidate) const; + bool IsAddrPortDuplicate(const NetInfoEntry& candidate) const; + + /** Returns true if there are addr duplicates within a given address list */ + bool HasAddrDuplicates(const NetInfoList& entries) const; + + /** Returns true if candidate is an addr duplicate within a given address list */ + bool IsAddrDuplicate(const NetInfoEntry& candidate, const NetInfoList& entries) const; /** Validate uniqueness requirements and add to object if passed */ NetInfoStatus ProcessCandidate(const NetInfoPurpose purpose, const NetInfoEntry& candidate); From bfbfe3c644a1ccf481b802694663fc1c193d8a94 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Sun, 14 Sep 2025 22:35:06 +0000 Subject: [PATCH 08/27] evo: allow storing platform P2P and HTTPS addresses in `ExtNetInfo` --- src/evo/deterministicmns.cpp | 3 --- src/evo/netinfo.h | 16 ++++++++------ src/evo/providertx.cpp | 35 +++++++++++++++++++++++++++--- src/rpc/coinjoin.cpp | 10 +++++++++ src/test/evo_netinfo_tests.cpp | 39 ++++++++++++++++++++++++++++++---- 5 files changed, 87 insertions(+), 16 deletions(-) diff --git a/src/evo/deterministicmns.cpp b/src/evo/deterministicmns.cpp index 22ae195cab33..c43da27d9a10 100644 --- a/src/evo/deterministicmns.cpp +++ b/src/evo/deterministicmns.cpp @@ -895,9 +895,6 @@ void CDeterministicMNManager::CleanupCache(int nHeight) template static bool CheckService(const ProTx& proTx, TxValidationState& state) { - if (!proTx.netInfo->HasEntries(NetInfoPurpose::CORE_P2P)) { - return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-netinfo-empty"); - } switch (proTx.netInfo->Validate()) { case NetInfoStatus::BadAddress: return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-netinfo-addr"); diff --git a/src/evo/netinfo.h b/src/evo/netinfo.h index d52fef7d75e5..ce657a8798c5 100644 --- a/src/evo/netinfo.h +++ b/src/evo/netinfo.h @@ -64,6 +64,9 @@ constexpr std::string_view NISToString(const NetInfoStatus code) enum class NetInfoPurpose : uint8_t { // Mandatory for masternodes CORE_P2P = 0, + // Mandatory for EvoNodes + PLATFORM_P2P = 1, + PLATFORM_HTTPS = 2, }; template<> struct is_serializable_enum : std::true_type {}; @@ -72,6 +75,8 @@ constexpr bool IsValidPurpose(const NetInfoPurpose purpose) { switch (purpose) { case NetInfoPurpose::CORE_P2P: + case NetInfoPurpose::PLATFORM_P2P: + case NetInfoPurpose::PLATFORM_HTTPS: return true; } // no default case, so the compiler can warn about missing cases return false; @@ -83,6 +88,10 @@ constexpr std::string_view PurposeToString(const NetInfoPurpose purpose) switch (purpose) { case NetInfoPurpose::CORE_P2P: return "core_p2p"; + case NetInfoPurpose::PLATFORM_P2P: + return "platform_p2p"; + case NetInfoPurpose::PLATFORM_HTTPS: + return "platform_https"; } // no default case, so the compiler can warn about missing cases return ""; } @@ -325,12 +334,7 @@ class ExtNetInfo final : public NetInfoInterface CService GetPrimary() const override; bool HasEntries(NetInfoPurpose purpose) const override; bool IsEmpty() const override { return m_version == CURRENT_VERSION && m_data.empty(); } - bool CanStorePlatform() const override - { - // TODO: Store Platform fields, reporting as true as used to differentiate - // with legacy implementation - return true; - } + bool CanStorePlatform() const override { return true; } NetInfoStatus Validate() const override; UniValue ToJson() const override; std::string ToString() const override; diff --git a/src/evo/providertx.cpp b/src/evo/providertx.cpp index 67717e41e318..2d850e80109c 100644 --- a/src/evo/providertx.cpp +++ b/src/evo/providertx.cpp @@ -32,6 +32,30 @@ template uint16_t GetMaxFromDeployment(gsl::not_null(gsl::not_null pindexPrev, std::optional is_basic_override); } // namespace ProTxVersion +template +bool IsNetInfoTriviallyValid(const ProTx& proTx, TxValidationState& state) +{ + if (!proTx.netInfo->HasEntries(NetInfoPurpose::CORE_P2P)) { + // Mandatory for all nodes + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-netinfo-empty"); + } + if (proTx.nType == MnType::Regular) { + // Regular nodes shouldn't populate Platform-specific fields + if (proTx.netInfo->HasEntries(NetInfoPurpose::PLATFORM_HTTPS) || + proTx.netInfo->HasEntries(NetInfoPurpose::PLATFORM_P2P)) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-netinfo-bad"); + } + } + if (proTx.netInfo->CanStorePlatform() && proTx.nType == MnType::Evo) { + // Platform fields are mandatory for EvoNodes + if (!proTx.netInfo->HasEntries(NetInfoPurpose::PLATFORM_HTTPS) || + !proTx.netInfo->HasEntries(NetInfoPurpose::PLATFORM_P2P)) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-netinfo-empty"); + } + } + return true; +} + bool CProRegTx::IsTriviallyValid(gsl::not_null pindexPrev, TxValidationState& state) const { if (nVersion == 0 || nVersion > ProTxVersion::GetMaxFromDeployment(pindexPrev)) { @@ -59,8 +83,9 @@ bool CProRegTx::IsTriviallyValid(gsl::not_null pindexPrev, T if (netInfo->CanStorePlatform() != (nVersion == ProTxVersion::ExtAddr)) { return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-protx-netinfo-version"); } - if (!netInfo->IsEmpty() && !netInfo->HasEntries(NetInfoPurpose::CORE_P2P)) { - return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-netinfo-empty"); + if (!netInfo->IsEmpty() && !IsNetInfoTriviallyValid(*this, state)) { + // pass the state returned by the function above + return false; } for (const auto& entry : netInfo->GetEntries()) { if (!entry.IsTriviallyValid()) { @@ -138,9 +163,13 @@ bool CProUpServTx::IsTriviallyValid(gsl::not_null pindexPrev if (netInfo->CanStorePlatform() != (nVersion == ProTxVersion::ExtAddr)) { return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-protx-netinfo-version"); } - if (netInfo->IsEmpty() || !netInfo->HasEntries(NetInfoPurpose::CORE_P2P)) { + if (netInfo->IsEmpty()) { return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-netinfo-empty"); } + if (!IsNetInfoTriviallyValid(*this, state)) { + // pass the state returned by the function above + return false; + } for (const auto& entry : netInfo->GetEntries()) { if (!entry.IsTriviallyValid()) { return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-netinfo-bad"); diff --git a/src/rpc/coinjoin.cpp b/src/rpc/coinjoin.cpp index bf2f4ea722ee..4973fff1b784 100644 --- a/src/rpc/coinjoin.cpp +++ b/src/rpc/coinjoin.cpp @@ -441,6 +441,16 @@ static RPCHelpMan getcoinjoininfo() {RPCResult::Type::STR, "address", ""}, } }, + {RPCResult::Type::ARR, "platform_p2p", /*optional=*/true, "Addresses used for Platform P2P (EvoNodes only)", + { + {RPCResult::Type::STR, "address", ""}, + } + }, + {RPCResult::Type::ARR, "platform_https", /*optional=*/true, "Addresses used for Platform HTTPS API (EvoNodes only)", + { + {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"}, diff --git a/src/test/evo_netinfo_tests.cpp b/src/test/evo_netinfo_tests.cpp index d3f739e97c66..7c20deebf4e9 100644 --- a/src/test/evo_netinfo_tests.cpp +++ b/src/test/evo_netinfo_tests.cpp @@ -47,6 +47,9 @@ static const std::vector vals_main{ {{NetInfoPurpose::CORE_P2P, ":9999"}, NetInfoStatus::BadInput, NetInfoStatus::BadInput}, // Bad purpose code {{static_cast(64), "1.1.1.1:9999"}, NetInfoStatus::MaxLimit, NetInfoStatus::MaxLimit}, + // - MnNetInfo doesn't allow storing anything except a Core P2P address + // - ExtNetInfo allows storing Platform P2P addresses + {{NetInfoPurpose::PLATFORM_P2P, "1.1.1.1:9999"}, NetInfoStatus::MaxLimit, NetInfoStatus::Success}, }; void ValidateGetEntries(const NetInfoList& entries, const size_t expected_size) @@ -107,6 +110,16 @@ BOOST_AUTO_TEST_CASE(mnnetinfo_rules_main) BOOST_CHECK(netInfo.HasEntries(NetInfoPurpose::CORE_P2P)); ValidateGetEntries(netInfo.GetEntries(), /*expected_size=*/1); } + + { + // MnNetInfo only allows storing a Core P2P address + MnNetInfo netInfo; + for (const auto purpose : {NetInfoPurpose::PLATFORM_HTTPS, NetInfoPurpose::PLATFORM_P2P}) { + BOOST_CHECK_EQUAL(netInfo.AddEntry(purpose, "1.1.1.1:9999"), NetInfoStatus::MaxLimit); + BOOST_CHECK(!netInfo.HasEntries(purpose)); + } + BOOST_CHECK(netInfo.GetEntries().empty()); + } } BOOST_AUTO_TEST_CASE(extnetinfo_rules_main) { TestExtNetInfo(vals_main); } @@ -138,17 +151,35 @@ BOOST_FIXTURE_TEST_CASE(extnetinfo_rules_reg, RegTestingSetup) } BOOST_CHECK_EQUAL(netInfo.AddEntry(NetInfoPurpose::CORE_P2P, "1.1.1.5:9998"), NetInfoStatus::MaxLimit); BOOST_CHECK(netInfo.HasEntries(NetInfoPurpose::CORE_P2P)); - ValidateGetEntries(netInfo.GetEntries(), /*expected_size=*/MAX_ENTRIES_EXTNETINFO); + // The limit applies *per purpose code* and therefore wouldn't error if the address was for a different purpose + BOOST_CHECK(!netInfo.HasEntries(NetInfoPurpose::PLATFORM_P2P)); + BOOST_CHECK_EQUAL(netInfo.AddEntry(NetInfoPurpose::PLATFORM_P2P, "1.1.1.5:9998"), NetInfoStatus::Success); + BOOST_CHECK(netInfo.HasEntries(NetInfoPurpose::PLATFORM_P2P)); + BOOST_CHECK_EQUAL(netInfo.Validate(), NetInfoStatus::Success); + // GetEntries() is a tally of all entries across all purpose codes + ValidateGetEntries(netInfo.GetEntries(), /*expected_size=*/MAX_ENTRIES_EXTNETINFO + 1); } { - // ExtNetInfo does not allow storing duplicates + // ExtNetInfo has restrictions on duplicates ExtNetInfo netInfo; BOOST_CHECK_EQUAL(netInfo.AddEntry(NetInfoPurpose::CORE_P2P, "1.1.1.1:9998"), NetInfoStatus::Success); - // Exact duplicates are prohibited + + // Exact (i.e. addr:port) duplicates are prohibited *within* a list BOOST_CHECK_EQUAL(netInfo.AddEntry(NetInfoPurpose::CORE_P2P, "1.1.1.1:9998"), NetInfoStatus::Duplicate); - // Partial duplicates (same address, different port) are also prohibited + // Partial (i.e. different port) duplicates are prohibited *within* a list BOOST_CHECK_EQUAL(netInfo.AddEntry(NetInfoPurpose::CORE_P2P, "1.1.1.1:9997"), NetInfoStatus::Duplicate); + + // Exact (i.e. addr:port) duplicates are prohibited *across* lists + BOOST_CHECK_EQUAL(netInfo.AddEntry(NetInfoPurpose::PLATFORM_P2P, "1.1.1.1:9998"), NetInfoStatus::Duplicate); + // Partial (i.e. different port) duplicates are allowed *across* a list + BOOST_CHECK_EQUAL(netInfo.AddEntry(NetInfoPurpose::PLATFORM_P2P, "1.1.1.1:9997"), NetInfoStatus::Success); + + BOOST_CHECK_EQUAL(netInfo.Validate(), NetInfoStatus::Success); + BOOST_CHECK(netInfo.HasEntries(NetInfoPurpose::CORE_P2P)); + BOOST_CHECK(netInfo.HasEntries(NetInfoPurpose::PLATFORM_P2P)); + BOOST_CHECK(!netInfo.HasEntries(NetInfoPurpose::PLATFORM_HTTPS)); + ValidateGetEntries(netInfo.GetEntries(), /*expected_size=*/2); } } From e155529ef7e6a2844def1f389886cebfa9d68dd1 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Sun, 14 Sep 2025 22:35:43 +0000 Subject: [PATCH 09/27] evo: stop using `platform{HTTP,P2P}Port` fields if using extended addrs --- src/evo/deterministicmns.cpp | 38 +++++++++++++++++++----------------- src/evo/dmnstate.h | 5 ++++- src/evo/netinfo.cpp | 17 ++++++++-------- src/evo/netinfo.h | 9 ++++++++- src/evo/providertx.cpp | 16 +++++++++------ src/evo/providertx.h | 10 ++++++++-- src/evo/simplifiedmns.cpp | 5 +++-- src/evo/simplifiedmns.h | 17 ++++++++++------ src/evo/specialtxman.cpp | 6 ++++-- 9 files changed, 76 insertions(+), 47 deletions(-) diff --git a/src/evo/deterministicmns.cpp b/src/evo/deterministicmns.cpp index c43da27d9a10..3b014a2173c6 100644 --- a/src/evo/deterministicmns.cpp +++ b/src/evo/deterministicmns.cpp @@ -918,39 +918,41 @@ static bool CheckService(const ProTx& proTx, TxValidationState& state) } template -static bool CheckPlatformFields(const ProTx& proTx, TxValidationState& state) +static bool CheckPlatformFields(const ProTx& proTx, bool is_extended_addr, TxValidationState& state) { if (proTx.platformNodeID.IsNull()) { return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-platform-nodeid"); } - // TODO: use real args here - static int mainnetPlatformP2PPort = CreateChainParams(ArgsManager{}, CBaseChainParams::MAIN)->GetDefaultPlatformP2PPort(); - if (Params().NetworkIDString() == CBaseChainParams::MAIN) { - if (proTx.platformP2PPort != mainnetPlatformP2PPort) { + if (is_extended_addr) { + // platformHTTPPort and platformP2PPort have been subsumed by netInfo. They should always be zero. + if (proTx.platformP2PPort != 0) { return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-platform-p2p-port"); } + if (proTx.platformHTTPPort != 0) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-platform-http-port"); + } + return true; } - // TODO: use real args here - static int mainnetPlatformHTTPPort = CreateChainParams(ArgsManager{}, CBaseChainParams::MAIN)->GetDefaultPlatformHTTPPort(); - if (Params().NetworkIDString() == CBaseChainParams::MAIN) { - if (proTx.platformHTTPPort != mainnetPlatformHTTPPort) { + if (::IsNodeOnMainnet()) { + if (proTx.platformP2PPort != ::MainParams().GetDefaultPlatformP2PPort()) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-platform-p2p-port"); + } + if (proTx.platformHTTPPort != ::MainParams().GetDefaultPlatformHTTPPort()) { return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-platform-http-port"); } } - - // TODO: use real args here - static int mainnetDefaultP2PPort = CreateChainParams(ArgsManager{}, CBaseChainParams::MAIN)->GetDefaultPort(); - if (proTx.platformP2PPort == mainnetDefaultP2PPort) { + if (proTx.platformP2PPort == ::MainParams().GetDefaultPort()) { return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-platform-p2p-port"); } - if (proTx.platformHTTPPort == mainnetDefaultP2PPort) { + if (proTx.platformHTTPPort == ::MainParams().GetDefaultPort()) { return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-platform-http-port"); } - if (proTx.platformP2PPort == proTx.platformHTTPPort || proTx.platformP2PPort == proTx.netInfo->GetPrimary().GetPort() || - proTx.platformHTTPPort == proTx.netInfo->GetPrimary().GetPort()) { + const uint16_t core_port{proTx.netInfo->GetPrimary().GetPort()}; + if (proTx.platformP2PPort == proTx.platformHTTPPort || proTx.platformP2PPort == core_port || + proTx.platformHTTPPort == core_port) { return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-platform-dup-ports"); } @@ -1062,7 +1064,7 @@ bool CheckProRegTx(CDeterministicMNManager& dmnman, const CTransaction& tx, gsl: } if (opt_ptx->nType == MnType::Evo) { - if (!CheckPlatformFields(*opt_ptx, state)) { + if (!CheckPlatformFields(*opt_ptx, opt_ptx->nVersion >= ProTxVersion::ExtAddr, state)) { return false; } } @@ -1181,7 +1183,7 @@ bool CheckProUpServTx(CDeterministicMNManager& dmnman, const CTransaction& tx, g } if (opt_ptx->nType == MnType::Evo) { - if (!CheckPlatformFields(*opt_ptx, state)) { + if (!CheckPlatformFields(*opt_ptx, opt_ptx->nVersion >= ProTxVersion::ExtAddr, state)) { return false; } } diff --git a/src/evo/dmnstate.h b/src/evo/dmnstate.h index 380bac93ea0e..d63460b2d931 100644 --- a/src/evo/dmnstate.h +++ b/src/evo/dmnstate.h @@ -102,9 +102,12 @@ class CDeterministicMNState obj.nVersion >= ProTxVersion::ExtAddr), obj.scriptPayout, obj.scriptOperatorPayout, - obj.platformNodeID, + obj.platformNodeID); + if (obj.nVersion < ProTxVersion::ExtAddr) { + READWRITE( obj.platformP2PPort, obj.platformHTTPPort); + } } void ResetOperatorFields() diff --git a/src/evo/netinfo.cpp b/src/evo/netinfo.cpp index d75b0fc287af..2e4738d794d3 100644 --- a/src/evo/netinfo.cpp +++ b/src/evo/netinfo.cpp @@ -21,21 +21,20 @@ static std::once_flag g_main_params_flag; static constexpr std::string_view SAFE_CHARS_IPV4{"1234567890."}; static constexpr std::string_view SAFE_CHARS_IPV4_6{"abcdefABCDEF1234567890.:[]"}; -bool IsNodeOnMainnet() { return Params().NetworkIDString() == CBaseChainParams::MAIN; } -const CChainParams& MainParams() -{ - // TODO: use real args here - std::call_once(g_main_params_flag, - [&]() { g_main_params = CreateChainParams(ArgsManager{}, CBaseChainParams::MAIN); }); - return *Assert(g_main_params); -} - bool MatchCharsFilter(std::string_view input, std::string_view filter) { return std::all_of(input.begin(), input.end(), [&filter](char c) { return filter.find(c) != std::string_view::npos; }); } } // anonymous namespace +bool IsNodeOnMainnet() { return Params().NetworkIDString() == CBaseChainParams::MAIN; } + +const CChainParams& MainParams() +{ + std::call_once(g_main_params_flag, [&]() { g_main_params = CreateChainParams(::gArgs, CBaseChainParams::MAIN); }); + return *Assert(g_main_params); +} + UniValue ArrFromService(const CService& addr) { UniValue obj(UniValue::VARR); diff --git a/src/evo/netinfo.h b/src/evo/netinfo.h index ce657a8798c5..b6a14bd1c156 100644 --- a/src/evo/netinfo.h +++ b/src/evo/netinfo.h @@ -11,6 +11,7 @@ #include +class CChainParams; class CService; class UniValue; @@ -96,9 +97,15 @@ constexpr std::string_view PurposeToString(const NetInfoPurpose purpose) return ""; } -/* Identical to IsDeprecatedRPCEnabled("service"). For use outside of RPC code. */ +/** Will return true if node is running on mainnet */ +bool IsNodeOnMainnet(); + +/** Identical to IsDeprecatedRPCEnabled("service"). For use outside of RPC code */ bool IsServiceDeprecatedRPCEnabled(); +/** Equivalent to Params() if node is running on mainnet */ +const CChainParams& MainParams(); + class NetInfoEntry { public: diff --git a/src/evo/providertx.cpp b/src/evo/providertx.cpp index 2d850e80109c..6702ede99223 100644 --- a/src/evo/providertx.cpp +++ b/src/evo/providertx.cpp @@ -144,12 +144,13 @@ std::string CProRegTx::ToString() const } return strprintf("CProRegTx(nVersion=%d, nType=%d, collateralOutpoint=%s, netInfo=%s, nOperatorReward=%f, " - "ownerAddress=%s, pubKeyOperator=%s, votingAddress=%s, scriptPayout=%s, platformNodeID=%s, " - "platformP2PPort=%d, platformHTTPPort=%d)\n", + "ownerAddress=%s, pubKeyOperator=%s, votingAddress=%s, scriptPayout=%s, platformNodeID=%s%s)\n", nVersion, ToUnderlying(nType), collateralOutpoint.ToStringShort(), netInfo->ToString(), (double)nOperatorReward / 100, EncodeDestination(PKHash(keyIDOwner)), pubKeyOperator.ToString(), - EncodeDestination(PKHash(keyIDVoting)), payee, platformNodeID.ToString(), platformP2PPort, - platformHTTPPort); + EncodeDestination(PKHash(keyIDVoting)), payee, platformNodeID.ToString(), + (nVersion >= ProTxVersion::ExtAddr + ? "" + : strprintf(", platformP2PPort=%d, platformHTTPPort=%d", platformP2PPort, platformHTTPPort))); } bool CProUpServTx::IsTriviallyValid(gsl::not_null pindexPrev, TxValidationState& state) const @@ -188,9 +189,12 @@ std::string CProUpServTx::ToString() const } return strprintf("CProUpServTx(nVersion=%d, nType=%d, proTxHash=%s, netInfo=%s, operatorPayoutAddress=%s, " - "platformNodeID=%s, platformP2PPort=%d, platformHTTPPort=%d)\n", + "platformNodeID=%s%s)\n", nVersion, ToUnderlying(nType), proTxHash.ToString(), netInfo->ToString(), payee, - platformNodeID.ToString(), platformP2PPort, platformHTTPPort); + platformNodeID.ToString(), + (nVersion >= ProTxVersion::ExtAddr + ? "" + : strprintf(", platformP2PPort=%d, platformHTTPPort=%d", platformP2PPort, platformHTTPPort))); } bool CProUpRegTx::IsTriviallyValid(gsl::not_null pindexPrev, TxValidationState& state) const diff --git a/src/evo/providertx.h b/src/evo/providertx.h index 9b756e20e997..f2173b20e086 100644 --- a/src/evo/providertx.h +++ b/src/evo/providertx.h @@ -102,9 +102,12 @@ class CProRegTx ); if (obj.nType == MnType::Evo) { READWRITE( - obj.platformNodeID, + obj.platformNodeID); + if (obj.nVersion < ProTxVersion::ExtAddr) { + READWRITE( obj.platformP2PPort, obj.platformHTTPPort); + } } if (!(s.GetType() & SER_GETHASH)) { READWRITE(obj.vchSig); @@ -161,9 +164,12 @@ class CProUpServTx ); if (obj.nType == MnType::Evo) { READWRITE( - obj.platformNodeID, + obj.platformNodeID); + if (obj.nVersion < ProTxVersion::ExtAddr) { + READWRITE( obj.platformP2PPort, obj.platformHTTPPort); + } } if (!(s.GetType() & SER_GETHASH)) { READWRITE( diff --git a/src/evo/simplifiedmns.cpp b/src/evo/simplifiedmns.cpp index 6daec5f0f14e..fcbc16d7587a 100644 --- a/src/evo/simplifiedmns.cpp +++ b/src/evo/simplifiedmns.cpp @@ -58,10 +58,11 @@ std::string CSimplifiedMNListEntry::ToString() const return strprintf("CSimplifiedMNListEntry(nVersion=%d, nType=%d, proRegTxHash=%s, confirmedHash=%s, netInfo=%s, " "pubKeyOperator=%s, votingAddress=%s, isValid=%d, payoutAddress=%s, operatorPayoutAddress=%s, " - "platformHTTPPort=%d, platformNodeID=%s)\n", + "platformNodeID=%s%s)\n", nVersion, ToUnderlying(nType), proRegTxHash.ToString(), confirmedHash.ToString(), netInfo->ToString(), pubKeyOperator.ToString(), EncodeDestination(PKHash(keyIDVoting)), isValid, - payoutAddress, operatorPayoutAddress, platformHTTPPort, platformNodeID.ToString()); + payoutAddress, operatorPayoutAddress, platformNodeID.ToString(), + (nVersion >= ProTxVersion::ExtAddr ? "" : strprintf(", platformHTTPPort=%d", platformHTTPPort))); } CSimplifiedMNList::CSimplifiedMNList(std::vector>&& smlEntries) diff --git a/src/evo/simplifiedmns.h b/src/evo/simplifiedmns.h index 7fef3623aa95..9153d0b025e1 100644 --- a/src/evo/simplifiedmns.h +++ b/src/evo/simplifiedmns.h @@ -65,7 +65,8 @@ class CSimplifiedMNListEntry SERIALIZE_METHODS(CSimplifiedMNListEntry, obj) { if ((s.GetType() & SER_NETWORK) && s.GetVersion() >= SMNLE_VERSIONED_PROTO_VERSION) { - READWRITE(obj.nVersion); + READWRITE( + obj.nVersion); } READWRITE( obj.proRegTxHash, @@ -74,16 +75,20 @@ class CSimplifiedMNListEntry obj.nVersion >= ProTxVersion::ExtAddr), CBLSLazyPublicKeyVersionWrapper(const_cast(obj.pubKeyOperator), (obj.nVersion == ProTxVersion::LegacyBLS)), obj.keyIDVoting, - obj.isValid - ); + obj.isValid); if ((s.GetType() & SER_NETWORK) && s.GetVersion() < DMN_TYPE_PROTO_VERSION) { return; } if (obj.nVersion >= ProTxVersion::BasicBLS) { - READWRITE(obj.nType); + READWRITE( + obj.nType); if (obj.nType == MnType::Evo) { - READWRITE(obj.platformHTTPPort); - READWRITE(obj.platformNodeID); + if (obj.nVersion < ProTxVersion::ExtAddr) { + READWRITE( + obj.platformHTTPPort); + } + READWRITE( + obj.platformNodeID); } } } diff --git a/src/evo/specialtxman.cpp b/src/evo/specialtxman.cpp index 0e8fa765e0de..9fcffe753299 100644 --- a/src/evo/specialtxman.cpp +++ b/src/evo/specialtxman.cpp @@ -323,8 +323,10 @@ bool CSpecialTxProcessor::BuildNewListFromBlock(const CBlock& block, gsl::not_nu newState->scriptOperatorPayout = opt_proTx->scriptOperatorPayout; if (opt_proTx->nType == MnType::Evo) { newState->platformNodeID = opt_proTx->platformNodeID; - newState->platformP2PPort = opt_proTx->platformP2PPort; - newState->platformHTTPPort = opt_proTx->platformHTTPPort; + if (opt_proTx->nVersion < ProTxVersion::ExtAddr) { + newState->platformP2PPort = opt_proTx->platformP2PPort; + newState->platformHTTPPort = opt_proTx->platformHTTPPort; + } } if (newState->IsBanned()) { // only revive when all keys are set From 7664ecf00c84c2ebaf6b9acdcc4b022f1ab0674d Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Sun, 14 Sep 2025 22:36:10 +0000 Subject: [PATCH 10/27] refactor: consolidate input processing in ProcessNetInfo*, update errs Co-Authored-By: Konstantin Akimov --- src/rpc/evo_util.cpp | 71 ++++++++++++++++------------------ src/rpc/evo_util.h | 10 +++-- test/functional/rpc_netinfo.py | 10 ++--- 3 files changed, 44 insertions(+), 47 deletions(-) diff --git a/src/rpc/evo_util.cpp b/src/rpc/evo_util.cpp index 714b7bd22aeb..dcfb10122396 100644 --- a/src/rpc/evo_util.cpp +++ b/src/rpc/evo_util.cpp @@ -13,61 +13,55 @@ #include -template -void ProcessNetInfoCore(T1& ptx, const UniValue& input, const bool optional) +namespace { +template +void ParseInput(ProTx& ptx, std::string_view field_name, const std::string& input_str, NetInfoPurpose purpose, + size_t idx, bool optional) { - CHECK_NONFATAL(ptx.netInfo); - - if (input.isStr()) { - const std::string& entry = input.get_str(); - if (entry.empty()) { - if (!optional) { - throw JSONRPCError(RPC_INVALID_PARAMETER, "Empty param for coreP2PAddrs not allowed"); - } - return; // Nothing to do - } - if (auto entryRet = ptx.netInfo->AddEntry(NetInfoPurpose::CORE_P2P, entry); entryRet != NetInfoStatus::Success) { + if (input_str.empty()) { + if (!optional) { throw JSONRPCError(RPC_INVALID_PARAMETER, - strprintf("Error setting coreP2PAddrs[0] to '%s' (%s)", entry, NISToString(entryRet))); + strprintf("Invalid param for %s[%zu], cannot be empty", field_name, idx)); } - return; // Parsing complete + return; // Nothing to do } + if (auto ret = ptx.netInfo->AddEntry(purpose, input_str); ret != NetInfoStatus::Success) { + throw JSONRPCError(RPC_INVALID_PARAMETER, + strprintf("Error setting %s[%zu] to '%s' (%s)", field_name, idx, input_str, NISToString(ret))); + } +} +} // anonymous namespace - if (input.isArray()) { +template +void ProcessNetInfoCore(ProTx& ptx, const UniValue& input, const bool optional) +{ + CHECK_NONFATAL(ptx.netInfo); + + if (input.isStr()) { + ParseInput(ptx, /*field_name=*/"coreP2PAddrs", input.get_str(), NetInfoPurpose::CORE_P2P, /*idx=*/0, optional); + } else if (input.isArray()) { const UniValue& entries = input.get_array(); - if (entries.empty()) { - if (!optional) { - throw JSONRPCError(RPC_INVALID_PARAMETER, "Empty params for coreP2PAddrs not allowed"); - } - return; // Nothing to do + if (!optional && entries.empty()) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid param for coreP2PAddrs, cannot be empty"); } 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 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 coreP2PAddrs[%d], cannot be empty string", idx)); - } - if (auto entryRet = ptx.netInfo->AddEntry(NetInfoPurpose::CORE_P2P, entry); entryRet != NetInfoStatus::Success) { - throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Error setting coreP2PAddrs[%d] to '%s' (%s)", idx, - entry, NISToString(entryRet))); + strprintf("Invalid param for coreP2PAddrs[%zu], must be string", idx)); } + ParseInput(ptx, /*field_name=*/"coreP2PAddrs", entry_uv.get_str(), NetInfoPurpose::CORE_P2P, idx, + /*optional=*/false); } - return; // Parsing complete + } else { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid param for coreP2PAddrs, must be string or array"); } - - // Invalid input - 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); -template -void ProcessNetInfoPlatform(T1& ptx, const UniValue& input_p2p, const UniValue& input_http) +template +void ProcessNetInfoPlatform(ProTx& ptx, const UniValue& input_p2p, const UniValue& input_http) { CHECK_NONFATAL(ptx.netInfo); @@ -78,7 +72,8 @@ void ProcessNetInfoPlatform(T1& ptx, const UniValue& input_p2p, const UniValue& 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)); + throw JSONRPCError(RPC_INVALID_PARAMETER, + strprintf("Invalid param for %s, must be a valid port [1-65535]", field_name)); } }; process_field(ptx.platformP2PPort, input_p2p, "platformP2PPort"); diff --git a/src/rpc/evo_util.h b/src/rpc/evo_util.h index de06acc36028..c99eb775af52 100644 --- a/src/rpc/evo_util.h +++ b/src/rpc/evo_util.h @@ -7,10 +7,12 @@ class UniValue; -template -void ProcessNetInfoCore(T1& ptx, const UniValue& input, const bool optional); +/** Process setting (legacy) Core network information field based on ProTx version */ +template +void ProcessNetInfoCore(ProTx& ptx, const UniValue& input, const bool optional); -template -void ProcessNetInfoPlatform(T1& ptx, const UniValue& input_p2p, const UniValue& input_http); +/** Process setting (legacy) Platform network information fields based on ProTx version */ +template +void ProcessNetInfoPlatform(ProTx& ptx, const UniValue& input_p2p, const UniValue& input_http); #endif // BITCOIN_RPC_EVO_UTIL_H diff --git a/test/functional/rpc_netinfo.py b/test/functional/rpc_netinfo.py index cbe2129d33da..63a5a13ef9c1 100755 --- a/test/functional/rpc_netinfo.py +++ b/test/functional/rpc_netinfo.py @@ -176,20 +176,20 @@ def test_validation_common(self): -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") + -8, "Invalid param for coreP2PAddrs[1], cannot be empty") 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]") + -8, "Invalid param for 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]") + -8, "Invalid param for 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]") + -8, "Invalid param for 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]") + -8, "Invalid param for platformHTTPPort, must be a valid port [1-65535]") def test_validation_legacy(self): # Using mainnet P2P port gets refused From 1d36005de8609b74aa7aa71b2e3fd18b3ab85045 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Sun, 14 Sep 2025 19:54:45 +0000 Subject: [PATCH 11/27] rpc: set `platform{HTTP,P2P}Port` with `netInfo`, allow addr:port str Co-Authored-By: Konstantin Akimov --- src/rpc/evo.cpp | 10 ++++---- src/rpc/evo_util.cpp | 43 +++++++++++++++++++++++++++------- test/functional/rpc_netinfo.py | 4 ++-- 3 files changed, 43 insertions(+), 14 deletions(-) diff --git a/src/rpc/evo.cpp b/src/rpc/evo.cpp index 02e2fb68de23..61483cf330a1 100644 --- a/src/rpc/evo.cpp +++ b/src/rpc/evo.cpp @@ -184,12 +184,14 @@ static RPCArg GetRpcArg(const std::string& strParamName) "Platform P2P node ID, derived from P2P public key."} }, {"platformP2PPort", - {"platformP2PPort", RPCArg::Type::NUM, RPCArg::Optional::NO, - "TCP port of Dash Platform peer-to-peer communication between nodes (network byte order)."} + {"platformP2PPort", RPCArg::Type::STR, RPCArg::Optional::NO, + "Address in the form \"ADDR:PORT\" used by Platform for peer-to-peer connection.\n" + "Must be unique on the network."} }, {"platformHTTPPort", - {"platformHTTPPort", RPCArg::Type::NUM, RPCArg::Optional::NO, - "TCP port of Platform HTTP/API interface (network byte order)."} + {"platformHTTPPort", RPCArg::Type::STR, RPCArg::Optional::NO, + "Address in the form \"ADDR:PORT\" used by Platform for their HTTPS API.\n" + "Must be unique on the network."} }, }; diff --git a/src/rpc/evo_util.cpp b/src/rpc/evo_util.cpp index dcfb10122396..a918ab1a0ddc 100644 --- a/src/rpc/evo_util.cpp +++ b/src/rpc/evo_util.cpp @@ -14,6 +14,8 @@ #include namespace { +bool IsNumeric(std::string_view input) { return input.find_first_not_of("0123456789") == std::string::npos; } + template void ParseInput(ProTx& ptx, std::string_view field_name, const std::string& input_str, NetInfoPurpose purpose, size_t idx, bool optional) @@ -65,19 +67,44 @@ void ProcessNetInfoPlatform(ProTx& ptx, const UniValue& input_p2p, const UniValu { CHECK_NONFATAL(ptx.netInfo); - auto process_field = [](uint16_t& target, const UniValue& input, const std::string& field_name) { + auto process_field = [&](uint16_t& maybe_target, const UniValue& input, const NetInfoPurpose purpose, + std::string_view field_name) { if (!input.isNum() && !input.isStr()) { - throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Invalid param for %s, must be number", field_name)); + throw JSONRPCError(RPC_INVALID_PARAMETER, + strprintf("Invalid param for %s, must be number or string", field_name)); } - if (int32_t port{ParseInt32V(input, field_name)}; port >= 1 && port <= std::numeric_limits::max()) { - target = static_cast(port); + + const auto& input_str{input.getValStr()}; + if (!IsNumeric(input_str)) { + // Cannot be parsed as a number (port) so must be an addr:port string + if (!ptx.netInfo->CanStorePlatform()) { + throw JSONRPCError(RPC_INVALID_PARAMETER, + strprintf("Invalid param for %s, ProTx version only supports ports", field_name)); + } + ParseInput(ptx, field_name, input.get_str(), purpose, /*idx=*/0, /*optional=*/false); } else { - throw JSONRPCError(RPC_INVALID_PARAMETER, - strprintf("Invalid param for %s, must be a valid port [1-65535]", field_name)); + if (int32_t port{0}; ParseInt32(input_str, &port) && port >= 1 && port <= std::numeric_limits::max()) { + // Valid port + if (!ptx.netInfo->CanStorePlatform()) { + maybe_target = static_cast(port); + return; // Parsing complete + } + // We cannot store *only* a port number in netInfo so we need to associate it with the primary service of CORE_P2P manually + if (!ptx.netInfo->HasEntries(NetInfoPurpose::CORE_P2P)) { + throw JSONRPCError(RPC_INVALID_PARAMETER, + strprintf("Cannot set param for %s, must specify coreP2PAddrs first", field_name)); + } + const CService service{CNetAddr{ptx.netInfo->GetPrimary()}, static_cast(port)}; + CHECK_NONFATAL(service.IsValid()); + ParseInput(ptx, field_name, service.ToStringAddrPort(), purpose, /*idx=*/0, /*optional=*/false); + } else { + throw JSONRPCError(RPC_INVALID_PARAMETER, + strprintf("Invalid param for %s, must be a valid port [1-65535]", field_name)); + } } }; - process_field(ptx.platformP2PPort, input_p2p, "platformP2PPort"); - process_field(ptx.platformHTTPPort, input_http, "platformHTTPPort"); + process_field(ptx.platformP2PPort, input_p2p, NetInfoPurpose::PLATFORM_P2P, "platformP2PPort"); + process_field(ptx.platformHTTPPort, input_http, NetInfoPurpose::PLATFORM_HTTPS, "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/test/functional/rpc_netinfo.py b/test/functional/rpc_netinfo.py index 63a5a13ef9c1..c970a95cfe06 100755 --- a/test/functional/rpc_netinfo.py +++ b/test/functional/rpc_netinfo.py @@ -204,11 +204,11 @@ def test_validation_legacy(self): # 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}')") + -8, "Invalid param for platformP2PPort, ProTx version only supports ports") 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}')") + -8, "Invalid param for platformHTTPPort, ProTx version only supports ports") 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") From 01ee293dacea6f525b903998fecf517c517faa88 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Sun, 14 Sep 2025 22:38:18 +0000 Subject: [PATCH 12/27] rpc: make setting `platform{HTTP,P2P}Port` optional if using `netInfo` --- src/rpc/evo.cpp | 18 ++++++++++++++---- src/rpc/evo_util.cpp | 20 ++++++++++++++++++-- src/rpc/evo_util.h | 2 +- 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/src/rpc/evo.cpp b/src/rpc/evo.cpp index 61483cf330a1..984e1e6446c2 100644 --- a/src/rpc/evo.cpp +++ b/src/rpc/evo.cpp @@ -184,11 +184,21 @@ static RPCArg GetRpcArg(const std::string& strParamName) "Platform P2P node ID, derived from P2P public key."} }, {"platformP2PPort", + {"platformP2PPort", RPCArg::Type::STR, RPCArg::Optional::NO, + "Address in the form \"ADDR:PORT\" used by Platform for peer-to-peer connection.\n" + "Must be unique on the network. Can be set to an empty string, which will require a ProUpServTx afterwards."} + }, + {"platformP2PPort_update", {"platformP2PPort", RPCArg::Type::STR, RPCArg::Optional::NO, "Address in the form \"ADDR:PORT\" used by Platform for peer-to-peer connection.\n" "Must be unique on the network."} }, {"platformHTTPPort", + {"platformHTTPPort", RPCArg::Type::STR, RPCArg::Optional::NO, + "Address in the form \"ADDR:PORT\" used by Platform for their HTTPS API.\n" + "Must be unique on the network. Can be set to an empty string, which will require a ProUpServTx afterwards."} + }, + {"platformHTTPPort_update", {"platformHTTPPort", RPCArg::Type::STR, RPCArg::Optional::NO, "Address in the form \"ADDR:PORT\" used by Platform for their HTTPS API.\n" "Must be unique on the network."} @@ -736,7 +746,7 @@ static UniValue protx_register_common_wrapper(const JSONRPCRequest& request, } ptx.platformNodeID.SetHex(request.params[paramIdx + 6].get_str()); - ProcessNetInfoPlatform(ptx, request.params[paramIdx + 7], request.params[paramIdx + 8]); + ProcessNetInfoPlatform(ptx, request.params[paramIdx + 7], request.params[paramIdx + 8], /*optional=*/true); paramIdx += 3; } @@ -944,8 +954,8 @@ static RPCHelpMan protx_update_service_evo() GetRpcArg("coreP2PAddrs_update"), GetRpcArg("operatorKey"), GetRpcArg("platformNodeID"), - GetRpcArg("platformP2PPort"), - GetRpcArg("platformHTTPPort"), + GetRpcArg("platformP2PPort_update"), + GetRpcArg("platformHTTPPort_update"), GetRpcArg("operatorPayoutAddress"), GetRpcArg("feeSourceAddress"), GetRpcArg("submit"), @@ -1006,7 +1016,7 @@ static UniValue protx_update_service_common_wrapper(const JSONRPCRequest& reques } ptx.platformNodeID.SetHex(request.params[paramIdx].get_str()); - ProcessNetInfoPlatform(ptx, request.params[paramIdx + 1], request.params[paramIdx + 2]); + ProcessNetInfoPlatform(ptx, request.params[paramIdx + 1], request.params[paramIdx + 2], /*optional=*/false); paramIdx += 3; } diff --git a/src/rpc/evo_util.cpp b/src/rpc/evo_util.cpp index a918ab1a0ddc..5abf8cd4c6b0 100644 --- a/src/rpc/evo_util.cpp +++ b/src/rpc/evo_util.cpp @@ -63,7 +63,7 @@ template void ProcessNetInfoCore(CProRegTx& ptx, const UniValue& input, const bo template void ProcessNetInfoCore(CProUpServTx& ptx, const UniValue& input, const bool optional); template -void ProcessNetInfoPlatform(ProTx& ptx, const UniValue& input_p2p, const UniValue& input_http) +void ProcessNetInfoPlatform(ProTx& ptx, const UniValue& input_p2p, const UniValue& input_http, const bool optional) { CHECK_NONFATAL(ptx.netInfo); @@ -75,7 +75,23 @@ void ProcessNetInfoPlatform(ProTx& ptx, const UniValue& input_p2p, const UniValu } const auto& input_str{input.getValStr()}; - if (!IsNumeric(input_str)) { + if (input_str.empty()) { + if (!optional) { + // Mandatory field, cannot specify blank value + throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Invalid param for %s, cannot be empty", field_name)); + } + if (!ptx.netInfo->CanStorePlatform()) { + // We can tolerate blank values if netInfo can store platform fields, if it cannot, we are relying + // on platform{HTTP,P2P}Port, where it is mandatory even if their netInfo counterpart is optional. + throw JSONRPCError(RPC_INVALID_PARAMETER, + strprintf("Invalid param for %s, ProTx version only supports ports", field_name)); + } + if (!ptx.netInfo->IsEmpty()) { + // Blank values are tolerable so long as no other field has been populated. + throw JSONRPCError(RPC_INVALID_PARAMETER, + strprintf("Invalid param for %s, cannot be empty if other fields populated", field_name)); + } + } else if (!IsNumeric(input_str)) { // Cannot be parsed as a number (port) so must be an addr:port string if (!ptx.netInfo->CanStorePlatform()) { throw JSONRPCError(RPC_INVALID_PARAMETER, diff --git a/src/rpc/evo_util.h b/src/rpc/evo_util.h index c99eb775af52..15b78398536d 100644 --- a/src/rpc/evo_util.h +++ b/src/rpc/evo_util.h @@ -13,6 +13,6 @@ void ProcessNetInfoCore(ProTx& ptx, const UniValue& input, const bool optional); /** Process setting (legacy) Platform network information fields based on ProTx version */ template -void ProcessNetInfoPlatform(ProTx& ptx, const UniValue& input_p2p, const UniValue& input_http); +void ProcessNetInfoPlatform(ProTx& ptx, const UniValue& input_p2p, const UniValue& input_http, const bool optional); #endif // BITCOIN_RPC_EVO_UTIL_H From d519eea3670a45aac0423eda8363ba72956da221 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Sun, 14 Sep 2025 22:25:33 +0000 Subject: [PATCH 13/27] rpc: allow multiple entries in `platform{HTTP,P2P}Port` Co-Authored-By: Konstantin Akimov --- src/rpc/evo.cpp | 36 +++++++++++++------- src/rpc/evo_util.cpp | 61 +++++++++++++++++++++------------- test/functional/rpc_netinfo.py | 4 +-- 3 files changed, 64 insertions(+), 37 deletions(-) diff --git a/src/rpc/evo.cpp b/src/rpc/evo.cpp index 984e1e6446c2..f75e5f6f9da8 100644 --- a/src/rpc/evo.cpp +++ b/src/rpc/evo.cpp @@ -184,24 +184,36 @@ static RPCArg GetRpcArg(const std::string& strParamName) "Platform P2P node ID, derived from P2P public key."} }, {"platformP2PPort", - {"platformP2PPort", RPCArg::Type::STR, RPCArg::Optional::NO, - "Address in the form \"ADDR:PORT\" used by Platform for peer-to-peer connection.\n" - "Must be unique on the network. Can be set to an empty string, which will require a ProUpServTx afterwards."} + {"platformP2PPort", RPCArg::Type::ARR, RPCArg::Optional::NO, + "Array of addresses in the form \"ADDR:PORT\" used by Platform for peer-to-peer connection.\n" + "Must be unique on the network. Can be set to an empty string, which will require a ProUpServTx afterwards.", + { + {"address", RPCArg::Type::STR, RPCArg::Optional::NO, ""}, + }} }, {"platformP2PPort_update", - {"platformP2PPort", RPCArg::Type::STR, RPCArg::Optional::NO, - "Address in the form \"ADDR:PORT\" used by Platform for peer-to-peer connection.\n" - "Must be unique on the network."} + {"platformP2PPort", RPCArg::Type::ARR, RPCArg::Optional::NO, + "Array of addresses in the form \"ADDR:PORT\" used by Platform for peer-to-peer connection.\n" + "Must be unique on the network.", + { + {"address", RPCArg::Type::STR, RPCArg::Optional::NO, ""}, + }} }, {"platformHTTPPort", - {"platformHTTPPort", RPCArg::Type::STR, RPCArg::Optional::NO, - "Address in the form \"ADDR:PORT\" used by Platform for their HTTPS API.\n" - "Must be unique on the network. Can be set to an empty string, which will require a ProUpServTx afterwards."} + {"platformHTTPPort", RPCArg::Type::ARR, RPCArg::Optional::NO, + "Array of addresses in the form \"ADDR:PORT\" used by Platform for their HTTPS API.\n" + "Must be unique on the network. Can be set to an empty string, which will require a ProUpServTx afterwards.", + { + {"address", RPCArg::Type::STR, RPCArg::Optional::NO, ""}, + }} }, {"platformHTTPPort_update", - {"platformHTTPPort", RPCArg::Type::STR, RPCArg::Optional::NO, - "Address in the form \"ADDR:PORT\" used by Platform for their HTTPS API.\n" - "Must be unique on the network."} + {"platformHTTPPort", RPCArg::Type::ARR, RPCArg::Optional::NO, + "Array of addresses in the form \"ADDR:PORT\" used by Platform for their HTTPS API.\n" + "Must be unique on the network.", + { + {"address", RPCArg::Type::STR, RPCArg::Optional::NO, ""}, + }} }, }; diff --git a/src/rpc/evo_util.cpp b/src/rpc/evo_util.cpp index 5abf8cd4c6b0..26f199a78b52 100644 --- a/src/rpc/evo_util.cpp +++ b/src/rpc/evo_util.cpp @@ -69,37 +69,52 @@ void ProcessNetInfoPlatform(ProTx& ptx, const UniValue& input_p2p, const UniValu auto process_field = [&](uint16_t& maybe_target, const UniValue& input, const NetInfoPurpose purpose, std::string_view field_name) { - if (!input.isNum() && !input.isStr()) { + if (!input.isArray() && !input.isNum() && !input.isStr()) { throw JSONRPCError(RPC_INVALID_PARAMETER, - strprintf("Invalid param for %s, must be number or string", field_name)); + strprintf("Invalid param for %s, must be array, number or string", field_name)); } - const auto& input_str{input.getValStr()}; - if (input_str.empty()) { - if (!optional) { - // Mandatory field, cannot specify blank value - throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Invalid param for %s, cannot be empty", field_name)); + bool is_empty{input.isArray() ? input.get_array().empty() : input.getValStr().empty()}; + bool is_nonnumeric_str{input.isStr() && !IsNumeric(input.getValStr())}; + if (is_empty || is_nonnumeric_str || input.isArray()) { + if (is_empty) { + if (!optional) { + // Mandatory field, cannot specify blank value + throw JSONRPCError(RPC_INVALID_PARAMETER, + strprintf("Invalid param for %s, cannot be empty", field_name)); + } + if (!ptx.netInfo->IsEmpty()) { + // Blank values are tolerable so long as no other field has been populated. + throw JSONRPCError(RPC_INVALID_PARAMETER, + strprintf("Invalid param for %s, cannot be empty if other fields populated", + field_name)); + } } if (!ptx.netInfo->CanStorePlatform()) { - // We can tolerate blank values if netInfo can store platform fields, if it cannot, we are relying - // on platform{HTTP,P2P}Port, where it is mandatory even if their netInfo counterpart is optional. + // Arrays: Expected to be address strings, if relying on platform{HTTP,P2P}Port, bail out. + // Empty Input: We can tolerate blank values if netInfo can store platform fields, if it cannot, we are relying + // on platform{HTTP,P2P}Port, where it is mandatory even if their netInfo counterpart is optional. + // String: If not parsable as port and relying on platform{HTTP,P2P}Port, bail out. throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Invalid param for %s, ProTx version only supports ports", field_name)); } - if (!ptx.netInfo->IsEmpty()) { - // Blank values are tolerable so long as no other field has been populated. - throw JSONRPCError(RPC_INVALID_PARAMETER, - strprintf("Invalid param for %s, cannot be empty if other fields populated", field_name)); - } - } else if (!IsNumeric(input_str)) { - // Cannot be parsed as a number (port) so must be an addr:port string - if (!ptx.netInfo->CanStorePlatform()) { - throw JSONRPCError(RPC_INVALID_PARAMETER, - strprintf("Invalid param for %s, ProTx version only supports ports", field_name)); + if (input.isArray()) { + const UniValue& entries = input.get_array(); + for (size_t idx{0}; idx < entries.size(); idx++) { + const UniValue& entry{entries[idx]}; + if (!entry.isStr() || IsNumeric(entry.get_str())) { + throw JSONRPCError(RPC_INVALID_PARAMETER, + strprintf("Invalid param for %s[%zu], must be string", field_name, idx)); + } + ParseInput(ptx, field_name, entry.get_str(), purpose, idx, /*optional=*/false); + } + } else { + CHECK_NONFATAL(is_empty || is_nonnumeric_str); + ParseInput(ptx, field_name, input.get_str(), purpose, /*idx=*/0, /*optional=*/true); } - ParseInput(ptx, field_name, input.get_str(), purpose, /*idx=*/0, /*optional=*/false); } else { - if (int32_t port{0}; ParseInt32(input_str, &port) && port >= 1 && port <= std::numeric_limits::max()) { + if (int32_t port{0}; + ParseInt32(input.getValStr(), &port) && port >= 1 && port <= std::numeric_limits::max()) { // Valid port if (!ptx.netInfo->CanStorePlatform()) { maybe_target = static_cast(port); @@ -122,5 +137,5 @@ void ProcessNetInfoPlatform(ProTx& ptx, const UniValue& input_p2p, const UniValu process_field(ptx.platformP2PPort, input_p2p, NetInfoPurpose::PLATFORM_P2P, "platformP2PPort"); process_field(ptx.platformHTTPPort, input_http, NetInfoPurpose::PLATFORM_HTTPS, "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); +template void ProcessNetInfoPlatform(CProRegTx& ptx, const UniValue& input_p2p, const UniValue& input_http, const bool optional); +template void ProcessNetInfoPlatform(CProUpServTx& ptx, const UniValue& input_p2p, const UniValue& input_http, const bool optional); diff --git a/test/functional/rpc_netinfo.py b/test/functional/rpc_netinfo.py index c970a95cfe06..ac67d62dc943 100755 --- a/test/functional/rpc_netinfo.py +++ b/test/functional/rpc_netinfo.py @@ -206,11 +206,11 @@ def test_validation_legacy(self): 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, ProTx version only supports ports") 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") + -8, "Invalid param for platformP2PPort, ProTx version only supports ports") 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, ProTx version only supports ports") 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") + -8, "Invalid param for platformHTTPPort, ProTx version only supports ports") def test_deprecation(self): # netInfo is represented with JSON in CProRegTx, CProUpServTx, CDeterministicMNState and CSimplifiedMNListEntry, From f59f9f5229d4316a208002b666baf6638db1cd7b Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Tue, 5 Aug 2025 07:58:34 +0000 Subject: [PATCH 14/27] refactor(rpc): `platform{HTTP,P2P}Port` > `platform{HTTPS,P2P}Addrs` The current name is now a misnomer as we accept (multiple) addr:port pairs, the name has been updated to reflect that. We still support submitting only ports but that is transitional support that may be removed in a future release. --- src/rpc/evo.cpp | 32 ++++++++++++++++---------------- src/rpc/evo_util.cpp | 4 ++-- test/functional/rpc_netinfo.py | 24 ++++++++++++------------ 3 files changed, 30 insertions(+), 30 deletions(-) diff --git a/src/rpc/evo.cpp b/src/rpc/evo.cpp index f75e5f6f9da8..cc7052b453c3 100644 --- a/src/rpc/evo.cpp +++ b/src/rpc/evo.cpp @@ -183,32 +183,32 @@ static RPCArg GetRpcArg(const std::string& strParamName) {"platformNodeID", RPCArg::Type::STR, RPCArg::Optional::NO, "Platform P2P node ID, derived from P2P public key."} }, - {"platformP2PPort", - {"platformP2PPort", RPCArg::Type::ARR, RPCArg::Optional::NO, + {"platformP2PAddrs", + {"platformP2PAddrs", RPCArg::Type::ARR, RPCArg::Optional::NO, "Array of addresses in the form \"ADDR:PORT\" used by Platform for peer-to-peer connection.\n" "Must be unique on the network. Can be set to an empty string, which will require a ProUpServTx afterwards.", { {"address", RPCArg::Type::STR, RPCArg::Optional::NO, ""}, }} }, - {"platformP2PPort_update", - {"platformP2PPort", RPCArg::Type::ARR, RPCArg::Optional::NO, + {"platformP2PAddrs_update", + {"platformP2PAddrs", RPCArg::Type::ARR, RPCArg::Optional::NO, "Array of addresses in the form \"ADDR:PORT\" used by Platform for peer-to-peer connection.\n" "Must be unique on the network.", { {"address", RPCArg::Type::STR, RPCArg::Optional::NO, ""}, }} }, - {"platformHTTPPort", - {"platformHTTPPort", RPCArg::Type::ARR, RPCArg::Optional::NO, + {"platformHTTPSAddrs", + {"platformHTTPSAddrs", RPCArg::Type::ARR, RPCArg::Optional::NO, "Array of addresses in the form \"ADDR:PORT\" used by Platform for their HTTPS API.\n" "Must be unique on the network. Can be set to an empty string, which will require a ProUpServTx afterwards.", { {"address", RPCArg::Type::STR, RPCArg::Optional::NO, ""}, }} }, - {"platformHTTPPort_update", - {"platformHTTPPort", RPCArg::Type::ARR, RPCArg::Optional::NO, + {"platformHTTPSAddrs_update", + {"platformHTTPSAddrs", RPCArg::Type::ARR, RPCArg::Optional::NO, "Array of addresses in the form \"ADDR:PORT\" used by Platform for their HTTPS API.\n" "Must be unique on the network.", { @@ -575,8 +575,8 @@ static RPCHelpMan protx_register_fund_evo() GetRpcArg("operatorReward"), GetRpcArg("payoutAddress_register"), GetRpcArg("platformNodeID"), - GetRpcArg("platformP2PPort"), - GetRpcArg("platformHTTPPort"), + GetRpcArg("platformP2PAddrs"), + GetRpcArg("platformHTTPSAddrs"), GetRpcArg("fundAddress"), GetRpcArg("submit"), }, @@ -614,8 +614,8 @@ static RPCHelpMan protx_register_evo() GetRpcArg("operatorReward"), GetRpcArg("payoutAddress_register"), GetRpcArg("platformNodeID"), - GetRpcArg("platformP2PPort"), - GetRpcArg("platformHTTPPort"), + GetRpcArg("platformP2PAddrs"), + GetRpcArg("platformHTTPSAddrs"), GetRpcArg("feeSourceAddress"), GetRpcArg("submit"), }, @@ -652,8 +652,8 @@ static RPCHelpMan protx_register_prepare_evo() GetRpcArg("operatorReward"), GetRpcArg("payoutAddress_register"), GetRpcArg("platformNodeID"), - GetRpcArg("platformP2PPort"), - GetRpcArg("platformHTTPPort"), + GetRpcArg("platformP2PAddrs"), + GetRpcArg("platformHTTPSAddrs"), GetRpcArg("feeSourceAddress"), }, RPCResult{ @@ -966,8 +966,8 @@ static RPCHelpMan protx_update_service_evo() GetRpcArg("coreP2PAddrs_update"), GetRpcArg("operatorKey"), GetRpcArg("platformNodeID"), - GetRpcArg("platformP2PPort_update"), - GetRpcArg("platformHTTPPort_update"), + GetRpcArg("platformP2PAddrs_update"), + GetRpcArg("platformHTTPSAddrs_update"), GetRpcArg("operatorPayoutAddress"), GetRpcArg("feeSourceAddress"), GetRpcArg("submit"), diff --git a/src/rpc/evo_util.cpp b/src/rpc/evo_util.cpp index 26f199a78b52..14f3d7fe1bc5 100644 --- a/src/rpc/evo_util.cpp +++ b/src/rpc/evo_util.cpp @@ -134,8 +134,8 @@ void ProcessNetInfoPlatform(ProTx& ptx, const UniValue& input_p2p, const UniValu } } }; - process_field(ptx.platformP2PPort, input_p2p, NetInfoPurpose::PLATFORM_P2P, "platformP2PPort"); - process_field(ptx.platformHTTPPort, input_http, NetInfoPurpose::PLATFORM_HTTPS, "platformHTTPPort"); + process_field(ptx.platformP2PPort, input_p2p, NetInfoPurpose::PLATFORM_P2P, "platformP2PAddrs"); + process_field(ptx.platformHTTPPort, input_http, NetInfoPurpose::PLATFORM_HTTPS, "platformHTTPSAddrs"); } template void ProcessNetInfoPlatform(CProRegTx& ptx, const UniValue& input_p2p, const UniValue& input_http, const bool optional); template void ProcessNetInfoPlatform(CProUpServTx& ptx, const UniValue& input_p2p, const UniValue& input_http, const bool optional); diff --git a/test/functional/rpc_netinfo.py b/test/functional/rpc_netinfo.py index ac67d62dc943..e86e757f5f8c 100755 --- a/test/functional/rpc_netinfo.py +++ b/test/functional/rpc_netinfo.py @@ -65,7 +65,7 @@ def register_mn(self, test: BitcoinTestFramework, submit: bool, addrs_core_p2p, 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") + raise AssertionError("EvoNode but platformP2PAddrs and platformHTTPSAddrs not specified") # Evonode-specific fields are ignored if regular masternode self.platform_nodeid = hash160(b'%d' % randint(1, 65535)).hex() @@ -97,7 +97,7 @@ def update_mn(self, test: BitcoinTestFramework, addrs_core_p2p, addrs_platform_p 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") + raise AssertionError("EvoNode but platformP2PAddrs and platformHTTPSAddrs 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, @@ -181,15 +181,15 @@ def test_validation_common(self): 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) + # platformP2PAddrs and platformHTTPSAddrs 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, "Invalid param for platformP2PPort, must be a valid port [1-65535]") + -8, "Invalid param for platformP2PAddrs, 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, "Invalid param for platformP2PPort, must be a valid port [1-65535]") + -8, "Invalid param for platformP2PAddrs, 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, "Invalid param for platformHTTPPort, must be a valid port [1-65535]") + -8, "Invalid param for platformHTTPSAddrs, 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, "Invalid param for platformHTTPPort, must be a valid port [1-65535]") + -8, "Invalid param for platformHTTPSAddrs, must be a valid port [1-65535]") def test_validation_legacy(self): # Using mainnet P2P port gets refused @@ -202,15 +202,15 @@ def test_validation_legacy(self): 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 + # platformP2PAddrs and platformHTTPSAddrs don'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, "Invalid param for platformP2PPort, ProTx version only supports ports") + -8, "Invalid param for platformP2PAddrs, ProTx version only supports ports") 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, ProTx version only supports ports") + -8, "Invalid param for platformP2PAddrs, ProTx version only supports ports") 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, ProTx version only supports ports") + -8, "Invalid param for platformHTTPSAddrs, ProTx version only supports ports") 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, ProTx version only supports ports") + -8, "Invalid param for platformHTTPSAddrs, ProTx version only supports ports") def test_deprecation(self): # netInfo is represented with JSON in CProRegTx, CProUpServTx, CDeterministicMNState and CSimplifiedMNListEntry, From f04ed9984933ff1c77c316191fcd4278d0549854 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Tue, 5 Aug 2025 08:26:35 +0000 Subject: [PATCH 15/27] refactor(test): `platform_{http,p2p}_port` > `addrs_platform_{https,p2p}` We also update the type annotations to reflect the three different types of valid input (port, single address, array of addresses). --- test/functional/rpc_netinfo.py | 18 ++--- .../test_framework/test_framework.py | 67 ++++++++++--------- 2 files changed, 44 insertions(+), 41 deletions(-) diff --git a/test/functional/rpc_netinfo.py b/test/functional/rpc_netinfo.py index e86e757f5f8c..9afea3daf944 100755 --- a/test/functional/rpc_netinfo.py +++ b/test/functional/rpc_netinfo.py @@ -61,17 +61,17 @@ 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, code = None, msg = None) -> str: + def register_mn(self, test: BitcoinTestFramework, submit: bool, addrs_core_p2p, addrs_platform_p2p = None, addrs_platform_https = 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): - raise AssertionError("EvoNode but platformP2PAddrs and platformHTTPSAddrs not specified") + if self.mn.evo and (not addrs_platform_https or not addrs_platform_p2p): + raise AssertionError("EvoNode but addrs_platform_p2p and addrs_platform_https 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, expected_assert_code=code, expected_assert_msg=msg) + platform_node_id=self.platform_nodeid, addrs_platform_p2p=addrs_platform_p2p, + addrs_platform_https=addrs_platform_https, expected_assert_code=code, expected_assert_msg=msg) # If we expected error, make sure the transaction didn't succeed if code and msg: @@ -93,15 +93,15 @@ def register_mn(self, test: BitcoinTestFramework, submit: bool, addrs_core_p2p, 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: + def update_mn(self, test: BitcoinTestFramework, addrs_core_p2p, addrs_platform_p2p = None, addrs_platform_https = 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 platformP2PAddrs and platformHTTPSAddrs not specified") + if self.mn.evo and (not addrs_platform_https or not addrs_platform_p2p): + raise AssertionError("EvoNode but addrs_platform_p2p and addrs_platform_https 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) + addrs_platform_p2p=addrs_platform_p2p, addrs_platform_https=addrs_platform_https) assert protx_output is not None self.mn.bury_tx(test, self.mn.nodeIdx, protx_output, 1) diff --git a/test/functional/test_framework/test_framework.py b/test/functional/test_framework/test_framework.py index 9e9a20c415e9..15729b5c220b 100755 --- a/test/functional/test_framework/test_framework.py +++ b/test/functional/test_framework/test_framework.py @@ -1221,10 +1221,11 @@ 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, - 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]: + 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, addrs_platform_p2p: Union[int, str, List[str], None] = None, + addrs_platform_https: Union[int, str, List[str], None] = 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): raise AssertionError("Intending to use assert_raises_rpc_error() but didn't specify code and message") @@ -1232,10 +1233,10 @@ def register(self, node: TestNode, submit: bool, collateral_txid: Optional[str] if self.evo: if platform_node_id is None: raise AssertionError("EvoNode but platform_node_id is missing, must be specified!") - if platform_p2p_port is None: - raise AssertionError("EvoNode but platform_p2p_port is missing, must be specified!") - if platform_http_port is None: - raise AssertionError("EvoNode but platform_http_port is missing, must be specified!") + if addrs_platform_p2p is None: + raise AssertionError("EvoNode but addrs_platform_p2p is missing, must be specified!") + if addrs_platform_https is None: + raise AssertionError("EvoNode but addrs_platform_https is missing, must be specified!") # Common arguments shared between regular masternodes and EvoNodes args = [ @@ -1261,7 +1262,7 @@ def register(self, node: TestNode, submit: bool, collateral_txid: Optional[str] # Construct final command and arguments if self.evo: command = "register_evo" - args = args + [platform_node_id, platform_p2p_port, platform_http_port, address_funds, submit] # type: ignore + args = args + [platform_node_id, addrs_platform_p2p, addrs_platform_https, address_funds, submit] # type: ignore else: command = "register_legacy" if self.legacy else "register" args = args + [address_funds, submit] # type: ignore @@ -1279,8 +1280,9 @@ def register(self, node: TestNode, submit: bool, collateral_txid: Optional[str] 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, - expected_assert_code: Optional[int] = None, expected_assert_msg: Optional[str] = None) -> Optional[str]: + platform_node_id: Optional[str] = None, addrs_platform_p2p: Union[int, str, List[str], None] = None, + addrs_platform_https: Union[int, str, List[str], None] = 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): raise AssertionError("Intending to use assert_raises_rpc_error() but didn't specify code and message") @@ -1288,10 +1290,10 @@ def register_fund(self, node: TestNode, submit: bool, collateral_address: Option if self.evo: if platform_node_id is None: raise AssertionError("EvoNode but platform_node_id is missing, must be specified!") - if platform_p2p_port is None: - raise AssertionError("EvoNode but platform_p2p_port is missing, must be specified!") - if platform_http_port is None: - raise AssertionError("EvoNode but platform_http_port is missing, must be specified!") + if addrs_platform_p2p is None: + raise AssertionError("EvoNode but addrs_platform_p2p is missing, must be specified!") + if addrs_platform_https is None: + raise AssertionError("EvoNode but addrs_platform_https is missing, must be specified!") # Use assert_raises_rpc_error if we expect to error out use_assert: bool = bool(expected_assert_code and expected_assert_msg) @@ -1316,7 +1318,7 @@ def register_fund(self, node: TestNode, submit: bool, collateral_address: Option # Construct final command and arguments if self.evo: command = "register_fund_evo" - args = args + [platform_node_id, platform_p2p_port, platform_http_port, address_funds, submit] # type: ignore + args = args + [platform_node_id, addrs_platform_p2p, addrs_platform_https, address_funds, submit] # type: ignore else: command = "register_fund_legacy" if self.legacy else "register_fund" args = args + [address_funds, submit] # type: ignore @@ -1415,9 +1417,10 @@ def update_registrar(self, node: TestNode, submit: bool, pubKeyOperator: Optiona return ret - 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]: + def update_service(self, node: TestNode, submit: bool, coreP2PAddrs: Union[str, List[str], None] = None, platform_node_id: Optional[str] = None, + addrs_platform_p2p: Union[int, str, List[str], None] = None, addrs_platform_https: Union[int, str, List[str], None] = 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): raise AssertionError("Intending to use assert_raises_rpc_error() but didn't specify code and message") @@ -1431,10 +1434,10 @@ def update_service(self, node: TestNode, submit: bool, coreP2PAddrs: Union[str, if self.evo: if platform_node_id is None: raise AssertionError("EvoNode but platform_node_id is missing, must be specified!") - if platform_p2p_port is None: - raise AssertionError("EvoNode but platform_p2p_port is missing, must be specified!") - if platform_http_port is None: - raise AssertionError("EvoNode but platform_http_port is missing, must be specified!") + if addrs_platform_p2p is None: + raise AssertionError("EvoNode but addrs_platform_p2p is missing, must be specified!") + if addrs_platform_https is None: + raise AssertionError("EvoNode but addrs_platform_https is missing, must be specified!") # Use assert_raises_rpc_error if we expect to error out use_assert: bool = bool(expected_assert_code and expected_assert_msg) @@ -1456,7 +1459,7 @@ def update_service(self, node: TestNode, submit: bool, coreP2PAddrs: Union[str, # Construct final command and arguments if self.evo: command = "update_service_evo" - args = args + [platform_node_id, platform_p2p_port, platform_http_port, address_operator, address_funds, submit] # type: ignore + args = args + [platform_node_id, addrs_platform_p2p, addrs_platform_https, address_operator, address_funds, submit] # type: ignore else: command = "update_service" args = args + [address_operator, address_funds, submit] # type: ignore @@ -1641,8 +1644,8 @@ def dynamically_prepare_masternode(self, idx, node_p2p_port, evo=False, rnd=None mn.generate_addresses(self.nodes[0]) platform_node_id = hash160(b'%d' % rnd).hex() if rnd is not None else hash160(b'%d' % node_p2p_port).hex() - platform_p2p_port = node_p2p_port + 101 - platform_http_port = node_p2p_port + 102 + addrs_platform_p2p = node_p2p_port + 101 + addrs_platform_https = node_p2p_port + 102 outputs = {mn.collateral_address: mn.get_collateral_value(), mn.fundsAddr: 1} collateral_txid = self.nodes[0].sendmany("", outputs) @@ -1653,9 +1656,9 @@ def dynamically_prepare_masternode(self, idx, node_p2p_port, evo=False, rnd=None 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 + # platform_node_id, addrs_platform_p2p and addrs_platform_https are ignored for regular masternodes 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) + platform_node_id=platform_node_id, addrs_platform_p2p=addrs_platform_p2p, addrs_platform_https=addrs_platform_https) assert protx_result is not None self.bump_mocktime(10 * 60 + 1) # to make tx safe to include in block @@ -1675,8 +1678,8 @@ def dynamically_evo_update_service(self, evo_info: MasternodeInfo, rnd=None, sho # For the sake of the test, generate random nodeid, p2p and http platform values r = rnd if rnd is not None else random.randint(21000, 65000) platform_node_id = hash160(b'%d' % r).hex() - platform_p2p_port = r + 1 - platform_http_port = r + 2 + addrs_platform_p2p = r + 1 + addrs_platform_https = r + 2 fund_txid = self.nodes[0].sendtoaddress(funds_address, 1) self.bump_mocktime(10 * 60 + 1) # to make tx safe to include in block @@ -1684,11 +1687,11 @@ def dynamically_evo_update_service(self, evo_info: MasternodeInfo, rnd=None, sho protx_success = False try: - protx_result = evo_info.update_service(self.nodes[0], True, f'127.0.0.1:{evo_info.nodePort}', platform_node_id, platform_p2p_port, platform_http_port, operator_reward_address, funds_address) + protx_result = evo_info.update_service(self.nodes[0], True, f'127.0.0.1:{evo_info.nodePort}', platform_node_id, addrs_platform_p2p, addrs_platform_https, operator_reward_address, funds_address) assert protx_result is not None self.bump_mocktime(10 * 60 + 1) # to make tx safe to include in block evo_info.bury_tx(self, genIdx=0, txid=protx_result, depth=1) - self.log.info("Updated EvoNode %s: platformNodeID=%s, platformP2PPort=%s, platformHTTPPort=%s" % (evo_info.proTxHash, platform_node_id, platform_p2p_port, platform_http_port)) + self.log.info("Updated EvoNode %s: platformNodeID=%s, platformP2PPort=%s, platformHTTPPort=%s" % (evo_info.proTxHash, platform_node_id, addrs_platform_p2p, addrs_platform_https)) protx_success = True except: self.log.info("protx_evo rejected") From 8efbad8ba0a2f089e03c3c14e19d1809b65693f8 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Tue, 5 Aug 2025 08:05:25 +0000 Subject: [PATCH 16/27] refactor(test): `coreP2PAddrs` > `addrs_core_p2p` `coreP2PAddrs` is the name given to the input field in Dash Core but the test suite follows a different naming convention, let's harmonize it for good. --- .../feature_dip3_deterministicmns.py | 2 +- test/functional/rpc_netinfo.py | 4 ++-- .../test_framework/test_framework.py | 24 +++++++++---------- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/test/functional/feature_dip3_deterministicmns.py b/test/functional/feature_dip3_deterministicmns.py index 77a5e4768a2b..ac73dd5af055 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, coreP2PAddrs=[f'127.0.0.2:{mn.nodePort}']) + mn.update_service(self.nodes[0], submit=True, addrs_core_p2p=[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/rpc_netinfo.py b/test/functional/rpc_netinfo.py index 9afea3daf944..5af8a4db2013 100755 --- a/test/functional/rpc_netinfo.py +++ b/test/functional/rpc_netinfo.py @@ -69,7 +69,7 @@ def register_mn(self, test: BitcoinTestFramework, submit: bool, addrs_core_p2p, # 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, + protx_output = self.mn.register(self.node, submit=submit, addrs_core_p2p=addrs_core_p2p, operator_reward=0, platform_node_id=self.platform_nodeid, addrs_platform_p2p=addrs_platform_p2p, addrs_platform_https=addrs_platform_https, expected_assert_code=code, expected_assert_msg=msg) @@ -100,7 +100,7 @@ def update_mn(self, test: BitcoinTestFramework, addrs_core_p2p, addrs_platform_p raise AssertionError("EvoNode but addrs_platform_p2p and addrs_platform_https 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, + protx_output = self.mn.update_service(self.node, submit=True, addrs_core_p2p=addrs_core_p2p, platform_node_id=self.platform_nodeid, addrs_platform_p2p=addrs_platform_p2p, addrs_platform_https=addrs_platform_https) assert protx_output is not None diff --git a/test/functional/test_framework/test_framework.py b/test/functional/test_framework/test_framework.py index 15729b5c220b..874d5e0f6e79 100755 --- a/test/functional/test_framework/test_framework.py +++ b/test/functional/test_framework/test_framework.py @@ -1221,7 +1221,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, - coreP2PAddrs: Union[str, List[str], None] = None, ownerAddr: Optional[str] = None, pubKeyOperator: Optional[str] = None, + addrs_core_p2p: 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, addrs_platform_p2p: Union[int, str, List[str], None] = None, addrs_platform_https: Union[int, str, List[str], None] = None, expected_assert_code: Optional[int] = None, @@ -1242,7 +1242,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, - coreP2PAddrs or [f'127.0.0.1:{self.nodePort}'], + addrs_core_p2p or [f'127.0.0.1:{self.nodePort}'], ownerAddr or self.ownerAddr, pubKeyOperator or self.pubKeyOperator, votingAddr or self.votingAddr, @@ -1277,7 +1277,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, coreP2PAddrs: Union[str, List[str], None] = None, + def register_fund(self, node: TestNode, submit: bool, collateral_address: Optional[str] = None, addrs_core_p2p: 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, addrs_platform_p2p: Union[int, str, List[str], None] = None, @@ -1306,7 +1306,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, - coreP2PAddrs or [f'127.0.0.1:{self.nodePort}'], + addrs_core_p2p or [f'127.0.0.1:{self.nodePort}'], ownerAddr or self.ownerAddr, pubKeyOperator or self.pubKeyOperator, votingAddr or self.votingAddr, @@ -1417,7 +1417,7 @@ def update_registrar(self, node: TestNode, submit: bool, pubKeyOperator: Optiona return ret - def update_service(self, node: TestNode, submit: bool, coreP2PAddrs: Union[str, List[str], None] = None, platform_node_id: Optional[str] = None, + def update_service(self, node: TestNode, submit: bool, addrs_core_p2p: Union[str, List[str], None] = None, platform_node_id: Optional[str] = None, addrs_platform_p2p: Union[int, str, List[str], None] = None, addrs_platform_https: Union[int, str, List[str], None] = None, address_operator: Optional[str] = None, fundsAddr: Optional[str] = None, expected_assert_code: Optional[int] = None, expected_assert_msg: Optional[str] = None) -> Optional[str]: @@ -1450,7 +1450,7 @@ def update_service(self, node: TestNode, submit: bool, coreP2PAddrs: Union[str, # Common arguments shared between regular masternodes and EvoNodes args = [ self.proTxHash, - coreP2PAddrs or [f'127.0.0.1:{self.nodePort}'], + addrs_core_p2p or [f'127.0.0.1:{self.nodePort}'], self.keyOperator, ] address_funds = fundsAddr or self.fundsAddr @@ -1653,11 +1653,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) - coreP2PAddrs = ['127.0.0.1:%d' % node_p2p_port] + addrs_core_p2p = ['127.0.0.1:%d' % node_p2p_port] operatorReward = idx # platform_node_id, addrs_platform_p2p and addrs_platform_https are ignored for regular masternodes - protx_result = mn.register(self.nodes[0], submit=True, collateral_txid=collateral_txid, collateral_vout=collateral_vout, coreP2PAddrs=coreP2PAddrs, operator_reward=operatorReward, + protx_result = mn.register(self.nodes[0], submit=True, collateral_txid=collateral_txid, collateral_vout=collateral_vout, addrs_core_p2p=addrs_core_p2p, operator_reward=operatorReward, platform_node_id=platform_node_id, addrs_platform_p2p=addrs_platform_p2p, addrs_platform_https=addrs_platform_https) assert protx_result is not None @@ -1719,16 +1719,16 @@ def prepare_masternode(self, idx): self.nodes[0].sendtoaddress(mn.fundsAddr, 0.001) port = p2p_port(len(self.nodes) + idx) - coreP2PAddrs = ['127.0.0.1:%d' % port] + addrs_core_p2p = ['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, coreP2PAddrs=coreP2PAddrs, operator_reward=operatorReward) + protx_result = mn.register_fund(self.nodes[0], submit=submit, addrs_core_p2p=addrs_core_p2p, 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, coreP2PAddrs=coreP2PAddrs, + protx_result = mn.register(self.nodes[0], submit=submit, collateral_txid=txid, collateral_vout=collateral_vout, addrs_core_p2p=addrs_core_p2p, operator_reward=operatorReward) if submit: proTxHash = protx_result @@ -1740,7 +1740,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, coreP2PAddrs=coreP2PAddrs, address_operator=operatorPayoutAddress) + mn.update_service(self.nodes[0], submit=True, addrs_core_p2p=addrs_core_p2p, 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 17d17af43a0b2d1eb0b463a085a609ab0da0e395 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Sun, 14 Sep 2025 22:42:37 +0000 Subject: [PATCH 17/27] rpc: allow `addresses` to report data from legacy platform port fields MnNetInfo doesn't actually store this information so we are pulling the data from platform{HTTP,P2P}Port to make it appear like it does. This is to ensure reporting parity with ExtNetInfo, which stores platform fields and should allow us to deprecate the platform{HTTP,P2P}Port when we need to. --- src/coinjoin/client.cpp | 3 ++- src/evo/core_write.cpp | 7 ++++--- src/evo/dmnstate.cpp | 4 +++- src/evo/netinfo.h | 3 +++ src/rpc/evo_util.cpp | 3 --- src/rpc/evo_util.h | 36 +++++++++++++++++++++++++++++++++- src/rpc/masternode.cpp | 3 ++- src/rpc/quorums.cpp | 3 ++- test/functional/rpc_netinfo.py | 19 ++++++++++++------ 9 files changed, 64 insertions(+), 17 deletions(-) diff --git a/src/coinjoin/client.cpp b/src/coinjoin/client.cpp index 543bce8f0a32..c39ef15a67e2 100644 --- a/src/coinjoin/client.cpp +++ b/src/coinjoin/client.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -1875,7 +1876,7 @@ void CCoinJoinClientSession::GetJsonInfo(UniValue& obj) const if (m_wallet->chain().rpcEnableDeprecated("service")) { obj.pushKV("service", mixingMasternode->pdmnState->netInfo->GetPrimary().ToStringAddrPort()); } - obj.pushKV("addresses", mixingMasternode->pdmnState->netInfo->ToJson()); + obj.pushKV("addresses", GetNetInfoWithLegacyFields(*mixingMasternode->pdmnState, mixingMasternode->nType)); } 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 bab2073d0f44..2f5a55579c7f 100644 --- a/src/evo/core_write.cpp +++ b/src/evo/core_write.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include @@ -74,7 +75,7 @@ if (IsServiceDeprecatedRPCEnabled()) { ret.pushKV("service", netInfo->GetPrimary().ToStringAddrPort()); } - ret.pushKV("addresses", netInfo->ToJson()); + ret.pushKV("addresses", GetNetInfoWithLegacyFields(*this, nType)); ret.pushKV("ownerAddress", EncodeDestination(PKHash(keyIDOwner))); ret.pushKV("votingAddress", EncodeDestination(PKHash(keyIDVoting))); if (CTxDestination dest; ExtractDestination(scriptPayout, dest)) { @@ -124,7 +125,7 @@ if (IsServiceDeprecatedRPCEnabled()) { ret.pushKV("service", netInfo->GetPrimary().ToStringAddrPort()); } - ret.pushKV("addresses", netInfo->ToJson()); + ret.pushKV("addresses", GetNetInfoWithLegacyFields(*this, nType)); if (CTxDestination dest; ExtractDestination(scriptOperatorPayout, dest)) { ret.pushKV("operatorPayoutAddress", EncodeDestination(dest)); } @@ -164,7 +165,7 @@ if (IsServiceDeprecatedRPCEnabled()) { obj.pushKV("service", netInfo->GetPrimary().ToStringAddrPort()); } - obj.pushKV("addresses", netInfo->ToJson()); + obj.pushKV("addresses", GetNetInfoWithLegacyFields(*this, nType)); obj.pushKV("pubKeyOperator", pubKeyOperator.ToString()); obj.pushKV("votingAddress", EncodeDestination(PKHash(keyIDVoting))); obj.pushKV("isValid", isValid); diff --git a/src/evo/dmnstate.cpp b/src/evo/dmnstate.cpp index 74f346303f3f..0741e3e6b2fc 100644 --- a/src/evo/dmnstate.cpp +++ b/src/evo/dmnstate.cpp @@ -6,6 +6,8 @@ #include