Skip to content
Merged
Show file tree
Hide file tree
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
8 changes: 4 additions & 4 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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.**
Expand Down
16 changes: 8 additions & 8 deletions src/core/networking/transport/libp2p/libp2pTransport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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");
Expand Down Expand Up @@ -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: {
Expand All @@ -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) {
Expand All @@ -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)
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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")) {
Expand Down
241 changes: 188 additions & 53 deletions test/core/networking/webrtc_direct_e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]+/);
Copy link

Copilot AI Aug 15, 2025

Choose a reason for hiding this comment

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

The regex pattern is complex and hard to maintain. Consider extracting it to a constant with a descriptive name like WEBRTC_DIRECT_ADDR_PATTERN or using multiple smaller assertions to validate each component separately.

Suggested change
expect(webrtcDirectAddr.toString()).toMatch(/\/ip4\/[\d\.]+\/udp\/\d+\/webrtc-direct\/certhash\/[a-zA-Z0-9_-]+\/p2p\/[a-zA-Z0-9]+/);
expect(webrtcDirectAddr.toString()).toMatch(WEBRTC_DIRECT_ADDR_PATTERN);

Copilot uses AI. Check for mistakes.

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<void>(
(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);
});