diff --git a/src/evo/deterministicmns.cpp b/src/evo/deterministicmns.cpp index ea789de75b59b..858611bdc656c 100644 --- a/src/evo/deterministicmns.cpp +++ b/src/evo/deterministicmns.cpp @@ -470,11 +470,17 @@ 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 CService& entry : dmn->pdmnState->netInfo.GetEntries()) { - if (!AddUniqueProperty(*dmn, entry)) { - mnUniquePropertyMap = mnUniquePropertyMapSaved; - throw(std::runtime_error(strprintf("%s: Can't add a masternode %s with a duplicate address=%s", __func__, - dmn->proTxHash.ToString(), entry.ToStringAddrPort()))); + 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)) { + 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())); + } + } else { + throw std::runtime_error( + strprintf("%s: Can't add a masternode %s with invalid address", __func__, dmn->proTxHash.ToString())); } } if (!AddUniqueProperty(*dmn, dmn->pdmnState->keyIDOwner)) { @@ -518,14 +524,24 @@ 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 CService& old_entry : oldState->netInfo.GetEntries()) { - if (!DeleteUniqueProperty(*dmn, old_entry)) { - return strprintf("internal error"); // This shouldn't be possible + for (const NetInfoEntry& old_entry : oldState->netInfo.GetEntries()) { + if (const auto& service_opt{old_entry.GetAddrPort()}; service_opt.has_value()) { + const CService& service{service_opt.value()}; + if (!DeleteUniqueProperty(*dmn, service)) { + return strprintf("internal error"); // This shouldn't be possible + } + } else { + return strprintf("invalid address"); } } - for (const CService& new_entry : pdmnState->netInfo.GetEntries()) { - if (!AddUniqueProperty(*dmn, new_entry)) { - return strprintf("duplicate (%s)", new_entry.ToStringAddrPort()); + for (const NetInfoEntry& new_entry : pdmnState->netInfo.GetEntries()) { + if (const auto& service_opt{new_entry.GetAddrPort()}; service_opt.has_value()) { + const CService& service{service_opt.value()}; + if (!AddUniqueProperty(*dmn, service)) { + return strprintf("duplicate (%s)", service.ToStringAddrPort()); + } + } else { + return strprintf("invalid address"); } } } @@ -591,11 +607,17 @@ 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 CService& entry : dmn->pdmnState->netInfo.GetEntries()) { - if (!DeleteUniqueProperty(*dmn, entry)) { - mnUniquePropertyMap = mnUniquePropertyMapSaved; - throw(std::runtime_error(strprintf("%s: Can't delete a masternode %s with an address=%s", __func__, - proTxHash.ToString(), entry.ToStringAddrPort()))); + 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)) { + mnUniquePropertyMap = mnUniquePropertyMapSaved; + throw std::runtime_error(strprintf("%s: Can't delete a masternode %s with an address=%s", __func__, + proTxHash.ToString(), service.ToStringAddrPort())); + } + } else { + throw std::runtime_error(strprintf("%s: Can't delete a masternode %s with invalid address", __func__, + dmn->proTxHash.ToString())); } } if (!DeleteUniqueProperty(*dmn, dmn->pdmnState->keyIDOwner)) { @@ -811,9 +833,14 @@ bool CDeterministicMNManager::BuildNewListFromBlock(const CBlock& block, gsl::no } } - for (const CService& entry : proTx.netInfo.GetEntries()) { - if (newList.HasUniqueProperty(entry)) { - return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "bad-protx-dup-netinfo-entry"); + 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)) { + return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "bad-protx-dup-netinfo-entry"); + } + } else { + return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "bad-protx-netinfo-entry"); } } if (newList.HasUniqueProperty(proTx.keyIDOwner) || newList.HasUniqueProperty(proTx.pubKeyOperator)) { @@ -842,10 +869,15 @@ bool CDeterministicMNManager::BuildNewListFromBlock(const CBlock& block, gsl::no return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "bad-protx-payload"); } - for (const CService& entry : opt_proTx->netInfo.GetEntries()) { - if (newList.HasUniqueProperty(entry) && - newList.GetUniquePropertyMN(entry)->proTxHash != opt_proTx->proTxHash) { - return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "bad-protx-dup-netinfo-entry"); + 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) { + return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "bad-protx-dup-netinfo-entry"); + } + } else { + return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "bad-protx-netinfo-entry"); } } @@ -1205,12 +1237,22 @@ template static bool CheckService(const ProTx& proTx, TxValidationState& state) { switch (proTx.netInfo.Validate()) { - case NetInfoStatus::BadInput: - return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-netinfo"); + case NetInfoStatus::BadAddress: + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-netinfo-addr"); case NetInfoStatus::BadPort: return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-netinfo-port"); + case NetInfoStatus::BadType: + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-netinfo-addr-type"); + case NetInfoStatus::NotRoutable: + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-netinfo-addr-unroutable"); + case NetInfoStatus::Malformed: + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-netinfo-bad"); case NetInfoStatus::Success: return true; + // Shouldn't be possible during self-checks + case NetInfoStatus::BadInput: + case NetInfoStatus::MaxLimit: + ASSERT_IF_DEBUG(false); } // no default case, so the compiler can warn about missing cases assert(false); } @@ -1373,10 +1415,15 @@ 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 CService& entry : opt_ptx->netInfo.GetEntries()) { - if (mnList.HasUniqueProperty(entry) && - mnList.GetUniquePropertyMN(entry)->collateralOutpoint != collateralOutpoint) { - return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-dup-netinfo-entry"); + 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) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-dup-netinfo-entry"); + } + } else { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-netinfo-entry"); } } @@ -1446,9 +1493,14 @@ bool CheckProUpServTx(CDeterministicMNManager& dmnman, const CTransaction& tx, g } // don't allow updating to addresses already used by other MNs - for (const CService& entry : opt_ptx->netInfo.GetEntries()) { - if (mnList.HasUniqueProperty(entry) && mnList.GetUniquePropertyMN(entry)->proTxHash != opt_ptx->proTxHash) { - return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-dup-netinfo-entry"); + 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) { + 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/deterministicmns.h b/src/evo/deterministicmns.h index 386d98a23063e..bb94a6aa4da03 100644 --- a/src/evo/deterministicmns.h +++ b/src/evo/deterministicmns.h @@ -398,6 +398,7 @@ class CDeterministicMNList static_assert(!std::is_same_v, name>, "GetUniquePropertyHash cannot be templated against " #name) DMNL_NO_TEMPLATE(CBLSPublicKey); DMNL_NO_TEMPLATE(MnNetInfo); + DMNL_NO_TEMPLATE(NetInfoEntry); #undef DMNL_NO_TEMPLATE return ::SerializeHash(v); } diff --git a/src/evo/netinfo.cpp b/src/evo/netinfo.cpp index 38e767d69ca70..4df42b4e00de7 100644 --- a/src/evo/netinfo.cpp +++ b/src/evo/netinfo.cpp @@ -13,6 +13,9 @@ namespace { static std::unique_ptr g_main_params{nullptr}; static std::once_flag g_main_params_flag; +static const CService empty_service{CService()}; + +static constexpr std::string_view SAFE_CHARS_IPV4{"1234567890."}; bool IsNodeOnMainnet() { return Params().NetworkIDString() == CBaseChainParams::MAIN; } const CChainParams& MainParams() @@ -22,18 +25,148 @@ const CChainParams& MainParams() [&]() { 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 NetInfoEntry::operator==(const NetInfoEntry& rhs) const +{ + if (m_type != rhs.m_type) return false; + return std::visit( + [](auto&& lhs, auto&& rhs) -> bool { + if constexpr (std::is_same_v) { + return lhs == rhs; + } + return false; + }, + m_data, rhs.m_data); +} + +bool NetInfoEntry::operator<(const NetInfoEntry& rhs) const +{ + if (m_type != rhs.m_type) return m_type < rhs.m_type; + return std::visit( + [](auto&& lhs, auto&& rhs) -> bool { + using T1 = std::decay_t; + using T2 = std::decay_t; + 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) { + // lhs is monostate and rhs is not, rhs is greater + return true; + } else if constexpr (!std::is_same_v && std::is_same_v) { + // rhs is monostate but lhs is not, lhs is greater + return false; + } + return false; + }, + m_data, rhs.m_data); +} + +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()); + 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) { + return input.GetPort(); + } else { + return 0; + } + }, + m_data); +} + +// NetInfoEntry is a dumb object that doesn't enforce validation rules, that is the responsibility of +// types that utilize NetInfoEntry (MnNetInfo and others). IsTriviallyValid() is there to check if a +// NetInfoEntry object is properly constructed. +bool NetInfoEntry::IsTriviallyValid() const +{ + if (m_type == NetInfoType::Invalid) return false; + return std::visit( + [this](auto&& input) -> bool { + using T1 = std::decay_t; + if constexpr (std::is_same_v) { + // Empty underlying data isn't a valid entry + 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::Service) return false; + // Underlying data must meet surface-level validity checks for its type + if (!input.IsValid()) return false; + } else { + return false; + } + return true; + }, + m_data); +} + +std::string NetInfoEntry::ToString() const +{ + return std::visit( + [](auto&& input) -> std::string { + using T1 = std::decay_t; + if constexpr (std::is_same_v) { + return strprintf("CService(addr=%s, port=%u)", input.ToStringAddr(), input.GetPort()); + } else { + return "[invalid entry]"; + } + }, + m_data); +} + +std::string NetInfoEntry::ToStringAddr() const +{ + return std::visit( + [](auto&& input) -> std::string { + using T1 = std::decay_t; + if constexpr (std::is_same_v) { + return input.ToStringAddr(); + } else { + return "[invalid entry]"; + } + }, + m_data); +} + +std::string NetInfoEntry::ToStringAddrPort() const +{ + return std::visit( + [](auto&& input) -> std::string { + using T1 = std::decay_t; + if constexpr (std::is_same_v) { + return input.ToStringAddrPort(); + } else { + return "[invalid entry]"; + } + }, + m_data); +} + NetInfoStatus MnNetInfo::ValidateService(const CService& service) { if (!service.IsValid()) { - return NetInfoStatus::BadInput; + return NetInfoStatus::BadAddress; } if (!service.IsIPv4()) { - return NetInfoStatus::BadInput; + return NetInfoStatus::BadType; } if (Params().RequireRoutableExternalIP() && !service.IsRoutable()) { - return NetInfoStatus::BadInput; + return NetInfoStatus::NotRoutable; } if (IsNodeOnMainnet() != (service.GetPort() == MainParams().GetDefaultPort())) { @@ -47,34 +180,60 @@ NetInfoStatus MnNetInfo::ValidateService(const CService& service) NetInfoStatus MnNetInfo::AddEntry(const std::string& input) { - if (auto service = Lookup(input, /*portDefault=*/Params().GetDefaultPort(), /*fAllowLookup=*/false); - service.has_value()) { - const auto ret = ValidateService(service.value()); + if (!IsEmpty()) { + return NetInfoStatus::MaxLimit; + } + + std::string addr; + uint16_t port{Params().GetDefaultPort()}; + SplitHostPort(input, port, addr); + // Contains invalid characters, unlikely to pass Lookup(), fast-fail + if (!MatchCharsFilter(addr, SAFE_CHARS_IPV4)) { + return NetInfoStatus::BadInput; + } + + if (auto service_opt{Lookup(addr, /*portDefault=*/port, /*fAllowLookup=*/false)}) { + const auto ret{ValidateService(*service_opt)}; if (ret == NetInfoStatus::Success) { - m_addr = service.value(); - ASSERT_IF_DEBUG(m_addr != CService()); + m_addr = NetInfoEntry{*service_opt}; + ASSERT_IF_DEBUG(m_addr.GetAddrPort().has_value()); } return ret; } return NetInfoStatus::BadInput; } -CServiceList MnNetInfo::GetEntries() const +NetInfoList MnNetInfo::GetEntries() const { - CServiceList ret; if (!IsEmpty()) { - ASSERT_IF_DEBUG(m_addr != CService()); - ret.push_back(m_addr); + ASSERT_IF_DEBUG(m_addr.GetAddrPort().has_value()); + return {m_addr}; } // If MnNetInfo is empty, we probably don't expect any entries to show up, so // we return a blank set instead. - return ret; + return {}; +} + +const CService& MnNetInfo::GetPrimary() const +{ + if (const auto& service_opt{m_addr.GetAddrPort()}) { + return *service_opt; + } + return empty_service; +} + +NetInfoStatus MnNetInfo::Validate() const +{ + if (!m_addr.IsTriviallyValid()) { + return NetInfoStatus::Malformed; + } + return ValidateService(GetPrimary()); } std::string MnNetInfo::ToString() const { // Extra padding to account for padding done by the calling function. return strprintf("MnNetInfo()\n" - " CService(addr=%s, port=%u)\n", - m_addr.ToStringAddr(), m_addr.GetPort()); + " %s\n", + m_addr.ToString()); } diff --git a/src/evo/netinfo.h b/src/evo/netinfo.h index a264d76b8e0d9..2db0a46302871 100644 --- a/src/evo/netinfo.h +++ b/src/evo/netinfo.h @@ -7,34 +7,143 @@ #include #include +#include + +#include class CService; enum class NetInfoStatus : uint8_t { + // Managing entries BadInput, + MaxLimit, + + // Validation + BadAddress, BadPort, + BadType, + NotRoutable, + Malformed, + Success }; constexpr std::string_view NISToString(const NetInfoStatus code) { switch (code) { - case NetInfoStatus::BadInput: + case NetInfoStatus::BadAddress: return "invalid address"; + case NetInfoStatus::BadInput: + return "invalid input"; case NetInfoStatus::BadPort: return "invalid port"; + case NetInfoStatus::BadType: + return "invalid address type"; + case NetInfoStatus::NotRoutable: + return "unroutable address"; + case NetInfoStatus::Malformed: + return "malformed"; + case NetInfoStatus::MaxLimit: + return "too many entries"; case NetInfoStatus::Success: return "success"; } // no default case, so the compiler can warn about missing cases assert(false); } -using CServiceList = std::vector>; +class NetInfoEntry +{ +public: + enum NetInfoType : uint8_t { + Service = 0x01, + Invalid = 0xff + }; + +private: + uint8_t m_type{NetInfoType::Invalid}; + std::variant m_data{std::monostate{}}; + +public: + NetInfoEntry() = default; + NetInfoEntry(const CService& service) + { + if (!service.IsValid()) return; + m_type = NetInfoType::Service; + m_data = service; + } + + ~NetInfoEntry() = default; + + bool operator<(const NetInfoEntry& rhs) const; + bool operator==(const NetInfoEntry& rhs) const; + bool operator!=(const NetInfoEntry& rhs) const { return !(*this == rhs); } + + template + void Serialize(Stream& s_) const + { + OverrideStream s(&s_, s_.GetType(), s_.GetVersion() | ADDRV2_FORMAT); + 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 { + s << NetInfoType::Invalid; + } + } + + void Serialize(CSizeComputer& s) const + { + auto size = ::GetSerializeSize(uint8_t{}, s.GetVersion()); + if (m_type == NetInfoType::Service) { + size += ::GetSerializeSize(CService{}, s.GetVersion() | ADDRV2_FORMAT); + } + s.seek(size); + } + + template + void Unserialize(Stream& s_) + { + OverrideStream s(&s_, s_.GetType(), s_.GetVersion() | ADDRV2_FORMAT); + s >> m_type; + if (m_type == NetInfoType::Service) { + m_data = CService{}; + try { + CService& service{std::get(m_data)}; + s >> service; + if (!service.IsValid()) { + throw std::ios_base::failure("Invalid CService"); + } + } catch (const std::ios_base::failure&) { + Clear(); + } + } else { + // Invalid type, reset to mark object as invalid + Clear(); + } + } + + void Clear() + { + m_type = NetInfoType::Invalid; + m_data = std::monostate{}; + } + + std::optional> GetAddrPort() const; + uint16_t GetPort() const; + bool IsEmpty() const { return *this == NetInfoEntry{}; } + bool IsTriviallyValid() const; + std::string ToString() const; + std::string ToStringAddr() const; + std::string ToStringAddrPort() const; +}; + +template<> struct is_serializable_enum : std::true_type {}; + +using NetInfoList = std::vector>; class MnNetInfo { private: - CService m_addr{}; + NetInfoEntry m_addr{}; private: static NetInfoStatus ValidateService(const CService& service); @@ -46,20 +155,38 @@ class MnNetInfo bool operator==(const MnNetInfo& rhs) const { return m_addr == rhs.m_addr; } bool operator!=(const MnNetInfo& rhs) const { return !(*this == rhs); } - SERIALIZE_METHODS(MnNetInfo, obj) + template + void Serialize(Stream& s) const + { + if (const auto& service{m_addr.GetAddrPort()}; service.has_value()) { + s << service->get(); + } else { + s << CService{}; + } + } + + void Serialize(CSizeComputer& s) const + { + s.seek(::GetSerializeSize(CService{}, s.GetVersion())); + } + + template + void Unserialize(Stream& s) { - READWRITE(obj.m_addr); + CService service; + s >> service; + m_addr = NetInfoEntry{service}; } NetInfoStatus AddEntry(const std::string& service); - CServiceList GetEntries() const; + NetInfoList GetEntries() const; - const CService& GetPrimary() const { return m_addr; } + const CService& GetPrimary() const; bool IsEmpty() const { return *this == MnNetInfo(); } - NetInfoStatus Validate() const { return ValidateService(m_addr); } + NetInfoStatus Validate() const; std::string ToString() const; - void Clear() { m_addr = CService(); } + void Clear() { m_addr.Clear(); } }; #endif // BITCOIN_EVO_NETINFO_H diff --git a/src/evo/providertx.cpp b/src/evo/providertx.cpp index cb869fc2b88c6..c2684fc0758bc 100644 --- a/src/evo/providertx.cpp +++ b/src/evo/providertx.cpp @@ -36,6 +36,11 @@ bool CProRegTx::IsTriviallyValid(bool is_basic_scheme_active, TxValidationState& if (!scriptPayout.IsPayToPublicKeyHash() && !scriptPayout.IsPayToScriptHash()) { return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-payee"); } + for (const NetInfoEntry& entry : netInfo.GetEntries()) { + if (!entry.IsTriviallyValid()) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-netinfo-bad"); + } + } CTxDestination payoutDest; if (!ExtractDestination(scriptPayout, payoutDest)) { @@ -105,6 +110,14 @@ bool CProUpServTx::IsTriviallyValid(bool is_basic_scheme_active, TxValidationSta if (nVersion < ProTxVersion::BasicBLS && nType == MnType::Evo) { return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-protx-evo-version"); } + if (netInfo.IsEmpty()) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-netinfo-empty"); + } + for (const NetInfoEntry& entry : netInfo.GetEntries()) { + if (!entry.IsTriviallyValid()) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-netinfo-bad"); + } + } return true; } diff --git a/src/rpc/masternode.cpp b/src/rpc/masternode.cpp index 601ad845b4b13..7f98c03b3af70 100644 --- a/src/rpc/masternode.cpp +++ b/src/rpc/masternode.cpp @@ -568,7 +568,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 CService& entry : dmn.pdmnState->netInfo.GetEntries()) { + for (const NetInfoEntry& entry : dmn.pdmnState->netInfo.GetEntries()) { strAddress += entry.ToStringAddrPort() + " "; } if (!strAddress.empty()) strAddress.pop_back(); // Remove trailing space diff --git a/src/test/evo_netinfo_tests.cpp b/src/test/evo_netinfo_tests.cpp index 2c8952b234f0c..234f149813d5a 100644 --- a/src/test/evo_netinfo_tests.cpp +++ b/src/test/evo_netinfo_tests.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -21,9 +22,9 @@ const std::vector> entry; + BOOST_CHECK(entry.IsEmpty() && !entry.IsTriviallyValid()); + } + + { + // Reading an invalid CService should fail trivial validation and return an empty object + CDataStream ds(SER_DISK, CLIENT_VERSION); + NetInfoEntry entry{}; + ds << NetInfoEntry::NetInfoType::Service << CService{}; + ds >> entry; + BOOST_CHECK(entry.IsEmpty() && !entry.IsTriviallyValid()); + } + + { + // Reading an unrecognized type should fail trivial validation and return an empty object + CDataStream ds(SER_DISK, CLIENT_VERSION); + NetInfoEntry entry{}; + ds << NetInfoEntry::NetInfoType::Service << uint256{}; + ds >> entry; + BOOST_CHECK(entry.IsEmpty() && !entry.IsTriviallyValid()); + } + + { + // A valid CService should be constructable, readable and pass validation + CDataStream ds(SER_DISK, CLIENT_VERSION | ADDRV2_FORMAT); + CService service{LookupNumeric("1.1.1.1", Params().GetDefaultPort())}; + BOOST_CHECK(service.IsValid()); + NetInfoEntry entry{service}, entry2{}; + ds << NetInfoEntry::NetInfoType::Service << service; + ds >> entry2; + BOOST_CHECK(entry == entry2); + BOOST_CHECK(!entry.IsEmpty() && entry.IsTriviallyValid()); + BOOST_CHECK(entry.GetAddrPort().value() == service); + } + + { + // NetInfoEntry should be able to read and write ADDRV2 addresses + CService service{}; + service.SetSpecial("pg6mmjiyjmcrsslvykfwnntlaru7p5svn6y2ymmju6nubxndf4pscryd.onion"); + BOOST_CHECK(service.IsValid() && service.IsTor()); + + CDataStream ds(SER_DISK, CLIENT_VERSION | ADDRV2_FORMAT); + ds << NetInfoEntry::NetInfoType::Service << service; + ds.SetVersion(CLIENT_VERSION); // Drop the explicit format flag + + NetInfoEntry entry{}; + ds >> entry; + BOOST_CHECK(!entry.IsEmpty() && entry.IsTriviallyValid()); + BOOST_CHECK(entry.GetAddrPort().value() == service); + ds.clear(); + + NetInfoEntry entry2{}; + ds << entry; + ds >> entry2; + BOOST_CHECK(entry == entry2 && entry2.GetAddrPort().value() == service); + } +} + +BOOST_AUTO_TEST_CASE(netinfo_retvals) +{ + uint16_t p2p_port{Params().GetDefaultPort()}; + CService service{LookupNumeric("1.1.1.1", p2p_port)}, service2{LookupNumeric("1.1.1.2", p2p_port)}; + NetInfoEntry entry{service}, entry2{service2}, entry_empty{}; + + // Check that values are correctly recorded and pass trivial validation + BOOST_CHECK(service.IsValid()); + BOOST_CHECK(!entry.IsEmpty() && entry.IsTriviallyValid()); + BOOST_CHECK(entry.GetAddrPort().value() == service); + BOOST_CHECK(!entry2.IsEmpty() && entry2.IsTriviallyValid()); + BOOST_CHECK(entry2.GetAddrPort().value() == service2); + + // Check that dispatch returns the expected values + BOOST_CHECK_EQUAL(entry.GetPort(), service.GetPort()); + BOOST_CHECK_EQUAL(entry.ToString(), strprintf("CService(addr=%s, port=%u)", service.ToStringAddr(), service.GetPort())); + BOOST_CHECK_EQUAL(entry.ToStringAddr(), service.ToStringAddr()); + BOOST_CHECK_EQUAL(entry.ToStringAddrPort(), service.ToStringAddrPort()); + BOOST_CHECK_EQUAL(service < service2, entry < entry2); + + // Check that empty/invalid entries return error messages + BOOST_CHECK_EQUAL(entry_empty.GetPort(), 0); + BOOST_CHECK_EQUAL(entry_empty.ToString(), "[invalid entry]"); + BOOST_CHECK_EQUAL(entry_empty.ToStringAddr(), "[invalid entry]"); + BOOST_CHECK_EQUAL(entry_empty.ToStringAddrPort(), "[invalid entry]"); + + // The invalid entry type code is 0xff (highest possible value) and therefore will return as greater + // in comparison to any valid entry + BOOST_CHECK(entry < entry_empty); +} + +bool CheckIfSerSame(const CService& lhs, const MnNetInfo& rhs) +{ + CHashWriter ss_lhs(SER_GETHASH, 0), ss_rhs(SER_GETHASH, 0); + ss_lhs << lhs; + ss_rhs << rhs; + return ss_lhs.GetSHA256() == ss_rhs.GetSHA256(); +} + +BOOST_AUTO_TEST_CASE(cservice_compatible) +{ + // Empty values should be the same + CService service; + MnNetInfo netInfo; + BOOST_CHECK(CheckIfSerSame(service, netInfo)); + + // 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(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(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(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(CheckIfSerSame(service, netInfo)); } BOOST_AUTO_TEST_SUITE_END() diff --git a/src/txmempool.cpp b/src/txmempool.cpp index 53d90a6be9fa5..fbbd8c098a1c0 100644 --- a/src/txmempool.cpp +++ b/src/txmempool.cpp @@ -691,7 +691,7 @@ void CTxMemPool::addUncheckedProTx(indexed_transaction_set::iterator& newit, con if (!proTx.collateralOutpoint.hash.IsNull()) { mapProTxRefs.emplace(tx_hash, proTx.collateralOutpoint.hash); } - for (const CService& entry : proTx.netInfo.GetEntries()) { + for (const NetInfoEntry& entry : proTx.netInfo.GetEntries()) { mapProTxAddresses.emplace(entry, tx_hash); } mapProTxPubKeyIDs.emplace(proTx.keyIDOwner, tx_hash); @@ -704,7 +704,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 CService& entry : proTx.netInfo.GetEntries()) { + for (const NetInfoEntry& entry : proTx.netInfo.GetEntries()) { mapProTxAddresses.emplace(entry, tx_hash); } } else if (tx.nType == TRANSACTION_PROVIDER_UPDATE_REGISTRAR) { @@ -795,7 +795,7 @@ void CTxMemPool::removeUncheckedProTx(const CTransaction& tx) if (!proTx.collateralOutpoint.IsNull()) { eraseProTxRef(tx_hash, proTx.collateralOutpoint.hash); } - for (const CService& entry : proTx.netInfo.GetEntries()) { + for (const NetInfoEntry& entry : proTx.netInfo.GetEntries()) { mapProTxAddresses.erase(entry); } mapProTxPubKeyIDs.erase(proTx.keyIDOwner); @@ -805,7 +805,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 CService& entry : proTx.netInfo.GetEntries()) { + for (const NetInfoEntry& entry : proTx.netInfo.GetEntries()) { mapProTxAddresses.erase(entry); } } else if (tx.nType == TRANSACTION_PROVIDER_UPDATE_REGISTRAR) { @@ -1034,7 +1034,7 @@ void CTxMemPool::removeProTxConflicts(const CTransaction &tx) } auto& proTx = *opt_proTx; - for (const CService& entry : proTx.netInfo.GetEntries()) { + for (const NetInfoEntry& entry : proTx.netInfo.GetEntries()) { if (mapProTxAddresses.count(entry)) { uint256 conflictHash = mapProTxAddresses[entry]; if (conflictHash != tx_hash && mapTx.count(conflictHash)) { @@ -1056,7 +1056,7 @@ void CTxMemPool::removeProTxConflicts(const CTransaction &tx) return; } - for (const CService& entry : opt_proTx->netInfo.GetEntries()) { + for (const NetInfoEntry& entry : opt_proTx->netInfo.GetEntries()) { if (mapProTxAddresses.count(entry)) { uint256 conflictHash = mapProTxAddresses[entry]; if (conflictHash != tx_hash && mapTx.count(conflictHash)) { @@ -1394,7 +1394,7 @@ bool CTxMemPool::existsProviderTxConflict(const CTransaction &tx) const { return true; // i.e. can't decode payload == conflict } auto& proTx = *opt_proTx; - for (const CService& entry : proTx.netInfo.GetEntries()) { + for (const NetInfoEntry& entry : proTx.netInfo.GetEntries()) { if (mapProTxAddresses.count(entry)) { return true; } @@ -1419,7 +1419,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 CService& entry : opt_proTx->netInfo.GetEntries()) { + for (const NetInfoEntry& entry : opt_proTx->netInfo.GetEntries()) { auto it = mapProTxAddresses.find(entry); if (it != mapProTxAddresses.end() && it->second != opt_proTx->proTxHash) { return true; diff --git a/src/txmempool.h b/src/txmempool.h index 9bdc667dc4d0c..6bb254eb22059 100644 --- a/src/txmempool.h +++ b/src/txmempool.h @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -525,7 +526,7 @@ class CTxMemPool std::map> mapSpentInserted; std::multimap mapProTxRefs; // proTxHash -> transaction (all TXs that refer to an existing proTx) - std::map mapProTxAddresses; + std::map mapProTxAddresses; std::map mapProTxPubKeyIDs; std::map mapProTxBlsPubKeyHashes; std::map mapProTxCollaterals;