ant_service_management/node/
node_service_data.rs

1// Copyright (C) 2024 MaidSafe.net limited.
2//
3// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3.
4// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed
5// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
6// KIND, either express or implied. Please review the Licences for the specific language governing
7// permissions and limitations relating to use of the SAFE Network Software.
8
9use crate::error::Result;
10use ant_protocol::get_port_from_multiaddr;
11use libp2p::PeerId;
12use serde::{Deserialize, Deserializer, Serializer, de::Error as DeError};
13use std::str::FromStr;
14
15/// Type alias for the latest version of the node service data structure.
16pub type NodeServiceData = super::node_service_data_v2::NodeServiceDataV2;
17/// Type alias for the latest node service data schema version.
18pub const NODE_SERVICE_DATA_SCHEMA_LATEST: u32 =
19    super::node_service_data_v2::NODE_SERVICE_DATA_SCHEMA_V2;
20
21/// Custom deserialization for NodeServiceData.
22/// This will perform conversion from V0 or V1 to V2 if needed.
23impl<'de> Deserialize<'de> for NodeServiceData {
24    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
25    where
26        D: Deserializer<'de>,
27    {
28        let json_value = serde_json::Value::deserialize(deserializer)?;
29
30        let schema_version = match &json_value {
31            serde_json::Value::Object(obj) => obj.get("schema_version").and_then(|v| v.as_u64()),
32            _ => None,
33        };
34
35        match schema_version {
36            Some(2) => {
37                match super::node_service_data_v2::NodeServiceDataV2::deserialize_v2(
38                    &mut serde_json::de::Deserializer::from_str(&json_value.to_string()),
39                ) {
40                    Ok(v2) => Ok(v2),
41                    Err(e) => Err(D::Error::custom(format!(
42                        "Failed to deserialize as V2: {e}"
43                    ))),
44                }
45            }
46            Some(1) => {
47                match serde_json::from_value::<super::node_service_data_v1::NodeServiceDataV1>(
48                    json_value,
49                ) {
50                    Ok(v1) => {
51                        let v2: super::node_service_data_v2::NodeServiceDataV2 = v1.into();
52                        Ok(v2)
53                    }
54                    Err(e) => Err(D::Error::custom(format!(
55                        "Failed to deserialize as V1: {e}"
56                    ))),
57                }
58            }
59            _ => {
60                match serde_json::from_value::<super::node_service_data_v0::NodeServiceDataV0>(
61                    json_value,
62                ) {
63                    Ok(v0) => {
64                        let v1: super::node_service_data_v1::NodeServiceDataV1 = v0.into();
65                        let v2: super::node_service_data_v2::NodeServiceDataV2 = v1.into();
66                        Ok(v2)
67                    }
68                    Err(e) => Err(D::Error::custom(format!(
69                        "Failed to deserialize as V0: {e}"
70                    ))),
71                }
72            }
73        }
74    }
75}
76
77impl NodeServiceData {
78    /// Returns the UDP port from our node's listen address.
79    pub fn get_antnode_port(&self) -> Option<u16> {
80        // assuming the listening addr contains /ip4/127.0.0.1/udp/56215/quic-v1/p2p/<peer_id>
81        if let Some(multi_addrs) = &self.listen_addr {
82            println!("Listening addresses are defined");
83            for addr in multi_addrs {
84                if let Some(port) = get_port_from_multiaddr(addr) {
85                    println!("Found port: {port}");
86                    return Some(port);
87                }
88            }
89        }
90        None
91    }
92
93    /// Returns an optional critical failure of the node.
94    pub fn get_critical_failure(&self) -> Option<(chrono::DateTime<chrono::Utc>, String)> {
95        const CRITICAL_FAILURE_LOG_FILE: &str = "critical_failure.log";
96
97        let log_path = self.log_dir_path.join(CRITICAL_FAILURE_LOG_FILE);
98
99        if let Ok(content) = std::fs::read_to_string(log_path)
100            && let Some((timestamp, message)) = content.split_once(']')
101        {
102            let timestamp_trimmed = timestamp.trim_start_matches('[').trim();
103            if let Ok(datetime) = timestamp_trimmed.parse::<chrono::DateTime<chrono::Utc>>() {
104                let message_trimmed = message
105                    .trim()
106                    .trim_start_matches("Node terminated due to: ");
107                return Some((datetime, message_trimmed.to_string()));
108            }
109        }
110
111        None
112    }
113
114    pub fn serialize_peer_id<S>(value: &Option<PeerId>, serializer: S) -> Result<S::Ok, S::Error>
115    where
116        S: Serializer,
117    {
118        if let Some(peer_id) = value {
119            return serializer.serialize_str(&peer_id.to_string());
120        }
121        serializer.serialize_none()
122    }
123
124    pub fn deserialize_peer_id<'de, D>(deserializer: D) -> Result<Option<PeerId>, D::Error>
125    where
126        D: Deserializer<'de>,
127    {
128        let s: Option<String> = Option::deserialize(deserializer)?;
129        if let Some(peer_id_str) = s {
130            PeerId::from_str(&peer_id_str)
131                .map(Some)
132                .map_err(DeError::custom)
133        } else {
134            Ok(None)
135        }
136    }
137
138    pub fn serialize_connected_peers<S>(
139        connected_peers: &Option<Vec<PeerId>>,
140        serializer: S,
141    ) -> Result<S::Ok, S::Error>
142    where
143        S: Serializer,
144    {
145        match connected_peers {
146            Some(peers) => {
147                let peer_strs: Vec<String> = peers.iter().map(|p| p.to_string()).collect();
148                serializer.serialize_some(&peer_strs)
149            }
150            None => serializer.serialize_none(),
151        }
152    }
153
154    pub fn deserialize_connected_peers<'de, D>(
155        deserializer: D,
156    ) -> Result<Option<Vec<PeerId>>, D::Error>
157    where
158        D: Deserializer<'de>,
159    {
160        let vec: Option<Vec<String>> = Option::deserialize(deserializer)?;
161        match vec {
162            Some(peer_strs) => {
163                let peers: Result<Vec<PeerId>, _> = peer_strs
164                    .into_iter()
165                    .map(|s| PeerId::from_str(&s).map_err(DeError::custom))
166                    .collect();
167                peers.map(Some)
168            }
169            None => Ok(None),
170        }
171    }
172}
173
174#[cfg(test)]
175mod tests {
176    use std::{
177        net::{Ipv4Addr, SocketAddr},
178        path::PathBuf,
179    };
180
181    use ant_bootstrap::InitialPeersConfig;
182    use ant_evm::{AttoTokens, EvmNetwork, RewardsAddress};
183    use ant_logging::LogFormat;
184    use libp2p::Multiaddr;
185    use serde::Serialize;
186
187    use super::*;
188    use crate::{ServiceStatus, node::node_service_data_v1::NODE_SERVICE_DATA_SCHEMA_V1};
189
190    /// Test to confirm that fields can be removed from the schema without breaking deserialization.
191    /// This test checks that the `disable_mainnet_contacts` field can be removed without requiring
192    /// additional logic in the deserialization process.
193    ///
194    /// Also adding a dummy field `dummy_field` to ensure that the deserialization
195    /// process does not fail when encountering fields that are not defined in the current schema.
196    #[test]
197    fn fields_can_be_removed_without_breaking() {
198        let json_with_deprecated_field = serde_json::json!({
199            "dummy_field": "This field is not used in the v1 schema",
200            "schema_version": NODE_SERVICE_DATA_SCHEMA_V1,
201            "antnode_path": "/usr/bin/antnode",
202            "auto_restart": true,
203            "connected_peers": [
204                "12D3KooWDpJ7As7BWAwRMfu1VU2WCqNjvq387JEYKDBj4kx6nXTN"
205            ],
206            "data_dir_path": "/home/user/.local/share/safe/node/1",
207            "evm_network": "ArbitrumSepoliaTest",
208            "initial_peers_config": {
209                "first": false,
210                "local": false,
211                "addrs": [],
212                "network_contacts_url": [],
213                "disable_mainnet_contacts": false,
214                "ignore_cache": false,
215                "bootstrap_cache_dir": null
216            },
217            "listen_addr": [
218                "/ip4/127.0.0.1/udp/56215/quic-v1/p2p/12D3KooWDpJ7As7BWAwRMfu1VU2WCqNjvq387JEYKDBj4kx6nXTN"
219            ],
220            "log_dir_path": "/home/user/.local/share/safe/node/1/logs",
221            "log_format": "Default",
222            "max_archived_log_files": 5,
223            "max_log_files": 10,
224            "metrics_port": 8080,
225            "network_id": 1,
226            "node_ip": "127.0.0.1",
227            "node_port": 56215,
228            "no_upnp": false,
229            "number": 1,
230            "peer_id": "12D3KooWDpJ7As7BWAwRMfu1VU2WCqNjvq387JEYKDBj4kx6nXTN",
231            "pid": 12345,
232            "relay": true,
233            "rewards_address": "0x1234567890123456789012345678901234567890",
234            "reward_balance": "1000000000000000000",
235            "rpc_socket_addr": "127.0.0.1:8000",
236            "service_name": "safenode-1",
237            "status": "Running",
238            "user": "safe",
239            "user_mode": true,
240            "version": "0.1.0"
241        });
242
243        let service_data: Result<NodeServiceData, _> =
244            serde_json::from_value(json_with_deprecated_field);
245
246        assert!(
247            service_data.is_ok(),
248            "Failed to deserialize data with deprecated field 'disable_mainnet_contacts': {:?}",
249            service_data.err()
250        );
251
252        let data = service_data.unwrap();
253
254        assert_eq!(data.schema_version, NODE_SERVICE_DATA_SCHEMA_LATEST);
255        assert_eq!(data.service_name, "safenode-1");
256        assert_eq!(data.node_port, Some(56215));
257
258        assert!(!data.initial_peers_config.first);
259        assert!(!data.initial_peers_config.local);
260        assert!(data.initial_peers_config.addrs.is_empty());
261        assert!(data.initial_peers_config.network_contacts_url.is_empty());
262        assert!(!data.initial_peers_config.ignore_cache);
263        assert!(data.initial_peers_config.bootstrap_cache_dir.is_none());
264    }
265
266    /// Test to confirm that fields can be added to the schema without breaking deserialization IF `serde(default)` is
267    /// used.
268    /// This test checks that the `dummy_addition` field can be added without requiring
269    /// additional logic in the deserialization process.
270    #[test]
271    fn fields_can_be_added_without_breaking_with_serde_default() {
272        #[derive(Clone, Debug, Serialize, Deserialize)]
273        pub struct NodeServiceDataTest {
274            #[serde(default)]
275            pub dummy_addition: String, // New field with serde(default)
276            pub schema_version: u32,
277            pub antnode_path: PathBuf,
278            #[serde(default)]
279            pub auto_restart: bool,
280            #[serde(serialize_with = "NodeServiceData::serialize_connected_peers")]
281            pub connected_peers: Option<Vec<PeerId>>,
282            pub data_dir_path: PathBuf,
283            #[serde(default)]
284            pub evm_network: EvmNetwork,
285            pub initial_peers_config: InitialPeersConfig,
286            pub listen_addr: Option<Vec<Multiaddr>>,
287            pub log_dir_path: PathBuf,
288            pub log_format: Option<LogFormat>,
289            pub max_archived_log_files: Option<usize>,
290            pub max_log_files: Option<usize>,
291            #[serde(default)]
292            pub metrics_port: Option<u16>,
293            pub network_id: Option<u8>,
294            #[serde(default)]
295            pub node_ip: Option<Ipv4Addr>,
296            #[serde(default)]
297            pub node_port: Option<u16>,
298            pub no_upnp: bool,
299            pub number: u16,
300            #[serde(serialize_with = "NodeServiceData::serialize_peer_id")]
301            pub peer_id: Option<PeerId>,
302            pub pid: Option<u32>,
303            pub relay: bool,
304            #[serde(default)]
305            pub rewards_address: RewardsAddress,
306            pub reward_balance: Option<AttoTokens>,
307            pub rpc_socket_addr: SocketAddr,
308            pub service_name: String,
309            pub status: ServiceStatus,
310            pub user: Option<String>,
311            pub user_mode: bool,
312            pub version: String,
313        }
314
315        let json_with_deprecated_field = serde_json::json!({
316            "schema_version": NODE_SERVICE_DATA_SCHEMA_V1,
317            "antnode_path": "/usr/bin/antnode",
318            "auto_restart": true,
319            "connected_peers": [
320                "12D3KooWDpJ7As7BWAwRMfu1VU2WCqNjvq387JEYKDBj4kx6nXTN"
321            ],
322            "data_dir_path": "/home/user/.local/share/safe/node/1",
323            "evm_network": "ArbitrumSepoliaTest",
324            "initial_peers_config": {
325                "first": false,
326                "local": false,
327                "addrs": [],
328                "network_contacts_url": [],
329                "ignore_cache": false,
330                "bootstrap_cache_dir": null
331            },
332            "listen_addr": [
333                "/ip4/127.0.0.1/udp/56215/quic-v1/p2p/12D3KooWDpJ7As7BWAwRMfu1VU2WCqNjvq387JEYKDBj4kx6nXTN"
334            ],
335            "log_dir_path": "/home/user/.local/share/safe/node/1/logs",
336            "log_format": "Default",
337            "max_archived_log_files": 5,
338            "max_log_files": 10,
339            "metrics_port": 8080,
340            "network_id": 1,
341            "node_ip": "127.0.0.1",
342            "node_port": 56215,
343            "no_upnp": false,
344            "number": 1,
345            "peer_id": "12D3KooWDpJ7As7BWAwRMfu1VU2WCqNjvq387JEYKDBj4kx6nXTN",
346            "pid": 12345,
347            "relay": true,
348            "rewards_address": "0x1234567890123456789012345678901234567890",
349            "reward_balance": "1000000000000000000",
350            "rpc_socket_addr": "127.0.0.1:8000",
351            "service_name": "safenode-1",
352            "status": "Running",
353            "user": "safe",
354            "user_mode": true,
355            "version": "0.1.0"
356        });
357
358        let service_data: Result<NodeServiceDataTest, _> =
359            serde_json::from_value(json_with_deprecated_field);
360        assert!(
361            service_data.is_ok(),
362            "Failed to deserialize data with new field 'dummy_addition': {:?}",
363            service_data.err()
364        );
365    }
366
367    /// Test to confirm that a new field without `serde(default)` can break the deserialization.
368    ///
369    /// This test checks that the `dummy_addition` field will cause a deserialization error
370    /// if it is not marked with `serde(default)`.
371    #[test]
372    fn fields_cannot_be_added_without_serde_default() {
373        #[derive(Clone, Debug, Serialize, Deserialize)]
374        pub struct NodeServiceDataTest {
375            pub dummy_addition: String, // New field without serde(default)
376            pub schema_version: u32,
377            pub antnode_path: PathBuf,
378            #[serde(default)]
379            pub auto_restart: bool,
380            #[serde(serialize_with = "NodeServiceData::serialize_connected_peers")]
381            pub connected_peers: Option<Vec<PeerId>>,
382            pub data_dir_path: PathBuf,
383            #[serde(default)]
384            pub evm_network: EvmNetwork,
385            pub initial_peers_config: InitialPeersConfig,
386            pub listen_addr: Option<Vec<Multiaddr>>,
387            pub log_dir_path: PathBuf,
388            pub log_format: Option<LogFormat>,
389            pub max_archived_log_files: Option<usize>,
390            pub max_log_files: Option<usize>,
391            #[serde(default)]
392            pub metrics_port: Option<u16>,
393            pub network_id: Option<u8>,
394            #[serde(default)]
395            pub node_ip: Option<Ipv4Addr>,
396            #[serde(default)]
397            pub node_port: Option<u16>,
398            pub no_upnp: bool,
399            pub number: u16,
400            #[serde(serialize_with = "NodeServiceData::serialize_peer_id")]
401            pub peer_id: Option<PeerId>,
402            pub pid: Option<u32>,
403            pub relay: bool,
404            #[serde(default)]
405            pub rewards_address: RewardsAddress,
406            pub reward_balance: Option<AttoTokens>,
407            pub rpc_socket_addr: SocketAddr,
408            pub service_name: String,
409            pub status: ServiceStatus,
410            pub user: Option<String>,
411            pub user_mode: bool,
412            pub version: String,
413        }
414
415        let json_with_deprecated_field = serde_json::json!({
416            "schema_version": NODE_SERVICE_DATA_SCHEMA_V1,
417            "antnode_path": "/usr/bin/antnode",
418            "auto_restart": true,
419            "connected_peers": [
420                "12D3KooWDpJ7As7BWAwRMfu1VU2WCqNjvq387JEYKDBj4kx6nXTN"
421            ],
422            "data_dir_path": "/home/user/.local/share/safe/node/1",
423            "evm_network": "ArbitrumSepoliaTest",
424            "initial_peers_config": {
425                "first": false,
426                "local": false,
427                "addrs": [],
428                "network_contacts_url": [],
429                "ignore_cache": false,
430                "bootstrap_cache_dir": null
431            },
432            "listen_addr": [
433                "/ip4/127.0.0.1/udp/56215/quic-v1/p2p/12D3KooWDpJ7As7BWAwRMfu1VU2WCqNjvq387JEYKDBj4kx6nXTN"
434            ],
435            "log_dir_path": "/home/user/.local/share/safe/node/1/logs",
436            "log_format": "Default",
437            "max_archived_log_files": 5,
438            "max_log_files": 10,
439            "metrics_port": 8080,
440            "network_id": 1,
441            "node_ip": "127.0.0.1",
442            "node_port": 56215,
443            "no_upnp": false,
444            "number": 1,
445            "peer_id": "12D3KooWDpJ7As7BWAwRMfu1VU2WCqNjvq387JEYKDBj4kx6nXTN",
446            "pid": 12345,
447            "relay": true,
448            "rewards_address": "0x1234567890123456789012345678901234567890",
449            "reward_balance": "1000000000000000000",
450            "rpc_socket_addr": "127.0.0.1:8000",
451            "service_name": "safenode-1",
452            "status": "Running",
453            "user": "safe",
454            "user_mode": true,
455            "version": "0.1.0"
456        });
457
458        let service_data: Result<NodeServiceDataTest, _> =
459            serde_json::from_value(json_with_deprecated_field);
460        assert!(
461            service_data.is_err(),
462            "We should not get an Ok().Deserialization should fail without serde default",
463        );
464    }
465
466    #[test]
467    fn enum_variants_can_be_added_without_breaking() {
468        #[derive(Clone, Debug, Serialize, Deserialize)]
469        pub enum TestEnum1 {
470            Variant1,
471            Variant2,
472        }
473
474        #[derive(Clone, Debug, Serialize, Deserialize)]
475        pub enum TestEnum2 {
476            Variant1,
477            Variant2,
478            Variant3,
479        }
480
481        let enum1 = TestEnum1::Variant1;
482        let enum1_json = serde_json::to_value(&enum1).unwrap();
483
484        let enum2: TestEnum2 = serde_json::from_value(enum1_json).unwrap();
485        assert!(matches!(enum2, TestEnum2::Variant1));
486    }
487
488    #[test]
489    fn enum_variants_cannot_be_removed_without_breaking() {
490        #[derive(Clone, Debug, Serialize, Deserialize)]
491        pub enum TestEnum1 {
492            Variant1,
493            Variant2,
494        }
495
496        #[derive(Clone, Debug, Serialize, Deserialize)]
497        pub enum TestEnum2 {
498            Variant1,
499            Variant2,
500            Variant3,
501        }
502
503        let enum2 = TestEnum2::Variant3;
504        let enum2_json = serde_json::to_value(&enum2).unwrap();
505
506        let result: Result<TestEnum1, _> = serde_json::from_value(enum2_json);
507        assert!(
508            result.is_err(),
509            "Deserialization should fail when removing variants"
510        );
511    }
512}