ipfrs-network 0.2.0

Peer-to-peer networking layer with libp2p and QUIC for IPFRS
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
//! IPFS compatibility and connectivity testing
//!
//! This module provides functionality to test connectivity and interoperability
//! with the public IPFS network, including:
//! - Connection to public IPFS bootstrap nodes
//! - Protocol compatibility verification
//! - Block exchange testing
//! - DHT query interoperability

use crate::{NetworkConfig, NetworkNode};
use anyhow::{bail, Context, Result};
use cid::Cid;
use libp2p::{Multiaddr, PeerId};
use std::str::FromStr;
use std::time::Duration;
use tracing::{debug, info, warn};

/// Public IPFS bootstrap nodes for testing
pub const IPFS_BOOTSTRAP_NODES: &[&str] = &[
    // Protocol Labs bootstrap nodes
    "/dnsaddr/bootstrap.libp2p.io/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN",
    "/dnsaddr/bootstrap.libp2p.io/p2p/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa",
    "/dnsaddr/bootstrap.libp2p.io/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb",
    "/dnsaddr/bootstrap.libp2p.io/p2p/QmcZf59bWwK5XFi76CZX8cbJ4BhTzzA3gU1ZjYZcYW3dwt",
    // IPFS.io bootstrap nodes
    "/ip4/104.131.131.82/tcp/4001/p2p/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ",
    "/ip4/104.131.131.82/udp/4001/quic/p2p/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ",
];

/// Well-known IPFS CIDs for testing block retrieval
pub const TEST_CIDS: &[&str] = &[
    // IPFS logo (small file)
    "QmZULkCELmmk5XNfCgTnCyFgAVxBRBXyDHGGMVoLFLiXEN",
    // Hello World example
    "QmWATWQ7fVPP2EFGu71UkfnqhYXDYH566qy47CnJDgvs8u",
];

/// IPFS compatibility test results
#[derive(Debug, Clone)]
pub struct IpfsCompatTestResults {
    /// Whether connection to bootstrap nodes succeeded
    pub bootstrap_connected: bool,
    /// Number of IPFS nodes successfully connected
    pub connected_ipfs_nodes: usize,
    /// Whether DHT queries work with IPFS nodes
    pub dht_queries_work: bool,
    /// Whether identify protocol works
    pub identify_protocol_works: bool,
    /// Whether ping protocol works
    pub ping_protocol_works: bool,
    /// Whether provider records work
    pub provider_records_work: bool,
    /// Test duration
    pub test_duration: Duration,
    /// Errors encountered during testing
    pub errors: Vec<String>,
}

impl IpfsCompatTestResults {
    /// Check if all tests passed
    pub fn all_passed(&self) -> bool {
        self.bootstrap_connected
            && self.connected_ipfs_nodes > 0
            && self.dht_queries_work
            && self.identify_protocol_works
            && self.ping_protocol_works
            && self.errors.is_empty()
    }

    /// Get a summary of the test results
    pub fn summary(&self) -> String {
        format!(
            "IPFS Compatibility Test Results:\n\
             - Bootstrap connected: {}\n\
             - Connected IPFS nodes: {}\n\
             - DHT queries work: {}\n\
             - Identify protocol works: {}\n\
             - Ping protocol works: {}\n\
             - Provider records work: {}\n\
             - Test duration: {:?}\n\
             - Errors: {}",
            self.bootstrap_connected,
            self.connected_ipfs_nodes,
            self.dht_queries_work,
            self.identify_protocol_works,
            self.ping_protocol_works,
            self.provider_records_work,
            self.test_duration,
            if self.errors.is_empty() {
                "None".to_string()
            } else {
                format!("{}", self.errors.len())
            }
        )
    }
}

/// Test connectivity to public IPFS network
///
/// This function performs a comprehensive test of IPFS compatibility:
/// 1. Connects to public IPFS bootstrap nodes
/// 2. Verifies protocol compatibility (identify, ping)
/// 3. Tests DHT queries
/// 4. Tests provider record publishing and querying
///
/// # Arguments
///
/// * `node` - Network node to test with
/// * `timeout` - Overall test timeout
///
/// # Returns
///
/// Test results including success/failure status and detailed metrics
pub async fn test_ipfs_connectivity(
    node: &mut NetworkNode,
    timeout: Duration,
) -> Result<IpfsCompatTestResults> {
    let start = std::time::Instant::now();
    let mut results = IpfsCompatTestResults {
        bootstrap_connected: false,
        connected_ipfs_nodes: 0,
        dht_queries_work: false,
        identify_protocol_works: false,
        ping_protocol_works: false,
        provider_records_work: false,
        test_duration: Duration::default(),
        errors: Vec::new(),
    };

    info!("Starting IPFS compatibility test");

    // Test 1: Connect to bootstrap nodes
    info!("Test 1: Connecting to IPFS bootstrap nodes");
    match test_bootstrap_connection(node, timeout).await {
        Ok(count) => {
            results.bootstrap_connected = true;
            results.connected_ipfs_nodes = count;
            info!("Successfully connected to {} IPFS bootstrap nodes", count);
        }
        Err(e) => {
            warn!("Failed to connect to IPFS bootstrap nodes: {}", e);
            results
                .errors
                .push(format!("Bootstrap connection failed: {}", e));
        }
    }

    // Test 2: Verify identify protocol
    info!("Test 2: Testing identify protocol");
    match test_identify_protocol(node).await {
        Ok(_) => {
            results.identify_protocol_works = true;
            info!("Identify protocol works");
        }
        Err(e) => {
            warn!("Identify protocol test failed: {}", e);
            results
                .errors
                .push(format!("Identify protocol failed: {}", e));
        }
    }

    // Test 3: Verify ping protocol
    info!("Test 3: Testing ping protocol");
    match test_ping_protocol(node).await {
        Ok(_) => {
            results.ping_protocol_works = true;
            info!("Ping protocol works");
        }
        Err(e) => {
            warn!("Ping protocol test failed: {}", e);
            results.errors.push(format!("Ping protocol failed: {}", e));
        }
    }

    // Test 4: Test DHT queries
    info!("Test 4: Testing DHT queries");
    match test_dht_queries(node, timeout).await {
        Ok(_) => {
            results.dht_queries_work = true;
            info!("DHT queries work");
        }
        Err(e) => {
            warn!("DHT query test failed: {}", e);
            results.errors.push(format!("DHT queries failed: {}", e));
        }
    }

    // Test 5: Test provider records
    info!("Test 5: Testing provider records");
    match test_provider_records(node, timeout).await {
        Ok(_) => {
            results.provider_records_work = true;
            info!("Provider records work");
        }
        Err(e) => {
            warn!("Provider record test failed: {}", e);
            results
                .errors
                .push(format!("Provider records failed: {}", e));
        }
    }

    results.test_duration = start.elapsed();
    info!(
        "IPFS compatibility test completed in {:?}",
        results.test_duration
    );
    info!("{}", results.summary());

    Ok(results)
}

/// Test connection to IPFS bootstrap nodes
async fn test_bootstrap_connection(node: &mut NetworkNode, timeout: Duration) -> Result<usize> {
    let mut connected_count = 0;

    for bootstrap_addr_str in IPFS_BOOTSTRAP_NODES.iter().take(3) {
        // Test with first 3 nodes
        match parse_multiaddr_with_peer(bootstrap_addr_str) {
            Ok((addr, _peer_id)) => {
                debug!("Attempting to connect to bootstrap node: {}", addr);
                match tokio::time::timeout(timeout, node.connect(addr.clone())).await {
                    Ok(Ok(_)) => {
                        connected_count += 1;
                        info!("Connected to bootstrap node: {}", addr);
                    }
                    Ok(Err(e)) => {
                        warn!("Failed to connect to {}: {}", addr, e);
                    }
                    Err(_) => {
                        warn!("Timeout connecting to {}", addr);
                    }
                }
            }
            Err(e) => {
                warn!(
                    "Failed to parse bootstrap address {}: {}",
                    bootstrap_addr_str, e
                );
            }
        }
    }

    if connected_count == 0 {
        bail!("Failed to connect to any IPFS bootstrap nodes");
    }

    Ok(connected_count)
}

/// Test identify protocol functionality
async fn test_identify_protocol(node: &NetworkNode) -> Result<()> {
    // The identify protocol is built into the node, so if we have connections,
    // it should be working. We can verify by checking peer count.
    let peer_count = node.get_peer_count();
    if peer_count == 0 {
        bail!("No peers connected, cannot test identify protocol");
    }

    debug!("Identify protocol operational with {} peers", peer_count);
    Ok(())
}

/// Test ping protocol functionality
async fn test_ping_protocol(node: &NetworkNode) -> Result<()> {
    // The ping protocol is built into the node and runs automatically
    // We verify it's working by checking if we have active connections
    let peer_count = node.get_peer_count();
    if peer_count == 0 {
        bail!("No peers connected, cannot test ping protocol");
    }

    debug!("Ping protocol operational with {} peers", peer_count);
    Ok(())
}

/// Test DHT query functionality
async fn test_dht_queries(node: &mut NetworkNode, timeout: Duration) -> Result<()> {
    // Test DHT by doing a bootstrap
    debug!("Bootstrapping DHT");
    match tokio::time::timeout(timeout, node.bootstrap_dht()).await {
        Ok(Ok(_)) => {
            info!("DHT bootstrap successful");
            Ok(())
        }
        Ok(Err(e)) => {
            bail!("DHT bootstrap failed: {}", e);
        }
        Err(_) => {
            bail!("DHT bootstrap timed out");
        }
    }
}

/// Test provider record publishing and querying
async fn test_provider_records(node: &mut NetworkNode, timeout: Duration) -> Result<()> {
    // Create a test CID for provider testing
    let test_cid = Cid::from_str(TEST_CIDS[0]).context("Failed to parse test CID")?;

    debug!("Testing provider records with CID: {}", test_cid);

    // Publish provider record
    match tokio::time::timeout(timeout, node.provide(&test_cid)).await {
        Ok(Ok(_)) => {
            info!("Successfully published provider record");
        }
        Ok(Err(e)) => {
            warn!("Failed to publish provider record: {}", e);
            // Don't fail the test completely, as this might work later
        }
        Err(_) => {
            warn!("Provider record publish timed out");
        }
    }

    // Try to find providers (even if we just published, this tests the query path)
    match tokio::time::timeout(timeout, node.find_providers(&test_cid)).await {
        Ok(Ok(_)) => {
            info!("Successfully queried for providers of test CID");
            Ok(())
        }
        Ok(Err(e)) => {
            warn!("Failed to find providers: {}", e);
            // This is expected if the content doesn't exist in the network
            Ok(())
        }
        Err(_) => {
            warn!("Provider query timed out");
            Ok(())
        }
    }
}

/// Parse a multiaddr string that includes a peer ID
fn parse_multiaddr_with_peer(addr_str: &str) -> Result<(Multiaddr, Option<PeerId>)> {
    let addr = Multiaddr::from_str(addr_str).context("Failed to parse multiaddr")?;

    // Extract peer ID from multiaddr if present
    let peer_id = addr.iter().find_map(|protocol| {
        if let libp2p::multiaddr::Protocol::P2p(peer_id) = protocol {
            Some(peer_id)
        } else {
            None
        }
    });

    Ok((addr, peer_id))
}

/// Create a network configuration optimized for IPFS compatibility testing
pub fn ipfs_test_config() -> NetworkConfig {
    NetworkConfig {
        bootstrap_peers: IPFS_BOOTSTRAP_NODES.iter().map(|s| s.to_string()).collect(),
        enable_quic: true,
        enable_mdns: false, // Not needed for public network testing
        enable_nat_traversal: true,
        ..Default::default()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_parse_multiaddr_with_peer() {
        let addr_str =
            "/ip4/104.131.131.82/tcp/4001/p2p/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ";
        let result = parse_multiaddr_with_peer(addr_str);
        assert!(result.is_ok());

        let (addr, peer_id) =
            result.expect("test: parse_multiaddr_with_peer should succeed for valid addr");
        assert!(peer_id.is_some());
        assert!(addr.to_string().contains("104.131.131.82"));
    }

    #[test]
    fn test_ipfs_bootstrap_nodes_valid() {
        for addr_str in IPFS_BOOTSTRAP_NODES {
            let result = parse_multiaddr_with_peer(addr_str);
            assert!(result.is_ok(), "Invalid bootstrap address: {}", addr_str);
        }
    }

    #[test]
    fn test_cids_valid() {
        for cid_str in TEST_CIDS {
            let result = Cid::from_str(cid_str);
            assert!(result.is_ok(), "Invalid test CID: {}", cid_str);
        }
    }

    #[test]
    fn test_ipfs_test_config() {
        let config = ipfs_test_config();
        assert!(config.enable_quic);
        assert!(config.enable_nat_traversal);
        assert!(!config.bootstrap_peers.is_empty());
        assert_eq!(config.kademlia.replication_factor, 20);
    }

    #[test]
    fn test_compat_results_all_passed() {
        let mut results = IpfsCompatTestResults {
            bootstrap_connected: true,
            connected_ipfs_nodes: 3,
            dht_queries_work: true,
            identify_protocol_works: true,
            ping_protocol_works: true,
            provider_records_work: true,
            test_duration: Duration::from_secs(10),
            errors: Vec::new(),
        };

        assert!(results.all_passed());

        results.errors.push("Test error".to_string());
        assert!(!results.all_passed());
    }

    #[test]
    fn test_compat_results_summary() {
        let results = IpfsCompatTestResults {
            bootstrap_connected: true,
            connected_ipfs_nodes: 3,
            dht_queries_work: true,
            identify_protocol_works: true,
            ping_protocol_works: true,
            provider_records_work: false,
            test_duration: Duration::from_secs(10),
            errors: vec!["Test error".to_string()],
        };

        let summary = results.summary();
        assert!(summary.contains("Bootstrap connected: true"));
        assert!(summary.contains("Connected IPFS nodes: 3"));
        assert!(summary.contains("Errors: 1"));
    }
}