Skip to content

Commit a37500b

Browse files
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@<height> (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. The RPC and GUI pre-checks evaluate it at the chain tip. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent cd54bfe commit a37500b

23 files changed

Lines changed: 290 additions & 63 deletions

‎doc/release-notes-8018.md‎

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
Consensus changes
22
-----------------
33

4-
- On mainnet, the first entry of each address list in a version 3 (extended addresses) ProRegTx or
5-
ProUpServTx must use that list's standard port: 9999 for `coreP2PAddrs`, 26656 for `platformP2PAddrs` and
6-
443 for `platformHTTPSAddrs`, as legacy ProTx versions already require on mainnet. Additional IPv4 entries
7-
may not use port 26656 or 443. Violations are rejected with `bad-protx-netinfo-port`.
4+
- A version 3 (extended addresses) ProRegTx or ProUpServTx must list an IPv4 address as its first
5+
`coreP2PAddrs` entry, and an EvoNode's first `platformP2PAddrs` and `platformHTTPSAddrs` entries must use
6+
that same IPv4 address. Further entries may use other addresses and network types (IPv6, Tor, I2P, CJDNS,
7+
and domains for Platform HTTPS). Violations are rejected with `bad-protx-netinfo-addr-type` or
8+
`bad-protx-netinfo-addr`. This applies until masternodes are allowed to operate without IPv4, which is
9+
not scheduled on mainnet. Testnet and devnets already allow it, so they are unaffected. Regtest follows
10+
mainnet unless `-testactivationheight=ipv4_optional@<height>` is set.
11+
- On mainnet, the first entry of each address list must also use that list's standard port: 9999 for
12+
`coreP2PAddrs`, 26656 for `platformP2PAddrs` and 443 for `platformHTTPSAddrs`, as legacy ProTx versions
13+
already require there. Additional IPv4 entries may not use port 26656 or 443. Violations are rejected
14+
with `bad-protx-netinfo-port`.

‎src/chainparams.cpp‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -338,7 +338,7 @@ void SetupChainParamsOptions(ArgsManager& argsman)
338338
argsman.AddArg("-llmqtestplatformparams=<size>:<threshold>", "Override the default LLMQ size for the LLMQ_TEST_PLATFORM quorum (default: 3:2, regtest-only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CHAINPARAMS);
339339
argsman.AddArg("-minimumdifficultyblocks=<n>", "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);
340340
argsman.AddArg("-powtargetspacing=<n>", "Override the default PowTargetSpacing value in seconds (default: 2.5 minutes, devnet-only)", ArgsManager::ALLOW_ANY | ArgsManager::DISALLOW_NEGATION, OptionsCategory::CHAINPARAMS);
341-
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);
341+
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);
342342
argsman.AddArg("-vbparams=<deployment>:<start>:<end>(:min_activation_height(:<window>:<threshold/thresholdstart>(:<thresholdmin>:<falloffcoeff>:<mnactivation>)))",
343343
"Use given start/end times and min_activation_height for specified version bits deployment (regtest-only). "
344344
"Specifying window, threshold/thresholdstart, thresholdmin, falloffcoeff and mnactivation is optional.", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CHAINPARAMS);

‎src/consensus/params.h‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,9 @@ enum BuriedDeployment : int16_t {
3636
DEPLOYMENT_V20,
3737
DEPLOYMENT_MN_RR,
3838
DEPLOYMENT_WITHDRAWALS,
39+
DEPLOYMENT_IPV4_OPTIONAL, // Masternodes may operate without IPv4
3940
};
40-
constexpr bool ValidDeployment(BuriedDeployment dep) { return dep <= DEPLOYMENT_WITHDRAWALS; }
41+
constexpr bool ValidDeployment(BuriedDeployment dep) { return dep <= DEPLOYMENT_IPV4_OPTIONAL; }
4142

4243
enum DeploymentPos : uint16_t {
4344
DEPLOYMENT_TESTDUMMY,
@@ -151,6 +152,9 @@ struct Params {
151152
int MN_RRHeight;
152153
/** Block height at which WITHDRAWALS (Deployment of quorum fix and higher limits for withdrawals) becomes active */
153154
int WithdrawalsHeight;
155+
/** Block height from which a masternode's primary addresses no longer need to be IPv4.
156+
* std::numeric_limits<int>::max() while no activation is scheduled. */
157+
int IPv4OptionalHeight;
154158
/** Don't warn about unknown BIP 9 activations below this height.
155159
* This prevents us from warning about the CSV and DIP activations. */
156160
int MinBIP9WarningHeight;
@@ -232,6 +236,8 @@ struct Params {
232236
return MN_RRHeight;
233237
case DEPLOYMENT_WITHDRAWALS:
234238
return WithdrawalsHeight;
239+
case DEPLOYMENT_IPV4_OPTIONAL:
240+
return IPv4OptionalHeight;
235241
} // no default case, so the compiler can warn about missing cases
236242
return std::numeric_limits<int>::max();
237243
}

‎src/deploymentinfo.cpp‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ std::string DeploymentName(Consensus::BuriedDeployment dep)
5757
return "mn_rr";
5858
case Consensus::DEPLOYMENT_WITHDRAWALS:
5959
return "withdrawals";
60+
case Consensus::DEPLOYMENT_IPV4_OPTIONAL:
61+
return "ipv4_optional";
6062
} // no default case, so the compiler can warn about missing cases
6163
return "";
6264
}
@@ -87,6 +89,8 @@ std::optional<Consensus::BuriedDeployment> GetBuriedDeployment(const std::string
8789
return Consensus::BuriedDeployment::DEPLOYMENT_V20;
8890
} else if (name == "mn_rr") {
8991
return Consensus::BuriedDeployment::DEPLOYMENT_MN_RR;
92+
} else if (name == "ipv4_optional") {
93+
return Consensus::BuriedDeployment::DEPLOYMENT_IPV4_OPTIONAL;
9094
}
9195
return std::nullopt;
9296
}

‎src/evo/core_write.cpp‎

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,9 @@ template UniValue GetNetInfoWithLegacyFields(const CDeterministicMNState& obj, c
6060
template <bool is_p2p, typename Obj>
6161
int32_t GetPlatformPort(const Obj& obj)
6262
{
63-
// Currently, there is nothing that prevents PLATFORM_{HTTPS,P2P} to be registered to
64-
// an addr *different* from the primary addr for CORE_P2P. This breaks the assumptions
65-
// under which platform{HTTP,P2P}Port operate under (i.e. all three are hosted with the
66-
// same addr).
67-
//
68-
// TODO: Introduce restrictions that enforce this assumption *until* we remove legacy
69-
// fields for good.
63+
// Until DEPLOYMENT_IPV4_OPTIONAL is active, consensus requires the first PLATFORM_{HTTPS,P2P} entries to
64+
// use the primary addr for CORE_P2P, as platform{HTTP,P2P}Port assume (i.e. all three are hosted with the
65+
// same addr). Once it is (always on testnet and devnets), the ports reported here may belong to another addr.
7066
//
7167
bool is_legacy{!(CHECK_NONFATAL(obj.netInfo)->CanStorePlatform())};
7268
if constexpr (is_p2p) {

‎src/evo/providertx.cpp‎

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,9 +229,35 @@ static bool CheckNetInfo(const NetInfoInterface& net_info, TxValidationState& st
229229
assert(false);
230230
}
231231

232+
//! The primary Core P2P address must be IPv4, and Platform's primary addresses must be on that same address
233+
static bool CheckIPv4Primaries(const NetInfoInterface& net_info, TxValidationState& state)
234+
{
235+
const CService core_primary{net_info.GetPrimary()};
236+
if (!core_primary.IsIPv4()) {
237+
return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-netinfo-addr-type",
238+
strprintf("first %s entry must be IPv4", PurposeToString(NetInfoPurpose::CORE_P2P)));
239+
}
240+
for (const auto purpose : {NetInfoPurpose::PLATFORM_P2P, NetInfoPurpose::PLATFORM_HTTPS}) {
241+
const auto entries{net_info.GetEntries(purpose)};
242+
if (entries.empty()) continue;
243+
const auto service_opt{entries.front().GetAddrPort()};
244+
if (!service_opt || !service_opt->IsIPv4()) {
245+
return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-netinfo-addr-type",
246+
strprintf("first %s entry must be IPv4", PurposeToString(purpose)));
247+
}
248+
if (CNetAddr{*service_opt} != CNetAddr{core_primary}) {
249+
return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-protx-netinfo-addr",
250+
strprintf("first %s entry must use the first %s address", PurposeToString(purpose),
251+
PurposeToString(NetInfoPurpose::CORE_P2P)));
252+
}
253+
}
254+
return true;
255+
}
256+
232257
bool CheckProviderNetworkFields(const std::shared_ptr<NetInfoInterface>& net_info, MnType type, uint16_t version,
233258
const uint160* platform_node_id, uint16_t platform_p2p_port,
234-
uint16_t platform_http_port, bool allow_empty, TxValidationState& state)
259+
uint16_t platform_http_port, bool allow_empty, bool ipv4_optional,
260+
TxValidationState& state)
235261
{
236262
if (!net_info || net_info->CanStorePlatform() != (version >= ProTxVersion::ExtAddr)) {
237263
return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-protx-netinfo-version");
@@ -244,6 +270,10 @@ bool CheckProviderNetworkFields(const std::shared_ptr<NetInfoInterface>& net_inf
244270
if (!IsNetInfoTriviallyValid(net_info, type, state) || !CheckNetInfo(*net_info, state)) {
245271
return false;
246272
}
273+
// Checked here rather than in Validate(), which also checks stored masternode states without a height
274+
if (!ipv4_optional && !CheckIPv4Primaries(*net_info, state)) {
275+
return false;
276+
}
247277
}
248278

249279
if (type != MnType::Evo) return true;

‎src/evo/providertx.h‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,10 +105,12 @@ template<class T>
105105
[[nodiscard]] UniValue PayoutListToJson(const MasternodePayoutShares& payouts);
106106

107107
/** Validate all provider network fields using the same rules as special transaction validation.
108-
* Pass nullptr for platform_node_id when validating endpoint input separately from the rest of a payload. */
108+
* Pass nullptr for platform_node_id when validating endpoint input separately from the rest of a payload.
109+
* ipv4_optional is SpecialTxRules::ipv4_optional for the block the transaction would be included in. */
109110
[[nodiscard]] bool CheckProviderNetworkFields(const std::shared_ptr<NetInfoInterface>& net_info, MnType type,
110111
uint16_t version, const uint160* platform_node_id, uint16_t platform_p2p_port,
111-
uint16_t platform_http_port, bool allow_empty, TxValidationState& state);
112+
uint16_t platform_http_port, bool allow_empty, bool ipv4_optional,
113+
TxValidationState& state);
112114

113115
/** The count, per-share bound and sum rules of IsShareListTriviallyValid, usable before any share address exists */
114116
[[nodiscard]] bool AreShareAmountsValid(const std::vector<CAmount>& amounts, CAmount required_collateral,

‎src/evo/providertx_service.cpp‎

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include <consensus/amount.h>
88
#include <consensus/tx_verify.h>
99
#include <consensus/validation.h>
10+
#include <chainparams.h>
1011
#include <deploymentstatus.h>
1112
#include <evo/deterministicmns.h>
1213
#include <evo/netinfo.h>
@@ -108,6 +109,14 @@ bool EvoSharesActive(node::NodeContext& node)
108109
return DeploymentActiveAfter(chainman.ActiveChain().Tip(), chainman, Consensus::DEPLOYMENT_EVO_SHARES);
109110
}
110111

112+
bool IPv4OptionalActive(node::NodeContext& node)
113+
{
114+
AssertLockNotHeld(::cs_main);
115+
auto& chainman{*Assert(node.chainman)};
116+
LOCK(::cs_main);
117+
return DeploymentActiveAfter(chainman.ActiveChain().Tip(), chainman, Consensus::DEPLOYMENT_IPV4_OPTIONAL);
118+
}
119+
111120
struct ChainSnapshot {
112121
uint16_t provider_tx_version{0};
113122
CDeterministicMNCPtr dmn;
@@ -252,13 +261,15 @@ std::optional<ProviderTxError> ApplyNetInfo(ProTx& payload, const ProviderNetInf
252261
}
253262

254263
template <typename ProTx>
255-
std::optional<ProviderTxError> ValidateNetworkFields(const ProTx& payload, bool allow_empty, bool check_platform_node_id)
264+
std::optional<ProviderTxError> ValidateNetworkFields(const ProTx& payload, bool allow_empty,
265+
bool check_platform_node_id, bool ipv4_optional)
256266
{
257267
TxValidationState state;
258268
const uint160* platform_node_id{check_platform_node_id && payload.nType == MnType::Evo ? &payload.platformNodeID
259269
: nullptr};
260270
if (!CheckProviderNetworkFields(payload.netInfo, payload.nType, payload.nVersion, platform_node_id,
261-
payload.platformP2PPort, payload.platformHTTPPort, allow_empty, state)) {
271+
payload.platformP2PPort, payload.platformHTTPPort, allow_empty, ipv4_optional,
272+
state)) {
262273
return Error(ProviderTxErrorCode::CONSENSUS_REJECTED, state.ToString(), state.GetRejectReason());
263274
}
264275
return std::nullopt;
@@ -535,8 +546,8 @@ RegistrationResult BuildRegistration(node::NodeContext& node, Wallet& wallet,
535546
}
536547

537548
if (auto error{ApplyPlatformNodeId(payload, request.platform_node_id)}) return *error;
538-
if (auto error{ValidateNetworkFields(payload, /*allow_empty=*/true,
539-
/*check_platform_node_id=*/true)}) {
549+
if (auto error{ValidateNetworkFields(payload, /*allow_empty=*/true, /*check_platform_node_id=*/true,
550+
IPv4OptionalActive(node))}) {
540551
return *error;
541552
}
542553

@@ -800,7 +811,8 @@ interfaces::ProviderTxCapabilities GetCapabilities(node::NodeContext& node, std:
800811
return interfaces::ProviderTxCapabilities{version, version >= ProTxVersion::ExtAddr};
801812
}
802813

803-
std::optional<ProviderTxError> ValidateNetInfo(const ProviderNetInfo& net_info, MnType type, uint16_t version, bool optional)
814+
std::optional<ProviderTxError> ValidateNetInfo(node::NodeContext& node, const ProviderNetInfo& net_info, MnType type,
815+
uint16_t version, bool optional)
804816
{
805817
if (!IsValidMnType(type)) {
806818
return Error(ProviderTxErrorCode::INVALID_PARAMETER, "invalid masternode type");
@@ -814,7 +826,11 @@ std::optional<ProviderTxError> ValidateNetInfo(const ProviderNetInfo& net_info,
814826
payload.nVersion = version;
815827
payload.netInfo = NetInfoInterface::MakeNetInfo(version);
816828
if (auto error{ApplyNetInfo(payload, net_info, type == MnType::Evo, optional)}) return error;
817-
return ValidateNetworkFields(payload, optional, /*check_platform_node_id=*/false);
829+
// Without a chain, as in GUI tests, use the rules for the first block
830+
const bool ipv4_optional{node.chainman ? IPv4OptionalActive(node)
831+
: DeploymentActiveAfter(nullptr, Params().GetConsensus(),
832+
Consensus::DEPLOYMENT_IPV4_OPTIONAL)};
833+
return ValidateNetworkFields(payload, optional, /*check_platform_node_id=*/false, ipv4_optional);
818834
}
819835

820836
ProviderTxResult<ProviderTxSubmission> Register(node::NodeContext& node, Wallet& wallet,
@@ -900,8 +916,8 @@ ProviderTxResult<ProviderTxSubmission> UpdateService(node::NodeContext& node, Wa
900916
return *error;
901917
}
902918
if (auto error{ApplyPlatformNodeId(payload, request.platform_node_id)}) return *error;
903-
if (auto error{ValidateNetworkFields(payload, /*allow_empty=*/false,
904-
/*check_platform_node_id=*/true)}) {
919+
if (auto error{ValidateNetworkFields(payload, /*allow_empty=*/false, /*check_platform_node_id=*/true,
920+
IPv4OptionalActive(node))}) {
905921
return *error;
906922
}
907923
payload.scriptOperatorPayout = request.operator_payout ? GetScriptForDestination(*request.operator_payout)
@@ -1107,7 +1123,8 @@ ProviderTxResult<PreparedSharedRegistration> PrepareSharedRegistration(node::Nod
11071123
return *error;
11081124
}
11091125
if (auto error{ApplyPlatformNodeId(payload, request.platform_node_id)}) return *error;
1110-
if (auto error{ValidateNetworkFields(payload, /*allow_empty=*/true, /*check_platform_node_id=*/true)}) {
1126+
if (auto error{ValidateNetworkFields(payload, /*allow_empty=*/true, /*check_platform_node_id=*/true,
1127+
IPv4OptionalActive(node))}) {
11111128
return *error;
11121129
}
11131130

‎src/evo/providertx_service.h‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ namespace evo::provider {
2222
interfaces::ProviderTxCapabilities GetCapabilities(node::NodeContext& node,
2323
std::optional<bool> basic_override = std::nullopt);
2424

25-
std::optional<interfaces::ProviderTxError> ValidateNetInfo(const interfaces::ProviderNetInfo& net_info, MnType type,
25+
std::optional<interfaces::ProviderTxError> ValidateNetInfo(node::NodeContext& node,
26+
const interfaces::ProviderNetInfo& net_info, MnType type,
2627
uint16_t version, bool optional);
2728

2829
interfaces::ProviderTxResult<interfaces::ProviderTxSubmission> Register(

0 commit comments

Comments
 (0)