diff --git a/packages/libp2p/.aegir.js b/packages/libp2p/.aegir.js index c7552f68b1..47b2553a9d 100644 --- a/packages/libp2p/.aegir.js +++ b/packages/libp2p/.aegir.js @@ -1,61 +1,6 @@ /** @type {import('aegir').PartialOptions} */ export default { build: { - bundlesizeMax: '147kB' - }, - test: { - before: async () => { - // use dynamic import because we only want to reference these files during the test run, e.g. after building - const { webSockets } = await import('@libp2p/websockets') - const { mplex } = await import('@libp2p/mplex') - const { yamux } = await import('@chainsafe/libp2p-yamux') - const { WebSockets } = await import('@multiformats/mafmt') - const { createLibp2p } = await import('./dist/src/index.js') - const { plaintext } = await import('@libp2p/plaintext') - const { circuitRelayServer, circuitRelayTransport } = await import('@libp2p/circuit-relay-v2') - const { identify } = await import('@libp2p/identify') - const { echo } = await import('./dist/test/fixtures/echo-service.js') - - const libp2p = await createLibp2p({ - connectionManager: { - inboundConnectionThreshold: Infinity - }, - addresses: { - listen: [ - '/ip4/127.0.0.1/tcp/0/ws' - ] - }, - transports: [ - circuitRelayTransport(), - webSockets() - ], - streamMuxers: [ - yamux(), - mplex() - ], - connectionEncrypters: [ - plaintext() - ], - services: { - identify: identify(), - relay: circuitRelayServer({ - reservations: { - maxReservations: Infinity - } - }), - echo: echo() - } - }) - - return { - libp2p, - env: { - RELAY_MULTIADDR: libp2p.getMultiaddrs().filter(ma => WebSockets.matches(ma)).pop() - } - } - }, - after: async (_, before) => { - await before.libp2p.stop() - } + bundlesizeMax: '95KB' } } diff --git a/packages/libp2p/package.json b/packages/libp2p/package.json index 34df69a4ce..007173c4a9 100644 --- a/packages/libp2p/package.json +++ b/packages/libp2p/package.json @@ -77,12 +77,12 @@ "prepublishOnly": "node scripts/update-version.js && npm run build", "build": "aegir build", "test": "aegir test", - "test:node": "aegir test -t node -f \"./dist/test/**/*.{node,spec}.js\" --cov", - "test:chrome": "aegir test -t browser -f \"./dist/test/**/*.spec.js\" --cov", - "test:chrome-webworker": "aegir test -t webworker -f \"./dist/test/**/*.spec.js\"", - "test:firefox": "aegir test -t browser -f \"./dist/test/**/*.spec.js\" -- --browser firefox", - "test:firefox-webworker": "aegir test -t webworker -f \"./dist/test/**/*.spec.js\" -- --browser firefox", - "test:webkit": "aegir test -t browser -f \"./dist/test/**/*.spec.js\" -- --browser webkit" + "test:node": "aegir test -t node --cov", + "test:chrome": "aegir test -t browser --cov", + "test:chrome-webworker": "aegir test -t webworker", + "test:firefox": "aegir test -t browser -- --browser firefox", + "test:firefox-webworker": "aegir test -t webworker -- --browser firefox", + "test:webkit": "aegir test -t browser -- --browser webkit" }, "dependencies": { "@libp2p/crypto": "^5.0.6", @@ -114,22 +114,17 @@ }, "devDependencies": { "@chainsafe/libp2p-yamux": "^7.0.0", - "@libp2p/circuit-relay-v2": "^3.1.0", - "@libp2p/identify": "^3.0.10", + "@libp2p/echo": "^2.1.1", "@libp2p/interface-compliance-tests": "^6.1.8", + "@libp2p/memory": "^0.0.0", "@libp2p/mplex": "^11.0.10", "@libp2p/plaintext": "^2.0.10", - "@libp2p/tcp": "^10.0.11", - "@libp2p/websockets": "^9.0.11", - "@multiformats/mafmt": "^12.1.6", "aegir": "^44.0.1", "delay": "^6.0.0", "it-all": "^3.0.6", "it-drain": "^3.0.7", "it-map": "^3.1.0", "it-pair": "^2.0.6", - "it-pipe": "^3.0.1", - "it-pushable": "^3.2.3", "it-stream-types": "^2.0.1", "it-take": "^3.0.5", "p-event": "^6.0.1", diff --git a/packages/libp2p/src/upgrader.ts b/packages/libp2p/src/upgrader.ts index c263e11d5c..239174177c 100644 --- a/packages/libp2p/src/upgrader.ts +++ b/packages/libp2p/src/upgrader.ts @@ -192,7 +192,7 @@ export class DefaultUpgrader implements Upgrader { accepted = await this.components.connectionManager.acceptIncomingConnection(maConn) if (!accepted) { - throw new ConnectionDeniedError('connection denied') + throw new ConnectionDeniedError('Connection denied') } await this.shouldBlockConnection('denyInboundConnection', maConn) diff --git a/packages/libp2p/test/addresses/addresses.node.ts b/packages/libp2p/test/addresses/addresses.spec.ts similarity index 56% rename from packages/libp2p/test/addresses/addresses.node.ts rename to packages/libp2p/test/addresses/addresses.spec.ts index 43367dc05e..a5de3e2e35 100644 --- a/packages/libp2p/test/addresses/addresses.node.ts +++ b/packages/libp2p/test/addresses/addresses.spec.ts @@ -1,40 +1,35 @@ /* eslint-env mocha */ -import { plaintext } from '@libp2p/plaintext' -import { isLoopback } from '@libp2p/utils/multiaddr/is-loopback' -import { webSockets } from '@libp2p/websockets' -import { type Multiaddr, multiaddr, protocols } from '@multiformats/multiaddr' +import { memory } from '@libp2p/memory' +import { multiaddr, protocols } from '@multiformats/multiaddr' import { expect } from 'aegir/chai' import { pEvent } from 'p-event' -import sinon from 'sinon' -import { createNode } from '../fixtures/creators/peer.js' +import { createLibp2p } from '../../src/index.js' import { getComponent } from '../fixtures/get-component.js' -import { AddressesOptions } from './utils.js' import type { Libp2p, PeerUpdate } from '@libp2p/interface' import type { AddressManager, TransportManager } from '@libp2p/interface-internal' +import type { Multiaddr } from '@multiformats/multiaddr' -const listenAddresses = ['/ip4/127.0.0.1/tcp/0', '/ip4/127.0.0.1/tcp/8000/ws'] +const listenAddresses = ['/memory/address-1', '/memory/address-2'] const announceAddresses = ['/dns4/peer.io/tcp/433/p2p/12D3KooWNvSZnPi3RrhrTwEY4LuuBeB6K6facKUCJcyWG1aoDd2p'] describe('libp2p.addressManager', () => { let libp2p: Libp2p afterEach(async () => { - if (libp2p != null) { - await libp2p.stop() - } + await libp2p?.stop() }) it('should keep listen addresses after start, even if changed', async () => { - libp2p = await createNode({ - started: false, - config: { - ...AddressesOptions, - addresses: { - listen: listenAddresses, - announce: announceAddresses - } - } + libp2p = await createLibp2p({ + start: false, + addresses: { + listen: listenAddresses, + announce: announceAddresses + }, + transports: [ + memory() + ] }) const addressManager = getComponent(libp2p, 'addressManager') @@ -54,44 +49,37 @@ describe('libp2p.addressManager', () => { }) it('should announce transport listen addresses if announce addresses are not provided', async () => { - libp2p = await createNode({ - started: false, - config: { - ...AddressesOptions, - addresses: { - listen: listenAddresses - } - } + libp2p = await createLibp2p({ + addresses: { + listen: listenAddresses + }, + transports: [ + memory() + ] }) - await libp2p.start() - const tmListen = getComponent(libp2p, 'transportManager').getAddrs().map((ma) => ma.toString()) // Announce 2 listen (transport) const advertiseMultiaddrs = getComponent(libp2p, 'addressManager').getAddresses().map((ma) => ma.decapsulateCode(protocols('p2p').code).toString()) - expect(advertiseMultiaddrs).to.have.lengthOf(2) + expect(advertiseMultiaddrs).to.have.lengthOf(listenAddresses.length) tmListen.forEach((m) => { expect(advertiseMultiaddrs).to.include(m) }) - expect(advertiseMultiaddrs).to.not.include(listenAddresses[0]) // Random Port switch }) it('should only announce the given announce addresses when provided', async () => { - libp2p = await createNode({ - started: false, - config: { - ...AddressesOptions, - addresses: { - listen: listenAddresses, - announce: announceAddresses - } - } + libp2p = await createLibp2p({ + addresses: { + listen: listenAddresses, + announce: announceAddresses + }, + transports: [ + memory() + ] }) - await libp2p.start() - const tmListen = getComponent(libp2p, 'transportManager').getAddrs().map((ma) => ma.toString()) // Announce 1 announce addr @@ -103,68 +91,58 @@ describe('libp2p.addressManager', () => { }) }) - it('can filter out loopback addresses by the announce filter', async () => { - libp2p = await createNode({ - started: false, - config: { - ...AddressesOptions, - addresses: { - listen: listenAddresses, - announceFilter: (multiaddrs: Multiaddr[]) => multiaddrs.filter(m => !isLoopback(m)) - } - } + it('should filter listen addresses filtered by the announce filter', async () => { + libp2p = await createLibp2p({ + addresses: { + listen: listenAddresses, + announceFilter: (multiaddrs: Multiaddr[]) => multiaddrs.slice(1) + }, + transports: [ + memory() + ] }) - await libp2p.start() - - expect(getComponent(libp2p, 'addressManager').getAddresses()).to.have.lengthOf(0) + const listenAddrs = getComponent(libp2p, 'addressManager').getListenAddrs().map((ma) => ma.toString()) + expect(listenAddrs).to.have.lengthOf(listenAddresses.length) + expect(listenAddrs).to.deep.equal(listenAddresses) - // Stub transportManager addresses to add a public address - const stubMa = multiaddr('/ip4/120.220.10.1/tcp/1000') - sinon.stub(getComponent(libp2p, 'transportManager'), 'getAddrs').returns([ - ...listenAddresses.map((a) => multiaddr(a)), - stubMa - ]) + await libp2p.start() - const multiaddrs = getComponent(libp2p, 'addressManager').getAddresses() - expect(multiaddrs.length).to.equal(1) - expect(multiaddrs[0].decapsulateCode(protocols('p2p').code).equals(stubMa)).to.eql(true) + const addresses = getComponent(libp2p, 'addressManager').getAddresses() + expect(addresses).to.have.lengthOf(1) }) - it('can filter out loopback addresses to announced by the announce filter', async () => { - libp2p = await createNode({ - started: false, - config: { - ...AddressesOptions, - addresses: { - listen: listenAddresses, - announce: announceAddresses, - announceFilter: (multiaddrs: Multiaddr[]) => multiaddrs.filter(m => !isLoopback(m)) - } - } + it('should filter announce addresses filtered by the announce filter', async () => { + libp2p = await createLibp2p({ + addresses: { + listen: listenAddresses, + announce: announceAddresses, + announceFilter: () => [] + }, + transports: [ + memory() + ] }) const listenAddrs = getComponent(libp2p, 'addressManager').getListenAddrs().map((ma) => ma.toString()) expect(listenAddrs).to.have.lengthOf(listenAddresses.length) - expect(listenAddrs).to.include(listenAddresses[0]) - expect(listenAddrs).to.include(listenAddresses[1]) - - await libp2p.start() + expect(listenAddrs).to.deep.equal(listenAddresses) - const loopbackAddrs = getComponent(libp2p, 'addressManager').getAddresses().filter(ma => isLoopback(ma)) - expect(loopbackAddrs).to.be.empty() + const addresses = getComponent(libp2p, 'addressManager').getAddresses() + expect(addresses).to.have.lengthOf(0) }) it('should include observed addresses in returned multiaddrs', async () => { - libp2p = await createNode({ - started: false, - config: { - ...AddressesOptions, - addresses: { - listen: listenAddresses - } - } + libp2p = await createLibp2p({ + start: false, + addresses: { + listen: listenAddresses + }, + transports: [ + memory() + ] }) + const ma = '/ip4/83.32.123.53/tcp/43928' await libp2p.start() @@ -180,20 +158,15 @@ describe('libp2p.addressManager', () => { }) it('should populate the AddressManager from the config', async () => { - libp2p = await createNode({ - started: false, - config: { - addresses: { - listen: listenAddresses, - announce: announceAddresses - }, - transports: [ - webSockets() - ], - connectionEncrypters: [ - plaintext() - ] - } + libp2p = await createLibp2p({ + start: false, + addresses: { + listen: listenAddresses, + announce: announceAddresses + }, + transports: [ + memory() + ] }) expect(libp2p.getMultiaddrs().map(ma => ma.decapsulateCode(protocols('p2p').code).toString())).to.have.members(announceAddresses) @@ -201,20 +174,15 @@ describe('libp2p.addressManager', () => { }) it('should update our peer record with announce addresses on startup', async () => { - libp2p = await createNode({ - started: false, - config: { - addresses: { - listen: listenAddresses, - announce: announceAddresses - }, - transports: [ - webSockets() - ], - connectionEncrypters: [ - plaintext() - ] - } + libp2p = await createLibp2p({ + start: false, + addresses: { + listen: listenAddresses, + announce: announceAddresses + }, + transports: [ + memory() + ] }) const eventPromise = pEvent<'self:peer:update', CustomEvent>(libp2p, 'self:peer:update', { @@ -233,20 +201,15 @@ describe('libp2p.addressManager', () => { }) it('should only include confirmed observed addresses in peer record', async () => { - libp2p = await createNode({ - started: false, - config: { - addresses: { - listen: listenAddresses, - announce: announceAddresses - }, - transports: [ - webSockets() - ], - connectionEncrypters: [ - plaintext() - ] - } + libp2p = await createLibp2p({ + start: false, + addresses: { + listen: listenAddresses, + announce: announceAddresses + }, + transports: [ + memory() + ] }) await libp2p.start() diff --git a/packages/libp2p/test/addresses/utils.ts b/packages/libp2p/test/addresses/utils.ts deleted file mode 100644 index d113505a51..0000000000 --- a/packages/libp2p/test/addresses/utils.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { tcp } from '@libp2p/tcp' -import { webSockets } from '@libp2p/websockets' -import { createBaseOptions } from '../fixtures/base-options.js' - -export const AddressesOptions = createBaseOptions({ - transports: [ - tcp(), - webSockets() - ] -}) diff --git a/packages/libp2p/test/connection-manager/connection-gater.spec.ts b/packages/libp2p/test/connection-manager/connection-gater.spec.ts new file mode 100644 index 0000000000..91f9b8a67a --- /dev/null +++ b/packages/libp2p/test/connection-manager/connection-gater.spec.ts @@ -0,0 +1,160 @@ +/* eslint-env mocha */ + +import { stop } from '@libp2p/interface' +import { expect } from 'aegir/chai' +import sinon from 'sinon' +import { createPeers } from '../fixtures/create-peers.js' +import type { Echo } from '@libp2p/echo' +import type { Libp2p } from '@libp2p/interface' + +describe('connection-gater', () => { + let dialer: Libp2p<{ echo: Echo }> + let listener: Libp2p<{ echo: Echo }> + + afterEach(async () => { + await stop(dialer, listener) + }) + + it('intercept peer dial', async () => { + const denyDialPeer = sinon.stub().returns(true) + + ;({ dialer, listener } = await createPeers({ + connectionGater: { + denyDialPeer + } + })) + + await expect(dialer.dial(listener.getMultiaddrs())) + .to.eventually.be.rejected().with.property('name', 'DialDeniedError') + }) + + it('intercept addr dial', async () => { + const denyDialMultiaddr = sinon.stub().returns(false) + + ;({ dialer, listener } = await createPeers({ + connectionGater: { + denyDialMultiaddr + } + })) + + await dialer.dial(listener.getMultiaddrs()) + + for (const multiaddr of listener.getMultiaddrs()) { + expect(denyDialMultiaddr.calledWith(multiaddr)).to.be.true() + } + }) + + it('intercept multiaddr store', async () => { + const filterMultiaddrForPeer = sinon.stub().returns(true) + + ;({ dialer, listener } = await createPeers({ + connectionGater: { + filterMultiaddrForPeer + } + })) + + const fullMultiaddr = listener.getMultiaddrs()[0] + + await dialer.peerStore.merge(listener.peerId, { + multiaddrs: [fullMultiaddr] + }) + + expect(filterMultiaddrForPeer.callCount).to.equal(1) + + const args = filterMultiaddrForPeer.getCall(0).args + expect(args[0].toString()).to.equal(listener.peerId.toString()) + expect(args[1].toString()).to.equal(fullMultiaddr.toString()) + }) + + it('intercept accept inbound connection', async () => { + const denyInboundConnection = sinon.stub().returns(false) + + ;({ dialer, listener } = await createPeers({}, { + connectionGater: { + denyInboundConnection + } + })) + + await dialer.dial(listener.getMultiaddrs()) + + expect(denyInboundConnection.called).to.be.true() + }) + + it('intercept accept outbound connection', async () => { + const denyOutboundConnection = sinon.stub().returns(false) + + ;({ dialer, listener } = await createPeers({ + connectionGater: { + denyOutboundConnection + } + })) + + await dialer.dial(listener.getMultiaddrs()) + + expect(denyOutboundConnection.called).to.be.true() + }) + + it('intercept inbound encrypted', async () => { + const denyInboundEncryptedConnection = sinon.stub().returns(false) + + ;({ dialer, listener } = await createPeers({}, { + connectionGater: { + denyInboundEncryptedConnection + } + })) + + await dialer.dial(listener.getMultiaddrs()) + + expect(denyInboundEncryptedConnection.called).to.be.true() + expect(denyInboundEncryptedConnection.getCall(0).args[0].toMultihash().bytes).to.equalBytes(dialer.peerId.toMultihash().bytes) + }) + + it('intercept outbound encrypted', async () => { + const denyOutboundEncryptedConnection = sinon.stub().returns(false) + + ;({ dialer, listener } = await createPeers({ + connectionGater: { + denyOutboundEncryptedConnection + } + })) + + await dialer.dial(listener.getMultiaddrs()) + + expect(denyOutboundEncryptedConnection.called).to.be.true() + expect(denyOutboundEncryptedConnection.getCall(0).args[0].toMultihash().bytes).to.equalBytes(listener.peerId.toMultihash().bytes) + }) + + it('intercept inbound upgraded', async () => { + const denyInboundUpgradedConnection = sinon.stub().returns(false) + + ;({ dialer, listener } = await createPeers({}, { + connectionGater: { + denyInboundUpgradedConnection + } + })) + + const input = Uint8Array.from([0]) + const output = await dialer.services.echo.echo(listener.getMultiaddrs(), input) + expect(output).to.equalBytes(input) + + expect(denyInboundUpgradedConnection.called).to.be.true() + expect(denyInboundUpgradedConnection.getCall(0).args[0].toMultihash().bytes).to.equalBytes(dialer.peerId.toMultihash().bytes) + }) + + it('intercept outbound upgraded', async () => { + const denyOutboundUpgradedConnection = sinon.stub().returns(false) + + ;({ dialer, listener } = await createPeers({ + connectionGater: { + denyOutboundUpgradedConnection + } + })) + + const input = Uint8Array.from([0]) + const output = await dialer.services.echo.echo(listener.getMultiaddrs(), input) + expect(output).to.equalBytes(input) + + expect(denyOutboundUpgradedConnection.called).to.be.true() + expect(denyOutboundUpgradedConnection.getCall(0).args[0].toMultihash().bytes).to.equalBytes(listener.peerId.toMultihash().bytes) + }) +}) diff --git a/packages/libp2p/test/connection-manager/dial-queue.spec.ts b/packages/libp2p/test/connection-manager/dial-queue.spec.ts index dc1d07df62..c5903670bc 100644 --- a/packages/libp2p/test/connection-manager/dial-queue.spec.ts +++ b/packages/libp2p/test/connection-manager/dial-queue.spec.ts @@ -2,8 +2,6 @@ import { generateKeyPair } from '@libp2p/crypto/keys' import { NotFoundError } from '@libp2p/interface' -import { matchMultiaddr } from '@libp2p/interface-compliance-tests/matchers' -import { mockConnection, mockDuplex, mockMultiaddrConnection } from '@libp2p/interface-compliance-tests/mocks' import { peerLogger } from '@libp2p/logger' import { peerIdFromPrivateKey } from '@libp2p/peer-id' import { multiaddr, resolvers } from '@multiformats/multiaddr' @@ -50,7 +48,7 @@ describe('dial queue', () => { }) it('should end when a single multiaddr dials succeeds', async () => { - const connection = mockConnection(mockMultiaddrConnection(mockDuplex(), peerIdFromPrivateKey(await generateKeyPair('Ed25519')))) + const connection = stubInterface() const deferredConn = pDefer() const actions: Record Promise> = { '/ip4/127.0.0.1/tcp/1231': async () => Promise.reject(new Error('dial failure')), @@ -87,7 +85,7 @@ describe('dial queue', () => { it('should load addresses from the peer routing when peer id is not in the peer store', async () => { const peerId = peerIdFromPrivateKey(await generateKeyPair('Ed25519')) - const connection = mockConnection(mockMultiaddrConnection(mockDuplex(), peerIdFromPrivateKey(await generateKeyPair('Ed25519')))) + const connection = stubInterface() const ma = multiaddr('/ip4/127.0.0.1/tcp/4001') components.peerStore.get.withArgs(peerId).rejects(new NotFoundError('Not found')) @@ -99,7 +97,7 @@ describe('dial queue', () => { }) components.transportManager.dialTransportForMultiaddr.returns(stubInterface()) - components.transportManager.dial.withArgs(matchMultiaddr(ma.encapsulate(`/p2p/${peerId}`))).resolves(connection) + components.transportManager.dial.withArgs(ma.encapsulate(`/p2p/${peerId}`)).resolves(connection) dialer = new DialQueue(components) @@ -108,7 +106,7 @@ describe('dial queue', () => { it('should load addresses from the peer routing when none are present in the peer store', async () => { const peerId = peerIdFromPrivateKey(await generateKeyPair('Ed25519')) - const connection = mockConnection(mockMultiaddrConnection(mockDuplex(), peerIdFromPrivateKey(await generateKeyPair('Ed25519')))) + const connection = stubInterface() const ma = multiaddr('/ip4/127.0.0.1/tcp/4001') components.peerStore.get.withArgs(peerId).resolves({ @@ -126,7 +124,7 @@ describe('dial queue', () => { }) components.transportManager.dialTransportForMultiaddr.returns(stubInterface()) - components.transportManager.dial.withArgs(matchMultiaddr(ma.encapsulate(`/p2p/${peerId}`))).resolves(connection) + components.transportManager.dial.withArgs(ma.encapsulate(`/p2p/${peerId}`)).resolves(connection) dialer = new DialQueue(components) @@ -134,7 +132,7 @@ describe('dial queue', () => { }) it('should end when a single multiaddr dials succeeds even when a final dial fails', async () => { - const connection = mockConnection(mockMultiaddrConnection(mockDuplex(), peerIdFromPrivateKey(await generateKeyPair('Ed25519')))) + const connection = stubInterface() const deferredConn = pDefer() const actions: Record Promise> = { '/ip4/127.0.0.1/tcp/1231': async () => Promise.reject(new Error('dial failure')), @@ -270,7 +268,9 @@ describe('dial queue', () => { }) components.transportManager.dialTransportForMultiaddr.returns(stubInterface()) - const connection = mockConnection(mockMultiaddrConnection(mockDuplex(), remotePeer)) + const connection = stubInterface({ + remotePeer + }) components.transportManager.dial.callsFake(async (ma, opts = {}) => { if (ma.toString() === maStr) { @@ -311,7 +311,9 @@ describe('dial queue', () => { }] }) - const connection = mockConnection(mockMultiaddrConnection(mockDuplex(), remotePeer)) + const connection = stubInterface({ + remotePeer + }) components.transportManager.dial.callsFake(async (ma, opts = {}) => { if (ma.toString() === maWithPeer) { diff --git a/packages/libp2p/test/connection-manager/direct.node.ts b/packages/libp2p/test/connection-manager/direct.node.ts deleted file mode 100644 index d12639eb3e..0000000000 --- a/packages/libp2p/test/connection-manager/direct.node.ts +++ /dev/null @@ -1,728 +0,0 @@ -/* eslint-env mocha */ - -import fs from 'node:fs' -import os from 'node:os' -import path from 'node:path' -import { yamux } from '@chainsafe/libp2p-yamux' -import { generateKeyPair } from '@libp2p/crypto/keys' -import { isConnection, AbortError, TypedEventEmitter, start, stop } from '@libp2p/interface' -import { mockConnection, mockConnectionGater, mockDuplex, mockMultiaddrConnection, mockUpgrader } from '@libp2p/interface-compliance-tests/mocks' -import { defaultLogger } from '@libp2p/logger' -import { mplex } from '@libp2p/mplex' -import { peerIdFromString, peerIdFromPrivateKey } from '@libp2p/peer-id' -import { persistentPeerStore } from '@libp2p/peer-store' -import { plaintext } from '@libp2p/plaintext' -import { tcp } from '@libp2p/tcp' -import { multiaddr } from '@multiformats/multiaddr' -import { expect } from 'aegir/chai' -import { MemoryDatastore } from 'datastore-core/memory' -import delay from 'delay' -import { pipe } from 'it-pipe' -import { pushable } from 'it-pushable' -import pDefer from 'p-defer' -import pWaitFor from 'p-wait-for' -import Sinon from 'sinon' -import { stubInterface } from 'sinon-ts' -import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' -import { DefaultAddressManager } from '../../src/address-manager/index.js' -import { defaultComponents, type Components } from '../../src/components.js' -import { DialQueue } from '../../src/connection-manager/dial-queue.js' -import { DefaultConnectionManager } from '../../src/connection-manager/index.js' -import { createLibp2p } from '../../src/index.js' -import { DefaultPeerRouting } from '../../src/peer-routing.js' -import { DefaultTransportManager } from '../../src/transport-manager.js' -import { ECHO_PROTOCOL, echo } from '../fixtures/echo-service.js' -import type { Connection, ConnectionProtector, Stream, Libp2p } from '@libp2p/interface' -import type { TransportManager } from '@libp2p/interface-internal' -import type { Multiaddr } from '@multiformats/multiaddr' - -const listenAddr = multiaddr('/ip4/127.0.0.1/tcp/0') -const unsupportedAddr = multiaddr('/ip4/127.0.0.1/tcp/9999/ws/p2p/QmckxVrJw1Yo8LqvmDJNUmdAsKtSbiKWmrXJFyKmUraBoN') - -describe('dialing (direct, TCP)', () => { - let remoteTM: DefaultTransportManager - let localTM: DefaultTransportManager - let remoteAddr: Multiaddr - let remoteComponents: Components - let localComponents: Components - let resolver: Sinon.SinonStub<[Multiaddr], Promise> - - beforeEach(async () => { - resolver = Sinon.stub<[Multiaddr], Promise>() - const [localPeerId, remotePeerId] = await Promise.all([ - peerIdFromPrivateKey(await generateKeyPair('Ed25519')), - peerIdFromPrivateKey(await generateKeyPair('Ed25519')) - ]) - - const remoteEvents = new TypedEventEmitter() - remoteComponents = defaultComponents({ - peerId: remotePeerId, - events: remoteEvents, - datastore: new MemoryDatastore(), - upgrader: mockUpgrader({ events: remoteEvents }), - connectionGater: mockConnectionGater(), - transportManager: stubInterface({ - getAddrs: Sinon.stub().returns([]) - }) - }) - remoteComponents.peerStore = persistentPeerStore(remoteComponents) - remoteComponents.addressManager = new DefaultAddressManager(remoteComponents, { - listen: [ - listenAddr.toString() - ] - }) - remoteTM = remoteComponents.transportManager = new DefaultTransportManager(remoteComponents) - remoteTM.add(tcp()({ - logger: defaultLogger() - })) - remoteComponents.peerRouting = new DefaultPeerRouting(remoteComponents) - - const localEvents = new TypedEventEmitter() - localComponents = defaultComponents({ - peerId: localPeerId, - events: localEvents, - datastore: new MemoryDatastore(), - upgrader: mockUpgrader({ events: localEvents }), - transportManager: stubInterface(), - connectionGater: mockConnectionGater() - }) - localComponents.peerStore = persistentPeerStore(localComponents) - localComponents.connectionManager = new DefaultConnectionManager(localComponents, { - maxConnections: 100, - inboundUpgradeTimeout: 1000 - }) - localComponents.addressManager = new DefaultAddressManager(localComponents) - localComponents.peerRouting = new DefaultPeerRouting(localComponents) - localTM = localComponents.transportManager = new DefaultTransportManager(localComponents) - localTM.add(tcp()({ - logger: defaultLogger() - })) - - await start(localComponents) - await start(remoteComponents) - - remoteAddr = remoteTM.getAddrs()[0].encapsulate(`/p2p/${remotePeerId.toString()}`) - }) - - afterEach(async () => { - await stop(localComponents) - await stop(remoteComponents) - }) - - afterEach(() => { - Sinon.restore() - }) - - it('should be able to connect to a remote node via its multiaddr', async () => { - const dialer = new DialQueue(localComponents) - - const connection = await dialer.dial(remoteAddr) - expect(connection).to.exist() - await connection.close() - }) - - it('should be able to connect to remote node with duplicated addresses', async () => { - const remotePeer = peerIdFromString(remoteAddr.getPeerId() ?? '') - const dnsaddr = multiaddr(`/dnsaddr/remote.libp2p.io/p2p/${remotePeer}`) - await localComponents.peerStore.merge(remotePeer, { - multiaddrs: [ - dnsaddr - ] - }) - const dialer = new DialQueue(localComponents, { - resolvers: { - dnsaddr: resolver - }, - maxParallelDials: 1 - }) - - // Resolver stub - resolver.withArgs(dnsaddr).resolves([remoteAddr.toString()]) - - const connection = await dialer.dial(remotePeer) - expect(connection).to.exist() - await connection.close() - }) - - it('should fail to connect to an unsupported multiaddr', async () => { - const dialer = new DialQueue(localComponents) - - await expect(dialer.dial(unsupportedAddr)) - .to.eventually.be.rejectedWith(Error) - .and.to.have.nested.property('.name', 'NoValidAddressesError') - }) - - it('should fail to connect if peer has no known addresses', async () => { - const dialer = new DialQueue(localComponents) - const peerId = peerIdFromPrivateKey(await generateKeyPair('Ed25519')) - - await expect(dialer.dial(peerId)) - .to.eventually.be.rejectedWith(Error) - .and.to.have.nested.property('.name', 'NoValidAddressesError') - }) - - it('should be able to connect to a given peer id', async () => { - await localComponents.peerStore.patch(remoteComponents.peerId, { - multiaddrs: remoteTM.getAddrs() - }) - - const dialer = new DialQueue(localComponents) - - const connection = await dialer.dial(remoteComponents.peerId) - expect(connection).to.exist() - await connection.close() - }) - - it('should fail to connect to a given peer with unsupported addresses', async () => { - await localComponents.peerStore.patch(remoteComponents.peerId, { - multiaddrs: [unsupportedAddr] - }) - - const dialer = new DialQueue(localComponents) - - await expect(dialer.dial(remoteComponents.peerId)) - .to.eventually.be.rejectedWith(Error) - .and.to.have.nested.property('.name', 'NoValidAddressesError') - }) - - it('should only try to connect to addresses supported by the transports configured', async () => { - const remoteAddrs = remoteTM.getAddrs() - - const peerId = peerIdFromPrivateKey(await generateKeyPair('Ed25519')) - await localComponents.peerStore.patch(peerId, { - multiaddrs: [...remoteAddrs, unsupportedAddr] - }) - - const dialer = new DialQueue(localComponents) - - Sinon.spy(localTM, 'dial') - const connection = await dialer.dial(peerId) - expect(localTM.dial).to.have.property('callCount', remoteAddrs.length) - expect(connection).to.exist() - - await connection.close() - }) - - it('should abort dials on queue task timeout', async () => { - const dialer = new DialQueue(localComponents, { - dialTimeout: 50 - }) - - Sinon.stub(localTM, 'dial').callsFake(async (addr, options = {}) => { - expect(options.signal).to.exist() - expect(options.signal?.aborted).to.equal(false) - expect(addr.toString()).to.eql(remoteAddr.toString()) - await delay(60) - expect(options.signal?.aborted).to.equal(true) - throw new AbortError() - }) - - await expect(dialer.dial(remoteAddr)) - .to.eventually.be.rejectedWith(Error) - .and.to.have.property('name', 'TimeoutError') - }) - - it('should only dial to the max concurrency', async () => { - const peerId1 = peerIdFromPrivateKey(await generateKeyPair('Ed25519')) - const peerId2 = peerIdFromPrivateKey(await generateKeyPair('Ed25519')) - const peerId3 = peerIdFromPrivateKey(await generateKeyPair('Ed25519')) - - const addr1 = multiaddr(`/ip4/127.0.0.1/tcp/1234/p2p/${peerId1}`) - const addr2 = multiaddr(`/ip4/127.0.12.4/tcp/3210/p2p/${peerId2}`) - const addr3 = multiaddr(`/ip4/123.3.11.1/tcp/2010/p2p/${peerId3}`) - - const slowDial = async (): Promise => { - await delay(100) - return mockConnection(mockMultiaddrConnection(mockDuplex(), peerId1)) - } - - const actions: Record Promise> = { - [addr1.toString()]: slowDial, - [addr2.toString()]: slowDial, - [addr3.toString()]: async () => mockConnection(mockMultiaddrConnection(mockDuplex(), peerId3)) - } - - const dialer = new DialQueue(localComponents, { - maxParallelDials: 2 - }) - - const transportManagerDialStub = Sinon.stub(localTM, 'dial') - transportManagerDialStub.callsFake(async ma => { - const maStr = ma.toString() - const action = actions[maStr] - - if (action != null) { - return action() - } - - throw new Error(`No action found for multiaddr ${maStr}`) - }) - - // dial 3 different peers - void Promise.all([ - dialer.dial(addr1), - dialer.dial(addr2), - dialer.dial(addr3) - ]) - - // Let the call stack run - await delay(0) - - // We should have 2 in progress, and 1 waiting - expect(transportManagerDialStub).to.have.property('callCount', 2) - - // stop dials - dialer.stop() - }) -}) - -describe('libp2p.dialer (direct, TCP)', () => { - let libp2p: Libp2p - let remoteLibp2p: Libp2p - let remoteAddr: Multiaddr - - beforeEach(async () => { - remoteLibp2p = await createLibp2p({ - addresses: { - listen: [listenAddr.toString()] - }, - transports: [ - tcp() - ], - streamMuxers: [ - yamux(), - mplex() - ], - connectionEncrypters: [ - plaintext() - ], - services: { - echo: echo() - } - }) - - await remoteLibp2p.start() - remoteAddr = remoteLibp2p.getMultiaddrs()[0] - }) - - afterEach(async () => { - Sinon.restore() - - if (libp2p != null) { - await libp2p.stop() - } - - if (remoteLibp2p != null) { - await remoteLibp2p.stop() - } - }) - - it('should use the dialer for connecting to a peer', async () => { - libp2p = await createLibp2p({ - transports: [ - tcp() - ], - streamMuxers: [ - yamux(), - mplex() - ], - connectionEncrypters: [ - plaintext() - ] - }) - - await libp2p.start() - - await libp2p.peerStore.patch(remoteLibp2p.peerId, { - multiaddrs: remoteLibp2p.getMultiaddrs() - }) - - const connection = await libp2p.dial(remoteLibp2p.peerId) - expect(connection).to.exist() - const stream = await connection.newStream(ECHO_PROTOCOL) - expect(stream).to.exist() - expect(stream).to.have.property('protocol', ECHO_PROTOCOL) - await connection.close() - }) - - it('should close all streams when the connection closes', async () => { - libp2p = await createLibp2p({ - transports: [ - tcp() - ], - streamMuxers: [ - yamux(), - mplex() - ], - connectionEncrypters: [ - plaintext() - ] - }) - - await libp2p.start() - - // register some stream handlers to simulate several protocols - await libp2p.handle('/stream-count/1', ({ stream }) => { - void pipe(stream, stream) - }) - await libp2p.handle('/stream-count/2', ({ stream }) => { - void pipe(stream, stream) - }) - await remoteLibp2p.handle('/stream-count/3', ({ stream }) => { - void pipe(stream, stream) - }) - await remoteLibp2p.handle('/stream-count/4', ({ stream }) => { - void pipe(stream, stream) - }) - - const connection = await libp2p.dial(remoteLibp2p.getMultiaddrs()) - - // Create local to remote streams - const stream = await connection.newStream([ECHO_PROTOCOL, '/other/1.0.0']) - await connection.newStream('/stream-count/3') - await libp2p.dialProtocol(remoteLibp2p.peerId, '/stream-count/4') - - // Partially write to the echo stream - const source = pushable() - void stream.sink(source) - source.push(uint8ArrayFromString('hello')) - - // Create remote to local streams - await remoteLibp2p.dialProtocol(libp2p.peerId, ['/stream-count/1', '/other/1.0.0']) - await remoteLibp2p.dialProtocol(libp2p.peerId, ['/stream-count/2', '/other/1.0.0']) - - // Verify stream count - const remoteConn = remoteLibp2p.getConnections(libp2p.peerId) - - if (remoteConn == null) { - throw new Error('No remote connection found') - } - - expect(connection.streams).to.have.length(5) - expect(remoteConn).to.have.lengthOf(1) - expect(remoteConn).to.have.nested.property('[0].streams').with.lengthOf(5) - - // Close the connection and verify all streams have been closed - await connection.close() - await pWaitFor(() => connection.streams.length === 0) - await pWaitFor(() => remoteConn[0].streams.length === 0) - }) - - it('should throw when using dialProtocol with no protocols', async () => { - libp2p = await createLibp2p({ - transports: [ - tcp() - ], - streamMuxers: [ - yamux(), - mplex() - ], - connectionEncrypters: [ - plaintext() - ] - }) - - await libp2p.start() - - // @ts-expect-error invalid params - await expect(libp2p.dialProtocol(remoteAddr)) - .to.eventually.be.rejectedWith(Error) - .and.to.have.property('name', 'InvalidParametersError') - - await expect(libp2p.dialProtocol(remoteAddr, [])) - .to.eventually.be.rejectedWith(Error) - .and.to.have.property('name', 'InvalidParametersError') - }) - - it('should be able to use hangup to close connections', async () => { - libp2p = await createLibp2p({ - transports: [ - tcp() - ], - streamMuxers: [ - yamux(), - mplex() - ], - connectionEncrypters: [ - plaintext() - ] - }) - - await libp2p.start() - - const connection = await libp2p.dial(remoteAddr) - expect(connection).to.exist() - expect(connection.timeline.close).to.not.exist() - await libp2p.hangUp(connection.remotePeer) - expect(connection.timeline.close).to.exist() - }) - - it('should use the protectors when provided for connecting', async () => { - const protector: ConnectionProtector = { - async protect (connection) { - return connection - } - } - - libp2p = await createLibp2p({ - transports: [ - tcp() - ], - streamMuxers: [ - yamux(), - mplex() - ], - connectionEncrypters: [ - plaintext() - ], - connectionProtector: () => protector - }) - - const protectorProtectSpy = Sinon.spy(protector, 'protect') - - await libp2p.start() - - const connection = await libp2p.dial(remoteAddr) - expect(connection).to.exist() - const stream = await connection.newStream(ECHO_PROTOCOL) - expect(stream).to.exist() - expect(stream).to.have.property('protocol', ECHO_PROTOCOL) - await connection.close() - expect(protectorProtectSpy.callCount).to.equal(1) - }) - - it('should coalesce parallel dials to the same peer (id in multiaddr)', async () => { - libp2p = await createLibp2p({ - transports: [ - tcp() - ], - streamMuxers: [ - yamux(), - mplex() - ], - connectionEncrypters: [ - plaintext() - ] - }) - - await libp2p.start() - - const dials = 10 - // PeerId should be in multiaddr - expect(remoteAddr.getPeerId()).to.equal(remoteLibp2p.peerId.toString()) - - await libp2p.peerStore.patch(remoteLibp2p.peerId, { - multiaddrs: remoteLibp2p.getMultiaddrs() - }) - const dialResults = await Promise.all([...new Array(dials)].map(async (_, index) => { - if (index % 2 === 0) return libp2p.dial(remoteLibp2p.peerId) - return libp2p.dial(remoteAddr) - })) - - // All should succeed and we should have ten results - expect(dialResults).to.have.length(10) - for (const connection of dialResults) { - expect(isConnection(connection)).to.equal(true) - } - - // 1 connection, because we know the peer in the multiaddr - expect(libp2p.getConnections()).to.have.lengthOf(1) - expect(remoteLibp2p.getConnections()).to.have.lengthOf(1) - }) - - it('should coalesce parallel dials to the same error on failure', async () => { - libp2p = await createLibp2p({ - transports: [ - tcp() - ], - streamMuxers: [ - yamux(), - mplex() - ], - connectionEncrypters: [ - plaintext() - ] - }) - - await libp2p.start() - - const dials = 10 - const error = new Error('Boom') - // @ts-expect-error private field access - Sinon.stub(libp2p.components.transportManager, 'dial').callsFake(async () => Promise.reject(error)) - - await libp2p.peerStore.patch(remoteLibp2p.peerId, { - multiaddrs: remoteLibp2p.getMultiaddrs() - }) - const dialResults = await Promise.allSettled([...new Array(dials)].map(async (_, index) => { - if (index % 2 === 0) return libp2p.dial(remoteLibp2p.peerId) - return libp2p.dial(remoteAddr) - })) - - // All should succeed and we should have ten results - expect(dialResults).to.have.length(10) - - for (const result of dialResults) { - // All errors should be the exact same as `error` - expect(result).to.have.property('status', 'rejected') - expect(result).to.have.property('reason', error) - } - - // 1 connection, because we know the peer in the multiaddr - expect(libp2p.getConnections()).to.have.lengthOf(0) - expect(remoteLibp2p.getConnections()).to.have.lengthOf(0) - }) - - it('should dial a unix socket', async () => { - if (os.platform() === 'win32') { - return - } - - if (remoteLibp2p != null) { - await remoteLibp2p.stop() - } - - const unixAddr = path.join(os.tmpdir(), `test-${Math.random()}.sock`) - const unixMultiaddr = multiaddr('/unix' + unixAddr) - - remoteLibp2p = await createLibp2p({ - addresses: { - listen: [ - unixMultiaddr.toString() - ] - }, - transports: [ - tcp() - ], - streamMuxers: [ - yamux(), - mplex() - ], - connectionEncrypters: [ - plaintext() - ] - }) - - await remoteLibp2p.start() - - expect(fs.existsSync(unixAddr)).to.be.true() - - libp2p = await createLibp2p({ - transports: [ - tcp() - ], - streamMuxers: [ - yamux(), - mplex() - ], - connectionEncrypters: [ - plaintext() - ] - }) - - await libp2p.start() - - const connection = await libp2p.dial(unixMultiaddr) - - expect(connection.remotePeer.toString()).to.equal(remoteLibp2p.peerId.toString()) - }) - - it('should negotiate protocol fully when dialing a protocol', async () => { - remoteLibp2p = await createLibp2p({ - addresses: { - listen: [ - '/ip4/0.0.0.0/tcp/0' - ] - }, - transports: [ - tcp() - ], - streamMuxers: [ - yamux() - ], - connectionEncrypters: [ - plaintext() - ] - }) - - libp2p = await createLibp2p({ - transports: [ - tcp() - ], - streamMuxers: [ - yamux() - ], - connectionEncrypters: [ - plaintext() - ] - }) - - await Promise.all([ - remoteLibp2p.start(), - libp2p.start() - ]) - - const protocol = '/test/1.0.0' - const streamOpen = pDefer() - - await remoteLibp2p.handle(protocol, ({ stream }) => { - streamOpen.resolve(stream) - }) - - const outboundStream = await libp2p.dialProtocol(remoteLibp2p.getMultiaddrs(), protocol) - - expect(outboundStream).to.have.property('protocol', protocol) - - await expect(streamOpen.promise).to.eventually.have.property('protocol', protocol) - }) - - it('should negotiate protocol fully when opening on a connection', async () => { - remoteLibp2p = await createLibp2p({ - addresses: { - listen: [ - '/ip4/0.0.0.0/tcp/0' - ] - }, - transports: [ - tcp() - ], - streamMuxers: [ - yamux() - ], - connectionEncrypters: [ - plaintext() - ] - }) - - libp2p = await createLibp2p({ - transports: [ - tcp() - ], - streamMuxers: [ - yamux() - ], - connectionEncrypters: [ - plaintext() - ] - }) - - await Promise.all([ - remoteLibp2p.start(), - libp2p.start() - ]) - - const protocol = '/test/1.0.0' - const streamOpen = pDefer() - - await remoteLibp2p.handle(protocol, ({ stream }) => { - streamOpen.resolve(stream) - }) - - const connection = await libp2p.dial(remoteLibp2p.getMultiaddrs()) - const outboundStream = await connection.newStream(protocol) - - expect(outboundStream).to.have.property('protocol', protocol) - - await expect(streamOpen.promise).to.eventually.have.property('protocol', protocol) - }) -}) diff --git a/packages/libp2p/test/connection-manager/direct.spec.ts b/packages/libp2p/test/connection-manager/direct.spec.ts deleted file mode 100644 index fb34dceefb..0000000000 --- a/packages/libp2p/test/connection-manager/direct.spec.ts +++ /dev/null @@ -1,521 +0,0 @@ -/* eslint-env mocha */ - -import { yamux } from '@chainsafe/libp2p-yamux' -import { generateKeyPair } from '@libp2p/crypto/keys' -import { type Identify, identify } from '@libp2p/identify' -import { AbortError, TypedEventEmitter } from '@libp2p/interface' -import { mockConnectionGater, mockDuplex, mockMultiaddrConnection, mockUpgrader, mockConnection } from '@libp2p/interface-compliance-tests/mocks' -import { defaultLogger } from '@libp2p/logger' -import { mplex } from '@libp2p/mplex' -import { peerIdFromString, peerIdFromPrivateKey } from '@libp2p/peer-id' -import { persistentPeerStore } from '@libp2p/peer-store' -import { plaintext } from '@libp2p/plaintext' -import { webSockets } from '@libp2p/websockets' -import * as filters from '@libp2p/websockets/filters' -import { multiaddr } from '@multiformats/multiaddr' -import { expect } from 'aegir/chai' -import { MemoryDatastore } from 'datastore-core/memory' -import delay from 'delay' -import pDefer from 'p-defer' -import { pEvent } from 'p-event' -import sinon from 'sinon' -import { stubInterface } from 'sinon-ts' -import { defaultComponents, type Components } from '../../src/components.js' -import { LAST_DIAL_FAILURE_KEY } from '../../src/connection-manager/constants.js' -import { DefaultConnectionManager } from '../../src/connection-manager/index.js' -import { createLibp2p } from '../../src/index.js' -import { DefaultTransportManager } from '../../src/transport-manager.js' -import type { Libp2p, Connection, Transport } from '@libp2p/interface' -import type { TransportManager } from '@libp2p/interface-internal' -import type { Multiaddr } from '@multiformats/multiaddr' - -const unsupportedAddr = multiaddr('/ip4/127.0.0.1/tcp/9999') -const relayMultiaddr = multiaddr(process.env.RELAY_MULTIADDR) - -describe('dialing (direct, WebSockets)', () => { - let localTM: TransportManager - let localComponents: Components - let remoteAddr: Multiaddr - let remoteComponents: Components - let connectionManager: DefaultConnectionManager - - beforeEach(async () => { - const localEvents = new TypedEventEmitter() - localComponents = defaultComponents({ - peerId: peerIdFromPrivateKey(await generateKeyPair('Ed25519')), - datastore: new MemoryDatastore(), - upgrader: mockUpgrader({ events: localEvents }), - connectionGater: mockConnectionGater(), - transportManager: stubInterface(), - events: localEvents - }) - localComponents.peerStore = persistentPeerStore(localComponents, { - addressFilter: localComponents.connectionGater.filterMultiaddrForPeer - }) - localComponents.connectionManager = new DefaultConnectionManager(localComponents, { - maxConnections: 100, - inboundUpgradeTimeout: 1000 - }) - - localTM = new DefaultTransportManager(localComponents) - localTM.add(webSockets({ filter: filters.all })({ - logger: defaultLogger() - })) - localComponents.transportManager = localTM - - // this peer is spun up in .aegir.cjs - remoteAddr = relayMultiaddr - remoteComponents = defaultComponents({ - peerId: peerIdFromString(remoteAddr.getPeerId() ?? '') - }) - }) - - afterEach(async () => { - sinon.restore() - - if (connectionManager != null) { - await connectionManager.stop() - } - }) - - it('should be able to connect to a remote node via its multiaddr', async () => { - connectionManager = new DefaultConnectionManager(localComponents) - await connectionManager.start() - - const remotePeerId = peerIdFromString(remoteAddr.getPeerId() ?? '') - await localComponents.peerStore.patch(remotePeerId, { - multiaddrs: [remoteAddr] - }) - - const connection = await connectionManager.openConnection(remoteAddr) - expect(connection).to.exist() - await connection.close() - }) - - it('should fail to connect to an unsupported multiaddr', async () => { - connectionManager = new DefaultConnectionManager(localComponents) - await connectionManager.start() - - await expect(connectionManager.openConnection(unsupportedAddr.encapsulate(`/p2p/${remoteComponents.peerId.toString()}`))) - .to.eventually.be.rejectedWith(Error) - .and.to.have.nested.property('.name', 'NoValidAddressesError') - }) - - it('should mark a peer as having recently failed to connect', async () => { - connectionManager = new DefaultConnectionManager(localComponents) - await connectionManager.start() - - await expect(connectionManager.openConnection(multiaddr(`/ip4/127.0.0.1/tcp/12984/ws/p2p/${remoteComponents.peerId.toString()}`))) - .to.eventually.be.rejected() - - const peer = await localComponents.peerStore.get(remoteComponents.peerId) - - expect(peer.metadata.has(LAST_DIAL_FAILURE_KEY)).to.be.true() - }) - - it('should be able to connect to a given peer', async () => { - connectionManager = new DefaultConnectionManager(localComponents) - await connectionManager.start() - - const remotePeerId = peerIdFromString(remoteAddr.getPeerId() ?? '') - await localComponents.peerStore.patch(remotePeerId, { - multiaddrs: [remoteAddr] - }) - - const connection = await connectionManager.openConnection(remotePeerId) - expect(connection).to.exist() - await connection.close() - }) - - it('should fail to connect to a given peer with unsupported addresses', async () => { - connectionManager = new DefaultConnectionManager(localComponents) - await connectionManager.start() - - const remotePeerId = peerIdFromString(remoteAddr.getPeerId() ?? '') - await localComponents.peerStore.patch(remotePeerId, { - multiaddrs: [unsupportedAddr] - }) - - await expect(connectionManager.openConnection(remotePeerId)) - .to.eventually.be.rejectedWith(Error) - .and.to.have.nested.property('.name', 'NoValidAddressesError') - }) - - it('should abort dials on queue task timeout', async () => { - connectionManager = new DefaultConnectionManager(localComponents, { - dialTimeout: 50 - }) - await connectionManager.start() - - const remotePeerId = peerIdFromString(remoteAddr.getPeerId() ?? '') - await localComponents.peerStore.patch(remotePeerId, { - multiaddrs: [remoteAddr] - }) - - sinon.stub(localTM, 'dial').callsFake(async (addr, options) => { - expect(options?.signal).to.exist() - expect(options?.signal?.aborted).to.equal(false) - expect(addr.toString()).to.eql(remoteAddr.toString()) - await delay(60) - expect(options?.signal?.aborted).to.equal(true) - throw new AbortError() - }) - - await expect(connectionManager.openConnection(remoteAddr)) - .to.eventually.be.rejected() - .and.to.have.property('name', 'TimeoutError') - }) - - it('should throw when a peer advertises more than the allowed number of addresses', async () => { - connectionManager = new DefaultConnectionManager(localComponents, { - maxPeerAddrsToDial: 10 - }) - await connectionManager.start() - - const remotePeerId = peerIdFromString(remoteAddr.getPeerId() ?? '') - await localComponents.peerStore.patch(remotePeerId, { - multiaddrs: Array.from({ length: 11 }, (_, i) => multiaddr(`/ip4/127.0.0.1/tcp/1500${i}/ws/p2p/${remotePeerId.toString()}`)) - }) - - await expect(connectionManager.openConnection(remotePeerId)) - .to.eventually.be.rejected() - .and.to.have.property('name', 'DialError') - }) - - it('should sort addresses on dial', async () => { - const peerMultiaddrs = [ - multiaddr('/ip4/127.0.0.1/tcp/15001/ws'), - multiaddr('/ip4/20.0.0.1/tcp/15001/ws'), - multiaddr('/ip4/30.0.0.1/tcp/15001/ws') - ] - - const addressSorter = (): 0 => 0 - const addressesSorttSpy = sinon.spy(addressSorter) - const localTMDialStub = sinon.stub(localTM, 'dial').callsFake(async (ma) => mockConnection(mockMultiaddrConnection(mockDuplex(), remoteComponents.peerId))) - - connectionManager = new DefaultConnectionManager(localComponents, { - addressSorter: addressesSorttSpy, - maxParallelDials: 3 - }) - await connectionManager.start() - - // Inject data into the AddressBook - await localComponents.peerStore.merge(remoteComponents.peerId, { - multiaddrs: peerMultiaddrs - }) - - // Perform 3 multiaddr dials - await connectionManager.openConnection(remoteComponents.peerId) - - const sortedAddresses = peerMultiaddrs - .map((m) => ({ multiaddr: m, isCertified: false })) - .sort(addressSorter) - - expect(localTMDialStub.getCall(0).args[0].equals(sortedAddresses[0].multiaddr)) - }) - - it('shutting down should abort pending dials', async () => { - const addrs = [ - multiaddr('/ip4/0.0.0.0/tcp/8000/ws'), - multiaddr('/ip4/0.0.0.0/tcp/8001/ws'), - multiaddr('/ip4/0.0.0.0/tcp/8002/ws') - ] - connectionManager = new DefaultConnectionManager(localComponents, { - maxParallelDials: 2 - }) - await connectionManager.start() - - // Inject data into the AddressBook - await localComponents.peerStore.merge(remoteComponents.peerId, { - multiaddrs: addrs - }) - - sinon.stub(localTM, 'dial').callsFake(async (_, options) => { - const deferredDial = pDefer() - const onAbort = (): void => { - options?.signal?.removeEventListener('abort', onAbort) - deferredDial.reject(new AbortError()) - } - options?.signal?.addEventListener('abort', onAbort) - return deferredDial.promise - }) - - // Perform 3 multiaddr dials - const dialPromise = connectionManager.openConnection(remoteComponents.peerId) - - // Let the call stack run - await delay(0) - - try { - await connectionManager.stop() - await dialPromise - expect.fail('should have failed') - } catch { - expect(connectionManager.getDialQueue()).to.have.lengthOf(0) // 0 dial requests - } - }) - - it('should dial only the multiaddr that is passed', async () => { - const addrs = [ - multiaddr(`/ip4/0.0.0.0/tcp/8000/ws/p2p/${remoteComponents.peerId.toString()}`), - multiaddr(`/ip4/0.0.0.0/tcp/8001/ws/p2p/${remoteComponents.peerId.toString()}`), - multiaddr(`/ip4/0.0.0.0/tcp/8002/ws/p2p/${remoteComponents.peerId.toString()}`) - ] - - // Inject data into the AddressBook - await localComponents.peerStore.merge(remoteComponents.peerId, { - multiaddrs: addrs - }) - - // different address not in the address book, same peer id - const dialMultiaddr = multiaddr(`/ip4/0.0.0.0/tcp/8003/ws/p2p/${remoteComponents.peerId.toString()}`) - - connectionManager = new DefaultConnectionManager(localComponents) - await connectionManager.start() - - const transactionManagerDialStub = sinon.stub(localTM, 'dial') - transactionManagerDialStub.callsFake(async (ma) => mockConnection(mockMultiaddrConnection(mockDuplex(), remoteComponents.peerId))) - - // Perform dial - await connectionManager.openConnection(dialMultiaddr) - - expect(transactionManagerDialStub).to.have.property('callCount', 1) - expect(transactionManagerDialStub.getCall(0).args[0].toString()).to.equal(dialMultiaddr.toString()) - }) - - it('should throw if dialling an empty array is attempted', async () => { - connectionManager = new DefaultConnectionManager(localComponents) - await connectionManager.start() - - // Perform dial - await expect(connectionManager.openConnection([])).to.eventually.rejected - .with.property('name', 'NoValidAddressesError') - }) - - it('should throw if dialling multiaddrs with mismatched peer ids', async () => { - connectionManager = new DefaultConnectionManager(localComponents) - await connectionManager.start() - - // Perform dial - await expect(connectionManager.openConnection([ - multiaddr(`/ip4/0.0.0.0/tcp/8000/ws/p2p/${(peerIdFromPrivateKey(await generateKeyPair('Ed25519'))).toString()}`), - multiaddr(`/ip4/0.0.0.0/tcp/8001/ws/p2p/${(peerIdFromPrivateKey(await generateKeyPair('Ed25519'))).toString()}`) - ])).to.eventually.rejected - .with.property('name', 'InvalidParametersError') - }) - - it('should throw if dialling multiaddrs with inconsistent peer ids', async () => { - connectionManager = new DefaultConnectionManager(localComponents) - await connectionManager.start() - - // Perform dial - await expect(connectionManager.openConnection([ - multiaddr(`/ip4/0.0.0.0/tcp/8000/ws/p2p/${(peerIdFromPrivateKey(await generateKeyPair('Ed25519'))).toString()}`), - multiaddr('/ip4/0.0.0.0/tcp/8001/ws') - ])).to.eventually.rejected - .with.property('name', 'InvalidParametersError') - - // Perform dial - await expect(connectionManager.openConnection([ - multiaddr('/ip4/0.0.0.0/tcp/8001/ws'), - multiaddr(`/ip4/0.0.0.0/tcp/8000/ws/p2p/${(peerIdFromPrivateKey(await generateKeyPair('Ed25519'))).toString()}`) - ])).to.eventually.rejected - .with.property('name', 'InvalidParametersError') - }) -}) - -describe('libp2p.dialer (direct, WebSockets)', () => { - let libp2p: Libp2p<{ identify: Identify }> - - afterEach(async () => { - sinon.restore() - - if (libp2p != null) { - await libp2p.stop() - } - }) - - it('should run identify automatically after connecting', async () => { - libp2p = await createLibp2p({ - transports: [ - webSockets({ - filter: filters.all - }) - ], - streamMuxers: [ - yamux(), - mplex() - ], - connectionEncrypters: [ - plaintext() - ], - services: { - identify: identify() - }, - connectionGater: mockConnectionGater() - }) - - if (libp2p.services.identify == null) { - throw new Error('Identify service missing') - } - - const identifySpy = sinon.spy(libp2p.services.identify, 'identify') - const peerStorePatchSpy = sinon.spy(libp2p.peerStore, 'patch') - const connectionPromise = pEvent(libp2p, 'connection:open') - - await libp2p.start() - - const connection = await libp2p.dial(relayMultiaddr) - expect(connection).to.exist() - - // Wait for connection event to be emitted - await connectionPromise - - expect(identifySpy.callCount).to.equal(1) - await identifySpy.firstCall.returnValue - - expect(peerStorePatchSpy.callCount).to.equal(1) - - await libp2p.stop() - }) - - it('should not run identify automatically after connecting', async () => { - libp2p = await createLibp2p({ - transports: [ - webSockets({ - filter: filters.all - }) - ], - streamMuxers: [ - yamux() - ], - connectionEncrypters: [ - plaintext() - ], - services: { - identify: identify({ - runOnConnectionOpen: false - }) - }, - connectionGater: mockConnectionGater() - }) - - if (libp2p.services.identify == null) { - throw new Error('Identify service missing') - } - - const identifySpy = sinon.spy(libp2p.services.identify, 'identify') - const connectionPromise = pEvent(libp2p, 'connection:open') - - await libp2p.start() - - const connection = await libp2p.dial(relayMultiaddr) - expect(connection).to.exist() - - // Wait for connection event to be emitted - await connectionPromise - - expect(identifySpy.callCount).to.equal(0) - - await libp2p.stop() - }) - - it('should be able to use hangup to close connections', async () => { - libp2p = await createLibp2p({ - transports: [ - webSockets({ - filter: filters.all - }) - ], - streamMuxers: [ - yamux(), - mplex() - ], - connectionEncrypters: [ - plaintext() - ], - connectionGater: mockConnectionGater() - }) - - await libp2p.start() - - const connection = await libp2p.dial(relayMultiaddr) - expect(connection).to.exist() - expect(connection.timeline.close).to.not.exist() - - await libp2p.hangUp(connection.remotePeer) - expect(connection.timeline.close).to.exist() - - await libp2p.stop() - }) - - it('should be able to use hangup when no connection exists', async () => { - libp2p = await createLibp2p({ - transports: [ - webSockets({ - filter: filters.all - }) - ], - streamMuxers: [ - yamux(), - mplex() - ], - connectionEncrypters: [ - plaintext() - ], - connectionGater: mockConnectionGater() - }) - - await libp2p.hangUp(relayMultiaddr) - }) - - it('should fail to dial self', async () => { - libp2p = await createLibp2p({ - transports: [ - webSockets({ - filter: filters.all - }) - ], - streamMuxers: [ - yamux(), - mplex() - ], - connectionEncrypters: [ - plaintext() - ], - connectionGater: mockConnectionGater() - }) - - await libp2p.start() - - await expect(libp2p.dial(multiaddr(`/ip4/127.0.0.1/tcp/1234/ws/p2p/${libp2p.peerId.toString()}`))) - .to.eventually.be.rejected() - .and.to.have.property('name', 'InvalidPeerIdError') - }) - - it('should limit the maximum dial queue size', async () => { - const transport = stubInterface({ - dialFilter: (ma) => ma, - dial: async () => { - await delay(1000) - return stubInterface() - } - }) - - libp2p = await createLibp2p({ - transports: [ - () => transport - ], - connectionManager: { - maxDialQueueLength: 1, - maxParallelDials: 1 - } - }) - - await expect(Promise.all([ - libp2p.dial(multiaddr('/ip4/127.0.0.1/tcp/1234')), - libp2p.dial(multiaddr('/ip4/127.0.0.1/tcp/1235')) - ])).to.eventually.be.rejected - .with.property('name', 'DialError') - }) -}) diff --git a/packages/libp2p/test/connection-manager/index.node.ts b/packages/libp2p/test/connection-manager/index.node.ts deleted file mode 100644 index aebb600c36..0000000000 --- a/packages/libp2p/test/connection-manager/index.node.ts +++ /dev/null @@ -1,505 +0,0 @@ -/* eslint-env mocha */ - -import { generateKeyPair } from '@libp2p/crypto/keys' -import { TypedEventEmitter, start } from '@libp2p/interface' -import { mockConnection, mockDuplex, mockMultiaddrConnection } from '@libp2p/interface-compliance-tests/mocks' -import { peerIdFromPrivateKey } from '@libp2p/peer-id' -import { dns } from '@multiformats/dns' -import { multiaddr } from '@multiformats/multiaddr' -import { expect } from 'aegir/chai' -import all from 'it-all' -import { pipe } from 'it-pipe' -import sinon from 'sinon' -import { stubInterface } from 'sinon-ts' -import { defaultComponents } from '../../src/components.js' -import { DefaultConnectionManager } from '../../src/connection-manager/index.js' -import { createBaseOptions } from '../fixtures/base-options.browser.js' -import { createNode } from '../fixtures/creators/peer.js' -import { ECHO_PROTOCOL, echo } from '../fixtures/echo-service.js' -import type { Libp2p } from '../../src/index.js' -import type { ConnectionGater, PeerId, PeerStore } from '@libp2p/interface' -import type { TransportManager } from '@libp2p/interface-internal' - -describe('Connection Manager', () => { - let libp2p: Libp2p - let peerIds: PeerId[] - - before(async () => { - peerIds = await Promise.all([ - peerIdFromPrivateKey(await generateKeyPair('Ed25519')), - peerIdFromPrivateKey(await generateKeyPair('Ed25519')) - ]) - }) - - beforeEach(async () => { - libp2p = await createNode({ - config: createBaseOptions({ - addresses: { - listen: ['/ip4/127.0.0.1/tcp/0/ws'] - } - }) - }) - }) - - afterEach(async () => { - await libp2p.stop() - }) - - it('should filter connections on disconnect, removing the closed one', async () => { - const peerStore = stubInterface() - const components = defaultComponents({ - peerId: peerIds[0], - peerStore, - transportManager: stubInterface(), - connectionGater: stubInterface(), - events: new TypedEventEmitter() - }) - const connectionManager = new DefaultConnectionManager(components, { - maxConnections: 1000, - inboundUpgradeTimeout: 1000 - }) - - await start(connectionManager) - - const conn1 = mockConnection(mockMultiaddrConnection(mockDuplex(), peerIds[1])) - const conn2 = mockConnection(mockMultiaddrConnection(mockDuplex(), peerIds[1])) - - expect(connectionManager.getConnections(peerIds[1])).to.have.lengthOf(0) - - // Add connection to the connectionManager - components.events.safeDispatchEvent('connection:open', { detail: conn1 }) - components.events.safeDispatchEvent('connection:open', { detail: conn2 }) - - expect(connectionManager.getConnections(peerIds[1])).to.have.lengthOf(2) - - await conn2.close() - components.events.safeDispatchEvent('connection:close', { detail: conn2 }) - - expect(connectionManager.getConnections(peerIds[1])).to.have.lengthOf(1) - - expect(conn1).to.have.nested.property('status', 'open') - - await connectionManager.stop() - }) - - it('should close connections on stop', async () => { - const peerStore = stubInterface() - const components = defaultComponents({ - peerId: peerIds[0], - peerStore, - transportManager: stubInterface(), - connectionGater: stubInterface(), - events: new TypedEventEmitter() - }) - const connectionManager = new DefaultConnectionManager(components, { - maxConnections: 1000, - inboundUpgradeTimeout: 1000 - }) - - await start(connectionManager) - - const conn1 = mockConnection(mockMultiaddrConnection(mockDuplex(), peerIds[1])) - const conn2 = mockConnection(mockMultiaddrConnection(mockDuplex(), peerIds[1])) - - // Add connection to the connectionManager - components.events.safeDispatchEvent('connection:open', { detail: conn1 }) - components.events.safeDispatchEvent('connection:open', { detail: conn2 }) - - expect(connectionManager.getConnections(peerIds[1])).to.have.lengthOf(2) - - await connectionManager.stop() - - expect(connectionManager.getConnections(peerIds[1])).to.have.lengthOf(0) - }) -}) - -describe('libp2p.connections', () => { - let libp2p: Libp2p - - afterEach(async () => { - if (libp2p != null) { - await libp2p.stop() - } - }) - - it('libp2p.connections gets the connectionManager conns', async () => { - libp2p = await createNode({ - config: createBaseOptions({ - addresses: { - listen: ['/ip4/127.0.0.1/tcp/15003/ws'] - } - }) - }) - const remoteLibp2p = await createNode({ - config: createBaseOptions({ - addresses: { - listen: ['/ip4/127.0.0.1/tcp/15004/ws'] - } - }) - }) - - await libp2p.peerStore.patch(remoteLibp2p.peerId, { - multiaddrs: remoteLibp2p.getMultiaddrs() - }) - const conn = await libp2p.dial(remoteLibp2p.peerId) - - expect(conn).to.be.ok() - expect(libp2p.getConnections()).to.have.lengthOf(1) - - await libp2p.stop() - await remoteLibp2p.stop() - }) - - describe('proactive connections', () => { - let libp2p: Libp2p - let nodes: Libp2p[] = [] - - beforeEach(async () => { - nodes = await Promise.all([ - createNode({ - config: { - addresses: { - listen: ['/ip4/127.0.0.1/tcp/0/ws'] - } - } - }), - createNode({ - config: { - addresses: { - listen: ['/ip4/127.0.0.1/tcp/0/ws'] - } - } - }) - ]) - }) - - afterEach(async () => { - await Promise.all(nodes.map(async (node) => { await node.stop() })) - - if (libp2p != null) { - await libp2p.stop() - } - - sinon.reset() - }) - - it('should be closed status once immediately stopping', async () => { - libp2p = await createNode({ - config: createBaseOptions({ - addresses: { - listen: ['/ip4/127.0.0.1/tcp/15003/ws'] - } - }) - }) - const remoteLibp2p = await createNode({ - config: createBaseOptions({ - addresses: { - listen: ['/ip4/127.0.0.1/tcp/15004/ws'] - } - }) - }) - - await libp2p.peerStore.patch(remoteLibp2p.peerId, { - multiaddrs: remoteLibp2p.getMultiaddrs() - }) - await libp2p.dial(remoteLibp2p.peerId) - - const conns = libp2p.getConnections() - expect(conns.length).to.eql(1) - const conn = conns[0] - - await libp2p.stop() - expect(conn.status).to.eql('closed') - - await remoteLibp2p.stop() - }) - - it('should open multiple connections when forced', async () => { - libp2p = await createNode({ - config: createBaseOptions({ - addresses: { - listen: ['/ip4/127.0.0.1/tcp/0/ws'] - } - }) - }) - - // connect once, should have one connection - await libp2p.dial(nodes[0].getMultiaddrs()) - expect(libp2p.getConnections()).to.have.lengthOf(1) - - // connect twice, should still only have one connection - await libp2p.dial(nodes[0].getMultiaddrs()) - expect(libp2p.getConnections()).to.have.lengthOf(1) - - // force connection, should have two connections now - await libp2p.dial(nodes[0].getMultiaddrs(), { - force: true - }) - expect(libp2p.getConnections()).to.have.lengthOf(2) - }) - - it('should use custom DNS resolver', async () => { - const resolver = sinon.stub() - - libp2p = await createNode({ - config: createBaseOptions({ - addresses: { - listen: ['/ip4/127.0.0.1/tcp/0/ws'] - }, - dns: dns({ - resolvers: { - '.': resolver - } - }) - }) - }) - - const ma = multiaddr('/dnsaddr/example.com/tcp/12345') - const err = new Error('Could not resolve') - - resolver.withArgs('_dnsaddr.example.com').rejects(err) - - await expect(libp2p.dial(ma)).to.eventually.be.rejectedWith(err) - }) - }) - - describe('connection gater', () => { - let libp2p: Libp2p - let remoteLibp2p: Libp2p - - beforeEach(async () => { - remoteLibp2p = await createNode({ - config: createBaseOptions({ - addresses: { - listen: ['/ip4/127.0.0.1/tcp/0/ws'] - }, - services: { - echo: echo() - } - }) - }) - }) - - afterEach(async () => { - if (remoteLibp2p != null) { - await remoteLibp2p.stop() - } - - if (libp2p != null) { - await libp2p.stop() - } - }) - - it('intercept peer dial', async () => { - const denyDialPeer = sinon.stub().returns(true) - - libp2p = await createNode({ - config: createBaseOptions({ - addresses: { - listen: ['/ip4/127.0.0.1/tcp/0/ws'] - }, - connectionGater: { - denyDialPeer - } - }) - }) - await libp2p.peerStore.patch(remoteLibp2p.peerId, { - multiaddrs: remoteLibp2p.getMultiaddrs() - }) - - await expect(libp2p.dial(remoteLibp2p.peerId)) - .to.eventually.be.rejected().with.property('name', 'DialDeniedError') - }) - - it('intercept addr dial', async () => { - const denyDialMultiaddr = sinon.stub().returns(false) - - libp2p = await createNode({ - config: createBaseOptions({ - addresses: { - listen: ['/ip4/127.0.0.1/tcp/0/ws'] - }, - connectionGater: { - denyDialMultiaddr - } - }) - }) - await libp2p.peerStore.patch(remoteLibp2p.peerId, { - multiaddrs: remoteLibp2p.getMultiaddrs() - }) - await libp2p.dial(remoteLibp2p.peerId) - - for (const multiaddr of remoteLibp2p.getMultiaddrs()) { - expect(denyDialMultiaddr.calledWith(multiaddr)).to.be.true() - } - }) - - it('intercept multiaddr store', async () => { - const filterMultiaddrForPeer = sinon.stub().returns(true) - - libp2p = await createNode({ - config: createBaseOptions({ - addresses: { - listen: ['/ip4/127.0.0.1/tcp/0/ws'] - }, - connectionGater: { - filterMultiaddrForPeer - } - }) - }) - - const fullMultiaddr = remoteLibp2p.getMultiaddrs()[0] - - await libp2p.peerStore.merge(remoteLibp2p.peerId, { - multiaddrs: [fullMultiaddr] - }) - - expect(filterMultiaddrForPeer.callCount).to.equal(1) - - const args = filterMultiaddrForPeer.getCall(0).args - expect(args[0].toString()).to.equal(remoteLibp2p.peerId.toString()) - expect(args[1].toString()).to.equal(fullMultiaddr.toString()) - }) - - it('intercept accept inbound connection', async () => { - const denyInboundConnection = sinon.stub().returns(false) - - libp2p = await createNode({ - config: createBaseOptions({ - addresses: { - listen: ['/ip4/127.0.0.1/tcp/0/ws'] - }, - connectionGater: { - denyInboundConnection - } - }) - }) - await remoteLibp2p.peerStore.patch(libp2p.peerId, { - multiaddrs: libp2p.getMultiaddrs() - }) - await remoteLibp2p.dial(libp2p.peerId) - - expect(denyInboundConnection.called).to.be.true() - }) - - it('intercept accept outbound connection', async () => { - const denyOutboundConnection = sinon.stub().returns(false) - - libp2p = await createNode({ - config: createBaseOptions({ - addresses: { - listen: ['/ip4/127.0.0.1/tcp/0/ws'] - }, - connectionGater: { - denyOutboundConnection - } - }) - }) - await libp2p.peerStore.patch(remoteLibp2p.peerId, { - multiaddrs: remoteLibp2p.getMultiaddrs() - }) - await libp2p.dial(remoteLibp2p.peerId) - - expect(denyOutboundConnection.called).to.be.true() - }) - - it('intercept inbound encrypted', async () => { - const denyInboundEncryptedConnection = sinon.stub().returns(false) - - libp2p = await createNode({ - config: createBaseOptions({ - addresses: { - listen: ['/ip4/127.0.0.1/tcp/0/ws'] - }, - connectionGater: { - denyInboundEncryptedConnection - } - }) - }) - await remoteLibp2p.peerStore.patch(libp2p.peerId, { - multiaddrs: libp2p.getMultiaddrs() - }) - await remoteLibp2p.dial(libp2p.peerId) - - expect(denyInboundEncryptedConnection.called).to.be.true() - expect(denyInboundEncryptedConnection.getCall(0).args[0].toMultihash().bytes).to.equalBytes(remoteLibp2p.peerId.toMultihash().bytes) - }) - - it('intercept outbound encrypted', async () => { - const denyOutboundEncryptedConnection = sinon.stub().returns(false) - - libp2p = await createNode({ - config: createBaseOptions({ - addresses: { - listen: ['/ip4/127.0.0.1/tcp/0/ws'] - }, - connectionGater: { - denyOutboundEncryptedConnection - } - }) - }) - await libp2p.peerStore.patch(remoteLibp2p.peerId, { - multiaddrs: remoteLibp2p.getMultiaddrs() - }) - await libp2p.dial(remoteLibp2p.peerId) - - expect(denyOutboundEncryptedConnection.called).to.be.true() - expect(denyOutboundEncryptedConnection.getCall(0).args[0].toMultihash().bytes).to.equalBytes(remoteLibp2p.peerId.toMultihash().bytes) - }) - - it('intercept inbound upgraded', async () => { - const denyInboundUpgradedConnection = sinon.stub().returns(false) - - libp2p = await createNode({ - config: createBaseOptions({ - addresses: { - listen: ['/ip4/127.0.0.1/tcp/0/ws'] - }, - connectionGater: { - denyInboundUpgradedConnection - }, - services: { - echo: echo() - } - }) - }) - await remoteLibp2p.peerStore.patch(libp2p.peerId, { - multiaddrs: libp2p.getMultiaddrs() - }) - const connection = await remoteLibp2p.dial(libp2p.peerId) - const stream = await connection.newStream(ECHO_PROTOCOL) - const input = [Uint8Array.from([0])] - const output = await pipe(input, stream, async (source) => all(source)) - - expect(denyInboundUpgradedConnection.called).to.be.true() - expect(denyInboundUpgradedConnection.getCall(0).args[0].toMultihash().bytes).to.equalBytes(remoteLibp2p.peerId.toMultihash().bytes) - expect(output.map(b => b.subarray())).to.deep.equal(input) - }) - - it('intercept outbound upgraded', async () => { - const denyOutboundUpgradedConnection = sinon.stub().returns(false) - - libp2p = await createNode({ - config: createBaseOptions({ - addresses: { - listen: ['/ip4/127.0.0.1/tcp/0/ws'] - }, - connectionGater: { - denyOutboundUpgradedConnection - } - }) - }) - await libp2p.peerStore.patch(remoteLibp2p.peerId, { - multiaddrs: remoteLibp2p.getMultiaddrs() - }) - const connection = await libp2p.dial(remoteLibp2p.peerId) - const stream = await connection.newStream(ECHO_PROTOCOL) - const input = [Uint8Array.from([0])] - const output = await pipe(input, stream, async (source) => all(source)) - - expect(denyOutboundUpgradedConnection.called).to.be.true() - expect(denyOutboundUpgradedConnection.getCall(0).args[0].toMultihash().bytes).to.equalBytes(remoteLibp2p.peerId.toMultihash().bytes) - expect(output.map(b => b.subarray())).to.deep.equal(input) - }) - }) -}) diff --git a/packages/libp2p/test/connection-manager/index.spec.ts b/packages/libp2p/test/connection-manager/index.spec.ts index 5328382a89..feda255d96 100644 --- a/packages/libp2p/test/connection-manager/index.spec.ts +++ b/packages/libp2p/test/connection-manager/index.spec.ts @@ -1,21 +1,24 @@ /* eslint-env mocha */ import { generateKeyPair } from '@libp2p/crypto/keys' -import { TypedEventEmitter, KEEP_ALIVE } from '@libp2p/interface' +import { TypedEventEmitter, KEEP_ALIVE, start, stop } from '@libp2p/interface' import { mockConnection, mockDuplex, mockMultiaddrConnection, mockMetrics } from '@libp2p/interface-compliance-tests/mocks' import { defaultLogger } from '@libp2p/logger' import { peerIdFromPrivateKey } from '@libp2p/peer-id' +import { dns } from '@multiformats/dns' import { multiaddr } from '@multiformats/multiaddr' import { expect } from 'aegir/chai' import { pEvent } from 'p-event' import pWaitFor from 'p-wait-for' import sinon from 'sinon' import { stubInterface } from 'sinon-ts' +import { defaultComponents } from '../../src/components.js' import { DefaultConnectionManager, type DefaultConnectionManagerComponents } from '../../src/connection-manager/index.js' -import { createBaseOptions } from '../fixtures/base-options.browser.js' -import { createNode } from '../fixtures/creators/peer.js' +import { createLibp2p } from '../../src/index.js' +import { createPeers } from '../fixtures/create-peers.js' import { getComponent } from '../fixtures/get-component.js' -import type { AbortOptions, Connection, ConnectionGater, Libp2p, PeerId, PeerRouting, PeerStore } from '@libp2p/interface' +import type { Echo } from '@libp2p/echo' +import type { ConnectionGater, PeerId, PeerStore, Libp2p, AbortOptions, Connection, PeerRouting } from '@libp2p/interface' import type { TransportManager } from '@libp2p/interface-internal' const defaultOptions = { @@ -23,7 +26,7 @@ const defaultOptions = { inboundUpgradeTimeout: 10000 } -function defaultComponents (peerId: PeerId): DefaultConnectionManagerComponents { +function createDefaultComponents (peerId: PeerId): DefaultConnectionManagerComponents { return { peerId, peerStore: stubInterface(), @@ -40,21 +43,12 @@ describe('Connection Manager', () => { let connectionManager: DefaultConnectionManager afterEach(async () => { - sinon.restore() - - if (connectionManager != null) { - await connectionManager.stop() - } - - if (libp2p != null) { - await libp2p.stop() - } + await stop(connectionManager, libp2p) }) it('should be able to create without metrics', async () => { - libp2p = await createNode({ - config: createBaseOptions(), - started: false + libp2p = await createLibp2p({ + start: false }) const spy = sinon.spy(getComponent(libp2p, 'connectionManager'), 'start') @@ -65,11 +59,9 @@ describe('Connection Manager', () => { }) it('should be able to create with metrics', async () => { - libp2p = await createNode({ - config: createBaseOptions({ - metrics: mockMetrics() - }), - started: false + libp2p = await createLibp2p({ + start: false, + metrics: mockMetrics() }) const spy = sinon.spy(getComponent(libp2p, 'connectionManager'), 'start') @@ -81,17 +73,12 @@ describe('Connection Manager', () => { it('should close connections with low tag values first', async () => { const max = 5 - libp2p = await createNode({ - config: createBaseOptions({ - connectionManager: { - maxConnections: max - } - }), - started: false + libp2p = await createLibp2p({ + connectionManager: { + maxConnections: max + } }) - await libp2p.start() - const connectionManager = getComponent(libp2p, 'connectionManager') const connectionManagerMaybePruneConnectionsSpy = sinon.spy(connectionManager.connectionPruner, '_maybePruneConnections') const spies = new Map>>() @@ -139,17 +126,12 @@ describe('Connection Manager', () => { it('should close shortest-lived connection if the tag values are equal', async () => { const max = 5 - libp2p = await createNode({ - config: createBaseOptions({ - connectionManager: { - maxConnections: max - } - }), - started: false + libp2p = await createLibp2p({ + connectionManager: { + maxConnections: max + } }) - await libp2p.start() - const connectionManager = getComponent(libp2p, 'connectionManager') const connectionManagerMaybePruneConnectionsSpy = sinon.spy(connectionManager.connectionPruner, '_maybePruneConnections') const spies = new Map>>() @@ -203,20 +185,15 @@ describe('Connection Manager', () => { const max = 2 const remoteAddr = multiaddr('/ip4/83.13.55.32/tcp/59283') - libp2p = await createNode({ - config: createBaseOptions({ - connectionManager: { - maxConnections: max, - allow: [ - '/ip4/83.13.55.32' - ] - } - }), - started: false + libp2p = await createLibp2p({ + connectionManager: { + maxConnections: max, + allow: [ + '/ip4/83.13.55.32' + ] + } }) - await libp2p.start() - const connectionManager = getComponent(libp2p, 'connectionManager') const connectionManagerMaybePruneConnectionsSpy = sinon.spy(connectionManager.connectionPruner, '_maybePruneConnections') const spies = new Map>>() @@ -287,17 +264,12 @@ describe('Connection Manager', () => { it('should close connection when the maximum connections has been reached even without tags', async () => { const max = 5 - libp2p = await createNode({ - config: createBaseOptions({ - connectionManager: { - maxConnections: max - } - }), - started: false + libp2p = await createLibp2p({ + connectionManager: { + maxConnections: max + } }) - await libp2p.start() - const connectionManager = getComponent(libp2p, 'connectionManager') const connectionManagerMaybePruneConnectionsSpy = sinon.spy(connectionManager.connectionPruner, '_maybePruneConnections') const eventPromise = pEvent(libp2p, 'connection:prune') @@ -319,22 +291,20 @@ describe('Connection Manager', () => { }) it('should fail if the connection manager has mismatched connection limit options', async () => { - await expect(createNode({ - config: createBaseOptions({ + await expect( + createLibp2p({ connectionManager: { maxConnections: -1 } - }), - started: false - })).to.eventually.rejected('maxConnections must be greater') + }) + ).to.eventually.rejected('maxConnections must be greater') }) it('should reconnect to important peers on startup', async () => { const peerId = peerIdFromPrivateKey(await generateKeyPair('Ed25519')) - libp2p = await createNode({ - config: createBaseOptions(), - started: false + libp2p = await createLibp2p({ + start: false }) const connectionManager = getComponent(libp2p, 'connectionManager') @@ -363,7 +333,7 @@ describe('Connection Manager', () => { it('should deny connections from denylist multiaddrs', async () => { const remoteAddr = multiaddr('/ip4/83.13.55.32/tcp/59283') - connectionManager = new DefaultConnectionManager(defaultComponents(libp2p.peerId), { + connectionManager = new DefaultConnectionManager(createDefaultComponents(libp2p.peerId), { ...defaultOptions, deny: [ '/ip4/83.13.55.32' @@ -385,7 +355,7 @@ describe('Connection Manager', () => { }) it('should deny connections when maxConnections is exceeded', async () => { - connectionManager = new DefaultConnectionManager(defaultComponents(libp2p.peerId), { + connectionManager = new DefaultConnectionManager(createDefaultComponents(libp2p.peerId), { ...defaultOptions, maxConnections: 1 }) @@ -414,7 +384,7 @@ describe('Connection Manager', () => { }) it('should deny connections from peers that connect too frequently', async () => { - connectionManager = new DefaultConnectionManager(defaultComponents(libp2p.peerId), { + connectionManager = new DefaultConnectionManager(createDefaultComponents(libp2p.peerId), { ...defaultOptions, inboundConnectionThreshold: 1 }) @@ -446,7 +416,7 @@ describe('Connection Manager', () => { it('should allow connections from allowlist multiaddrs', async () => { const remoteAddr = multiaddr('/ip4/83.13.55.32/tcp/59283') - connectionManager = new DefaultConnectionManager(defaultComponents(libp2p.peerId), { + connectionManager = new DefaultConnectionManager(createDefaultComponents(libp2p.peerId), { ...defaultOptions, maxConnections: 1, allow: [ @@ -479,7 +449,7 @@ describe('Connection Manager', () => { }) it('should limit the number of inbound pending connections', async () => { - connectionManager = new DefaultConnectionManager(defaultComponents(libp2p.peerId), { + connectionManager = new DefaultConnectionManager(createDefaultComponents(libp2p.peerId), { ...defaultOptions, maxIncomingPendingConnections: 1 }) @@ -522,7 +492,7 @@ describe('Connection Manager', () => { }) it('should allow dialing peers when an existing limited connection exists', async () => { - connectionManager = new DefaultConnectionManager(defaultComponents(libp2p.peerId), { + connectionManager = new DefaultConnectionManager(createDefaultComponents(libp2p.peerId), { ...defaultOptions, maxIncomingPendingConnections: 1 }) @@ -560,3 +530,145 @@ describe('Connection Manager', () => { expect(conn).to.equal(newConnection) }) }) + +describe('Connection Manager', () => { + let peerIds: PeerId[] + + before(async () => { + peerIds = await Promise.all([ + peerIdFromPrivateKey(await generateKeyPair('Ed25519')), + peerIdFromPrivateKey(await generateKeyPair('Ed25519')) + ]) + }) + + it('should filter connections on disconnect, removing the closed one', async () => { + const peerStore = stubInterface() + const components = defaultComponents({ + peerId: peerIds[0], + peerStore, + transportManager: stubInterface(), + connectionGater: stubInterface(), + events: new TypedEventEmitter() + }) + const connectionManager = new DefaultConnectionManager(components, { + maxConnections: 1000, + inboundUpgradeTimeout: 1000 + }) + + await start(connectionManager) + + const conn1 = mockConnection(mockMultiaddrConnection(mockDuplex(), peerIds[1])) + const conn2 = mockConnection(mockMultiaddrConnection(mockDuplex(), peerIds[1])) + + expect(connectionManager.getConnections(peerIds[1])).to.have.lengthOf(0) + + // Add connection to the connectionManager + components.events.safeDispatchEvent('connection:open', { detail: conn1 }) + components.events.safeDispatchEvent('connection:open', { detail: conn2 }) + + expect(connectionManager.getConnections(peerIds[1])).to.have.lengthOf(2) + + await conn2.close() + components.events.safeDispatchEvent('connection:close', { detail: conn2 }) + + expect(connectionManager.getConnections(peerIds[1])).to.have.lengthOf(1) + + expect(conn1).to.have.nested.property('status', 'open') + + await connectionManager.stop() + }) + + it('should close connections on stop', async () => { + const peerStore = stubInterface() + const components = defaultComponents({ + peerId: peerIds[0], + peerStore, + transportManager: stubInterface(), + connectionGater: stubInterface(), + events: new TypedEventEmitter() + }) + const connectionManager = new DefaultConnectionManager(components, { + maxConnections: 1000, + inboundUpgradeTimeout: 1000 + }) + + await start(connectionManager) + + const conn1 = mockConnection(mockMultiaddrConnection(mockDuplex(), peerIds[1])) + const conn2 = mockConnection(mockMultiaddrConnection(mockDuplex(), peerIds[1])) + + // Add connection to the connectionManager + components.events.safeDispatchEvent('connection:open', { detail: conn1 }) + components.events.safeDispatchEvent('connection:open', { detail: conn2 }) + + expect(connectionManager.getConnections(peerIds[1])).to.have.lengthOf(2) + + await connectionManager.stop() + + expect(connectionManager.getConnections(peerIds[1])).to.have.lengthOf(0) + }) +}) + +describe('libp2p.connections', () => { + let dialer: Libp2p<{ echo: Echo }> + let listener: Libp2p<{ echo: Echo }> + + afterEach(async () => { + await stop(dialer, listener) + }) + + it('libp2p.getConnections gets the connectionManager conns', async () => { + ({ dialer, listener } = await createPeers()) + + const conn = await dialer.dial(listener.getMultiaddrs()) + + expect(conn).to.be.ok() + expect(dialer.getConnections()).to.have.lengthOf(1) + }) + + it('should be closed status after stopping', async () => { + ({ dialer, listener } = await createPeers()) + + const conn = await dialer.dial(listener.getMultiaddrs()) + + await dialer.stop() + expect(conn.status).to.eql('closed') + }) + + it('should open multiple connections when forced', async () => { + ({ dialer, listener } = await createPeers()) + + // connect once, should have one connection + await dialer.dial(listener.getMultiaddrs()) + expect(dialer.getConnections()).to.have.lengthOf(1) + + // connect twice, should still only have one connection + await dialer.dial(listener.getMultiaddrs()) + expect(dialer.getConnections()).to.have.lengthOf(1) + + // force connection, should have two connections now + await dialer.dial(listener.getMultiaddrs(), { + force: true + }) + expect(dialer.getConnections()).to.have.lengthOf(2) + }) + + it('should use custom DNS resolver', async () => { + const resolver = sinon.stub() + + ;({ dialer, listener } = await createPeers({ + dns: dns({ + resolvers: { + '.': resolver + } + }) + })) + + const ma = multiaddr('/dnsaddr/example.com/tcp/12345') + const err = new Error('Could not resolve') + + resolver.withArgs('_dnsaddr.example.com').rejects(err) + + await expect(dialer.dial(ma)).to.eventually.be.rejectedWith(err) + }) +}) diff --git a/packages/libp2p/test/connection-manager/resolver.spec.ts b/packages/libp2p/test/connection-manager/resolver.spec.ts index 69184ac6f9..a03d2df2ae 100644 --- a/packages/libp2p/test/connection-manager/resolver.spec.ts +++ b/packages/libp2p/test/connection-manager/resolver.spec.ts @@ -1,50 +1,29 @@ /* eslint-env mocha */ import { yamux } from '@chainsafe/libp2p-yamux' -import { RELAY_V2_HOP_CODEC } from '@libp2p/circuit-relay-v2' -import { circuitRelayServer, type CircuitRelayService, circuitRelayTransport } from '@libp2p/circuit-relay-v2' -import { generateKeyPair } from '@libp2p/crypto/keys' -import { identify } from '@libp2p/identify' -import { mockConnection, mockConnectionGater, mockDuplex, mockMultiaddrConnection } from '@libp2p/interface-compliance-tests/mocks' +import { stop } from '@libp2p/interface' +import { memory } from '@libp2p/memory' import { mplex } from '@libp2p/mplex' -import { peerIdFromString, peerIdFromPrivateKey } from '@libp2p/peer-id' import { plaintext } from '@libp2p/plaintext' -import { webSockets } from '@libp2p/websockets' -import * as filters from '@libp2p/websockets/filters' import { multiaddr } from '@multiformats/multiaddr' import { expect } from 'aegir/chai' -import pDefer from 'p-defer' import sinon from 'sinon' import { createLibp2p } from '../../src/index.js' -import type { Libp2p, PeerId, Transport } from '@libp2p/interface' +import type { Libp2p } from '@libp2p/interface' import type { Multiaddr } from '@multiformats/multiaddr' -const relayAddr = multiaddr(process.env.RELAY_MULTIADDR) - -const relayedAddr = (peerId: PeerId): string => `${relayAddr.toString()}/p2p-circuit/p2p/${peerId.toString()}` - -const getDnsRelayedAddrStub = (peerId: PeerId): string[] => [ - `${relayedAddr(peerId)}` -] - -describe('dialing (resolvable addresses)', () => { - let libp2p: Libp2p - let remoteLibp2p: Libp2p<{ relay: CircuitRelayService }> +describe('resolver', () => { + let dialer: Libp2p + let listener: Libp2p let resolver: sinon.SinonStub<[Multiaddr], Promise> beforeEach(async () => { resolver = sinon.stub<[Multiaddr], Promise>(); - [libp2p, remoteLibp2p] = await Promise.all([ + [dialer, listener] = await Promise.all([ createLibp2p({ - addresses: { - listen: [`${relayAddr.toString()}/p2p-circuit`] - }, transports: [ - circuitRelayTransport(), - webSockets({ - filter: filters.all - }) + memory() ], streamMuxers: [ yamux(), @@ -57,21 +36,14 @@ describe('dialing (resolvable addresses)', () => { }, connectionEncrypters: [ plaintext() - ], - connectionGater: mockConnectionGater(), - services: { - identify: identify() - } + ] }), createLibp2p({ addresses: { - listen: [`${relayAddr.toString()}/p2p-circuit`] + listen: ['/memory/location'] }, transports: [ - circuitRelayTransport(), - webSockets({ - filter: filters.all - }) + memory() ], streamMuxers: [ yamux(), @@ -84,108 +56,37 @@ describe('dialing (resolvable addresses)', () => { }, connectionEncrypters: [ plaintext() - ], - services: { - relay: circuitRelayServer(), - identify: identify() - }, - connectionGater: mockConnectionGater() + ] }) ]) - - await Promise.all([ - libp2p.start(), - remoteLibp2p.start() - ]) }) afterEach(async () => { sinon.restore() - await Promise.all([libp2p, remoteLibp2p].map(async n => { - if (n != null) { - await n.stop() - } - })) + await stop(dialer, listener) }) - it('resolves dnsaddr to ws local address', async () => { - const peerId = peerIdFromPrivateKey(await generateKeyPair('Ed25519')) - // ensure remote libp2p creates reservation on relay - await remoteLibp2p.peerStore.merge(peerId, { - protocols: [RELAY_V2_HOP_CODEC] - }) - const remoteId = remoteLibp2p.peerId - const dialAddr = multiaddr(`/dnsaddr/remote.libp2p.io/p2p/${remoteId.toString()}`) - const relayedAddrFetched = multiaddr(relayedAddr(remoteId)) + it('should use the dnsaddr resolver to resolve a dnsaddr address', async () => { + const dialAddr = multiaddr(`/dnsaddr/remote.libp2p.io/p2p/${listener.peerId}`) - // Transport spy - const transport = getTransport(libp2p, '@libp2p/circuit-relay-v2-transport') - const transportDialSpy = sinon.spy(transport, 'dial') + // resolver stub + resolver.withArgs(dialAddr).resolves(listener.getMultiaddrs().map(ma => ma.toString())) - // Resolver stub - resolver.onCall(0).returns(Promise.resolve(getDnsRelayedAddrStub(remoteId))) - - // Dial with address resolve - const connection = await libp2p.dial(dialAddr) + // dial with resolved address + const connection = await dialer.dial(dialAddr) expect(connection).to.exist() - expect(connection.remoteAddr.equals(relayedAddrFetched)) - - const dialArgs = transportDialSpy.firstCall.args - expect(dialArgs[0].equals(relayedAddrFetched)).to.eql(true) - }) - - // TODO: Temporary solution does not resolve dns4/dns6 - // Resolver just returns the received multiaddrs - it('stops recursive resolve if finds dns4/dns6 and dials it', async () => { - const remoteId = remoteLibp2p.peerId - const dialAddr = multiaddr(`/dnsaddr/remote.libp2p.io/p2p/${remoteId.toString()}`) - - // Stub resolver - const dnsMa = multiaddr(`/dns4/ams-1.remote.libp2p.io/tcp/443/wss/p2p/${remoteId.toString()}`) - resolver.returns(Promise.resolve([ - `${dnsMa.toString()}` - ])) - - const deferred = pDefer() - - // Stub transport - const transport = getTransport(libp2p, '@libp2p/websockets') - const stubTransport = sinon.stub(transport, 'dial') - stubTransport.callsFake(async (multiaddr) => { - expect(multiaddr.equals(dnsMa)).to.equal(true) - - deferred.resolve() - - return mockConnection(mockMultiaddrConnection(mockDuplex(), peerIdFromString(multiaddr.getPeerId() ?? ''))) - }) - - void libp2p.dial(dialAddr) - - await deferred.promise + expect(connection.remoteAddr.equals(listener.getMultiaddrs()[0])) }) it('fails to dial if resolve fails and there are no addresses to dial', async () => { - const remoteId = remoteLibp2p.peerId - const dialAddr = multiaddr(`/dnsaddr/remote.libp2p.io/p2p/${remoteId.toString()}`) + const dialAddr = multiaddr(`/dnsaddr/remote.libp2p.io/p2p/${listener.peerId}`) const err = new Error() // Stub resolver - resolver.returns(Promise.reject(err)) + resolver.rejects(err) - await expect(libp2p.dial(dialAddr)) + await expect(dialer.dial(dialAddr)) .to.eventually.be.rejectedWith(err) }) }) - -function getTransport (libp2p: any, tag: string): Transport { - const transport = libp2p.components.transportManager.getTransports().find((t: any) => { - return t[Symbol.toStringTag] === tag - }) - - if (transport != null) { - return transport - } - - throw new Error(`No transport found for ${tag}`) -} diff --git a/packages/libp2p/test/core/consume-peer-record.spec.ts b/packages/libp2p/test/core/consume-peer-record.spec.ts index 65915e0069..f3da3952bd 100644 --- a/packages/libp2p/test/core/consume-peer-record.spec.ts +++ b/packages/libp2p/test/core/consume-peer-record.spec.ts @@ -1,7 +1,5 @@ /* eslint-env mocha */ -import { plaintext } from '@libp2p/plaintext' -import { webSockets } from '@libp2p/websockets' import { multiaddr } from '@multiformats/multiaddr' import { createLibp2p } from '../../src/index.js' import type { Libp2p } from '@libp2p/interface' @@ -10,14 +8,7 @@ describe('Consume peer record', () => { let libp2p: Libp2p beforeEach(async () => { - libp2p = await createLibp2p({ - transports: [ - webSockets() - ], - connectionEncrypters: [ - plaintext() - ] - }) + libp2p = await createLibp2p() }) afterEach(async () => { diff --git a/packages/libp2p/test/core/core.spec.ts b/packages/libp2p/test/core/core.spec.ts index a7d9b92772..1674efcc45 100644 --- a/packages/libp2p/test/core/core.spec.ts +++ b/packages/libp2p/test/core/core.spec.ts @@ -1,12 +1,11 @@ /* eslint-env mocha */ -import { circuitRelayTransport } from '@libp2p/circuit-relay-v2' -import { identify } from '@libp2p/identify' -import { webSockets } from '@libp2p/websockets' +import { memory } from '@libp2p/memory' import { multiaddr } from '@multiformats/multiaddr' import { expect } from 'aegir/chai' +import { stubInterface } from 'sinon-ts' import { createLibp2p } from '../../src/index.js' -import type { Libp2p } from '@libp2p/interface' +import type { Libp2p, Transport } from '@libp2p/interface' describe('core', () => { let libp2p: Libp2p @@ -22,11 +21,7 @@ describe('core', () => { }) it('should say an address is not dialable if we have no transport for it', async () => { - libp2p = await createLibp2p({ - transports: [ - webSockets() - ] - }) + libp2p = await createLibp2p() const ma = multiaddr('/dns4/example.com/sctp/1234') @@ -36,11 +31,11 @@ describe('core', () => { it('should say an address is dialable if a transport is configured', async () => { libp2p = await createLibp2p({ transports: [ - webSockets() + memory() ] }) - const ma = multiaddr('/dns4/example.com/tls/ws') + const ma = multiaddr('/memory/address-1') await expect(libp2p.isDialable(ma)).to.eventually.be.true() }) @@ -48,24 +43,25 @@ describe('core', () => { it('should test if a protocol can run over a limited connection', async () => { libp2p = await createLibp2p({ transports: [ - webSockets(), - circuitRelayTransport() - ], - services: { - identify: identify() - } + () => { + // stub a transport that can dial any address + return stubInterface({ + dialFilter: (addrs) => addrs + }) + } + ] }) await expect(libp2p.isDialable(multiaddr('/dns4/example.com/tls/ws'), { runOnLimitedConnection: false - })).to.eventually.be.true() + })).to.eventually.be.true('could not dial memory address') await expect(libp2p.isDialable(multiaddr('/dns4/example.com/tls/ws/p2p/12D3KooWSExt8hTzoaHEhn435BTK6BPNSY1LpTc1j2o9Gw53tXE1/p2p-circuit/p2p/12D3KooWSExt8hTzoaHEhn435BTK6BPNSY1LpTc1j2o9Gw53tXE2'), { runOnLimitedConnection: true - })).to.eventually.be.true() + })).to.eventually.be.true('could not circuit relay address') await expect(libp2p.isDialable(multiaddr('/dns4/example.com/tls/ws/p2p/12D3KooWSExt8hTzoaHEhn435BTK6BPNSY1LpTc1j2o9Gw53tXE1/p2p-circuit/p2p/12D3KooWSExt8hTzoaHEhn435BTK6BPNSY1LpTc1j2o9Gw53tXE2'), { runOnLimitedConnection: false - })).to.eventually.be.false() + })).to.eventually.be.false('could dial circuit address') }) }) diff --git a/packages/libp2p/test/core/encryption.spec.ts b/packages/libp2p/test/core/encryption.spec.ts deleted file mode 100644 index e4745fd003..0000000000 --- a/packages/libp2p/test/core/encryption.spec.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* eslint-env mocha */ - -import { plaintext } from '@libp2p/plaintext' -import { webSockets } from '@libp2p/websockets' -import { createLibp2p, type Libp2pOptions } from '../../src/index.js' - -describe('Connection encryption configuration', () => { - it('can be created', async () => { - const config: Libp2pOptions = { - start: false, - transports: [ - webSockets() - ], - connectionEncrypters: [ - plaintext() - ] - } - await createLibp2p(config) - }) -}) diff --git a/packages/libp2p/test/core/events.spec.ts b/packages/libp2p/test/core/events.spec.ts index 17479c7c34..6911508cbc 100644 --- a/packages/libp2p/test/core/events.spec.ts +++ b/packages/libp2p/test/core/events.spec.ts @@ -1,7 +1,5 @@ /* eslint-env mocha */ -import { plaintext } from '@libp2p/plaintext' -import { webSockets } from '@libp2p/websockets' import { expect } from 'aegir/chai' import { pEvent } from 'p-event' import { createLibp2p } from '../../src/index.js' @@ -18,13 +16,7 @@ describe('events', () => { it('should emit a start event', async () => { node = await createLibp2p({ - start: false, - transports: [ - webSockets() - ], - connectionEncrypters: [ - plaintext() - ] + start: false }) const eventPromise = pEvent<'start', CustomEvent>(node, 'start') @@ -34,14 +26,7 @@ describe('events', () => { }) it('should emit a stop event', async () => { - node = await createLibp2p({ - transports: [ - webSockets() - ], - connectionEncrypters: [ - plaintext() - ] - }) + node = await createLibp2p() const eventPromise = pEvent<'stop', CustomEvent>(node, 'stop') diff --git a/packages/libp2p/test/core/listening.node.ts b/packages/libp2p/test/core/listening.spec.ts similarity index 58% rename from packages/libp2p/test/core/listening.node.ts rename to packages/libp2p/test/core/listening.spec.ts index 38b69653f5..e7976d7499 100644 --- a/packages/libp2p/test/core/listening.node.ts +++ b/packages/libp2p/test/core/listening.spec.ts @@ -1,27 +1,30 @@ /* eslint-env mocha */ +import { stop } from '@libp2p/interface' +import { memory } from '@libp2p/memory' import { plaintext } from '@libp2p/plaintext' -import { tcp } from '@libp2p/tcp' import { expect } from 'aegir/chai' import { createLibp2p } from '../../src/index.js' import type { Libp2p } from '@libp2p/interface' -const listenAddr = '/ip4/0.0.0.0/tcp/0' - describe('Listening', () => { let libp2p: Libp2p after(async () => { - await libp2p.stop() + await stop(libp2p) }) it('should replace wildcard host and port with actual host and port on startup', async () => { + const listenAddress = '/memory/address-1' + libp2p = await createLibp2p({ addresses: { - listen: [listenAddr] + listen: [ + listenAddress + ] }, transports: [ - tcp() + memory() ], connectionEncrypters: [ plaintext() @@ -34,15 +37,8 @@ describe('Listening', () => { const addrs = libp2p.components.transportManager.getAddrs() // Should get something like: - // /ip4/127.0.0.1/tcp/50866 - // /ip4/192.168.1.2/tcp/50866 - expect(addrs.length).to.be.at.least(1) - for (const addr of addrs) { - const opts = addr.toOptions() - expect(opts.family).to.equal(4) - expect(opts.transport).to.equal('tcp') - expect(opts.host).to.match(/[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/) - expect(opts.port).to.be.gt(0) - } + // /memory/address-1 + expect(addrs).to.have.lengthOf(1) + expect(addrs[0].toString()).to.equal(listenAddress) }) }) diff --git a/packages/libp2p/test/core/peer-id.spec.ts b/packages/libp2p/test/core/peer-id.spec.ts index 208b26bacd..160506ad0e 100644 --- a/packages/libp2p/test/core/peer-id.spec.ts +++ b/packages/libp2p/test/core/peer-id.spec.ts @@ -1,7 +1,5 @@ /* eslint-env mocha */ -import { plaintext } from '@libp2p/plaintext' -import { webSockets } from '@libp2p/websockets' import { expect } from 'aegir/chai' import { createLibp2p, type Libp2p } from '../../src/index.js' @@ -15,14 +13,7 @@ describe('peer-id', () => { }) it('should create a PeerId if none is passed', async () => { - libp2p = await createLibp2p({ - transports: [ - webSockets() - ], - connectionEncrypters: [ - plaintext() - ] - }) + libp2p = await createLibp2p() expect(libp2p.peerId).to.be.ok() }) diff --git a/packages/libp2p/test/core/status.node.ts b/packages/libp2p/test/core/status.spec.ts similarity index 69% rename from packages/libp2p/test/core/status.node.ts rename to packages/libp2p/test/core/status.spec.ts index 9ff81d9db2..7819cef09e 100644 --- a/packages/libp2p/test/core/status.node.ts +++ b/packages/libp2p/test/core/status.spec.ts @@ -1,32 +1,20 @@ /* eslint-env mocha */ -import { plaintext } from '@libp2p/plaintext' -import { tcp } from '@libp2p/tcp' +import { stop } from '@libp2p/interface' import { expect } from 'aegir/chai' import { createLibp2p } from '../../src/index.js' import type { Libp2p } from '@libp2p/interface' -const listenAddr = '/ip4/0.0.0.0/tcp/0' - describe('status', () => { let libp2p: Libp2p after(async () => { - await libp2p.stop() + await stop(libp2p) }) it('should have status', async () => { libp2p = await createLibp2p({ - start: false, - addresses: { - listen: [listenAddr] - }, - transports: [ - tcp() - ], - connectionEncrypters: [ - plaintext() - ] + start: false }) expect(libp2p).to.have.property('status', 'stopped') diff --git a/packages/libp2p/test/fixtures/base-options.browser.ts b/packages/libp2p/test/fixtures/base-options.browser.ts deleted file mode 100644 index 386917efd1..0000000000 --- a/packages/libp2p/test/fixtures/base-options.browser.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { circuitRelayTransport } from '@libp2p/circuit-relay-v2' -import { identify } from '@libp2p/identify' -import { mockConnectionGater } from '@libp2p/interface-compliance-tests/mocks' -import { mplex } from '@libp2p/mplex' -import { plaintext } from '@libp2p/plaintext' -import { webSockets } from '@libp2p/websockets' -import * as filters from '@libp2p/websockets/filters' -import mergeOptions from 'merge-options' -import type { Libp2pOptions } from '../../src/index.js' -import type { ServiceMap } from '@libp2p/interface' - -export function createBaseOptions > (overrides?: Libp2pOptions): Libp2pOptions { - const options: Libp2pOptions = { - transports: [ - webSockets({ - filter: filters.all - }), - circuitRelayTransport() - ], - streamMuxers: [ - mplex(), - mplex() - ], - connectionEncrypters: [ - plaintext() - ], - connectionGater: mockConnectionGater(), - services: { - identify: identify() - } - } - - return mergeOptions(options, overrides) -} diff --git a/packages/libp2p/test/fixtures/base-options.ts b/packages/libp2p/test/fixtures/base-options.ts deleted file mode 100644 index a0b90ce90b..0000000000 --- a/packages/libp2p/test/fixtures/base-options.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { yamux } from '@chainsafe/libp2p-yamux' -import { circuitRelayTransport } from '@libp2p/circuit-relay-v2' -import { identify } from '@libp2p/identify' -import { mplex } from '@libp2p/mplex' -import { plaintext } from '@libp2p/plaintext' -import { tcp } from '@libp2p/tcp' -import { webSockets } from '@libp2p/websockets' -import * as filters from '@libp2p/websockets/filters' -import mergeOptions from 'merge-options' -import type { Libp2pOptions } from '../../src' -import type { ServiceMap } from '@libp2p/interface' - -export function createBaseOptions > (...overrides: Array>): Libp2pOptions { - const options: Libp2pOptions = { - addresses: { - listen: [`${process.env.RELAY_MULTIADDR}/p2p-circuit`] - }, - transports: [ - tcp(), - webSockets({ - filter: filters.all - }), - circuitRelayTransport() - ], - streamMuxers: [ - yamux(), - mplex() - ], - connectionEncrypters: [ - plaintext() - ], - services: { - identify: identify() - } - } - - return mergeOptions(options, ...overrides) -} diff --git a/packages/libp2p/test/fixtures/create-peers.ts b/packages/libp2p/test/fixtures/create-peers.ts new file mode 100644 index 0000000000..aecf65dd11 --- /dev/null +++ b/packages/libp2p/test/fixtures/create-peers.ts @@ -0,0 +1,68 @@ +/* eslint-env mocha */ + +import { echo } from '@libp2p/echo' +import { memory } from '@libp2p/memory' +import { mplex } from '@libp2p/mplex' +import { plaintext } from '@libp2p/plaintext' +import { stubInterface } from 'sinon-ts' +import { createLibp2p } from '../../src/index.js' +import type { Components } from '../../src/components.js' +import type { Libp2pOptions } from '../../src/index.js' +import type { Echo } from '@libp2p/echo' +import type { Libp2p } from '@libp2p/interface' + +async function createNode (config: Partial> = {}): Promise<{ node: Libp2p<{ echo: Echo }>, components: Components }> { + let components: Components = stubInterface() + + const node = await createLibp2p({ + transports: [ + memory() + ], + connectionEncrypters: [ + plaintext() + ], + streamMuxers: [ + mplex() + ], + ...config, + services: { + echo: echo(), + components: (c) => { + components = c + } + } + }) + + return { + node, + components + } +} + +interface DialerAndListener { + dialer: Libp2p<{ echo: Echo }> + dialerComponents: Components + + listener: Libp2p<{ echo: Echo }> + listenerComponents: Components +} + +export async function createPeers (dialerConfig: Partial> = {}, listenerConfig: Partial> = {}): Promise { + const { node: dialer, components: dialerComponents } = await createNode(dialerConfig) + const { node: listener, components: listenerComponents } = await createNode({ + ...listenerConfig, + addresses: { + listen: [ + '/memory/address-1' + ] + } + }) + + return { + dialer, + dialerComponents, + + listener, + listenerComponents + } +} diff --git a/packages/libp2p/test/fixtures/creators/peer.ts b/packages/libp2p/test/fixtures/creators/peer.ts deleted file mode 100644 index 1c006ed483..0000000000 --- a/packages/libp2p/test/fixtures/creators/peer.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { multiaddr } from '@multiformats/multiaddr' -import { createLibp2p } from '../../../src/index.js' -import { createBaseOptions } from '../base-options.browser.js' -import type { AddressManagerInit } from '../../../src/address-manager/index.js' -import type { Libp2pOptions } from '../../../src/index.js' -import type { Libp2p, ServiceMap } from '@libp2p/interface' - -const listenAddr = multiaddr('/ip4/127.0.0.1/tcp/0') - -export interface CreatePeerOptions { - /** - * number of peers - * - * @default 1 - */ - number?: number - - /** - * nodes should start - * - * @default true - */ - started?: boolean - - config?: Libp2pOptions -} - -/** - * Create libp2p nodes. - */ -export async function createNode (options: CreatePeerOptions = {}): Promise> { - const started = options.started ?? true - const config = options.config ?? {} - const addresses: AddressManagerInit = started - ? { - listen: [listenAddr.toString()], - announce: [], - noAnnounce: [], - announceFilter: (addrs) => addrs - } - : { - listen: [], - announce: [], - noAnnounce: [], - announceFilter: (addrs) => addrs - } - const peer = await createLibp2p(createBaseOptions({ - addresses, - start: started, - ...config - })) - - if (started) { - await peer.start() - } - - return peer -} diff --git a/packages/libp2p/test/fixtures/echo-service.ts b/packages/libp2p/test/fixtures/echo-service.ts deleted file mode 100644 index b9dd9d05e0..0000000000 --- a/packages/libp2p/test/fixtures/echo-service.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { pipe } from 'it-pipe' -import type { Startable } from '@libp2p/interface' -import type { Registrar } from '@libp2p/interface-internal' - -export const ECHO_PROTOCOL = '/echo/1.0.0' - -export interface EchoInit { - protocol?: string -} - -export interface EchoComponents { - registrar: Registrar -} - -class EchoService implements Startable { - private readonly protocol: string - private readonly registrar: Registrar - - constructor (components: EchoComponents, init: EchoInit = {}) { - this.protocol = init.protocol ?? ECHO_PROTOCOL - this.registrar = components.registrar - } - - async start (): Promise { - await this.registrar.handle(this.protocol, ({ stream }) => { - void pipe(stream, stream) - // sometimes connections are closed before multistream-select finishes - // which causes an error - .catch() - }) - } - - async stop (): Promise { - await this.registrar.unhandle(this.protocol) - } -} - -export function echo (init: EchoInit = {}): (components: EchoComponents) => unknown { - return (components) => { - return new EchoService(components, init) - } -} diff --git a/packages/libp2p/test/fixtures/slow-muxer.ts b/packages/libp2p/test/fixtures/slow-muxer.ts new file mode 100644 index 0000000000..03279ca32e --- /dev/null +++ b/packages/libp2p/test/fixtures/slow-muxer.ts @@ -0,0 +1,29 @@ +/* eslint-env mocha */ + +import { mplex } from '@libp2p/mplex' +import delay from 'delay' +import map from 'it-map' +import type { Components } from '../../src/components.js' +import type { StreamMuxerFactory } from '@libp2p/interface' + +/** + * Creates a muxer with a delay between each sent packet + */ +export function slowMuxer (packetDelay: number): ((components: Components) => StreamMuxerFactory) { + return (components) => { + const muxerFactory = mplex()(components) + const originalCreateStreamMuxer = muxerFactory.createStreamMuxer.bind(muxerFactory) + + muxerFactory.createStreamMuxer = (init) => { + const muxer = originalCreateStreamMuxer(init) + muxer.source = map(muxer.source, async (buf) => { + await delay(packetDelay) + return buf + }) + + return muxer + } + + return muxerFactory + } +} diff --git a/packages/libp2p/test/registrar/errors.spec.ts b/packages/libp2p/test/registrar/errors.spec.ts index 3ad554331d..04ad6e0329 100644 --- a/packages/libp2p/test/registrar/errors.spec.ts +++ b/packages/libp2p/test/registrar/errors.spec.ts @@ -1,8 +1,7 @@ /* eslint-env mocha */ import { generateKeyPair } from '@libp2p/crypto/keys' -import { TypedEventEmitter, type ConnectionGater, type PeerId } from '@libp2p/interface' -import { mockUpgrader } from '@libp2p/interface-compliance-tests/mocks' +import { TypedEventEmitter } from '@libp2p/interface' import { peerIdFromPrivateKey } from '@libp2p/peer-id' import { persistentPeerStore } from '@libp2p/peer-store' import { expect } from 'aegir/chai' @@ -12,6 +11,7 @@ import { defaultComponents } from '../../src/components.js' import { DefaultConnectionManager } from '../../src/connection-manager/index.js' import { DefaultRegistrar } from '../../src/registrar.js' import type { Components } from '../../src/components.js' +import type { Upgrader, ConnectionGater, PeerId } from '@libp2p/interface' import type { Registrar, TransportManager } from '@libp2p/interface-internal' describe('registrar errors', () => { @@ -26,7 +26,7 @@ describe('registrar errors', () => { peerId, events, datastore: new MemoryDatastore(), - upgrader: mockUpgrader({ events }), + upgrader: stubInterface(), transportManager: stubInterface(), connectionGater: stubInterface() }) diff --git a/packages/libp2p/test/registrar/protocols.spec.ts b/packages/libp2p/test/registrar/protocols.spec.ts index 21003d25f6..17fb577cad 100644 --- a/packages/libp2p/test/registrar/protocols.spec.ts +++ b/packages/libp2p/test/registrar/protocols.spec.ts @@ -1,9 +1,5 @@ /* eslint-env mocha */ -import { yamux } from '@chainsafe/libp2p-yamux' -import { mplex } from '@libp2p/mplex' -import { plaintext } from '@libp2p/plaintext' -import { webSockets } from '@libp2p/websockets' import { expect } from 'aegir/chai' import pDefer from 'p-defer' import { createLibp2p } from '../../src/index.js' @@ -21,16 +17,6 @@ describe('registrar protocols', () => { const deferred = pDefer() libp2p = await createLibp2p({ - transports: [ - webSockets() - ], - streamMuxers: [ - yamux(), - mplex() - ], - connectionEncrypters: [ - plaintext() - ], services: { test: (components: any) => { deferred.resolve(components) diff --git a/packages/libp2p/test/registrar/registrar.spec.ts b/packages/libp2p/test/registrar/registrar.spec.ts index ef628d3d85..c274f1856b 100644 --- a/packages/libp2p/test/registrar/registrar.spec.ts +++ b/packages/libp2p/test/registrar/registrar.spec.ts @@ -2,7 +2,6 @@ import { generateKeyPair } from '@libp2p/crypto/keys' import { TypedEventEmitter } from '@libp2p/interface' -import { matchPeerId } from '@libp2p/interface-compliance-tests/matchers' import { mockDuplex, mockMultiaddrConnection, mockConnection } from '@libp2p/interface-compliance-tests/mocks' import { defaultLogger } from '@libp2p/logger' import { peerFilter } from '@libp2p/peer-collections' @@ -108,7 +107,7 @@ describe('registrar topologies', () => { await registrar.register(protocol, topology) // Peer data is in the peer store - peerStore.get.withArgs(matchPeerId(remotePeerId)).resolves({ + peerStore.get.withArgs(remotePeerId).resolves({ id: remotePeerId, addresses: [], protocols: [protocol], @@ -143,7 +142,7 @@ describe('registrar topologies', () => { const conn = mockConnection(mockMultiaddrConnection(mockDuplex(), remotePeerId)) // return connection from connection manager - connectionManager.getConnections.withArgs(matchPeerId(remotePeerId)).returns([conn]) + connectionManager.getConnections.withArgs(remotePeerId).returns([conn]) const topology: Topology = { onConnect: () => { @@ -167,7 +166,7 @@ describe('registrar topologies', () => { }) // Can get details after identify - peerStore.get.withArgs(matchPeerId(conn.remotePeer)).resolves({ + peerStore.get.withArgs(conn.remotePeer).resolves({ id: conn.remotePeer, addresses: [], protocols: [protocol], @@ -176,7 +175,7 @@ describe('registrar topologies', () => { }) // we have a connection to this peer - connectionManager.getConnections.withArgs(matchPeerId(conn.remotePeer)).returns([conn]) + connectionManager.getConnections.withArgs(conn.remotePeer).returns([conn]) // identify completes events.safeDispatchEvent('peer:update', { @@ -227,7 +226,7 @@ describe('registrar topologies', () => { } // return connection from connection manager - connectionManager.getConnections.withArgs(matchPeerId(remotePeerId)).returns([conn]) + connectionManager.getConnections.withArgs(remotePeerId).returns([conn]) const topology: Topology = { onConnect: () => { @@ -274,7 +273,7 @@ describe('registrar topologies', () => { } // return connection from connection manager - connectionManager.getConnections.withArgs(matchPeerId(remotePeerId)).returns([conn]) + connectionManager.getConnections.withArgs(remotePeerId).returns([conn]) const topology: Topology = { notifyOnLimitedConnection: true, @@ -329,7 +328,7 @@ describe('registrar topologies', () => { } // return connection from connection manager - connectionManager.getConnections.withArgs(matchPeerId(remotePeerId)).returns([ + connectionManager.getConnections.withArgs(remotePeerId).returns([ limitedConnection, nonLimitedConnection ]) diff --git a/packages/libp2p/test/transports/transport-manager.node.ts b/packages/libp2p/test/transports/transport-manager.node.ts deleted file mode 100644 index 7b93d1fd5d..0000000000 --- a/packages/libp2p/test/transports/transport-manager.node.ts +++ /dev/null @@ -1,143 +0,0 @@ -/* eslint-env mocha */ - -import { generateKeyPair } from '@libp2p/crypto/keys' -import { TypedEventEmitter, start, stop, FaultTolerance } from '@libp2p/interface' -import { mockUpgrader } from '@libp2p/interface-compliance-tests/mocks' -import { defaultLogger } from '@libp2p/logger' -import { peerIdFromPrivateKey } from '@libp2p/peer-id' -import { persistentPeerStore } from '@libp2p/peer-store' -import { tcp } from '@libp2p/tcp' -import { multiaddr } from '@multiformats/multiaddr' -import { expect } from 'aegir/chai' -import { MemoryDatastore } from 'datastore-core/memory' -import { pEvent } from 'p-event' -import pWaitFor from 'p-wait-for' -import sinon from 'sinon' -import { DefaultAddressManager } from '../../src/address-manager/index.js' -import { defaultComponents, type Components } from '../../src/components.js' -import { DefaultTransportManager } from '../../src/transport-manager.js' -import type { PeerId } from '@libp2p/interface' - -const addrs = [ - multiaddr('/ip4/127.0.0.1/tcp/0'), - multiaddr('/ip4/127.0.0.1/tcp/0') -] - -describe('Transport Manager (TCP)', () => { - let tm: DefaultTransportManager - let localPeer: PeerId - let components: Components - - before(async () => { - localPeer = peerIdFromPrivateKey(await generateKeyPair('Ed25519')) - }) - - beforeEach(async () => { - const events = new TypedEventEmitter() - components = defaultComponents({ - peerId: localPeer, - events, - datastore: new MemoryDatastore(), - upgrader: mockUpgrader({ events }) - }) - components.addressManager = new DefaultAddressManager(components, { listen: addrs.map(addr => addr.toString()) }) - components.peerStore = persistentPeerStore(components) - - tm = new DefaultTransportManager(components, { - faultTolerance: FaultTolerance.NO_FATAL - }) - - components.transportManager = tm - - await start(tm) - }) - - afterEach(async () => { - await tm.removeAll() - expect(tm.getTransports()).to.be.empty() - await stop(tm) - }) - - it('should be able to add and remove a transport', async () => { - expect(tm.getTransports()).to.have.lengthOf(0) - tm.add(tcp()({ - logger: defaultLogger() - })) - expect(tm.getTransports()).to.have.lengthOf(1) - await tm.remove('@libp2p/tcp') - expect(tm.getTransports()).to.have.lengthOf(0) - }) - - it('should be able to listen', async () => { - const transport = tcp()({ - logger: defaultLogger() - }) - - expect(tm.getTransports()).to.be.empty() - - tm.add(transport) - - expect(tm.getTransports()).to.have.lengthOf(1) - - const spyListener = sinon.spy(transport, 'createListener') - await tm.listen(addrs) - - // Ephemeral ip addresses may result in multiple listeners - expect(tm.getAddrs().length).to.equal(addrs.length) - await tm.stop() - expect(spyListener.called).to.be.true() - }) - - it('should be able to dial', async () => { - tm.add(tcp()({ - logger: defaultLogger() - })) - await tm.listen(addrs) - const addr = tm.getAddrs().shift() - - if (addr == null) { - throw new Error('Could not find addr') - } - - const connection = await tm.dial(addr) - expect(connection).to.exist() - await connection.close() - }) - - it('should remove listeners when they stop listening', async () => { - const transport = tcp()({ - logger: defaultLogger() - }) - tm.add(transport) - - expect(tm.getListeners()).to.have.lengthOf(0) - - const spyListener = sinon.spy(transport, 'createListener') - - await tm.listen(addrs) - - expect(spyListener.callCount).to.equal(addrs.length) - - // wait for listeners to start listening - await pWaitFor(async () => { - return tm.getListeners().length === addrs.length - }) - - // wait for listeners to stop listening - const closePromise = Promise.all( - spyListener.getCalls().map(async call => { - return pEvent(call.returnValue, 'close') - }) - ) - - await Promise.all( - tm.getListeners().map(async l => { await l.close() }) - ) - - await closePromise - - expect(tm.getListeners()).to.have.lengthOf(0) - - await tm.stop() - }) -}) diff --git a/packages/libp2p/test/transports/transport-manager.spec.ts b/packages/libp2p/test/transports/transport-manager.spec.ts index bdb7c2f45b..277502bb35 100644 --- a/packages/libp2p/test/transports/transport-manager.spec.ts +++ b/packages/libp2p/test/transports/transport-manager.spec.ts @@ -2,24 +2,31 @@ import { generateKeyPair } from '@libp2p/crypto/keys' import { TypedEventEmitter, start, stop, FaultTolerance } from '@libp2p/interface' -import { mockUpgrader } from '@libp2p/interface-compliance-tests/mocks' import { defaultLogger } from '@libp2p/logger' +import { memory } from '@libp2p/memory' import { peerIdFromPrivateKey } from '@libp2p/peer-id' +import { persistentPeerStore } from '@libp2p/peer-store' import { plaintext } from '@libp2p/plaintext' -import { webSockets } from '@libp2p/websockets' -import * as filters from '@libp2p/websockets/filters' import { multiaddr } from '@multiformats/multiaddr' import { expect } from 'aegir/chai' +import { MemoryDatastore } from 'datastore-core' +import { pEvent } from 'p-event' +import pWaitFor from 'p-wait-for' import sinon from 'sinon' +import { stubInterface } from 'sinon-ts' import { DefaultAddressManager } from '../../src/address-manager/index.js' import { createLibp2p } from '../../src/index.js' import { DefaultTransportManager } from '../../src/transport-manager.js' import type { Components } from '../../src/components.js' -import type { Libp2p } from '@libp2p/interface' +import type { Connection, Libp2p, Upgrader } from '@libp2p/interface' const listenAddr = multiaddr('/ip4/127.0.0.1/tcp/0') +const addrs = [ + multiaddr('/memory/address-1'), + multiaddr('/memory/address-2') +] -describe('Transport Manager (WebSockets)', () => { +describe('Transport Manager', () => { let tm: DefaultTransportManager let components: Components @@ -28,12 +35,17 @@ describe('Transport Manager (WebSockets)', () => { components = { peerId: peerIdFromPrivateKey(await generateKeyPair('Ed25519')), events, - upgrader: mockUpgrader({ events }), - logger: defaultLogger() + upgrader: stubInterface({ + upgradeInbound: async (ma) => stubInterface(ma), + upgradeOutbound: async (ma) => stubInterface(ma) + }), + logger: defaultLogger(), + datastore: new MemoryDatastore() } as any components.addressManager = new DefaultAddressManager(components, { listen: [listenAddr.toString()] }) + components.peerStore = persistentPeerStore(components) - tm = new DefaultTransportManager(components, { + components.transportManager = tm = new DefaultTransportManager(components, { faultTolerance: FaultTolerance.NO_FATAL }) await start(tm) @@ -46,48 +58,28 @@ describe('Transport Manager (WebSockets)', () => { }) it('should be able to add and remove a transport', async () => { - const transport = webSockets({ - filter: filters.all - }) + const transport = memory() expect(tm.getTransports()).to.have.lengthOf(0) - tm.add(transport({ - logger: defaultLogger() - })) + tm.add(transport(components)) expect(tm.getTransports()).to.have.lengthOf(1) - await tm.remove('@libp2p/websockets') + await tm.remove('@libp2p/memory') expect(tm.getTransports()).to.have.lengthOf(0) }) it('should not be able to add a transport twice', async () => { - tm.add(webSockets()({ - logger: defaultLogger() - })) + tm.add(memory()(components)) expect(() => { - tm.add(webSockets()({ - logger: defaultLogger() - })) + tm.add(memory()(components)) }) .to.throw() .and.to.have.property('name', 'InvalidParametersError') }) - it('should be able to dial', async () => { - tm.add(webSockets({ filter: filters.all })({ - logger: defaultLogger() - })) - const addr = multiaddr(process.env.RELAY_MULTIADDR) - const connection = await tm.dial(addr) - expect(connection).to.exist() - await connection.close() - }) - it('should fail to dial an unsupported address', async () => { - tm.add(webSockets({ filter: filters.all })({ - logger: defaultLogger() - })) + tm.add(memory()(components)) const addr = multiaddr('/ip4/127.0.0.1/tcp/0') await expect(tm.dial(addr)) .to.eventually.be.rejected() @@ -96,9 +88,7 @@ describe('Transport Manager (WebSockets)', () => { it('should fail to listen with no valid address', async () => { tm = new DefaultTransportManager(components) - tm.add(webSockets({ filter: filters.all })({ - logger: defaultLogger() - })) + tm.add(memory()(components)) await expect(start(tm)) .to.eventually.be.rejected() @@ -106,17 +96,88 @@ describe('Transport Manager (WebSockets)', () => { await stop(tm) }) + + it('should be able to add and remove a transport', async () => { + expect(tm.getTransports()).to.have.lengthOf(0) + tm.add(memory()(components)) + expect(tm.getTransports()).to.have.lengthOf(1) + await tm.remove('@libp2p/memory') + expect(tm.getTransports()).to.have.lengthOf(0) + }) + + it('should be able to listen', async () => { + const transport = memory()(components) + + expect(tm.getTransports()).to.be.empty() + + tm.add(transport) + + expect(tm.getTransports()).to.have.lengthOf(1) + + const spyListener = sinon.spy(transport, 'createListener') + await tm.listen(addrs) + + // Ephemeral ip addresses may result in multiple listeners + expect(tm.getAddrs().length).to.equal(addrs.length) + await tm.stop() + expect(spyListener.called).to.be.true() + }) + + it('should be able to dial', async () => { + tm.add(memory()(components)) + await tm.listen(addrs) + const addr = tm.getAddrs().shift() + + if (addr == null) { + throw new Error('Could not find addr') + } + + const connection = await tm.dial(addr) + expect(connection).to.exist() + await connection.close() + }) + + it('should remove listeners when they stop listening', async () => { + const transport = memory()(components) + tm.add(transport) + + expect(tm.getListeners()).to.have.lengthOf(0) + + const spyListener = sinon.spy(transport, 'createListener') + + await tm.listen(addrs) + + expect(spyListener.callCount).to.equal(addrs.length) + + // wait for listeners to start listening + await pWaitFor(async () => { + return tm.getListeners().length === addrs.length + }) + + // wait for listeners to stop listening + const closePromise = Promise.all( + spyListener.getCalls().map(async call => { + return pEvent(call.returnValue, 'close') + }) + ) + + await Promise.all( + tm.getListeners().map(async l => { await l.close() }) + ) + + await closePromise + + expect(tm.getListeners()).to.have.lengthOf(0) + + await tm.stop() + }) }) describe('libp2p.transportManager (dial only)', () => { let libp2p: Libp2p afterEach(async () => { - sinon.restore() - - if (libp2p != null) { - await libp2p.stop() - } + await stop(libp2p) }) it('fails to start if multiaddr fails to listen', async () => { @@ -124,7 +185,7 @@ describe('libp2p.transportManager (dial only)', () => { addresses: { listen: ['/ip4/127.0.0.1/tcp/0'] }, - transports: [webSockets()], + transports: [memory()], connectionEncrypters: [plaintext()], start: false }) @@ -142,7 +203,7 @@ describe('libp2p.transportManager (dial only)', () => { faultTolerance: FaultTolerance.NO_FATAL }, transports: [ - webSockets() + memory() ], connectionEncrypters: [ plaintext() @@ -162,7 +223,7 @@ describe('libp2p.transportManager (dial only)', () => { faultTolerance: FaultTolerance.NO_FATAL }, transports: [ - webSockets() + memory() ], connectionEncrypters: [ plaintext() diff --git a/packages/libp2p/test/upgrading/upgrader.spec.ts b/packages/libp2p/test/upgrading/upgrader.spec.ts index 8f4c8d88bb..0b8110406c 100644 --- a/packages/libp2p/test/upgrading/upgrader.spec.ts +++ b/packages/libp2p/test/upgrading/upgrader.spec.ts @@ -1,447 +1,221 @@ /* eslint-env mocha */ -import { yamux } from '@chainsafe/libp2p-yamux' -import { circuitRelayTransport } from '@libp2p/circuit-relay-v2' -import { generateKeyPair } from '@libp2p/crypto/keys' -import { identify } from '@libp2p/identify' -import { TypedEventEmitter } from '@libp2p/interface' -import { mockConnectionGater, mockConnectionManager, mockMultiaddrConnPair, mockRegistrar, mockStream, mockMuxer } from '@libp2p/interface-compliance-tests/mocks' -import { mplex } from '@libp2p/mplex' -import { peerIdFromCID, peerIdFromPrivateKey } from '@libp2p/peer-id' -import { persistentPeerStore } from '@libp2p/peer-store' -import { plaintext } from '@libp2p/plaintext' -import { webSockets } from '@libp2p/websockets' -import * as filters from '@libp2p/websockets/filters' -import { multiaddr } from '@multiformats/multiaddr' +import { stop } from '@libp2p/interface' +import { memory } from '@libp2p/memory' import { expect } from 'aegir/chai' -import { MemoryDatastore } from 'datastore-core' import delay from 'delay' -import all from 'it-all' import drain from 'it-drain' -import { pipe } from 'it-pipe' import pDefer from 'p-defer' -import { pEvent } from 'p-event' -import sinon from 'sinon' -import { type StubbedInstance, stubInterface } from 'sinon-ts' -import { Uint8ArrayList } from 'uint8arraylist' -import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' -import { type Components, defaultComponents } from '../../src/components.js' -import { createLibp2p } from '../../src/index.js' -import { DEFAULT_MAX_OUTBOUND_STREAMS } from '../../src/registrar.js' -import { DefaultUpgrader } from '../../src/upgrader.js' -import type { Libp2p, Connection, ConnectionProtector, Stream, ConnectionEncrypter, SecuredConnection, PeerId, StreamMuxer, StreamMuxerFactory, StreamMuxerInit, Upgrader, PrivateKey, MultiaddrConnection } from '@libp2p/interface' -import type { ConnectionManager } from '@libp2p/interface-internal' - -const addrs = [ - multiaddr('/ip4/127.0.0.1/tcp/0'), - multiaddr('/ip4/127.0.0.1/tcp/0') -] - -describe('Upgrader', () => { - let localUpgrader: Upgrader - let localMuxerFactory: StreamMuxerFactory - let localYamuxerFactory: StreamMuxerFactory - let localConnectionEncrypter: ConnectionEncrypter - let localConnectionProtector: StubbedInstance - let remoteUpgrader: Upgrader - let remoteMuxerFactory: StreamMuxerFactory - let remoteYamuxerFactory: StreamMuxerFactory - let remoteConnectionEncrypter: ConnectionEncrypter - let remoteConnectionProtector: StubbedInstance - let localPeer: PeerId - let remotePeer: PeerId - let localComponents: Components - let remoteComponents: Components - - beforeEach(async () => { - const localKey = await generateKeyPair('Ed25519') - localPeer = peerIdFromPrivateKey(localKey) - - const remoteKey = await generateKeyPair('Ed25519') - remotePeer = peerIdFromPrivateKey(remoteKey) - - localConnectionProtector = stubInterface() - localConnectionProtector.protect.resolvesArg(0) - - localComponents = defaultComponents({ - peerId: localPeer, - privateKey: localKey, - connectionGater: mockConnectionGater(), - registrar: mockRegistrar(), - datastore: new MemoryDatastore(), - connectionProtector: localConnectionProtector, - events: new TypedEventEmitter() - }) - localComponents.peerStore = persistentPeerStore(localComponents) - localComponents.connectionManager = mockConnectionManager(localComponents) - localMuxerFactory = mplex()(localComponents) - localYamuxerFactory = yamux()(localComponents) - localConnectionEncrypter = plaintext()(localComponents) - localUpgrader = new DefaultUpgrader(localComponents, { - connectionEncrypters: [ - localConnectionEncrypter - ], - streamMuxers: [ - localMuxerFactory, - localYamuxerFactory - ], - inboundUpgradeTimeout: 1000 - }) - - remoteConnectionProtector = stubInterface() - remoteConnectionProtector.protect.resolvesArg(0) - - remoteComponents = defaultComponents({ - peerId: remotePeer, - privateKey: remoteKey, - connectionGater: mockConnectionGater(), - registrar: mockRegistrar(), - datastore: new MemoryDatastore(), - connectionProtector: remoteConnectionProtector, - events: new TypedEventEmitter() - }) - remoteComponents.peerStore = persistentPeerStore(remoteComponents) - remoteComponents.connectionManager = mockConnectionManager(remoteComponents) - remoteMuxerFactory = mplex()(remoteComponents) - remoteYamuxerFactory = yamux()(remoteComponents) - remoteConnectionEncrypter = plaintext()(remoteComponents) - remoteUpgrader = new DefaultUpgrader(remoteComponents, { - connectionEncrypters: [ - remoteConnectionEncrypter - ], - streamMuxers: [ - remoteMuxerFactory, - remoteYamuxerFactory - ], - inboundUpgradeTimeout: 1000 - }) - - await localComponents.registrar.handle('/echo/1.0.0', ({ stream }) => { - void pipe(stream, stream) - }, { - maxInboundStreams: 10, - maxOutboundStreams: 10 - }) - await remoteComponents.registrar.handle('/echo/1.0.0', ({ stream }) => { - void pipe(stream, stream) - }, { - maxInboundStreams: 10, - maxOutboundStreams: 10 - }) - }) +import Sinon from 'sinon' +import { stubInterface } from 'sinon-ts' +import { createPeers } from '../fixtures/create-peers.js' +import { slowMuxer } from '../fixtures/slow-muxer.js' +import type { Components } from '../../src/components.js' +import type { Echo } from '@libp2p/echo' +import type { Libp2p, ConnectionProtector, ConnectionEncrypter, SecuredConnection, StreamMuxerFactory } from '@libp2p/interface' + +describe('upgrader', () => { + let dialer: Libp2p<{ echo: Echo }> + let listener: Libp2p<{ echo: Echo }> + let dialerComponents: Components + let listenerComponents: Components - afterEach(() => { - sinon.restore() + afterEach(async () => { + await stop(dialer, listener) }) it('should upgrade with valid muxers and crypto', async () => { - const { inbound, outbound } = mockMultiaddrConnPair({ addrs, remotePeer }) - - const connections = await Promise.all([ - localUpgrader.upgradeOutbound(outbound), - remoteUpgrader.upgradeInbound(inbound) - ]) - - expect(connections).to.have.length(2) + ({ dialer, listener } = await createPeers()) - const stream = await connections[0].newStream('/echo/1.0.0') - expect(stream).to.have.property('protocol', '/echo/1.0.0') - - const hello = uint8ArrayFromString('hello there!') - const result = await pipe( - [hello], - stream, - function toBuffer (source) { - return (async function * () { - for await (const val of source) yield val.slice() - })() - }, - async (source) => all(source) - ) - - expect(result).to.eql([hello]) + const input = Uint8Array.from([0, 1, 2, 3, 4]) + const output = await dialer.services.echo.echo(listener.getMultiaddrs(), input) + expect(output).to.equalBytes(input) }) it('should upgrade with only crypto', async () => { - const { inbound, outbound } = mockMultiaddrConnPair({ addrs, remotePeer }) - - // No available muxers - localUpgrader = new DefaultUpgrader(localComponents, { - connectionEncrypters: [ - plaintext()(localComponents) - ], - streamMuxers: [], - inboundUpgradeTimeout: 1000 - }) - remoteUpgrader = new DefaultUpgrader(remoteComponents, { - connectionEncrypters: [ - plaintext()(remoteComponents) - ], - streamMuxers: [], - inboundUpgradeTimeout: 1000 - }) - - const connections = await Promise.all([ - localUpgrader.upgradeOutbound(outbound), - remoteUpgrader.upgradeInbound(inbound) - ]) - - expect(connections).to.have.length(2) + ({ dialer, listener } = await createPeers({ streamMuxers: [] }, { streamMuxers: [] })) - await expect(connections[0].newStream('/echo/1.0.0')).to.be.rejected() + const connection = await dialer.dial(listener.getMultiaddrs()) - // Verify the MultiaddrConnection close method is called - const inboundCloseSpy = sinon.spy(inbound, 'close') - const outboundCloseSpy = sinon.spy(outbound, 'close') - await Promise.all(connections.map(async conn => { await conn.close() })) - expect(inboundCloseSpy.callCount).to.equal(1) - expect(outboundCloseSpy.callCount).to.equal(1) + await expect(connection.newStream('/echo/1.0.0')).to.eventually.be.rejected + .with.property('name', 'MuxerUnavailableError') }) it('should use a private connection protector when provided', async () => { - const { inbound, outbound } = mockMultiaddrConnPair({ addrs, remotePeer }) - - const protector: ConnectionProtector = { - async protect (connection) { - return connection - } - } - - const protectorProtectSpy = sinon.spy(protector, 'protect') + const protector = stubInterface() + protector.protect.callsFake(async (conn) => conn) + const connectionProtector = (): ConnectionProtector => protector - localComponents.connectionProtector = protector - remoteComponents.connectionProtector = protector + ;({ dialer, listener } = await createPeers({ connectionProtector }, { connectionProtector })) - const connections = await Promise.all([ - localUpgrader.upgradeOutbound(outbound), - remoteUpgrader.upgradeInbound(inbound) - ]) + const input = Uint8Array.from([0, 1, 2, 3, 4]) + const output = await dialer.services.echo.echo(listener.getMultiaddrs(), input) + expect(output).to.equalBytes(input) - expect(connections).to.have.length(2) - - const stream = await connections[0].newStream('/echo/1.0.0') - expect(stream).to.have.property('protocol', '/echo/1.0.0') - - const hello = uint8ArrayFromString('hello there!') - const result = await pipe( - [hello], - stream, - function toBuffer (source) { - return (async function * () { - for await (const val of source) yield val.slice() - })() - }, - async (source) => all(source) - ) - - expect(result).to.eql([hello]) - expect(protectorProtectSpy.callCount).to.eql(2) + expect(protector.protect.callCount).to.equal(2) }) it('should fail if crypto fails', async () => { - const { inbound, outbound } = mockMultiaddrConnPair({ addrs, remotePeer }) - class BoomCrypto implements ConnectionEncrypter { - static protocol = '/insecure' - public protocol = '/insecure' + static protocol = '/unstable' + public protocol = '/unstable' async secureInbound (): Promise { throw new Error('Boom') } async secureOutbound (): Promise { throw new Error('Boom') } } - localUpgrader = new DefaultUpgrader(localComponents, { + ({ dialer, dialerComponents, listener, listenerComponents } = await createPeers({ connectionEncrypters: [ - new BoomCrypto() - ], - streamMuxers: [], - inboundUpgradeTimeout: 1000 - }) - remoteUpgrader = new DefaultUpgrader(remoteComponents, { + () => new BoomCrypto() + ] + }, { connectionEncrypters: [ - new BoomCrypto() - ], - streamMuxers: [], - inboundUpgradeTimeout: 1000 - }) + () => new BoomCrypto() + ] + })) - // Wait for the results of each side of the connection - const results = await Promise.allSettled([ - localUpgrader.upgradeOutbound(outbound), - remoteUpgrader.upgradeInbound(inbound) - ]) + const dialerUpgraderUpgradeOutboundSpy = Sinon.spy(dialerComponents.upgrader, 'upgradeOutbound') + const listenerUpgraderUpgradeInboundSpy = Sinon.spy(listenerComponents.upgrader, 'upgradeInbound') + + await expect(dialer.dial(listener.getMultiaddrs())).to.eventually.be.rejected + .with.property('name', 'EncryptionFailedError') // Ensure both sides fail - expect(results).to.have.length(2) - results.forEach(result => { - expect(result).to.have.property('status', 'rejected') - expect(result).to.have.nested.property('reason.name', 'EncryptionFailedError') - }) + await expect(dialerUpgraderUpgradeOutboundSpy.getCall(0).returnValue).to.eventually.be.rejected + .with.property('name', 'EncryptionFailedError') + await expect(listenerUpgraderUpgradeInboundSpy.getCall(0).returnValue).to.eventually.be.rejected + .with.property('name', 'EncryptionFailedError') }) it('should clear timeout if upgrade is successful', async () => { - const { inbound, outbound } = mockMultiaddrConnPair({ addrs, remotePeer }) - - localUpgrader = new DefaultUpgrader(localComponents, { - connectionEncrypters: [ - plaintext()(localComponents) - ], - streamMuxers: [ - yamux()(localComponents) - ], - inboundUpgradeTimeout: 1000 - }) - remoteUpgrader = new DefaultUpgrader(remoteComponents, { - connectionEncrypters: [ - plaintext()(remoteComponents) - ], - streamMuxers: [ - yamux()(remoteComponents) - ], - inboundUpgradeTimeout: 1000 - }) - - const connections = await Promise.all([ - localUpgrader.upgradeOutbound(outbound), - remoteUpgrader.upgradeInbound(inbound) - ]) - - await delay(2000) - - expect(connections).to.have.length(2) - - connections.forEach(conn => { - conn.close().catch(() => { - throw new Error('Failed to close connection') - }) - }) - }) - - it('should fail if muxers do not match', async () => { - const { inbound, outbound } = mockMultiaddrConnPair({ addrs, remotePeer }) - - class OtherMuxer implements StreamMuxer { - protocol = '/muxer-local' - streams = [] - newStream (name?: string): Stream { - throw new Error('Not implemented') + ({ dialer, dialerComponents, listener, listenerComponents } = await createPeers({ + connectionManager: { + inboundUpgradeTimeout: 100 } + }, { + connectionManager: { + inboundUpgradeTimeout: 100 + } + })) - source = (async function * () { - yield * [] - })() + await dialer.dial(listener.getMultiaddrs()) - async sink (): Promise {} - async close (): Promise {} - abort (): void {} - } + await delay(1000) - class OtherMuxerFactory implements StreamMuxerFactory { - protocol = '/muxer-local' + // connections should still be open after timeout + expect(dialer.getConnections(listener.peerId)).to.have.lengthOf(1) + expect(listener.getConnections(dialer.peerId)).to.have.lengthOf(1) + }) - createStreamMuxer (init?: StreamMuxerInit): StreamMuxer { - return new OtherMuxer() + it('should not abort if upgrade is successful', async () => { + ({ dialer, dialerComponents, listener, listenerComponents } = await createPeers({ + connectionManager: { + inboundUpgradeTimeout: 10000 } - } - - class OtherOtherMuxerFactory implements StreamMuxerFactory { - protocol = '/muxer-local-other' - - createStreamMuxer (init?: StreamMuxerInit): StreamMuxer { - return new OtherMuxer() + }, { + connectionManager: { + inboundUpgradeTimeout: 10000 } - } + })) - localUpgrader = new DefaultUpgrader(localComponents, { - connectionEncrypters: [ - plaintext()(localComponents) - ], - streamMuxers: [ - new OtherMuxerFactory(), - new OtherOtherMuxerFactory() - ], - inboundUpgradeTimeout: 1000 - }) - remoteUpgrader = new DefaultUpgrader(remoteComponents, { - connectionEncrypters: [ - plaintext()(remoteComponents) - ], - streamMuxers: [ - yamux()(remoteComponents), - mplex()(remoteComponents) - ], - inboundUpgradeTimeout: 1000 + await dialer.dial(listener.getMultiaddrs(), { + signal: AbortSignal.timeout(100) }) - // Wait for the results of each side of the connection - const results = await Promise.allSettled([ - localUpgrader.upgradeOutbound(inbound), - remoteUpgrader.upgradeInbound(outbound) - ]) + await delay(1000) - // Ensure both sides fail - expect(results).to.have.length(2) - results.forEach(result => { - expect(result).to.have.property('status', 'rejected') - expect(result).to.have.nested.property('reason.name', 'MuxerUnavailableError') - }) + // connections should still be open after timeout + expect(dialer.getConnections(listener.peerId)).to.have.lengthOf(1) + expect(listener.getConnections(dialer.peerId)).to.have.lengthOf(1) }) - it('should map getStreams and close methods', async () => { - const { inbound, outbound } = mockMultiaddrConnPair({ addrs, remotePeer }) - - const connections = await Promise.all([ - localUpgrader.upgradeOutbound(outbound), - remoteUpgrader.upgradeInbound(inbound) - ]) + it('should fail if muxers do not match', async () => { + ({ dialer, dialerComponents, listener, listenerComponents } = await createPeers({ + streamMuxers: [ + () => stubInterface({ + protocol: '/acme-muxer' + }) + ] + }, { + streamMuxers: [ + () => stubInterface({ + protocol: '/example-muxer' + }) + ] + })) - expect(connections).to.have.length(2) + const dialerUpgraderUpgradeOutboundSpy = Sinon.spy(dialerComponents.upgrader, 'upgradeOutbound') + const listenerUpgraderUpgradeInboundSpy = Sinon.spy(listenerComponents.upgrader, 'upgradeInbound') - // Create a few streams, at least 1 in each direction - // use multiple protocols to trigger regular multistream select - await connections[0].newStream(['/echo/1.0.0', '/echo/1.0.1']) - await connections[1].newStream(['/echo/1.0.0', '/echo/1.0.1']) - await connections[0].newStream(['/echo/1.0.0', '/echo/1.0.1']) - connections.forEach(conn => { - expect(conn.streams).to.have.length(3) - }) + await expect(dialer.dial(listener.getMultiaddrs())).to.eventually.be.rejected + .with.property('name', 'MuxerUnavailableError') - // Verify the MultiaddrConnection close method is called - const inboundCloseSpy = sinon.spy(inbound, 'close') - const outboundCloseSpy = sinon.spy(outbound, 'close') - await Promise.all(connections.map(async conn => { await conn.close() })) - expect(inboundCloseSpy.callCount).to.equal(1) - expect(outboundCloseSpy.callCount).to.equal(1) + // Ensure both sides fail + await expect(dialerUpgraderUpgradeOutboundSpy.getCall(0).returnValue).to.eventually.be.rejected + .with.property('name', 'MuxerUnavailableError') + await expect(listenerUpgraderUpgradeInboundSpy.getCall(0).returnValue).to.eventually.be.rejected + .with.property('name', 'MuxerUnavailableError') }) - it('should call connection handlers', async () => { - const { inbound, outbound } = mockMultiaddrConnPair({ addrs, remotePeer }) + it('should emit connection events', async () => { + ({ dialer, dialerComponents, listener, listenerComponents } = await createPeers()) + const localConnectionEventReceived = pDefer() const localConnectionEndEventReceived = pDefer() + const localPeerConnectEventReceived = pDefer() + const localPeerDisconnectEventReceived = pDefer() const remoteConnectionEventReceived = pDefer() const remoteConnectionEndEventReceived = pDefer() + const remotePeerConnectEventReceived = pDefer() + const remotePeerDisconnectEventReceived = pDefer() - localComponents.events.addEventListener('connection:open', () => { + dialerComponents.events.addEventListener('connection:open', (event) => { + expect(event.detail.remotePeer.equals(listener.peerId)).to.be.true() localConnectionEventReceived.resolve() }) - localComponents.events.addEventListener('connection:close', () => { + dialerComponents.events.addEventListener('connection:close', (event) => { + expect(event.detail.remotePeer.equals(listener.peerId)).to.be.true() localConnectionEndEventReceived.resolve() }) - remoteComponents.events.addEventListener('connection:open', () => { + dialerComponents.events.addEventListener('peer:connect', (event) => { + expect(event.detail.equals(listener.peerId)).to.be.true() + localPeerConnectEventReceived.resolve() + }) + dialerComponents.events.addEventListener('peer:disconnect', (event) => { + expect(event.detail.equals(listener.peerId)).to.be.true() + localPeerDisconnectEventReceived.resolve() + }) + + listenerComponents.events.addEventListener('connection:open', (event) => { + expect(event.detail.remotePeer.equals(dialer.peerId)).to.be.true() remoteConnectionEventReceived.resolve() }) - remoteComponents.events.addEventListener('connection:close', () => { + listenerComponents.events.addEventListener('connection:close', (event) => { + expect(event.detail.remotePeer.equals(dialer.peerId)).to.be.true() remoteConnectionEndEventReceived.resolve() }) + listenerComponents.events.addEventListener('peer:connect', (event) => { + expect(event.detail.equals(dialer.peerId)).to.be.true() + remotePeerConnectEventReceived.resolve() + }) + listenerComponents.events.addEventListener('peer:disconnect', (event) => { + expect(event.detail.equals(dialer.peerId)).to.be.true() + remotePeerDisconnectEventReceived.resolve() + }) + + await dialer.dial(listener.getMultiaddrs()) // Verify onConnection is called with the connection const connections = await Promise.all([ - localUpgrader.upgradeOutbound(outbound), - remoteUpgrader.upgradeInbound(inbound) + ...dialer.getConnections(listener.peerId), + ...listener.getConnections(dialer.peerId) ]) - expect(connections).to.have.length(2) + expect(connections).to.have.lengthOf(2) await Promise.all([ localConnectionEventReceived.promise, - remoteConnectionEventReceived.promise + localPeerConnectEventReceived.promise, + remoteConnectionEventReceived.promise, + remotePeerConnectEventReceived.promise ]) // Verify onConnectionEnd is called with the connection @@ -449,79 +223,58 @@ describe('Upgrader', () => { await Promise.all([ localConnectionEndEventReceived.promise, - remoteConnectionEndEventReceived.promise + localPeerDisconnectEventReceived.promise, + remoteConnectionEndEventReceived.promise, + remotePeerDisconnectEventReceived.promise ]) }) it('should fail to create a stream for an unsupported protocol', async () => { - const { inbound, outbound } = mockMultiaddrConnPair({ addrs, remotePeer }) + ({ dialer, listener } = await createPeers()) + + await dialer.dial(listener.getMultiaddrs()) const connections = await Promise.all([ - localUpgrader.upgradeOutbound(outbound), - remoteUpgrader.upgradeInbound(inbound) + ...dialer.getConnections(listener.peerId), + ...listener.getConnections(dialer.peerId) ]) + expect(connections).to.have.lengthOf(2) - expect(connections).to.have.length(2) - - const results = await Promise.allSettled([ - connections[0].newStream('/unsupported/1.0.0'), - connections[1].newStream('/unsupported/1.0.0') - ]) - expect(results).to.have.length(2) - results.forEach(result => { - expect(result).to.have.property('status', 'rejected') - expect(result).to.have.nested.property('reason.name', 'UnsupportedProtocolError') - }) + await expect(connections[0].newStream('/unsupported/1.0.0')).to.eventually.be.rejected + .with.property('name', 'UnsupportedProtocolError') + await expect(connections[1].newStream('/unsupported/1.0.0')).to.eventually.be.rejected + .with.property('name', 'UnsupportedProtocolError') }) - it('should abort protocol selection for slow streams', async () => { - const createStreamMuxerSpy = sinon.spy(localMuxerFactory, 'createStreamMuxer') - const { inbound, outbound } = mockMultiaddrConnPair({ addrs, remotePeer }) - - const connections = await Promise.all([ - localUpgrader.upgradeOutbound(outbound), - remoteUpgrader.upgradeInbound(inbound) - ]) + it('should abort protocol selection for slow stream creation', async () => { + ({ dialer, listener } = await createPeers({ + streamMuxers: [ + slowMuxer(1000) + ] + })) - // 10 ms timeout - const signal = AbortSignal.timeout(10) - - // should have created muxer for connection - expect(createStreamMuxerSpy).to.have.property('callCount', 1) - - // create mock muxed stream that never sends data - const muxer = createStreamMuxerSpy.getCall(0).returnValue - muxer.newStream = () => { - return mockStream({ - source: (async function * () { - // longer than the timeout - await delay(1000) - yield new Uint8ArrayList() - }()), - sink: drain - }) - } + const connection = await dialer.dial(listener.getMultiaddrs()) - await expect(connections[0].newStream(['/echo/1.0.0', '/echo/1.0.1'], { - signal - })) - .to.eventually.be.rejected.with.property('name', 'AbortError') + await expect(connection.newStream('/echo/1.0.0', { + signal: AbortSignal.timeout(100) + })).to.eventually.be.rejected + .with.property('name', 'AbortError') }) it('should close streams when protocol negotiation fails', async () => { - await remoteComponents.registrar.unhandle('/echo/1.0.0') + ({ dialer, listener } = await createPeers()) - const { inbound, outbound } = mockMultiaddrConnPair({ addrs, remotePeer }) + await dialer.dial(listener.getMultiaddrs()) const connections = await Promise.all([ - localUpgrader.upgradeOutbound(outbound), - remoteUpgrader.upgradeInbound(inbound) + ...dialer.getConnections(listener.peerId), + ...listener.getConnections(dialer.peerId) ]) - + expect(connections).to.have.lengthOf(2) expect(connections[0].streams).to.have.lengthOf(0) expect(connections[1].streams).to.have.lengthOf(0) - await expect(connections[0].newStream(['/echo/1.0.0', '/echo/1.0.1'])) + await expect(connections[0].newStream('/foo/1.0.0')) .to.eventually.be.rejected.with.property('name', 'UnsupportedProtocolError') // wait for remote to close @@ -531,526 +284,133 @@ describe('Upgrader', () => { expect(connections[1].streams).to.have.lengthOf(0) }) - it('should allow skipping encryption, protection and muxing', async () => { - const localStreamMuxerFactorySpy = sinon.spy(localMuxerFactory, 'createStreamMuxer') - const localMuxerFactoryOverride = mockMuxer() - const localStreamMuxerFactoryOverrideSpy = sinon.spy(localMuxerFactoryOverride, 'createStreamMuxer') - const localConnectionEncrypterSpy = sinon.spy(localConnectionEncrypter, 'secureOutbound') - - const remoteStreamMuxerFactorySpy = sinon.spy(remoteMuxerFactory, 'createStreamMuxer') - const remoteMuxerFactoryOverride = mockMuxer() - const remoteStreamMuxerFactoryOverrideSpy = sinon.spy(remoteMuxerFactoryOverride, 'createStreamMuxer') - const remoteConnectionEncrypterSpy = sinon.spy(remoteConnectionEncrypter, 'secureInbound') - - const { inbound, outbound } = mockMultiaddrConnPair({ - addrs: [ - multiaddr('/ip4/127.0.0.1/tcp/0').encapsulate(`/p2p/${remotePeer.toString()}`), - multiaddr('/ip4/127.0.0.1/tcp/0') - ], - remotePeer - }) - - outbound.remoteAddr = multiaddr('/ip4/127.0.0.1/tcp/0').encapsulate(`/p2p/${remotePeer.toString()}`) - inbound.remoteAddr = multiaddr('/ip4/127.0.0.1/tcp/0').encapsulate(`/p2p/${localPeer.toString()}`) - - const connections = await Promise.all([ - localUpgrader.upgradeOutbound(outbound, { - skipEncryption: true, - skipProtection: true, - muxerFactory: localMuxerFactoryOverride - }), - remoteUpgrader.upgradeInbound(inbound, { - skipEncryption: true, - skipProtection: true, - muxerFactory: remoteMuxerFactoryOverride - }) - ]) - - expect(connections).to.have.length(2) - - const stream = await connections[0].newStream(['/echo/1.0.0', '/echo/1.0.1']) - expect(stream).to.have.property('protocol', '/echo/1.0.0') - - const hello = uint8ArrayFromString('hello there!') - const result = await pipe( - [hello], - stream, - function toBuffer (source) { - return (async function * () { - for await (const val of source) yield val.slice() - })() - }, - async (source) => all(source) - ) - - expect(result).to.eql([hello]) - - expect(localStreamMuxerFactorySpy.callCount).to.equal(0, 'did not use passed stream muxer factory') - expect(localStreamMuxerFactoryOverrideSpy.callCount).to.equal(1, 'did not use passed stream muxer factory') - - expect(remoteStreamMuxerFactorySpy.callCount).to.equal(0, 'did not use passed stream muxer factory') - expect(remoteStreamMuxerFactoryOverrideSpy.callCount).to.equal(1, 'did not use passed stream muxer factory') - - expect(localConnectionEncrypterSpy.callCount).to.equal(0, 'used local connection encrypter') - expect(remoteConnectionEncrypterSpy.callCount).to.equal(0, 'used remote connection encrypter') - - expect(localConnectionProtector.protect.callCount).to.equal(0, 'used local connection protector') - expect(remoteConnectionProtector.protect.callCount).to.equal(0, 'used remote connection protector') - }) - - it('should not decrement inbound pending connection count if the connection is denied', async () => { - const connectionManager = stubInterface() - - // @ts-expect-error private field - localUpgrader.components.connectionManager = connectionManager - - const maConn = stubInterface() - - connectionManager.acceptIncomingConnection.resolves(false) - - await expect(localUpgrader.upgradeInbound(maConn)).to.eventually.be.rejected - .with.property('name', 'ConnectionDeniedError') - - expect(connectionManager.afterUpgradeInbound.called).to.be.false() - }) -}) - -describe('libp2p.upgrader', () => { - let peers: PrivateKey[] - let libp2p: Libp2p - let remoteLibp2p: Libp2p - - before(async () => { - peers = await Promise.all([ - generateKeyPair('Ed25519'), - generateKeyPair('Ed25519') - ]) - }) + it('should allow skipping encryption and protection', async () => { + const protector = stubInterface() + const encrypter = stubInterface() - afterEach(async () => { - sinon.restore() - - if (libp2p != null) { - await libp2p.stop() - } - - if (remoteLibp2p != null) { - await remoteLibp2p.stop() - } - }) - - it('should create an Upgrader', async () => { - const deferred = pDefer() - - const protector: ConnectionProtector = { - async protect (connection) { - return connection - } - } - - libp2p = await createLibp2p({ - privateKey: peers[0], - transports: [ - webSockets() - ], - streamMuxers: [ - yamux(), - mplex() - ], - connectionEncrypters: [ - plaintext() - ], - connectionProtector: () => protector, - services: { - test: (components: any) => { - deferred.resolve(components) - } - } - }) - - const components = await deferred.promise - - expect(components.upgrader).to.exist() - expect(components.connectionProtector).to.exist() - }) - - it('should return muxed streams', async () => { - const localDeferred = pDefer() - const remoteDeferred = pDefer() - - const remotePeer = peers[1] - libp2p = await createLibp2p({ - privateKey: peers[0], + ;({ dialer, listener } = await createPeers({ transports: [ - webSockets() - ], - streamMuxers: [ - yamux(), - mplex() + memory({ + upgraderOptions: { + skipEncryption: true, + skipProtection: true + } + }) ], connectionEncrypters: [ - plaintext() + () => encrypter ], - services: { - test: (components: any) => { - localDeferred.resolve(components) - } - } - }) - const echoHandler = (): void => {} - await libp2p.handle(['/echo/1.0.0'], echoHandler) - - remoteLibp2p = await createLibp2p({ - privateKey: remotePeer, + connectionProtector: () => protector + }, { transports: [ - webSockets() - ], - streamMuxers: [ - yamux(), - mplex() + memory({ + upgraderOptions: { + skipEncryption: true, + skipProtection: true + } + }) ], connectionEncrypters: [ - plaintext() + () => encrypter ], - services: { - test: (components: any) => { - remoteDeferred.resolve(components) - } - } - }) - await remoteLibp2p.handle('/echo/1.0.0', echoHandler) - - const localComponents = await localDeferred.promise - const remoteComponents = await remoteDeferred.promise - - const { inbound, outbound } = mockMultiaddrConnPair({ addrs, remotePeer: peerIdFromCID(remotePeer.publicKey.toCID()) }) - const [localConnection] = await Promise.all([ - localComponents.upgrader.upgradeOutbound(outbound), - remoteComponents.upgrader.upgradeInbound(inbound) - ]) - const remoteLibp2pUpgraderOnStreamSpy = sinon.spy(remoteComponents.upgrader as DefaultUpgrader, '_onStream') + connectionProtector: () => protector + })) - const stream = await localConnection.newStream(['/echo/1.0.0', '/echo/1.0.1']) - expect(stream).to.include.keys(['id', 'sink', 'source']) + const input = Uint8Array.from([0, 1, 2, 3, 4]) + const output = await dialer.services.echo.echo(listener.getMultiaddrs(), input) + expect(output).to.equalBytes(input) - const [arg0] = remoteLibp2pUpgraderOnStreamSpy.getCall(0).args - expect(arg0.stream).to.include.keys(['id', 'sink', 'source']) + expect(encrypter.secureInbound.called).to.be.false('used connection encrypter') + expect(encrypter.secureOutbound.called).to.be.false('used connection encrypter') + expect(protector.protect.called).to.be.false('used connection protector') }) - it('should emit connect and disconnect events', async () => { - const remotePeer = peers[1] - libp2p = await createLibp2p({ - privateKey: peers[0], - addresses: { - listen: [ - `${process.env.RELAY_MULTIADDR}/p2p-circuit` - ] - }, - transports: [ - webSockets({ - filter: filters.all - }), - circuitRelayTransport() - ], - streamMuxers: [ - yamux(), - mplex() - ], - connectionEncrypters: [ - plaintext() - ], - connectionGater: mockConnectionGater(), - services: { - identify: identify() - } - }) - await libp2p.start() - - remoteLibp2p = await createLibp2p({ - privateKey: remotePeer, - transports: [ - webSockets({ - filter: filters.all - }), - circuitRelayTransport() - ], - streamMuxers: [ - yamux(), - mplex() - ], - connectionEncrypters: [ - plaintext() - ], - connectionGater: mockConnectionGater(), - services: { - identify: identify() - } - }) - await remoteLibp2p.start() - - // Upgrade and check the connect event - const connectionPromise = pEvent<'connection:open', CustomEvent>(libp2p, 'connection:open') - - const connection = await remoteLibp2p.dial(libp2p.getMultiaddrs()) - - const connectEvent = await connectionPromise - - if (connectEvent.type !== 'connection:open') { - throw new Error(`Incorrect event type, expected: 'connection:open' actual: ${connectEvent.type}`) - } - - expect(remotePeer.publicKey.equals(connectEvent.detail.remotePeer.publicKey)).to.equal(true) - - const disconnectionPromise = pEvent<'peer:disconnect', CustomEvent>(libp2p, 'peer:disconnect') + it('should not decrement inbound pending connection count if the connection is denied', async () => { + ({ dialer, dialerComponents, listener, listenerComponents } = await createPeers()) - // Close and check the disconnect event - await connection.close() + listenerComponents.connectionManager.acceptIncomingConnection = async () => false + const afterUpgradeInboundSpy = Sinon.spy(listenerComponents.connectionManager, 'afterUpgradeInbound') - const disconnectEvent = await disconnectionPromise + await expect(dialer.dial(listener.getMultiaddrs())).to.eventually.be.rejected + .with.property('message', 'Connection denied') - if (disconnectEvent.type !== 'peer:disconnect') { - throw new Error(`Incorrect event type, expected: 'peer:disconnect' actual: ${disconnectEvent.type}`) - } - - expect(remotePeer.publicKey.equals(disconnectEvent.detail.publicKey)).to.equal(true) + expect(afterUpgradeInboundSpy.called).to.be.false() }) it('should limit the number of incoming streams that can be opened using a protocol', async () => { - const localDeferred = pDefer() - const remoteDeferred = pDefer() - const protocol = '/a-test-protocol/1.0.0' - const remotePeer = peers[1] - libp2p = await createLibp2p({ - privateKey: peers[0], - transports: [ - webSockets() - ], - streamMuxers: [ - mplex() - ], - connectionEncrypters: [ - plaintext() - ], - services: { - test: (components: any) => { - localDeferred.resolve(components) - } - }, - connectionGater: mockConnectionGater() - }) - - remoteLibp2p = await createLibp2p({ - privateKey: remotePeer, - transports: [ - webSockets() - ], - streamMuxers: [ - mplex() - ], - connectionEncrypters: [ - plaintext() - ], - services: { - test: (components: any) => { - remoteDeferred.resolve(components) - } - }, - connectionGater: mockConnectionGater() - }) - - const { inbound, outbound } = mockMultiaddrConnPair({ addrs, remotePeer: peerIdFromPrivateKey(remotePeer) }) - - const localComponents = await localDeferred.promise - const remoteComponents = await remoteDeferred.promise + ({ dialer, listener } = await createPeers()) - const [localToRemote] = await Promise.all([ - localComponents.upgrader.upgradeOutbound(outbound), - remoteComponents.upgrader.upgradeInbound(inbound) - ]) - - let streamCount = 0 + const protocol = '/a-test-protocol/1.0.0' - await libp2p.handle(protocol, (data) => {}, { - maxInboundStreams: 10, - maxOutboundStreams: 10 + await listener.handle(protocol, () => {}, { + maxInboundStreams: 2, + maxOutboundStreams: 2 }) - await remoteLibp2p.handle(protocol, (data) => { - streamCount++ - }, { - maxInboundStreams: 1, - maxOutboundStreams: 1 - }) + const connection = await dialer.dial(listener.getMultiaddrs()) + expect(connection.streams).to.have.lengthOf(0) - expect(streamCount).to.equal(0) + await connection.newStream(protocol) + await connection.newStream(protocol) - await localToRemote.newStream([protocol, '/other/1.0.0']) + expect(connection.streams).to.have.lengthOf(2) - expect(streamCount).to.equal(1) + const stream = await connection.newStream(protocol) - const s = await localToRemote.newStream(protocol) - - await expect(drain(s.source)).to.eventually.be.rejected() + await expect(drain(stream.source)).to.eventually.be.rejected() .with.property('name', 'StreamResetError') }) it('should limit the number of outgoing streams that can be opened using a protocol', async () => { - const localDeferred = pDefer() - const remoteDeferred = pDefer() - const protocol = '/a-test-protocol/1.0.0' - const remotePeer = peers[1] - libp2p = await createLibp2p({ - privateKey: peers[0], - transports: [ - webSockets() - ], - streamMuxers: [ - yamux() - ], - connectionEncrypters: [ - plaintext() - ], - services: { - test: (components: any) => { - localDeferred.resolve(components) - } - }, - connectionGater: mockConnectionGater() - }) + ({ dialer, listener } = await createPeers()) - remoteLibp2p = await createLibp2p({ - privateKey: remotePeer, - transports: [ - webSockets() - ], - streamMuxers: [ - yamux() - ], - connectionEncrypters: [ - plaintext() - ], - services: { - test: (components: any) => { - remoteDeferred.resolve(components) - } - } - }) - - const { inbound, outbound } = mockMultiaddrConnPair({ addrs, remotePeer: peerIdFromPrivateKey(remotePeer) }) - - const localComponents = await localDeferred.promise - const remoteComponents = await remoteDeferred.promise - - const [localToRemote] = await Promise.all([ - localComponents.upgrader.upgradeOutbound(outbound), - remoteComponents.upgrader.upgradeInbound(inbound) - ]) - - let streamCount = 0 + const protocol = '/a-test-protocol/1.0.0' - await libp2p.handle(protocol, (data) => {}, { - maxInboundStreams: 1, - maxOutboundStreams: 1 + await listener.handle(protocol, () => {}, { + maxInboundStreams: 20, + maxOutboundStreams: 20 }) - await remoteLibp2p.handle(protocol, (data) => { - streamCount++ - }, { - maxInboundStreams: 10, - maxOutboundStreams: 10 + await dialer.handle(protocol, () => {}, { + maxInboundStreams: 2, + maxOutboundStreams: 2 }) - expect(streamCount).to.equal(0) + const connection = await dialer.dial(listener.getMultiaddrs()) + expect(connection.streams).to.have.lengthOf(0) - await localToRemote.newStream([protocol, '/other/1.0.0']) + await connection.newStream(protocol) + await connection.newStream(protocol) - expect(streamCount).to.equal(1) + expect(connection.streams).to.have.lengthOf(2) - await expect(localToRemote.newStream(protocol)).to.eventually.be.rejected() + await expect(connection.newStream(protocol)).to.eventually.be.rejected() .with.property('name', 'TooManyOutboundProtocolStreamsError') }) it('should allow overriding the number of outgoing streams that can be opened using a protocol without a handler', async () => { - const localDeferred = pDefer() - const remoteDeferred = pDefer() - const protocol = '/a-test-protocol/1.0.0' - const remotePeer = peers[1] - libp2p = await createLibp2p({ - privateKey: peers[0], - transports: [ - webSockets() - ], - streamMuxers: [ - yamux() - ], - connectionEncrypters: [ - plaintext() - ], - services: { - test: (components: any) => { - localDeferred.resolve(components) - } - }, - connectionGater: mockConnectionGater() - }) - - remoteLibp2p = await createLibp2p({ - privateKey: remotePeer, - transports: [ - webSockets() - ], - streamMuxers: [ - yamux() - ], - connectionEncrypters: [ - plaintext() - ], - services: { - test: (components: any) => { - remoteDeferred.resolve(components) - } - } - }) - - const { inbound, outbound } = mockMultiaddrConnPair({ addrs, remotePeer: peerIdFromPrivateKey(remotePeer) }) - - const localComponents = await localDeferred.promise - const remoteComponents = await remoteDeferred.promise - - const [localToRemote] = await Promise.all([ - localComponents.upgrader.upgradeOutbound(outbound), - remoteComponents.upgrader.upgradeInbound(inbound) - ]) + ({ dialer, listener } = await createPeers()) - let streamCount = 0 - - const limit = DEFAULT_MAX_OUTBOUND_STREAMS + 1 + const protocol = '/a-test-protocol/1.0.0' - await remoteLibp2p.handle(protocol, (data) => { - streamCount++ - }, { - maxInboundStreams: limit + 1, - maxOutboundStreams: 10 + await listener.handle(protocol, () => {}, { + maxInboundStreams: 20, + maxOutboundStreams: 20 }) - expect(streamCount).to.equal(0) + const connection = await dialer.dial(listener.getMultiaddrs()) + expect(connection.streams).to.have.lengthOf(0) - for (let i = 0; i < limit; i++) { - await localToRemote.newStream([protocol, '/other/1.0.0'], { - maxOutboundStreams: limit - }) + const opts = { + maxOutboundStreams: 2 } - expect(streamCount).to.equal(limit) + await connection.newStream(protocol, opts) + await connection.newStream(protocol, opts) - // should reject without overriding limit - await expect(localToRemote.newStream(protocol)).to.eventually.be.rejected() - .with.property('name', 'TooManyOutboundProtocolStreamsError') + expect(connection.streams).to.have.lengthOf(2) - // should reject even with overriding limit - await expect(localToRemote.newStream(protocol, { - maxOutboundStreams: limit - })).to.eventually.be.rejected() + await expect(connection.newStream(protocol, opts)).to.eventually.be.rejected() .with.property('name', 'TooManyOutboundProtocolStreamsError') }) })