UNPKG

@projectlibertylabs/p2p-peer-test

Version:

CLI tool to test libp2p connections and discover peer protocols

241 lines (213 loc) 8.14 kB
import { createLibp2p } from 'libp2p'; import { webSockets } from '@libp2p/websockets'; import { tcp } from '@libp2p/tcp'; import { identify } from '@libp2p/identify'; import { ping } from '@libp2p/ping'; import { noise } from '@chainsafe/libp2p-noise'; import { yamux } from '@chainsafe/libp2p-yamux'; import { multiaddr } from '@multiformats/multiaddr'; import { DEFAULT_CONFIG, ERROR_MESSAGES } from './config.js'; import { createPromiseWithTimeout, measureDuration } from './utils.js'; /** * Creates a libp2p node with standard configuration * @returns {Promise<Object>} Configured libp2p node instance */ const createLibp2pNode = () => createLibp2p({ addresses: { listen: DEFAULT_CONFIG.LISTEN_ADDRESSES }, transports: [webSockets(), tcp()], connectionEncrypters: [noise()], streamMuxers: [yamux()], services: { identify: identify(), ping: ping({ timeout: DEFAULT_CONFIG.IDENTIFY_TIMEOUT }) }, connectionManager: { maxConnections: DEFAULT_CONFIG.CONNECTION_MAX } }); /** * Discovers protocols supported by the connected peer * @param {Object} node - The libp2p node instance * @param {Object} connection - The active connection to the peer * @returns {Promise<string[]>} Array of protocol strings supported by the peer */ const discoverProtocols = async (node, connection) => { // Try peer store first const peerStoreProtocols = await tryGetPeerStoreProtocols(node, connection); if (peerStoreProtocols.length > 0) return peerStoreProtocols; // Try connection streams const streamProtocols = tryGetStreamProtocols(connection); if (streamProtocols.length > 0) return streamProtocols; // Fallback based on connection type return getFallbackProtocols(connection); }; /** * Attempts to get protocols from the peer store * @param {Object} node - The libp2p node instance * @param {Object} connection - The active connection to the peer * @returns {Promise<string[]>} Array of protocols from peer store or empty array */ const tryGetPeerStoreProtocols = async (node, connection) => { try { await new Promise((resolve) => setTimeout(resolve, DEFAULT_CONFIG.PROTOCOL_DISCOVERY_DELAY)); const peer = await node.peerStore.get(connection.remotePeer); return Array.from(peer.protocols || []); } catch { return []; } }; /** * Attempts to get protocols from connection streams * @param {Object} connection - The active connection to the peer * @returns {string[]} Array of protocols from streams or empty array */ const tryGetStreamProtocols = (connection) => { try { const streams = connection.streams; return streams && streams.length > 0 ? streams.map((stream) => stream.protocol || 'unknown') : []; } catch { return []; } }; /** * Provides fallback protocols based on connection type * @param {Object} connection - The active connection to the peer * @returns {string[]} Array with fallback protocol based on connection transport */ const getFallbackProtocols = (connection) => [ connection.remoteAddr.toString().includes('ws') ? 'libp2p/websocket' : 'libp2p/tcp' ]; /** * Extracts the expected peer ID from a multiaddr string * @param {string} multiaddrStr - The multiaddr string * @returns {string|null} The expected peer ID or null if not specified */ export function extractExpectedPeerId(multiaddrStr) { const p2pMatch = multiaddrStr.match(/\/p2p\/([^/]+)/); return p2pMatch ? p2pMatch[1] : null; } /** * Tests a libp2p connection to a given multiaddr * @param {string} multiaddrStr - The multiaddr string to connect to * @param {number} [timeoutMs=DEFAULT_CONFIG.TIMEOUT_MS] - Connection timeout in milliseconds * @returns {Promise<Object>} Connection result object with success status, protocols, peerId, duration, and error details */ export async function testLibp2pConnection(multiaddrStr, timeoutMs = DEFAULT_CONFIG.TIMEOUT_MS) { let node = null; const startTime = performance.now(); try { const addr = multiaddr(multiaddrStr); const expectedPeerId = extractExpectedPeerId(multiaddrStr); node = await createLibp2pNode(); await node.start(); // Use utility function instead of Promise.race const connection = await createPromiseWithTimeout( node.dial(addr), timeoutMs, ERROR_MESSAGES.CONNECTION_TIMEOUT(timeoutMs) ); const protocols = await discoverProtocols(node, connection); const actualPeerId = connection.remotePeer.toString(); const duration = measureDuration(startTime); await connection.close(); await node.stop(); // Check if peer ID matches expected (if specified in multiaddr) if (expectedPeerId && actualPeerId !== expectedPeerId) { return { success: false, protocols: [], peerId: actualPeerId, expectedPeerId, error: `Peer ID mismatch: expected ${expectedPeerId}, got ${actualPeerId}`, details: 'Connected to wrong peer - check the multiaddr peer ID component', duration }; } return { success: true, protocols, peerId: actualPeerId, duration }; } catch (error) { if (node) { try { console.error('Connection Failure', error); await node.stop(); } catch (e) { console.warn('Connection cleanup failure', e); } } const errorMessage = error instanceof Error ? error.message : String(error); const duration = measureDuration(startTime); return { success: false, protocols: [], error: errorMessage, details: getErrorDetails(errorMessage), duration }; } } /** * Provides detailed error messages and hints based on error patterns * @param {string} errorMessage - The error message to analyze * @returns {string} Detailed error description with troubleshooting hints */ function getErrorDetails(errorMessage) { // Network errors if (errorMessage.includes('ECONNREFUSED')) { return 'Connection refused - check if the node is running and the port is correct'; } if (errorMessage.includes('ENOTFOUND') || errorMessage.includes('ENOTREACHED')) { return 'Host not found - check the hostname/IP address'; } if (errorMessage.includes('EHOSTUNREACH')) { return 'Host unreachable - check network connectivity and firewall settings'; } if (errorMessage.includes('ENETUNREACH')) { return 'Network unreachable - check your internet connection'; } // Timeout errors if (errorMessage.includes('timeout') || errorMessage.includes('ETIMEDOUT')) { return 'Connection timeout - the node may be unreachable or overloaded'; } // Protocol errors if (errorMessage.includes('protocol') || errorMessage.includes('handshake')) { return 'Protocol mismatch - ensure the node supports libp2p connections'; } if (errorMessage.includes('noise') || errorMessage.includes('encryption')) { return 'Encryption handshake failed - the node may not support Noise protocol'; } if (errorMessage.includes('yamux') || errorMessage.includes('mux')) { return 'Stream multiplexing failed - the node may not support Yamux'; } // Transport errors if (errorMessage.includes('websocket') || errorMessage.includes('ws')) { return 'WebSocket connection failed - check WebSocket support and network connectivity'; } if (errorMessage.includes('tcp')) { return 'TCP connection failed - check network connectivity and firewall settings'; } // DNS resolution errors if (errorMessage.includes('dns4') || errorMessage.includes('dns6')) { return 'DNS resolution failed - check hostname or try using IP address instead'; } // Multiaddr errors if (errorMessage.includes('multiaddr') || errorMessage.includes('invalid address')) { return 'Invalid multiaddr format - check the syntax and protocol components'; } // Peer errors if (errorMessage.includes('peer') || errorMessage.includes('identify')) { return 'Peer identification failed - the remote node may not be a libp2p node'; } return 'Unknown connection error - check logs for more details'; } export const EXAMPLE_MULTIADDRS = [ '/ip4/127.0.0.1/tcp/30333', '/ip4/127.0.0.1/tcp/30334', '/ip4/127.0.0.1/tcp/30333/ws', '/dns/bootnode-0.polkadot.io/tcp/30333/p2p/12D3KooWEdsXX9ejydkzw6nt4jKBrGg8T5zwcKC5TXv2RUNt5khz', '/dns/bootnode-1.polkadot.io/tcp/30333/p2p/12D3KooWAtJF1vTYdgBbHmIajSFHwtZuq8Xza1Fb6Lwsb4r4gN5p' ];