From 58407bc1813856b277a3323a6c920c50fb7e6a40 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Thu, 2 Oct 2025 17:12:37 +0000 Subject: [PATCH 1/6] evo: add `DomainPort` for validating and storing domain and port pair --- src/evo/netinfo.cpp | 59 ++++++++++++++++++++++++++++ src/evo/netinfo.h | 71 +++++++++++++++++++++++++++++++++ src/test/evo_netinfo_tests.cpp | 72 +++++++++++++++++++++++++++++++--- 3 files changed, 196 insertions(+), 6 deletions(-) diff --git a/src/evo/netinfo.cpp b/src/evo/netinfo.cpp index 2c4dc7ef712c..d9458edcc019 100644 --- a/src/evo/netinfo.cpp +++ b/src/evo/netinfo.cpp @@ -18,8 +18,16 @@ namespace { static std::unique_ptr g_main_params{nullptr}; static std::once_flag g_main_params_flag; +/** Maximum length of a label in a domain per RFC 1035 */ +static constexpr uint8_t DOMAIN_LABEL_MAX_LEN{63}; +/** Maximum possible length of a ASCII FQDN */ +static constexpr uint8_t DOMAIN_MAX_LEN{253}; +/** Minimum length of a FQDN */ +static constexpr uint8_t DOMAIN_MIN_LEN{3}; + static constexpr std::string_view SAFE_CHARS_IPV4{"1234567890."}; static constexpr std::string_view SAFE_CHARS_IPV4_6{"abcdefABCDEF1234567890.:[]"}; +static constexpr std::string_view SAFE_CHARS_RFC1035{"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.-"}; bool MatchCharsFilter(std::string_view input, std::string_view filter) { @@ -42,6 +50,57 @@ UniValue ArrFromService(const CService& addr) return obj; } +DomainPort::Status DomainPort::ValidateDomain(const std::string& addr) +{ + if (addr.length() > DOMAIN_MAX_LEN || addr.length() < DOMAIN_MIN_LEN) { + return DomainPort::Status::BadLen; + } + if (!MatchCharsFilter(addr, SAFE_CHARS_RFC1035)) { + return DomainPort::Status::BadChar; + } + if (addr.front() == '.' || addr.back() == '.') { + return DomainPort::Status::BadCharPos; + } + std::vector labels{SplitString(addr, '.')}; + if (labels.size() < 2) { + return DomainPort::Status::BadDotless; + } + for (const auto& label : labels) { + if (label.empty() || label.length() > DOMAIN_LABEL_MAX_LEN) { + return DomainPort::Status::BadLabelLen; + } + if (label.front() == '-' || label.back() == '-') { + return DomainPort::Status::BadLabelCharPos; + } + } + return DomainPort::Status::Success; +} + +DomainPort::Status DomainPort::Set(const std::string& addr, const uint16_t port) +{ + if (port == 0) { + return DomainPort::Status::BadPort; + } + const auto ret{ValidateDomain(addr)}; + if (ret == DomainPort::Status::Success) { + // Convert to lowercase to avoid duplication by changing case (domains are case-insensitive) + m_addr = ToLower(addr); + m_port = port; + } + return ret; +} + +DomainPort::Status DomainPort::Validate() const +{ + if (m_addr.empty() || m_addr != ToLower(m_addr)) { + return DomainPort::Status::Malformed; + } + if (m_port == 0) { + return DomainPort::Status::BadPort; + } + return ValidateDomain(m_addr); +} + bool NetInfoEntry::operator==(const NetInfoEntry& rhs) const { if (m_type != rhs.m_type) return false; diff --git a/src/evo/netinfo.h b/src/evo/netinfo.h index 4f1cdb9f1717..22367dda556d 100644 --- a/src/evo/netinfo.h +++ b/src/evo/netinfo.h @@ -106,6 +106,77 @@ UniValue ArrFromService(const CService& addr); /** Equivalent to Params() if node is running on mainnet */ const CChainParams& MainParams(); +class DomainPort +{ +public: + enum class Status : uint8_t { + BadChar, + BadCharPos, + BadDotless, + BadLabelCharPos, + BadLabelLen, + BadLen, + BadPort, + Malformed, + + Success + }; + + static constexpr std::string_view StatusToString(const DomainPort::Status code) + { + switch (code) { + case DomainPort::Status::BadChar: + return "invalid character"; + case DomainPort::Status::BadCharPos: + return "bad domain character position"; + case DomainPort::Status::BadDotless: + return "prohibited dotless"; + case DomainPort::Status::BadLabelCharPos: + return "bad label character position"; + case DomainPort::Status::BadLabelLen: + return "bad label length"; + case DomainPort::Status::BadLen: + return "bad domain length"; + case DomainPort::Status::BadPort: + return "bad port"; + case DomainPort::Status::Malformed: + return "malformed"; + case DomainPort::Status::Success: + return "success"; + } // no default case, so the compiler can warn about missing cases + assert(false); + } + +private: + std::string m_addr{}; + uint16_t m_port{0}; + +private: + static DomainPort::Status ValidateDomain(const std::string& input); + +public: + DomainPort() = default; + ~DomainPort() = default; + + bool operator<(const DomainPort& rhs) const { return std::tie(m_addr, m_port) < std::tie(rhs.m_addr, rhs.m_port); } + bool operator==(const DomainPort& rhs) const { return std::tie(m_addr, m_port) == std::tie(rhs.m_addr, rhs.m_port); } + bool operator!=(const DomainPort& rhs) const { return !(*this == rhs); } + + SERIALIZE_METHODS(DomainPort, obj) + { + READWRITE(obj.m_addr); + READWRITE(Using>(obj.m_port)); + } + + bool IsEmpty() const { return m_addr.empty() && m_port == 0; } + bool IsValid() const { return Validate() == DomainPort::Status::Success; } + DomainPort::Status Set(const std::string& addr, const uint16_t port); + DomainPort::Status Validate() const; + uint16_t GetPort() const { return m_port; } + std::string ToStringAddr() const { return m_addr; } + std::string ToStringAddrPort() const { return strprintf("%s:%d", m_addr, m_port); } +}; + class NetInfoEntry { public: diff --git a/src/test/evo_netinfo_tests.cpp b/src/test/evo_netinfo_tests.cpp index 7c20deebf4e9..509042f22dd1 100644 --- a/src/test/evo_netinfo_tests.cpp +++ b/src/test/evo_netinfo_tests.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include @@ -21,7 +22,7 @@ struct TestEntry { NetInfoStatus expected_ret_ext; }; -static const std::vector vals_main{ +static const std::vector addr_vals_main{ // Address and port specified {{NetInfoPurpose::CORE_P2P, "1.1.1.1:9999"}, NetInfoStatus::Success, NetInfoStatus::Success}, // - Port should default to default P2P core with MnNetInfo @@ -100,7 +101,7 @@ void TestExtNetInfo(const std::vector& vals) BOOST_AUTO_TEST_CASE(mnnetinfo_rules_main) { - TestMnNetInfo(vals_main); + TestMnNetInfo(addr_vals_main); { // MnNetInfo only stores one value, overwriting prohibited @@ -122,9 +123,9 @@ BOOST_AUTO_TEST_CASE(mnnetinfo_rules_main) } } -BOOST_AUTO_TEST_CASE(extnetinfo_rules_main) { TestExtNetInfo(vals_main); } +BOOST_AUTO_TEST_CASE(extnetinfo_rules_main) { TestExtNetInfo(addr_vals_main); } -static const std::vector vals_reg{ +static const std::vector addr_vals_reg{ // - MnNetInfo doesn't mind using port 0 // - ExtNetInfo requires non-zero ports {{NetInfoPurpose::CORE_P2P, "1.1.1.1:0"}, NetInfoStatus::Success, NetInfoStatus::BadPort}, @@ -136,11 +137,11 @@ static const std::vector vals_reg{ {{NetInfoPurpose::CORE_P2P, "1.1.1.1:22"}, NetInfoStatus::Success, NetInfoStatus::BadPort}, }; -BOOST_FIXTURE_TEST_CASE(mnnetinfo_rules_reg, RegTestingSetup) { TestMnNetInfo(vals_reg); } +BOOST_FIXTURE_TEST_CASE(mnnetinfo_rules_reg, RegTestingSetup) { TestMnNetInfo(addr_vals_reg); } BOOST_FIXTURE_TEST_CASE(extnetinfo_rules_reg, RegTestingSetup) { - TestExtNetInfo(vals_reg); + TestExtNetInfo(addr_vals_reg); { // ExtNetInfo can store up to 4 entries per purpose code, check limit enforcement @@ -364,4 +365,63 @@ 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)); } +BOOST_AUTO_TEST_CASE(domainport_rules) +{ + static const std::vector> domain_vals{ + // Domain name labels can be as small as one character long and remain valid + {"r.server-1.ab.cd", DomainPort::Status::Success}, + // Domain names labels can trail with numbers or consist entirely of numbers due to RFC 1123 + {"9998.9example7.ab", DomainPort::Status::Success}, + // dotless domains prohibited + {"abcd", DomainPort::Status::BadDotless}, + // no empty label (trailing delimiter) + {"abc.", DomainPort::Status::BadCharPos}, + // no empty label (leading delimiter) + {".abc", DomainPort::Status::BadCharPos}, + // no empty label (extra delimiters) + {"a..dot..b", DomainPort::Status::BadLabelLen}, + // ' is not a valid character in domains + {"somebody's macbook pro.local", DomainPort::Status::BadChar}, + // spaces are not a valid character in domains + {"somebodys macbook pro.local", DomainPort::Status::BadChar}, + // trailing hyphens are not allowed + {"-a-.bc.de", DomainPort::Status::BadLabelCharPos}, + // 2 (characters in domain) < 3 (minimum length) + {"ac", DomainPort::Status::BadLen}, + // 278 (characters in domain) > 253 (maximum limit) + {"Loremipsumdolorsitametconsecteturadipiscingelitseddoeiusmodtempor" + "incididuntutlaboreetdoloremagnaaliquaUtenimadminimveniamquisnostrud" + "exercitationullamcolaborisnisiutaliquipexeacommodoconsequatDuisaute" + "iruredolorinreprehenderitinvoluptatevelitessecillumdoloreeufugiatnullapariat.ur", DomainPort::Status::BadLen}, + // 64 (characters in label) > 63 (maximum limit) + {"loremipsumdolorsitametconsecteturadipiscingelitseddoeiusmodtempo.ri.nc", DomainPort::Status::BadLabelLen}, + }; + + for (const auto& [addr, retval] : domain_vals) { + DomainPort domain; + BOOST_CHECK_EQUAL(domain.Set(addr, 9999), retval); + if (retval != DomainPort::Status::Success) { + BOOST_CHECK_EQUAL(domain.Validate(), DomainPort::Status::Malformed); // Empty values report as Malformed + } else { + BOOST_CHECK_EQUAL(domain.Validate(), DomainPort::Status::Success); + } + } + + { + // DomainPort requires non-zero ports + DomainPort domain; + BOOST_CHECK_EQUAL(domain.Set("example.com", 0), DomainPort::Status::BadPort); + BOOST_CHECK_EQUAL(domain.Validate(), DomainPort::Status::Malformed); + } + + { + // DomainPort stores the domain in lower-case + DomainPort lhs, rhs; + BOOST_CHECK_EQUAL(lhs.Set("example.com", 9999), DomainPort::Status::Success); + BOOST_CHECK_EQUAL(rhs.Set(ToUpper("example.com"), 9999), DomainPort::Status::Success); + BOOST_CHECK_EQUAL(lhs.ToStringAddr(), rhs.ToStringAddr()); + BOOST_CHECK(lhs == rhs); + } +} + BOOST_AUTO_TEST_SUITE_END() From 79cc52ca4d848d6c456c1b3dbc9b51c4a2fb4cb9 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Sat, 9 Aug 2025 11:54:00 +0000 Subject: [PATCH 2/6] evo: allow storing `DomainPort` in `NetInfoEntry` --- src/evo/deterministicmns.cpp | 29 +++++++++++++++++++++++++++++ src/evo/netinfo.cpp | 29 +++++++++++++++++++++++++---- src/evo/netinfo.h | 25 ++++++++++++++++++++++--- src/evo/specialtxman.cpp | 9 +++++++++ 4 files changed, 85 insertions(+), 7 deletions(-) diff --git a/src/evo/deterministicmns.cpp b/src/evo/deterministicmns.cpp index ec00139807f6..1faff56d4e48 100644 --- a/src/evo/deterministicmns.cpp +++ b/src/evo/deterministicmns.cpp @@ -440,6 +440,12 @@ void CDeterministicMNList::AddMN(const CDeterministicMNCPtr& dmn, bool fBumpTota 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 if (const auto domain_opt{entry.GetDomainPort()}) { + if (!AddUniqueProperty(*dmn, *domain_opt)) { + mnUniquePropertyMap = mnUniquePropertyMapSaved; + throw std::runtime_error(strprintf("%s: Can't add a masternode %s with a duplicate address=%s", + __func__, dmn->proTxHash.ToString(), domain_opt->ToStringAddrPort())); + } } else { mnUniquePropertyMap = mnUniquePropertyMapSaved; throw std::runtime_error( @@ -494,6 +500,10 @@ void CDeterministicMNList::UpdateMN(const CDeterministicMN& oldDmn, const std::s if (!DeleteUniqueProperty(dmn, *service_opt)) { return "internal error"; // This shouldn't be possible } + } else if (const auto domain_opt{old_entry.GetDomainPort()}) { + if (!DeleteUniqueProperty(dmn, *domain_opt)) { + return "internal error"; // This shouldn't be possible + } } else { return "invalid address"; } @@ -503,6 +513,10 @@ void CDeterministicMNList::UpdateMN(const CDeterministicMN& oldDmn, const std::s if (!AddUniqueProperty(dmn, *service_opt)) { return strprintf("duplicate (%s)", service_opt->ToStringAddrPort()); } + } else if (const auto domain_opt{new_entry.GetDomainPort()}) { + if (!AddUniqueProperty(dmn, *domain_opt)) { + return strprintf("duplicate (%s)", domain_opt->ToStringAddrPort()); + } } else { return "invalid address"; } @@ -583,6 +597,12 @@ void CDeterministicMNList::RemoveMN(const uint256& proTxHash) throw std::runtime_error(strprintf("%s: Can't delete a masternode %s with an address=%s", __func__, proTxHash.ToString(), service_opt->ToStringAddrPort())); } + } else if (const auto domain_opt{entry.GetDomainPort()}) { + if (!DeleteUniqueProperty(*dmn, *domain_opt)) { + mnUniquePropertyMap = mnUniquePropertyMapSaved; + throw std::runtime_error(strprintf("%s: Can't delete a masternode %s with an address=%s", __func__, + proTxHash.ToString(), domain_opt->ToStringAddrPort())); + } } else { mnUniquePropertyMap = mnUniquePropertyMapSaved; throw std::runtime_error(strprintf("%s: Can't delete a masternode %s with invalid address", __func__, @@ -1142,6 +1162,11 @@ bool CheckProRegTx(CDeterministicMNManager& dmnman, const CTransaction& tx, gsl: mnList.GetUniquePropertyMN(*service_opt)->collateralOutpoint != collateralOutpoint) { return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-dup-netinfo-entry"); } + } else if (const auto domain_opt{entry.GetDomainPort()}) { + if (mnList.HasUniqueProperty(*domain_opt) && + mnList.GetUniquePropertyMN(*domain_opt)->collateralOutpoint != collateralOutpoint) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-dup-netinfo-entry"); + } } else { return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-netinfo-entry"); } @@ -1224,6 +1249,10 @@ bool CheckProUpServTx(CDeterministicMNManager& dmnman, const CTransaction& tx, g mnList.GetUniquePropertyMN(*service_opt)->proTxHash != opt_ptx->proTxHash) { return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-dup-netinfo-entry"); } + } else if (const auto domain_opt{entry.GetDomainPort()}) { + if (mnList.HasUniqueProperty(*domain_opt) && mnList.GetUniquePropertyMN(*domain_opt)->proTxHash != opt_ptx->proTxHash) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-dup-netinfo-entry"); + } } else { return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-netinfo-entry"); } diff --git a/src/evo/netinfo.cpp b/src/evo/netinfo.cpp index d9458edcc019..b8b09c74ff4c 100644 --- a/src/evo/netinfo.cpp +++ b/src/evo/netinfo.cpp @@ -124,6 +124,10 @@ bool NetInfoEntry::operator<(const NetInfoEntry& rhs) const if constexpr (std::is_same_v) { // Both the same type, compare as usual return lhs < rhs; + } else if constexpr ((std::is_same_v || std::is_same_v) && + (std::is_same_v || std::is_same_v)) { + // Differing types but both implement ToStringAddrPort(), lexicographical compare strings + return lhs.ToStringAddrPort() < rhs.ToStringAddrPort(); } // If lhs is monostate, it less than rhs; otherwise rhs is greater return std::is_same_v; @@ -140,12 +144,21 @@ std::optional NetInfoEntry::GetAddrPort() const return std::nullopt; } +std::optional NetInfoEntry::GetDomainPort() const +{ + if (const auto* data_ptr{std::get_if(&m_data)}; m_type == NetInfoType::Domain && data_ptr) { + ASSERT_IF_DEBUG(data_ptr->IsValid()); + return *data_ptr; + } + return std::nullopt; +} + uint16_t NetInfoEntry::GetPort() const { return std::visit( [](auto&& input) -> uint16_t { using T1 = std::decay_t; - if constexpr (std::is_same_v) { + if constexpr (std::is_same_v || std::is_same_v) { return input.GetPort(); } return 0; @@ -162,7 +175,8 @@ bool NetInfoEntry::IsTriviallyValid() const return std::visit( [this](auto&& input) -> bool { using T1 = std::decay_t; - static_assert(std::is_same_v || std::is_same_v, "Unexpected type"); + static_assert(std::is_same_v || std::is_same_v || std::is_same_v, + "Unexpected type"); if constexpr (std::is_same_v) { // Empty underlying data isn't a valid entry return false; @@ -171,6 +185,11 @@ bool NetInfoEntry::IsTriviallyValid() const if (m_type != NetInfoType::Service) return false; // Underlying data must meet surface-level validity checks for its type if (!input.IsValid()) return false; + } else if constexpr (std::is_same_v) { + // Type code should be truthful as it decides what underlying type is used when (de)serializing + if (m_type != NetInfoType::Domain) return false; + // Underlying data should at least meet surface-level validity checks + if (!input.IsValid()) return false; } return true; }, @@ -184,6 +203,8 @@ std::string NetInfoEntry::ToString() const using T1 = std::decay_t; if constexpr (std::is_same_v) { return strprintf("CService(addr=%s, port=%u)", input.ToStringAddr(), input.GetPort()); + } else if constexpr (std::is_same_v) { + return strprintf("DomainPort(addr=%s, port=%u)", input.ToStringAddr(), input.GetPort()); } return "[invalid entry]"; }, @@ -195,7 +216,7 @@ std::string NetInfoEntry::ToStringAddr() const return std::visit( [](auto&& input) -> std::string { using T1 = std::decay_t; - if constexpr (std::is_same_v) { + if constexpr (std::is_same_v || std::is_same_v) { return input.ToStringAddr(); } return "[invalid entry]"; @@ -208,7 +229,7 @@ std::string NetInfoEntry::ToStringAddrPort() const return std::visit( [](auto&& input) -> std::string { using T1 = std::decay_t; - if constexpr (std::is_same_v) { + if constexpr (std::is_same_v || std::is_same_v) { return input.ToStringAddrPort(); } return "[invalid entry]"; diff --git a/src/evo/netinfo.h b/src/evo/netinfo.h index 22367dda556d..e377df6b001d 100644 --- a/src/evo/netinfo.h +++ b/src/evo/netinfo.h @@ -156,6 +156,9 @@ class DomainPort public: DomainPort() = default; + template + DomainPort(deserialize_type, Stream& s) { s >> *this; } + ~DomainPort() = default; bool operator<(const DomainPort& rhs) const { return std::tie(m_addr, m_port) < std::tie(rhs.m_addr, rhs.m_port); } @@ -182,15 +185,22 @@ class NetInfoEntry public: enum NetInfoType : uint8_t { Service = 0x01, + Domain = 0x02, Invalid = 0xff }; private: uint8_t m_type{NetInfoType::Invalid}; - std::variant m_data{std::monostate{}}; + std::variant m_data{std::monostate{}}; public: NetInfoEntry() = default; + NetInfoEntry(const DomainPort& domain) + { + if (!domain.IsValid()) return; + m_type = NetInfoType::Domain; + m_data = domain; + } NetInfoEntry(const CService& service) { if (!service.IsValid()) return; @@ -213,6 +223,9 @@ class NetInfoEntry if (const auto* data_ptr{std::get_if(&m_data)}; m_type == NetInfoType::Service && data_ptr && data_ptr->IsValid()) { s << m_type << *data_ptr; + } else if (const auto* data_ptr{std::get_if(&m_data)}; + m_type == NetInfoType::Domain && data_ptr && data_ptr->IsValid()) { + s << m_type << *data_ptr; } else { s << NetInfoType::Invalid; } @@ -224,12 +237,17 @@ class NetInfoEntry OverrideStream s(&s_, /*nType=*/0, s_.GetVersion() | ADDRV2_FORMAT); s >> m_type; if (m_type == NetInfoType::Service) { - m_data = CService{}; try { - CService& service{std::get(m_data)}; + auto& service{m_data.emplace()}; s >> service; if (!service.IsValid()) { Clear(); } // Invalid CService, mark as invalid } catch (const std::ios_base::failure&) { Clear(); } // Deser failed, mark as invalid + } else if (m_type == NetInfoType::Domain) { + try { + auto& domain{m_data.emplace()}; + s >> domain; + if (!domain.IsValid()) { Clear(); } // Invalid DomainPort, mark as invalid + } catch (const std::ios_base::failure&) { Clear(); } // Deser failed, mark as invalid } else { Clear(); } // Invalid type code, mark as invalid } @@ -240,6 +258,7 @@ class NetInfoEntry } std::optional GetAddrPort() const; + std::optional GetDomainPort() const; uint16_t GetPort() const; bool IsEmpty() const { return *this == NetInfoEntry{}; } bool IsTriviallyValid() const; diff --git a/src/evo/specialtxman.cpp b/src/evo/specialtxman.cpp index 9fcffe753299..e7fe59efff89 100644 --- a/src/evo/specialtxman.cpp +++ b/src/evo/specialtxman.cpp @@ -262,6 +262,10 @@ bool CSpecialTxProcessor::BuildNewListFromBlock(const CBlock& block, gsl::not_nu if (newList.HasUniqueProperty(*service_opt)) { return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "bad-protx-dup-netinfo-entry"); } + } else if (const auto domain_opt{entry.GetDomainPort()}) { + if (newList.HasUniqueProperty(*domain_opt)) { + return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "bad-protx-dup-netinfo-entry"); + } } else { return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "bad-protx-netinfo-entry"); } @@ -298,6 +302,11 @@ bool CSpecialTxProcessor::BuildNewListFromBlock(const CBlock& block, gsl::not_nu newList.GetUniquePropertyMN(*service_opt)->proTxHash != opt_proTx->proTxHash) { return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "bad-protx-dup-netinfo-entry"); } + } else if (const auto domain_opt{entry.GetDomainPort()}) { + if (newList.HasUniqueProperty(*domain_opt) && + newList.GetUniquePropertyMN(*domain_opt)->proTxHash != opt_proTx->proTxHash) { + return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "bad-protx-dup-netinfo-entry"); + } } else { return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "bad-protx-netinfo-entry"); } From f04b0d2dea123981179d02991ff039a2c1ec7ed2 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Thu, 2 Oct 2025 18:44:47 +0000 Subject: [PATCH 3/6] evo: allow `ExtNetInfo` to accept `DomainPort` if `PLATFORM_HTTPS` --- src/evo/netinfo.cpp | 83 +++++++++++++++++++++++++++++++++- src/evo/netinfo.h | 1 + src/test/evo_netinfo_tests.cpp | 27 ++++++++++- 3 files changed, 107 insertions(+), 4 deletions(-) diff --git a/src/evo/netinfo.cpp b/src/evo/netinfo.cpp index b8b09c74ff4c..17279d0448b9 100644 --- a/src/evo/netinfo.cpp +++ b/src/evo/netinfo.cpp @@ -25,14 +25,46 @@ static constexpr uint8_t DOMAIN_MAX_LEN{253}; /** Minimum length of a FQDN */ static constexpr uint8_t DOMAIN_MIN_LEN{3}; +static constexpr std::string_view SAFE_CHARS_ALPHA{"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"}; static constexpr std::string_view SAFE_CHARS_IPV4{"1234567890."}; static constexpr std::string_view SAFE_CHARS_IPV4_6{"abcdefABCDEF1234567890.:[]"}; static constexpr std::string_view SAFE_CHARS_RFC1035{"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.-"}; +static constexpr std::array TLDS_BAD{ + // ICANN resolution 2018.02.04.12 + ".mail", + // Infrastructure TLD + ".arpa", + // RFC 6761 + ".example", ".invalid", ".localhost", ".test", + // RFC 6762 + ".local", + // RFC 6762, Appendix G + ".corp", ".home", ".internal", ".intranet", ".lan", ".private", +}; 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; }); } + +bool MatchSuffix(const std::string& str, Span list) +{ + if (str.empty()) return false; + for (const auto& suffix : list) { + if (suffix.size() > str.size()) continue; + if (std::string_view{str}.ends_with(suffix)) return true; + } + return false; +} + +bool IsAllowedPlatformHTTPPort(uint16_t port) +{ + switch (port) { + case 443: + return true; + } + return false; +} } // anonymous namespace bool IsNodeOnMainnet() { return Params().NetworkIDString() == CBaseChainParams::MAIN; } @@ -387,6 +419,10 @@ NetInfoStatus ExtNetInfo::ProcessCandidate(const NetInfoPurpose purpose, const N if (IsAddrPortDuplicate(candidate)) { return NetInfoStatus::Duplicate; } + if (candidate.GetDomainPort().has_value() && purpose != NetInfoPurpose::PLATFORM_HTTPS) { + // Domains only allowed for Platform HTTPS API + return NetInfoStatus::BadInput; + } if (auto it{m_data.find(purpose)}; it != m_data.end()) { // Existing entries list found, check limit auto& [_, entries] = *it; @@ -426,6 +462,26 @@ NetInfoStatus ExtNetInfo::ValidateService(const CService& service) return NetInfoStatus::Success; } +NetInfoStatus ExtNetInfo::ValidateDomainPort(const DomainPort& domain) +{ + if (!domain.IsValid()) { + return NetInfoStatus::BadInput; + } + const uint16_t domain_port{domain.GetPort()}; + if (domain_port == 0 || (IsBadPort(domain_port) && !IsAllowedPlatformHTTPPort(domain_port))) { + return NetInfoStatus::BadPort; + } + const std::string& addr{domain.ToStringAddr()}; + if (MatchSuffix(addr, TLDS_BAD)) { + return NetInfoStatus::BadInput; + } + if (const auto labels{SplitString(addr, '.')}; !MatchCharsFilter(labels.at(labels.size() - 1), SAFE_CHARS_ALPHA)) { + return NetInfoStatus::BadInput; + } + + return NetInfoStatus::Success; +} + NetInfoStatus ExtNetInfo::AddEntry(const NetInfoPurpose purpose, const std::string& input) { if (!IsValidPurpose(purpose)) { @@ -437,11 +493,25 @@ NetInfoStatus ExtNetInfo::AddEntry(const NetInfoPurpose purpose, const std::stri 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 (!MatchCharsFilter(addr, SAFE_CHARS_RFC1035)) { + // Neither IP:port safe nor domain-safe, we can safely assume it's bad input + return NetInfoStatus::BadInput; + } + + // Not IP:port safe but domain safe, treat as domain. + if (DomainPort domain; domain.Set(addr, port) == DomainPort::Status::Success) { + const auto ret{ValidateDomainPort(domain)}; + if (ret == NetInfoStatus::Success) { + return ProcessCandidate(purpose, NetInfoEntry{domain}); + } + return ret; /* ValidateDomainPort() failed */ + } + return NetInfoStatus::BadInput; /* DomainPort::Set() failed */ } + // IP:port safe, try to parse it as IP:port if (auto service_opt{Lookup(addr, /*portDefault=*/port, /*fAllowLookup=*/false)}) { const auto ret{ValidateService(*service_opt)}; if (ret == NetInfoStatus::Success) { @@ -516,6 +586,15 @@ NetInfoStatus ExtNetInfo::Validate() const // Stores CService underneath but doesn't pass validation rules return ret; } + } else if (const auto domain_opt{entry.GetDomainPort()}) { + if (purpose != NetInfoPurpose::PLATFORM_HTTPS) { + // Domains only allowed for Platform HTTPS API + return NetInfoStatus::BadInput; + } + if (auto ret{ValidateDomainPort(*domain_opt)}; ret != NetInfoStatus::Success) { + // Stores DomainPort underneath but doesn't pass validation rules + return ret; + } } else { // Doesn't store valid type underneath return NetInfoStatus::Malformed; diff --git a/src/evo/netinfo.h b/src/evo/netinfo.h index e377df6b001d..3ea51992b79e 100644 --- a/src/evo/netinfo.h +++ b/src/evo/netinfo.h @@ -384,6 +384,7 @@ class ExtNetInfo final : public NetInfoInterface /** Validate CService candidate address against ruleset */ static NetInfoStatus ValidateService(const CService& service); + static NetInfoStatus ValidateDomainPort(const DomainPort& domain); private: uint8_t m_version{CURRENT_VERSION}; diff --git a/src/test/evo_netinfo_tests.cpp b/src/test/evo_netinfo_tests.cpp index 509042f22dd1..3d79556e048a 100644 --- a/src/test/evo_netinfo_tests.cpp +++ b/src/test/evo_netinfo_tests.cpp @@ -40,8 +40,12 @@ static const std::vector addr_vals_main{ // - Non-IPv4 addresses are prohibited in MnNetInfo // - Any valid BIP155 address is allowed in ExtNetInfo {{NetInfoPurpose::CORE_P2P, "[2606:4700:4700::1111]:9999"}, NetInfoStatus::BadInput, NetInfoStatus::Success}, - // Domains are not allowed + // Domains are not allowed for Core P2P or Platform P2P {{NetInfoPurpose::CORE_P2P, "example.com:9999"}, NetInfoStatus::BadInput, NetInfoStatus::BadInput}, + {{NetInfoPurpose::PLATFORM_P2P, "example.com:9999"}, NetInfoStatus::MaxLimit, NetInfoStatus::BadInput}, + // - MnNetInfo doesn't allow storing anything except a Core P2P address + // - ExtNetInfo can store Platform HTTPS addresses *as domains* + {{NetInfoPurpose::PLATFORM_HTTPS, "example.com:9999"}, NetInfoStatus::MaxLimit, NetInfoStatus::Success}, // Incorrect IPv4 address {{NetInfoPurpose::CORE_P2P, "1.1.1.256:9999"}, NetInfoStatus::BadInput, NetInfoStatus::BadInput}, // Missing address @@ -182,6 +186,21 @@ BOOST_FIXTURE_TEST_CASE(extnetinfo_rules_reg, RegTestingSetup) BOOST_CHECK(!netInfo.HasEntries(NetInfoPurpose::PLATFORM_HTTPS)); ValidateGetEntries(netInfo.GetEntries(), /*expected_size=*/2); } + + { + // ExtNetInfo has additional rules for domains + const std::vector domain_vals{ + // Port 80 (HTTP) is below the privileged ports threshold (1023), not allowed + {{NetInfoPurpose::PLATFORM_HTTPS, "example.com:80"}, NetInfoStatus::MaxLimit, NetInfoStatus::BadPort}, + // Port 443 (HTTPS) is below the privileged ports threshold (1023) but still allowed + {{NetInfoPurpose::PLATFORM_HTTPS, "example.com:443"}, NetInfoStatus::MaxLimit, NetInfoStatus::Success}, + // TLDs must be alphabetic to avoid ambiguation with IP addresses (per ICANN guidelines) + {{NetInfoPurpose::PLATFORM_HTTPS, "example.123:443"}, NetInfoStatus::MaxLimit, NetInfoStatus::BadInput}, + // .local is a prohibited TLD + {{NetInfoPurpose::PLATFORM_HTTPS, "somebodys-macbook-pro.local:9998"}, NetInfoStatus::MaxLimit, NetInfoStatus::BadInput}, + }; + TestExtNetInfo(domain_vals); + } } BOOST_AUTO_TEST_CASE(netinfo_ser) @@ -399,11 +418,15 @@ BOOST_AUTO_TEST_CASE(domainport_rules) for (const auto& [addr, retval] : domain_vals) { DomainPort domain; - BOOST_CHECK_EQUAL(domain.Set(addr, 9999), retval); + ExtNetInfo netInfo; + BOOST_CHECK_EQUAL(domain.Set(addr, 443), retval); if (retval != DomainPort::Status::Success) { BOOST_CHECK_EQUAL(domain.Validate(), DomainPort::Status::Malformed); // Empty values report as Malformed + BOOST_CHECK_EQUAL(netInfo.AddEntry(NetInfoPurpose::PLATFORM_HTTPS, domain.ToStringAddrPort()), + NetInfoStatus::BadInput); } else { BOOST_CHECK_EQUAL(domain.Validate(), DomainPort::Status::Success); + BOOST_CHECK_EQUAL(netInfo.AddEntry(NetInfoPurpose::PLATFORM_HTTPS, domain.ToStringAddrPort()), NetInfoStatus::Success); } } From 69a92427744b18fb1b57120741e78bc740a13124 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Tue, 23 Sep 2025 15:00:41 +0000 Subject: [PATCH 4/6] evo: allow and recognize CJDNS addresses --- src/evo/netinfo.cpp | 7 ++++--- src/test/evo_netinfo_tests.cpp | 27 +++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/src/evo/netinfo.cpp b/src/evo/netinfo.cpp index 17279d0448b9..0e44affa85df 100644 --- a/src/evo/netinfo.cpp +++ b/src/evo/netinfo.cpp @@ -449,7 +449,7 @@ NetInfoStatus ExtNetInfo::ValidateService(const CService& service) if (!service.IsValid()) { return NetInfoStatus::BadAddress; } - if (!service.IsIPv4() && !service.IsIPv6()) { + if (!service.IsCJDNS() && !service.IsIPv4() && !service.IsIPv6()) { return NetInfoStatus::BadType; } if (Params().RequireRoutableExternalIP() && !service.IsRoutable()) { @@ -513,9 +513,10 @@ NetInfoStatus ExtNetInfo::AddEntry(const NetInfoPurpose purpose, const std::stri // IP:port safe, try to parse it as IP:port if (auto service_opt{Lookup(addr, /*portDefault=*/port, /*fAllowLookup=*/false)}) { - const auto ret{ValidateService(*service_opt)}; + const auto service{MaybeFlipIPv6toCJDNS(*service_opt)}; + const auto ret{ValidateService(service)}; if (ret == NetInfoStatus::Success) { - return ProcessCandidate(purpose, NetInfoEntry{*service_opt}); + return ProcessCandidate(purpose, NetInfoEntry{service}); } return ret; /* ValidateService() failed */ } diff --git a/src/test/evo_netinfo_tests.cpp b/src/test/evo_netinfo_tests.cpp index 3d79556e048a..f334b83aa240 100644 --- a/src/test/evo_netinfo_tests.cpp +++ b/src/test/evo_netinfo_tests.cpp @@ -141,6 +141,14 @@ static const std::vector addr_vals_reg{ {{NetInfoPurpose::CORE_P2P, "1.1.1.1:22"}, NetInfoStatus::Success, NetInfoStatus::BadPort}, }; +enum class ExpectedType : uint8_t { + CJDNS, +}; + +static const std::vector> privacy_addr_vals{ + {ExpectedType::CJDNS, "[fc00:3344:5566:7788:9900:aabb:ccdd:eeff]:9998", NetInfoStatus::Success}, +}; + BOOST_FIXTURE_TEST_CASE(mnnetinfo_rules_reg, RegTestingSetup) { TestMnNetInfo(addr_vals_reg); } BOOST_FIXTURE_TEST_CASE(extnetinfo_rules_reg, RegTestingSetup) @@ -201,6 +209,25 @@ BOOST_FIXTURE_TEST_CASE(extnetinfo_rules_reg, RegTestingSetup) }; TestExtNetInfo(domain_vals); } + + // Privacy network entry checks + for (const auto& [type, input, expected_ret] : privacy_addr_vals) { + const bool expected_success{expected_ret == NetInfoStatus::Success}; + + ExtNetInfo netInfo{}; + BOOST_CHECK_EQUAL(netInfo.AddEntry(NetInfoPurpose::CORE_P2P, input), expected_ret); + ValidateGetEntries(netInfo.GetEntries(), /*expected_size=*/expected_success ? 1 : 0); + if (!expected_success) continue; + + // Type registration check + const CService service{netInfo.GetEntries().at(0).GetAddrPort().value()}; + BOOST_CHECK(service.IsValid()); + switch (type) { + case ExpectedType::CJDNS: + BOOST_CHECK(service.IsCJDNS()); + break; + } // no default case, so the compiler can warn about missing cases + } } BOOST_AUTO_TEST_CASE(netinfo_ser) From 7b0de72e8e88cf5bee5c9c21eaa81af1c5f451c5 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Sun, 10 Aug 2025 19:29:23 +0000 Subject: [PATCH 5/6] evo: recognize and store I2P and onion domains in `ExtNetInfo` --- src/evo/deterministicmns.h | 12 +++++++++++- src/evo/netinfo.cpp | 35 +++++++++++++++++++++++++++------- src/test/evo_netinfo_tests.cpp | 26 +++++++++++++++++++++++-- 3 files changed, 63 insertions(+), 10 deletions(-) diff --git a/src/evo/deterministicmns.h b/src/evo/deterministicmns.h index 12643e5f1f1a..0353c46634fc 100644 --- a/src/evo/deterministicmns.h +++ b/src/evo/deterministicmns.h @@ -442,7 +442,17 @@ class CDeterministicMNList DMNL_NO_TEMPLATE(NetInfoInterface); DMNL_NO_TEMPLATE(std::shared_ptr); #undef DMNL_NO_TEMPLATE - return ::SerializeHash(v); + int ser_version{PROTOCOL_VERSION}; + if constexpr (std::is_same_v, CService>) { + // Special handling is required if we're using addresses that can only be (de)serialized using + // ADDRv2. Without this step, the address gets truncated, the hashmap gets contaminated with + // an invalid entry and subsequent attempts at registering ADDRv2 entries get blocked. We cannot + // apply this treatment ADDRv1 compatible addresses for backwards compatibility with the existing map. + if (!v.IsAddrV1Compatible()) { + ser_version |= ADDRV2_FORMAT; + } + } + return ::SerializeHash(v, /*nType=*/SER_GETHASH, /*nVersion=*/ser_version); } template [[nodiscard]] bool AddUniqueProperty(const CDeterministicMN& dmn, const T& v) diff --git a/src/evo/netinfo.cpp b/src/evo/netinfo.cpp index 0e44affa85df..3a8a920d46eb 100644 --- a/src/evo/netinfo.cpp +++ b/src/evo/netinfo.cpp @@ -41,6 +41,7 @@ static constexpr std::array TLDS_BAD{ // RFC 6762, Appendix G ".corp", ".home", ".internal", ".intranet", ".lan", ".private", }; +static constexpr std::array TLDS_PRIVACY{".i2p", ".onion"}; bool MatchCharsFilter(std::string_view input, std::string_view filter) { @@ -449,14 +450,22 @@ NetInfoStatus ExtNetInfo::ValidateService(const CService& service) if (!service.IsValid()) { return NetInfoStatus::BadAddress; } - if (!service.IsCJDNS() && !service.IsIPv4() && !service.IsIPv6()) { + if (!service.IsCJDNS() && !service.IsI2P() && !service.IsIPv4() && !service.IsIPv6() && !service.IsTor()) { return NetInfoStatus::BadType; } if (Params().RequireRoutableExternalIP() && !service.IsRoutable()) { return NetInfoStatus::NotRoutable; } - if (IsBadPort(service.GetPort()) || service.GetPort() == 0) { - return NetInfoStatus::BadPort; + const uint16_t service_port{service.GetPort()}; + if (service.IsI2P()) { + if (service_port != I2P_SAM31_PORT) { + // I2P SAM 3.1 and earlier don't support arbitrary ports + return NetInfoStatus::BadPort; + } + } else { + if (service_port == 0 || IsBadPort(service_port)) { + return NetInfoStatus::BadPort; + } } return NetInfoStatus::Success; @@ -472,7 +481,7 @@ NetInfoStatus ExtNetInfo::ValidateDomainPort(const DomainPort& domain) return NetInfoStatus::BadPort; } const std::string& addr{domain.ToStringAddr()}; - if (MatchSuffix(addr, TLDS_BAD)) { + if (MatchSuffix(addr, TLDS_BAD) || MatchSuffix(addr, TLDS_PRIVACY)) { return NetInfoStatus::BadInput; } if (const auto labels{SplitString(addr, '.')}; !MatchCharsFilter(labels.at(labels.size() - 1), SAFE_CHARS_ALPHA)) { @@ -500,15 +509,27 @@ NetInfoStatus ExtNetInfo::AddEntry(const NetInfoPurpose purpose, const std::stri return NetInfoStatus::BadInput; } - // Not IP:port safe but domain safe, treat as domain. - if (DomainPort domain; domain.Set(addr, port) == DomainPort::Status::Success) { + // Not IP:port safe but domain safe + if (MatchSuffix(addr, TLDS_PRIVACY)) { + // Special domain, try storing it as CService + CNetAddr netaddr; + if (netaddr.SetSpecial(addr)) { + const CService service{netaddr, port}; + const auto ret{ValidateService(service)}; + if (ret == NetInfoStatus::Success) { + return ProcessCandidate(purpose, NetInfoEntry{service}); + } + return ret; /* ValidateService() failed */ + } + } else if (DomainPort domain; domain.Set(addr, port) == DomainPort::Status::Success) { + // Regular domain const auto ret{ValidateDomainPort(domain)}; if (ret == NetInfoStatus::Success) { return ProcessCandidate(purpose, NetInfoEntry{domain}); } return ret; /* ValidateDomainPort() failed */ } - return NetInfoStatus::BadInput; /* DomainPort::Set() failed */ + return NetInfoStatus::BadInput; /* CNetAddr::SetSpecial() or DomainPort::Set() failed */ } // IP:port safe, try to parse it as IP:port diff --git a/src/test/evo_netinfo_tests.cpp b/src/test/evo_netinfo_tests.cpp index f334b83aa240..a1df594c820b 100644 --- a/src/test/evo_netinfo_tests.cpp +++ b/src/test/evo_netinfo_tests.cpp @@ -40,12 +40,16 @@ static const std::vector addr_vals_main{ // - Non-IPv4 addresses are prohibited in MnNetInfo // - Any valid BIP155 address is allowed in ExtNetInfo {{NetInfoPurpose::CORE_P2P, "[2606:4700:4700::1111]:9999"}, NetInfoStatus::BadInput, NetInfoStatus::Success}, - // Domains are not allowed for Core P2P or Platform P2P + // - MnNetInfo doesn't allow storing anything except a Core P2P address + // - Privacy network domains are allowed in ExtNetInfo but internet domains are not {{NetInfoPurpose::CORE_P2P, "example.com:9999"}, NetInfoStatus::BadInput, NetInfoStatus::BadInput}, + {{NetInfoPurpose::CORE_P2P, "pg6mmjiyjmcrsslvykfwnntlaru7p5svn6y2ymmju6nubxndf4pscryd.onion:9999"}, NetInfoStatus::BadInput, NetInfoStatus::Success}, {{NetInfoPurpose::PLATFORM_P2P, "example.com:9999"}, NetInfoStatus::MaxLimit, NetInfoStatus::BadInput}, + {{NetInfoPurpose::PLATFORM_P2P, "pg6mmjiyjmcrsslvykfwnntlaru7p5svn6y2ymmju6nubxndf4pscryd.onion:9999"}, NetInfoStatus::MaxLimit, NetInfoStatus::Success}, // - MnNetInfo doesn't allow storing anything except a Core P2P address - // - ExtNetInfo can store Platform HTTPS addresses *as domains* + // - ExtNetInfo can store Platform HTTPS addresses *as domains* alongside privacy network domains {{NetInfoPurpose::PLATFORM_HTTPS, "example.com:9999"}, NetInfoStatus::MaxLimit, NetInfoStatus::Success}, + {{NetInfoPurpose::PLATFORM_HTTPS, "pg6mmjiyjmcrsslvykfwnntlaru7p5svn6y2ymmju6nubxndf4pscryd.onion:9999"}, NetInfoStatus::MaxLimit, NetInfoStatus::Success}, // Incorrect IPv4 address {{NetInfoPurpose::CORE_P2P, "1.1.1.256:9999"}, NetInfoStatus::BadInput, NetInfoStatus::BadInput}, // Missing address @@ -143,10 +147,20 @@ static const std::vector addr_vals_reg{ enum class ExpectedType : uint8_t { CJDNS, + I2P, + Tor, }; static const std::vector> privacy_addr_vals{ {ExpectedType::CJDNS, "[fc00:3344:5566:7788:9900:aabb:ccdd:eeff]:9998", NetInfoStatus::Success}, + // ExtNetInfo can store I2P addresses as long as it uses port 0 + {ExpectedType::I2P, "udhdrtrcetjm5sxzskjyr5ztpeszydbh4dpl3pl4utgqqw2v4jna.b32.i2p:0", NetInfoStatus::Success}, + // ExtNetInfo can store onion addresses + {ExpectedType::Tor, "pg6mmjiyjmcrsslvykfwnntlaru7p5svn6y2ymmju6nubxndf4pscryd.onion:9998", NetInfoStatus::Success}, + // ExtNetInfo can store I2P addresses but non-zero ports are not allowed + {ExpectedType::I2P, "udhdrtrcetjm5sxzskjyr5ztpeszydbh4dpl3pl4utgqqw2v4jna.b32.i2p:9998", NetInfoStatus::BadPort}, + // ExtNetInfo can store onion addresses but zero ports are not allowed + {ExpectedType::Tor, "pg6mmjiyjmcrsslvykfwnntlaru7p5svn6y2ymmju6nubxndf4pscryd.onion:0", NetInfoStatus::BadPort}, }; BOOST_FIXTURE_TEST_CASE(mnnetinfo_rules_reg, RegTestingSetup) { TestMnNetInfo(addr_vals_reg); } @@ -206,6 +220,8 @@ BOOST_FIXTURE_TEST_CASE(extnetinfo_rules_reg, RegTestingSetup) {{NetInfoPurpose::PLATFORM_HTTPS, "example.123:443"}, NetInfoStatus::MaxLimit, NetInfoStatus::BadInput}, // .local is a prohibited TLD {{NetInfoPurpose::PLATFORM_HTTPS, "somebodys-macbook-pro.local:9998"}, NetInfoStatus::MaxLimit, NetInfoStatus::BadInput}, + // DomainPort isn't used for storing privacy network TLDs like .onion + {{NetInfoPurpose::PLATFORM_HTTPS, "pg6mmjiyjmcrsslvykfwnntlaru7p5svn6y2ymmju6nubxndf4pscryd:9998"}, NetInfoStatus::MaxLimit, NetInfoStatus::BadInput}, }; TestExtNetInfo(domain_vals); } @@ -226,6 +242,12 @@ BOOST_FIXTURE_TEST_CASE(extnetinfo_rules_reg, RegTestingSetup) case ExpectedType::CJDNS: BOOST_CHECK(service.IsCJDNS()); break; + case ExpectedType::I2P: + BOOST_CHECK(service.IsI2P()); + break; + case ExpectedType::Tor: + BOOST_CHECK(service.IsTor()); + break; } // no default case, so the compiler can warn about missing cases } } From 2ba86f91c56ba69bf218c37fc9d2ae8fa9f8f80e Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Sun, 10 Aug 2025 13:09:42 +0000 Subject: [PATCH 6/6] test: use domains in functional test, add test for unique map checking --- test/functional/rpc_netinfo.py | 65 ++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/test/functional/rpc_netinfo.py b/test/functional/rpc_netinfo.py index 5852fa4e40cc..8fb9cb2e6448 100755 --- a/test/functional/rpc_netinfo.py +++ b/test/functional/rpc_netinfo.py @@ -35,6 +35,21 @@ PROTXVER_BASIC = 2 PROTXVER_EXTADDR = 3 +# Sample domains +DOMAINS_CLR = [ + "server-1.example.com", + "server-2.example.com", +] +DOMAINS_TOR = [ + "kpgvmscirrdqpekbqjsvw5teanhatztpp2gl6eee4zkowvwfxwenqaid.onion", + "pg6mmjiyjmcrsslvykfwnntlaru7p5svn6y2ymmju6nubxndf4pscryd.onion", +] +DOMAINS_I2P = [ + "c4gfnttsuwqomiygupdqqqyy5y5emnk5c73hrfvatri67prd7vyq.b32.i2p", + "udhdrtrcetjm5sxzskjyr5ztpeszydbh4dpl3pl4utgqqw2v4jna.b32.i2p", + "ukeu3k5oycgaauneqgtnvselmt4yemvoilkln7jpvamvfx7dnkdq.b32.i2p", +] + class EvoNode: mn: MasternodeInfo node: TestNode @@ -191,6 +206,12 @@ def run_test(self): self.test_empty_fields() self.log.info("Test output masternode address fields for consistency (post-fork)") self.test_shims() + # Need to destroy masternodes as the next test will be re-creating them + self.node_evo.destroy_mn(self) + self.node_two.destroy_mn(self) + self.reconnect_nodes() + self.log.info("Test unique properties map duplication checks") + self.test_uniqueness() def test_validation_common(self): # Arrays of addresses with invalid inputs get refused @@ -326,6 +347,15 @@ def test_validation_extended(self): 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}"])])[0]['allowed'] + # coreP2PAddrs and platformP2PAddrs accept privacy network domains and platformHTTPSAddrs additionally supports internet domains + # Note: I2P entries cannot be differentiated by port, they must always use port 0 + assert self.node_evo.node.testmempoolaccept([ + self.node_evo.register_mn(self, False, + [f"127.0.0.1:{self.node_evo.mn.nodePort}", f"{DOMAINS_TOR[0]}:{self.node_evo.mn.nodePort}", f"{DOMAINS_I2P[0]}:0"], + [f"127.0.0.1:{DEFAULT_PORT_PLATFORM_P2P}", f"{DOMAINS_TOR[0]}:{DEFAULT_PORT_PLATFORM_P2P}", f"{DOMAINS_I2P[1]}:0"], + [f"127.0.0.1:{DEFAULT_PORT_PLATFORM_HTTP}", f"{DOMAINS_TOR[0]}:{DEFAULT_PORT_PLATFORM_HTTP}", f"{DOMAINS_I2P[2]}:0", + f"{DOMAINS_CLR[0]}:{DEFAULT_PORT_PLATFORM_HTTP}"] )])[0]['allowed'] + # Port numbers may not be wrapped in arrays, either as integers or strings 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 platformP2PAddrs[0], must be string") @@ -511,5 +541,40 @@ def test_shims(self): self.node_evo.set_active_state(self, False) self.reconnect_nodes() + def test_uniqueness(self): + # Empty registrations are not registered as conflicts + self.node_evo.register_mn(self, True, "", "", "") + self.node_two.register_mn(self, True, "", "", "") + + # Validate that the unique properties map correctly recognizes entries as duplicates + self.node_evo.update_mn(self, True, + [f"127.0.0.1:{self.node_evo.mn.nodePort}", f"{DOMAINS_TOR[0]}:{self.node_evo.mn.nodePort}"], + [f"127.0.0.1:{DEFAULT_PORT_PLATFORM_P2P}", f"{DOMAINS_I2P[0]}:0"], + [f"127.0.0.1:{DEFAULT_PORT_PLATFORM_HTTP}", f"{DOMAINS_CLR[0]}:{DEFAULT_PORT_PLATFORM_HTTP}"]) + + def update_node_two(self, duplicate_addr = None, duplicate_tor = None, duplicate_i2p = None, duplicate_domain = None): + args = [ + self, True, + [duplicate_addr or f"127.0.0.2:{self.node_two.mn.nodePort}", duplicate_tor or f"{DOMAINS_TOR[1]}:{self.node_two.mn.nodePort}"], + [f"127.0.0.2:{DEFAULT_PORT_PLATFORM_P2P}", duplicate_i2p or f"{DOMAINS_I2P[1]}:0"], + [f"127.0.0.2:{DEFAULT_PORT_PLATFORM_HTTP}", duplicate_domain or f"{DOMAINS_CLR[1]}:{DEFAULT_PORT_PLATFORM_HTTP}"] + ] + if duplicate_addr or duplicate_tor or duplicate_i2p or duplicate_domain: + args += [-1, "bad-protx-dup-netinfo-entry"] + self.node_two.update_mn(*args) + + # Check for detection of duplicate IP:addr (CService) + update_node_two(self, duplicate_addr=f"127.0.0.1:{self.node_evo.mn.nodePort}") + + # Check for detection of duplicate privacy addr (CService) + update_node_two(self, duplicate_tor=f"{DOMAINS_TOR[0]}:{self.node_evo.mn.nodePort}") + update_node_two(self, duplicate_i2p=f"{DOMAINS_I2P[0]}:0") + + # Check for detection of duplicate internet addr (DomainPort) + update_node_two(self, duplicate_domain=f"{DOMAINS_CLR[0]}:{DEFAULT_PORT_PLATFORM_HTTP}") + + # All non-duplicate entries should still succeed + update_node_two(self) + if __name__ == "__main__": NetInfoTest().main()