diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 245d5908..b18f1107 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -55,7 +55,7 @@ Verity is a decentralized and censorship-resistant data storage and distribution npm run start -- -w 1984 -t ``` - Runs a full Verity network node on port 1984 -- Will show network connectivity warnings in sandboxed environments (normal) +- May not have external connectivity in sandboxed environments (normal) - Use Ctrl+C to stop #### Web Application Development Server @@ -90,7 +90,7 @@ npm run webpack - **The web application should load successfully** - Application shows the basic Verity UI structure - Service worker registration succeeds -3. Running the full test suite and ensuring no new test failures beyond occasional flaky tests +3. Running the full test suite and ensuring no new test failures beyond tests already marked as flaky 4. Testing any cube operations, identity management, or networking features through the test suite ## Committing and merging @@ -153,7 +153,7 @@ npm run webpack - **Some webpack compilation issues** may occur with specific TypeScript modules - **1900+ linting errors** - do not attempt to fix unless specifically requested - **Occasional flaky test failures** - these are expected and documented in test files -- **Network connectivity warnings** in sandboxed environments (normal for support node) +- **May not have external connectivity** in sandboxed environments (normal for support node) ### Dependencies - **Node.js 20+** required @@ -180,7 +180,7 @@ npm run webpack ### If support node fails to start: - Ensure port 1984 is available -- Network connectivity warnings are normal in sandboxed environments +- May not have external connectivity in sandboxed environments (normal) - Should display ASCII art logo when starting successfully **Always prioritize working functionality (support node, tests, development server, build) over any remaining minor issues.** diff --git a/src/core/networking/transport/libp2p/libp2pTransport.ts b/src/core/networking/transport/libp2p/libp2pTransport.ts index 1f07636f..e5c7f09d 100644 --- a/src/core/networking/transport/libp2p/libp2pTransport.ts +++ b/src/core/networking/transport/libp2p/libp2pTransport.ts @@ -59,7 +59,7 @@ export class Libp2pTransport extends NetworkTransport { `/ip4/0.0.0.0/udp/${listenSpec}/webrtc`, // `/ip6/::1/udp/${listenSpec}/webrtc`, ]); - + // Add WebRTC-Direct listen addresses on Node.js for direct peer-to-peer connections if (isNode) { // Use port 0 to let the system assign an available port for WebRTC-Direct @@ -75,7 +75,7 @@ export class Libp2pTransport extends NetworkTransport { } } if (!this.listen.includes("/webrtc")) this.listen.push("/webrtc"); - + // Always add generic WebRTC-Direct on Node.js for maximum connectivity if (isNode && !this.listen.includes("/webrtc-direct")) { this.listen.push("/webrtc-direct"); @@ -106,7 +106,7 @@ export class Libp2pTransport extends NetworkTransport { filter: filters.all, // allow all kinds of connections for testing, effectively disabling sanitizing - maybe TODO remove this? })); } - + // webRTC (standard WebRTC with circuit relay) transports.push(webRTC({ rtcConfiguration: { @@ -122,7 +122,7 @@ export class Libp2pTransport extends NetworkTransport { }] } })); - + // webRTC-Direct (direct peer-to-peer connections without circuit relay) // Enable on Node.js for maximum connectivity including HTTPS nodes if (isNode) { @@ -138,7 +138,7 @@ export class Libp2pTransport extends NetworkTransport { } })); } - + // relaying - always add circuit relay transport as webRTC requires it in v2 transports.push(circuitRelayTransport()); // addressing (listen and possibly announce, which are basically public address override) @@ -179,7 +179,7 @@ export class Libp2pTransport extends NetworkTransport { await this.server.start(); if (this.options.useRelaying) { // Find the circuit relay transport in the services - // Note: In libp2p v2, services are accessed differently + // Note: In libp2p v2, services are accessed differently try { // Try to access through services if it's exposed there this.circuitRelayTransport = (this.node as any)?.services?.relay; @@ -226,14 +226,14 @@ export class Libp2pTransport extends NetworkTransport { } for (const multiaddr of this.node.getMultiaddrs()) { // TODO rename multiaddr, it conflicts with the multiaddr() creation method (actually not strictly in conflict due to scoping but still confusing) const protos: string[] = multiaddr.protoNames(); - + // Check for WebRTC-Direct addresses (preferred for direct connections) if (protos.includes("p2p") && protos.includes("webrtc-direct")) { this.dialableAddress = new AddressAbstraction(multiaddr); this.emit("serverAddress", this.dialableAddress); return; // Prefer WebRTC-Direct over circuit relay } - + // Fallback to circuit relay WebRTC addresses if (protos.includes("p2p") && protos.includes("p2p-circuit") && protos.includes("webrtc")) { diff --git a/test/core/networking/webrtc_direct_e2e.test.ts b/test/core/networking/webrtc_direct_e2e.test.ts index 5bda29be..dc5b3ec5 100644 --- a/test/core/networking/webrtc_direct_e2e.test.ts +++ b/test/core/networking/webrtc_direct_e2e.test.ts @@ -2,67 +2,202 @@ import 'promise.withresolvers/auto'; import { describe, it, expect } from 'vitest'; -import { createLibp2p } from 'libp2p'; -import { webRTCDirect } from '@libp2p/webrtc'; -import { noise } from '@chainsafe/libp2p-noise'; -import { yamux } from '@chainsafe/libp2p-yamux'; -import { isNode } from 'browser-or-node'; +import { CoreNode } from '../../../src/core/coreNode'; +import { SupportedTransports } from '../../../src/core/networking/networkDefinitions'; +import { AddressAbstraction } from '../../../src/core/peering/addressing'; +import { Cube } from '../../../src/core/cube/cube'; +import { CubeField } from '../../../src/core/cube/cubeField'; +import { CubeType, NotificationKey, CubeFieldType } from '../../../src/core/cube/cube.definitions'; +import { testCoreOptions } from '../testcore.definition'; +import { Buffer } from 'buffer'; +import { Libp2pTransport } from '../../../src/core/networking/transport/libp2p/libp2pTransport'; +import type { NetworkPeerIf } from '../../../src/core/networking/networkPeerIf'; -describe('WebRTC-Direct end-to-end connectivity', () => { - it('should establish direct peer-to-peer connection without relay', async () => { - if (!isNode) { - console.log('Skipping WebRTC-Direct e2e test in browser environment'); - return; - } - - // Create listener node with WebRTC-Direct - const listener = await createLibp2p({ - addresses: { - listen: ['/ip4/0.0.0.0/udp/0/webrtc-direct'] - }, - transports: [webRTCDirect()], - connectionEncrypters: [noise()], - streamMuxers: [yamux()] +describe('WebRTC-Direct end-to-end connectivity with Verity nodes', () => { + it('should configure WebRTC-Direct transport and verify multiaddrs', async () => { + // Create node configured with libp2p transport + // WebRTC-Direct is enabled by default on Node.js in Verity's libp2p configuration + const node: CoreNode = new CoreNode({ + ...testCoreOptions, + lightNode: false, + transports: new Map([ + [SupportedTransports.libp2p, 16001], + ]), }); + + await node.readyPromise; + + // Give the transport some time to start up properly + await new Promise(resolve => setTimeout(resolve, 1000)); - await listener.start(); + // Get the node's transport and verify WebRTC-Direct configuration + const nodeTransport = node.networkManager.transports.get(SupportedTransports.libp2p) as Libp2pTransport; + expect(nodeTransport).toBeDefined(); + + const multiaddrs = nodeTransport!.node.getMultiaddrs(); + console.log('Node multiaddrs:', multiaddrs.map(ma => ma.toString())); + + // Verify WebRTC-Direct addresses are present (enabled by default on Node.js) + const webrtcDirectAddrs = multiaddrs.filter(ma => + ma.toString().includes('/webrtc-direct/') && ma.toString().includes('/certhash/')); - const listenerMultiaddrs = listener.getMultiaddrs(); - console.log('Listener multiaddrs:', listenerMultiaddrs.map(ma => ma.toString())); + expect(webrtcDirectAddrs.length).toBeGreaterThan(0); + console.log('WebRTC-Direct addresses found:', webrtcDirectAddrs.map(ma => ma.toString())); - expect(listenerMultiaddrs.length).toBeGreaterThan(0); - const webrtcDirectAddr = listenerMultiaddrs.find(ma => + // Verify address format + const webrtcDirectAddr = webrtcDirectAddrs[0]; + expect(webrtcDirectAddr.toString()).toMatch(/\/ip4\/[\d\.]+\/udp\/\d+\/webrtc-direct\/certhash\/[a-zA-Z0-9_-]+\/p2p\/[a-zA-Z0-9]+/); + + await node.shutdown(); + }, 8000); + + it('should establish WebRTC-Direct connection and transmit cubes between Verity nodes', async () => { + // Create listener node with WebRTC-Direct enabled (default on Node.js) + const listener: CoreNode = new CoreNode({ + ...testCoreOptions, + lightNode: false, + transports: new Map([ + [SupportedTransports.libp2p, 16005], + ]), + }); + await listener.readyPromise; + + // Wait for transport to start + await new Promise(resolve => setTimeout(resolve, 1000)); + + // Get WebRTC-Direct address + const listenerTransport = listener.networkManager.transports.get(SupportedTransports.libp2p) as Libp2pTransport; + const multiaddrs = listenerTransport!.node.getMultiaddrs(); + const webrtcDirectAddr = multiaddrs.find(ma => ma.toString().includes('/webrtc-direct/') && ma.toString().includes('/certhash/')); + + // Verify WebRTC-Direct is configured expect(webrtcDirectAddr).toBeDefined(); + expect(webrtcDirectAddr!.toString()).toContain('/webrtc-direct/'); + console.log('Using WebRTC-Direct address for connection:', webrtcDirectAddr!.toString()); + + // Set up promise to capture incoming peer on listener side + let listenerToDialer: NetworkPeerIf; + const listenerIncomingPeerPromise = new Promise( + (resolve) => listener.networkManager.once('incomingPeer', (np: NetworkPeerIf) => { + listenerToDialer = np; + resolve(); + })); + + // Create dialer node and attempt actual connection + const dialer: CoreNode = new CoreNode({ + ...testCoreOptions, + lightNode: true, + transports: new Map([ + [SupportedTransports.libp2p, 16006], + ]), + // Use WebRTC-Direct address as initial peer + initialPeers: [new AddressAbstraction(webrtcDirectAddr!.toString())], + }); + + await dialer.readyPromise; + + // Wait for connection to establish and HELLO messages to be exchanged + await Promise.race([ + dialer.onlinePromise, + new Promise((_, reject) => setTimeout(() => reject(new Error('Dialer connection timeout')), 8000)) + ]); + + // Wait for incoming peer to be registered on listener side + await Promise.race([ + listenerIncomingPeerPromise, + new Promise((_, reject) => setTimeout(() => reject(new Error('Listener incoming peer timeout')), 5000)) + ]); + + // Wait for listener side to complete HELLO exchange + await Promise.race([ + listenerToDialer!.onlinePromise, + new Promise((_, reject) => setTimeout(() => reject(new Error('Listener peer online timeout')), 5000)) + ]); - // Create dialer node with WebRTC-Direct - const dialer = await createLibp2p({ - transports: [webRTCDirect()], - connectionEncrypters: [noise()], - streamMuxers: [yamux()] + // Verify connection was established on both sides + expect(dialer.networkManager.onlinePeers.length).toBeGreaterThan(0); + expect(listener.networkManager.onlinePeers.length).toBeGreaterThan(0); + console.log('WebRTC-Direct connection established successfully'); + + // Test cube transmission over WebRTC-Direct + const testCube = Cube.Frozen({ + fields: [ + CubeField.RawContent(CubeType.FROZEN, "WebRTC-Direct e2e test message"), + ], + requiredDifficulty: 0, + }); + + await listener.cubeStore.addCube(testCube); + + // Retrieve cube over WebRTC-Direct connection + const retrievedCube = await Promise.race([ + dialer.cubeRetriever.getCube(testCube.getKeyIfAvailable()!), + new Promise((_, reject) => setTimeout(() => reject(new Error('Cube retrieval timeout')), 3000)) + ]); + + expect(retrievedCube).toBeDefined(); + expect((retrievedCube as Cube).getKeyIfAvailable()).toEqual(testCube.getKeyIfAvailable()); + console.log('Successfully transmitted cube over WebRTC-Direct connection'); + + await Promise.all([ + listener.shutdown(), + dialer.shutdown(), + ]); + }, 12000); + + it('should create WebRTC-Direct-capable nodes for notification delivery', async () => { + // Create sender node with WebRTC-Direct enabled (default on Node.js) + const sender: CoreNode = new CoreNode({ + ...testCoreOptions, + lightNode: true, + transports: new Map([ + [SupportedTransports.libp2p, 16007], + ]), + }); + await sender.readyPromise; + + // Create recipient node with WebRTC-Direct enabled (default on Node.js) + const recipient: CoreNode = new CoreNode({ + ...testCoreOptions, + lightNode: true, + transports: new Map([ + [SupportedTransports.libp2p, 16008], + ]), }); + await recipient.readyPromise; + + // Give transports time to start + await new Promise(resolve => setTimeout(resolve, 1000)); + + // Verify both nodes have WebRTC-Direct capability + const senderTransport = sender.networkManager.transports.get(SupportedTransports.libp2p) as Libp2pTransport; + const recipientTransport = recipient.networkManager.transports.get(SupportedTransports.libp2p) as Libp2pTransport; + + expect(senderTransport).toBeDefined(); + expect(recipientTransport).toBeDefined(); + + const senderAddrs = senderTransport!.node.getMultiaddrs(); + const recipientAddrs = recipientTransport!.node.getMultiaddrs(); + + const senderWebRTCDirect = senderAddrs.find(ma => ma.toString().includes('/webrtc-direct/')); + const recipientWebRTCDirect = recipientAddrs.find(ma => ma.toString().includes('/webrtc-direct/')); + + expect(senderWebRTCDirect).toBeDefined(); + expect(recipientWebRTCDirect).toBeDefined(); + + console.log('Sender WebRTC-Direct addr:', senderWebRTCDirect?.toString()); + console.log('Recipient WebRTC-Direct addr:', recipientWebRTCDirect?.toString()); + + // Verify both nodes are capable of WebRTC-Direct connectivity for notifications + expect(senderWebRTCDirect!.toString()).toMatch(/\/webrtc-direct\/certhash\//); + expect(recipientWebRTCDirect!.toString()).toMatch(/\/webrtc-direct\/certhash\//); + + console.log('Both nodes successfully configured with WebRTC-Direct capability'); - await dialer.start(); - - // Attempt to establish direct connection (note: this may not work in CI environment) - try { - console.log('Attempting WebRTC-Direct connection to:', webrtcDirectAddr!.toString()); - const connection = await dialer.dial(webrtcDirectAddr!, { - signal: AbortSignal.timeout(5000) - }); - - console.log('WebRTC-Direct connection established successfully!'); - expect(connection.status).toBe('open'); - - await connection.close(); - } catch (error) { - // In CI/sandboxed environments, actual WebRTC connections may fail due to networking restrictions - // This is expected behavior - the important part is that the transport is configured correctly - console.log('WebRTC-Direct connection attempt failed (expected in CI):', error.message); - expect(error.message).toContain('timeout'); // Should timeout rather than fail immediately - } - - await dialer.stop(); - await listener.stop(); - }, 15000); + await Promise.all([ + sender.shutdown(), + recipient.shutdown(), + ]); + }, 6000); }); \ No newline at end of file