diff --git a/doc/release-notes-8018.md b/doc/release-notes-8018.md new file mode 100644 index 000000000000..ba245ac17045 --- /dev/null +++ b/doc/release-notes-8018.md @@ -0,0 +1,14 @@ +Consensus changes +----------------- + +- 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/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/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 4dcb7e37af44..a8222b25a61f 100644 --- a/src/rpc/evo.cpp +++ b/src/rpc/evo.cpp @@ -116,6 +116,8 @@ 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.", { {"address", RPCArg::Type::STR, RPCArg::Optional::NO, ""}, @@ -126,7 +128,9 @@ 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" + "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, ""}, }, @@ -246,6 +250,9 @@ 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.", { {"address", RPCArg::Type::STR, RPCArg::Optional::NO, ""}, @@ -257,6 +264,9 @@ 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.", { {"address", RPCArg::Type::STR, RPCArg::Optional::NO, ""}, @@ -268,6 +278,9 @@ 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.", { {"address", RPCArg::Type::STR, RPCArg::Optional::NO, ""}, @@ -279,6 +292,9 @@ 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.", { {"address", RPCArg::Type::STR, RPCArg::Optional::NO, ""}, 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 20b4fe0fa329..99c539f91e34 100644 --- a/src/test/evo_netinfo_tests.cpp +++ b/src/test/evo_netinfo_tests.cpp @@ -5,15 +5,19 @@ #include #include +#include #include #include #include #include +#include +#include #include #include #include #include +#include #include @@ -103,19 +107,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); } } } @@ -164,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"); @@ -192,9 +209,228 @@ 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) 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"}; + 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, 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 + {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"}, + // 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, reject_reason] : primary_vals) { + validation_error = node->evo().validateProviderNetInfo(case_net_info, type, ProTxVersion::ExtAddr, + /*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 && !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 { + 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, + /*ipv4_optional=*/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); +} + +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{ diff --git a/src/test/evo_snapshot_tests.cpp b/src/test/evo_snapshot_tests.cpp index 83b911c98811..8c38d0677ff4 100644 --- a/src/test/evo_snapshot_tests.cpp +++ b/src/test/evo_snapshot_tests.cpp @@ -79,8 +79,11 @@ 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: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 +319,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) 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.