hedera/client/network/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2
3pub(super) mod managed;
4pub(super) mod mirror;
5
6use std::collections::{
7    BTreeSet,
8    HashMap,
9};
10use std::num::NonZeroUsize;
11use std::time::{
12    Duration,
13    Instant,
14};
15
16use backoff::backoff::Backoff;
17use once_cell::sync::OnceCell;
18use parking_lot::RwLock;
19use rand::thread_rng;
20use tonic::transport::{
21    Channel,
22    Endpoint,
23};
24use triomphe::Arc;
25
26use crate::{
27    AccountId,
28    ArcSwap,
29    Error,
30    NodeAddressBook,
31};
32
33pub(crate) const MAINNET: &[(u64, &[&str])] = &[
34    (3, &["13.124.142.126", "15.164.44.66", "15.165.118.251", "34.239.82.6", "35.237.200.180"]),
35    (4, &["3.130.52.236", "35.186.191.247"]),
36    (5, &["3.18.18.254", "23.111.186.250", "35.192.2.25", "74.50.117.35", "107.155.64.98"]),
37    (6, &["13.52.108.243", "13.71.90.154", "35.199.161.108", "104.211.205.124"]),
38    (7, &["3.114.54.4", "35.203.82.240"]),
39    (8, &["35.183.66.150", "35.236.5.219"]),
40    (9, &["35.181.158.250", "35.197.192.225"]),
41    (10, &["3.248.27.48", "35.242.233.154", "177.154.62.234"]),
42    (11, &["13.53.119.185", "35.240.118.96"]),
43    (12, &["35.177.162.180", "35.204.86.32", "170.187.184.238"]),
44    (13, &["34.215.192.104", "35.234.132.107"]),
45    (14, &["35.236.2.27", "52.8.21.141"]),
46    (15, &["3.121.238.26", "35.228.11.53"]),
47    (16, &["18.157.223.230", "34.91.181.183"]),
48    (17, &["18.232.251.19", "34.86.212.247"]),
49    (18, &["141.94.175.187"]),
50    (19, &["13.244.166.210", "13.246.51.42", "18.168.4.59", "34.89.87.138"]),
51    (20, &["34.82.78.255", "52.39.162.216"]),
52    (21, &["13.36.123.209", "34.76.140.109"]),
53    (22, &["34.64.141.166", "52.78.202.34"]),
54    (23, &["3.18.91.176", "35.232.244.145", "69.167.169.208"]),
55    (24, &["18.135.7.211", "34.89.103.38"]),
56    (25, &["13.232.240.207", "34.93.112.7"]),
57    (26, &["13.228.103.14", "34.87.150.174"]),
58    (27, &["13.56.4.96", "34.125.200.96"]),
59    (28, &["18.139.47.5", "35.198.220.75"]),
60    (29, &["34.142.71.129", "54.74.60.120", "80.85.70.197"]),
61    (30, &["34.201.177.212", "35.234.249.150"]),
62    (31, &["3.77.94.254", "34.107.78.179"]),
63];
64
65pub(crate) const TESTNET: &[(u64, &[&str])] = &[
66    (3, &["0.testnet.hedera.com", "34.94.106.61", "50.18.132.211"]),
67    (4, &["1.testnet.hedera.com", "35.237.119.55", "3.212.6.13"]),
68    (5, &["2.testnet.hedera.com", "35.245.27.193", "52.20.18.86"]),
69    (6, &["3.testnet.hedera.com", "34.83.112.116", "54.70.192.33"]),
70    (7, &["4.testnet.hedera.com", "34.94.160.4", "54.176.199.109"]),
71    (8, &["5.testnet.hedera.com", "34.106.102.218", "35.155.49.147"]),
72    (9, &["6.testnet.hedera.com", "34.133.197.230", "52.14.252.207"]),
73];
74
75pub(crate) const PREVIEWNET: &[(u64, &[&str])] = &[
76    (3, &["0.previewnet.hedera.com", "35.231.208.148", "3.211.248.172", "40.121.64.48"]),
77    (4, &["1.previewnet.hedera.com", "35.199.15.177", "3.133.213.146", "40.70.11.202"]),
78    (5, &["2.previewnet.hedera.com", "35.225.201.195", "52.15.105.130", "104.43.248.63"]),
79    (6, &["3.previewnet.hedera.com", "35.247.109.135", "54.241.38.1", "13.88.22.47"]),
80    (7, &["4.previewnet.hedera.com", "35.235.65.51", "54.177.51.127", "13.64.170.40"]),
81    (8, &["5.previewnet.hedera.com", "34.106.247.65", "35.83.89.171", "13.78.232.192"]),
82    (9, &["6.previewnet.hedera.com", "34.125.23.49", "50.18.17.93", "20.150.136.89"]),
83];
84
85#[derive(Default)]
86pub(crate) struct Network(pub(crate) ArcSwap<NetworkData>);
87
88impl Network {
89    pub(super) fn mainnet() -> Self {
90        NetworkData::from_static(MAINNET).into()
91    }
92
93    pub(super) fn testnet() -> Self {
94        NetworkData::from_static(TESTNET).into()
95    }
96
97    pub(super) fn previewnet() -> Self {
98        NetworkData::from_static(PREVIEWNET).into()
99    }
100
101    pub(super) fn from_addresses(addresses: &HashMap<String, AccountId>) -> crate::Result<Self> {
102        Ok(NetworkData::from_addresses(addresses)?.into())
103    }
104
105    fn try_rcu<T: Into<Arc<NetworkData>>, E, F: FnMut(&Arc<NetworkData>) -> Result<T, E>>(
106        &self,
107        mut f: F,
108    ) -> Result<Arc<NetworkData>, E> {
109        // note: we can't use the `arc_swap` rcu function because we return a result
110        let mut cur = self.0.load();
111        loop {
112            let new = f(&cur)?.into();
113            let prev = self.0.compare_and_swap(&*cur, new);
114            let swapped = Arc::ptr_eq(&*cur, &*prev);
115            if swapped {
116                return Ok(arc_swap::Guard::into_inner(cur));
117            }
118
119            cur = prev;
120        }
121    }
122
123    fn rcu<T: Into<Arc<NetworkData>>, F: FnMut(&Arc<NetworkData>) -> T>(
124        &self,
125        mut f: F,
126    ) -> Arc<NetworkData> {
127        match self.try_rcu(|it| -> Result<T, std::convert::Infallible> { Ok(f(it)) }) {
128            Ok(it) => it,
129            Err(e) => match e {},
130        }
131    }
132
133    pub(crate) fn update_from_addresses(
134        &self,
135        addresses: &HashMap<String, AccountId>,
136    ) -> crate::Result<()> {
137        self.try_rcu(|old| old.with_addresses(addresses))?;
138
139        Ok(())
140    }
141
142    pub(crate) fn update_from_address_book(&self, address_book: &NodeAddressBook) {
143        // todo: skip the updating whem `map` is the same and `connections` is the same.
144        self.rcu(|old| NetworkData::with_address_book(old, address_book));
145    }
146}
147
148impl From<NetworkData> for Network {
149    fn from(value: NetworkData) -> Self {
150        Self(ArcSwap::new(Arc::new(value)))
151    }
152}
153
154// note: `Default` here is mostly only useful so that we don't need to implement `from_addresses` twice, notably this doesn't allocate.
155#[derive(Default)]
156pub(crate) struct NetworkData {
157    map: HashMap<AccountId, usize>,
158    node_ids: Box<[AccountId]>,
159    backoff: RwLock<NodeBackoff>,
160    // Health stuff has to be in an Arc because it needs to stick around even if the map changes.
161    health: Box<[Arc<parking_lot::RwLock<NodeHealth>>]>,
162    connections: Box<[NodeConnection]>,
163}
164
165impl NetworkData {
166    pub(crate) fn from_addresses(addresses: &HashMap<String, AccountId>) -> crate::Result<Self> {
167        Self::default().with_addresses(addresses)
168    }
169
170    pub(crate) fn from_static(network: &'static [(u64, &'static [&'static str])]) -> Self {
171        let mut map = HashMap::with_capacity(network.len());
172        let mut node_ids = Vec::with_capacity(network.len());
173        let mut connections = Vec::with_capacity(network.len());
174        let mut health = Vec::with_capacity(network.len());
175
176        for (i, (num, address)) in network.iter().copied().enumerate() {
177            let node_account_id = AccountId::from(num);
178
179            map.insert(node_account_id, i);
180            node_ids.push(node_account_id);
181            health.push(Arc::default());
182            connections.push(NodeConnection::new_static(address));
183        }
184
185        Self {
186            map,
187            node_ids: node_ids.into_boxed_slice(),
188            health: health.into_boxed_slice(),
189            connections: connections.into_boxed_slice(),
190            backoff: NodeBackoff::default().into(),
191        }
192    }
193
194    fn with_address_book(old: &Self, address_book: &NodeAddressBook) -> Self {
195        let address_book = &address_book.node_addresses;
196
197        let mut map = HashMap::with_capacity(address_book.len());
198        let mut node_ids = Vec::with_capacity(address_book.len());
199        let mut connections = Vec::with_capacity(address_book.len());
200        let mut health = Vec::with_capacity(address_book.len());
201
202        for (i, address) in address_book.iter().enumerate() {
203            let new: BTreeSet<_> = address
204                .service_endpoints
205                .iter()
206                .filter(|endpoint_str| {
207                    // Check if port matches PLAINTEXT_PORT
208                    if let Some(port_str) = endpoint_str.split(':').nth(1) {
209                        if let Ok(port) = port_str.parse::<i32>() {
210                            return port == NodeConnection::PLAINTEXT_PORT as i32;
211                        }
212                    }
213                    false
214                })
215                .cloned()
216                .collect();
217
218            // if the node is the exact same we want to reuse everything (namely the connections and `healthy`).
219            // if the node has different routes then we still want to reuse `healthy` but replace the channel with a new channel.
220            // if the node just flat out doesn't exist in `old`, we want to add the new node.
221            // and, last but not least, if the node doesn't exist in `new` we want to get rid of it.
222            let upsert = match old.map.get(&address.node_account_id) {
223                Some(&account) => {
224                    let connection =
225                        match old.connections[account].addresses.symmetric_difference(&new).count()
226                        {
227                            0 => old.connections[account].clone(),
228                            _ => NodeConnection { addresses: new, channel: OnceCell::new() },
229                        };
230
231                    (old.health[account].clone(), connection)
232                }
233                None => {
234                    (Arc::default(), NodeConnection { addresses: new, channel: OnceCell::new() })
235                }
236            };
237
238            map.insert(address.node_account_id, i);
239            node_ids.push(address.node_account_id);
240            health.push(upsert.0);
241            connections.push(upsert.1);
242        }
243
244        Self {
245            map,
246            node_ids: node_ids.into_boxed_slice(),
247            health: health.into_boxed_slice(),
248            connections: connections.into_boxed_slice(),
249            backoff: NodeBackoff::default().into(),
250        }
251    }
252
253    fn with_addresses(&self, addresses: &HashMap<String, AccountId>) -> crate::Result<Self> {
254        use std::collections::hash_map::Entry;
255        let mut map: HashMap<AccountId, usize> = HashMap::new();
256        let mut node_ids = Vec::new();
257        let mut connections: Vec<NodeConnection> = Vec::new();
258        let mut health = Vec::new();
259
260        for (address, node) in addresses {
261            let next_index = node_ids.len();
262
263            match map.entry(*node) {
264                Entry::Occupied(entry) => {
265                    connections[*entry.get()].addresses.insert(address.clone());
266                }
267                Entry::Vacant(entry) => {
268                    entry.insert(next_index);
269                    node_ids.push(*node);
270                    // fixme: keep the channel around more.
271                    connections.push(NodeConnection {
272                        addresses: BTreeSet::from([address.clone()]),
273                        channel: OnceCell::new(),
274                    });
275
276                    health.push(match self.map.get(node) {
277                        Some(it) => self.health[*it].clone(),
278                        None => Arc::default(),
279                    });
280                }
281            };
282        }
283
284        Ok(Self {
285            map,
286            node_ids: node_ids.into_boxed_slice(),
287            health: health.into_boxed_slice(),
288            connections: connections.into_boxed_slice(),
289            backoff: NodeBackoff::default().into(),
290        })
291    }
292
293    pub(crate) fn node_ids(&self) -> &[AccountId] {
294        &self.node_ids
295    }
296
297    pub(crate) fn node_indexes_for_ids(&self, ids: &[AccountId]) -> crate::Result<Vec<usize>> {
298        let mut indexes = Vec::new();
299        for id in ids {
300            indexes.push(
301                self.map
302                    .get(id)
303                    .copied()
304                    .ok_or_else(|| Error::NodeAccountUnknown(Box::new(*id)))?,
305            );
306        }
307
308        Ok(indexes)
309    }
310
311    // Sets the max attempts that an unhealthy node can retry
312    pub(crate) fn set_max_node_attempts(&self, max_attempts: Option<NonZeroUsize>) {
313        self.backoff.write().max_attempts = max_attempts
314    }
315
316    // Returns the max attempts that an unhealthy node can retry
317    pub(crate) fn max_node_attempts(&self) -> Option<NonZeroUsize> {
318        self.backoff.read().max_attempts
319    }
320
321    // Sets the max backoff for a node.
322    pub(crate) fn set_max_backoff(&self, max_backoff: Duration) {
323        self.backoff.write().max_backoff = max_backoff
324    }
325
326    // Return the initial backoff for a node.
327    #[must_use]
328    pub(crate) fn max_backoff(&self) -> Duration {
329        self.backoff.read().max_backoff
330    }
331
332    // Sets the initial backoff for a request being executed.
333    pub(crate) fn set_min_backoff(&self, min_backoff: Duration) {
334        self.backoff.write().min_backoff = min_backoff
335    }
336
337    // Return the initial backoff for a request being executed.
338    #[must_use]
339    pub(crate) fn min_backoff(&self) -> Duration {
340        self.backoff.read().min_backoff
341    }
342
343    pub(crate) fn mark_node_unhealthy(&self, node_index: usize) {
344        let now = Instant::now();
345
346        self.health[node_index].write().mark_unhealthy(*self.backoff.read(), now);
347    }
348
349    pub(crate) fn mark_node_healthy(&self, node_index: usize) {
350        self.health[node_index].write().mark_healthy(Instant::now());
351    }
352
353    pub(crate) fn is_node_healthy(&self, node_index: usize, now: Instant) -> bool {
354        // a healthy node has a healthiness before now.
355
356        self.health[node_index].read().is_healthy(now)
357    }
358
359    pub(crate) fn node_recently_pinged(&self, node_index: usize, now: Instant) -> bool {
360        self.health[node_index].read().recently_pinged(now)
361    }
362
363    pub(crate) fn healthy_node_indexes(&self, time: Instant) -> impl Iterator<Item = usize> + '_ {
364        (0..self.node_ids.len()).filter(move |index| self.is_node_healthy(*index, time))
365    }
366
367    pub(crate) fn healthy_node_ids(&self) -> impl Iterator<Item = AccountId> + '_ {
368        self.healthy_node_indexes(Instant::now()).map(|it| self.node_ids[it])
369    }
370    pub(crate) fn random_node_ids(&self) -> Vec<AccountId> {
371        let mut node_ids: Vec<_> = self.healthy_node_ids().collect();
372        // self.remove_dead_nodes();
373
374        if node_ids.is_empty() {
375            log::warn!("No healthy nodes, randomly picking some unhealthy ones");
376            // hack, slowpath, don't care perf, fix this better later tho.
377            node_ids = self.node_ids.to_vec();
378        }
379
380        let node_sample_amount = (node_ids.len() + 2) / 3;
381
382        let node_id_indecies =
383            rand::seq::index::sample(&mut thread_rng(), node_ids.len(), node_sample_amount);
384
385        node_id_indecies.into_iter().map(|index| node_ids[index]).collect()
386    }
387
388    pub(crate) fn channel(&self, index: usize) -> (AccountId, Channel) {
389        let id = self.node_ids[index];
390
391        let channel = self.connections[index].channel();
392
393        (id, channel)
394    }
395
396    pub(crate) fn addresses(&self) -> HashMap<String, AccountId> {
397        self.map
398            .iter()
399            .flat_map(|(&account, &index)| {
400                self.connections[index].addresses.iter().map(move |it| (it.clone(), account))
401            })
402            .collect()
403    }
404}
405
406#[derive(Default)]
407enum NodeHealth {
408    /// The node has never been used, so we don't know anything about it.
409    ///
410    /// However, we'll vaguely consider it healthy (`is_healthy` returns `true`).
411    #[default]
412    Unused,
413
414    /// When we used or pinged the node we got some kind of error with it (like a BUSY response).
415    ///
416    /// Repeated errors cause the backoff to increase.
417    ///
418    /// Once we've reached `healthyAt` the node is *semantically* in the ``unused`` state,
419    /// other than retaining the backoff until a `healthy` request happens.
420    Unhealthy { backoff: NodeBackoff, healthy_at: Instant, attempts: usize },
421
422    /// When we last used the node the node acted as normal, so, we get to treat it as a healthy node for 15 minutes.
423    Healthy { used_at: Instant },
424}
425
426#[derive(Copy, Clone)]
427pub(crate) struct NodeBackoff {
428    pub(crate) current_interval: Duration,
429    pub(crate) max_backoff: Duration,
430    pub(crate) min_backoff: Duration,
431    pub(crate) max_attempts: Option<NonZeroUsize>,
432}
433
434impl Default for NodeBackoff {
435    fn default() -> Self {
436        Self {
437            current_interval: Duration::from_millis(250),
438            max_backoff: Duration::from_secs(60 * 60),
439            min_backoff: Duration::from_millis(250),
440            max_attempts: NonZeroUsize::new(10),
441        }
442    }
443}
444
445impl NodeHealth {
446    fn backoff(&self, backoff_config: NodeBackoff) -> (backoff::ExponentialBackoff, usize) {
447        // If node is already labeled Unhealthy, preserve backoff and attempt amount
448        // For new Unhealthy nodes, apply config and start attempt count at 0
449        let (node_backoff, attempts) = match self {
450            Self::Unhealthy { backoff, healthy_at: _, attempts } => (*backoff, attempts),
451            _ => (backoff_config, &0),
452        };
453
454        (
455            backoff::ExponentialBackoff {
456                current_interval: node_backoff.current_interval,
457                initial_interval: node_backoff.min_backoff,
458                max_elapsed_time: None,
459                max_interval: node_backoff.max_backoff,
460                ..Default::default()
461            },
462            *attempts + 1,
463        )
464    }
465
466    pub(crate) fn mark_unhealthy(&mut self, backoff_config: NodeBackoff, now: Instant) {
467        let (mut backoff, unhealthy_node_attempts) = self.backoff(backoff_config);
468
469        // Remove node if max_attempts has been reached and max_attempts is not 0
470        if backoff_config
471            .max_attempts
472            .map_or(false, |max_attempts| unhealthy_node_attempts > max_attempts.get())
473        {
474            log::debug!("Node has reached the max amount of retries, removing from network")
475        }
476
477        // Generates the next current_interval with a random duration
478        let next_backoff = backoff.next_backoff().expect("`max_elapsed_time` is hardwired to None");
479
480        let healthy_at = now + next_backoff;
481
482        *self = Self::Unhealthy {
483            backoff: NodeBackoff {
484                current_interval: next_backoff,
485                max_backoff: backoff.max_interval,
486                min_backoff: backoff.initial_interval,
487                max_attempts: backoff_config.max_attempts,
488            },
489            healthy_at,
490            attempts: unhealthy_node_attempts,
491        };
492    }
493
494    pub(crate) fn mark_healthy(&mut self, now: Instant) {
495        *self = Self::Healthy { used_at: now };
496    }
497
498    pub(crate) fn is_healthy(&self, now: Instant) -> bool {
499        // a healthy node has a healthiness before now.
500        match self {
501            Self::Unhealthy { backoff: _, healthy_at, attempts: _ } => healthy_at < &now,
502            _ => true,
503        }
504    }
505
506    pub(crate) fn recently_pinged(&self, now: Instant) -> bool {
507        match self {
508            // when used at was less than 15 minutes ago we consider ourselves "pinged", otherwise we're basically `.unused`.
509            Self::Healthy { used_at } => now < *used_at + Duration::from_secs(15 * 60),
510            // likewise an unhealthy node (healthyAt > now) has been "pinged" (although we don't want to use it probably we at least *have* gotten *something* from it)
511            Self::Unhealthy { backoff: _, healthy_at, attempts: _ } => now < *healthy_at,
512
513            // an unused node is by definition not pinged.
514            Self::Unused => false,
515        }
516    }
517}
518
519#[derive(Clone)]
520struct NodeConnection {
521    addresses: BTreeSet<String>,
522    channel: OnceCell<Channel>,
523}
524
525impl NodeConnection {
526    const PLAINTEXT_PORT: u16 = 50211;
527
528    fn new_static(addresses: &[&'static str]) -> NodeConnection {
529        Self {
530            addresses: addresses
531                .iter()
532                .copied()
533                .map(|addr| format!("{}:{}", addr, Self::PLAINTEXT_PORT))
534                .collect(),
535            channel: OnceCell::default(),
536        }
537    }
538
539    pub(crate) fn channel(&self) -> Channel {
540        let channel = self
541            .channel
542            .get_or_init(|| {
543                let addresses = self.addresses.iter().map(|it| {
544                    Endpoint::from_shared(format!("tcp://{it}"))
545                        .unwrap()
546                        .keep_alive_timeout(Duration::from_secs(10))
547                        .keep_alive_while_idle(true)
548                        .tcp_keepalive(Some(Duration::from_secs(10)))
549                        .connect_timeout(Duration::from_secs(10))
550                });
551
552                Channel::balance_list(addresses)
553            })
554            .clone();
555
556        channel
557    }
558}
559
560#[cfg(test)]
561mod tests {
562    use super::*;
563    use crate::{
564        NodeAddress,
565        NodeAddressBook,
566    };
567
568    #[test]
569    fn test_network_with_string_endpoints() {
570        let node_address = NodeAddress {
571            node_id: 1,
572            rsa_public_key: vec![1, 2, 3, 4],
573            node_account_id: AccountId::new(0, 0, 1),
574            tls_certificate_hash: vec![5, 6, 7, 8],
575            service_endpoints: vec![
576                "192.168.1.1:50211".to_string(),
577                "example.com:50211".to_string(),
578                "localhost:50211".to_string(),
579            ],
580            description: "Test node".to_string(),
581        };
582
583        let address_book = NodeAddressBook { node_addresses: vec![node_address] };
584
585        let network = Network::default();
586        network.update_from_address_book(&address_book);
587
588        // Test that the network properly filters endpoints with PLAINTEXT_PORT
589        let addresses = network.0.load().addresses();
590        assert_eq!(addresses.len(), 3);
591        assert!(addresses.contains_key("192.168.1.1:50211"));
592        assert!(addresses.contains_key("example.com:50211"));
593        assert!(addresses.contains_key("localhost:50211"));
594    }
595
596    #[test]
597    fn test_network_filters_by_port() {
598        let node_address = NodeAddress {
599            node_id: 2,
600            rsa_public_key: vec![1, 2, 3, 4],
601            node_account_id: AccountId::new(0, 0, 2),
602            tls_certificate_hash: vec![5, 6, 7, 8],
603            service_endpoints: vec![
604                "192.168.1.1:50211".to_string(), // Should be included
605                "192.168.1.1:50212".to_string(), // Should be filtered out
606                "example.com:50211".to_string(), // Should be included
607                "example.com:50213".to_string(), // Should be filtered out
608            ],
609            description: "Test node with different ports".to_string(),
610        };
611
612        let address_book = NodeAddressBook { node_addresses: vec![node_address] };
613
614        let network = Network::default();
615        network.update_from_address_book(&address_book);
616
617        let addresses = network.0.load().addresses();
618        assert_eq!(addresses.len(), 2);
619        assert!(addresses.contains_key("192.168.1.1:50211"));
620        assert!(addresses.contains_key("example.com:50211"));
621        assert!(!addresses.contains_key("192.168.1.1:50212"));
622        assert!(!addresses.contains_key("example.com:50213"));
623    }
624
625    #[test]
626    fn test_network_with_kubernetes_domain() {
627        let node_address = NodeAddress {
628            node_id: 3,
629            rsa_public_key: vec![1, 2, 3, 4],
630            node_account_id: AccountId::new(0, 0, 3),
631            tls_certificate_hash: vec![5, 6, 7, 8],
632            service_endpoints: vec![
633                "network-node1-svc.solo-e2e.svc.cluster.local:50211".to_string()
634            ],
635            description: "Test node with k8s domain".to_string(),
636        };
637
638        let address_book = NodeAddressBook { node_addresses: vec![node_address] };
639
640        let network = Network::default();
641        network.update_from_address_book(&address_book);
642
643        let addresses = network.0.load().addresses();
644        assert_eq!(addresses.len(), 1);
645        assert!(addresses.contains_key("network-node1-svc.solo-e2e.svc.cluster.local:50211"));
646    }
647
648    #[test]
649    fn test_network_with_mixed_ip_and_domain() {
650        let node_address = NodeAddress {
651            node_id: 4,
652            rsa_public_key: vec![1, 2, 3, 4],
653            node_account_id: AccountId::new(0, 0, 4),
654            tls_certificate_hash: vec![5, 6, 7, 8],
655            service_endpoints: vec![
656                "192.168.1.1:50211".to_string(),
657                "10.0.0.1:50211".to_string(),
658                "example.com:50211".to_string(),
659                "localhost:50211".to_string(),
660            ],
661            description: "Test node with mixed endpoints".to_string(),
662        };
663
664        let address_book = NodeAddressBook { node_addresses: vec![node_address] };
665
666        let network = Network::default();
667        network.update_from_address_book(&address_book);
668
669        let addresses = network.0.load().addresses();
670        assert_eq!(addresses.len(), 4);
671        assert!(addresses.contains_key("192.168.1.1:50211"));
672        assert!(addresses.contains_key("10.0.0.1:50211"));
673        assert!(addresses.contains_key("example.com:50211"));
674        assert!(addresses.contains_key("localhost:50211"));
675    }
676
677    #[test]
678    fn test_node_connection_with_string_addresses() {
679        let connection = NodeConnection {
680            addresses: BTreeSet::from([
681                "192.168.1.1:50211".to_string(),
682                "example.com:50211".to_string(),
683            ]),
684            channel: OnceCell::new(),
685        };
686
687        assert_eq!(connection.addresses.len(), 2);
688        assert!(connection.addresses.contains("192.168.1.1:50211"));
689        assert!(connection.addresses.contains("example.com:50211"));
690    }
691
692    #[test]
693    fn test_network_data_with_address_book() {
694        let node_address = NodeAddress {
695            node_id: 5,
696            rsa_public_key: vec![1, 2, 3, 4],
697            node_account_id: AccountId::new(0, 0, 5),
698            tls_certificate_hash: vec![5, 6, 7, 8],
699            service_endpoints: vec![
700                "192.168.1.1:50211".to_string(),
701                "example.com:50211".to_string(),
702            ],
703            description: "Test node".to_string(),
704        };
705
706        let address_book = NodeAddressBook { node_addresses: vec![node_address] };
707
708        let network_data = NetworkData::with_address_book(&NetworkData::default(), &address_book);
709
710        assert_eq!(network_data.node_ids.len(), 1);
711        assert_eq!(network_data.node_ids[0], AccountId::new(0, 0, 5));
712        assert_eq!(network_data.connections.len(), 1);
713        assert_eq!(network_data.connections[0].addresses.len(), 2);
714    }
715}