diff --git a/rpc-json/src/lib.rs b/rpc-json/src/lib.rs index 5d4e7015e..6416f2857 100644 --- a/rpc-json/src/lib.rs +++ b/rpc-json/src/lib.rs @@ -2052,51 +2052,6 @@ pub struct GetMasternodePaymentsResult { pub masternodes: Vec, } -/// Nested `addresses` object on Core 23+ masternode entries. -/// -/// Each purpose maps to an array of `"host:port"` strings. Dash Core 23 moved the -/// platform ports here, away from the deprecated top-level `platformP2PPort`/ -/// `platformHTTPPort` keys. Unknown purposes are ignored. -#[derive(Clone, PartialEq, Eq, Debug, Default, Deserialize, Serialize)] -pub struct MasternodeAddresses { - #[serde(default)] - pub core_p2p: Vec, - #[serde(default)] - pub platform_p2p: Vec, - #[serde(default)] - pub platform_https: Vec, -} - -impl MasternodeAddresses { - /// First valid `(host, port)` from a `"host:port"` array, if any. - /// - /// "Valid" means parseable and with a non-zero in-range port; zero ports are - /// skipped because Dash Core uses `0` as a "not set" sentinel. - fn first_valid_host_port(addrs: &[String]) -> Option<(String, u32)> { - addrs.iter().filter_map(|a| parse_host_port(a)).find(|(_, p)| *p != 0) - } -} - -/// Splits a `"host:port"` string into `(host, port)` with a non-fabricated host. -/// -/// Supports IPv4 (`1.2.3.4:9999`) and bracketed IPv6 (`[2001:db8::1]:9999`). The -/// final `:port` segment is parsed through [`u16`] then widened to `u32`, rejecting -/// values outside the TCP/UDP port range (e.g. `"host:70000"`). Unbracketed -/// multi-colon hosts (bare IPv6) are rejected rather than silently mangled. An empty -/// host (e.g. `":36656"`) is also rejected. Returns `None` when no colon is present, -/// the host is empty or ambiguous, or the suffix is not a valid `u16`. -fn parse_host_port(addr: &str) -> Option<(String, u32)> { - let (host, port) = addr.rsplit_once(':')?; - if host.is_empty() { - return None; - } - if host.contains(':') && !(host.starts_with('[') && host.ends_with(']')) { - return None; - } - let port = port.parse::().ok()?; - Some((host.to_string(), u32::from(port))) -} - #[serde_as] #[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] @@ -2125,54 +2080,10 @@ pub struct DMNState { rename = "platformNodeID" )] pub platform_node_id: Option<[u8; 20]>, - #[deprecated(note = "Core 23+ nested addresses.platform_p2p should be used instead")] #[serde(default, rename = "platformP2PPort")] - pub legacy_platform_p2p_port: Option, - #[deprecated(note = "Core 23+ nested addresses.platform_https should be used instead")] + pub platform_p2p_port: Option, #[serde(default, rename = "platformHTTPPort")] - pub legacy_platform_http_port: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub addresses: Option, -} - -impl DMNState { - /// Resolved platform P2P `(host, port)`. - /// - /// Prefers the Core 23+ nested `addresses.platform_p2p` entry, returning its - /// host and port verbatim. Falls back to the deprecated top-level - /// `platformP2PPort`, pairing it with the node IP from [`service`](Self::service) - /// because Dash deploys platform services on the masternode's core IP. Returns - /// `None` when no source yields a non-zero in-range port. - #[allow(deprecated)] - pub fn platform_p2p_address(&self) -> Option<(String, u32)> { - self.addresses - .as_ref() - .and_then(|a| MasternodeAddresses::first_valid_host_port(&a.platform_p2p)) - .or_else(|| self.legacy_platform_address(self.legacy_platform_p2p_port)) - } - - /// Resolved platform HTTPS `(host, port)`. - /// - /// Prefers the Core 23+ nested `addresses.platform_https` entry, returning its - /// host and port verbatim. Falls back to the deprecated top-level - /// `platformHTTPPort`, pairing it with the node IP from [`service`](Self::service) - /// because Dash deploys platform services on the masternode's core IP. Returns - /// `None` when no source yields a non-zero in-range port. - #[allow(deprecated)] - pub fn platform_http_address(&self) -> Option<(String, u32)> { - self.addresses - .as_ref() - .and_then(|a| MasternodeAddresses::first_valid_host_port(&a.platform_https)) - .or_else(|| self.legacy_platform_address(self.legacy_platform_http_port)) - } - - /// Pairs a legacy platform port with the node IP, dropping zero, absent, and - /// out-of-`u16`-range ports so the result honors the TCP/UDP port range. - fn legacy_platform_address(&self, port: Option) -> Option<(String, u32)> { - port.and_then(|p| u16::try_from(p).ok()) - .filter(|&p| p != 0) - .map(|p| (self.service.ip().to_string(), u32::from(p))) - } + pub platform_http_port: Option, } #[derive(Clone, PartialEq, Eq, Debug, Deserialize)] @@ -2192,19 +2103,13 @@ pub struct DMNStateDiff { pub pub_key_operator: Option>, pub operator_payout_address: Option>, pub platform_node_id: Option<[u8; 20]>, - #[deprecated(note = "Core 23+ nested addresses.platform_p2p should be used instead")] - pub legacy_platform_p2p_port: Option, - #[deprecated(note = "Core 23+ nested addresses.platform_https should be used instead")] - pub legacy_platform_http_port: Option, - /// Three-state nested addresses: `None` = unchanged, `Some(None)` = cleared, - /// `Some(Some(_))` = set. Mirrors [`pose_ban_height`](Self::pose_ban_height). - pub addresses: Option>, + pub platform_p2p_port: Option, + pub platform_http_port: Option, } impl TryFrom for DMNStateDiff { type Error = encode::Error; - #[allow(deprecated)] fn try_from(value: DMNStateDiffIntermediate) -> Result { let DMNStateDiffIntermediate { service, @@ -2218,11 +2123,10 @@ impl TryFrom for DMNStateDiff { owner_address, voting_address, platform_node_id, - legacy_platform_p2p_port, - legacy_platform_http_port, + platform_p2p_port, + platform_http_port, payout_address, pub_key_operator, - addresses, } = value; let owner_address = owner_address @@ -2284,43 +2188,12 @@ impl TryFrom for DMNStateDiff { pub_key_operator, operator_payout_address, platform_node_id, - #[allow(deprecated)] - legacy_platform_p2p_port, - #[allow(deprecated)] - legacy_platform_http_port, - addresses, + platform_p2p_port, + platform_http_port, }) } } -impl DMNStateDiff { - /// Resolved platform P2P `(host, port)` carried by this diff, if any. - /// - /// Returns the host and port from the Core 23+ nested `addresses.platform_p2p` - /// entry. The legacy top-level `platformP2PPort` is not resolved here: a diff - /// carries no node IP to pair it with, so a host would have to be fabricated. - /// Read the legacy port via [`legacy_platform_p2p_port`](Self::legacy_platform_p2p_port). - pub fn platform_p2p_address(&self) -> Option<(String, u32)> { - self.addresses - .as_ref() - .and_then(|a| a.as_ref()) - .and_then(|a| MasternodeAddresses::first_valid_host_port(&a.platform_p2p)) - } - - /// Resolved platform HTTPS `(host, port)` carried by this diff, if any. - /// - /// Returns the host and port from the Core 23+ nested `addresses.platform_https` - /// entry. The legacy top-level `platformHTTPPort` is not resolved here: a diff - /// carries no node IP to pair it with, so a host would have to be fabricated. - /// Read the legacy port via [`legacy_platform_http_port`](Self::legacy_platform_http_port). - pub fn platform_http_address(&self) -> Option<(String, u32)> { - self.addresses - .as_ref() - .and_then(|a| a.as_ref()) - .and_then(|a| MasternodeAddresses::first_valid_host_port(&a.platform_https)) - } -} - impl DMNState { pub fn compare_to_older_dmn_state(&self, older: &DMNState) -> Option { older.compare_to_newer_dmn_state(self) @@ -2399,27 +2272,15 @@ impl DMNState { } else { None }, - #[allow(deprecated)] - legacy_platform_p2p_port: if self.legacy_platform_p2p_port - != newer.legacy_platform_p2p_port - { - has_diff = true; - newer.legacy_platform_p2p_port - } else { - None - }, - #[allow(deprecated)] - legacy_platform_http_port: if self.legacy_platform_http_port - != newer.legacy_platform_http_port - { + platform_p2p_port: if self.platform_p2p_port != newer.platform_p2p_port { has_diff = true; - newer.legacy_platform_http_port + newer.platform_p2p_port } else { None }, - addresses: if self.addresses != newer.addresses { + platform_http_port: if self.platform_http_port != newer.platform_http_port { has_diff = true; - Some(newer.addresses.clone()) + newer.platform_http_port } else { None }, @@ -2443,11 +2304,8 @@ impl DMNState { pub_key_operator, operator_payout_address, platform_node_id, - #[allow(deprecated)] - legacy_platform_p2p_port, - #[allow(deprecated)] - legacy_platform_http_port, - addresses, + platform_p2p_port, + platform_http_port, .. } = diff; self.pose_revived_height = pose_revived_height; @@ -2480,18 +2338,12 @@ impl DMNState { self.platform_node_id = Some(platform_node_id); } - #[allow(deprecated)] - if let Some(legacy_platform_p2p_port) = legacy_platform_p2p_port { - self.legacy_platform_p2p_port = Some(legacy_platform_p2p_port); - } - - #[allow(deprecated)] - if let Some(legacy_platform_http_port) = legacy_platform_http_port { - self.legacy_platform_http_port = Some(legacy_platform_http_port); + if let Some(platform_p2p_port) = platform_p2p_port { + self.platform_p2p_port = Some(platform_p2p_port); } - if let Some(addresses) = addresses { - self.addresses = addresses; + if let Some(platform_http_port) = platform_http_port { + self.platform_http_port = Some(platform_http_port); } } } @@ -3033,19 +2885,14 @@ pub struct DMNStateDiffIntermediate { pub voting_address: Option, #[serde(default, rename = "platformNodeID")] pub platform_node_id: Option, - #[deprecated(note = "Core 23+ nested addresses.platform_p2p should be used instead")] #[serde(default, rename = "platformP2PPort")] - pub legacy_platform_p2p_port: Option, - #[deprecated(note = "Core 23+ nested addresses.platform_https should be used instead")] + pub platform_p2p_port: Option, #[serde(default, rename = "platformHTTPPort")] - pub legacy_platform_http_port: Option, + pub platform_http_port: Option, #[serde(default)] pub payout_address: Option, #[serde(default, deserialize_with = "deserialize_hex_opt")] pub pub_key_operator: Option>, - // Three-state: missing field = None, `null` = Some(None), object = Some(Some(_)). - #[serde(default, deserialize_with = "deserialize_addresses_2opt")] - pub addresses: Option>, } #[derive(Clone, PartialEq, Debug, Deserialize)] @@ -3404,20 +3251,6 @@ where Ok(Some(Some(val as u32))) } -/// Deserializes the present-but-clearable nested `addresses` object. -/// -/// Paired with `#[serde(default)]`, the field absence yields `None` (unchanged), -/// a JSON `null` yields `Some(None)` (cleared), and an object yields -/// `Some(Some(_))` (set) — the canonical serde double-`Option` pattern. -fn deserialize_addresses_2opt<'de, D>( - deserializer: D, -) -> Result>, D::Error> -where - D: Deserializer<'de>, -{ - Ok(Some(Option::::deserialize(deserializer)?)) -} - #[cfg(test)] mod tests { use dashcore::hashes::Hash; @@ -3425,8 +3258,7 @@ mod tests { use serde_json::json; use crate::{ - DMNState, DMNStateDiff, ExtendedQuorumListResult, MasternodeAddresses, MasternodeListDiff, - MnSyncStatus, QuorumType, deserialize_u32_opt, parse_host_port, + ExtendedQuorumListResult, MasternodeListDiff, MnSyncStatus, QuorumType, deserialize_u32_opt, }; #[test] @@ -3603,313 +3435,6 @@ mod tests { ); } - #[test] - #[allow(deprecated)] - fn dmn_state_core23_addresses_resolve_platform_ports() { - // Core 23 entry: legacy platformP2PPort/platformHTTPPort absent, ports live - // in the nested `addresses` object. Raw fields stay None; accessors resolve. - let json = r#"{ - "service": "192.0.2.1:9999", - "registeredHeight": 123456, - "revocationReason": 0, - "ownerAddress": "yPBWCdMRY5PsS3hJzs7csbdWQVRR85yxUz", - "votingAddress": "ySM11LUD65Bi4p1gm68XLkdWc65TBKRzvQ", - "payoutAddress": "yX4Ve7Q8Y4jscV4LZJD8HVCHKyePzR3MhA", - "pubKeyOperator": "8ed3f0c208efbcfc815cbfb94490dc68cf2e29d44dd9f8a91e20e06057aa110d7062c8ab7ccc85a9ff0c88760157f563", - "platformNodeID": "f2dbd9b0a1f541a7c44d34a58674d0262f5feca5", - "addresses": { - "core_p2p": ["192.0.2.1:9999"], - "platform_p2p": ["192.0.2.2:36656"], - "platform_https": ["192.0.2.2:443"] - } - }"#; - let state: DMNState = serde_json::from_str(json).expect("expected to deserialize json"); - assert_eq!(state.legacy_platform_p2p_port, None, "raw legacy field deserialized as-is"); - assert_eq!(state.legacy_platform_http_port, None, "raw legacy field deserialized as-is"); - assert_eq!( - state.platform_p2p_address(), - Some(("192.0.2.2".to_string(), 36656)), - "p2p resolved from addresses" - ); - assert_eq!( - state.platform_http_address(), - Some(("192.0.2.2".to_string(), 443)), - "http resolved from addresses" - ); - } - - #[test] - #[allow(deprecated)] - fn dmn_state_diff_core23_addresses_resolve_platform_ports() { - // updatedMNs entry carrying only the new `addresses` object. - let json = r#"{ - "addresses": { - "platform_p2p": ["192.0.2.2:36656"], - "platform_https": ["192.0.2.2:443"] - } - }"#; - let diff: DMNStateDiff = serde_json::from_str(json).expect("expected to deserialize json"); - assert_eq!(diff.legacy_platform_p2p_port, None, "raw legacy diff field deserialized as-is"); - assert_eq!( - diff.legacy_platform_http_port, None, - "raw legacy diff field deserialized as-is" - ); - assert_eq!( - diff.platform_p2p_address(), - Some(("192.0.2.2".to_string(), 36656)), - "diff p2p resolved from addresses" - ); - assert_eq!( - diff.platform_http_address(), - Some(("192.0.2.2".to_string(), 443)), - "diff http resolved from addresses" - ); - } - - #[test] - fn dmn_state_legacy_platform_ports_resolve_to_node_ip() { - // Pre-23 entry: legacy top-level keys, no `addresses`. Accessors fall back to - // the legacy port paired with the node IP from `service`. - let json = r#"{ - "service": "192.0.2.1:9999", - "registeredHeight": 123456, - "revocationReason": 0, - "ownerAddress": "yPBWCdMRY5PsS3hJzs7csbdWQVRR85yxUz", - "votingAddress": "ySM11LUD65Bi4p1gm68XLkdWc65TBKRzvQ", - "payoutAddress": "yX4Ve7Q8Y4jscV4LZJD8HVCHKyePzR3MhA", - "pubKeyOperator": "8ed3f0c208efbcfc815cbfb94490dc68cf2e29d44dd9f8a91e20e06057aa110d7062c8ab7ccc85a9ff0c88760157f563", - "platformNodeID": "f2dbd9b0a1f541a7c44d34a58674d0262f5feca5", - "platformP2PPort": 26656, - "platformHTTPPort": 443 - }"#; - let state: DMNState = serde_json::from_str(json).expect("expected to deserialize json"); - assert!(state.addresses.is_none(), "no addresses object present"); - assert_eq!( - state.platform_p2p_address(), - Some(("192.0.2.1".to_string(), 26656)), - "p2p resolved from legacy paired with node IP" - ); - assert_eq!( - state.platform_http_address(), - Some(("192.0.2.1".to_string(), 443)), - "http resolved from legacy paired with node IP" - ); - } - - #[test] - #[allow(deprecated)] - fn dmn_state_zero_legacy_port_resolves_to_addresses() { - // Transitional entry: legacy port present but zero -> addresses wins (new-first). - let json = r#"{ - "service": "192.0.2.1:9999", - "registeredHeight": 123456, - "revocationReason": 0, - "ownerAddress": "yPBWCdMRY5PsS3hJzs7csbdWQVRR85yxUz", - "votingAddress": "ySM11LUD65Bi4p1gm68XLkdWc65TBKRzvQ", - "payoutAddress": "yX4Ve7Q8Y4jscV4LZJD8HVCHKyePzR3MhA", - "pubKeyOperator": "8ed3f0c208efbcfc815cbfb94490dc68cf2e29d44dd9f8a91e20e06057aa110d7062c8ab7ccc85a9ff0c88760157f563", - "platformNodeID": "f2dbd9b0a1f541a7c44d34a58674d0262f5feca5", - "platformP2PPort": 0, - "addresses": { - "platform_p2p": ["192.0.2.2:36656"] - } - }"#; - let state: DMNState = serde_json::from_str(json).expect("expected to deserialize json"); - assert_eq!(state.legacy_platform_p2p_port, Some(0), "raw legacy zero deserialized as-is"); - assert_eq!( - state.platform_p2p_address(), - Some(("192.0.2.2".to_string(), 36656)), - "zero legacy yields to addresses" - ); - } - - #[test] - #[allow(deprecated)] - fn dmn_state_zero_legacy_port_no_addresses_resolves_to_none() { - // Legacy port present but zero and no `addresses` -> accessor returns None, - // never `(host, 0)`. - let json = r#"{ - "service": "192.0.2.1:9999", - "registeredHeight": 123456, - "revocationReason": 0, - "ownerAddress": "yPBWCdMRY5PsS3hJzs7csbdWQVRR85yxUz", - "votingAddress": "ySM11LUD65Bi4p1gm68XLkdWc65TBKRzvQ", - "payoutAddress": "yX4Ve7Q8Y4jscV4LZJD8HVCHKyePzR3MhA", - "pubKeyOperator": "8ed3f0c208efbcfc815cbfb94490dc68cf2e29d44dd9f8a91e20e06057aa110d7062c8ab7ccc85a9ff0c88760157f563", - "platformNodeID": "f2dbd9b0a1f541a7c44d34a58674d0262f5feca5", - "platformP2PPort": 0 - }"#; - let state: DMNState = serde_json::from_str(json).expect("expected to deserialize json"); - assert_eq!(state.legacy_platform_p2p_port, Some(0), "raw legacy zero deserialized as-is"); - assert_eq!(state.platform_p2p_address(), None, "zero legacy with no addresses -> None"); - } - - #[test] - fn dmn_state_out_of_range_port_rejected() { - // A port above the u16 range must be rejected rather than truncated/accepted. - let json = r#"{ - "service": "192.0.2.1:9999", - "registeredHeight": 123456, - "revocationReason": 0, - "ownerAddress": "yPBWCdMRY5PsS3hJzs7csbdWQVRR85yxUz", - "votingAddress": "ySM11LUD65Bi4p1gm68XLkdWc65TBKRzvQ", - "payoutAddress": "yX4Ve7Q8Y4jscV4LZJD8HVCHKyePzR3MhA", - "pubKeyOperator": "8ed3f0c208efbcfc815cbfb94490dc68cf2e29d44dd9f8a91e20e06057aa110d7062c8ab7ccc85a9ff0c88760157f563", - "platformNodeID": "f2dbd9b0a1f541a7c44d34a58674d0262f5feca5", - "addresses": { - "platform_p2p": ["192.0.2.2:70000"] - } - }"#; - let state: DMNState = serde_json::from_str(json).expect("expected to deserialize json"); - assert_eq!(state.platform_p2p_address(), None, "out-of-range port rejected"); - } - - #[test] - fn dmn_state_legacy_out_of_range_port_rejected() { - // A legacy port above the u16 range must be rejected, matching the addresses - // path, so the accessor honors its documented in-range invariant. - let json = r#"{ - "service": "192.0.2.1:9999", - "registeredHeight": 123456, - "revocationReason": 0, - "ownerAddress": "yPBWCdMRY5PsS3hJzs7csbdWQVRR85yxUz", - "votingAddress": "ySM11LUD65Bi4p1gm68XLkdWc65TBKRzvQ", - "payoutAddress": "yX4Ve7Q8Y4jscV4LZJD8HVCHKyePzR3MhA", - "pubKeyOperator": "8ed3f0c208efbcfc815cbfb94490dc68cf2e29d44dd9f8a91e20e06057aa110d7062c8ab7ccc85a9ff0c88760157f563", - "platformNodeID": "f2dbd9b0a1f541a7c44d34a58674d0262f5feca5", - "platformP2PPort": 70000 - }"#; - let state: DMNState = serde_json::from_str(json).expect("expected to deserialize json"); - assert_eq!(state.platform_p2p_address(), None, "out-of-range legacy port rejected"); - } - - #[test] - fn dmn_state_no_ports_resolve_to_none() { - // No addresses and no legacy ports -> accessors return None. - let json = r#"{ - "service": "192.0.2.1:9999", - "registeredHeight": 123456, - "revocationReason": 0, - "ownerAddress": "yPBWCdMRY5PsS3hJzs7csbdWQVRR85yxUz", - "votingAddress": "ySM11LUD65Bi4p1gm68XLkdWc65TBKRzvQ", - "payoutAddress": "yX4Ve7Q8Y4jscV4LZJD8HVCHKyePzR3MhA", - "pubKeyOperator": "8ed3f0c208efbcfc815cbfb94490dc68cf2e29d44dd9f8a91e20e06057aa110d7062c8ab7ccc85a9ff0c88760157f563", - "platformNodeID": "f2dbd9b0a1f541a7c44d34a58674d0262f5feca5" - }"#; - let state: DMNState = serde_json::from_str(json).expect("expected to deserialize json"); - assert_eq!(state.platform_p2p_address(), None, "no source -> None"); - assert_eq!(state.platform_http_address(), None, "no source -> None"); - } - - fn dmn_state_with_legacy_p2p_zero() -> DMNState { - let json = r#"{ - "service": "192.0.2.1:9999", - "registeredHeight": 123456, - "revocationReason": 0, - "ownerAddress": "yPBWCdMRY5PsS3hJzs7csbdWQVRR85yxUz", - "votingAddress": "ySM11LUD65Bi4p1gm68XLkdWc65TBKRzvQ", - "payoutAddress": "yX4Ve7Q8Y4jscV4LZJD8HVCHKyePzR3MhA", - "pubKeyOperator": "8ed3f0c208efbcfc815cbfb94490dc68cf2e29d44dd9f8a91e20e06057aa110d7062c8ab7ccc85a9ff0c88760157f563", - "platformNodeID": "f2dbd9b0a1f541a7c44d34a58674d0262f5feca5", - "platformP2PPort": 0 - }"#; - serde_json::from_str(json).expect("expected to deserialize json") - } - - #[test] - fn dmn_state_apply_diff_propagates_addresses() { - // Stored entry has a zero legacy port and no addresses; a diff carrying a - // nested `addresses` object must make the merged state resolvable. - let mut state = dmn_state_with_legacy_p2p_zero(); - assert_eq!(state.platform_p2p_address(), None, "unresolvable before diff"); - - let diff = DMNStateDiff { - service: None, - registered_height: None, - last_paid_height: None, - consecutive_payments: None, - pose_penalty: None, - pose_revived_height: None, - pose_ban_height: None, - revocation_reason: None, - owner_address: None, - voting_address: None, - payout_address: None, - pub_key_operator: None, - operator_payout_address: None, - platform_node_id: None, - #[allow(deprecated)] - legacy_platform_p2p_port: None, - #[allow(deprecated)] - legacy_platform_http_port: None, - addresses: Some(Some(MasternodeAddresses { - core_p2p: vec![], - platform_p2p: vec!["192.0.2.2:36656".to_string()], - platform_https: vec![], - })), - }; - - state.apply_diff(diff); - assert_eq!( - state.platform_p2p_address(), - Some(("192.0.2.2".to_string(), 36656)), - "diff addresses propagated and resolvable" - ); - } - - #[test] - fn dmn_state_diff_clears_addresses() { - // A Some -> None transition must survive the compare/apply round-trip: the - // diff carries `Some(None)` and applying it clears the stored addresses. - let mut newer = dmn_state_with_legacy_p2p_zero(); - newer.addresses = Some(MasternodeAddresses { - core_p2p: vec![], - platform_p2p: vec!["192.0.2.2:36656".to_string()], - platform_https: vec![], - }); - let older = dmn_state_with_legacy_p2p_zero(); - - let diff = - newer.compare_to_newer_dmn_state(&older).expect("addresses change yields a diff"); - assert_eq!(diff.addresses, Some(None), "clear is encoded as Some(None)"); - - let mut applied = newer; - applied.apply_diff(diff); - assert!(applied.addresses.is_none(), "Some(None) diff clears stored addresses"); - } - - #[test] - fn dmn_state_diff_addresses_null_wire_clears() { - // Wire-level three-state: `null` -> Some(None) (clear), absent -> None - // (unchanged). Exercises `deserialize_addresses_2opt` through the intermediate. - let diff: DMNStateDiff = - serde_json::from_str(r#"{"addresses": null}"#).expect("expected to deserialize json"); - assert_eq!(diff.addresses, Some(None), "null wire -> Some(None) (clear)"); - - let diff: DMNStateDiff = - serde_json::from_str(r#"{}"#).expect("expected to deserialize json"); - assert_eq!(diff.addresses, None, "absent wire -> None (unchanged)"); - } - - #[test] - fn parse_host_port_ipv6() { - // Bracketed IPv6 keeps host intact; unbracketed (ambiguous) is rejected. - assert_eq!( - parse_host_port("[2001:db8::1]:9999"), - Some(("[2001:db8::1]".to_string(), 9999)), - "bracketed IPv6 parses host + port" - ); - assert_eq!(parse_host_port("2001:db8::1"), None, "unbracketed IPv6 rejected"); - assert_eq!( - parse_host_port("192.0.2.1:9999"), - Some(("192.0.2.1".to_string(), 9999)), - "IPv4 still parses" - ); - // Empty host must be rejected. - assert_eq!(parse_host_port(":36656"), None, "empty host rejected"); - assert_eq!(parse_host_port(":443"), None, "empty host rejected"); - } - #[test] fn deserialize_mnsync_status() { let json_value = json!({