From 4a449f6fa148e8d074613e2aafe74d6e84d8c49b Mon Sep 17 00:00:00 2001 From: pasta Date: Tue, 3 Nov 2026 18:13:12 +0000 Subject: [PATCH 1/2] fix(evo): pin the mainnet ports of version 3 primary provider addresses Legacy provider transactions already require port 9999 for Core P2P and 26656/443 for Platform on mainnet, and the extended addresses draft (DIP-0003 Appendix C) keeps that for the first entry of each purpose. ExtNetInfo enforced neither, so a version 3 ProTx could move a mainnet masternode's primary addresses to any permitted port. On mainnet, require the first entry of each purpose to use its standard port. Additional entries keep the general port rules, so they may use the standard ports too, e.g. for a second IPv4 address. The rule depends only on the network, so ExtNetInfo::Validate() and AddEntry() apply it. Co-Authored-By: Claude Opus 5.5 --- doc/release-notes-8018.md | 7 ++ src/evo/netinfo.cpp | 31 +++++++ src/rpc/evo.cpp | 8 +- src/test/evo_netinfo_tests.cpp | 143 +++++++++++++++++++++++++++++++- src/test/evo_snapshot_tests.cpp | 4 +- 5 files changed, 186 insertions(+), 7 deletions(-) create mode 100644 doc/release-notes-8018.md diff --git a/doc/release-notes-8018.md b/doc/release-notes-8018.md new file mode 100644 index 000000000000..73d47091f8e7 --- /dev/null +++ b/doc/release-notes-8018.md @@ -0,0 +1,7 @@ +Consensus changes +----------------- + +- On mainnet, the first entry of each address list in a version 3 (extended addresses) ProRegTx or + ProUpServTx must use that list's standard port: 9999 for `coreP2PAddrs`, 26656 for `platformP2PAddrs` and + 443 for `platformHTTPSAddrs`, as legacy ProTx versions already require on mainnet. Additional entries + may use any otherwise valid port. Violations are rejected with `bad-protx-netinfo-port`. diff --git a/src/evo/netinfo.cpp b/src/evo/netinfo.cpp index b5c806706d92..029c60e8a2a7 100644 --- a/src/evo/netinfo.cpp +++ b/src/evo/netinfo.cpp @@ -64,6 +64,27 @@ bool IsAllowedPlatformHTTPPort(uint16_t port) } return false; } + +uint16_t GetPrimaryAddrPort(NetInfoPurpose purpose) +{ + switch (purpose) { + case NetInfoPurpose::CORE_P2P: + return MainParams().GetDefaultPort(); + case NetInfoPurpose::PLATFORM_P2P: + return MainParams().GetDefaultPlatformP2PPort(); + case NetInfoPurpose::PLATFORM_HTTPS: + return MainParams().GetDefaultPlatformHTTPPort(); + } // no default case, so the compiler can warn about missing cases + Assume(false); + return 0; +} + +//! On mainnet, the first entry of a purpose must use that purpose's standard port +NetInfoStatus ValidateMainnetPrimaryPort(NetInfoPurpose purpose, const NetInfoEntry& primary) +{ + if (!IsNodeOnMainnet() || primary.GetPort() == GetPrimaryAddrPort(purpose)) return NetInfoStatus::Success; + return NetInfoStatus::BadPort; +} } // anonymous namespace bool IsNodeOnMainnet() { return Params().NetworkIDString() == CBaseChainParams::MAIN; } @@ -427,6 +448,11 @@ NetInfoStatus ExtNetInfo::ProcessCandidate(const NetInfoPurpose purpose, const N // Domains only allowed for Platform HTTPS API return NetInfoStatus::BadInput; } + if (!HasEntries(purpose)) { + if (const auto ret{ValidateMainnetPrimaryPort(purpose, candidate)}; ret != NetInfoStatus::Success) { + return ret; + } + } if (auto it{m_data.find(purpose)}; it != m_data.end()) { // Existing entries list found, check limit auto& [_, entries] = *it; @@ -619,6 +645,11 @@ NetInfoStatus ExtNetInfo::Validate() const } } } + for (const auto& [purpose, entries] : m_data) { + if (const auto ret{ValidateMainnetPrimaryPort(purpose, entries.front())}; ret != NetInfoStatus::Success) { + return ret; + } + } return NetInfoStatus::Success; } diff --git a/src/rpc/evo.cpp b/src/rpc/evo.cpp index 4dcb7e37af44..012b286b5fb9 100644 --- a/src/rpc/evo.cpp +++ b/src/rpc/evo.cpp @@ -116,6 +116,7 @@ static RPCArg GetRpcArg(const std::string& strParamName) "Array of addresses in the form \"ADDR:PORT\". Must be unique on the network.\n" "A legacy ProTx can only store a single entry; storing multiple entries\n" "requires upgrading to a version 3 ProTx.\n" + "On mainnet, the first entry must use port 9999.\n" "Can be set to an empty string, which will require a ProUpServTx afterwards.", { {"address", RPCArg::Type::STR, RPCArg::Optional::NO, ""}, @@ -126,7 +127,8 @@ static RPCArg GetRpcArg(const std::string& strParamName) {"coreP2PAddrs", RPCArg::Type::ARR, RPCArg::Optional::NO, "Array of addresses in the form \"ADDR:PORT\". Must be unique on the network.\n" "A legacy ProTx can only store a single entry; storing multiple entries\n" - "requires upgrading to a version 3 ProTx.", + "requires upgrading to a version 3 ProTx.\n" + "On mainnet, the first entry must use port 9999.", { {"address", RPCArg::Type::STR, RPCArg::Optional::NO, ""}, }, @@ -246,6 +248,7 @@ static RPCArg GetRpcArg(const std::string& strParamName) "Array of addresses in the form \"ADDR:PORT\" used by Platform for peer-to-peer connection.\n" "For a legacy ProTx, pass a bare port number instead (e.g. 26656);\n" "the \"ADDR:PORT\" / address-array form requires upgrading to a version 3 ProTx.\n" + "On mainnet, the first entry must use port 26656.\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, ""}, @@ -257,6 +260,7 @@ static RPCArg GetRpcArg(const std::string& strParamName) "Array of addresses in the form \"ADDR:PORT\" used by Platform for peer-to-peer connection.\n" "For a legacy ProTx, pass a bare port number instead (e.g. 26656);\n" "the \"ADDR:PORT\" / address-array form requires upgrading to a version 3 ProTx.\n" + "On mainnet, the first entry must use port 26656.\n" "Must be unique on the network.", { {"address", RPCArg::Type::STR, RPCArg::Optional::NO, ""}, @@ -268,6 +272,7 @@ static RPCArg GetRpcArg(const std::string& strParamName) "Array of addresses in the form \"ADDR:PORT\" used by Platform for their HTTPS API.\n" "For a legacy ProTx, pass a bare port number instead (e.g. 443);\n" "the \"ADDR:PORT\" / address-array form requires upgrading to a version 3 ProTx.\n" + "On mainnet, the first entry must use port 443.\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, ""}, @@ -279,6 +284,7 @@ static RPCArg GetRpcArg(const std::string& strParamName) "Array of addresses in the form \"ADDR:PORT\" used by Platform for their HTTPS API.\n" "For a legacy ProTx, pass a bare port number instead (e.g. 443);\n" "the \"ADDR:PORT\" / address-array form requires upgrading to a version 3 ProTx.\n" + "On mainnet, the first entry must use port 443.\n" "Must be unique on the network.", { {"address", RPCArg::Type::STR, RPCArg::Optional::NO, ""}, diff --git a/src/test/evo_netinfo_tests.cpp b/src/test/evo_netinfo_tests.cpp index 20b4fe0fa329..8b49a9adfdc6 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 @@ -14,6 +15,7 @@ #include #include #include +#include #include @@ -103,19 +105,30 @@ void TestMnNetInfo(const std::vector& vals) void TestExtNetInfo(const std::vector& vals) { + // Mainnet has fixed rules for the first entry of each purpose (see provider_network_fields_main), so + // there the table checks the rules for additional entries, added behind valid primaries + const bool add_primaries{IsNodeOnMainnet()}; + const size_t primaries_count{add_primaries ? 3U : 0U}; for (const auto& [input, _, expected_ret] : vals) { const auto& [purpose, addr] = input; ExtNetInfo netInfo; + if (add_primaries) { + for (const auto& [primary_purpose, primary] : {std::pair{NetInfoPurpose::CORE_P2P, "1.2.3.4:9999"}, + std::pair{NetInfoPurpose::PLATFORM_P2P, "1.2.3.4:26656"}, + std::pair{NetInfoPurpose::PLATFORM_HTTPS, "1.2.3.4:443"}}) { + BOOST_REQUIRE_EQUAL(netInfo.AddEntry(primary_purpose, primary), NetInfoStatus::Success); + } + } 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()); + BOOST_CHECK_EQUAL(netInfo.Validate(), add_primaries ? NetInfoStatus::Success : NetInfoStatus::Malformed); + BOOST_CHECK_EQUAL(netInfo.HasEntries(purpose), add_primaries && IsValidPurpose(purpose)); + BOOST_CHECK_EQUAL(netInfo.GetEntries().size(), primaries_count); } else { BOOST_CHECK_EQUAL(netInfo.Validate(), NetInfoStatus::Success); BOOST_CHECK(netInfo.HasEntries(purpose)); - ValidateGetEntries(netInfo.GetEntries(), /*expected_size=*/1); + ValidateGetEntries(netInfo.GetEntries(), /*expected_size=*/primaries_count + 1); } } } @@ -195,6 +208,128 @@ BOOST_AUTO_TEST_CASE(provider_network_fields_main) /*optional=*/false); BOOST_REQUIRE(validation_error); BOOST_CHECK(validation_error->message.original.find("unroutable address") != std::string::npos); + + // On mainnet the first entry of every purpose must use that purpose's port (9999, 26656, 443). Additional + // entries may use other addresses, ports and network types. Shared masternodes use the same network fields. + const std::string a{"1.1.1.1"}, b{"1.1.1.2"}, ipv6{"[2606:4700:4700::1111]"}, + onion{"pg6mmjiyjmcrsslvykfwnntlaru7p5svn6y2ymmju6nubxndf4pscryd.onion"}, + i2p{"udhdrtrcetjm5sxzskjyr5ztpeszydbh4dpl3pl4utgqqw2v4jna.b32.i2p:0"}, domain{"platform.example.com"}; + const auto make = [](std::vector core, interfaces::ProviderPlatformEndpoints p2p = {}, + interfaces::ProviderPlatformEndpoints https = {}) { + return interfaces::ProviderNetInfo{.core_p2p = std::move(core), .platform_p2p = std::move(p2p), + .platform_https = std::move(https)}; + }; + using Strs = std::vector; + struct Case { + MnType type{MnType::Regular}; + interfaces::ProviderNetInfo net_info; + std::string error_field; // Empty if valid + std::string error_status{}; + }; + const std::vector primary_vals{ + // Valid, including additional entries of any type, address and port + {MnType::Regular, make({a + ":9999"}), ""}, + {MnType::Regular, make({a + ":9999", b + ":9998", ipv6 + ":9999", onion + ":9999"}), ""}, + {MnType::Regular, make({a + ":9999", i2p}), ""}, + {MnType::Evo, make({a + ":9999"}, Strs{a + ":26656"}, Strs{a + ":443"}), ""}, + {MnType::Evo, + make({a + ":9999", ipv6 + ":9999"}, Strs{a + ":26656", b + ":26657", ipv6 + ":26656", onion + ":26656"}, + Strs{a + ":443", domain + ":443", ipv6 + ":8443"}), + ""}, + {MnType::Evo, make({a + ":9999"}, Strs{a + ":26656"}, Strs{a + ":443", ipv6 + ":443", onion + ":443"}), ""}, + // Additional addresses may use the standard ports too, e.g. a second IPv4 address on the same ports + {MnType::Regular, make({a + ":9999", b + ":9999"}), ""}, + {MnType::Regular, make({a + ":9999", b + ":26656"}), ""}, + {MnType::Evo, + make({a + ":9999", b + ":9999"}, Strs{a + ":26656", b + ":26656"}, Strs{a + ":443", b + ":443"}), ""}, + // A bare Platform port is placed on the first Core P2P address + {MnType::Evo, make({a + ":9999"}, uint16_t{26656}, uint16_t{443}), ""}, + {MnType::Evo, make({a + ":9999"}, uint16_t{26657}, uint16_t{443}), "platformP2PAddrs[0]", "invalid port"}, + // The first entry of each purpose must use that purpose's port + {MnType::Regular, make({a + ":9998"}), "coreP2PAddrs[0]", "invalid port"}, + {MnType::Regular, make({i2p}), "coreP2PAddrs[0]", "invalid port"}, + {MnType::Evo, make({a + ":9998"}, Strs{a + ":26656"}, Strs{a + ":443"}), "coreP2PAddrs[0]", "invalid port"}, + {MnType::Evo, make({a + ":9999"}, Strs{a + ":26657"}, Strs{a + ":443"}), "platformP2PAddrs[0]", "invalid port"}, + {MnType::Evo, make({a + ":9999"}, Strs{a + ":26656"}, Strs{a + ":8443"}), "platformHTTPSAddrs[0]", + "invalid port"}, + {MnType::Evo, make({a + ":9999"}, Strs{a + ":26656"}, Strs{domain + ":8443", a + ":443"}), + "platformHTTPSAddrs[0]", "invalid port"}, + }; + for (const auto& [type, case_net_info, error_field, error_status] : primary_vals) { + validation_error = node->evo().validateProviderNetInfo(case_net_info, type, ProTxVersion::ExtAddr, + /*optional=*/false); + BOOST_CHECK_MESSAGE(!validation_error == error_field.empty(), + strprintf("%s: %s", Join(case_net_info.core_p2p, ","), + validation_error ? validation_error->message.original : "accepted")); + if (validation_error && !error_field.empty()) { + const auto& message{validation_error->message.original}; + BOOST_CHECK_MESSAGE(message.find(error_field) != std::string::npos && + message.find(strprintf("(%s)", error_status)) != std::string::npos, + message); + } + } +} + +struct TestNetSetup : public BasicTestingSetup { + TestNetSetup() : BasicTestingSetup{CBaseChainParams::TESTNET} {} +}; + +BOOST_FIXTURE_TEST_CASE(extnetinfo_ports_mainnet_only, TestNetSetup) +{ + struct Case { + MnType type{MnType::Regular}; + std::vector> entries; + bool valid_on_mainnet{false}; + }; + const std::string ipv6{"[2606:4700:4700::1111]"}; + const std::vector vals{ + {MnType::Regular, {{NetInfoPurpose::CORE_P2P, "1.1.1.1:19999"}}}, + {MnType::Evo, + {{NetInfoPurpose::CORE_P2P, "1.1.1.1:9999"}, {NetInfoPurpose::PLATFORM_P2P, "1.1.1.1:22000"}, + {NetInfoPurpose::PLATFORM_HTTPS, "1.1.1.1:443"}}}, + {MnType::Evo, + {{NetInfoPurpose::CORE_P2P, "1.1.1.1:9999"}, {NetInfoPurpose::PLATFORM_P2P, "1.1.1.1:26656"}, + {NetInfoPurpose::PLATFORM_HTTPS, "1.1.1.1:8443"}}}, + // Satisfies the mainnet port rules, with additional entries on any port, including the standard ones + {MnType::Evo, + {{NetInfoPurpose::CORE_P2P, "1.1.1.1:9999"}, {NetInfoPurpose::CORE_P2P, ipv6 + ":19999"}, + {NetInfoPurpose::CORE_P2P, "1.1.1.2:9999"}, {NetInfoPurpose::PLATFORM_P2P, "1.1.1.1:26656"}, + {NetInfoPurpose::PLATFORM_P2P, "1.1.1.2:22000"}, {NetInfoPurpose::PLATFORM_P2P, "1.1.1.3:26656"}, + {NetInfoPurpose::PLATFORM_HTTPS, "1.1.1.1:443"}, {NetInfoPurpose::PLATFORM_HTTPS, "1.1.1.2:443"}, + {NetInfoPurpose::PLATFORM_HTTPS, "platform.example.com:443"}}, + /*valid_on_mainnet=*/true}, + }; + const auto check_fields = [](const std::shared_ptr& net_info, MnType type, TxValidationState& state) { + return CheckProviderNetworkFields(net_info, type, ProTxVersion::ExtAddr, /*platform_node_id=*/nullptr, + /*platform_p2p_port=*/0, /*platform_http_port=*/0, /*allow_empty=*/false, + state); + }; + + // Outside mainnet, these port rules don't apply + std::vector serialized; + for (const auto& [type, entries, _] : vals) { + ExtNetInfo net_info; + for (const auto& [purpose, entry] : entries) { + BOOST_CHECK_EQUAL(net_info.AddEntry(purpose, entry), NetInfoStatus::Success); + } + BOOST_CHECK_EQUAL(net_info.Validate(), NetInfoStatus::Success); + TxValidationState state; + BOOST_CHECK(check_fields(std::make_shared(net_info), type, state)); + serialized.emplace_back(SER_DISK, CLIENT_VERSION) << net_info; + } + + // A serialized ProTx carries its entries as-is, so consensus on mainnet must apply the rules to the + // same objects even though they never passed through AddEntry() there + SelectParams(CBaseChainParams::MAIN); + for (size_t idx{0}; idx < vals.size(); ++idx) { + const auto& [type, _, valid_on_mainnet] = vals[idx]; + const auto net_info{std::make_shared(deserialize, serialized[idx])}; + BOOST_CHECK_EQUAL(net_info->Validate(), valid_on_mainnet ? NetInfoStatus::Success : NetInfoStatus::BadPort); + TxValidationState state; + BOOST_CHECK_EQUAL(check_fields(net_info, type, state), valid_on_mainnet); + BOOST_CHECK_EQUAL(state.GetRejectReason(), valid_on_mainnet ? "" : "bad-protx-netinfo-port"); + } + SelectParams(CBaseChainParams::TESTNET); } static const std::vector addr_vals_reg{ diff --git a/src/test/evo_snapshot_tests.cpp b/src/test/evo_snapshot_tests.cpp index 83b911c98811..5fe56b2972c0 100644 --- a/src/test/evo_snapshot_tests.cpp +++ b/src/test/evo_snapshot_tests.cpp @@ -80,7 +80,7 @@ CDeterministicMNCPtr MN(uint64_t internal_id, uint8_t hash_suffix, MnType type, if (type == MnType::Evo) { state->platformNodeID = H160(address_tag + 40); BOOST_REQUIRE_EQUAL(state->netInfo->AddEntry(NetInfoPurpose::PLATFORM_P2P, - strprintf("2.2.2.%d:26657", address_tag)), + strprintf("2.2.2.%d:26656", address_tag)), NetInfoStatus::Success); BOOST_REQUIRE_EQUAL(state->netInfo->AddEntry(NetInfoPurpose::PLATFORM_HTTPS, strprintf("evo%d.example.org:443", address_tag)), @@ -316,7 +316,7 @@ BOOST_AUTO_TEST_CASE(rotation_bitset_larger_than_quorum_roundtrips) BOOST_FIXTURE_TEST_CASE(populated_v3_golden_value, BasicTestingSetup) { BOOST_CHECK_EQUAL(GetEvoSnapshotHash(SyntheticSnapshot()).ToString(), - "5b5d496a66d93775a6a6d1badf2ea2d5bbb25b58031fe0dd2c3c670674c6908d"); + "8a2883207069e92589ffcce1f9867736881ecdf04ccfc89b492570740e7d3f31"); } BOOST_FIXTURE_TEST_CASE(canonical_mn_reader_rejects_order_and_counter, BasicTestingSetup) From 428293cae5373631ba4faebab5deebeff6e842c2 Mon Sep 17 00:00:00 2001 From: pasta Date: Tue, 3 Nov 2026 15:37:38 +0000 Subject: [PATCH 2/2] fix(evo): require IPv4 primary addresses until masternodes may run without IPv4 Extended addresses let a masternode list IPv6 and privacy-network addresses, but the rest of the system still expects masternodes to reach each other over IPv4: GetPrimary() is the address peers and quorum members dial, CActiveMasternodeManager only accepts an IPv4 local address, and the legacy platform{P2P,HTTP}Port fields assume Platform runs on the Core address. Nothing required a version 3 ProTx to keep any of that. Require the first Core P2P entry to be IPv4, and the first Platform entries to use that address, until a new DEPLOYMENT_IPV4_OPTIONAL activates. Its activation mechanism isn't defined yet, so for now it is a height: never on mainnet, always on testnet and devnets, which already have version 3 ProTxs without IPv4 primaries, and settable on regtest with -testactivationheight=ipv4_optional@ (default never, like mainnet). The rule depends on the block a ProTx is mined in, so it is checked in CheckProviderNetworkFields() through SpecialTxRules, not in ExtNetInfo::Validate(), which snapshot loading also runs on stored masternode states without a height. RPC transaction building evaluates it at the chain tip. The GUI and its input checks take it from the provider capability snapshot, so validating typed endpoints never waits for cs_main. Co-Authored-By: Claude Opus 5.5 --- doc/release-notes-8018.md | 15 ++- src/chainparams.cpp | 2 +- src/consensus/params.h | 11 +- src/deploymentinfo.cpp | 4 + src/evo/core_write.cpp | 10 +- src/evo/providertx.cpp | 32 +++++- src/evo/providertx.h | 6 +- src/evo/providertx_service.cpp | 48 +++++--- src/evo/providertx_service.h | 4 +- src/evo/specialtxman.cpp | 22 ++-- src/evo/specialtxman.h | 4 +- src/interfaces/node.h | 9 +- src/interfaces/providertx.h | 2 + src/kernel/chainparams.cpp | 7 ++ src/node/interfaces.cpp | 9 +- src/qt/masternodedialogs.cpp | 3 +- src/qt/masternodewizard.cpp | 21 ++-- src/qt/masternodewizard.h | 1 + src/qt/sharedmncreatedialog.cpp | 11 +- src/qt/test/masternodemaintenancetests.cpp | 34 +++++- src/qt/test/masternodewidgettests.cpp | 29 ++--- src/rpc/evo.cpp | 10 ++ src/test/evo_deterministicmns_tests.cpp | 15 +-- src/test/evo_netinfo_tests.cpp | 127 ++++++++++++++++++--- src/test/evo_snapshot_tests.cpp | 3 + src/test/interfaces_tests.cpp | 85 +++++++++++--- src/validation.cpp | 1 + test/functional/rpc_netinfo.py | 50 +++++++- 28 files changed, 462 insertions(+), 113 deletions(-) diff --git a/doc/release-notes-8018.md b/doc/release-notes-8018.md index 73d47091f8e7..ba245ac17045 100644 --- a/doc/release-notes-8018.md +++ b/doc/release-notes-8018.md @@ -1,7 +1,14 @@ Consensus changes ----------------- -- On mainnet, the first entry of each address list in a version 3 (extended addresses) ProRegTx or - ProUpServTx must use that list's standard port: 9999 for `coreP2PAddrs`, 26656 for `platformP2PAddrs` and - 443 for `platformHTTPSAddrs`, as legacy ProTx versions already require on mainnet. Additional entries - may use any otherwise valid port. Violations are rejected with `bad-protx-netinfo-port`. +- A version 3 (extended addresses) ProRegTx or ProUpServTx must list an IPv4 address as its first + `coreP2PAddrs` entry, and an EvoNode's first `platformP2PAddrs` and `platformHTTPSAddrs` entries must use + that same IPv4 address. Further entries may use other addresses and network types (IPv6, Tor, I2P, CJDNS, + and domains for Platform HTTPS). Violations are rejected with `bad-protx-netinfo-addr-type` or + `bad-protx-netinfo-addr`. This applies until masternodes are allowed to operate without IPv4, which is + not scheduled on mainnet. Testnet and devnets already allow it, so they are unaffected. Regtest follows + mainnet unless `-testactivationheight=ipv4_optional@` is set. +- On mainnet, the first entry of each address list must also use that list's standard port: 9999 for + `coreP2PAddrs`, 26656 for `platformP2PAddrs` and 443 for `platformHTTPSAddrs`, as legacy ProTx versions + already require there. Additional entries may use any otherwise valid port. Violations are rejected + with `bad-protx-netinfo-port`. diff --git a/src/chainparams.cpp b/src/chainparams.cpp index 8d5a1195363d..b6bd830ec539 100644 --- a/src/chainparams.cpp +++ b/src/chainparams.cpp @@ -338,7 +338,7 @@ void SetupChainParamsOptions(ArgsManager& argsman) argsman.AddArg("-llmqtestplatformparams=:", "Override the default LLMQ size for the LLMQ_TEST_PLATFORM quorum (default: 3:2, regtest-only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CHAINPARAMS); argsman.AddArg("-minimumdifficultyblocks=", "The number of blocks that can be mined with the minimum difficulty at the start of a chain (default: 0, devnet-only)", ArgsManager::ALLOW_ANY, OptionsCategory::CHAINPARAMS); argsman.AddArg("-powtargetspacing=", "Override the default PowTargetSpacing value in seconds (default: 2.5 minutes, devnet-only)", ArgsManager::ALLOW_ANY | ArgsManager::DISALLOW_NEGATION, OptionsCategory::CHAINPARAMS); - argsman.AddArg("-testactivationheight=name@height.", "Set the activation height of 'name' (bip147, bip34, dersig, cltv, csv, brr, dip0001, dip0008, dip0024, v19, v20, mn_rr). (regtest-only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CHAINPARAMS); + argsman.AddArg("-testactivationheight=name@height.", "Set the activation height of 'name' (bip147, bip34, dersig, cltv, csv, brr, dip0001, dip0008, dip0024, v19, v20, mn_rr, ipv4_optional). (regtest-only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CHAINPARAMS); argsman.AddArg("-vbparams=::(:min_activation_height(::(:::)))", "Use given start/end times and min_activation_height for specified version bits deployment (regtest-only). " "Specifying window, threshold/thresholdstart, thresholdmin, falloffcoeff and mnactivation is optional.", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CHAINPARAMS); diff --git a/src/consensus/params.h b/src/consensus/params.h index 7a6f724b8e48..59384abc67da 100644 --- a/src/consensus/params.h +++ b/src/consensus/params.h @@ -36,8 +36,9 @@ enum BuriedDeployment : int16_t { DEPLOYMENT_V20, DEPLOYMENT_MN_RR, DEPLOYMENT_WITHDRAWALS, + DEPLOYMENT_IPV4_OPTIONAL, // Masternodes may operate without IPv4 }; -constexpr bool ValidDeployment(BuriedDeployment dep) { return dep <= DEPLOYMENT_WITHDRAWALS; } +constexpr bool ValidDeployment(BuriedDeployment dep) { return dep <= DEPLOYMENT_IPV4_OPTIONAL; } enum DeploymentPos : uint16_t { DEPLOYMENT_TESTDUMMY, @@ -151,6 +152,12 @@ struct Params { int MN_RRHeight; /** Block height at which WITHDRAWALS (Deployment of quorum fix and higher limits for withdrawals) becomes active */ int WithdrawalsHeight; + /** Block height from which a masternode's primary addresses no longer need to be IPv4. + * std::numeric_limits::max() while no activation is scheduled. A placeholder: a version bits (EHF) + * deployment replacing it must stay always active on testnet and devnets. The mainnet primary port pins + * don't depend on it, so a first entry that can't use the standard port (such as I2P, which uses port 0) + * stays invalid there. */ + int IPv4OptionalHeight; /** Don't warn about unknown BIP 9 activations below this height. * This prevents us from warning about the CSV and DIP activations. */ int MinBIP9WarningHeight; @@ -232,6 +239,8 @@ struct Params { return MN_RRHeight; case DEPLOYMENT_WITHDRAWALS: return WithdrawalsHeight; + case DEPLOYMENT_IPV4_OPTIONAL: + return IPv4OptionalHeight; } // no default case, so the compiler can warn about missing cases return std::numeric_limits::max(); } diff --git a/src/deploymentinfo.cpp b/src/deploymentinfo.cpp index d2c8d9c35c9d..d5646f3840b8 100644 --- a/src/deploymentinfo.cpp +++ b/src/deploymentinfo.cpp @@ -57,6 +57,8 @@ std::string DeploymentName(Consensus::BuriedDeployment dep) return "mn_rr"; case Consensus::DEPLOYMENT_WITHDRAWALS: return "withdrawals"; + case Consensus::DEPLOYMENT_IPV4_OPTIONAL: + return "ipv4_optional"; } // no default case, so the compiler can warn about missing cases return ""; } @@ -87,6 +89,8 @@ std::optional GetBuriedDeployment(const std::string return Consensus::BuriedDeployment::DEPLOYMENT_V20; } else if (name == "mn_rr") { return Consensus::BuriedDeployment::DEPLOYMENT_MN_RR; + } else if (name == "ipv4_optional") { + return Consensus::BuriedDeployment::DEPLOYMENT_IPV4_OPTIONAL; } return std::nullopt; } diff --git a/src/evo/core_write.cpp b/src/evo/core_write.cpp index 2a645501acf2..a5cbe174aee8 100644 --- a/src/evo/core_write.cpp +++ b/src/evo/core_write.cpp @@ -60,13 +60,9 @@ template UniValue GetNetInfoWithLegacyFields(const CDeterministicMNState& obj, c template int32_t GetPlatformPort(const Obj& obj) { - // Currently, there is nothing that prevents PLATFORM_{HTTPS,P2P} to be registered to - // an addr *different* from the primary addr for CORE_P2P. This breaks the assumptions - // under which platform{HTTP,P2P}Port operate under (i.e. all three are hosted with the - // same addr). - // - // TODO: Introduce restrictions that enforce this assumption *until* we remove legacy - // fields for good. + // Until DEPLOYMENT_IPV4_OPTIONAL is active, consensus requires the first PLATFORM_{HTTPS,P2P} entries to + // use the primary addr for CORE_P2P, as platform{HTTP,P2P}Port assume (i.e. all three are hosted with the + // same addr). Once it is (always on testnet and devnets), the ports reported here may belong to another addr. // bool is_legacy{!(CHECK_NONFATAL(obj.netInfo)->CanStorePlatform())}; if constexpr (is_p2p) { diff --git a/src/evo/providertx.cpp b/src/evo/providertx.cpp index f20a981ca3ff..36385f051c1a 100644 --- a/src/evo/providertx.cpp +++ b/src/evo/providertx.cpp @@ -229,9 +229,35 @@ static bool CheckNetInfo(const NetInfoInterface& net_info, TxValidationState& st assert(false); } +//! The primary Core P2P address must be IPv4, and Platform's primary addresses must be on that same address +static bool CheckIPv4Primaries(const NetInfoInterface& net_info, TxValidationState& state) +{ + const CService core_primary{net_info.GetPrimary()}; + if (!core_primary.IsIPv4()) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-netinfo-addr-type", + strprintf("first %s entry must be IPv4", PurposeToString(NetInfoPurpose::CORE_P2P))); + } + for (const auto purpose : {NetInfoPurpose::PLATFORM_P2P, NetInfoPurpose::PLATFORM_HTTPS}) { + const auto entries{net_info.GetEntries(purpose)}; + if (entries.empty()) continue; + const auto service_opt{entries.front().GetAddrPort()}; + if (!service_opt || !service_opt->IsIPv4()) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-netinfo-addr-type", + strprintf("first %s entry must be IPv4", PurposeToString(purpose))); + } + if (CNetAddr{*service_opt} != CNetAddr{core_primary}) { + return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-netinfo-addr", + strprintf("first %s entry must use the first %s address", PurposeToString(purpose), + PurposeToString(NetInfoPurpose::CORE_P2P))); + } + } + return true; +} + bool CheckProviderNetworkFields(const std::shared_ptr& net_info, MnType type, uint16_t version, const uint160* platform_node_id, uint16_t platform_p2p_port, - uint16_t platform_http_port, bool allow_empty, TxValidationState& state) + uint16_t platform_http_port, bool allow_empty, bool ipv4_optional, + TxValidationState& state) { if (!net_info || net_info->CanStorePlatform() != (version >= ProTxVersion::ExtAddr)) { return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-protx-netinfo-version"); @@ -244,6 +270,10 @@ bool CheckProviderNetworkFields(const std::shared_ptr& net_inf if (!IsNetInfoTriviallyValid(net_info, type, state) || !CheckNetInfo(*net_info, state)) { return false; } + // Checked here rather than in Validate(), which also checks stored masternode states without a height + if (!ipv4_optional && !CheckIPv4Primaries(*net_info, state)) { + return false; + } } if (type != MnType::Evo) return true; diff --git a/src/evo/providertx.h b/src/evo/providertx.h index 943805567374..aa1c1f907a3a 100644 --- a/src/evo/providertx.h +++ b/src/evo/providertx.h @@ -105,10 +105,12 @@ template [[nodiscard]] UniValue PayoutListToJson(const MasternodePayoutShares& payouts); /** Validate all provider network fields using the same rules as special transaction validation. - * Pass nullptr for platform_node_id when validating endpoint input separately from the rest of a payload. */ + * Pass nullptr for platform_node_id when validating endpoint input separately from the rest of a payload. + * ipv4_optional is SpecialTxRules::ipv4_optional for the block the transaction would be included in. */ [[nodiscard]] bool CheckProviderNetworkFields(const std::shared_ptr& net_info, MnType type, uint16_t version, const uint160* platform_node_id, uint16_t platform_p2p_port, - uint16_t platform_http_port, bool allow_empty, TxValidationState& state); + uint16_t platform_http_port, bool allow_empty, bool ipv4_optional, + TxValidationState& state); /** The count, per-share bound and sum rules of IsShareListTriviallyValid, usable before any share address exists */ [[nodiscard]] bool AreShareAmountsValid(const std::vector& amounts, CAmount required_collateral, diff --git a/src/evo/providertx_service.cpp b/src/evo/providertx_service.cpp index 81aaa02eb9b6..3ae3420f248d 100644 --- a/src/evo/providertx_service.cpp +++ b/src/evo/providertx_service.cpp @@ -108,6 +108,14 @@ bool EvoSharesActive(node::NodeContext& node) return DeploymentActiveAfter(chainman.ActiveChain().Tip(), chainman, Consensus::DEPLOYMENT_EVO_SHARES); } +bool IPv4OptionalActive(node::NodeContext& node) +{ + AssertLockNotHeld(::cs_main); + auto& chainman{*Assert(node.chainman)}; + LOCK(::cs_main); + return DeploymentActiveAfter(chainman.ActiveChain().Tip(), chainman, Consensus::DEPLOYMENT_IPV4_OPTIONAL); +} + struct ChainSnapshot { uint16_t provider_tx_version{0}; CDeterministicMNCPtr dmn; @@ -252,13 +260,15 @@ std::optional ApplyNetInfo(ProTx& payload, const ProviderNetInf } template -std::optional ValidateNetworkFields(const ProTx& payload, bool allow_empty, bool check_platform_node_id) +std::optional ValidateNetworkFields(const ProTx& payload, bool allow_empty, + bool check_platform_node_id, bool ipv4_optional) { TxValidationState state; const uint160* platform_node_id{check_platform_node_id && payload.nType == MnType::Evo ? &payload.platformNodeID : nullptr}; if (!CheckProviderNetworkFields(payload.netInfo, payload.nType, payload.nVersion, platform_node_id, - payload.platformP2PPort, payload.platformHTTPPort, allow_empty, state)) { + payload.platformP2PPort, payload.platformHTTPPort, allow_empty, ipv4_optional, + state)) { return Error(ProviderTxErrorCode::CONSENSUS_REJECTED, state.ToString(), state.GetRejectReason()); } return std::nullopt; @@ -535,8 +545,8 @@ RegistrationResult BuildRegistration(node::NodeContext& node, Wallet& wallet, } if (auto error{ApplyPlatformNodeId(payload, request.platform_node_id)}) return *error; - if (auto error{ValidateNetworkFields(payload, /*allow_empty=*/true, - /*check_platform_node_id=*/true)}) { + if (auto error{ValidateNetworkFields(payload, /*allow_empty=*/true, /*check_platform_node_id=*/true, + IPv4OptionalActive(node))}) { return *error; } @@ -796,11 +806,20 @@ std::optional CheckRegistrarUpdateFunding(Wallet& wallet, const interfaces::ProviderTxCapabilities GetCapabilities(node::NodeContext& node, std::optional basic_override) { - const uint16_t version{CurrentProviderTxVersion(node, basic_override)}; - return interfaces::ProviderTxCapabilities{version, version >= ProTxVersion::ExtAddr}; + AssertLockNotHeld(::cs_main); + auto& chainman{*Assert(node.chainman)}; + LOCK(::cs_main); + const CBlockIndex* tip{chainman.ActiveChain().Tip()}; + const uint16_t version{DeploymentToProtxVersion(tip, chainman, basic_override)}; + return interfaces::ProviderTxCapabilities{ + .version = version, + .extended_addresses = version >= ProTxVersion::ExtAddr, + .ipv4_optional = DeploymentActiveAfter(tip, chainman, Consensus::DEPLOYMENT_IPV4_OPTIONAL), + }; } -std::optional ValidateNetInfo(const ProviderNetInfo& net_info, MnType type, uint16_t version, bool optional) +std::optional ValidateNetInfo(const ProviderNetInfo& net_info, MnType type, uint16_t version, + bool ipv4_optional, bool optional) { if (!IsValidMnType(type)) { return Error(ProviderTxErrorCode::INVALID_PARAMETER, "invalid masternode type"); @@ -814,13 +833,15 @@ std::optional ValidateNetInfo(const ProviderNetInfo& net_info, payload.nVersion = version; payload.netInfo = NetInfoInterface::MakeNetInfo(version); if (auto error{ApplyNetInfo(payload, net_info, type == MnType::Evo, optional)}) return error; - return ValidateNetworkFields(payload, optional, /*check_platform_node_id=*/false); + return ValidateNetworkFields(payload, optional, /*check_platform_node_id=*/false, ipv4_optional); } std::optional FindNetInfoInUse(node::NodeContext& node, const ProviderNetInfo& net_info, MnType type, - uint16_t version) + uint16_t version, bool ipv4_optional) { - if (ValidateNetInfo(net_info, type, version, /*optional=*/true) || !node.dmnman) return std::nullopt; + if (ValidateNetInfo(net_info, type, version, ipv4_optional, /*optional=*/true) || !node.dmnman) { + return std::nullopt; + } CProRegTx payload; payload.nType = type; payload.nVersion = version; @@ -921,8 +942,8 @@ ProviderTxResult UpdateService(node::NodeContext& node, Wa return *error; } if (auto error{ApplyPlatformNodeId(payload, request.platform_node_id)}) return *error; - if (auto error{ValidateNetworkFields(payload, /*allow_empty=*/false, - /*check_platform_node_id=*/true)}) { + if (auto error{ValidateNetworkFields(payload, /*allow_empty=*/false, /*check_platform_node_id=*/true, + IPv4OptionalActive(node))}) { return *error; } payload.scriptOperatorPayout = request.operator_payout ? GetScriptForDestination(*request.operator_payout) @@ -1128,7 +1149,8 @@ ProviderTxResult PrepareSharedRegistration(node::Nod return *error; } if (auto error{ApplyPlatformNodeId(payload, request.platform_node_id)}) return *error; - if (auto error{ValidateNetworkFields(payload, /*allow_empty=*/true, /*check_platform_node_id=*/true)}) { + if (auto error{ValidateNetworkFields(payload, /*allow_empty=*/true, /*check_platform_node_id=*/true, + IPv4OptionalActive(node))}) { return *error; } diff --git a/src/evo/providertx_service.h b/src/evo/providertx_service.h index 49fef91ea983..1a7534b48d9c 100644 --- a/src/evo/providertx_service.h +++ b/src/evo/providertx_service.h @@ -24,10 +24,10 @@ interfaces::ProviderTxCapabilities GetCapabilities(node::NodeContext& node, std::optional basic_override = std::nullopt); std::optional ValidateNetInfo(const interfaces::ProviderNetInfo& net_info, MnType type, - uint16_t version, bool optional); + uint16_t version, bool ipv4_optional, bool optional); std::optional FindNetInfoInUse(node::NodeContext& node, const interfaces::ProviderNetInfo& net_info, - MnType type, uint16_t version); + MnType type, uint16_t version, bool ipv4_optional); interfaces::ProviderTxResult Register( node::NodeContext& node, interfaces::Wallet& wallet, const interfaces::ProviderRegistrationRequest& request); diff --git a/src/evo/specialtxman.cpp b/src/evo/specialtxman.cpp index 102df6408d2d..4a3dc64626c9 100644 --- a/src/evo/specialtxman.cpp +++ b/src/evo/specialtxman.cpp @@ -266,7 +266,7 @@ bool CSpecialTxProcessor::CheckSpecialTxInner(const CChain* chain, const CTransa case TRANSACTION_PROVIDER_REGISTER: return CheckProRegTx(tx, pindexPrev, m_dmnman, view, m_consensus_params, rules, state, check_sigs); case TRANSACTION_PROVIDER_UPDATE_SERVICE: - return CheckProUpServTx(tx, pindexPrev, m_dmnman, m_consensus_params, rules.v24, state, check_sigs); + return CheckProUpServTx(tx, pindexPrev, m_dmnman, m_consensus_params, rules, state, check_sigs); case TRANSACTION_PROVIDER_UPDATE_REGISTRAR: return CheckProUpRegTx(tx, pindexPrev, m_dmnman, view, m_consensus_params, rules, state, check_sigs); case TRANSACTION_PROVIDER_UPDATE_REVOKE: @@ -1284,7 +1284,8 @@ bool CheckProRegTx(const CTransaction& tx, gsl::not_null pin // It's allowed to set addr to 0, which will put the MN into PoSe-banned state and require a ProUpServTx to be // issues later. If any of both is set, it must be valid however if (!CheckProviderNetworkFields(opt_ptx->netInfo, opt_ptx->nType, opt_ptx->nVersion, &opt_ptx->platformNodeID, - opt_ptx->platformP2PPort, opt_ptx->platformHTTPPort, /*allow_empty=*/true, state)) { + opt_ptx->platformP2PPort, opt_ptx->platformHTTPPort, /*allow_empty=*/true, + rules.ipv4_optional, state)) { return false; } @@ -1433,17 +1434,18 @@ bool CheckProRegTx(const CTransaction& tx, gsl::not_null pin } bool CheckProUpServTx(const CTransaction& tx, gsl::not_null pindexPrev, - CDeterministicMNManager& dmnman, const Consensus::Params& consensus_params, bool is_v24_active, + CDeterministicMNManager& dmnman, const Consensus::Params& consensus_params, SpecialTxRules rules, TxValidationState& state, bool check_sigs) { - const auto opt_ptx = GetValidatedPayload(tx, pindexPrev, consensus_params, is_v24_active, state); + const auto opt_ptx = GetValidatedPayload(tx, pindexPrev, consensus_params, rules.v24, state); if (!opt_ptx) { // pass the state returned by the function above return false; } if (!CheckProviderNetworkFields(opt_ptx->netInfo, opt_ptx->nType, opt_ptx->nVersion, &opt_ptx->platformNodeID, - opt_ptx->platformP2PPort, opt_ptx->platformHTTPPort, /*allow_empty=*/false, state)) { + opt_ptx->platformP2PPort, opt_ptx->platformHTTPPort, /*allow_empty=*/false, + rules.ipv4_optional, state)) { return false; } @@ -1461,21 +1463,21 @@ bool CheckProUpServTx(const CTransaction& tx, gsl::not_null return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-protx-type-mismatch"); } - if (!IsVersionChangeValid(dmn->pdmnState->nVersion, opt_ptx->nVersion, is_v24_active, state)) { + if (!IsVersionChangeValid(dmn->pdmnState->nVersion, opt_ptx->nVersion, rules.v24, state)) { // pass the state returned by the function above return false; } // From ExtAddr on the Platform ports live in netInfo and are never derived from the scalar fields, // so an EvoNode service update has to spell them out - if (is_v24_active && opt_ptx->nType == MnType::Evo && opt_ptx->nVersion < ProTxVersion::ExtAddr) { + if (rules.v24 && opt_ptx->nType == MnType::Evo && opt_ptx->nVersion < ProTxVersion::ExtAddr) { return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-protx-version-disallowed"); } - if (is_v24_active) { + if (rules.v24) { CDeterministicMNState new_state{*dmn->pdmnState}; BlockValidationState migration_state; - if (!ApplyProUpServTx(new_state, *opt_ptx, is_v24_active, migration_state)) { + if (!ApplyProUpServTx(new_state, *opt_ptx, rules.v24, migration_state)) { return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, migration_state.GetRejectReason()); } } @@ -1484,7 +1486,7 @@ bool CheckProUpServTx(const CTransaction& tx, gsl::not_null // re-encodes its stored key, moving it to the basic-scheme unique-property slot. If another // masternode already holds that key under either encoding, the re-key in UpdateMN() would throw // out of block assembly, so reject the migration cleanly here. - if (is_v24_active && IsSchemeMigration(dmn->pdmnState->nVersion, opt_ptx->nVersion) && + if (rules.v24 && IsSchemeMigration(dmn->pdmnState->nVersion, opt_ptx->nVersion) && mnList.HasOperatorKeyUnderAnyScheme(dmn->pdmnState->pubKeyOperator.Get(), /*self=*/opt_ptx->proTxHash)) { return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-dup-key"); } diff --git a/src/evo/specialtxman.h b/src/evo/specialtxman.h index 05c66ce753df..10ec910e30e3 100644 --- a/src/evo/specialtxman.h +++ b/src/evo/specialtxman.h @@ -53,6 +53,8 @@ struct SpecialTxRules { bool v24{false}; //! EvoNodes may register with shared collateral and have multiple owner payouts bool evo_shares{false}; + //! A masternode's primary addresses need not be IPv4 + bool ipv4_optional{false}; }; class CSpecialTxProcessor @@ -186,7 +188,7 @@ bool CheckProRegTx(const CTransaction& tx, gsl::not_null pin const CCoinsViewCache& view, const Consensus::Params& consensus_params, SpecialTxRules rules, TxValidationState& state, bool check_sigs); bool CheckProUpServTx(const CTransaction& tx, gsl::not_null pindexPrev, - CDeterministicMNManager& dmnman, const Consensus::Params& consensus_params, bool is_v24_active, + CDeterministicMNManager& dmnman, const Consensus::Params& consensus_params, SpecialTxRules rules, TxValidationState& state, bool check_sigs); bool CheckProUpRegTx(const CTransaction& tx, gsl::not_null pindexPrev, CDeterministicMNManager& dmnman, const CCoinsViewCache& view, const Consensus::Params& consensus_params, SpecialTxRules rules, diff --git a/src/interfaces/node.h b/src/interfaces/node.h index b20890c43470..e68c2ae8e8f3 100644 --- a/src/interfaces/node.h +++ b/src/interfaces/node.h @@ -151,15 +151,16 @@ class EVO virtual ~EVO() {} virtual std::pair getListAtChainTip() = 0; virtual ProviderTxCapabilities getProviderTxCapabilities() = 0; - //! Validate provider endpoints against a capability snapshot without changing node or wallet state. - //! Transaction creation validates them again against the then-active rules. + //! Validate provider endpoints against a capability snapshot without changing node or wallet state or + //! taking cs_main. Transaction creation validates them again against the then-active rules. virtual std::optional validateProviderNetInfo(const ProviderNetInfo& net_info, MnType type, - uint16_t version, bool optional) = 0; + uint16_t version, bool ipv4_optional, + bool optional) = 0; //! The first endpoint a masternode at the chain tip already uses, which a //! registration with a new collateral cannot reuse. Nothing is reported for //! endpoints validateProviderNetInfo refuses. virtual std::optional findProviderNetInfoInUse(const ProviderNetInfo& net_info, MnType type, - uint16_t version) = 0; + uint16_t version, bool ipv4_optional) = 0; //! Successful registration with an existing collateral leaves that coin locked, //! including when the signed transaction is returned without submission. virtual ProviderTxResult registerMasternode(Wallet& wallet, diff --git a/src/interfaces/providertx.h b/src/interfaces/providertx.h index d23c71dee4c2..fe13093c72c1 100644 --- a/src/interfaces/providertx.h +++ b/src/interfaces/providertx.h @@ -66,6 +66,8 @@ struct PreparedProviderRegistration { struct ProviderTxCapabilities { uint16_t version{0}; bool extended_addresses{false}; + //! Masternodes may operate without IPv4 (DEPLOYMENT_IPV4_OPTIONAL) + bool ipv4_optional{false}; }; using ProviderPlatformEndpoints = std::variant>; diff --git a/src/kernel/chainparams.cpp b/src/kernel/chainparams.cpp index 58906cd42b94..738140a13184 100644 --- a/src/kernel/chainparams.cpp +++ b/src/kernel/chainparams.cpp @@ -206,6 +206,7 @@ class CMainParams : public CChainParams { consensus.V20Height = 1987776; // 000000000000001bf41cff06b76780050682ca29e61a91c391893d4745579777 consensus.MN_RRHeight = 2128896; // 0000000000000009a9696da93d3807eb14eb00a4ff449206d689156a21b27f26 consensus.WithdrawalsHeight = 2201472; // 00000000000000210518749e17c00b035a2a4982c906236c28c41ea2231bf7ef + consensus.IPv4OptionalHeight = std::numeric_limits::max(); // Not scheduled consensus.MinBIP9WarningHeight = 2201472 + 2016; // withdrawals activation height + miner confirmation window consensus.powLimit = uint256S("00000fffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); // ~uint256(0) >> 20 consensus.nPowTargetTimespan = 24 * 60 * 60; // Dash: 1 day @@ -417,6 +418,7 @@ class CTestNetParams : public CChainParams { consensus.V20Height = 905100; // 0000020c5e0f86f385cbf8e90210de9a9fd63633f01433bf47a6b3227a2851fd consensus.MN_RRHeight = 1066900; // 000000d05d445958a9a4ad6bdc0f4bfb25af124b2326060703373ff2d3b397e9 consensus.WithdrawalsHeight = 1148500; // 000000212a6fec2ee2af040c6d7a176360b154cbaa998888170cfd9ae7dd632d + consensus.IPv4OptionalHeight = 0; // Version 3 ProTxs without IPv4 primary addresses are already mined consensus.MinBIP9WarningHeight = 1148500 + 2016; // withdrawals activation height + miner confirmation window consensus.powLimit = uint256S("00000fffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); // ~uint256(0) >> 20 consensus.nPowTargetTimespan = 24 * 60 * 60; // Dash: 1 day @@ -601,6 +603,7 @@ class CDevNetParams : public CChainParams { consensus.V20Height = 2; // V20 activated immediately on devnet consensus.MN_RRHeight = 2; // MN_RR activated immediately on devnet consensus.WithdrawalsHeight = 2; // withdrawals activated immediately on devnet + consensus.IPv4OptionalHeight = 0; // same as testnet consensus.MinBIP9WarningHeight = 2 + 2016; // withdrawals activation height + miner confirmation window consensus.powLimit = uint256S("7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); // ~uint256(0) >> 1 consensus.nPowTargetTimespan = 24 * 60 * 60; // Dash: 1 day @@ -896,6 +899,7 @@ class CRegTestParams : public CChainParams { consensus.V20Height = consensus.DIP0003Height; // Active not earlier than dip0003. Functional tests (DashTestFramework) uses height 100 (same as coinbase maturity) consensus.MN_RRHeight = consensus.V20Height; // MN_RR does not really have effect before v20 activation consensus.WithdrawalsHeight = 600; + consensus.IPv4OptionalHeight = std::numeric_limits::max(); // see -testactivationheight consensus.MinBIP9WarningHeight = 0; consensus.powLimit = uint256S("7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); // ~uint256(0) >> 1 consensus.nPowTargetTimespan = 24 * 60 * 60; // Dash: 1 day @@ -1092,6 +1096,9 @@ class CRegTestParams : public CChainParams { case Consensus::BuriedDeployment::DEPLOYMENT_WITHDRAWALS: consensus.WithdrawalsHeight = int{height}; break; + case Consensus::BuriedDeployment::DEPLOYMENT_IPV4_OPTIONAL: + consensus.IPv4OptionalHeight = int{height}; + break; } // no default case, so the compiler can warn about missing cases } diff --git a/src/node/interfaces.cpp b/src/node/interfaces.cpp index 8a5c03987b49..d4d7fa25caa6 100644 --- a/src/node/interfaces.cpp +++ b/src/node/interfaces.cpp @@ -276,13 +276,14 @@ class EVOImpl : public EVO } ProviderTxCapabilities getProviderTxCapabilities() override { return evo::provider::GetCapabilities(context()); } std::optional validateProviderNetInfo(const ProviderNetInfo& net_info, MnType type, - uint16_t version, bool optional) override + uint16_t version, bool ipv4_optional, bool optional) override { - return evo::provider::ValidateNetInfo(net_info, type, version, optional); + return evo::provider::ValidateNetInfo(net_info, type, version, ipv4_optional, optional); } - std::optional findProviderNetInfoInUse(const ProviderNetInfo& net_info, MnType type, uint16_t version) override + std::optional findProviderNetInfoInUse(const ProviderNetInfo& net_info, MnType type, uint16_t version, + bool ipv4_optional) override { - return evo::provider::FindNetInfoInUse(context(), net_info, type, version); + return evo::provider::FindNetInfoInUse(context(), net_info, type, version, ipv4_optional); } ProviderTxResult registerMasternode(Wallet& wallet, const ProviderRegistrationRequest& request) override { diff --git a/src/qt/masternodedialogs.cpp b/src/qt/masternodedialogs.cpp index 5d7146651956..d2432d64a1e2 100644 --- a/src/qt/masternodedialogs.cpp +++ b/src/qt/masternodedialogs.cpp @@ -456,7 +456,8 @@ std::optional UpdateServiceDialog::buildNetInfo(QSt } } if (const auto validation_error{ - m_node.evo().validateProviderNetInfo(net_info, m_type, m_capabilities.version, /*optional=*/false)}) { + m_node.evo().validateProviderNetInfo(net_info, m_type, m_capabilities.version, + m_capabilities.ipv4_optional, /*optional=*/false)}) { error = MasternodeOperationRunner::errorText(*validation_error); return std::nullopt; } diff --git a/src/qt/masternodewizard.cpp b/src/qt/masternodewizard.cpp index 8bf184d891d2..2278492d37fd 100644 --- a/src/qt/masternodewizard.cpp +++ b/src/qt/masternodewizard.cpp @@ -492,7 +492,7 @@ QWidget* RegisterMasternodeWizard::createPlatformPage() card.body_layout->addWidget(m_platform_p2p); card.body_layout->addWidget(makeHint(tr("Platform HTTPS API"), m_platform_addr_box)); m_platform_https = new QLineEdit(card.body); - m_platform_https->setPlaceholderText(QString("platform.example.org:443")); + m_platform_https->setPlaceholderText(QString("1.2.3.4:443")); card.body_layout->addWidget(m_platform_https); layout->addWidget(m_platform_addr_box); } @@ -849,6 +849,7 @@ void RegisterMasternodeWizard::enterPage(Page page) const auto capabilities{m_node.evo().getProviderTxCapabilities()}; const bool v3{capabilities.extended_addresses}; m_platform_provider_version = capabilities.version; + m_platform_ipv4_optional = capabilities.ipv4_optional; m_platform_extended_addresses = v3; m_platform_addr_box->setVisible(v3); m_platform_port_box->setVisible(!v3); @@ -926,9 +927,9 @@ bool RegisterMasternodeWizard::validatePage(Page page, QString& err) } else if (!list.isEmpty()) { interfaces::ProviderNetInfo core_only; for (const QString& entry : list) core_only.core_p2p.push_back(entry.toStdString()); - const uint16_t version{m_node.evo().getProviderTxCapabilities().version}; + const auto capabilities{m_node.evo().getProviderTxCapabilities()}; if (const auto error{m_node.evo().validateProviderNetInfo( - core_only, MnType::Regular, version, /*optional=*/true)}) { + core_only, MnType::Regular, capabilities.version, capabilities.ipv4_optional, /*optional=*/true)}) { err = QString::fromStdString(error->message.translated); } } @@ -1047,11 +1048,15 @@ interfaces::ProviderNetInfo RegisterMasternodeWizard::providerNetInfo() const bool RegisterMasternodeWizard::validateProviderNetInfo(bool optional, QString& err) const { - const uint16_t version{isEvo() && m_platform_provider_version != 0 ? - m_platform_provider_version : - m_node.evo().getProviderTxCapabilities().version}; - if (const auto error{m_node.evo().validateProviderNetInfo( - providerNetInfo(), isEvo() ? MnType::Evo : MnType::Regular, version, optional)}) { + // The Platform page pins the rules it was entered with, so typing there doesn't wait for cs_main + const interfaces::ProviderTxCapabilities pinned{.version = m_platform_provider_version, + .extended_addresses = m_platform_extended_addresses, + .ipv4_optional = m_platform_ipv4_optional}; + const auto capabilities{isEvo() && m_platform_provider_version != 0 ? pinned + : m_node.evo().getProviderTxCapabilities()}; + const MnType type{isEvo() ? MnType::Evo : MnType::Regular}; + if (const auto error{m_node.evo().validateProviderNetInfo(providerNetInfo(), type, capabilities.version, + capabilities.ipv4_optional, optional)}) { err = QString::fromStdString(error->message.translated); return false; } diff --git a/src/qt/masternodewizard.h b/src/qt/masternodewizard.h index eec0dbe16af2..e2187adbe100 100644 --- a/src/qt/masternodewizard.h +++ b/src/qt/masternodewizard.h @@ -160,6 +160,7 @@ private Q_SLOTS: bool m_registered{false}; bool m_platform_extended_addresses{false}; uint16_t m_platform_provider_version{0}; + bool m_platform_ipv4_optional{false}; std::optional m_validation_page; QStackedWidget* m_pages; diff --git a/src/qt/sharedmncreatedialog.cpp b/src/qt/sharedmncreatedialog.cpp index dee635db4355..cbd4c68c5f47 100644 --- a/src/qt/sharedmncreatedialog.cpp +++ b/src/qt/sharedmncreatedialog.cpp @@ -192,8 +192,9 @@ QString ServiceInUseProblem(interfaces::Node& node, const QString& services) for (const QString& entry : MasternodeWidgetUtil::tokenizeEndpointList(services)) { net_info.core_p2p.push_back(entry.toStdString()); } - const uint16_t version{node.evo().getProviderTxCapabilities().version}; - const auto in_use{node.evo().findProviderNetInfoInUse(net_info, MnType::Regular, version)}; + const auto capabilities{node.evo().getProviderTxCapabilities()}; + const auto in_use{node.evo().findProviderNetInfoInUse(net_info, MnType::Regular, capabilities.version, + capabilities.ipv4_optional)}; if (!in_use) return {}; return SharedMnCreateDialog::tr("%1 is already the service address of a registered masternode. Every service " "address must be unique on the network.") @@ -1378,9 +1379,9 @@ bool SharedMnCreateDialog::validatePage(Page page, QString& err) for (const QString& entry : MasternodeWidgetUtil::tokenizeEndpointList(m_service_edit->text())) { net_info.core_p2p.push_back(entry.toStdString()); } - const uint16_t version{m_node.evo().getProviderTxCapabilities().version}; - if (const auto error{ - m_node.evo().validateProviderNetInfo(net_info, MnType::Regular, version, /*optional=*/true)}) { + const auto capabilities{m_node.evo().getProviderTxCapabilities()}; + if (const auto error{m_node.evo().validateProviderNetInfo(net_info, MnType::Regular, capabilities.version, + capabilities.ipv4_optional, /*optional=*/true)}) { err = QString::fromStdString(error->message.translated); } else { err = ServiceInUseProblem(m_node, m_service_edit->text()); diff --git a/src/qt/test/masternodemaintenancetests.cpp b/src/qt/test/masternodemaintenancetests.cpp index ff74fed68119..1e6c880de079 100644 --- a/src/qt/test/masternodemaintenancetests.cpp +++ b/src/qt/test/masternodemaintenancetests.cpp @@ -55,9 +55,12 @@ #include #include +#include +#include #include #include #include +#include #include using wallet::WalletContext; @@ -223,7 +226,7 @@ class TestMnEntry final : public interfaces::MnEntry std::string m_operator_display; std::string m_core_endpoint; std::string m_platform_p2p{"127.0.0.1:26656"}; - std::string m_platform_https{"api.example.com:443"}; + std::string m_platform_https{"127.0.0.1:443"}; std::string m_platform_node_id{"1111111111111111111111111111111111111111"}; }; @@ -431,7 +434,7 @@ void MasternodeMaintenanceTests::serviceRequestConstruction() QCOMPARE(std::get>(request->net_info.platform_p2p), std::vector{"127.0.0.1:26656"}); QCOMPARE(std::get>(request->net_info.platform_https), - std::vector{"api.example.com:443"}); + std::vector{"127.0.0.1:443"}); QVERIFY(request->platform_node_id.has_value()); } else { QCOMPARE(std::get(request->net_info.platform_p2p), uint16_t{26656}); @@ -457,6 +460,33 @@ void MasternodeMaintenanceTests::serviceRequestConstruction() check_request(MnType::Evo, {ProTxVersion::BasicBLS, false}); check_request(MnType::Evo, {ProTxVersion::ExtAddr, true}); + // The dialog validates against its capability snapshot, without waiting for cs_main while the user types + auto other_host{std::make_shared(MnType::Evo)}; + other_host->setPlatformEndpoints("127.0.0.2:26656", "127.0.0.2:443"); + const MasternodeEntry other_host_entry{other_host, "collateral", 50}; + for (const bool ipv4_optional : {false, true}) { + UpdateServiceDialog dialog(m_node, /*wallet_model=*/nullptr, other_host_entry, + interfaces::ProviderTxCapabilities{ProTxVersion::ExtAddr, true, ipv4_optional}, + /*parent=*/nullptr); + QString error; + QCOMPARE(dialog.buildRequest(other_host->operatorSecret(), error).has_value(), ipv4_optional); + QVERIFY2(ipv4_optional || error.contains("bad-protx-netinfo-addr"), qPrintable(error)); + + std::promise locked; + std::promise release; + bool lock_timed_out{false}; + std::thread holder{[&] { + LOCK(::cs_main); + locked.set_value(); + lock_timed_out = release.get_future().wait_for(std::chrono::seconds{10}) == std::future_status::timeout; + }}; + locked.get_future().wait(); + dialog.m_platform_p2p_edit->setText("127.0.0.1:26656"); + release.set_value(); + holder.join(); + QVERIFY(!lock_timed_out); + } + auto malformed_source{std::make_shared(MnType::Evo)}; malformed_source->setPlatformEndpoints("missing-port", "api.example.com:not-a-port"); MasternodeEntry malformed_entry{malformed_source, "collateral", 50}; diff --git a/src/qt/test/masternodewidgettests.cpp b/src/qt/test/masternodewidgettests.cpp index 95f90b97ba36..8532984d4d3f 100644 --- a/src/qt/test/masternodewidgettests.cpp +++ b/src/qt/test/masternodewidgettests.cpp @@ -111,11 +111,12 @@ class BlockingEVO final : public interfaces::EVO std::pair getListAtChainTip() override { return {}; } interfaces::ProviderTxCapabilities getProviderTxCapabilities() override { return {}; } std::optional validateProviderNetInfo(const interfaces::ProviderNetInfo&, MnType, - uint16_t, bool) override + uint16_t, bool, bool) override { return std::nullopt; } - std::optional findProviderNetInfoInUse(const interfaces::ProviderNetInfo&, MnType, uint16_t) override + std::optional findProviderNetInfoInUse(const interfaces::ProviderNetInfo&, MnType, uint16_t, + bool) override { return std::nullopt; } @@ -315,11 +316,10 @@ void MasternodeWidgetTests::providerNetInfoValidation_data() const int evo{static_cast(MnType::Evo)}; const int basic{ProTxVersion::BasicBLS}; const int extended{ProTxVersion::ExtAddr}; - const QString core1{QString("1.1.1.1:%1").arg(Params().GetDefaultPort())}; - const QString core2{QString("1.1.1.2:%1").arg(Params().GetDefaultPort())}; - const int wrong_network_port{Params().GetDefaultPort() == MainParams().GetDefaultPort() - ? MainParams().GetDefaultPort() + 1 - : MainParams().GetDefaultPort()}; + // providerNetInfoValidation() runs on mainnet + const QString core1{QString("1.1.1.1:%1").arg(MainParams().GetDefaultPort())}; + const QString core2{QString("1.1.1.2:%1").arg(MainParams().GetDefaultPort())}; + const int wrong_network_port{MainParams().GetDefaultPort() + 1}; const auto add_row = [](const char* name, int type, int version, bool optional, QStringList core, QStringList p2p, QStringList https, int p2p_port, int https_port, bool valid, QString error = {}) { @@ -348,18 +348,18 @@ void MasternodeWidgetTests::providerNetInfoValidation_data() add_row("evo-basic-ports", evo, basic, true, {core1}, {}, {}, 26656, 443, true); add_row("evo-basic-zero-port", evo, basic, true, {core1}, {}, {}, 0, 443, false, "valid port"); add_row("evo-extended-all-empty", evo, extended, true, {}, {}, {}, -1, -1, true); - add_row("evo-extended-complete", evo, extended, true, {core1}, {"1.1.1.2:26656"}, {"example.com:443"}, -1, -1, true); + add_row("evo-extended-complete", evo, extended, true, {core1}, {"1.1.1.1:26656"}, {"1.1.1.1:443"}, -1, -1, true); add_row("evo-extended-platform-only", evo, extended, true, {}, {"1.1.1.2:26656"}, {"example.com:443"}, -1, -1, false, "bad-protx-netinfo-empty"); - add_row("evo-extended-https-domain", evo, extended, true, {core1}, {"1.1.1.2:26656"}, {"rpc.example.org:443"}, -1, - -1, true); + add_row("evo-extended-https-domain", evo, extended, true, {core1}, {"1.1.1.1:26656"}, + {"1.1.1.1:443", "rpc.example.org:443"}, -1, -1, true); add_row("evo-extended-p2p-domain", evo, extended, true, {core1}, {"p2p.example.org:26656"}, {"rpc.example.org:443"}, -1, -1, false, "invalid input"); add_row("evo-extended-core-only", evo, extended, true, {core1}, {}, {}, -1, -1, false, "cannot be empty if other fields populated"); - add_row("evo-extended-one-platform-family", evo, extended, true, {core1}, {"1.1.1.2:26656"}, {}, -1, -1, false, + add_row("evo-extended-one-platform-family", evo, extended, true, {core1}, {"1.1.1.1:26656"}, {}, -1, -1, false, "cannot be empty if other fields populated"); - add_row("evo-extended-cross-purpose-duplicate", evo, extended, true, {core1}, {"1.1.1.2:26656"}, {"1.1.1.2:26656"}, + add_row("evo-extended-cross-purpose-duplicate", evo, extended, true, {core1}, {"1.1.1.1:26656"}, {"1.1.1.1:26656"}, -1, -1, false, "duplicate"); add_row("invalid-version", regular, 0, true, {}, {}, {}, -1, -1, false, "invalid provider transaction version"); add_row("invalid-type", static_cast(MnType::Invalid), basic, true, {}, {}, {}, -1, -1, false, @@ -368,6 +368,8 @@ void MasternodeWidgetTests::providerNetInfoValidation_data() void MasternodeWidgetTests::providerNetInfoValidation() { + BasicTestingSetup test{CBaseChainParams::MAIN}; + m_node.setContext(&test.m_node); QFETCH(int, mn_type); QFETCH(int, version); QFETCH(bool, optional); @@ -399,7 +401,8 @@ void MasternodeWidgetTests::providerNetInfoValidation() } const auto error{m_node.evo().validateProviderNetInfo(net_info, static_cast(mn_type), - static_cast(version), optional)}; + static_cast(version), /*ipv4_optional=*/false, + optional)}; QCOMPARE(!error.has_value(), valid); if (!valid) { QVERIFY(error.has_value()); diff --git a/src/rpc/evo.cpp b/src/rpc/evo.cpp index 012b286b5fb9..a8222b25a61f 100644 --- a/src/rpc/evo.cpp +++ b/src/rpc/evo.cpp @@ -116,6 +116,7 @@ static RPCArg GetRpcArg(const std::string& strParamName) "Array of addresses in the form \"ADDR:PORT\". Must be unique on the network.\n" "A legacy ProTx can only store a single entry; storing multiple entries\n" "requires upgrading to a version 3 ProTx.\n" + "The first entry must be IPv4 where masternodes require IPv4 (mainnet and, by default, regtest).\n" "On mainnet, the first entry must use port 9999.\n" "Can be set to an empty string, which will require a ProUpServTx afterwards.", { @@ -128,6 +129,7 @@ static RPCArg GetRpcArg(const std::string& strParamName) "Array of addresses in the form \"ADDR:PORT\". Must be unique on the network.\n" "A legacy ProTx can only store a single entry; storing multiple entries\n" "requires upgrading to a version 3 ProTx.\n" + "The first entry must be IPv4 where masternodes require IPv4 (mainnet and, by default, regtest).\n" "On mainnet, the first entry must use port 9999.", { {"address", RPCArg::Type::STR, RPCArg::Optional::NO, ""}, @@ -248,6 +250,8 @@ static RPCArg GetRpcArg(const std::string& strParamName) "Array of addresses in the form \"ADDR:PORT\" used by Platform for peer-to-peer connection.\n" "For a legacy ProTx, pass a bare port number instead (e.g. 26656);\n" "the \"ADDR:PORT\" / address-array form requires upgrading to a version 3 ProTx.\n" + "Where masternodes require IPv4 (mainnet and, by default, regtest), the first entry must use the\n" + "first coreP2PAddrs address.\n" "On mainnet, the first entry must use port 26656.\n" "Must be unique on the network. Can be set to an empty string, which will require a ProUpServTx afterwards.", { @@ -260,6 +264,8 @@ static RPCArg GetRpcArg(const std::string& strParamName) "Array of addresses in the form \"ADDR:PORT\" used by Platform for peer-to-peer connection.\n" "For a legacy ProTx, pass a bare port number instead (e.g. 26656);\n" "the \"ADDR:PORT\" / address-array form requires upgrading to a version 3 ProTx.\n" + "Where masternodes require IPv4 (mainnet and, by default, regtest), the first entry must use the\n" + "first coreP2PAddrs address.\n" "On mainnet, the first entry must use port 26656.\n" "Must be unique on the network.", { @@ -272,6 +278,8 @@ static RPCArg GetRpcArg(const std::string& strParamName) "Array of addresses in the form \"ADDR:PORT\" used by Platform for their HTTPS API.\n" "For a legacy ProTx, pass a bare port number instead (e.g. 443);\n" "the \"ADDR:PORT\" / address-array form requires upgrading to a version 3 ProTx.\n" + "Where masternodes require IPv4 (mainnet and, by default, regtest), the first entry must use the\n" + "first coreP2PAddrs address.\n" "On mainnet, the first entry must use port 443.\n" "Must be unique on the network. Can be set to an empty string, which will require a ProUpServTx afterwards.", { @@ -284,6 +292,8 @@ static RPCArg GetRpcArg(const std::string& strParamName) "Array of addresses in the form \"ADDR:PORT\" used by Platform for their HTTPS API.\n" "For a legacy ProTx, pass a bare port number instead (e.g. 443);\n" "the \"ADDR:PORT\" / address-array form requires upgrading to a version 3 ProTx.\n" + "Where masternodes require IPv4 (mainnet and, by default, regtest), the first entry must use the\n" + "first coreP2PAddrs address.\n" "On mainnet, the first entry must use port 443.\n" "Must be unique on the network.", { diff --git a/src/test/evo_deterministicmns_tests.cpp b/src/test/evo_deterministicmns_tests.cpp index 9cc1f4bf6dac..df0146e63a87 100644 --- a/src/test/evo_deterministicmns_tests.cpp +++ b/src/test/evo_deterministicmns_tests.cpp @@ -1197,7 +1197,7 @@ void FuncProUpServInvalidNType(TestChainSetup& setup) { LOCK(cs_main); BOOST_CHECK(!CheckProUpServTx(CTransaction(tx), tip_index(), dmnman, chainman.GetConsensus(), - IsV24Active(chainman), check_state, /*check_sigs=*/true)); + TipRules(chainman), check_state, /*check_sigs=*/true)); } BOOST_CHECK_EQUAL(check_state.GetRejectReason(), "bad-protx-type"); @@ -1223,7 +1223,7 @@ void FuncProUpServInvalidNType(TestChainSetup& setup) { LOCK(cs_main); BOOST_CHECK(!CheckProUpServTx(CTransaction(tx), tip_index(), dmnman, chainman.GetConsensus(), - IsV24Active(chainman), check_state, /*check_sigs=*/true)); + TipRules(chainman), check_state, /*check_sigs=*/true)); } BOOST_CHECK_EQUAL(check_state.GetRejectReason(), "bad-protx-type-mismatch"); BOOST_CHECK(check_state.GetResult() == TxValidationResult::TX_CONSENSUS); @@ -1265,7 +1265,7 @@ void FuncProUpServInvalidNType(TestChainSetup& setup) { LOCK(cs_main); BOOST_CHECK_MESSAGE(CheckProUpServTx(CTransaction(tx), tip_index(), dmnman, chainman.GetConsensus(), - IsV24Active(chainman), check_state, + TipRules(chainman), check_state, /*check_sigs=*/true), "unexpected rejection: " << check_state.GetRejectReason()); } @@ -2703,7 +2703,7 @@ void FuncMigrationRejectedWhenKeySquatted(TestChainV24SignalBeforeV19Setup& setu TxValidationState st; LOCK(cs_main); BOOST_CHECK(!CheckProUpServTx(CTransaction(tx), chainman.ActiveChain().Tip(), dmnman, chainman.GetConsensus(), - IsV24Active(chainman), st, true)); + TipRules(chainman), st, true)); BOOST_CHECK_EQUAL(st.GetRejectReason(), "bad-protx-dup-key"); } @@ -2759,7 +2759,7 @@ void FuncProUpServTxMigratesLegacy(TestChainV24SignalBeforeV19Setup& setup) TxValidationState val_state; LOCK(cs_main); BOOST_REQUIRE_MESSAGE(CheckProUpServTx(CTransaction(tx), chainman.ActiveChain().Tip(), dmnman, - chainman.GetConsensus(), IsV24Active(chainman), val_state, + chainman.GetConsensus(), TipRules(chainman), val_state, /*check_sigs=*/true), "migration ProUpServTx rejected: " << val_state.GetRejectReason()); } @@ -3061,7 +3061,7 @@ void FuncEvoNodeRaisedToExtAddrByServiceUpdateOnly(TestChainV24SignalBeforeV19Se TxValidationState val_state; LOCK(cs_main); CheckProUpServTx(CTransaction(tx), chainman.ActiveChain().Tip(), dmnman, chainman.GetConsensus(), - IsV24Active(chainman), val_state, /*check_sigs=*/true); + TipRules(chainman), val_state, /*check_sigs=*/true); return val_state.GetRejectReason(); }; auto check_state = [&](uint16_t version, const std::string& what) { @@ -3997,7 +3997,8 @@ void FuncPreV24BehaviourUnchanged(TestChainSetup& setup) TxValidationState val_state; LOCK(cs_main); BOOST_CHECK_MESSAGE(CheckProUpServTx(CTransaction(tx_ups), chainman.ActiveChain().Tip(), dmnman, - chainman.GetConsensus(), IsV24Active(chainman), val_state, /*check_sigs=*/true), + chainman.GetConsensus(), TipRules(chainman), val_state, + /*check_sigs=*/true), "pre-v24 v2 ProUpServTx wrongly rejected: " << val_state.GetRejectReason()); } setup.CreateAndProcessBlock({tx_ups}, coinbase_pk); diff --git a/src/test/evo_netinfo_tests.cpp b/src/test/evo_netinfo_tests.cpp index 8b49a9adfdc6..99c539f91e34 100644 --- a/src/test/evo_netinfo_tests.cpp +++ b/src/test/evo_netinfo_tests.cpp @@ -11,6 +11,8 @@ #include #include +#include +#include #include #include #include @@ -177,25 +179,27 @@ BOOST_AUTO_TEST_CASE(provider_network_fields_main) .platform_p2p = MainParams().GetDefaultPlatformP2PPort(), .platform_https = MainParams().GetDefaultPlatformHTTPPort(), }; - BOOST_CHECK(!node->evo().validateProviderNetInfo(net_info, MnType::Evo, ProTxVersion::BasicBLS, /*optional=*/false)); + BOOST_CHECK(!node->evo().validateProviderNetInfo(net_info, MnType::Evo, ProTxVersion::BasicBLS, + /*ipv4_optional=*/false, /*optional=*/false)); net_info.platform_p2p = static_cast(MainParams().GetDefaultPlatformP2PPort() + 1); auto validation_error{ - node->evo().validateProviderNetInfo(net_info, MnType::Evo, ProTxVersion::BasicBLS, /*optional=*/false)}; + node->evo().validateProviderNetInfo(net_info, MnType::Evo, ProTxVersion::BasicBLS, + /*ipv4_optional=*/false, /*optional=*/false)}; BOOST_REQUIRE(validation_error); BOOST_CHECK_EQUAL(validation_error->reject_reason, "bad-protx-platform-p2p-port"); net_info.platform_p2p = MainParams().GetDefaultPlatformP2PPort(); net_info.platform_https = static_cast(MainParams().GetDefaultPlatformHTTPPort() + 1); validation_error = node->evo().validateProviderNetInfo(net_info, MnType::Evo, ProTxVersion::BasicBLS, - /*optional=*/false); + /*ipv4_optional=*/false, /*optional=*/false); BOOST_REQUIRE(validation_error); BOOST_CHECK_EQUAL(validation_error->reject_reason, "bad-protx-platform-http-port"); net_info.platform_p2p = MainParams().GetDefaultPort(); net_info.platform_https = MainParams().GetDefaultPlatformHTTPPort(); validation_error = node->evo().validateProviderNetInfo(net_info, MnType::Evo, ProTxVersion::BasicBLS, - /*optional=*/false); + /*ipv4_optional=*/false, /*optional=*/false); BOOST_REQUIRE(validation_error); BOOST_CHECK_EQUAL(validation_error->reject_reason, "bad-protx-platform-p2p-port"); @@ -205,12 +209,14 @@ BOOST_AUTO_TEST_CASE(provider_network_fields_main) .platform_https = std::vector{"server.example.com:443"}, }; validation_error = node->evo().validateProviderNetInfo(net_info, MnType::Evo, ProTxVersion::ExtAddr, - /*optional=*/false); + /*ipv4_optional=*/false, /*optional=*/false); BOOST_REQUIRE(validation_error); BOOST_CHECK(validation_error->message.original.find("unroutable address") != std::string::npos); - // On mainnet the first entry of every purpose must use that purpose's port (9999, 26656, 443). Additional - // entries may use other addresses, ports and network types. Shared masternodes use the same network fields. + // On mainnet the first entry of every purpose must use that purpose's port (9999, 26656, 443) and, as + // masternodes may not operate without IPv4 there yet, be IPv4, with the first Platform entries on the first + // Core P2P address. Additional entries may use other addresses, ports and network types. Shared masternodes + // use the same network fields. const std::string a{"1.1.1.1"}, b{"1.1.1.2"}, ipv6{"[2606:4700:4700::1111]"}, onion{"pg6mmjiyjmcrsslvykfwnntlaru7p5svn6y2ymmju6nubxndf4pscryd.onion"}, i2p{"udhdrtrcetjm5sxzskjyr5ztpeszydbh4dpl3pl4utgqqw2v4jna.b32.i2p:0"}, domain{"platform.example.com"}; @@ -223,8 +229,9 @@ BOOST_AUTO_TEST_CASE(provider_network_fields_main) struct Case { MnType type{MnType::Regular}; interfaces::ProviderNetInfo net_info; - std::string error_field; // Empty if valid + std::string error_field; // Empty if valid, unless reject_reason is set std::string error_status{}; + std::string reject_reason{}; // Checked instead of the message if set }; const std::vector primary_vals{ // Valid, including additional entries of any type, address and port @@ -254,20 +261,44 @@ BOOST_AUTO_TEST_CASE(provider_network_fields_main) "invalid port"}, {MnType::Evo, make({a + ":9999"}, Strs{a + ":26656"}, Strs{domain + ":8443", a + ":443"}), "platformHTTPSAddrs[0]", "invalid port"}, + // The first entry of each purpose must be IPv4, Platform's on the first Core P2P address + {MnType::Regular, make({ipv6 + ":9999"}), "", "", "bad-protx-netinfo-addr-type"}, + {MnType::Regular, make({onion + ":9999", a + ":9999"}), "", "", "bad-protx-netinfo-addr-type"}, + {MnType::Evo, make({a + ":9999"}, Strs{b + ":26656"}, Strs{a + ":443"}), "", "", "bad-protx-netinfo-addr"}, + {MnType::Evo, make({a + ":9999"}, Strs{a + ":26656"}, Strs{b + ":443"}), "", "", "bad-protx-netinfo-addr"}, + {MnType::Evo, make({a + ":9999", ipv6 + ":9999"}, Strs{ipv6 + ":26656"}, Strs{a + ":443"}), "", "", + "bad-protx-netinfo-addr-type"}, + {MnType::Evo, make({a + ":9999"}, Strs{a + ":26656"}, Strs{domain + ":443"}), "", "", + "bad-protx-netinfo-addr-type"}, }; - for (const auto& [type, case_net_info, error_field, error_status] : primary_vals) { + for (const auto& [type, case_net_info, error_field, error_status, reject_reason] : primary_vals) { validation_error = node->evo().validateProviderNetInfo(case_net_info, type, ProTxVersion::ExtAddr, - /*optional=*/false); - BOOST_CHECK_MESSAGE(!validation_error == error_field.empty(), + /*ipv4_optional=*/false, /*optional=*/false); + BOOST_CHECK_MESSAGE(!validation_error == (error_field.empty() && reject_reason.empty()), strprintf("%s: %s", Join(case_net_info.core_p2p, ","), validation_error ? validation_error->message.original : "accepted")); - if (validation_error && !error_field.empty()) { + if (validation_error && !reject_reason.empty()) { + BOOST_CHECK_MESSAGE(validation_error->reject_reason == reject_reason, validation_error->message.original); + } else if (validation_error && !error_field.empty()) { const auto& message{validation_error->message.original}; BOOST_CHECK_MESSAGE(message.find(error_field) != std::string::npos && message.find(strprintf("(%s)", error_status)) != std::string::npos, message); } } + + // Validation needs no chain: the caller supplies whether masternodes may operate without IPv4, which lifts + // the IPv4 and same-address rules but not the port pins + for (const auto& [type, case_net_info, _field, _status, reject_reason] : primary_vals) { + if (reject_reason.empty()) continue; + BOOST_CHECK(!node->evo().validateProviderNetInfo(case_net_info, type, ProTxVersion::ExtAddr, + /*ipv4_optional=*/true, /*optional=*/false)); + } + validation_error = node->evo().validateProviderNetInfo(make({ipv6 + ":9998"}), MnType::Regular, + ProTxVersion::ExtAddr, /*ipv4_optional=*/true, + /*optional=*/false); + BOOST_REQUIRE(validation_error); + BOOST_CHECK(validation_error->message.original.find("(invalid port)") != std::string::npos); } struct TestNetSetup : public BasicTestingSetup { @@ -302,7 +333,7 @@ BOOST_FIXTURE_TEST_CASE(extnetinfo_ports_mainnet_only, TestNetSetup) const auto check_fields = [](const std::shared_ptr& net_info, MnType type, TxValidationState& state) { return CheckProviderNetworkFields(net_info, type, ProTxVersion::ExtAddr, /*platform_node_id=*/nullptr, /*platform_p2p_port=*/0, /*platform_http_port=*/0, /*allow_empty=*/false, - state); + /*ipv4_optional=*/false, state); }; // Outside mainnet, these port rules don't apply @@ -332,6 +363,76 @@ BOOST_FIXTURE_TEST_CASE(extnetinfo_ports_mainnet_only, TestNetSetup) SelectParams(CBaseChainParams::TESTNET); } +BOOST_FIXTURE_TEST_CASE(ipv4_primaries_follow_activation, TestNetSetup) +{ + // Masternodes may operate without IPv4 on testnet (and devnets), which already has such registrations, but + // not yet on mainnet + BOOST_CHECK(DeploymentActiveAfter(nullptr, Params().GetConsensus(), Consensus::DEPLOYMENT_IPV4_OPTIONAL)); + BOOST_CHECK(!DeploymentEnabled(MainParams().GetConsensus(), Consensus::DEPLOYMENT_IPV4_OPTIONAL)); + + struct Case { + MnType type{MnType::Regular}; + std::vector> entries; + std::string reject_reason{}; // Before activation, empty if valid + }; + const std::string ipv6{"[2606:4700:4700::1111]"}; + const std::string onion{"pg6mmjiyjmcrsslvykfwnntlaru7p5svn6y2ymmju6nubxndf4pscryd.onion"}; + const std::vector vals{ + {MnType::Regular, {{NetInfoPurpose::CORE_P2P, ipv6 + ":9999"}, {NetInfoPurpose::CORE_P2P, "1.1.1.1:9999"}}, + "bad-protx-netinfo-addr-type"}, + {MnType::Regular, {{NetInfoPurpose::CORE_P2P, onion + ":9999"}}, "bad-protx-netinfo-addr-type"}, + {MnType::Evo, + {{NetInfoPurpose::CORE_P2P, "1.1.1.1:9999"}, {NetInfoPurpose::PLATFORM_P2P, "1.1.1.2:26656"}, + {NetInfoPurpose::PLATFORM_HTTPS, "1.1.1.1:443"}}, + "bad-protx-netinfo-addr"}, + {MnType::Evo, + {{NetInfoPurpose::CORE_P2P, "1.1.1.1:9999"}, {NetInfoPurpose::PLATFORM_P2P, "1.1.1.1:26656"}, + {NetInfoPurpose::PLATFORM_HTTPS, "1.1.1.2:443"}}, + "bad-protx-netinfo-addr"}, + {MnType::Evo, + {{NetInfoPurpose::CORE_P2P, "1.1.1.1:9999"}, {NetInfoPurpose::CORE_P2P, ipv6 + ":9999"}, + {NetInfoPurpose::PLATFORM_P2P, ipv6 + ":26656"}, {NetInfoPurpose::PLATFORM_HTTPS, "1.1.1.1:443"}}, + "bad-protx-netinfo-addr-type"}, + {MnType::Evo, + {{NetInfoPurpose::CORE_P2P, "1.1.1.1:9999"}, {NetInfoPurpose::PLATFORM_P2P, "1.1.1.1:26656"}, + {NetInfoPurpose::PLATFORM_HTTPS, "platform.example.com:443"}}, + "bad-protx-netinfo-addr-type"}, + {MnType::Evo, + {{NetInfoPurpose::CORE_P2P, "1.1.1.1:9999"}, {NetInfoPurpose::CORE_P2P, ipv6 + ":9999"}, + {NetInfoPurpose::PLATFORM_P2P, "1.1.1.1:26656"}, {NetInfoPurpose::PLATFORM_P2P, onion + ":26656"}, + {NetInfoPurpose::PLATFORM_HTTPS, "1.1.1.1:443"}, + {NetInfoPurpose::PLATFORM_HTTPS, "platform.example.com:443"}}}, + }; + const auto check = [](const std::shared_ptr& net_info, const Case& val) { + BOOST_CHECK_EQUAL(net_info->Validate(), NetInfoStatus::Success); + TxValidationState state; + BOOST_CHECK(CheckProviderNetworkFields(net_info, val.type, ProTxVersion::ExtAddr, nullptr, 0, 0, + /*allow_empty=*/false, /*ipv4_optional=*/true, state)); + BOOST_CHECK_EQUAL(CheckProviderNetworkFields(net_info, val.type, ProTxVersion::ExtAddr, nullptr, 0, 0, + /*allow_empty=*/false, /*ipv4_optional=*/false, state), + val.reject_reason.empty()); + BOOST_CHECK_EQUAL(state.GetRejectReason(), val.reject_reason); + }; + + std::vector serialized; + for (const auto& val : vals) { + auto net_info{std::make_shared()}; + for (const auto& [purpose, entry] : val.entries) { + BOOST_CHECK_EQUAL(net_info->AddEntry(purpose, entry), NetInfoStatus::Success); + } + check(net_info, val); + serialized.emplace_back(SER_DISK, CLIENT_VERSION) << *net_info; + } + + // The rule depends on the activation, not the network. Validate(), which also checks stored masternode states + // without a height, doesn't apply it. + SelectParams(CBaseChainParams::MAIN); + for (size_t idx{0}; idx < vals.size(); ++idx) { + check(std::make_shared(deserialize, serialized[idx]), vals[idx]); + } + SelectParams(CBaseChainParams::TESTNET); +} + static const std::vector addr_vals_reg{ // - MnNetInfo doesn't mind using port 0 // - ExtNetInfo requires non-zero ports diff --git a/src/test/evo_snapshot_tests.cpp b/src/test/evo_snapshot_tests.cpp index 5fe56b2972c0..8c38d0677ff4 100644 --- a/src/test/evo_snapshot_tests.cpp +++ b/src/test/evo_snapshot_tests.cpp @@ -79,6 +79,9 @@ CDeterministicMNCPtr MN(uint64_t internal_id, uint8_t hash_suffix, MnType type, NetInfoStatus::Success); if (type == MnType::Evo) { state->platformNodeID = H160(address_tag + 40); + // Platform on another address, with a domain first for HTTPS, as DEPLOYMENT_IPV4_OPTIONAL allows, on the + // standard ports, which mainnet pins. Such states load on every network: the IPv4 rule applies to ProTxs, + // at the height they are mined at. BOOST_REQUIRE_EQUAL(state->netInfo->AddEntry(NetInfoPurpose::PLATFORM_P2P, strprintf("2.2.2.%d:26656", address_tag)), NetInfoStatus::Success); diff --git a/src/test/interfaces_tests.cpp b/src/test/interfaces_tests.cpp index 1ccbff17eb8c..d4533e4b245a 100644 --- a/src/test/interfaces_tests.cpp +++ b/src/test/interfaces_tests.cpp @@ -25,6 +25,9 @@ #include #include +#include +#include +#include #include @@ -86,6 +89,41 @@ BOOST_AUTO_TEST_CASE(providerTxCapabilities) *Assert(m_node.chainman)))}; BOOST_CHECK_EQUAL(capabilities.version, expected_version); BOOST_CHECK_EQUAL(capabilities.extended_addresses, expected_version >= ProTxVersion::ExtAddr); + BOOST_CHECK(!capabilities.ipv4_optional); // Not active on regtest by default +} + +struct IPv4OptionalChainSetup : public TestChain100Setup { + IPv4OptionalChainSetup() + : TestChain100Setup{CBaseChainParams::REGTEST, {"-testactivationheight=ipv4_optional@1"}} {} +}; + +BOOST_FIXTURE_TEST_CASE(providerTxCapabilitiesIPv4Optional, IPv4OptionalChainSetup) +{ + BOOST_CHECK(interfaces::MakeNode(m_node)->evo().getProviderTxCapabilities().ipv4_optional); +} + +BOOST_AUTO_TEST_CASE(providerNetInfoValidationDoesNotWaitForChain) +{ + // Dialogs validate endpoints on every keystroke, so that must not wait for cs_main + auto node{interfaces::MakeNode(m_node)}; + const interfaces::ProviderNetInfo net_info{.core_p2p = {"1.1.1.1:9998"}, + .platform_p2p = std::vector{"1.1.1.1:22200"}, + .platform_https = std::vector{"1.1.1.1:443"}}; + std::promise locked; + std::promise release; + bool lock_timed_out{false}; + std::thread holder{[&] { + LOCK(::cs_main); + locked.set_value(); + lock_timed_out = release.get_future().wait_for(std::chrono::seconds{10}) == std::future_status::timeout; + }}; + locked.get_future().wait(); + const auto error{node->evo().validateProviderNetInfo(net_info, MnType::Evo, ProTxVersion::ExtAddr, + /*ipv4_optional=*/false, /*optional=*/false)}; + release.set_value(); + holder.join(); + BOOST_CHECK(!lock_timed_out); + BOOST_CHECK(!error); } BOOST_AUTO_TEST_CASE(providerNetInfoValidation) @@ -97,33 +135,47 @@ BOOST_AUTO_TEST_CASE(providerNetInfoValidation) .platform_https = std::vector{"server.example.com:443"}, }; - BOOST_CHECK(!node->evo().validateProviderNetInfo(net_info, MnType::Evo, ProTxVersion::ExtAddr, /*optional=*/false)); + // Unless the caller's capabilities say masternodes may operate without IPv4, Platform must use the Core address + auto error{node->evo().validateProviderNetInfo(net_info, MnType::Evo, ProTxVersion::ExtAddr, + /*ipv4_optional=*/false, /*optional=*/false)}; + BOOST_REQUIRE(error); + BOOST_CHECK_EQUAL(error->reject_reason, "bad-protx-netinfo-addr"); + + net_info.platform_p2p = std::vector{"1.1.1.1:22200"}; + net_info.platform_https = std::vector{"1.1.1.1:443", "server.example.com:443"}; + BOOST_CHECK(!node->evo().validateProviderNetInfo(net_info, MnType::Evo, ProTxVersion::ExtAddr, + /*ipv4_optional=*/false, /*optional=*/false)); net_info.platform_p2p = std::vector{"server.example.com:22200"}; - auto error{node->evo().validateProviderNetInfo(net_info, MnType::Evo, ProTxVersion::ExtAddr, /*optional=*/false)}; + error = node->evo().validateProviderNetInfo(net_info, MnType::Evo, ProTxVersion::ExtAddr, + /*ipv4_optional=*/false, /*optional=*/false); BOOST_REQUIRE(error); BOOST_CHECK_EQUAL(error->code, interfaces::ProviderTxErrorCode::INVALID_PARAMETER); BOOST_CHECK_EQUAL(error->message.original, "Error setting platformP2PAddrs[0] to 'server.example.com:22200' (invalid input)"); net_info.platform_p2p = std::vector{"1.1.1.1:9998"}; - error = node->evo().validateProviderNetInfo(net_info, MnType::Evo, ProTxVersion::ExtAddr, /*optional=*/false); + error = node->evo().validateProviderNetInfo(net_info, MnType::Evo, ProTxVersion::ExtAddr, + /*ipv4_optional=*/false, /*optional=*/false); BOOST_REQUIRE(error); BOOST_CHECK_EQUAL(error->message.original, "Error setting platformP2PAddrs[0] to '1.1.1.1:9998' (duplicate)"); net_info.platform_p2p = std::vector{"1.1.1.2:22200", "1.1.1.3:22200", "1.1.1.4:22200", "1.1.1.5:22200", "1.1.1.6:22200"}; - error = node->evo().validateProviderNetInfo(net_info, MnType::Evo, ProTxVersion::ExtAddr, /*optional=*/false); + error = node->evo().validateProviderNetInfo(net_info, MnType::Evo, ProTxVersion::ExtAddr, + /*ipv4_optional=*/false, /*optional=*/false); BOOST_REQUIRE(error); BOOST_CHECK_EQUAL(error->message.original, "Error setting platformP2PAddrs[4] to '1.1.1.6:22200' (too many entries)"); net_info.platform_p2p = uint16_t{22200}; net_info.platform_https = uint16_t{22201}; - BOOST_CHECK(!node->evo().validateProviderNetInfo(net_info, MnType::Evo, ProTxVersion::BasicBLS, /*optional=*/false)); + BOOST_CHECK(!node->evo().validateProviderNetInfo(net_info, MnType::Evo, ProTxVersion::BasicBLS, + /*ipv4_optional=*/false, /*optional=*/false)); net_info.platform_p2p = std::vector{"1.1.1.2:22200"}; - error = node->evo().validateProviderNetInfo(net_info, MnType::Evo, ProTxVersion::BasicBLS, /*optional=*/false); + error = node->evo().validateProviderNetInfo(net_info, MnType::Evo, ProTxVersion::BasicBLS, + /*ipv4_optional=*/false, /*optional=*/false); BOOST_REQUIRE(error); BOOST_CHECK(error->message.original.find("only accepts a bare port number") != std::string::npos); @@ -132,7 +184,8 @@ BOOST_AUTO_TEST_CASE(providerNetInfoValidation) .platform_p2p = std::vector{"1.1.1.2:22200"}, .platform_https = std::vector{"server.example.com:443"}, }; - error = node->evo().validateProviderNetInfo(net_info, MnType::Evo, ProTxVersion::ExtAddr, /*optional=*/true); + error = node->evo().validateProviderNetInfo(net_info, MnType::Evo, ProTxVersion::ExtAddr, + /*ipv4_optional=*/false, /*optional=*/true); BOOST_REQUIRE(error); BOOST_CHECK_EQUAL(error->reject_reason, "bad-protx-netinfo-empty"); @@ -141,14 +194,17 @@ BOOST_AUTO_TEST_CASE(providerNetInfoValidation) .platform_p2p = std::monostate{}, .platform_https = std::monostate{}, }; - error = node->evo().validateProviderNetInfo(net_info, MnType::Evo, ProTxVersion::ExtAddr, /*optional=*/true); + error = node->evo().validateProviderNetInfo(net_info, MnType::Evo, ProTxVersion::ExtAddr, + /*ipv4_optional=*/false, /*optional=*/true); BOOST_REQUIRE(error); BOOST_CHECK(error->message.original.find("platformP2PAddrs, cannot be empty if other fields populated") != std::string::npos); - BOOST_CHECK(!node->evo().validateProviderNetInfo(net_info, MnType::Regular, ProTxVersion::ExtAddr, /*optional=*/true)); + BOOST_CHECK(!node->evo().validateProviderNetInfo(net_info, MnType::Regular, ProTxVersion::ExtAddr, + /*ipv4_optional=*/false, /*optional=*/true)); net_info.platform_p2p = std::vector{"1.1.1.2:22200"}; - error = node->evo().validateProviderNetInfo(net_info, MnType::Regular, ProTxVersion::ExtAddr, /*optional=*/true); + error = node->evo().validateProviderNetInfo(net_info, MnType::Regular, ProTxVersion::ExtAddr, + /*ipv4_optional=*/false, /*optional=*/true); BOOST_REQUIRE(error); BOOST_CHECK_EQUAL(error->message.original, "Platform endpoints are only valid for an EvoNode"); @@ -157,7 +213,8 @@ BOOST_AUTO_TEST_CASE(providerNetInfoValidation) .platform_p2p = std::vector{"1.1.1.2:22200"}, .platform_https = std::monostate{}, }; - error = node->evo().validateProviderNetInfo(net_info, MnType::Evo, ProTxVersion::ExtAddr, /*optional=*/true); + error = node->evo().validateProviderNetInfo(net_info, MnType::Evo, ProTxVersion::ExtAddr, + /*ipv4_optional=*/false, /*optional=*/true); BOOST_REQUIRE(error); BOOST_CHECK(error->message.original.find("platformHTTPSAddrs, cannot be empty if other fields populated") != std::string::npos); @@ -167,13 +224,15 @@ BOOST_AUTO_TEST_CASE(providerNetInfoValidation) .platform_p2p = uint16_t{22200}, .platform_https = uint16_t{22200}, }; - error = node->evo().validateProviderNetInfo(net_info, MnType::Evo, ProTxVersion::BasicBLS, /*optional=*/false); + error = node->evo().validateProviderNetInfo(net_info, MnType::Evo, ProTxVersion::BasicBLS, + /*ipv4_optional=*/false, /*optional=*/false); BOOST_REQUIRE(error); BOOST_CHECK_EQUAL(error->reject_reason, "bad-protx-platform-dup-ports"); net_info.platform_p2p = uint16_t{MainParams().GetDefaultPort()}; net_info.platform_https = uint16_t{22201}; - error = node->evo().validateProviderNetInfo(net_info, MnType::Evo, ProTxVersion::BasicBLS, /*optional=*/false); + error = node->evo().validateProviderNetInfo(net_info, MnType::Evo, ProTxVersion::BasicBLS, + /*ipv4_optional=*/false, /*optional=*/false); BOOST_REQUIRE(error); BOOST_CHECK_EQUAL(error->reject_reason, "bad-protx-platform-p2p-port"); } diff --git a/src/validation.cpp b/src/validation.cpp index c0934f9271e0..cb6c95dc8e03 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -7035,6 +7035,7 @@ SpecialTxRules GetSpecialTxRules(const CBlockIndex* pindexPrev, const Chainstate return SpecialTxRules{ .v24 = DeploymentActiveAfter(pindexPrev, chainman, Consensus::DEPLOYMENT_V24), .evo_shares = DeploymentActiveAfter(pindexPrev, chainman, Consensus::DEPLOYMENT_EVO_SHARES), + .ipv4_optional = DeploymentActiveAfter(pindexPrev, chainman, Consensus::DEPLOYMENT_IPV4_OPTIONAL), }; } diff --git a/test/functional/rpc_netinfo.py b/test/functional/rpc_netinfo.py index 37cbb188318f..d6f84336beb0 100755 --- a/test/functional/rpc_netinfo.py +++ b/test/functional/rpc_netinfo.py @@ -6,6 +6,7 @@ from test_framework.util import ( assert_equal, + assert_raises_rpc_error, softfork_active ) from test_framework.script import ( @@ -24,6 +25,8 @@ # Height at which BIP9 deployment DEPLOYMENT_V24 is activated V24_ACTIVATION_THRESHOLD = 100 +# Height from which masternodes may operate without IPv4, see DEPLOYMENT_IPV4_OPTIONAL in src/consensus/params.h +IPV4_OPTIONAL_HEIGHT = 500 # See CMainParams in src/chainparams.cpp DEFAULT_PORT_MAINNET_CORE_P2P = 9999 # See CRegTestParams in src/chainparams.cpp @@ -160,7 +163,8 @@ def add_options(self, parser): def set_test_params(self): self.num_nodes = 3 self.extra_args = [[ - "-dip3params=2:2", f"-vbparams=v24:{self.mocktime}:999999999999:{V24_ACTIVATION_THRESHOLD}:10:8:6:5:0" + "-dip3params=2:2", f"-vbparams=v24:{self.mocktime}:999999999999:{V24_ACTIVATION_THRESHOLD}:10:8:6:5:0", + f"-testactivationheight=ipv4_optional@{IPV4_OPTIONAL_HEIGHT}" ] for _ in range(self.num_nodes)] self.extra_args[2] += ["-deprecatedrpc=service"] @@ -210,6 +214,8 @@ def run_test(self): self.test_deprecation() self.log.info("Mine blocks to activate DEPLOYMENT_V24") self.activate_v24() + self.log.info("Test that primary addresses must be IPv4 until DEPLOYMENT_IPV4_OPTIONAL") + self.test_ipv4_optional() self.log.info("Test input validation for masternode address fields (post-fork)") self.test_validation_common() self.test_validation_extended() @@ -405,6 +411,48 @@ def test_validation_extended(self): self.node_evo.register_mn(self, False, "127.0.0.1", DEFAULT_PORT_PLATFORM_P2P, DEFAULT_PORT_PLATFORM_HTTP, -8, "Error setting coreP2PAddrs[0] to '127.0.0.1' (invalid port)") + def test_ipv4_optional(self): + port_evo, port_two = self.node_evo.mn.nodePort, self.node_two.mn.nodePort + p2p, https = DEFAULT_PORT_PLATFORM_P2P, DEFAULT_PORT_PLATFORM_HTTP + # The first entry of each purpose must be IPv4, and Platform's must use the first Core P2P address + cases = [ + ([f"[::1]:{port_evo}", f"127.0.0.1:{port_evo}"], [f"127.0.0.1:{p2p}"], [f"127.0.0.1:{https}"], + "bad-protx-netinfo-addr-type, first core_p2p entry must be IPv4"), + ([f"127.0.0.1:{port_evo}"], [f"127.0.0.2:{p2p}"], [f"127.0.0.1:{https}"], + "bad-protx-netinfo-addr, first platform_p2p entry must use the first core_p2p address"), + ([f"127.0.0.1:{port_evo}"], [f"127.0.0.1:{p2p}"], [f"{DOMAINS_CLR[0]}:{https}", f"127.0.0.1:{https}"], + "bad-protx-netinfo-addr-type, first platform_https entry must be IPv4"), + ] + update = ([f"127.0.0.1:{port_two}"], [f"{DOMAINS_TOR[0]}:{p2p}", f"127.0.0.1:{p2p}"], [f"127.0.0.2:{https}"], + "bad-protx-netinfo-addr-type, first platform_p2p entry must be IPv4") + + # The block after the tip is the one these would be included in + assert self.nodes[0].getblockcount() < IPV4_OPTIONAL_HEIGHT - 2 + self.generate(self.nodes[0], IPV4_OPTIONAL_HEIGHT - 2 - self.nodes[0].getblockcount()) + for core, plat_p2p, plat_https, error in cases: + self.node_evo.register_mn(self, False, core, plat_p2p, plat_https, -1, error) + self.node_two.update_mn(self, False, *update[:3], -1, update[3]) + + self.generate(self.nodes[0], 1) + txs = [self.node_evo.register_mn(self, False, *case[:3]) for case in cases] + txs.append(self.node_two.update_mn(self, False, *update[:3])) + for tx in txs: + assert self.nodes[0].testmempoolaccept([tx])[0]['allowed'] + + # Validation outside the RPCs, for the mempool and for blocks, applies the same rule at the same height + tip = self.nodes[0].getbestblockhash() + self.nodes[0].invalidateblock(tip) + address = self.nodes[0].getnewaddress() + for tx, (*_, error) in zip(txs, cases + [update]): + reason = error.split(",")[0] + result = self.nodes[0].testmempoolaccept([tx])[0] + assert_equal(result['allowed'], False) + assert_equal(result['reject-reason'], reason) + assert_raises_rpc_error(-25, f"TestBlockValidity failed: {reason}", self.nodes[0].generateblock, address, + [tx], invalid_call=False) + self.nodes[0].reconsiderblock(tip) + self.sync_blocks() + def test_deprecation(self): # netInfo is represented with JSON in CProRegTx, CProUpServTx, CDeterministicMNState and CSimplifiedMNListEntry, # so we need to test calls that rely on these underlying implementations. Start by collecting RPC responses.