diff --git a/packages/integration-tests/package.json b/packages/integration-tests/package.json index c291a21572..fa7f21c729 100644 --- a/packages/integration-tests/package.json +++ b/packages/integration-tests/package.json @@ -36,6 +36,7 @@ "@chainsafe/libp2p-gossipsub": "^11.0.0", "@chainsafe/libp2p-noise": "^14.0.0", "@chainsafe/libp2p-yamux": "^6.0.1", + "@libp2p/bootstrap": "^10.0.7", "@libp2p/circuit-relay-v2": "^1.0.0", "@libp2p/crypto": "^3.0.2", "@libp2p/daemon-client": "^8.0.1", @@ -50,6 +51,7 @@ "@libp2p/interop": "^10.0.0", "@libp2p/kad-dht": "^12.0.0", "@libp2p/logger": "^4.0.1", + "@libp2p/mdns": "^10.0.7", "@libp2p/mplex": "^10.0.0", "@libp2p/peer-id": "^4.0.0", "@libp2p/peer-id-factory": "^4.0.1", diff --git a/packages/integration-tests/test/bootstrap.spec.ts b/packages/integration-tests/test/bootstrap.spec.ts new file mode 100644 index 0000000000..3334ccf8ac --- /dev/null +++ b/packages/integration-tests/test/bootstrap.spec.ts @@ -0,0 +1,106 @@ +/* eslint-env mocha */ + +import { bootstrap } from '@libp2p/bootstrap' +import { TypedEventEmitter, peerDiscoverySymbol } from '@libp2p/interface' +import { createEd25519PeerId } from '@libp2p/peer-id-factory' +import { webSockets } from '@libp2p/websockets' +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' +import { createLibp2p } from 'libp2p' +import defer from 'p-defer' +import sinon from 'sinon' +import type { Libp2p, PeerDiscovery, PeerDiscoveryEvents, PeerId } from '@libp2p/interface' + +const listenAddr = multiaddr('/ip4/127.0.0.1/tcp/0/ws') + +class TestPeerDiscovery extends TypedEventEmitter implements PeerDiscovery { + get [peerDiscoverySymbol] (): PeerDiscovery { + return this + } + + readonly [Symbol.toStringTag] = '@libp2p/test-peer-discovery' +} + +describe('bootstrap', () => { + let peerId: PeerId + let remotePeerId1: PeerId + let remotePeerId2: PeerId + let libp2p: Libp2p + + beforeEach(async () => { + [peerId, remotePeerId1, remotePeerId2] = await Promise.all([ + createEd25519PeerId(), + createEd25519PeerId(), + createEd25519PeerId() + ]) + }) + + afterEach(async () => { + if (libp2p != null) { + await libp2p.stop() + } + }) + + it('should ignore self on discovery', async () => { + const discovery = new TestPeerDiscovery() + + libp2p = await createLibp2p({ + peerId, + peerDiscovery: [ + () => discovery + ] + }) + + await libp2p.start() + const discoverySpy = sinon.spy() + libp2p.addEventListener('peer:discovery', discoverySpy) + discovery.safeDispatchEvent('peer', { + detail: { + id: libp2p.peerId, + multiaddrs: [], + protocols: [] + } + }) + + expect(discoverySpy.called).to.eql(false) + }) + + it('bootstrap should discover all peers in the list', async () => { + const deferred = defer() + + const bootstrappers = [ + `${listenAddr.toString()}/p2p/${remotePeerId1.toString()}`, + `${listenAddr.toString()}/p2p/${remotePeerId2.toString()}` + ] + + libp2p = await createLibp2p({ + transports: [ + webSockets() + ], + peerDiscovery: [ + bootstrap({ + list: bootstrappers + }) + ] + }) + + const expectedPeers = new Set([ + remotePeerId1.toString(), + remotePeerId2.toString() + ]) + + libp2p.addEventListener('peer:discovery', (evt) => { + const { id } = evt.detail + + expectedPeers.delete(id.toString()) + if (expectedPeers.size === 0) { + libp2p.removeEventListener('peer:discovery') + deferred.resolve() + } + }) + + await libp2p.start() + + return deferred.promise + }) +}) diff --git a/packages/integration-tests/test/dht.node.ts b/packages/integration-tests/test/dht.node.ts new file mode 100644 index 0000000000..dc19017730 --- /dev/null +++ b/packages/integration-tests/test/dht.node.ts @@ -0,0 +1,222 @@ +/* eslint-env mocha */ + +import { yamux } from '@chainsafe/libp2p-yamux' +import { kadDHT, passthroughMapper } from '@libp2p/kad-dht' +import { mplex } from '@libp2p/mplex' +import { createEd25519PeerId } from '@libp2p/peer-id-factory' +import { plaintext } from '@libp2p/plaintext' +import { tcp } from '@libp2p/tcp' +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' +import { createLibp2p } from 'libp2p' +import pDefer from 'p-defer' +import pWaitFor from 'p-wait-for' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import type { Libp2p, PeerId } from '@libp2p/interface' +import type { KadDHT } from '@libp2p/kad-dht' +import type { Multiaddr } from '@multiformats/multiaddr' +import type { Libp2pOptions } from 'libp2p' + +export const subsystemMulticodecs = [ + '/test/kad/1.0.0', + '/other/1.0.0' +] + +const listenAddr = multiaddr('/ip4/127.0.0.1/tcp/8000') +const remoteListenAddr = multiaddr('/ip4/127.0.0.1/tcp/8001') + +async function getRemoteAddr (remotePeerId: PeerId, libp2p: Libp2p): Promise { + const { addresses } = await libp2p.peerStore.get(remotePeerId) + + if (addresses.length === 0) { + throw new Error('No addrs found') + } + + const addr = addresses[0] + + return addr.multiaddr.encapsulate(`/p2p/${remotePeerId.toString()}`) +} + +describe('DHT subsystem operates correctly', () => { + let peerId: PeerId + let remotePeerId: PeerId + let libp2p: Libp2p<{ dht: KadDHT }> + let remoteLibp2p: Libp2p<{ dht: KadDHT }> + let remAddr: Multiaddr + + beforeEach(async () => { + [peerId, remotePeerId] = await Promise.all([ + createEd25519PeerId(), + createEd25519PeerId() + ]) + }) + + describe('dht started before connect', () => { + beforeEach(async () => { + libp2p = await createLibp2p({ + peerId, + addresses: { + listen: [listenAddr.toString()] + }, + transports: [ + tcp() + ], + connectionEncryption: [ + plaintext() + ], + streamMuxers: [ + yamux(), + mplex() + ], + services: { + dht: kadDHT({ + protocol: subsystemMulticodecs[0], + peerInfoMapper: passthroughMapper, + allowQueryWithZeroPeers: true + }) + } + }) + + remoteLibp2p = await createLibp2p({ + peerId: remotePeerId, + addresses: { + listen: [remoteListenAddr.toString()] + }, + transports: [ + tcp() + ], + connectionEncryption: [ + plaintext() + ], + streamMuxers: [ + yamux(), + mplex() + ], + services: { + dht: kadDHT({ + protocol: subsystemMulticodecs[0], + peerInfoMapper: passthroughMapper, + allowQueryWithZeroPeers: true + }) + } + }) + + await Promise.all([ + libp2p.start(), + remoteLibp2p.start() + ]) + + await libp2p.peerStore.patch(remotePeerId, { + multiaddrs: [remoteListenAddr] + }) + remAddr = await getRemoteAddr(remotePeerId, libp2p) + }) + + afterEach(async () => { + if (libp2p != null) { + await libp2p.stop() + } + + if (remoteLibp2p != null) { + await remoteLibp2p.stop() + } + }) + + it('should get notified of connected peers on dial', async () => { + const stream = await libp2p.dialProtocol(remAddr, subsystemMulticodecs) + + expect(stream).to.exist() + + return Promise.all([ + // @ts-expect-error private field + pWaitFor(() => libp2p.services.dht.routingTable.size === 1), + // @ts-expect-error private field + pWaitFor(() => remoteLibp2p.services.dht.routingTable.size === 1) + ]) + }) + + it('should put on a peer and get from the other', async () => { + const key = uint8ArrayFromString('hello') + const value = uint8ArrayFromString('world') + + await libp2p.dialProtocol(remotePeerId, subsystemMulticodecs) + await Promise.all([ + // @ts-expect-error private field + pWaitFor(() => libp2p.services.dht.routingTable.size === 1), + // @ts-expect-error private field + pWaitFor(() => remoteLibp2p.services.dht.routingTable.size === 1) + ]) + + await libp2p.contentRouting.put(key, value) + + const fetchedValue = await remoteLibp2p.contentRouting.get(key) + expect(fetchedValue).to.equalBytes(value) + }) + }) + + it('kad-dht should discover other peers', async () => { + const remotePeerId1 = await createEd25519PeerId() + const remotePeerId2 = await createEd25519PeerId() + + const deferred = pDefer() + + const getConfig = (peerId: PeerId): Libp2pOptions<{ dht: KadDHT }> => ({ + peerId, + addresses: { + listen: [ + listenAddr.toString() + ] + }, + services: { + dht: kadDHT({ + protocol: subsystemMulticodecs[0], + peerInfoMapper: passthroughMapper, + allowQueryWithZeroPeers: true + }) + } + }) + + const localConfig = getConfig(peerId) + + libp2p = await createLibp2p(localConfig) + + const remoteLibp2p1 = await createLibp2p(getConfig(remotePeerId1)) + const remoteLibp2p2 = await createLibp2p(getConfig(remotePeerId2)) + + libp2p.addEventListener('peer:discovery', (evt) => { + const { id } = evt.detail + + if (id.equals(remotePeerId1)) { + libp2p.removeEventListener('peer:discovery') + deferred.resolve() + } + }) + + await Promise.all([ + libp2p.start(), + remoteLibp2p1.start(), + remoteLibp2p2.start() + ]) + + await libp2p.peerStore.patch(remotePeerId1, { + multiaddrs: remoteLibp2p1.getMultiaddrs() + }) + await remoteLibp2p2.peerStore.patch(remotePeerId1, { + multiaddrs: remoteLibp2p1.getMultiaddrs() + }) + + // Topology: + // A -> B + // C -> B + await Promise.all([ + libp2p.dial(remotePeerId1), + remoteLibp2p2.dial(remotePeerId1) + ]) + + await deferred.promise + return Promise.all([ + remoteLibp2p1.stop(), + remoteLibp2p2.stop() + ]) + }) +}) diff --git a/packages/integration-tests/test/mdns.node.ts b/packages/integration-tests/test/mdns.node.ts new file mode 100644 index 0000000000..dbf03c4579 --- /dev/null +++ b/packages/integration-tests/test/mdns.node.ts @@ -0,0 +1,90 @@ +/* eslint-env mocha */ + +import { randomBytes } from '@libp2p/crypto' +import { mdns } from '@libp2p/mdns' +import { createEd25519PeerId } from '@libp2p/peer-id-factory' +import { tcp } from '@libp2p/tcp' +import { multiaddr } from '@multiformats/multiaddr' +import { createLibp2p, type Libp2pOptions } from 'libp2p' +import defer from 'p-defer' +import { toString as uint8ArrayToString } from 'uint8arrays/to-string' +import type { Libp2p, PeerId } from '@libp2p/interface' + +const listenAddr = multiaddr('/ip4/127.0.0.1/tcp/0') + +describe('mdns', () => { + let peerId: PeerId + let remotePeerId1: PeerId + let remotePeerId2: PeerId + let libp2p: Libp2p + + beforeEach(async () => { + [peerId, remotePeerId1, remotePeerId2] = await Promise.all([ + createEd25519PeerId(), + createEd25519PeerId(), + createEd25519PeerId() + ]) + }) + + afterEach(async () => { + if (libp2p != null) { + await libp2p.stop() + } + }) + + it('should discover all peers on the local network', async () => { + const deferred = defer() + + // use a random tag to prevent CI collision + const serviceTag = `libp2p-test-${uint8ArrayToString(randomBytes(4), 'base16')}.local` + + const getConfig = (peerId: PeerId): Libp2pOptions => ({ + peerId, + addresses: { + listen: [ + listenAddr.toString() + ] + }, + transports: [ + tcp() + ], + peerDiscovery: [ + mdns({ + interval: 200, // discover quickly + serviceTag + }) + ] + }) + + libp2p = await createLibp2p(getConfig(peerId)) + const remoteLibp2p1 = await createLibp2p(getConfig(remotePeerId1)) + const remoteLibp2p2 = await createLibp2p(getConfig(remotePeerId2)) + + const expectedPeers = new Set([ + remotePeerId1.toString(), + remotePeerId2.toString() + ]) + + libp2p.addEventListener('peer:discovery', (evt) => { + const { id } = evt.detail + + expectedPeers.delete(id.toString()) + + if (expectedPeers.size === 0) { + libp2p.removeEventListener('peer:discovery') + deferred.resolve() + } + }) + + await Promise.all([ + remoteLibp2p1.start(), + remoteLibp2p2.start(), + libp2p.start() + ]) + + await deferred.promise + + await remoteLibp2p1.stop() + await remoteLibp2p2.stop() + }) +}) diff --git a/packages/integration-tests/test/node.ts b/packages/integration-tests/test/node.ts index 50f7bcbcf8..ff940f168e 100644 --- a/packages/integration-tests/test/node.ts +++ b/packages/integration-tests/test/node.ts @@ -2,3 +2,4 @@ import './circuit-relay-discovery.node.js' import './circuit-relay.node.js' import './dcutr.node.js' import './identify.node.js' +import './mdns.node.js'