Skip to main content

hiero_sdk/
node_address.rs

1// SPDX-License-Identifier: Apache-2.0
2
3use std::net::SocketAddrV4;
4
5use hiero_sdk_proto::services;
6
7use crate::protobuf::ToProtobuf;
8use crate::{
9    AccountId,
10    Error,
11    FromProtobuf,
12    ServiceEndpoint,
13};
14
15fn parse_socket_addr_v4(ip: Vec<u8>, port: i32) -> crate::Result<SocketAddrV4> {
16    let octets: Result<[u8; 4], _> = ip.try_into();
17    let octets = octets.map_err(|v| {
18        Error::from_protobuf(format!("expected 4 byte ip address, got `{}` bytes", v.len()))
19    })?;
20
21    let port = u16::try_from(port).map_err(|_| {
22        Error::from_protobuf(format!(
23            "expected 16 bit non-negative port number, but the port was actually `{port}`",
24        ))
25    })?;
26
27    Ok(SocketAddrV4::new(octets.into(), port))
28}
29
30/// The data about a node, including its service endpoints and the Hiero account to be paid for
31/// services provided by the node (that is, queries answered and transactions submitted.).
32#[derive(Debug, Clone)]
33pub struct NodeAddress {
34    /// A non-sequential, unique, static identifier for the node
35    pub node_id: u64,
36
37    /// The node's X509 RSA public key used to sign stream files.
38    pub rsa_public_key: Vec<u8>,
39
40    /// The account to be paid for queries and transactions sent to this node.
41    pub node_account_id: AccountId,
42
43    /// Hash of the node's TLS certificate.
44    ///
45    /// Precisely, this field is a string of
46    /// hexadecimal characters which, translated to binary, are the SHA-384 hash of
47    /// the UTF-8 NFKD encoding of the node's TLS cert in PEM format.
48    ///
49    /// Its value can be used to verify the node's certificate it presents during TLS negotiations.
50    pub tls_certificate_hash: Vec<u8>,
51
52    /// A node's service endpoints as strings in format "host:port".
53    pub service_endpoints: Vec<String>,
54
55    /// A description of the node, up to 100 bytes.
56    pub description: String,
57}
58
59impl FromProtobuf<services::NodeAddress> for NodeAddress {
60    fn from_protobuf(pb: services::NodeAddress) -> crate::Result<Self>
61    where
62        Self: Sized,
63    {
64        // sometimes this will be oversized by 1, but that's fine.
65        let mut addresses = Vec::with_capacity(pb.service_endpoint.len() + 1);
66
67        // `ip_address`/`portno` are deprecated, but lets handle them anyway.
68        #[allow(deprecated)]
69        if !pb.ip_address.is_empty() {
70            let socket_addr = parse_socket_addr_v4(pb.ip_address, pb.portno)?;
71            addresses.push(format!("{}:{}", socket_addr.ip(), socket_addr.port()));
72        }
73
74        for address in pb.service_endpoint {
75            let endpoint = ServiceEndpoint::from_protobuf(address)?;
76            let host = match endpoint.ip_address_v4 {
77                Some(ip) => ip.to_string(),
78                None => endpoint.domain_name,
79            };
80            addresses.push(format!("{}:{}", host, endpoint.port));
81        }
82
83        let node_account_id = AccountId::from_protobuf(pb_getf!(pb, node_account_id)?)?;
84
85        Ok(Self {
86            description: pb.description,
87            rsa_public_key: hex::decode(pb.rsa_pub_key).map_err(Error::from_protobuf)?,
88            node_id: pb.node_id as u64,
89            service_endpoints: addresses,
90            tls_certificate_hash: pb.node_cert_hash,
91            node_account_id,
92        })
93    }
94}
95
96impl ToProtobuf for NodeAddress {
97    type Protobuf = services::NodeAddress;
98
99    fn to_protobuf(&self) -> Self::Protobuf {
100        let service_endpoint = self
101            .service_endpoints
102            .iter()
103            .map(|endpoint_str| {
104                // Parse "host:port" format back to ServiceEndpoint
105                let parts: Vec<&str> = endpoint_str.split(':').collect();
106                if parts.len() != 2 {
107                    return services::ServiceEndpoint {
108                        ip_address_v4: Vec::new(),
109                        port: 50211,
110                        domain_name: String::new(),
111                    };
112                }
113
114                let host = parts[0];
115                let port = parts[1].parse::<i32>().unwrap_or(50211);
116
117                // Try to parse as IP address first, otherwise treat as domain name
118                if let Ok(ip) = host.parse::<std::net::Ipv4Addr>() {
119                    services::ServiceEndpoint {
120                        ip_address_v4: ip.octets().to_vec(),
121                        port,
122                        domain_name: String::new(),
123                    }
124                } else {
125                    services::ServiceEndpoint {
126                        ip_address_v4: Vec::new(),
127                        port,
128                        domain_name: host.to_string(),
129                    }
130                }
131            })
132            .collect();
133
134        services::NodeAddress {
135            rsa_pub_key: hex::encode(&self.rsa_public_key),
136            node_id: self.node_id as i64,
137            node_account_id: Some(self.node_account_id.to_protobuf()),
138            node_cert_hash: self.tls_certificate_hash.clone(),
139            service_endpoint,
140            description: self.description.clone(),
141
142            // deprecated fields
143            ..Default::default()
144        }
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    #[test]
153    fn test_node_address_with_ip_endpoints() {
154        let pb = services::NodeAddress {
155            node_id: 3,
156            rsa_pub_key: "746573745f6b6579".to_string(), // hex encoded "test_key"
157            node_account_id: Some(AccountId::new(0, 0, 3).to_protobuf()),
158            node_cert_hash: vec![1, 2, 3, 4],
159            service_endpoint: vec![
160                services::ServiceEndpoint {
161                    ip_address_v4: vec![192, 168, 1, 1],
162                    port: 50211,
163                    domain_name: String::new(),
164                },
165                services::ServiceEndpoint {
166                    ip_address_v4: vec![10, 0, 0, 1],
167                    port: 50211,
168                    domain_name: String::new(),
169                },
170            ],
171            description: "Test node".to_string(),
172            ..Default::default()
173        };
174
175        let node_address = NodeAddress::from_protobuf(pb).unwrap();
176        assert_eq!(node_address.node_id, 3);
177        assert_eq!(node_address.node_account_id, AccountId::new(0, 0, 3));
178        assert_eq!(node_address.service_endpoints, vec!["192.168.1.1:50211", "10.0.0.1:50211"]);
179        assert_eq!(node_address.description, "Test node");
180    }
181
182    #[test]
183    fn test_node_address_with_domain_endpoints() {
184        let pb = services::NodeAddress {
185            node_id: 4,
186            rsa_pub_key: "746573745f6b6579".to_string(), // hex encoded "test_key"
187            node_account_id: Some(AccountId::new(0, 0, 4).to_protobuf()),
188            node_cert_hash: vec![1, 2, 3, 4],
189            service_endpoint: vec![
190                services::ServiceEndpoint {
191                    ip_address_v4: vec![],
192                    port: 50211,
193                    domain_name: "example.com".to_string(),
194                },
195                services::ServiceEndpoint {
196                    ip_address_v4: vec![],
197                    port: 50211,
198                    domain_name: "localhost".to_string(),
199                },
200            ],
201            description: "Test node with domains".to_string(),
202            ..Default::default()
203        };
204
205        let node_address = NodeAddress::from_protobuf(pb).unwrap();
206        assert_eq!(node_address.node_id, 4);
207        assert_eq!(node_address.node_account_id, AccountId::new(0, 0, 4));
208        assert_eq!(node_address.service_endpoints, vec!["example.com:50211", "localhost:50211"]);
209        assert_eq!(node_address.description, "Test node with domains");
210    }
211
212    #[test]
213    fn test_node_address_with_mixed_endpoints() {
214        let pb = services::NodeAddress {
215            node_id: 5,
216            rsa_pub_key: "746573745f6b6579".to_string(), // hex encoded "test_key"
217            node_account_id: Some(AccountId::new(0, 0, 5).to_protobuf()),
218            node_cert_hash: vec![1, 2, 3, 4],
219            service_endpoint: vec![
220                services::ServiceEndpoint {
221                    ip_address_v4: vec![192, 168, 1, 1],
222                    port: 50211,
223                    domain_name: String::new(),
224                },
225                services::ServiceEndpoint {
226                    ip_address_v4: vec![],
227                    port: 50211,
228                    domain_name: "example.com".to_string(),
229                },
230            ],
231            description: "Test node with mixed endpoints".to_string(),
232            ..Default::default()
233        };
234
235        let node_address = NodeAddress::from_protobuf(pb).unwrap();
236        assert_eq!(node_address.node_id, 5);
237        assert_eq!(node_address.node_account_id, AccountId::new(0, 0, 5));
238        assert_eq!(node_address.service_endpoints, vec!["192.168.1.1:50211", "example.com:50211"]);
239        assert_eq!(node_address.description, "Test node with mixed endpoints");
240    }
241
242    #[test]
243    fn test_node_address_to_protobuf() {
244        let node_address = NodeAddress {
245            node_id: 7,
246            rsa_public_key: vec![1, 2, 3, 4],
247            node_account_id: AccountId::new(0, 0, 7),
248            tls_certificate_hash: vec![5, 6, 7, 8],
249            service_endpoints: vec![
250                "192.168.1.1:50211".to_string(),
251                "example.com:50211".to_string(),
252            ],
253            description: "Test node for to_protobuf".to_string(),
254        };
255
256        let pb = node_address.to_protobuf();
257        assert_eq!(pb.node_id, 7);
258        assert_eq!(pb.rsa_pub_key, "01020304");
259        assert_eq!(pb.node_cert_hash, vec![5, 6, 7, 8]);
260        assert_eq!(pb.description, "Test node for to_protobuf");
261        assert_eq!(pb.service_endpoint.len(), 2);
262
263        // Check first endpoint (IP address)
264        assert_eq!(pb.service_endpoint[0].ip_address_v4, vec![192, 168, 1, 1]);
265        assert_eq!(pb.service_endpoint[0].port, 50211);
266        assert_eq!(pb.service_endpoint[0].domain_name, "");
267
268        // Check second endpoint (domain name)
269        assert_eq!(pb.service_endpoint[1].ip_address_v4, vec![] as Vec<u8>);
270        assert_eq!(pb.service_endpoint[1].port, 50211);
271        assert_eq!(pb.service_endpoint[1].domain_name, "example.com");
272    }
273
274    #[test]
275    fn test_node_address_round_trip() {
276        let original = NodeAddress {
277            node_id: 8,
278            rsa_public_key: vec![1, 2, 3, 4],
279            node_account_id: AccountId::new(0, 0, 8),
280            tls_certificate_hash: vec![5, 6, 7, 8],
281            service_endpoints: vec![
282                "192.168.1.1:50211".to_string(),
283                "example.com:50211".to_string(),
284                "localhost:50211".to_string(),
285            ],
286            description: "Test node round trip".to_string(),
287        };
288
289        let pb = original.to_protobuf();
290        let deserialized = NodeAddress::from_protobuf(pb).unwrap();
291
292        assert_eq!(deserialized.node_id, original.node_id);
293        assert_eq!(deserialized.node_account_id, original.node_account_id);
294        assert_eq!(deserialized.service_endpoints, original.service_endpoints);
295        assert_eq!(deserialized.description, original.description);
296    }
297
298    #[test]
299    fn test_node_address_with_invalid_string_format() {
300        let node_address = NodeAddress {
301            node_id: 9,
302            rsa_public_key: vec![1, 2, 3, 4],
303            node_account_id: AccountId::new(0, 0, 9),
304            tls_certificate_hash: vec![5, 6, 7, 8],
305            service_endpoints: vec![
306                "invalid-format".to_string(),           // Missing port
307                "192.168.1.1:invalid-port".to_string(), // Invalid port
308            ],
309            description: "Test node with invalid strings".to_string(),
310        };
311
312        let pb = node_address.to_protobuf();
313        // Should handle gracefully and use defaults
314        assert_eq!(pb.service_endpoint.len(), 2);
315        assert_eq!(pb.service_endpoint[0].port, 50211); // Default port
316        assert_eq!(pb.service_endpoint[1].port, 50211); // Default port
317    }
318
319    #[test]
320    fn test_node_address_with_localhost_and_127_0_0_1() {
321        let pb = services::NodeAddress {
322            node_id: 10,
323            rsa_pub_key: "746573745f6b6579".to_string(), // hex encoded "test_key"
324            node_account_id: Some(AccountId::new(0, 0, 10).to_protobuf()),
325            node_cert_hash: vec![1, 2, 3, 4],
326            service_endpoint: vec![
327                services::ServiceEndpoint {
328                    ip_address_v4: vec![],
329                    port: 50211,
330                    domain_name: "localhost".to_string(),
331                },
332                services::ServiceEndpoint {
333                    ip_address_v4: vec![127, 0, 0, 1],
334                    port: 50211,
335                    domain_name: String::new(),
336                },
337            ],
338            description: "Test node with localhost".to_string(),
339            ..Default::default()
340        };
341
342        let node_address = NodeAddress::from_protobuf(pb).unwrap();
343        assert_eq!(node_address.service_endpoints, vec!["localhost:50211", "127.0.0.1:50211"]);
344    }
345
346    #[test]
347    fn test_node_address_with_kubernetes_style_domain() {
348        let pb = services::NodeAddress {
349            node_id: 11,
350            rsa_pub_key: "746573745f6b6579".to_string(), // hex encoded "test_key"
351            node_account_id: Some(AccountId::new(0, 0, 11).to_protobuf()),
352            node_cert_hash: vec![1, 2, 3, 4],
353            service_endpoint: vec![services::ServiceEndpoint {
354                ip_address_v4: vec![],
355                port: 50211,
356                domain_name: "network-node1-svc.solo-e2e.svc.cluster.local".to_string(),
357            }],
358            description: "Test node with k8s domain".to_string(),
359            ..Default::default()
360        };
361
362        let node_address = NodeAddress::from_protobuf(pb).unwrap();
363        assert_eq!(
364            node_address.service_endpoints,
365            vec!["network-node1-svc.solo-e2e.svc.cluster.local:50211"]
366        );
367    }
368
369    #[test]
370    fn test_node_address_with_different_ports() {
371        let pb = services::NodeAddress {
372            node_id: 12,
373            rsa_pub_key: "746573745f6b6579".to_string(), // hex encoded "test_key"
374            node_account_id: Some(AccountId::new(0, 0, 12).to_protobuf()),
375            node_cert_hash: vec![1, 2, 3, 4],
376            service_endpoint: vec![
377                services::ServiceEndpoint {
378                    ip_address_v4: vec![192, 168, 1, 1],
379                    port: 50211,
380                    domain_name: String::new(),
381                },
382                services::ServiceEndpoint {
383                    ip_address_v4: vec![10, 0, 0, 1],
384                    port: 50212,
385                    domain_name: String::new(),
386                },
387            ],
388            description: "Test node with different ports".to_string(),
389            ..Default::default()
390        };
391
392        let node_address = NodeAddress::from_protobuf(pb).unwrap();
393        assert_eq!(node_address.service_endpoints, vec!["192.168.1.1:50211", "10.0.0.1:50212"]);
394    }
395}