1pub(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 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 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#[derive(Default)]
156pub(crate) struct NetworkData {
157 map: HashMap<AccountId, usize>,
158 node_ids: Box<[AccountId]>,
159 backoff: RwLock<NodeBackoff>,
160 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 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 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 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 pub(crate) fn set_max_node_attempts(&self, max_attempts: Option<NonZeroUsize>) {
313 self.backoff.write().max_attempts = max_attempts
314 }
315
316 pub(crate) fn max_node_attempts(&self) -> Option<NonZeroUsize> {
318 self.backoff.read().max_attempts
319 }
320
321 pub(crate) fn set_max_backoff(&self, max_backoff: Duration) {
323 self.backoff.write().max_backoff = max_backoff
324 }
325
326 #[must_use]
328 pub(crate) fn max_backoff(&self) -> Duration {
329 self.backoff.read().max_backoff
330 }
331
332 pub(crate) fn set_min_backoff(&self, min_backoff: Duration) {
334 self.backoff.write().min_backoff = min_backoff
335 }
336
337 #[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 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 if node_ids.is_empty() {
375 log::warn!("No healthy nodes, randomly picking some unhealthy ones");
376 node_ids = self.node_ids.to_vec();
378 }
379
380 let node_sample_amount = node_ids.len();
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, grpc_deadline: Duration) -> (AccountId, Channel) {
389 let id = self.node_ids[index];
390
391 let channel = self.connections[index].channel(grpc_deadline);
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 #[default]
412 Unused,
413
414 Unhealthy { backoff: NodeBackoff, healthy_at: Instant, attempts: usize },
421
422 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 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 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 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 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 Self::Healthy { used_at } => now < *used_at + Duration::from_secs(15 * 60),
510 Self::Unhealthy { backoff: _, healthy_at, attempts: _ } => now < *healthy_at,
512
513 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, grpc_deadline: Duration) -> 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(grpc_deadline)
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 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(), "192.168.1.1:50212".to_string(), "example.com:50211".to_string(), "example.com:50213".to_string(), ],
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}