Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
194 changes: 192 additions & 2 deletions app/src/network/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,19 @@ pub type EnrSyncCommitteeBitfield<T> = BitVector<<T as EthSpec>::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,
Expand Down Expand Up @@ -479,6 +492,177 @@ 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"
Comment on lines +495 to +499
Copy link

Copilot AI Aug 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function lacks proper documentation. Consider adding a docstring that explains the parsing logic, error conditions, and provides examples of the supported formats.

Suggested change
/// 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"
/// Parses multiple multiaddresses from a string input.
///
/// # Supported 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"`
///
/// The function attempts to parse the input string as a list of multiaddresses using the following logic:
/// 1. If the input contains commas, it is split by commas and each part is parsed as a separate multiaddress.
/// 2. If the input contains spaces, it is split by whitespace and each part is parsed as a separate multiaddress.
/// 3. Otherwise, the input is parsed as a single multiaddress, or as concatenated multiaddresses if supported.
///
/// # Error conditions
/// - If any substring fails to parse as a valid `Multiaddr`, the function returns an error.
/// - The error is propagated from `Multiaddr::from_str`.
///
/// # Examples
/// ```
/// // Comma-separated
/// let input = "/ip4/1.2.3.4/tcp/1234,/ip4/5.6.7.8/tcp/5678";
/// let addrs = parse_multiple_multiaddrs(input)?;
///
/// // Space-separated
/// let input = "/ip4/1.2.3.4/tcp/1234 /ip4/5.6.7.8/tcp/5678";
/// let addrs = parse_multiple_multiaddrs(input)?;
///
/// // Concatenated
/// let input = "/ip4/1.2.3.4/tcp/1234/ip4/5.6.7.8/tcp/5678";
/// let addrs = parse_multiple_multiaddrs(input)?;
/// ```

Copilot uses AI. Check for mistakes.
fn parse_multiple_multiaddrs(input: &str) -> Result<Vec<Multiaddr>, 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 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() {
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,
Expand Down Expand Up @@ -524,8 +708,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);
Copy link

Copilot AI Aug 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider tracking and returning information about failed dial attempts to the caller, as silently continuing after failures might mask important connectivity issues.

Copilot uses AI. Check for mistakes.
}
}
}

Ok(client)
Expand Down
Loading