ipfrs_network/
ipfs_compat.rs

1//! IPFS compatibility and connectivity testing
2//!
3//! This module provides functionality to test connectivity and interoperability
4//! with the public IPFS network, including:
5//! - Connection to public IPFS bootstrap nodes
6//! - Protocol compatibility verification
7//! - Block exchange testing
8//! - DHT query interoperability
9
10use crate::{NetworkConfig, NetworkNode};
11use anyhow::{bail, Context, Result};
12use cid::Cid;
13use libp2p::{Multiaddr, PeerId};
14use std::str::FromStr;
15use std::time::Duration;
16use tracing::{debug, info, warn};
17
18/// Public IPFS bootstrap nodes for testing
19pub const IPFS_BOOTSTRAP_NODES: &[&str] = &[
20    // Protocol Labs bootstrap nodes
21    "/dnsaddr/bootstrap.libp2p.io/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN",
22    "/dnsaddr/bootstrap.libp2p.io/p2p/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa",
23    "/dnsaddr/bootstrap.libp2p.io/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb",
24    "/dnsaddr/bootstrap.libp2p.io/p2p/QmcZf59bWwK5XFi76CZX8cbJ4BhTzzA3gU1ZjYZcYW3dwt",
25    // IPFS.io bootstrap nodes
26    "/ip4/104.131.131.82/tcp/4001/p2p/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ",
27    "/ip4/104.131.131.82/udp/4001/quic/p2p/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ",
28];
29
30/// Well-known IPFS CIDs for testing block retrieval
31pub const TEST_CIDS: &[&str] = &[
32    // IPFS logo (small file)
33    "QmZULkCELmmk5XNfCgTnCyFgAVxBRBXyDHGGMVoLFLiXEN",
34    // Hello World example
35    "QmWATWQ7fVPP2EFGu71UkfnqhYXDYH566qy47CnJDgvs8u",
36];
37
38/// IPFS compatibility test results
39#[derive(Debug, Clone)]
40pub struct IpfsCompatTestResults {
41    /// Whether connection to bootstrap nodes succeeded
42    pub bootstrap_connected: bool,
43    /// Number of IPFS nodes successfully connected
44    pub connected_ipfs_nodes: usize,
45    /// Whether DHT queries work with IPFS nodes
46    pub dht_queries_work: bool,
47    /// Whether identify protocol works
48    pub identify_protocol_works: bool,
49    /// Whether ping protocol works
50    pub ping_protocol_works: bool,
51    /// Whether provider records work
52    pub provider_records_work: bool,
53    /// Test duration
54    pub test_duration: Duration,
55    /// Errors encountered during testing
56    pub errors: Vec<String>,
57}
58
59impl IpfsCompatTestResults {
60    /// Check if all tests passed
61    pub fn all_passed(&self) -> bool {
62        self.bootstrap_connected
63            && self.connected_ipfs_nodes > 0
64            && self.dht_queries_work
65            && self.identify_protocol_works
66            && self.ping_protocol_works
67            && self.errors.is_empty()
68    }
69
70    /// Get a summary of the test results
71    pub fn summary(&self) -> String {
72        format!(
73            "IPFS Compatibility Test Results:\n\
74             - Bootstrap connected: {}\n\
75             - Connected IPFS nodes: {}\n\
76             - DHT queries work: {}\n\
77             - Identify protocol works: {}\n\
78             - Ping protocol works: {}\n\
79             - Provider records work: {}\n\
80             - Test duration: {:?}\n\
81             - Errors: {}",
82            self.bootstrap_connected,
83            self.connected_ipfs_nodes,
84            self.dht_queries_work,
85            self.identify_protocol_works,
86            self.ping_protocol_works,
87            self.provider_records_work,
88            self.test_duration,
89            if self.errors.is_empty() {
90                "None".to_string()
91            } else {
92                format!("{}", self.errors.len())
93            }
94        )
95    }
96}
97
98/// Test connectivity to public IPFS network
99///
100/// This function performs a comprehensive test of IPFS compatibility:
101/// 1. Connects to public IPFS bootstrap nodes
102/// 2. Verifies protocol compatibility (identify, ping)
103/// 3. Tests DHT queries
104/// 4. Tests provider record publishing and querying
105///
106/// # Arguments
107///
108/// * `node` - Network node to test with
109/// * `timeout` - Overall test timeout
110///
111/// # Returns
112///
113/// Test results including success/failure status and detailed metrics
114pub async fn test_ipfs_connectivity(
115    node: &mut NetworkNode,
116    timeout: Duration,
117) -> Result<IpfsCompatTestResults> {
118    let start = std::time::Instant::now();
119    let mut results = IpfsCompatTestResults {
120        bootstrap_connected: false,
121        connected_ipfs_nodes: 0,
122        dht_queries_work: false,
123        identify_protocol_works: false,
124        ping_protocol_works: false,
125        provider_records_work: false,
126        test_duration: Duration::default(),
127        errors: Vec::new(),
128    };
129
130    info!("Starting IPFS compatibility test");
131
132    // Test 1: Connect to bootstrap nodes
133    info!("Test 1: Connecting to IPFS bootstrap nodes");
134    match test_bootstrap_connection(node, timeout).await {
135        Ok(count) => {
136            results.bootstrap_connected = true;
137            results.connected_ipfs_nodes = count;
138            info!("Successfully connected to {} IPFS bootstrap nodes", count);
139        }
140        Err(e) => {
141            warn!("Failed to connect to IPFS bootstrap nodes: {}", e);
142            results
143                .errors
144                .push(format!("Bootstrap connection failed: {}", e));
145        }
146    }
147
148    // Test 2: Verify identify protocol
149    info!("Test 2: Testing identify protocol");
150    match test_identify_protocol(node).await {
151        Ok(_) => {
152            results.identify_protocol_works = true;
153            info!("Identify protocol works");
154        }
155        Err(e) => {
156            warn!("Identify protocol test failed: {}", e);
157            results
158                .errors
159                .push(format!("Identify protocol failed: {}", e));
160        }
161    }
162
163    // Test 3: Verify ping protocol
164    info!("Test 3: Testing ping protocol");
165    match test_ping_protocol(node).await {
166        Ok(_) => {
167            results.ping_protocol_works = true;
168            info!("Ping protocol works");
169        }
170        Err(e) => {
171            warn!("Ping protocol test failed: {}", e);
172            results.errors.push(format!("Ping protocol failed: {}", e));
173        }
174    }
175
176    // Test 4: Test DHT queries
177    info!("Test 4: Testing DHT queries");
178    match test_dht_queries(node, timeout).await {
179        Ok(_) => {
180            results.dht_queries_work = true;
181            info!("DHT queries work");
182        }
183        Err(e) => {
184            warn!("DHT query test failed: {}", e);
185            results.errors.push(format!("DHT queries failed: {}", e));
186        }
187    }
188
189    // Test 5: Test provider records
190    info!("Test 5: Testing provider records");
191    match test_provider_records(node, timeout).await {
192        Ok(_) => {
193            results.provider_records_work = true;
194            info!("Provider records work");
195        }
196        Err(e) => {
197            warn!("Provider record test failed: {}", e);
198            results
199                .errors
200                .push(format!("Provider records failed: {}", e));
201        }
202    }
203
204    results.test_duration = start.elapsed();
205    info!(
206        "IPFS compatibility test completed in {:?}",
207        results.test_duration
208    );
209    info!("{}", results.summary());
210
211    Ok(results)
212}
213
214/// Test connection to IPFS bootstrap nodes
215async fn test_bootstrap_connection(node: &mut NetworkNode, timeout: Duration) -> Result<usize> {
216    let mut connected_count = 0;
217
218    for bootstrap_addr_str in IPFS_BOOTSTRAP_NODES.iter().take(3) {
219        // Test with first 3 nodes
220        match parse_multiaddr_with_peer(bootstrap_addr_str) {
221            Ok((addr, _peer_id)) => {
222                debug!("Attempting to connect to bootstrap node: {}", addr);
223                match tokio::time::timeout(timeout, node.connect(addr.clone())).await {
224                    Ok(Ok(_)) => {
225                        connected_count += 1;
226                        info!("Connected to bootstrap node: {}", addr);
227                    }
228                    Ok(Err(e)) => {
229                        warn!("Failed to connect to {}: {}", addr, e);
230                    }
231                    Err(_) => {
232                        warn!("Timeout connecting to {}", addr);
233                    }
234                }
235            }
236            Err(e) => {
237                warn!(
238                    "Failed to parse bootstrap address {}: {}",
239                    bootstrap_addr_str, e
240                );
241            }
242        }
243    }
244
245    if connected_count == 0 {
246        bail!("Failed to connect to any IPFS bootstrap nodes");
247    }
248
249    Ok(connected_count)
250}
251
252/// Test identify protocol functionality
253async fn test_identify_protocol(node: &NetworkNode) -> Result<()> {
254    // The identify protocol is built into the node, so if we have connections,
255    // it should be working. We can verify by checking peer count.
256    let peer_count = node.get_peer_count();
257    if peer_count == 0 {
258        bail!("No peers connected, cannot test identify protocol");
259    }
260
261    debug!("Identify protocol operational with {} peers", peer_count);
262    Ok(())
263}
264
265/// Test ping protocol functionality
266async fn test_ping_protocol(node: &NetworkNode) -> Result<()> {
267    // The ping protocol is built into the node and runs automatically
268    // We verify it's working by checking if we have active connections
269    let peer_count = node.get_peer_count();
270    if peer_count == 0 {
271        bail!("No peers connected, cannot test ping protocol");
272    }
273
274    debug!("Ping protocol operational with {} peers", peer_count);
275    Ok(())
276}
277
278/// Test DHT query functionality
279async fn test_dht_queries(node: &mut NetworkNode, timeout: Duration) -> Result<()> {
280    // Test DHT by doing a bootstrap
281    debug!("Bootstrapping DHT");
282    match tokio::time::timeout(timeout, node.bootstrap_dht()).await {
283        Ok(Ok(_)) => {
284            info!("DHT bootstrap successful");
285            Ok(())
286        }
287        Ok(Err(e)) => {
288            bail!("DHT bootstrap failed: {}", e);
289        }
290        Err(_) => {
291            bail!("DHT bootstrap timed out");
292        }
293    }
294}
295
296/// Test provider record publishing and querying
297async fn test_provider_records(node: &mut NetworkNode, timeout: Duration) -> Result<()> {
298    // Create a test CID for provider testing
299    let test_cid = Cid::from_str(TEST_CIDS[0]).context("Failed to parse test CID")?;
300
301    debug!("Testing provider records with CID: {}", test_cid);
302
303    // Publish provider record
304    match tokio::time::timeout(timeout, node.provide(&test_cid)).await {
305        Ok(Ok(_)) => {
306            info!("Successfully published provider record");
307        }
308        Ok(Err(e)) => {
309            warn!("Failed to publish provider record: {}", e);
310            // Don't fail the test completely, as this might work later
311        }
312        Err(_) => {
313            warn!("Provider record publish timed out");
314        }
315    }
316
317    // Try to find providers (even if we just published, this tests the query path)
318    match tokio::time::timeout(timeout, node.find_providers(&test_cid)).await {
319        Ok(Ok(_)) => {
320            info!("Successfully queried for providers of test CID");
321            Ok(())
322        }
323        Ok(Err(e)) => {
324            warn!("Failed to find providers: {}", e);
325            // This is expected if the content doesn't exist in the network
326            Ok(())
327        }
328        Err(_) => {
329            warn!("Provider query timed out");
330            Ok(())
331        }
332    }
333}
334
335/// Parse a multiaddr string that includes a peer ID
336fn parse_multiaddr_with_peer(addr_str: &str) -> Result<(Multiaddr, Option<PeerId>)> {
337    let addr = Multiaddr::from_str(addr_str).context("Failed to parse multiaddr")?;
338
339    // Extract peer ID from multiaddr if present
340    let peer_id = addr.iter().find_map(|protocol| {
341        if let libp2p::multiaddr::Protocol::P2p(peer_id) = protocol {
342            Some(peer_id)
343        } else {
344            None
345        }
346    });
347
348    Ok((addr, peer_id))
349}
350
351/// Create a network configuration optimized for IPFS compatibility testing
352pub fn ipfs_test_config() -> NetworkConfig {
353    NetworkConfig {
354        bootstrap_peers: IPFS_BOOTSTRAP_NODES.iter().map(|s| s.to_string()).collect(),
355        enable_quic: true,
356        enable_mdns: false, // Not needed for public network testing
357        enable_nat_traversal: true,
358        ..Default::default()
359    }
360}
361
362#[cfg(test)]
363mod tests {
364    use super::*;
365
366    #[test]
367    fn test_parse_multiaddr_with_peer() {
368        let addr_str =
369            "/ip4/104.131.131.82/tcp/4001/p2p/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ";
370        let result = parse_multiaddr_with_peer(addr_str);
371        assert!(result.is_ok());
372
373        let (addr, peer_id) = result.unwrap();
374        assert!(peer_id.is_some());
375        assert!(addr.to_string().contains("104.131.131.82"));
376    }
377
378    #[test]
379    fn test_ipfs_bootstrap_nodes_valid() {
380        for addr_str in IPFS_BOOTSTRAP_NODES {
381            let result = parse_multiaddr_with_peer(addr_str);
382            assert!(result.is_ok(), "Invalid bootstrap address: {}", addr_str);
383        }
384    }
385
386    #[test]
387    fn test_cids_valid() {
388        for cid_str in TEST_CIDS {
389            let result = Cid::from_str(cid_str);
390            assert!(result.is_ok(), "Invalid test CID: {}", cid_str);
391        }
392    }
393
394    #[test]
395    fn test_ipfs_test_config() {
396        let config = ipfs_test_config();
397        assert!(config.enable_quic);
398        assert!(config.enable_nat_traversal);
399        assert!(!config.bootstrap_peers.is_empty());
400        assert_eq!(config.kademlia.replication_factor, 20);
401    }
402
403    #[test]
404    fn test_compat_results_all_passed() {
405        let mut results = IpfsCompatTestResults {
406            bootstrap_connected: true,
407            connected_ipfs_nodes: 3,
408            dht_queries_work: true,
409            identify_protocol_works: true,
410            ping_protocol_works: true,
411            provider_records_work: true,
412            test_duration: Duration::from_secs(10),
413            errors: Vec::new(),
414        };
415
416        assert!(results.all_passed());
417
418        results.errors.push("Test error".to_string());
419        assert!(!results.all_passed());
420    }
421
422    #[test]
423    fn test_compat_results_summary() {
424        let results = IpfsCompatTestResults {
425            bootstrap_connected: true,
426            connected_ipfs_nodes: 3,
427            dht_queries_work: true,
428            identify_protocol_works: true,
429            ping_protocol_works: true,
430            provider_records_work: false,
431            test_duration: Duration::from_secs(10),
432            errors: vec!["Test error".to_string()],
433        };
434
435        let summary = results.summary();
436        assert!(summary.contains("Bootstrap connected: true"));
437        assert!(summary.contains("Connected IPFS nodes: 3"));
438        assert!(summary.contains("Errors: 1"));
439    }
440}