From 020e8220062ae7f0d61d71363a5126681d015a08 Mon Sep 17 00:00:00 2001 From: Michael Iglesias Date: Fri, 1 Aug 2025 17:04:10 -0400 Subject: [PATCH 1/4] feat(network): support multiple peer addresses in remote bootnode configuration Add support for connecting to multiple peers via the --remote-bootnode argument to improve network resilience and connectivity options. Key changes: - Add parse_multiple_multiaddrs() function to handle various multiaddress formats - Support concatenated format: /ip4/1.2.3.4/tcp/1234/ip4/5.6.7.8/tcp/5678 - Support comma-separated format: /ip4/1.2.3.4/tcp/1234,/ip4/5.6.7.8/tcp/5678 - Support space-separated format: /ip4/1.2.3.4/tcp/1234 /ip4/5.6.7.8/tcp/5678 - Maintain backward compatibility with single peer addresses - Handle connection failures gracefully with warning-level logging - Add comprehensive test suite covering all supported formats The network handler now attempts to connect to all specified peers sequentially, logging each attempt and continuing even if some peers are unreachable. Example usage: --remote-bootnode "/ip4/10.38.1.103/tcp/55444/ip4/10.38.1.105/tcp/55444" Fixes: AN-268 --- app/src/network/mod.rs | 178 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 176 insertions(+), 2 deletions(-) diff --git a/app/src/network/mod.rs b/app/src/network/mod.rs index 546ef7a5..702ccdbe 100644 --- a/app/src/network/mod.rs +++ b/app/src/network/mod.rs @@ -479,6 +479,174 @@ impl NetworkBackend { } } +/// Parse multiple multiaddresses from a string. +/// Supports the following formats: +/// - Comma-separated: "/ip4/1.2.3.4/tcp/1234,/ip4/5.6.7.8/tcp/5678" +/// - Space-separated: "/ip4/1.2.3.4/tcp/1234 /ip4/5.6.7.8/tcp/5678" +/// - Concatenated: "/ip4/1.2.3.4/tcp/1234/ip4/5.6.7.8/tcp/5678" +fn parse_multiple_multiaddrs(input: &str) -> Result, Error> { + let mut addresses = Vec::new(); + + // First, try to parse as comma-separated + if input.contains(',') { + for addr_str in input.split(',') { + let addr_str = addr_str.trim(); + if !addr_str.is_empty() { + let addr = Multiaddr::from_str(addr_str)?; + addresses.push(addr); + } + } + return Ok(addresses); + } + + // Then, try to parse as space-separated + if input.contains(' ') { + for addr_str in input.split_whitespace() { + let addr_str = addr_str.trim(); + if !addr_str.is_empty() { + let addr = Multiaddr::from_str(addr_str)?; + addresses.push(addr); + } + } + return Ok(addresses); + } + + // Finally, try to parse the concatenated format + // This is more complex as we need to split at protocol boundaries + let mut current_pos = 0; + + while current_pos < input.len() { + // Find the next complete multiaddress starting with '/' + if let Some(slash_pos) = input[current_pos..].find('/') { + let start_pos = current_pos + slash_pos; + + // Try to find the end of this multiaddress + let mut end_pos = input.len(); + + // Look for the next occurrence of a protocol that could start a new multiaddress + let mut search_pos = start_pos + 1; + while search_pos < input.len() { + if let Some(next_slash) = input[search_pos..].find('/') { + let protocol_start = search_pos + next_slash + 1; + if protocol_start < input.len() { + // Find the end of the protocol name + if let Some(protocol_end) = input[protocol_start..].find('/') { + let protocol = &input[protocol_start..protocol_start + protocol_end]; + // Check if this looks like a new multiaddress protocol + if ["ip4", "ip6", "dns", "dns4", "dns6", "unix", "p2p", "p2p-webrtc-star", "p2p-websocket-star"].contains(&protocol) { + // Verify this is actually a new multiaddress by trying to parse it + let potential_addr = &input[protocol_start - 1..]; + if Multiaddr::from_str(potential_addr).is_ok() { + end_pos = protocol_start - 1; + break; + } + } + } + } + search_pos = protocol_start; + } else { + break; + } + } + + let multiaddr_str = &input[start_pos..end_pos]; + match Multiaddr::from_str(multiaddr_str) { + Ok(addr) => { + addresses.push(addr); + current_pos = end_pos; + } + Err(e) => { + return Err(Error::MultiaddrError(e)); + } + } + } else { + break; + } + } + + if addresses.is_empty() { + return Err(Error::MultiaddrError(libp2p::multiaddr::Error::InvalidMultiaddr)); + } + + Ok(addresses) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_multiple_multiaddrs() { + // Test the example from the user query (concatenated format) + let input = "/ip4/10.38.1.103/tcp/55444/ip4/10.38.1.105/tcp/55444"; + let result = parse_multiple_multiaddrs(input).unwrap(); + + assert_eq!(result.len(), 2); + assert_eq!(result[0].to_string(), "/ip4/10.38.1.103/tcp/55444"); + assert_eq!(result[1].to_string(), "/ip4/10.38.1.105/tcp/55444"); + } + + #[test] + fn test_parse_comma_separated_multiaddrs() { + // Test comma-separated format + let input = "/ip4/10.38.1.103/tcp/55444,/ip4/10.38.1.105/tcp/55444"; + let result = parse_multiple_multiaddrs(input).unwrap(); + + assert_eq!(result.len(), 2); + assert_eq!(result[0].to_string(), "/ip4/10.38.1.103/tcp/55444"); + assert_eq!(result[1].to_string(), "/ip4/10.38.1.105/tcp/55444"); + } + + #[test] + fn test_parse_space_separated_multiaddrs() { + // Test space-separated format + let input = "/ip4/10.38.1.103/tcp/55444 /ip4/10.38.1.105/tcp/55444"; + let result = parse_multiple_multiaddrs(input).unwrap(); + + assert_eq!(result.len(), 2); + assert_eq!(result[0].to_string(), "/ip4/10.38.1.103/tcp/55444"); + assert_eq!(result[1].to_string(), "/ip4/10.38.1.105/tcp/55444"); + } + + #[test] + fn test_parse_single_multiaddr() { + // Test with a single multiaddress + let input = "/ip4/10.38.1.103/tcp/55444"; + let result = parse_multiple_multiaddrs(input).unwrap(); + + assert_eq!(result.len(), 1); + assert_eq!(result[0].to_string(), "/ip4/10.38.1.103/tcp/55444"); + } + + #[test] + fn test_parse_three_multiaddrs() { + // Test with three multiaddresses + let input = "/ip4/10.38.1.103/tcp/55444/ip4/10.38.1.105/tcp/55444/ip4/10.38.1.106/tcp/55444"; + let result = parse_multiple_multiaddrs(input).unwrap(); + + assert_eq!(result.len(), 3); + assert_eq!(result[0].to_string(), "/ip4/10.38.1.103/tcp/55444"); + assert_eq!(result[1].to_string(), "/ip4/10.38.1.105/tcp/55444"); + assert_eq!(result[2].to_string(), "/ip4/10.38.1.106/tcp/55444"); + } + + #[test] + fn test_parse_empty_input() { + // Test with empty input + let input = ""; + let result = parse_multiple_multiaddrs(input); + assert!(result.is_err()); + } + + #[test] + fn test_parse_invalid_multiaddr() { + // Test with invalid multiaddress + let input = "/invalid/protocol"; + let result = parse_multiple_multiaddrs(input); + assert!(result.is_err()); + } +} + pub async fn spawn_network_handler( addr: String, port: u16, @@ -524,8 +692,14 @@ pub async fn spawn_network_handler( if let Some(bootnode) = remote_bootnode { trace!("Dialing bootnode: {}", bootnode); - let address = Multiaddr::from_str(&bootnode)?; - client.dial(address).await?; + let addresses = parse_multiple_multiaddrs(&bootnode)?; + + for (i, address) in addresses.iter().enumerate() { + trace!("Dialing bootnode {}: {}", i + 1, address); + if let Err(e) = client.dial(address.clone()).await { + warn!("Failed to dial bootnode {} ({}): {}", i + 1, address, e); + } + } } Ok(client) From 343a65ae124b7b4d0965102fc76fd32e46ca9656 Mon Sep 17 00:00:00 2001 From: Michael Iglesias Date: Fri, 1 Aug 2025 17:08:42 -0400 Subject: [PATCH 2/4] chore: fix cargo fmt errors --- app/src/network/mod.rs | 51 +++++++++++++++++++++++++++--------------- 1 file changed, 33 insertions(+), 18 deletions(-) diff --git a/app/src/network/mod.rs b/app/src/network/mod.rs index 702ccdbe..e9d1bcea 100644 --- a/app/src/network/mod.rs +++ b/app/src/network/mod.rs @@ -486,7 +486,7 @@ impl NetworkBackend { /// - Concatenated: "/ip4/1.2.3.4/tcp/1234/ip4/5.6.7.8/tcp/5678" fn parse_multiple_multiaddrs(input: &str) -> Result, Error> { let mut addresses = Vec::new(); - + // First, try to parse as comma-separated if input.contains(',') { for addr_str in input.split(',') { @@ -498,7 +498,7 @@ fn parse_multiple_multiaddrs(input: &str) -> Result, Error> { } return Ok(addresses); } - + // Then, try to parse as space-separated if input.contains(' ') { for addr_str in input.split_whitespace() { @@ -510,19 +510,19 @@ fn parse_multiple_multiaddrs(input: &str) -> Result, Error> { } return Ok(addresses); } - + // Finally, try to parse the concatenated format // This is more complex as we need to split at protocol boundaries let mut current_pos = 0; - + while current_pos < input.len() { // Find the next complete multiaddress starting with '/' if let Some(slash_pos) = input[current_pos..].find('/') { let start_pos = current_pos + slash_pos; - + // Try to find the end of this multiaddress let mut end_pos = input.len(); - + // Look for the next occurrence of a protocol that could start a new multiaddress let mut search_pos = start_pos + 1; while search_pos < input.len() { @@ -533,7 +533,19 @@ fn parse_multiple_multiaddrs(input: &str) -> Result, Error> { if let Some(protocol_end) = input[protocol_start..].find('/') { let protocol = &input[protocol_start..protocol_start + protocol_end]; // Check if this looks like a new multiaddress protocol - if ["ip4", "ip6", "dns", "dns4", "dns6", "unix", "p2p", "p2p-webrtc-star", "p2p-websocket-star"].contains(&protocol) { + if [ + "ip4", + "ip6", + "dns", + "dns4", + "dns6", + "unix", + "p2p", + "p2p-webrtc-star", + "p2p-websocket-star", + ] + .contains(&protocol) + { // Verify this is actually a new multiaddress by trying to parse it let potential_addr = &input[protocol_start - 1..]; if Multiaddr::from_str(potential_addr).is_ok() { @@ -548,7 +560,7 @@ fn parse_multiple_multiaddrs(input: &str) -> Result, Error> { break; } } - + let multiaddr_str = &input[start_pos..end_pos]; match Multiaddr::from_str(multiaddr_str) { Ok(addr) => { @@ -563,11 +575,13 @@ fn parse_multiple_multiaddrs(input: &str) -> Result, Error> { break; } } - + if addresses.is_empty() { - return Err(Error::MultiaddrError(libp2p::multiaddr::Error::InvalidMultiaddr)); + return Err(Error::MultiaddrError( + libp2p::multiaddr::Error::InvalidMultiaddr, + )); } - + Ok(addresses) } @@ -580,7 +594,7 @@ mod tests { // Test the example from the user query (concatenated format) let input = "/ip4/10.38.1.103/tcp/55444/ip4/10.38.1.105/tcp/55444"; let result = parse_multiple_multiaddrs(input).unwrap(); - + assert_eq!(result.len(), 2); assert_eq!(result[0].to_string(), "/ip4/10.38.1.103/tcp/55444"); assert_eq!(result[1].to_string(), "/ip4/10.38.1.105/tcp/55444"); @@ -591,7 +605,7 @@ mod tests { // Test comma-separated format let input = "/ip4/10.38.1.103/tcp/55444,/ip4/10.38.1.105/tcp/55444"; let result = parse_multiple_multiaddrs(input).unwrap(); - + assert_eq!(result.len(), 2); assert_eq!(result[0].to_string(), "/ip4/10.38.1.103/tcp/55444"); assert_eq!(result[1].to_string(), "/ip4/10.38.1.105/tcp/55444"); @@ -602,7 +616,7 @@ mod tests { // Test space-separated format let input = "/ip4/10.38.1.103/tcp/55444 /ip4/10.38.1.105/tcp/55444"; let result = parse_multiple_multiaddrs(input).unwrap(); - + assert_eq!(result.len(), 2); assert_eq!(result[0].to_string(), "/ip4/10.38.1.103/tcp/55444"); assert_eq!(result[1].to_string(), "/ip4/10.38.1.105/tcp/55444"); @@ -613,7 +627,7 @@ mod tests { // Test with a single multiaddress let input = "/ip4/10.38.1.103/tcp/55444"; let result = parse_multiple_multiaddrs(input).unwrap(); - + assert_eq!(result.len(), 1); assert_eq!(result[0].to_string(), "/ip4/10.38.1.103/tcp/55444"); } @@ -621,9 +635,10 @@ mod tests { #[test] fn test_parse_three_multiaddrs() { // Test with three multiaddresses - let input = "/ip4/10.38.1.103/tcp/55444/ip4/10.38.1.105/tcp/55444/ip4/10.38.1.106/tcp/55444"; + let input = + "/ip4/10.38.1.103/tcp/55444/ip4/10.38.1.105/tcp/55444/ip4/10.38.1.106/tcp/55444"; let result = parse_multiple_multiaddrs(input).unwrap(); - + assert_eq!(result.len(), 3); assert_eq!(result[0].to_string(), "/ip4/10.38.1.103/tcp/55444"); assert_eq!(result[1].to_string(), "/ip4/10.38.1.105/tcp/55444"); @@ -693,7 +708,7 @@ pub async fn spawn_network_handler( if let Some(bootnode) = remote_bootnode { trace!("Dialing bootnode: {}", bootnode); let addresses = parse_multiple_multiaddrs(&bootnode)?; - + for (i, address) in addresses.iter().enumerate() { trace!("Dialing bootnode {}: {}", i + 1, address); if let Err(e) = client.dial(address.clone()).await { From db0a2742f0c373f078618025771623e7069b348c Mon Sep 17 00:00:00 2001 From: Michael Iglesias Date: Fri, 1 Aug 2025 17:23:24 -0400 Subject: [PATCH 3/4] chore: define supported multiaddress protocols const to improve the clarity and maintainability of the multiaddress parsing logic. --- app/src/network/mod.rs | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/app/src/network/mod.rs b/app/src/network/mod.rs index e9d1bcea..049f2933 100644 --- a/app/src/network/mod.rs +++ b/app/src/network/mod.rs @@ -38,6 +38,19 @@ pub type EnrSyncCommitteeBitfield = BitVector<::SyncCommitteeSu const RECONNECT_INTERVAL_SECS: u64 = 5; const RECONNECT_MAX_ATTEMPTS: u32 = 12; +/// Supported multiaddress protocols that can start a new multiaddress +const SUPPORTED_MULTIADDR_PROTOCOLS: &[&str] = &[ + "ip4", + "ip6", + "dns", + "dns4", + "dns6", + "unix", + "p2p", + "p2p-webrtc-star", + "p2p-websocket-star", +]; + #[derive(NetworkBehaviour)] struct MyBehaviour { gossipsub: gossipsub::Behaviour, @@ -533,18 +546,7 @@ fn parse_multiple_multiaddrs(input: &str) -> Result, Error> { if let Some(protocol_end) = input[protocol_start..].find('/') { let protocol = &input[protocol_start..protocol_start + protocol_end]; // Check if this looks like a new multiaddress protocol - if [ - "ip4", - "ip6", - "dns", - "dns4", - "dns6", - "unix", - "p2p", - "p2p-webrtc-star", - "p2p-websocket-star", - ] - .contains(&protocol) + if SUPPORTED_MULTIADDR_PROTOCOLS.contains(&protocol) { // Verify this is actually a new multiaddress by trying to parse it let potential_addr = &input[protocol_start - 1..]; From af33d7d4b56b2ae48c1715133c08fc7e70a3658f Mon Sep 17 00:00:00 2001 From: Michael Iglesias Date: Fri, 1 Aug 2025 17:24:03 -0400 Subject: [PATCH 4/4] chore: fix cargo fmt --- app/src/network/mod.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/src/network/mod.rs b/app/src/network/mod.rs index 049f2933..d31a3de9 100644 --- a/app/src/network/mod.rs +++ b/app/src/network/mod.rs @@ -41,7 +41,7 @@ const RECONNECT_MAX_ATTEMPTS: u32 = 12; /// Supported multiaddress protocols that can start a new multiaddress const SUPPORTED_MULTIADDR_PROTOCOLS: &[&str] = &[ "ip4", - "ip6", + "ip6", "dns", "dns4", "dns6", @@ -546,8 +546,7 @@ fn parse_multiple_multiaddrs(input: &str) -> Result, Error> { if let Some(protocol_end) = input[protocol_start..].find('/') { let protocol = &input[protocol_start..protocol_start + protocol_end]; // Check if this looks like a new multiaddress protocol - if SUPPORTED_MULTIADDR_PROTOCOLS.contains(&protocol) - { + if SUPPORTED_MULTIADDR_PROTOCOLS.contains(&protocol) { // Verify this is actually a new multiaddress by trying to parse it let potential_addr = &input[protocol_start - 1..]; if Multiaddr::from_str(potential_addr).is_ok() {