From 7c085d10c4d9f491ab85b2467ab1202769ec7a5a Mon Sep 17 00:00:00 2001 From: Matthew Turland Date: Mon, 4 Nov 2024 03:15:12 -0600 Subject: [PATCH 1/5] docs: fix "Closing connections" link syntax in LIMITS.md (#2799) --- doc/LIMITS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/LIMITS.md b/doc/LIMITS.md index 7505384da9..a479c8ba8c 100644 --- a/doc/LIMITS.md +++ b/doc/LIMITS.md @@ -19,7 +19,7 @@ This is important for [DoS](https://en.wikipedia.org/wiki/Denial-of-service_atta ## Connection limits -It's possible to limit the total amount of connections a node is able to make (combining incoming and outgoing). When this limit is reached and an attempt to open a new connection is made, existing connections may be closed to make room for the new connection (see [Closing connections][#closing-connections]). +It's possible to limit the total amount of connections a node is able to make (combining incoming and outgoing). When this limit is reached and an attempt to open a new connection is made, existing connections may be closed to make room for the new connection (see [Closing connections](#closing-connections)). - Note: there currently isn't a way to specify different limits for incoming vs. outgoing. Connection limits are applied across both incoming and outgoing connections combined. There is a backlog item for this [here](https://github.com/libp2p/js-libp2p/issues/1508). From adc767899d3fcf186a2bfb37a4d53decadc3a93f Mon Sep 17 00:00:00 2001 From: Alex Potsides Date: Tue, 5 Nov 2024 07:12:40 +0000 Subject: [PATCH 2/5] feat: add memory transport (#2802) Adds an in-memory only transport using the `/memory/...` multiaddr protocol. This lets us connect multiple in-process nodes together with controlable latency settings to aid testing. --- .release-please-manifest.json | 2 +- .release-please.json | 1 + packages/transport-memory/.aegir.js | 6 + packages/transport-memory/LICENSE | 4 + packages/transport-memory/LICENSE-APACHE | 5 + packages/transport-memory/LICENSE-MIT | 19 ++ packages/transport-memory/README.md | 86 +++++++++ packages/transport-memory/package.json | 75 ++++++++ packages/transport-memory/src/connections.ts | 176 ++++++++++++++++++ packages/transport-memory/src/index.ts | 62 ++++++ packages/transport-memory/src/listener.ts | 135 ++++++++++++++ packages/transport-memory/src/memory.ts | 113 +++++++++++ .../transport-memory/test/compliance.spec.ts | 69 +++++++ packages/transport-memory/tsconfig.json | 27 +++ packages/transport-memory/typedoc.json | 5 + 15 files changed, 784 insertions(+), 1 deletion(-) create mode 100644 packages/transport-memory/.aegir.js create mode 100644 packages/transport-memory/LICENSE create mode 100644 packages/transport-memory/LICENSE-APACHE create mode 100644 packages/transport-memory/LICENSE-MIT create mode 100644 packages/transport-memory/README.md create mode 100644 packages/transport-memory/package.json create mode 100644 packages/transport-memory/src/connections.ts create mode 100644 packages/transport-memory/src/index.ts create mode 100644 packages/transport-memory/src/listener.ts create mode 100644 packages/transport-memory/src/memory.ts create mode 100644 packages/transport-memory/test/compliance.spec.ts create mode 100644 packages/transport-memory/tsconfig.json create mode 100644 packages/transport-memory/typedoc.json diff --git a/.release-please-manifest.json b/.release-please-manifest.json index c16fe6e36f..fec9fa0774 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1 +1 @@ -{"packages/connection-encrypter-plaintext":"2.0.10","packages/connection-encrypter-tls":"2.0.10","packages/crypto":"5.0.6","packages/interface":"2.2.0","packages/interface-compliance-tests":"6.1.8","packages/interface-internal":"2.0.10","packages/kad-dht":"14.1.0","packages/keychain":"5.0.9","packages/libp2p":"2.2.1","packages/logger":"5.1.3","packages/metrics-devtools":"1.1.8","packages/metrics-prometheus":"4.2.4","packages/metrics-simple":"1.2.6","packages/multistream-select":"6.0.8","packages/peer-collections":"6.0.10","packages/peer-discovery-bootstrap":"11.0.10","packages/peer-discovery-mdns":"11.0.10","packages/peer-id":"5.0.7","packages/peer-record":"8.0.10","packages/peer-store":"11.0.10","packages/pnet":"2.0.10","packages/protocol-autonat":"2.0.10","packages/protocol-dcutr":"2.0.10","packages/protocol-echo":"2.1.1","packages/protocol-fetch":"2.0.10","packages/protocol-identify":"3.0.10","packages/protocol-perf":"4.0.10","packages/protocol-ping":"2.0.10","packages/pubsub":"10.0.10","packages/pubsub-floodsub":"10.1.8","packages/record":"4.0.4","packages/stream-multiplexer-mplex":"11.0.10","packages/transport-circuit-relay-v2":"3.1.0","packages/transport-tcp":"10.0.11","packages/transport-webrtc":"5.0.16","packages/transport-websockets":"9.0.11","packages/transport-webtransport":"5.0.16","packages/upnp-nat":"2.0.10","packages/utils":"6.1.3"} +{"packages/connection-encrypter-plaintext":"2.0.10","packages/connection-encrypter-tls":"2.0.10","packages/crypto":"5.0.6","packages/interface":"2.2.0","packages/interface-compliance-tests":"6.1.8","packages/interface-internal":"2.0.10","packages/kad-dht":"14.1.0","packages/keychain":"5.0.9","packages/libp2p":"2.2.1","packages/logger":"5.1.3","packages/metrics-devtools":"1.1.8","packages/metrics-prometheus":"4.2.4","packages/metrics-simple":"1.2.6","packages/multistream-select":"6.0.8","packages/peer-collections":"6.0.10","packages/peer-discovery-bootstrap":"11.0.10","packages/peer-discovery-mdns":"11.0.10","packages/peer-id":"5.0.7","packages/peer-record":"8.0.10","packages/peer-store":"11.0.10","packages/pnet":"2.0.10","packages/protocol-autonat":"2.0.10","packages/protocol-dcutr":"2.0.10","packages/protocol-echo":"2.1.1","packages/protocol-fetch":"2.0.10","packages/protocol-identify":"3.0.10","packages/protocol-perf":"4.0.10","packages/protocol-ping":"2.0.10","packages/pubsub":"10.0.10","packages/pubsub-floodsub":"10.1.8","packages/record":"4.0.4","packages/stream-multiplexer-mplex":"11.0.10","packages/transport-circuit-relay-v2":"3.1.0","packages/transport-memory":"0.0.0","packages/transport-tcp":"10.0.11","packages/transport-webrtc":"5.0.16","packages/transport-websockets":"9.0.11","packages/transport-webtransport":"5.0.16","packages/upnp-nat":"2.0.10","packages/utils":"6.1.3"} diff --git a/.release-please.json b/.release-please.json index 07baf97322..6fb8596cd7 100644 --- a/.release-please.json +++ b/.release-please.json @@ -42,6 +42,7 @@ "packages/record": {}, "packages/stream-multiplexer-mplex": {}, "packages/transport-circuit-relay-v2": {}, + "packages/transport-memory": {}, "packages/transport-tcp": {}, "packages/transport-webrtc": {}, "packages/transport-websockets": {}, diff --git a/packages/transport-memory/.aegir.js b/packages/transport-memory/.aegir.js new file mode 100644 index 0000000000..13d33c8d5c --- /dev/null +++ b/packages/transport-memory/.aegir.js @@ -0,0 +1,6 @@ +/** @type {import('aegir').PartialOptions} */ +export default { + build: { + bundlesizeMax: '15KB' + } +} diff --git a/packages/transport-memory/LICENSE b/packages/transport-memory/LICENSE new file mode 100644 index 0000000000..20ce483c86 --- /dev/null +++ b/packages/transport-memory/LICENSE @@ -0,0 +1,4 @@ +This project is dual licensed under MIT and Apache-2.0. + +MIT: https://www.opensource.org/licenses/mit +Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/packages/transport-memory/LICENSE-APACHE b/packages/transport-memory/LICENSE-APACHE new file mode 100644 index 0000000000..14478a3b60 --- /dev/null +++ b/packages/transport-memory/LICENSE-APACHE @@ -0,0 +1,5 @@ +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/packages/transport-memory/LICENSE-MIT b/packages/transport-memory/LICENSE-MIT new file mode 100644 index 0000000000..72dc60d84b --- /dev/null +++ b/packages/transport-memory/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/packages/transport-memory/README.md b/packages/transport-memory/README.md new file mode 100644 index 0000000000..ce955ad2a1 --- /dev/null +++ b/packages/transport-memory/README.md @@ -0,0 +1,86 @@ +# @libp2p/tcp + +[![libp2p.io](https://img.shields.io/badge/project-libp2p-yellow.svg?style=flat-square)](http://libp2p.io/) +[![Discuss](https://img.shields.io/discourse/https/discuss.libp2p.io/posts.svg?style=flat-square)](https://discuss.libp2p.io) +[![codecov](https://img.shields.io/codecov/c/github/libp2p/js-libp2p.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p) +[![CI](https://img.shields.io/github/actions/workflow/status/libp2p/js-libp2p/main.yml?branch=main\&style=flat-square)](https://github.com/libp2p/js-libp2p/actions/workflows/main.yml?query=branch%3Amain) + +> A memory transport for libp2p + +# About + + + +A [libp2p transport](https://docs.libp2p.io/concepts/transports/overview/) +that operates in-memory only. + +This is intended for testing and can only be used to connect two libp2p nodes +that are running in the same process. + +## Example + +```TypeScript +import { createLibp2p } from 'libp2p' +import { memory } from '@libp2p/memory' +import { multiaddr } from '@multiformats/multiaddr' + +const listener = await createLibp2p({ + addresses: { + listen: [ + '/memory/address-a' + ] + }, + transports: [ + memory() + ] +}) + +const dialer = await createLibp2p({ + transports: [ + memory() + ] +}) + +const ma = multiaddr('/memory/address-a') + +// dial the listener, timing out after 10s +const connection = await dialer.dial(ma, { + signal: AbortSignal.timeout(10_000) +}) + +// use connection... +``` + +# Install + +```console +$ npm i @libp2p/tcp +``` + +# API Docs + +- + +# License + +Licensed under either of + +- Apache 2.0, ([LICENSE-APACHE](https://github.com/libp2p/js-libp2p/blob/main/packages/transport-tcp/LICENSE-APACHE) / ) +- MIT ([LICENSE-MIT](https://github.com/libp2p/js-libp2p/blob/main/packages/transport-tcp/LICENSE-MIT) / ) + +# Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. diff --git a/packages/transport-memory/package.json b/packages/transport-memory/package.json new file mode 100644 index 0000000000..0269be5766 --- /dev/null +++ b/packages/transport-memory/package.json @@ -0,0 +1,75 @@ +{ + "name": "@libp2p/memory", + "version": "0.0.0", + "description": "A memory transport for libp2p", + "license": "Apache-2.0 OR MIT", + "homepage": "https://github.com/libp2p/js-libp2p/tree/main/packages/transport-tcp#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/libp2p/js-libp2p.git" + }, + "bugs": { + "url": "https://github.com/libp2p/js-libp2p/issues" + }, + "publishConfig": { + "access": "public", + "provenance": true + }, + "type": "module", + "types": "./dist/src/index.d.ts", + "files": [ + "src", + "dist", + "!dist/test", + "!**/*.tsbuildinfo" + ], + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "import": "./dist/src/index.js" + } + }, + "eslintConfig": { + "extends": "ipfs", + "parserOptions": { + "project": true, + "sourceType": "module" + } + }, + "scripts": { + "clean": "aegir clean", + "lint": "aegir lint", + "dep-check": "aegir dep-check", + "doc-check": "aegir doc-check", + "build": "aegir build", + "test": "aegir test -t node -t electron-main", + "test:chrome": "aegir test -t browser -f ./dist/test/browser.js --cov", + "test:chrome-webworker": "aegir test -t webworker -f ./dist/test/browser.js", + "test:firefox": "aegir test -t browser -f ./dist/test/browser.js -- --browser firefox", + "test:firefox-webworker": "aegir test -t webworker -f ./dist/test/browser.js -- --browser firefox", + "test:node": "aegir test -t node --cov", + "test:electron-main": "aegir test -t electron-main" + }, + "dependencies": { + "@libp2p/interface": "^2.2.0", + "@multiformats/multiaddr": "^12.2.3", + "@multiformats/multiaddr-matcher": "^1.5.0", + "@types/sinon": "^17.0.3", + "delay": "^6.0.0", + "it-map": "^3.1.1", + "it-pushable": "^3.2.3", + "nanoid": "^5.0.8", + "uint8arraylist": "^2.4.8" + }, + "devDependencies": { + "@libp2p/crypto": "^5.0.6", + "@libp2p/interface-compliance-tests": "^6.1.8", + "@libp2p/logger": "^5.1.3", + "@libp2p/peer-id": "^5.0.7", + "aegir": "^44.0.1" + }, + "browser": { + "./dist/src/tcp.js": "./dist/src/tcp.browser.js" + }, + "sideEffects": false +} diff --git a/packages/transport-memory/src/connections.ts b/packages/transport-memory/src/connections.ts new file mode 100644 index 0000000000..a28ace40a8 --- /dev/null +++ b/packages/transport-memory/src/connections.ts @@ -0,0 +1,176 @@ +/** + * @packageDocumentation + * + * A [libp2p transport](https://docs.libp2p.io/concepts/transports/overview/) + * that operates in-memory only. + * + * This is intended for testing and can only be used to connect two libp2p nodes + * that are running in the same process. + * + * @example + * + * ```TypeScript + * import { createLibp2p } from 'libp2p' + * import { memory } from '@libp2p/memory' + * import { multiaddr } from '@multiformats/multiaddr' + * + * const listener = await createLibp2p({ + * addresses: { + * listen: [ + * '/memory/node-a' + * ] + * }, + * transports: [ + * memory() + * ] + * }) + * + * const dialer = await createLibp2p({ + * transports: [ + * memory() + * ] + * }) + * + * const ma = multiaddr('/memory/node-a') + * + * // dial the listener, timing out after 10s + * const connection = await dialer.dial(ma, { + * signal: AbortSignal.timeout(10_000) + * }) + * + * // use connection... + * ``` + */ + +import { ConnectionFailedError } from '@libp2p/interface' +import { multiaddr } from '@multiformats/multiaddr' +import delay from 'delay' +import map from 'it-map' +import { pushable } from 'it-pushable' +import type { MemoryTransportComponents } from './index.js' +import type { MultiaddrConnection, PeerId } from '@libp2p/interface' +import type { Uint8ArrayList } from 'uint8arraylist' + +export const connections = new Map() + +interface MemoryConnectionHandler { + (maConn: MultiaddrConnection): void +} + +interface MemoryConnectionInit { + onConnection: MemoryConnectionHandler + address: string +} + +export class MemoryConnection { + private readonly components: MemoryTransportComponents + private readonly init: MemoryConnectionInit + private readonly connections: Set + private latency: number + + constructor (components: MemoryTransportComponents, init: MemoryConnectionInit) { + this.components = components + this.init = init + this.connections = new Set() + this.latency = 0 + } + + async dial (dialingPeerId: PeerId): Promise { + const dialerPushable = pushable() + const listenerPushable = pushable() + const self = this + + const dialer: MultiaddrConnection = { + source: (async function * () { + yield * map(listenerPushable, async buf => { + await delay(self.latency) + return buf + }) + })(), + sink: async (source) => { + for await (const buf of source) { + dialerPushable.push(buf) + } + }, + close: async () => { + dialerPushable.end() + this.connections.delete(dialer) + dialer.timeline.close = Date.now() + + listenerPushable.end() + this.connections.delete(listener) + listener.timeline.close = Date.now() + }, + abort: (err) => { + dialerPushable.end(err) + this.connections.delete(dialer) + dialer.timeline.close = Date.now() + + listenerPushable.end(err) + this.connections.delete(listener) + listener.timeline.close = Date.now() + }, + timeline: { + open: Date.now() + }, + remoteAddr: multiaddr(`${this.init.address}/p2p/${this.components.peerId}`), + log: this.components.logger.forComponent(`libp2p:memory:outgoing:${1}`) + } + + const listener: MultiaddrConnection = { + source: (async function * () { + yield * map(dialerPushable, async buf => { + await delay(self.latency) + return buf + }) + })(), + sink: async (source) => { + for await (const buf of source) { + listenerPushable.push(buf) + } + }, + close: async () => { + listenerPushable.end() + this.connections.delete(listener) + listener.timeline.close = Date.now() + + dialerPushable.end() + this.connections.delete(dialer) + dialer.timeline.close = Date.now() + }, + abort: (err) => { + listenerPushable.end(err) + this.connections.delete(listener) + listener.timeline.close = Date.now() + + dialerPushable.end(err) + this.connections.delete(dialer) + dialer.timeline.close = Date.now() + }, + timeline: { + open: Date.now() + }, + remoteAddr: multiaddr(`${this.init.address}-outgoing/p2p/${dialingPeerId}`), + log: this.components.logger.forComponent(`libp2p:memory:outgoing:${1}`) + } + + this.connections.add(dialer) + this.connections.add(listener) + + await delay(this.latency) + + this.init.onConnection(listener) + + return dialer + } + + close (): void { + [...this.connections].forEach(maConn => { + maConn.abort(new ConnectionFailedError('Memory Connection closed')) + }) + } + + setLatency (ms: number): void { + this.latency = ms + } +} diff --git a/packages/transport-memory/src/index.ts b/packages/transport-memory/src/index.ts new file mode 100644 index 0000000000..cab3478b87 --- /dev/null +++ b/packages/transport-memory/src/index.ts @@ -0,0 +1,62 @@ +/** + * @packageDocumentation + * + * A [libp2p transport](https://docs.libp2p.io/concepts/transports/overview/) + * that operates in-memory only. + * + * This is intended for testing and can only be used to connect two libp2p nodes + * that are running in the same process. + * + * @example + * + * ```TypeScript + * import { createLibp2p } from 'libp2p' + * import { memory } from '@libp2p/memory' + * import { multiaddr } from '@multiformats/multiaddr' + * + * const listener = await createLibp2p({ + * addresses: { + * listen: [ + * '/memory/address-a' + * ] + * }, + * transports: [ + * memory() + * ] + * }) + * + * const dialer = await createLibp2p({ + * transports: [ + * memory() + * ] + * }) + * + * const ma = multiaddr('/memory/address-a') + * + * // dial the listener, timing out after 10s + * const connection = await dialer.dial(ma, { + * signal: AbortSignal.timeout(10_000) + * }) + * + * // use connection... + * ``` + */ + +import { MemoryTransport } from './memory.js' +import type { Transport, ComponentLogger, UpgraderOptions, PeerId } from '@libp2p/interface' + +export interface MemoryTransportComponents { + peerId: PeerId + logger: ComponentLogger +} + +export interface MemoryTransportInit { + upgraderOptions?: UpgraderOptions + inboundUpgradeTimeout?: number +} + +export function memory (init?: MemoryTransportInit): (components: MemoryTransportComponents) => Transport { + return (components) => { + return new MemoryTransport(components, init) + } +} diff --git a/packages/transport-memory/src/listener.ts b/packages/transport-memory/src/listener.ts new file mode 100644 index 0000000000..ffedea10dc --- /dev/null +++ b/packages/transport-memory/src/listener.ts @@ -0,0 +1,135 @@ +/** + * @packageDocumentation + * + * A [libp2p transport](https://docs.libp2p.io/concepts/transports/overview/) + * that operates in-memory only. + * + * This is intended for testing and can only be used to connect two libp2p nodes + * that are running in the same process. + * + * @example + * + * ```TypeScript + * import { createLibp2p } from 'libp2p' + * import { memory } from '@libp2p/memory' + * import { multiaddr } from '@multiformats/multiaddr' + * + * const listener = await createLibp2p({ + * addresses: { + * listen: [ + * '/memory/node-a' + * ] + * }, + * transports: [ + * memory() + * ] + * }) + * + * const dialer = await createLibp2p({ + * transports: [ + * memory() + * ] + * }) + * + * const ma = multiaddr('/memory/node-a') + * + * // dial the listener, timing out after 10s + * const connection = await dialer.dial(ma, { + * signal: AbortSignal.timeout(10_000) + * }) + * + * // use connection... + * ``` + */ + +import { ListenError, TypedEventEmitter } from '@libp2p/interface' +import { multiaddr } from '@multiformats/multiaddr' +import { nanoid } from 'nanoid' +import { MemoryConnection, connections } from './connections.js' +import type { MemoryTransportComponents, MemoryTransportInit } from './index.js' +import type { Listener, CreateListenerOptions, ListenerEvents, MultiaddrConnection, UpgraderOptions } from '@libp2p/interface' +import type { Multiaddr } from '@multiformats/multiaddr' + +export interface MemoryTransportListenerInit extends CreateListenerOptions, MemoryTransportInit { + upgraderOptions?: UpgraderOptions +} + +export class MemoryTransportListener extends TypedEventEmitter implements Listener { + private listenAddr?: Multiaddr + private connection?: MemoryConnection + private readonly components: MemoryTransportComponents + private readonly init: MemoryTransportListenerInit + + constructor (components: MemoryTransportComponents, init: MemoryTransportListenerInit) { + super() + + this.components = components + this.init = init + } + + async listen (ma: Multiaddr): Promise { + const [[, value]] = ma.stringTuples() + + const address = `/memory/${value ?? nanoid()}` + + if (value != null && connections.has(address)) { + throw new ListenError(`Memory address ${address} already in use`) + } + + this.connection = new MemoryConnection(this.components, { + onConnection: this.onConnection.bind(this), + address + }) + this.listenAddr = multiaddr(address) + + connections.set(address, this.connection) + + queueMicrotask(() => { + this.safeDispatchEvent('listening') + }) + } + + onConnection (maConn: MultiaddrConnection): void { + let signal: AbortSignal | undefined + + if (this.init.inboundUpgradeTimeout != null) { + signal = AbortSignal.timeout(this.init.inboundUpgradeTimeout) + } + + this.init.upgrader.upgradeInbound(maConn, { + ...this.init.upgraderOptions, + signal + }) + .then(connection => { + this.init.handler?.(connection) + this.safeDispatchEvent('connection', { + detail: connection + }) + }) + .catch(err => { + maConn.abort(err) + }) + } + + getAddrs (): Multiaddr[] { + if (this.listenAddr == null) { + return [] + } + + return [ + this.listenAddr + ] + } + + async close (): Promise { + this.connection?.close() + + if (this.listenAddr != null) { + connections.delete(this.listenAddr.toString()) + } + + queueMicrotask(() => { + this.safeDispatchEvent('close') + }) + } +} diff --git a/packages/transport-memory/src/memory.ts b/packages/transport-memory/src/memory.ts new file mode 100644 index 0000000000..315509cba5 --- /dev/null +++ b/packages/transport-memory/src/memory.ts @@ -0,0 +1,113 @@ +/** + * @packageDocumentation + * + * A [libp2p transport](https://docs.libp2p.io/concepts/transports/overview/) + * that operates in-memory only. + * + * This is intended for testing and can only be used to connect two libp2p nodes + * that are running in the same process. + * + * @example + * + * ```TypeScript + * import { createLibp2p } from 'libp2p' + * import { memory } from '@libp2p/memory' + * import { multiaddr } from '@multiformats/multiaddr' + * + * const listener = await createLibp2p({ + * addresses: { + * listen: [ + * '/memory/node-a' + * ] + * }, + * transports: [ + * memory() + * ] + * }) + * + * const dialer = await createLibp2p({ + * transports: [ + * memory() + * ] + * }) + * + * const ma = multiaddr('/memory/node-a') + * + * // dial the listener, timing out after 10s + * const connection = await dialer.dial(ma, { + * signal: AbortSignal.timeout(10_000) + * }) + * + * // use connection... + * ``` + */ + +import { ConnectionFailedError, serviceCapabilities, transportSymbol } from '@libp2p/interface' +import { Memory } from '@multiformats/multiaddr-matcher' +import { connections } from './connections.js' +import { MemoryTransportListener } from './listener.js' +import type { MemoryTransportComponents, MemoryTransportInit } from './index.js' +import type { Connection, Transport, Listener, CreateListenerOptions, DialTransportOptions } from '@libp2p/interface' +import type { Multiaddr } from '@multiformats/multiaddr' + +export class MemoryTransport implements Transport { + private readonly components: MemoryTransportComponents + private readonly init: MemoryTransportInit + + constructor (components: MemoryTransportComponents, init: MemoryTransportInit = {}) { + this.components = components + this.init = init + } + + readonly [transportSymbol] = true + + readonly [Symbol.toStringTag] = '@libp2p/memory' + + readonly [serviceCapabilities]: string[] = [ + '@libp2p/transport' + ] + + async dial (ma: Multiaddr, options: DialTransportOptions): Promise { + options.signal?.throwIfAborted() + + const memoryConnection = connections.get(`${ma.getPeerId() == null ? ma : ma.decapsulate('/p2p')}`) + + if (memoryConnection == null) { + throw new ConnectionFailedError(`No memory listener found at ${ma}`) + } + + const maConn = await memoryConnection.dial(this.components.peerId) + + try { + options.signal?.throwIfAborted() + + return await options.upgrader.upgradeOutbound(maConn, { + ...options, + ...this.init.upgraderOptions + }) + } catch (err: any) { + maConn.abort(err) + throw err + } + } + + /** + * Creates a TCP listener. The provided `handler` function will be called + * anytime a new incoming Connection has been successfully upgraded via + * `upgrader.upgradeInbound`. + */ + createListener (options: CreateListenerOptions): Listener { + return new MemoryTransportListener(this.components, { + ...options, + ...this.init + }) + } + + listenFilter (multiaddrs: Multiaddr[]): Multiaddr[] { + return multiaddrs.filter(ma => Memory.exactMatch(ma)) + } + + dialFilter (multiaddrs: Multiaddr[]): Multiaddr[] { + return this.listenFilter(multiaddrs) + } +} diff --git a/packages/transport-memory/test/compliance.spec.ts b/packages/transport-memory/test/compliance.spec.ts new file mode 100644 index 0000000000..6620843256 --- /dev/null +++ b/packages/transport-memory/test/compliance.spec.ts @@ -0,0 +1,69 @@ +import { generateKeyPair } from '@libp2p/crypto/keys' +import tests from '@libp2p/interface-compliance-tests/transport' +import { defaultLogger } from '@libp2p/logger' +import { peerIdFromPrivateKey } from '@libp2p/peer-id' +import { multiaddr } from '@multiformats/multiaddr' +import { connections } from '../src/connections.js' +import { memory } from '../src/index.js' +import type { MemoryTransportListener } from '../src/listener.js' +import type { Listener } from '@libp2p/interface' + +describe('transport compliance tests', () => { + tests({ + async setup () { + const privateKey = await generateKeyPair('Ed25519') + + const transport = memory()({ + logger: defaultLogger(), + peerId: peerIdFromPrivateKey(privateKey) + }) + const addrs = [ + multiaddr('/memory/addr-1'), + multiaddr('/memory/addr-2'), + multiaddr('/memory/addr-3'), + multiaddr('/memory/addr-4') + ] + + let delayMs = 0 + const delayedCreateListener = (options: any): Listener => { + const listener = transport.createListener(options) as unknown as MemoryTransportListener + + const onConnection = listener.onConnection.bind(listener) + + listener.onConnection = (maConn: any) => { + setTimeout(() => { + onConnection(maConn) + }, delayMs) + } + + return listener + } + + const transportProxy = new Proxy(transport, { + // @ts-expect-error cannot access props with a string + get: (_, prop) => prop === 'createListener' ? delayedCreateListener : transport[prop] + }) + + // Used by the dial tests to simulate a delayed connect + const connector = { + delay (ms: number) { + delayMs = ms + connections.get('/memory/addr-1')?.setLatency(ms) + connections.get('/memory/addr-2')?.setLatency(ms) + connections.get('/memory/addr-3')?.setLatency(ms) + connections.get('/memory/addr-4')?.setLatency(ms) + }, + restore () { + delayMs = 0 + connections.get('/memory/addr-1')?.setLatency(0) + connections.get('/memory/addr-2')?.setLatency(0) + connections.get('/memory/addr-3')?.setLatency(0) + connections.get('/memory/addr-4')?.setLatency(0) + } + } + + return { dialer: transportProxy, listener: transportProxy, listenAddrs: addrs, dialAddrs: addrs, connector } + }, + async teardown () {} + }) +}) diff --git a/packages/transport-memory/tsconfig.json b/packages/transport-memory/tsconfig.json new file mode 100644 index 0000000000..85ad88030d --- /dev/null +++ b/packages/transport-memory/tsconfig.json @@ -0,0 +1,27 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": [ + "src", + "test" + ], + "references": [ + { + "path": "../crypto" + }, + { + "path": "../interface" + }, + { + "path": "../interface-compliance-tests" + }, + { + "path": "../logger" + }, + { + "path": "../peer-id" + } + ] +} diff --git a/packages/transport-memory/typedoc.json b/packages/transport-memory/typedoc.json new file mode 100644 index 0000000000..f599dc728d --- /dev/null +++ b/packages/transport-memory/typedoc.json @@ -0,0 +1,5 @@ +{ + "entryPoints": [ + "./src/index.ts" + ] +} From 58ca04449a78d0f70d4a7e7954688b38566c559d Mon Sep 17 00:00:00 2001 From: Alex Potsides Date: Tue, 5 Nov 2024 11:49:52 +0000 Subject: [PATCH 3/5] chore: convert node-specific tests to run in browsers (#2803) Converts tests to use the memory transport to allow running all tests in browsers. Removes dependencies on WebSockets, TCP, circuit relay etc so changes in these modules don't cause additional releases here. --- packages/libp2p/.aegir.js | 57 +- packages/libp2p/package.json | 21 +- packages/libp2p/src/upgrader.ts | 2 +- .../{addresses.node.ts => addresses.spec.ts} | 227 ++-- packages/libp2p/test/addresses/utils.ts | 10 - .../connection-gater.spec.ts | 160 +++ .../connection-manager/dial-queue.spec.ts | 22 +- .../test/connection-manager/direct.node.ts | 728 ----------- .../test/connection-manager/direct.spec.ts | 521 -------- .../test/connection-manager/index.node.ts | 505 -------- .../test/connection-manager/index.spec.ts | 262 ++-- .../test/connection-manager/resolver.spec.ts | 145 +-- .../test/core/consume-peer-record.spec.ts | 11 +- packages/libp2p/test/core/core.spec.ts | 36 +- packages/libp2p/test/core/encryption.spec.ts | 20 - packages/libp2p/test/core/events.spec.ts | 19 +- .../{listening.node.ts => listening.spec.ts} | 28 +- packages/libp2p/test/core/peer-id.spec.ts | 11 +- .../core/{status.node.ts => status.spec.ts} | 18 +- .../test/fixtures/base-options.browser.ts | 34 - packages/libp2p/test/fixtures/base-options.ts | 38 - packages/libp2p/test/fixtures/create-peers.ts | 68 + .../libp2p/test/fixtures/creators/peer.ts | 58 - packages/libp2p/test/fixtures/echo-service.ts | 42 - packages/libp2p/test/fixtures/slow-muxer.ts | 29 + packages/libp2p/test/registrar/errors.spec.ts | 6 +- .../libp2p/test/registrar/protocols.spec.ts | 14 - .../libp2p/test/registrar/registrar.spec.ts | 15 +- .../test/transports/transport-manager.node.ts | 143 --- .../test/transports/transport-manager.spec.ts | 151 ++- .../libp2p/test/upgrading/upgrader.spec.ts | 1132 ++++------------- 31 files changed, 981 insertions(+), 3552 deletions(-) rename packages/libp2p/test/addresses/{addresses.node.ts => addresses.spec.ts} (56%) delete mode 100644 packages/libp2p/test/addresses/utils.ts create mode 100644 packages/libp2p/test/connection-manager/connection-gater.spec.ts delete mode 100644 packages/libp2p/test/connection-manager/direct.node.ts delete mode 100644 packages/libp2p/test/connection-manager/direct.spec.ts delete mode 100644 packages/libp2p/test/connection-manager/index.node.ts delete mode 100644 packages/libp2p/test/core/encryption.spec.ts rename packages/libp2p/test/core/{listening.node.ts => listening.spec.ts} (58%) rename packages/libp2p/test/core/{status.node.ts => status.spec.ts} (69%) delete mode 100644 packages/libp2p/test/fixtures/base-options.browser.ts delete mode 100644 packages/libp2p/test/fixtures/base-options.ts create mode 100644 packages/libp2p/test/fixtures/create-peers.ts delete mode 100644 packages/libp2p/test/fixtures/creators/peer.ts delete mode 100644 packages/libp2p/test/fixtures/echo-service.ts create mode 100644 packages/libp2p/test/fixtures/slow-muxer.ts delete mode 100644 packages/libp2p/test/transports/transport-manager.node.ts 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') }) }) From d30d07e6ff1b2825338a767f80c5dc14ae7fa3cd Mon Sep 17 00:00:00 2001 From: Alex Potsides Date: Tue, 5 Nov 2024 13:35:53 +0000 Subject: [PATCH 4/5] chore: remove interface tests dep from libp2p (#2804) Use sinon to stub module behaviour instead of using mock implementations --- packages/libp2p/package.json | 1 - packages/libp2p/src/registrar.ts | 3 +- .../connection-pruner.spec.ts | 258 ++++++++++++- .../test/connection-manager/index.spec.ts | 350 +++--------------- .../libp2p/test/connection/compliance.spec.ts | 76 ---- packages/libp2p/test/connection/index.spec.ts | 197 +++++++++- .../libp2p/test/registrar/registrar.spec.ts | 86 ++--- .../libp2p/test/upgrading/upgrader.spec.ts | 2 +- 8 files changed, 537 insertions(+), 436 deletions(-) delete mode 100644 packages/libp2p/test/connection/compliance.spec.ts diff --git a/packages/libp2p/package.json b/packages/libp2p/package.json index 007173c4a9..a3e9406472 100644 --- a/packages/libp2p/package.json +++ b/packages/libp2p/package.json @@ -115,7 +115,6 @@ "devDependencies": { "@chainsafe/libp2p-yamux": "^7.0.0", "@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", diff --git a/packages/libp2p/src/registrar.ts b/packages/libp2p/src/registrar.ts index 91d2fe9b83..ad50a83f69 100644 --- a/packages/libp2p/src/registrar.ts +++ b/packages/libp2p/src/registrar.ts @@ -2,7 +2,7 @@ import { InvalidParametersError } from '@libp2p/interface' import merge from 'merge-options' import * as errorsJs from './errors.js' import type { IdentifyResult, Libp2pEvents, Logger, PeerUpdate, TypedEventTarget, PeerId, PeerStore, Topology } from '@libp2p/interface' -import type { ConnectionManager, StreamHandlerOptions, StreamHandlerRecord, Registrar, StreamHandler } from '@libp2p/interface-internal' +import type { StreamHandlerOptions, StreamHandlerRecord, Registrar, StreamHandler } from '@libp2p/interface-internal' import type { ComponentLogger } from '@libp2p/logger' export const DEFAULT_MAX_INBOUND_STREAMS = 32 @@ -10,7 +10,6 @@ export const DEFAULT_MAX_OUTBOUND_STREAMS = 64 export interface RegistrarComponents { peerId: PeerId - connectionManager: ConnectionManager peerStore: PeerStore events: TypedEventTarget logger: ComponentLogger diff --git a/packages/libp2p/test/connection-manager/connection-pruner.spec.ts b/packages/libp2p/test/connection-manager/connection-pruner.spec.ts index fbb2a68de6..8f93fae1f4 100644 --- a/packages/libp2p/test/connection-manager/connection-pruner.spec.ts +++ b/packages/libp2p/test/connection-manager/connection-pruner.spec.ts @@ -1,12 +1,15 @@ import { generateKeyPair } from '@libp2p/crypto/keys' -import { TypedEventEmitter } from '@libp2p/interface' +import { TypedEventEmitter, stop } from '@libp2p/interface' import { defaultLogger } from '@libp2p/logger' import { PeerMap } from '@libp2p/peer-collections' import { peerIdFromPrivateKey } from '@libp2p/peer-id' +import { multiaddr } from '@multiformats/multiaddr' import { expect } from 'aegir/chai' +import { pEvent } from 'p-event' +import Sinon from 'sinon' import { stubInterface } from 'sinon-ts' import { ConnectionPruner } from '../../src/connection-manager/connection-pruner.js' -import type { Libp2pEvents, PeerStore, Stream, TypedEventTarget, Connection } from '@libp2p/interface' +import type { Libp2pEvents, PeerStore, Stream, TypedEventTarget, Connection, AbortOptions, ComponentLogger, Peer } from '@libp2p/interface' import type { ConnectionManager } from '@libp2p/interface-internal' import type { StubbedInstance } from 'sinon-ts' @@ -14,6 +17,7 @@ interface ConnectionPrunerComponents { connectionManager: StubbedInstance peerStore: StubbedInstance events: TypedEventTarget + logger: ComponentLogger } describe('connection-pruner', () => { @@ -24,13 +28,15 @@ describe('connection-pruner', () => { components = { connectionManager: stubInterface(), peerStore: stubInterface(), - events: new TypedEventEmitter() + events: new TypedEventEmitter(), + logger: defaultLogger() } - pruner = new ConnectionPruner({ - ...components, - logger: defaultLogger() - }, {}) + pruner = new ConnectionPruner(components) + }) + + afterEach(async () => { + await stop(pruner) }) it('should sort connections for pruning, closing connections without streams first unless they are tagged', async () => { @@ -96,4 +102,242 @@ describe('connection-pruner', () => { 'tagged-streams-outbound-old' ]) }) + + it('should close connections with low tag values first', async () => { + const max = 5 + pruner = new ConnectionPruner(components, { + maxConnections: max + }) + pruner.start() + + const connections: Connection[] = [] + components.connectionManager.getConnections.returns(connections) + + const spies = new Map>>() + + // wait for prune event + const eventPromise = pEvent(components.events, 'connection:prune') + + // Add 1 connection too many + for (let i = 0; i < max + 1; i++) { + const connection = stubInterface({ + remotePeer: peerIdFromPrivateKey(await generateKeyPair('Ed25519')), + streams: [] + }) + const spy = connection.close + + const value = i * 10 + spies.set(value, spy) + components.peerStore.get.withArgs(connection.remotePeer).resolves(stubInterface({ + tags: new Map([['test-tag', { value }]]) + })) + + connections.push(connection) + + components.events.safeDispatchEvent('connection:open', { + detail: connection + }) + } + + await eventPromise + + // get the lowest value + const lowest = Array.from(spies.keys()).sort((a, b) => { + if (a > b) { + return 1 + } + + if (a < b) { + return -1 + } + + return 0 + })[0] + + expect(spies.get(lowest)).to.have.property('callCount', 1) + }) + + it('should close shortest-lived connection if the tag values are equal', async () => { + const max = 5 + pruner = new ConnectionPruner(components, { + maxConnections: max + }) + pruner.start() + + const connections: Connection[] = [] + components.connectionManager.getConnections.returns(connections) + + const spies = new Map>>() + const eventPromise = pEvent(components.events, 'connection:prune') + + const createConnection = async (value: number, open: number = Date.now(), peerTag: string = 'test-tag'): Promise => { + // #TODO: Mock the connection timeline to simulate an older connection + const connection = stubInterface({ + remotePeer: peerIdFromPrivateKey(await generateKeyPair('Ed25519')), + streams: [], + timeline: { + open + } + }) + const spy = connection.close + + // The lowest tag value will have the longest connection + spies.set(peerTag, spy) + components.peerStore.get.withArgs(connection.remotePeer).resolves(stubInterface({ + tags: new Map([['test-tag', { value }]]) + })) + + connections.push(connection) + + components.events.safeDispatchEvent('connection:open', { + detail: connection + }) + } + + // Create one short of enough connections to initiate pruning + for (let i = 1; i < max; i++) { + const value = i * 10 + await createConnection(value) + } + + const value = 0 * 10 + // Add a connection with the lowest tag value BUT the longest lived connection + await createConnection(value, 18000, 'longest') + // Add one more connection with the lowest tag value BUT the shortest-lived connection + await createConnection(value, Date.now(), 'shortest') + + // wait for prune event + await eventPromise + + // get the lowest tagged value, but this would be also the longest lived connection + const longestLivedWithLowestTagSpy = spies.get('longest') + + // Get lowest tagged connection but with a shorter-lived connection + const shortestLivedWithLowestTagSpy = spies.get('shortest') + + expect(longestLivedWithLowestTagSpy).to.have.property('callCount', 0) + expect(shortestLivedWithLowestTagSpy).to.have.property('callCount', 1) + }) + + it('should not close connection that is on the allowlist when pruning', async () => { + const max = 2 + const remoteAddr = multiaddr('/ip4/83.13.55.32/tcp/59283') + + pruner = new ConnectionPruner(components, { + maxConnections: max, + allow: [ + multiaddr('/ip4/83.13.55.32') + ] + }) + pruner.start() + + const connections: Connection[] = [] + components.connectionManager.getConnections.returns(connections) + + const spies = new Map>>() + const eventPromise = pEvent(components.events, 'connection:prune') + + // Max out connections + for (let i = 0; i < max; i++) { + const connection = stubInterface({ + remotePeer: peerIdFromPrivateKey(await generateKeyPair('Ed25519')), + streams: [] + }) + const spy = connection.close + + const value = (i + 1) * 10 + spies.set(value, spy) + components.peerStore.get.withArgs(connection.remotePeer).resolves(stubInterface({ + tags: new Map([['test-tag', { value }]]) + })) + + connections.push(connection) + + components.events.safeDispatchEvent('connection:open', { + detail: connection + }) + } + + // an outbound connection is opened from an address in the allow list + const remotePeer = peerIdFromPrivateKey(await generateKeyPair('Ed25519')) + const connection = stubInterface({ + remotePeer, + remoteAddr, + streams: [] + }) + + const value = 0 + const spy = connection.close + spies.set(value, spy) + + // Tag that allowed peer with lowest value + components.peerStore.get.withArgs(connection.remotePeer).resolves(stubInterface({ + tags: new Map([['test-tag', { value }]]) + })) + + connections.push(connection) + + components.events.safeDispatchEvent('connection:open', { + detail: connection + }) + + // wait for prune event + await eventPromise + + // get the lowest value + const lowest = Array.from(spies.keys()).sort((a, b) => { + if (a > b) { + return 1 + } + + if (a < b) { + return -1 + } + + return 0 + })[0] + const lowestSpy = spies.get(lowest) + + // expect lowest value spy NOT to be called since the peer is in the allow list + expect(lowestSpy).to.have.property('callCount', 0) + }) + + it('should close connection when the maximum connections has been reached even without tags', async () => { + const max = 5 + pruner = new ConnectionPruner(components, { + maxConnections: max + }) + pruner.start() + + const connections: Connection[] = [] + components.connectionManager.getConnections.returns(connections) + + const eventPromise = pEvent(components.events, 'connection:prune') + + // Add 1 too many connections + const spy = Sinon.spy() + for (let i = 0; i < max + 1; i++) { + const connection = stubInterface({ + remotePeer: peerIdFromPrivateKey(await generateKeyPair('Ed25519')), + streams: [], + limits: undefined, + close: spy + }) + + components.peerStore.get.withArgs(connection.remotePeer).resolves(stubInterface({ + tags: new Map() + })) + + connections.push(connection) + + components.events.safeDispatchEvent('connection:open', { + detail: connection + }) + } + + // wait for prune event + await eventPromise + + expect(spy).to.have.property('callCount', 1) + }) }) diff --git a/packages/libp2p/test/connection-manager/index.spec.ts b/packages/libp2p/test/connection-manager/index.spec.ts index feda255d96..629c2a9d7a 100644 --- a/packages/libp2p/test/connection-manager/index.spec.ts +++ b/packages/libp2p/test/connection-manager/index.spec.ts @@ -2,13 +2,11 @@ import { generateKeyPair } from '@libp2p/crypto/keys' 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' @@ -18,7 +16,7 @@ import { createLibp2p } from '../../src/index.js' import { createPeers } from '../fixtures/create-peers.js' import { getComponent } from '../fixtures/get-component.js' import type { Echo } from '@libp2p/echo' -import type { ConnectionGater, PeerId, PeerStore, Libp2p, AbortOptions, Connection, PeerRouting } from '@libp2p/interface' +import type { ConnectionGater, PeerId, PeerStore, Libp2p, Connection, PeerRouting, MultiaddrConnection } from '@libp2p/interface' import type { TransportManager } from '@libp2p/interface-internal' const defaultOptions = { @@ -29,7 +27,9 @@ const defaultOptions = { function createDefaultComponents (peerId: PeerId): DefaultConnectionManagerComponents { return { peerId, - peerStore: stubInterface(), + peerStore: stubInterface({ + all: async () => [] + }), peerRouting: stubInterface(), transportManager: stubInterface(), connectionGater: stubInterface(), @@ -42,252 +42,12 @@ describe('Connection Manager', () => { let libp2p: Libp2p let connectionManager: DefaultConnectionManager - afterEach(async () => { - await stop(connectionManager, libp2p) - }) - - it('should be able to create without metrics', async () => { - libp2p = await createLibp2p({ - start: false - }) - - const spy = sinon.spy(getComponent(libp2p, 'connectionManager'), 'start') - - await libp2p.start() - expect(spy).to.have.property('callCount', 1) - expect(libp2p.metrics).to.not.exist() - }) - - it('should be able to create with metrics', async () => { - libp2p = await createLibp2p({ - start: false, - metrics: mockMetrics() - }) - - const spy = sinon.spy(getComponent(libp2p, 'connectionManager'), 'start') - - await libp2p.start() - expect(spy).to.have.property('callCount', 1) - expect(libp2p.metrics).to.exist() - }) - - it('should close connections with low tag values first', async () => { - const max = 5 - libp2p = await createLibp2p({ - connectionManager: { - maxConnections: max - } - }) - - const connectionManager = getComponent(libp2p, 'connectionManager') - const connectionManagerMaybePruneConnectionsSpy = sinon.spy(connectionManager.connectionPruner, '_maybePruneConnections') - const spies = new Map>>() - - // wait for prune event - const eventPromise = pEvent(libp2p, 'connection:prune') - - // Add 1 connection too many - for (let i = 0; i < max + 1; i++) { - const connection = mockConnection(mockMultiaddrConnection(mockDuplex(), peerIdFromPrivateKey(await generateKeyPair('Ed25519')))) - const spy = sinon.spy(connection, 'close') - - const value = i * 10 - spies.set(value, spy) - await libp2p.peerStore.merge(connection.remotePeer, { - tags: { - 'test-tag': { - value - } - } - }) - - getComponent(libp2p, 'events').safeDispatchEvent('connection:open', { detail: connection }) - } - - await eventPromise - - // get the lowest value - const lowest = Array.from(spies.keys()).sort((a, b) => { - if (a > b) { - return 1 - } - - if (a < b) { - return -1 - } - - return 0 - })[0] - const lowestSpy = spies.get(lowest) - - expect(connectionManagerMaybePruneConnectionsSpy.callCount).to.equal(6) - expect(lowestSpy).to.have.property('callCount', 1) - }) - - it('should close shortest-lived connection if the tag values are equal', async () => { - const max = 5 - libp2p = await createLibp2p({ - connectionManager: { - maxConnections: max - } - }) - - const connectionManager = getComponent(libp2p, 'connectionManager') - const connectionManagerMaybePruneConnectionsSpy = sinon.spy(connectionManager.connectionPruner, '_maybePruneConnections') - const spies = new Map>>() - const eventPromise = pEvent(libp2p, 'connection:prune') - - const createConnection = async (value: number, open: number = Date.now(), peerTag: string = 'test-tag'): Promise => { - // #TODO: Mock the connection timeline to simulate an older connection - const connection = mockConnection(mockMultiaddrConnection({ ...mockDuplex(), timeline: { open } }, peerIdFromPrivateKey(await generateKeyPair('Ed25519')))) - const spy = sinon.spy(connection, 'close') - - // The lowest tag value will have the longest connection - spies.set(peerTag, spy) - await libp2p.peerStore.merge(connection.remotePeer, { - tags: { - [peerTag]: { - value - } - } - }) - - getComponent(libp2p, 'events').safeDispatchEvent('connection:open', { detail: connection }) - } - - // Create one short of enough connections to initiate pruning - for (let i = 1; i < max; i++) { - const value = i * 10 - await createConnection(value) - } - - const value = 0 * 10 - // Add a connection with the lowest tag value BUT the longest lived connection - await createConnection(value, 18000, 'longest') - // Add one more connection with the lowest tag value BUT the shortest-lived connection - await createConnection(value, Date.now(), 'shortest') - - // wait for prune event - await eventPromise - - // get the lowest tagged value, but this would be also the longest lived connection - const longestLivedWithLowestTagSpy = spies.get('longest') - - // Get lowest tagged connection but with a shorter-lived connection - const shortestLivedWithLowestTagSpy = spies.get('shortest') - - expect(connectionManagerMaybePruneConnectionsSpy.callCount).to.equal(6) - expect(longestLivedWithLowestTagSpy).to.have.property('callCount', 0) - expect(shortestLivedWithLowestTagSpy).to.have.property('callCount', 1) + beforeEach(async () => { + libp2p = await createLibp2p() }) - it('should not close connection that is on the allowlist when pruning', async () => { - const max = 2 - const remoteAddr = multiaddr('/ip4/83.13.55.32/tcp/59283') - - libp2p = await createLibp2p({ - connectionManager: { - maxConnections: max, - allow: [ - '/ip4/83.13.55.32' - ] - } - }) - - const connectionManager = getComponent(libp2p, 'connectionManager') - const connectionManagerMaybePruneConnectionsSpy = sinon.spy(connectionManager.connectionPruner, '_maybePruneConnections') - const spies = new Map>>() - const eventPromise = pEvent(libp2p, 'connection:prune') - - // Max out connections - for (let i = 0; i < max; i++) { - const connection = mockConnection(mockMultiaddrConnection(mockDuplex(), peerIdFromPrivateKey(await generateKeyPair('Ed25519')))) - const spy = sinon.spy(connection, 'close') - const value = (i + 1) * 10 - spies.set(value, spy) - await libp2p.peerStore.merge(connection.remotePeer, { - tags: { - 'test-tag': { - value - } - } - }) - getComponent(libp2p, 'events').safeDispatchEvent('connection:open', { detail: connection }) - } - - // an outbound connection is opened from an address in the allow list - const remotePeer = peerIdFromPrivateKey(await generateKeyPair('Ed25519')) - const connection = mockConnection(mockMultiaddrConnection({ - remoteAddr, - source: (async function * () { - yield * [] - })(), - sink: async () => {} - }, remotePeer)) - - const value = 0 - const spy = sinon.spy(connection, 'close') - spies.set(value, spy) - - // Tag that allowed peer with lowest value - await libp2p.peerStore.merge(connection.remotePeer, { - tags: { - 'test-tag': { - value - } - } - }) - - getComponent(libp2p, 'events').safeDispatchEvent('connection:open', { detail: connection }) - - // wait for prune event - await eventPromise - - // get the lowest value - const lowest = Array.from(spies.keys()).sort((a, b) => { - if (a > b) { - return 1 - } - - if (a < b) { - return -1 - } - - return 0 - })[0] - const lowestSpy = spies.get(lowest) - - expect(connectionManagerMaybePruneConnectionsSpy.callCount).to.equal(3) - // expect lowest value spy NOT to be called since the peer is in the allow list - expect(lowestSpy).to.have.property('callCount', 0) - }) - - it('should close connection when the maximum connections has been reached even without tags', async () => { - const max = 5 - libp2p = await createLibp2p({ - connectionManager: { - maxConnections: max - } - }) - - const connectionManager = getComponent(libp2p, 'connectionManager') - const connectionManagerMaybePruneConnectionsSpy = sinon.spy(connectionManager.connectionPruner, '_maybePruneConnections') - const eventPromise = pEvent(libp2p, 'connection:prune') - - // Add 1 too many connections - const spy = sinon.spy() - for (let i = 0; i < max + 1; i++) { - const connection = mockConnection(mockMultiaddrConnection(mockDuplex(), peerIdFromPrivateKey(await generateKeyPair('Ed25519')))) - sinon.stub(connection, 'close').callsFake(async () => spy()) // eslint-disable-line - getComponent(libp2p, 'events').safeDispatchEvent('connection:open', { detail: connection }) - } - - // wait for prune event - await eventPromise - - expect(connectionManagerMaybePruneConnectionsSpy.callCount).to.equal(6) - - expect(spy).to.have.property('callCount', 1) + afterEach(async () => { + await stop(connectionManager, libp2p) }) it('should fail if the connection manager has mismatched connection limit options', async () => { @@ -303,9 +63,7 @@ describe('Connection Manager', () => { it('should reconnect to important peers on startup', async () => { const peerId = peerIdFromPrivateKey(await generateKeyPair('Ed25519')) - libp2p = await createLibp2p({ - start: false - }) + await libp2p.stop() const connectionManager = getComponent(libp2p, 'connectionManager') const connectionManagerOpenConnectionSpy = sinon.spy(connectionManager, 'openConnection') @@ -341,14 +99,9 @@ describe('Connection Manager', () => { }) await connectionManager.start() - const remotePeer = peerIdFromPrivateKey(await generateKeyPair('Ed25519')) - const maConn = mockMultiaddrConnection({ - remoteAddr, - source: (async function * () { - yield * [] - })(), - sink: async () => {} - }, remotePeer) + const maConn = stubInterface({ + remoteAddr + }) await expect(connectionManager.acceptIncomingConnection(maConn)) .to.eventually.be.false() @@ -371,13 +124,9 @@ describe('Connection Manager', () => { expect(connectionManager.getConnections()).to.have.lengthOf(1) // an inbound connection is opened - const remotePeer = peerIdFromPrivateKey(await generateKeyPair('Ed25519')) - const maConn = mockMultiaddrConnection({ - source: (async function * () { - yield * [] - })(), - sink: async () => {} - }, remotePeer) + const maConn = stubInterface({ + remoteAddr: multiaddr('/ip4/83.13.55.32/tcp/59283') + }) await expect(connectionManager.acceptIncomingConnection(maConn)) .to.eventually.be.false() @@ -395,16 +144,11 @@ describe('Connection Manager', () => { })) // an inbound connection is opened - const remotePeer = peerIdFromPrivateKey(await generateKeyPair('Ed25519')) - const maConn = mockMultiaddrConnection({ - source: (async function * () { - yield * [] - })(), - sink: async () => {}, + const maConn = stubInterface({ // has to be thin waist, which it will be since we've not done the peer id handshake // yet in the code being exercised by this test remoteAddr: multiaddr('/ip4/34.4.63.125/tcp/4001') - }, remotePeer) + }) await expect(connectionManager.acceptIncomingConnection(maConn)) .to.eventually.be.true() @@ -435,14 +179,9 @@ describe('Connection Manager', () => { expect(connectionManager.getConnections()).to.have.lengthOf(1) // an inbound connection is opened from an address in the allow list - const remotePeer = peerIdFromPrivateKey(await generateKeyPair('Ed25519')) - const maConn = mockMultiaddrConnection({ - remoteAddr, - source: (async function * () { - yield * [] - })(), - sink: async () => {} - }, remotePeer) + const maConn = stubInterface({ + remoteAddr + }) await expect(connectionManager.acceptIncomingConnection(maConn)) .to.eventually.be.true() @@ -461,23 +200,17 @@ describe('Connection Manager', () => { })) // start the upgrade - const maConn1 = mockMultiaddrConnection({ - source: (async function * () { - yield * [] - })(), - sink: async () => {} - }, peerIdFromPrivateKey(await generateKeyPair('Ed25519'))) + const maConn1 = stubInterface({ + remoteAddr: multiaddr('/ip4/34.4.63.125/tcp/4001') + }) await expect(connectionManager.acceptIncomingConnection(maConn1)) .to.eventually.be.true() // start the upgrade - const maConn2 = mockMultiaddrConnection({ - source: (async function * () { - yield * [] - })(), - sink: async () => {} - }, peerIdFromPrivateKey(await generateKeyPair('Ed25519'))) + const maConn2 = stubInterface({ + remoteAddr: multiaddr('/ip4/34.4.63.126/tcp/4001') + }) // should be false because we have not completed the upgrade of maConn1 await expect(connectionManager.acceptIncomingConnection(maConn2)) @@ -557,8 +290,16 @@ describe('Connection Manager', () => { await start(connectionManager) - const conn1 = mockConnection(mockMultiaddrConnection(mockDuplex(), peerIds[1])) - const conn2 = mockConnection(mockMultiaddrConnection(mockDuplex(), peerIds[1])) + const conn1 = stubInterface({ + remoteAddr: multiaddr('/ip4/34.4.63.125/tcp/4001'), + remotePeer: peerIds[1], + status: 'open' + }) + const conn2 = stubInterface({ + remoteAddr: multiaddr('/ip4/34.4.63.126/tcp/4001'), + remotePeer: peerIds[1], + status: 'open' + }) expect(connectionManager.getConnections(peerIds[1])).to.have.lengthOf(0) @@ -568,12 +309,12 @@ describe('Connection Manager', () => { expect(connectionManager.getConnections(peerIds[1])).to.have.lengthOf(2) - await conn2.close() + conn2.status = 'closed' components.events.safeDispatchEvent('connection:close', { detail: conn2 }) expect(connectionManager.getConnections(peerIds[1])).to.have.lengthOf(1) - expect(conn1).to.have.nested.property('status', 'open') + expect(conn1.close.called).to.be.false() await connectionManager.stop() }) @@ -594,8 +335,16 @@ describe('Connection Manager', () => { await start(connectionManager) - const conn1 = mockConnection(mockMultiaddrConnection(mockDuplex(), peerIds[1])) - const conn2 = mockConnection(mockMultiaddrConnection(mockDuplex(), peerIds[1])) + const conn1 = stubInterface({ + remoteAddr: multiaddr('/ip4/34.4.63.125/tcp/4001'), + remotePeer: peerIds[1], + status: 'open' + }) + const conn2 = stubInterface({ + remoteAddr: multiaddr('/ip4/34.4.63.126/tcp/4001'), + remotePeer: peerIds[1], + status: 'open' + }) // Add connection to the connectionManager components.events.safeDispatchEvent('connection:open', { detail: conn1 }) @@ -605,6 +354,9 @@ describe('Connection Manager', () => { await connectionManager.stop() + expect(conn1.close.called).to.be.true() + expect(conn2.close.called).to.be.true() + expect(connectionManager.getConnections(peerIds[1])).to.have.lengthOf(0) }) }) diff --git a/packages/libp2p/test/connection/compliance.spec.ts b/packages/libp2p/test/connection/compliance.spec.ts deleted file mode 100644 index 5c1e3477d2..0000000000 --- a/packages/libp2p/test/connection/compliance.spec.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { generateKeyPair } from '@libp2p/crypto/keys' -import tests from '@libp2p/interface-compliance-tests/connection' -import { logger, peerLogger } from '@libp2p/logger' -import { peerIdFromPrivateKey } from '@libp2p/peer-id' -import { multiaddr } from '@multiformats/multiaddr' -import { createConnection } from '../../src/connection/index.js' -import { pair } from './fixtures/pair.js' -import type { Stream } from '@libp2p/interface' - -describe('connection compliance', () => { - tests({ - /** - * Test setup. `properties` allows the compliance test to override - * certain values for testing. - */ - async setup (properties) { - const localPeer = peerIdFromPrivateKey(await generateKeyPair('Ed25519')) - const remoteAddr = multiaddr('/ip4/127.0.0.1/tcp/8081') - const remotePeer = peerIdFromPrivateKey(await generateKeyPair('Ed25519')) - let openStreams: Stream[] = [] - let streamId = 0 - - const connection = createConnection({ - remotePeer, - remoteAddr, - timeline: { - open: Date.now() - 10, - upgraded: Date.now() - }, - direction: 'outbound', - encryption: '/secio/1.0.0', - multiplexer: '/mplex/6.7.0', - status: 'open', - logger: peerLogger(localPeer), - newStream: async (protocols) => { - const id = `${streamId++}` - const stream: Stream = { - ...pair(), - close: async () => { - void stream.sink(async function * () {}()) - openStreams = openStreams.filter(s => s.id !== id) - }, - closeRead: async () => {}, - closeWrite: async () => { - void stream.sink(async function * () {}()) - }, - id, - abort: () => {}, - direction: 'outbound', - protocol: protocols[0], - timeline: { - open: 0 - }, - metadata: {}, - status: 'open', - writeStatus: 'ready', - readStatus: 'ready', - log: logger('test') - } - - openStreams.push(stream) - - return stream - }, - close: async () => {}, - abort: () => {}, - getStreams: () => openStreams, - ...properties - }) - return connection - }, - async teardown () { - // cleanup resources created by setup() - } - }) -}) diff --git a/packages/libp2p/test/connection/index.spec.ts b/packages/libp2p/test/connection/index.spec.ts index ceb3ed14d5..9c8f217414 100644 --- a/packages/libp2p/test/connection/index.spec.ts +++ b/packages/libp2p/test/connection/index.spec.ts @@ -1,10 +1,12 @@ import { generateKeyPair } from '@libp2p/crypto/keys' -import { defaultLogger } from '@libp2p/logger' +import { defaultLogger, logger, peerLogger } from '@libp2p/logger' import { peerIdFromPrivateKey } from '@libp2p/peer-id' import { multiaddr } from '@multiformats/multiaddr' import { expect } from 'aegir/chai' import Sinon from 'sinon' import { createConnection } from '../../src/connection/index.js' +import { pair } from './fixtures/pair.js' +import type { Connection, Stream } from '@libp2p/interface' function defaultConnectionInit (): any { return { @@ -60,3 +62,196 @@ describe('connection', () => { expect(conn.remoteAddr.getPeerId()).to.equal(otherPeer.toString()) }) }) + +describe('compliance', () => { + let connection: Connection + let timelineProxy + const proxyHandler = { + set () { + // @ts-expect-error - TS fails to infer here + return Reflect.set(...arguments) + } + } + + beforeEach(async () => { + const localPeer = peerIdFromPrivateKey(await generateKeyPair('Ed25519')) + const remoteAddr = multiaddr('/ip4/127.0.0.1/tcp/8081') + const remotePeer = peerIdFromPrivateKey(await generateKeyPair('Ed25519')) + let openStreams: Stream[] = [] + let streamId = 0 + + connection = createConnection({ + remotePeer, + remoteAddr, + timeline: { + open: Date.now() - 10, + upgraded: Date.now() + }, + direction: 'outbound', + encryption: '/secio/1.0.0', + multiplexer: '/mplex/6.7.0', + status: 'open', + logger: peerLogger(localPeer), + newStream: async (protocols) => { + const id = `${streamId++}` + const stream: Stream = { + ...pair(), + close: async () => { + void stream.sink(async function * () {}()) + openStreams = openStreams.filter(s => s.id !== id) + }, + closeRead: async () => {}, + closeWrite: async () => { + void stream.sink(async function * () {}()) + }, + id, + abort: () => {}, + direction: 'outbound', + protocol: protocols[0], + timeline: { + open: 0 + }, + metadata: {}, + status: 'open', + writeStatus: 'ready', + readStatus: 'ready', + log: logger('test') + } + + openStreams.push(stream) + + return stream + }, + close: async () => {}, + abort: () => {}, + getStreams: () => openStreams + }) + + timelineProxy = new Proxy({ + open: Date.now() - 10, + upgraded: Date.now() + }, proxyHandler) + + connection.timeline = timelineProxy + }) + + it('should have properties set', () => { + expect(connection.id).to.exist() + expect(connection.remotePeer).to.exist() + expect(connection.remoteAddr).to.exist() + expect(connection.status).to.equal('open') + expect(connection.timeline.open).to.exist() + expect(connection.timeline.close).to.not.exist() + expect(connection.direction).to.exist() + expect(connection.streams).to.eql([]) + expect(connection.tags).to.eql([]) + }) + + it('should get the metadata of an open connection', () => { + expect(connection.status).to.equal('open') + expect(connection.direction).to.exist() + expect(connection.timeline.open).to.exist() + expect(connection.timeline.close).to.not.exist() + }) + + it('should return an empty array of streams', () => { + const streams = connection.streams + + expect(streams).to.eql([]) + }) + + it('should be able to create a new stream', async () => { + expect(connection.streams).to.be.empty() + + const protocolToUse = '/echo/0.0.1' + const stream = await connection.newStream([protocolToUse]) + + expect(stream).to.have.property('protocol', protocolToUse) + + const connStreams = connection.streams + + expect(stream).to.exist() + expect(connStreams).to.exist() + expect(connStreams).to.have.lengthOf(1) + expect(connStreams[0]).to.equal(stream) + }) + + it('should be able to close the connection after being created', async () => { + expect(connection.timeline.close).to.not.exist() + await connection.close() + + expect(connection.timeline.close).to.exist() + expect(connection.status).to.equal('closed') + }) + + it('should be able to close the connection after opening a stream', async () => { + // Open stream + const protocol = '/echo/0.0.1' + await connection.newStream([protocol]) + + // Close connection + expect(connection.timeline.close).to.not.exist() + await connection.close() + + expect(connection.timeline.close).to.exist() + expect(connection.status).to.equal('closed') + }) + + it('should properly track streams', async () => { + // Open stream + const protocol = '/echo/0.0.1' + const stream = await connection.newStream([protocol]) + expect(stream).to.have.property('protocol', protocol) + + // Close stream + await stream.close() + + expect(connection.streams.filter(s => s.id === stream.id)).to.be.empty() + }) + + it('should track outbound streams', async () => { + // Open stream + const protocol = '/echo/0.0.1' + const stream = await connection.newStream(protocol) + expect(stream).to.have.property('direction', 'outbound') + }) + + it('should support a proxy on the timeline', async () => { + Sinon.spy(proxyHandler, 'set') + expect(connection.timeline.close).to.not.exist() + + await connection.close() + // @ts-expect-error - fails to infer callCount + expect(proxyHandler.set.callCount).to.equal(1) + // @ts-expect-error - fails to infer getCall + const [obj, key, value] = proxyHandler.set.getCall(0).args + expect(obj).to.eql(connection.timeline) + expect(key).to.equal('close') + expect(value).to.be.a('number').that.equals(connection.timeline.close) + }) + + it('should fail to create a new stream if the connection is closing', async () => { + expect(connection.timeline.close).to.not.exist() + const p = connection.close() + + try { + const protocol = '/echo/0.0.1' + await connection.newStream([protocol]) + } catch (err: any) { + expect(err).to.exist() + return + } finally { + await p + } + + throw new Error('should fail to create a new stream if the connection is closing') + }) + + it('should fail to create a new stream if the connection is closed', async () => { + expect(connection.timeline.close).to.not.exist() + await connection.close() + + await expect(connection.newStream(['/echo/0.0.1'])).to.eventually.be.rejected + .with.property('name', 'ConnectionClosedError') + }) +}) diff --git a/packages/libp2p/test/registrar/registrar.spec.ts b/packages/libp2p/test/registrar/registrar.spec.ts index c274f1856b..02810dc881 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 { mockDuplex, mockMultiaddrConnection, mockConnection } from '@libp2p/interface-compliance-tests/mocks' import { defaultLogger } from '@libp2p/logger' import { peerFilter } from '@libp2p/peer-collections' import { peerIdFromPrivateKey } from '@libp2p/peer-id' @@ -10,8 +9,8 @@ import { expect } from 'aegir/chai' import pDefer from 'p-defer' import { stubInterface } from 'sinon-ts' import { DefaultRegistrar } from '../../src/registrar.js' -import type { TypedEventTarget, Libp2pEvents, PeerId, PeerStore, Topology, Peer } from '@libp2p/interface' -import type { ConnectionManager, Registrar } from '@libp2p/interface-internal' +import type { TypedEventTarget, Libp2pEvents, PeerId, PeerStore, Topology, Peer, Connection } from '@libp2p/interface' +import type { Registrar } from '@libp2p/interface-internal' import type { StubbedInstance } from 'sinon-ts' const protocol = '/test/1.0.0' @@ -24,19 +23,16 @@ describe('registrar topologies', () => { peerId = peerIdFromPrivateKey(await generateKeyPair('Ed25519')) }) - let connectionManager: StubbedInstance let peerStore: StubbedInstance let events: TypedEventTarget beforeEach(async () => { peerId = peerIdFromPrivateKey(await generateKeyPair('Ed25519')) - connectionManager = stubInterface() peerStore = stubInterface() events = new TypedEventEmitter() registrar = new DefaultRegistrar({ peerId, - connectionManager, peerStore, events, logger: defaultLogger() @@ -84,15 +80,15 @@ describe('registrar topologies', () => { // setup connections before registrar const remotePeerId = peerIdFromPrivateKey(await generateKeyPair('Ed25519')) - const conn = mockConnection(mockMultiaddrConnection(mockDuplex(), remotePeerId)) - - // return connection from connection manager - connectionManager.getConnections.withArgs(remotePeerId).returns([conn]) + const conn = stubInterface({ + remotePeer: remotePeerId, + limits: undefined + }) const topology: Topology = { onConnect: (peerId, connection) => { expect(peerId.equals(remotePeerId)).to.be.true() - expect(connection.id).to.eql(conn.id) + expect(connection).to.equal(conn) onConnectDefer.resolve() }, @@ -139,10 +135,10 @@ describe('registrar topologies', () => { // setup connections before registrar const remotePeerId = peerIdFromPrivateKey(await generateKeyPair('Ed25519')) - const conn = mockConnection(mockMultiaddrConnection(mockDuplex(), remotePeerId)) - - // return connection from connection manager - connectionManager.getConnections.withArgs(remotePeerId).returns([conn]) + const conn = stubInterface({ + remotePeer: remotePeerId, + limits: undefined + }) const topology: Topology = { onConnect: () => { @@ -174,9 +170,6 @@ describe('registrar topologies', () => { tags: new Map() }) - // we have a connection to this peer - connectionManager.getConnections.withArgs(conn.remotePeer).returns([conn]) - // identify completes events.safeDispatchEvent('peer:update', { detail: { @@ -218,15 +211,13 @@ describe('registrar topologies', () => { // setup connections before registrar const remotePeerId = peerIdFromPrivateKey(await generateKeyPair('Ed25519')) - const conn = mockConnection(mockMultiaddrConnection(mockDuplex(), remotePeerId)) - // connection is limited - conn.limits = { - bytes: 100n - } - - // return connection from connection manager - connectionManager.getConnections.withArgs(remotePeerId).returns([conn]) + const conn = stubInterface({ + remotePeer: remotePeerId, + limits: { + bytes: 100n + } + }) const topology: Topology = { onConnect: () => { @@ -265,15 +256,13 @@ describe('registrar topologies', () => { // setup connections before registrar const remotePeerId = peerIdFromPrivateKey(await generateKeyPair('Ed25519')) - const conn = mockConnection(mockMultiaddrConnection(mockDuplex(), remotePeerId)) - // connection is limited - conn.limits = { - bytes: 100n - } - - // return connection from connection manager - connectionManager.getConnections.withArgs(remotePeerId).returns([conn]) + const conn = stubInterface({ + remotePeer: remotePeerId, + limits: { + bytes: 100n + } + }) const topology: Topology = { notifyOnLimitedConnection: true, @@ -317,21 +306,17 @@ describe('registrar topologies', () => { // setup connections before registrar const remotePeerId = peerIdFromPrivateKey(await generateKeyPair('Ed25519')) - const limitedConnection = mockConnection(mockMultiaddrConnection(mockDuplex(), remotePeerId)) - limitedConnection.limits = { - bytes: 100n - } - - const nonLimitedConnection = mockConnection(mockMultiaddrConnection(mockDuplex(), remotePeerId)) - nonLimitedConnection.limits = { - bytes: 100n - } + const limitedConnection = stubInterface({ + remotePeer: remotePeerId, + limits: { + bytes: 100n + } + }) - // return connection from connection manager - connectionManager.getConnections.withArgs(remotePeerId).returns([ - limitedConnection, - nonLimitedConnection - ]) + const nonLimitedConnection = stubInterface({ + remotePeer: remotePeerId, + limits: undefined + }) // remote peer connects over limited connection events.safeDispatchEvent('peer:identify', { @@ -364,7 +349,10 @@ describe('registrar topologies', () => { // setup connections before registrar const remotePeerId = peerIdFromPrivateKey(await generateKeyPair('Ed25519')) - const connection = mockConnection(mockMultiaddrConnection(mockDuplex(), remotePeerId)) + const connection = stubInterface({ + remotePeer: remotePeerId, + limits: undefined + }) // remote peer runs identify a few times for (let i = 0; i < 5; i++) { diff --git a/packages/libp2p/test/upgrading/upgrader.spec.ts b/packages/libp2p/test/upgrading/upgrader.spec.ts index 0b8110406c..b2aae00528 100644 --- a/packages/libp2p/test/upgrading/upgrader.spec.ts +++ b/packages/libp2p/test/upgrading/upgrader.spec.ts @@ -118,7 +118,7 @@ describe('upgrader', () => { })) await dialer.dial(listener.getMultiaddrs(), { - signal: AbortSignal.timeout(100) + signal: AbortSignal.timeout(500) }) await delay(1000) From 91687998d7ae536549ddcd840aa430098860f0fb Mon Sep 17 00:00:00 2001 From: Alex Potsides Date: Wed, 6 Nov 2024 06:25:58 +0000 Subject: [PATCH 5/5] chore: remove all sibling test deps (#2806) Converts last set of tests to use stubbing instead of depending on sibling modules in the monorepo. This will reduce release churn in the future. --- packages/libp2p/package.json | 6 +- .../libp2p/test/addresses/addresses.spec.ts | 233 ------ .../connection-gater.spec.ts | 229 +++-- .../test/connection-manager/index.spec.ts | 158 +--- .../test/connection-manager/resolver.spec.ts | 92 --- .../libp2p/test/connection-manager/utils.ts | 35 + packages/libp2p/test/core/core.spec.ts | 13 - packages/libp2p/test/core/listening.spec.ts | 44 - packages/libp2p/test/fixtures/create-peers.ts | 68 -- packages/libp2p/test/fixtures/slow-muxer.ts | 29 - .../test/transports/transport-manager.spec.ts | 128 ++- .../libp2p/test/upgrading/upgrader.spec.ts | 780 +++++++++++------- packages/libp2p/test/upgrading/utils.ts | 39 + 13 files changed, 782 insertions(+), 1072 deletions(-) delete mode 100644 packages/libp2p/test/addresses/addresses.spec.ts delete mode 100644 packages/libp2p/test/connection-manager/resolver.spec.ts create mode 100644 packages/libp2p/test/connection-manager/utils.ts delete mode 100644 packages/libp2p/test/core/listening.spec.ts delete mode 100644 packages/libp2p/test/fixtures/create-peers.ts delete mode 100644 packages/libp2p/test/fixtures/slow-muxer.ts create mode 100644 packages/libp2p/test/upgrading/utils.ts diff --git a/packages/libp2p/package.json b/packages/libp2p/package.json index a3e9406472..46f818a813 100644 --- a/packages/libp2p/package.json +++ b/packages/libp2p/package.json @@ -113,15 +113,11 @@ "uint8arrays": "^5.1.0" }, "devDependencies": { - "@chainsafe/libp2p-yamux": "^7.0.0", - "@libp2p/echo": "^2.1.1", - "@libp2p/memory": "^0.0.0", - "@libp2p/mplex": "^11.0.10", - "@libp2p/plaintext": "^2.0.10", "aegir": "^44.0.1", "delay": "^6.0.0", "it-all": "^3.0.6", "it-drain": "^3.0.7", + "it-length-prefixed": "^9.1.0", "it-map": "^3.1.0", "it-pair": "^2.0.6", "it-stream-types": "^2.0.1", diff --git a/packages/libp2p/test/addresses/addresses.spec.ts b/packages/libp2p/test/addresses/addresses.spec.ts deleted file mode 100644 index a5de3e2e35..0000000000 --- a/packages/libp2p/test/addresses/addresses.spec.ts +++ /dev/null @@ -1,233 +0,0 @@ -/* eslint-env mocha */ - -import { memory } from '@libp2p/memory' -import { multiaddr, protocols } from '@multiformats/multiaddr' -import { expect } from 'aegir/chai' -import { pEvent } from 'p-event' -import { createLibp2p } from '../../src/index.js' -import { getComponent } from '../fixtures/get-component.js' -import type { Libp2p, PeerUpdate } from '@libp2p/interface' -import type { AddressManager, TransportManager } from '@libp2p/interface-internal' -import type { Multiaddr } from '@multiformats/multiaddr' - -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 () => { - await libp2p?.stop() - }) - - it('should keep listen addresses after start, even if changed', async () => { - libp2p = await createLibp2p({ - start: false, - addresses: { - listen: listenAddresses, - announce: announceAddresses - }, - transports: [ - memory() - ] - }) - - const addressManager = getComponent(libp2p, 'addressManager') - let listenAddrs = 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]) - - // Should not replace listen addresses after transport listen - // Only transportManager has visibility of the port used - await libp2p.start() - - listenAddrs = 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]) - }) - - it('should announce transport listen addresses if announce addresses are not provided', async () => { - libp2p = await createLibp2p({ - addresses: { - listen: listenAddresses - }, - transports: [ - memory() - ] - }) - - 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(listenAddresses.length) - tmListen.forEach((m) => { - expect(advertiseMultiaddrs).to.include(m) - }) - }) - - it('should only announce the given announce addresses when provided', async () => { - libp2p = await createLibp2p({ - addresses: { - listen: listenAddresses, - announce: announceAddresses - }, - transports: [ - memory() - ] - }) - - const tmListen = getComponent(libp2p, 'transportManager').getAddrs().map((ma) => ma.toString()) - - // Announce 1 announce addr - const advertiseMultiaddrs = getComponent(libp2p, 'addressManager').getAddresses().map((ma) => ma.decapsulateCode(protocols('p2p').code).toString()) - expect(advertiseMultiaddrs.length).to.equal(announceAddresses.length) - advertiseMultiaddrs.forEach((m) => { - expect(tmListen).to.not.include(m) - expect(announceAddresses).to.include(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() - ] - }) - - const listenAddrs = getComponent(libp2p, 'addressManager').getListenAddrs().map((ma) => ma.toString()) - expect(listenAddrs).to.have.lengthOf(listenAddresses.length) - expect(listenAddrs).to.deep.equal(listenAddresses) - - await libp2p.start() - - const addresses = getComponent(libp2p, 'addressManager').getAddresses() - expect(addresses).to.have.lengthOf(1) - }) - - 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.deep.equal(listenAddresses) - - const addresses = getComponent(libp2p, 'addressManager').getAddresses() - expect(addresses).to.have.lengthOf(0) - }) - - it('should include observed addresses in returned multiaddrs', async () => { - libp2p = await createLibp2p({ - start: false, - addresses: { - listen: listenAddresses - }, - transports: [ - memory() - ] - }) - - const ma = '/ip4/83.32.123.53/tcp/43928' - - await libp2p.start() - - const addressManager = getComponent(libp2p, 'addressManager') - - expect(addressManager.getAddresses()).to.have.lengthOf(listenAddresses.length) - - addressManager.confirmObservedAddr(multiaddr(ma)) - - expect(addressManager.getAddresses()).to.have.lengthOf(listenAddresses.length + 1) - expect(addressManager.getAddresses().map(ma => ma.decapsulateCode(protocols('p2p').code).toString())).to.include(ma) - }) - - it('should populate the AddressManager from the config', async () => { - 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) - expect(libp2p.getMultiaddrs().map(ma => ma.decapsulateCode(protocols('p2p').code).toString())).to.not.have.members(listenAddresses) - }) - - it('should update our peer record with announce addresses on startup', async () => { - libp2p = await createLibp2p({ - start: false, - addresses: { - listen: listenAddresses, - announce: announceAddresses - }, - transports: [ - memory() - ] - }) - - const eventPromise = pEvent<'self:peer:update', CustomEvent>(libp2p, 'self:peer:update', { - filter: (event) => { - return event.detail.peer.addresses.map(({ multiaddr }) => multiaddr.toString()) - .includes(announceAddresses[0]) - } - }) - - await libp2p.start() - - const event = await eventPromise - - expect(event.detail.peer.addresses.map(({ multiaddr }) => multiaddr.toString())) - .to.include.members(announceAddresses, 'peer info did not include announce addresses') - }) - - it('should only include confirmed observed addresses in peer record', async () => { - libp2p = await createLibp2p({ - start: false, - addresses: { - listen: listenAddresses, - announce: announceAddresses - }, - transports: [ - memory() - ] - }) - - await libp2p.start() - - const eventPromise = pEvent<'self:peer:update', CustomEvent>(libp2p, 'self:peer:update') - - const unconfirmedAddress = multiaddr('/ip4/127.0.0.1/tcp/4010/ws') - getComponent(libp2p, 'addressManager').addObservedAddr(unconfirmedAddress) - - const confirmedAddress = multiaddr('/ip4/127.0.0.1/tcp/4011/ws') - getComponent(libp2p, 'addressManager').confirmObservedAddr(confirmedAddress) - - const event = await eventPromise - - expect(event.detail.peer.addresses.map(({ multiaddr }) => multiaddr.toString())) - .to.not.include(unconfirmedAddress.toString(), 'peer info included unconfirmed observed address') - - expect(event.detail.peer.addresses.map(({ multiaddr }) => multiaddr.toString())) - .to.include(confirmedAddress.toString(), 'peer info did not include confirmed observed address') - }) -}) diff --git a/packages/libp2p/test/connection-manager/connection-gater.spec.ts b/packages/libp2p/test/connection-manager/connection-gater.spec.ts index 91f9b8a67a..56c0a3688a 100644 --- a/packages/libp2p/test/connection-manager/connection-gater.spec.ts +++ b/packages/libp2p/test/connection-manager/connection-gater.spec.ts @@ -1,160 +1,247 @@ /* eslint-env mocha */ -import { stop } from '@libp2p/interface' +import { generateKeyPair } from '@libp2p/crypto/keys' +import { start, stop } from '@libp2p/interface' +import { logger } from '@libp2p/logger' +import { peerIdFromPrivateKey } from '@libp2p/peer-id' +import { multiaddr } from '@multiformats/multiaddr' 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' +import Sinon from 'sinon' +import { stubInterface } from 'sinon-ts' +import { DefaultConnectionManager } from '../../src/connection-manager/index.js' +import { DefaultUpgrader } from '../../src/upgrader.js' +import { createDefaultUpgraderComponents } from '../upgrading/utils.js' +import { createDefaultConnectionManagerComponents } from './utils.js' +import type { Transport, MultiaddrConnection, StreamMuxerFactory } from '@libp2p/interface' +import type { TransportManager } from '@libp2p/interface-internal' describe('connection-gater', () => { - let dialer: Libp2p<{ echo: Echo }> - let listener: Libp2p<{ echo: Echo }> + let connectionManager: DefaultConnectionManager afterEach(async () => { - await stop(dialer, listener) + await stop(connectionManager) }) it('intercept peer dial', async () => { - const denyDialPeer = sinon.stub().returns(true) + const denyDialPeer = Sinon.stub().returns(true) + const remotePeer = peerIdFromPrivateKey(await generateKeyPair('Ed25519')) + const ma = multiaddr(`/ip4/123.123.123.123/tcp/1234/p2p/${remotePeer}`) - ;({ dialer, listener } = await createPeers({ + connectionManager = new DefaultConnectionManager(await createDefaultConnectionManagerComponents({ connectionGater: { denyDialPeer } })) + await start(connectionManager) - await expect(dialer.dial(listener.getMultiaddrs())) + await expect(connectionManager.openConnection(ma)) .to.eventually.be.rejected().with.property('name', 'DialDeniedError') + + expect(denyDialPeer.called).to.be.true() }) it('intercept addr dial', async () => { - const denyDialMultiaddr = sinon.stub().returns(false) + const denyDialMultiaddr = Sinon.stub().returns(true) + const ma = multiaddr('/ip4/123.123.123.123/tcp/1234') - ;({ dialer, listener } = await createPeers({ + connectionManager = new DefaultConnectionManager(await createDefaultConnectionManagerComponents({ connectionGater: { denyDialMultiaddr - } + }, + transportManager: stubInterface({ + dialTransportForMultiaddr: () => stubInterface() + }) })) + await start(connectionManager) - await dialer.dial(listener.getMultiaddrs()) + await expect(connectionManager.openConnection(ma)) + .to.eventually.be.rejected().with.property('name', 'DialDeniedError') - for (const multiaddr of listener.getMultiaddrs()) { - expect(denyDialMultiaddr.calledWith(multiaddr)).to.be.true() - } + expect(denyDialMultiaddr.called).to.be.true() }) - it('intercept multiaddr store', async () => { - const filterMultiaddrForPeer = sinon.stub().returns(true) + it('intercept accept inbound connection', async () => { + const denyInboundConnection = Sinon.stub().returns(true) - ;({ dialer, listener } = await createPeers({ + const upgrader = new DefaultUpgrader(await createDefaultUpgraderComponents({ connectionGater: { - filterMultiaddrForPeer + denyInboundConnection } - })) - - const fullMultiaddr = listener.getMultiaddrs()[0] - - await dialer.peerStore.merge(listener.peerId, { - multiaddrs: [fullMultiaddr] + }), { + connectionEncrypters: [], + streamMuxers: [] }) - expect(filterMultiaddrForPeer.callCount).to.equal(1) + const remotePeer = peerIdFromPrivateKey(await generateKeyPair('Ed25519')) + const remoteAddr = multiaddr(`/ip4/123.123.123.123/tcp/1234/p2p/${remotePeer}`) - 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) + const maConn = stubInterface({ + remoteAddr + }) - ;({ dialer, listener } = await createPeers({}, { - connectionGater: { - denyInboundConnection - } + await expect(upgrader.upgradeInbound(maConn, { + skipEncryption: true, + skipProtection: true, + muxerFactory: stubInterface() })) - - await dialer.dial(listener.getMultiaddrs()) + .to.eventually.be.rejected().with.property('name', 'ConnectionInterceptedError') expect(denyInboundConnection.called).to.be.true() }) it('intercept accept outbound connection', async () => { - const denyOutboundConnection = sinon.stub().returns(false) + const denyOutboundConnection = Sinon.stub().returns(true) - ;({ dialer, listener } = await createPeers({ + const upgrader = new DefaultUpgrader(await createDefaultUpgraderComponents({ connectionGater: { denyOutboundConnection } - })) + }), { + connectionEncrypters: [], + streamMuxers: [] + }) + + const remotePeer = peerIdFromPrivateKey(await generateKeyPair('Ed25519')) + const remoteAddr = multiaddr(`/ip4/123.123.123.123/tcp/1234/p2p/${remotePeer}`) - await dialer.dial(listener.getMultiaddrs()) + const maConn = stubInterface({ + remoteAddr + }) - expect(denyOutboundConnection.called).to.be.true() + await expect(upgrader.upgradeOutbound(maConn, { + skipEncryption: true, + skipProtection: true, + muxerFactory: stubInterface() + })) + .to.eventually.be.rejected().with.property('name', 'ConnectionInterceptedError') }) it('intercept inbound encrypted', async () => { - const denyInboundEncryptedConnection = sinon.stub().returns(false) + const denyInboundEncryptedConnection = Sinon.stub().returns(true) + const remotePeer = peerIdFromPrivateKey(await generateKeyPair('Ed25519')) + const remoteAddr = multiaddr(`/ip4/123.123.123.123/tcp/1234/p2p/${remotePeer}`) - ;({ dialer, listener } = await createPeers({}, { + const upgrader = new DefaultUpgrader(await createDefaultUpgraderComponents({ connectionGater: { denyInboundEncryptedConnection } - })) + }), { + connectionEncrypters: [], + streamMuxers: [] + }) + upgrader._encryptInbound = async (maConn) => { + return { + conn: maConn, + remotePeer, + protocol: '/test-encrypter' + } + } + + const maConn = stubInterface({ + remoteAddr, + log: logger('test') + }) - await dialer.dial(listener.getMultiaddrs()) + await expect(upgrader.upgradeInbound(maConn, { + skipProtection: true, + muxerFactory: stubInterface() + })) + .to.eventually.be.rejected().with.property('name', 'ConnectionInterceptedError') 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) + const denyOutboundEncryptedConnection = Sinon.stub().returns(true) + + const remotePeer = peerIdFromPrivateKey(await generateKeyPair('Ed25519')) + const remoteAddr = multiaddr(`/ip4/123.123.123.123/tcp/1234/p2p/${remotePeer}`) - ;({ dialer, listener } = await createPeers({ + const upgrader = new DefaultUpgrader(await createDefaultUpgraderComponents({ connectionGater: { denyOutboundEncryptedConnection } - })) + }), { + connectionEncrypters: [], + streamMuxers: [] + }) + upgrader._encryptOutbound = async (maConn) => { + return { + conn: maConn, + remotePeer, + protocol: '/test-encrypter' + } + } - await dialer.dial(listener.getMultiaddrs()) + const maConn = stubInterface({ + remoteAddr, + log: logger('test') + }) + + await expect(upgrader.upgradeOutbound(maConn, { + skipProtection: true, + muxerFactory: stubInterface() + })) + .to.eventually.be.rejected().with.property('name', 'ConnectionInterceptedError') 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) + const denyInboundUpgradedConnection = Sinon.stub().returns(true) + const remotePeer = peerIdFromPrivateKey(await generateKeyPair('Ed25519')) + const remoteAddr = multiaddr(`/ip4/123.123.123.123/tcp/1234/p2p/${remotePeer}`) - ;({ dialer, listener } = await createPeers({}, { + const upgrader = new DefaultUpgrader(await createDefaultUpgraderComponents({ connectionGater: { denyInboundUpgradedConnection } - })) + }), { + connectionEncrypters: [], + streamMuxers: [] + }) - const input = Uint8Array.from([0]) - const output = await dialer.services.echo.echo(listener.getMultiaddrs(), input) - expect(output).to.equalBytes(input) + const maConn = stubInterface({ + remoteAddr, + log: logger('test') + }) + + await expect(upgrader.upgradeInbound(maConn, { + skipEncryption: true, + skipProtection: true, + muxerFactory: stubInterface() + })) + .to.eventually.be.rejected().with.property('name', 'ConnectionInterceptedError') 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) + const denyOutboundUpgradedConnection = Sinon.stub().returns(true) + const remotePeer = peerIdFromPrivateKey(await generateKeyPair('Ed25519')) + const remoteAddr = multiaddr(`/ip4/123.123.123.123/tcp/1234/p2p/${remotePeer}`) - ;({ dialer, listener } = await createPeers({ + const upgrader = new DefaultUpgrader(await createDefaultUpgraderComponents({ connectionGater: { denyOutboundUpgradedConnection } - })) + }), { + connectionEncrypters: [], + streamMuxers: [] + }) - const input = Uint8Array.from([0]) - const output = await dialer.services.echo.echo(listener.getMultiaddrs(), input) - expect(output).to.equalBytes(input) + const maConn = stubInterface({ + remoteAddr, + log: logger('test') + }) + + await expect(upgrader.upgradeOutbound(maConn, { + skipEncryption: true, + skipProtection: true, + muxerFactory: stubInterface() + })) + .to.eventually.be.rejected().with.property('name', 'ConnectionInterceptedError') 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/index.spec.ts b/packages/libp2p/test/connection-manager/index.spec.ts index 629c2a9d7a..32556dc624 100644 --- a/packages/libp2p/test/connection-manager/index.spec.ts +++ b/packages/libp2p/test/connection-manager/index.spec.ts @@ -1,49 +1,32 @@ /* eslint-env mocha */ import { generateKeyPair } from '@libp2p/crypto/keys' -import { TypedEventEmitter, KEEP_ALIVE, start, stop } from '@libp2p/interface' -import { defaultLogger } from '@libp2p/logger' +import { KEEP_ALIVE, start, stop } from '@libp2p/interface' import { peerIdFromPrivateKey } from '@libp2p/peer-id' -import { dns } from '@multiformats/dns' import { multiaddr } from '@multiformats/multiaddr' import { expect } from 'aegir/chai' 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 { DefaultConnectionManager } from '../../src/connection-manager/index.js' import { createLibp2p } from '../../src/index.js' -import { createPeers } from '../fixtures/create-peers.js' import { getComponent } from '../fixtures/get-component.js' -import type { Echo } from '@libp2p/echo' -import type { ConnectionGater, PeerId, PeerStore, Libp2p, Connection, PeerRouting, MultiaddrConnection } from '@libp2p/interface' -import type { TransportManager } from '@libp2p/interface-internal' +import { createDefaultConnectionManagerComponents, type StubbedDefaultConnectionManagerComponents } from './utils.js' +import type { Libp2p, Connection, MultiaddrConnection } from '@libp2p/interface' const defaultOptions = { maxConnections: 10, inboundUpgradeTimeout: 10000 } -function createDefaultComponents (peerId: PeerId): DefaultConnectionManagerComponents { - return { - peerId, - peerStore: stubInterface({ - all: async () => [] - }), - peerRouting: stubInterface(), - transportManager: stubInterface(), - connectionGater: stubInterface(), - events: new TypedEventEmitter(), - logger: defaultLogger() - } -} - describe('Connection Manager', () => { let libp2p: Libp2p let connectionManager: DefaultConnectionManager + let components: StubbedDefaultConnectionManagerComponents beforeEach(async () => { libp2p = await createLibp2p() + components = await createDefaultConnectionManagerComponents() }) afterEach(async () => { @@ -91,7 +74,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(createDefaultComponents(libp2p.peerId), { + connectionManager = new DefaultConnectionManager(components, { ...defaultOptions, deny: [ '/ip4/83.13.55.32' @@ -108,7 +91,7 @@ describe('Connection Manager', () => { }) it('should deny connections when maxConnections is exceeded', async () => { - connectionManager = new DefaultConnectionManager(createDefaultComponents(libp2p.peerId), { + connectionManager = new DefaultConnectionManager(components, { ...defaultOptions, maxConnections: 1 }) @@ -133,7 +116,7 @@ describe('Connection Manager', () => { }) it('should deny connections from peers that connect too frequently', async () => { - connectionManager = new DefaultConnectionManager(createDefaultComponents(libp2p.peerId), { + connectionManager = new DefaultConnectionManager(components, { ...defaultOptions, inboundConnectionThreshold: 1 }) @@ -160,7 +143,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(createDefaultComponents(libp2p.peerId), { + connectionManager = new DefaultConnectionManager(components, { ...defaultOptions, maxConnections: 1, allow: [ @@ -188,7 +171,7 @@ describe('Connection Manager', () => { }) it('should limit the number of inbound pending connections', async () => { - connectionManager = new DefaultConnectionManager(createDefaultComponents(libp2p.peerId), { + connectionManager = new DefaultConnectionManager(components, { ...defaultOptions, maxIncomingPendingConnections: 1 }) @@ -225,7 +208,7 @@ describe('Connection Manager', () => { }) it('should allow dialing peers when an existing limited connection exists', async () => { - connectionManager = new DefaultConnectionManager(createDefaultComponents(libp2p.peerId), { + connectionManager = new DefaultConnectionManager(components, { ...defaultOptions, maxIncomingPendingConnections: 1 }) @@ -262,72 +245,45 @@ 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, { + connectionManager = new DefaultConnectionManager(components, { maxConnections: 1000, inboundUpgradeTimeout: 1000 }) await start(connectionManager) + const remotePeer = peerIdFromPrivateKey(await generateKeyPair('Ed25519')) + const conn1 = stubInterface({ remoteAddr: multiaddr('/ip4/34.4.63.125/tcp/4001'), - remotePeer: peerIds[1], + remotePeer, status: 'open' }) const conn2 = stubInterface({ remoteAddr: multiaddr('/ip4/34.4.63.126/tcp/4001'), - remotePeer: peerIds[1], + remotePeer, status: 'open' }) - expect(connectionManager.getConnections(peerIds[1])).to.have.lengthOf(0) + expect(connectionManager.getConnections(remotePeer)).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) + expect(connectionManager.getConnections(remotePeer)).to.have.lengthOf(2) conn2.status = 'closed' components.events.safeDispatchEvent('connection:close', { detail: conn2 }) - expect(connectionManager.getConnections(peerIds[1])).to.have.lengthOf(1) + expect(connectionManager.getConnections(remotePeer)).to.have.lengthOf(1) expect(conn1.close.called).to.be.false() - - 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 @@ -335,14 +291,16 @@ describe('Connection Manager', () => { await start(connectionManager) + const remotePeer = peerIdFromPrivateKey(await generateKeyPair('Ed25519')) + const conn1 = stubInterface({ remoteAddr: multiaddr('/ip4/34.4.63.125/tcp/4001'), - remotePeer: peerIds[1], + remotePeer, status: 'open' }) const conn2 = stubInterface({ remoteAddr: multiaddr('/ip4/34.4.63.126/tcp/4001'), - remotePeer: peerIds[1], + remotePeer, status: 'open' }) @@ -350,77 +308,13 @@ describe('Connection Manager', () => { components.events.safeDispatchEvent('connection:open', { detail: conn1 }) components.events.safeDispatchEvent('connection:open', { detail: conn2 }) - expect(connectionManager.getConnections(peerIds[1])).to.have.lengthOf(2) + expect(connectionManager.getConnections(remotePeer)).to.have.lengthOf(2) await connectionManager.stop() expect(conn1.close.called).to.be.true() expect(conn2.close.called).to.be.true() - 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) + expect(connectionManager.getConnections(remotePeer)).to.have.lengthOf(0) }) }) diff --git a/packages/libp2p/test/connection-manager/resolver.spec.ts b/packages/libp2p/test/connection-manager/resolver.spec.ts deleted file mode 100644 index a03d2df2ae..0000000000 --- a/packages/libp2p/test/connection-manager/resolver.spec.ts +++ /dev/null @@ -1,92 +0,0 @@ -/* eslint-env mocha */ - -import { yamux } from '@chainsafe/libp2p-yamux' -import { stop } from '@libp2p/interface' -import { memory } from '@libp2p/memory' -import { mplex } from '@libp2p/mplex' -import { plaintext } from '@libp2p/plaintext' -import { multiaddr } from '@multiformats/multiaddr' -import { expect } from 'aegir/chai' -import sinon from 'sinon' -import { createLibp2p } from '../../src/index.js' -import type { Libp2p } from '@libp2p/interface' -import type { Multiaddr } from '@multiformats/multiaddr' - -describe('resolver', () => { - let dialer: Libp2p - let listener: Libp2p - let resolver: sinon.SinonStub<[Multiaddr], Promise> - - beforeEach(async () => { - resolver = sinon.stub<[Multiaddr], Promise>(); - - [dialer, listener] = await Promise.all([ - createLibp2p({ - transports: [ - memory() - ], - streamMuxers: [ - yamux(), - mplex() - ], - connectionManager: { - resolvers: { - dnsaddr: resolver - } - }, - connectionEncrypters: [ - plaintext() - ] - }), - createLibp2p({ - addresses: { - listen: ['/memory/location'] - }, - transports: [ - memory() - ], - streamMuxers: [ - yamux(), - mplex() - ], - connectionManager: { - resolvers: { - dnsaddr: resolver - } - }, - connectionEncrypters: [ - plaintext() - ] - }) - ]) - }) - - afterEach(async () => { - sinon.restore() - - await stop(dialer, listener) - }) - - it('should use the dnsaddr resolver to resolve a dnsaddr address', async () => { - const dialAddr = multiaddr(`/dnsaddr/remote.libp2p.io/p2p/${listener.peerId}`) - - // resolver stub - resolver.withArgs(dialAddr).resolves(listener.getMultiaddrs().map(ma => ma.toString())) - - // dial with resolved address - const connection = await dialer.dial(dialAddr) - expect(connection).to.exist() - expect(connection.remoteAddr.equals(listener.getMultiaddrs()[0])) - }) - - it('fails to dial if resolve fails and there are no addresses to dial', async () => { - const dialAddr = multiaddr(`/dnsaddr/remote.libp2p.io/p2p/${listener.peerId}`) - const err = new Error() - - // Stub resolver - resolver.rejects(err) - - await expect(dialer.dial(dialAddr)) - .to.eventually.be.rejectedWith(err) - }) -}) diff --git a/packages/libp2p/test/connection-manager/utils.ts b/packages/libp2p/test/connection-manager/utils.ts new file mode 100644 index 0000000000..838b97ee90 --- /dev/null +++ b/packages/libp2p/test/connection-manager/utils.ts @@ -0,0 +1,35 @@ +/* eslint-env mocha */ + +import { generateKeyPair } from '@libp2p/crypto/keys' +import { TypedEventEmitter } from '@libp2p/interface' +import { defaultLogger } from '@libp2p/logger' +import { peerIdFromPrivateKey } from '@libp2p/peer-id' +import { stubInterface, type StubbedInstance } from 'sinon-ts' +import type { DefaultConnectionManagerComponents } from '../../src/connection-manager/index.js' +import type { ConnectionGater, PeerId, PeerStore, PeerRouting, TypedEventTarget, Libp2pEvents, ComponentLogger } from '@libp2p/interface' +import type { TransportManager } from '@libp2p/interface-internal' + +export interface StubbedDefaultConnectionManagerComponents { + peerId: PeerId + peerStore: StubbedInstance + peerRouting: StubbedInstance + transportManager: StubbedInstance + connectionGater: StubbedInstance + events: TypedEventTarget + logger: ComponentLogger +} + +export async function createDefaultConnectionManagerComponents (options?: Partial): Promise { + return { + peerId: peerIdFromPrivateKey(await generateKeyPair('Ed25519')), + peerStore: stubInterface({ + all: async () => [] + }), + peerRouting: stubInterface(), + transportManager: stubInterface(), + connectionGater: stubInterface(), + events: new TypedEventEmitter(), + logger: defaultLogger(), + ...options + } as unknown as any +} diff --git a/packages/libp2p/test/core/core.spec.ts b/packages/libp2p/test/core/core.spec.ts index 1674efcc45..0e8ab1a1ff 100644 --- a/packages/libp2p/test/core/core.spec.ts +++ b/packages/libp2p/test/core/core.spec.ts @@ -1,6 +1,5 @@ /* eslint-env mocha */ -import { memory } from '@libp2p/memory' import { multiaddr } from '@multiformats/multiaddr' import { expect } from 'aegir/chai' import { stubInterface } from 'sinon-ts' @@ -28,18 +27,6 @@ describe('core', () => { await expect(libp2p.isDialable(ma)).to.eventually.be.false() }) - it('should say an address is dialable if a transport is configured', async () => { - libp2p = await createLibp2p({ - transports: [ - memory() - ] - }) - - const ma = multiaddr('/memory/address-1') - - await expect(libp2p.isDialable(ma)).to.eventually.be.true() - }) - it('should test if a protocol can run over a limited connection', async () => { libp2p = await createLibp2p({ transports: [ diff --git a/packages/libp2p/test/core/listening.spec.ts b/packages/libp2p/test/core/listening.spec.ts deleted file mode 100644 index e7976d7499..0000000000 --- a/packages/libp2p/test/core/listening.spec.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* eslint-env mocha */ - -import { stop } from '@libp2p/interface' -import { memory } from '@libp2p/memory' -import { plaintext } from '@libp2p/plaintext' -import { expect } from 'aegir/chai' -import { createLibp2p } from '../../src/index.js' -import type { Libp2p } from '@libp2p/interface' - -describe('Listening', () => { - let libp2p: Libp2p - - after(async () => { - 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: [ - listenAddress - ] - }, - transports: [ - memory() - ], - connectionEncrypters: [ - plaintext() - ] - }) - - await libp2p.start() - - // @ts-expect-error components field is private - const addrs = libp2p.components.transportManager.getAddrs() - - // Should get something like: - // /memory/address-1 - expect(addrs).to.have.lengthOf(1) - expect(addrs[0].toString()).to.equal(listenAddress) - }) -}) diff --git a/packages/libp2p/test/fixtures/create-peers.ts b/packages/libp2p/test/fixtures/create-peers.ts deleted file mode 100644 index aecf65dd11..0000000000 --- a/packages/libp2p/test/fixtures/create-peers.ts +++ /dev/null @@ -1,68 +0,0 @@ -/* 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/slow-muxer.ts b/packages/libp2p/test/fixtures/slow-muxer.ts deleted file mode 100644 index 03279ca32e..0000000000 --- a/packages/libp2p/test/fixtures/slow-muxer.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* 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/transports/transport-manager.spec.ts b/packages/libp2p/test/transports/transport-manager.spec.ts index 277502bb35..51884a583a 100644 --- a/packages/libp2p/test/transports/transport-manager.spec.ts +++ b/packages/libp2p/test/transports/transport-manager.spec.ts @@ -3,11 +3,9 @@ import { generateKeyPair } from '@libp2p/crypto/keys' import { TypedEventEmitter, start, stop, FaultTolerance } from '@libp2p/interface' 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 { multiaddr } from '@multiformats/multiaddr' +import { multiaddr, type Multiaddr } from '@multiformats/multiaddr' import { expect } from 'aegir/chai' import { MemoryDatastore } from 'datastore-core' import { pEvent } from 'p-event' @@ -15,20 +13,21 @@ 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 { Connection, Libp2p, Upgrader } from '@libp2p/interface' +import type { Connection, Transport, Upgrader, Listener } from '@libp2p/interface' const listenAddr = multiaddr('/ip4/127.0.0.1/tcp/0') const addrs = [ multiaddr('/memory/address-1'), multiaddr('/memory/address-2') ] +const testTransportTag = 'test-transport' describe('Transport Manager', () => { let tm: DefaultTransportManager let components: Components + let transport: Transport beforeEach(async () => { const events = new TypedEventEmitter() @@ -49,6 +48,39 @@ describe('Transport Manager', () => { faultTolerance: FaultTolerance.NO_FATAL }) await start(tm) + + transport = stubInterface({ + dial: async () => stubInterface(), + dialFilter: (addrs) => { + return addrs.filter(ma => ma.toString().startsWith('/memory')) + }, + listenFilter: (addrs) => { + return addrs.filter(ma => ma.toString().startsWith('/memory')) + }, + createListener: () => { + let addr: Multiaddr | undefined + const closeListeners: Array<() => void> = [] + + return stubInterface({ + listen: async (a) => { + addr = a + }, + getAddrs: () => addr != null ? [addr] : [], + close: async () => { + addr = undefined + closeListeners.forEach(fn => { + fn() + }) + }, + addEventListener: (event, handler: any) => { + if (event === 'close') { + closeListeners.push(handler) + } + } + }) + } + }) + transport[Symbol.toStringTag] = testTransportTag }) afterEach(async () => { @@ -58,28 +90,26 @@ describe('Transport Manager', () => { }) it('should be able to add and remove a transport', async () => { - const transport = memory() - expect(tm.getTransports()).to.have.lengthOf(0) - tm.add(transport(components)) + tm.add(transport) expect(tm.getTransports()).to.have.lengthOf(1) - await tm.remove('@libp2p/memory') + await tm.remove(testTransportTag) expect(tm.getTransports()).to.have.lengthOf(0) }) it('should not be able to add a transport twice', async () => { - tm.add(memory()(components)) + tm.add(transport) expect(() => { - tm.add(memory()(components)) + tm.add(transport) }) .to.throw() .and.to.have.property('name', 'InvalidParametersError') }) it('should fail to dial an unsupported address', async () => { - tm.add(memory()(components)) + tm.add(transport) const addr = multiaddr('/ip4/127.0.0.1/tcp/0') await expect(tm.dial(addr)) .to.eventually.be.rejected() @@ -88,7 +118,7 @@ describe('Transport Manager', () => { it('should fail to listen with no valid address', async () => { tm = new DefaultTransportManager(components) - tm.add(memory()(components)) + tm.add(transport) await expect(start(tm)) .to.eventually.be.rejected() @@ -99,15 +129,13 @@ describe('Transport Manager', () => { it('should be able to add and remove a transport', async () => { expect(tm.getTransports()).to.have.lengthOf(0) - tm.add(memory()(components)) + tm.add(transport) expect(tm.getTransports()).to.have.lengthOf(1) - await tm.remove('@libp2p/memory') + await tm.remove(testTransportTag) 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) @@ -117,14 +145,13 @@ describe('Transport Manager', () => { 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)) + tm.add(transport) await tm.listen(addrs) const addr = tm.getAddrs().shift() @@ -138,7 +165,6 @@ describe('Transport Manager', () => { }) it('should remove listeners when they stop listening', async () => { - const transport = memory()(components) tm.add(transport) expect(tm.getListeners()).to.have.lengthOf(0) @@ -172,65 +198,3 @@ describe('Transport Manager', () => { await tm.stop() }) }) - -describe('libp2p.transportManager (dial only)', () => { - let libp2p: Libp2p - - afterEach(async () => { - await stop(libp2p) - }) - - it('fails to start if multiaddr fails to listen', async () => { - libp2p = await createLibp2p({ - addresses: { - listen: ['/ip4/127.0.0.1/tcp/0'] - }, - transports: [memory()], - connectionEncrypters: [plaintext()], - start: false - }) - - await expect(libp2p.start()).to.eventually.be.rejected - .with.property('name', 'NoValidAddressesError') - }) - - it('does not fail to start if provided listen multiaddr are not compatible to configured transports (when supporting dial only mode)', async () => { - libp2p = await createLibp2p({ - addresses: { - listen: ['/ip4/127.0.0.1/tcp/0'] - }, - transportManager: { - faultTolerance: FaultTolerance.NO_FATAL - }, - transports: [ - memory() - ], - connectionEncrypters: [ - plaintext() - ], - start: false - }) - - await expect(libp2p.start()).to.eventually.be.undefined() - }) - - it('does not fail to start if provided listen multiaddr fail to listen on configured transports (when supporting dial only mode)', async () => { - libp2p = await createLibp2p({ - addresses: { - listen: ['/ip4/127.0.0.1/tcp/12345/p2p/QmWDn2LY8nannvSWJzruUYoLZ4vV83vfCBwd8DipvdgQc3/p2p-circuit'] - }, - transportManager: { - faultTolerance: FaultTolerance.NO_FATAL - }, - transports: [ - memory() - ], - connectionEncrypters: [ - plaintext() - ], - start: false - }) - - await expect(libp2p.start()).to.eventually.be.undefined() - }) -}) diff --git a/packages/libp2p/test/upgrading/upgrader.spec.ts b/packages/libp2p/test/upgrading/upgrader.spec.ts index b2aae00528..47c0002017 100644 --- a/packages/libp2p/test/upgrading/upgrader.spec.ts +++ b/packages/libp2p/test/upgrading/upgrader.spec.ts @@ -1,416 +1,590 @@ /* eslint-env mocha */ -import { stop } from '@libp2p/interface' -import { memory } from '@libp2p/memory' +import { generateKeyPair } from '@libp2p/crypto/keys' +import { logger } from '@libp2p/logger' +import { peerIdFromPrivateKey } from '@libp2p/peer-id' +import { multiaddr, type Multiaddr } from '@multiformats/multiaddr' import { expect } from 'aegir/chai' import delay from 'delay' import drain from 'it-drain' -import pDefer from 'p-defer' +import { encode } from 'it-length-prefixed' +import map from 'it-map' 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' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { DefaultUpgrader, type UpgraderInit } from '../../src/upgrader.js' +import { createDefaultUpgraderComponents } from './utils.js' +import type { ConnectionEncrypter, StreamMuxerFactory, MultiaddrConnection, StreamMuxer, ConnectionProtector, PeerId, SecuredConnection, Stream, StreamMuxerInit } from '@libp2p/interface' +import type { ConnectionManager, Registrar } from '@libp2p/interface-internal' describe('upgrader', () => { - let dialer: Libp2p<{ echo: Echo }> - let listener: Libp2p<{ echo: Echo }> - let dialerComponents: Components - let listenerComponents: Components + let init: UpgraderInit + const encrypterProtocol = '/test-encrypter' + const muxerProtocol = '/test-muxer' + let remotePeer: PeerId + let remoteAddr: Multiaddr + let maConn: MultiaddrConnection + + class BoomCrypto implements ConnectionEncrypter { + static protocol = encrypterProtocol + public protocol = encrypterProtocol + async secureInbound (): Promise { throw new Error('Boom') } + async secureOutbound (): Promise { throw new Error('Boom') } + } + + beforeEach(async () => { + remotePeer = peerIdFromPrivateKey(await generateKeyPair('Ed25519')) + remoteAddr = multiaddr(`/ip4/123.123.123.123/tcp/1234/p2p/${remotePeer}`) + + init = { + connectionEncrypters: [ + stubInterface({ + protocol: encrypterProtocol, + secureOutbound: async (connection) => ({ + conn: connection, + remotePeer + }), + secureInbound: async (connection) => ({ + conn: connection, + remotePeer + }) + }) + ], + streamMuxers: [ + stubInterface({ + protocol: muxerProtocol, + createStreamMuxer: () => stubInterface({ + protocol: muxerProtocol, + sink: async (source) => drain(source), + source: (async function * () {})() + }) + }) + ] + } - afterEach(async () => { - await stop(dialer, listener) + maConn = stubInterface({ + remoteAddr, + log: logger('test'), + sink: async (source) => drain(source), + source: map((async function * () { + yield '/multistream/1.0.0\n' + yield `${encrypterProtocol}\n` + yield `${muxerProtocol}\n` + })(), str => encode.single(uint8ArrayFromString(str))) + }) }) - it('should upgrade with valid muxers and crypto', async () => { - ({ dialer, listener } = await createPeers()) - - 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 outbound with valid muxers and crypto', async () => { + const upgrader = new DefaultUpgrader(await createDefaultUpgraderComponents(), init) + const conn = await upgrader.upgradeOutbound(maConn) + expect(conn.encryption).to.equal(encrypterProtocol) + expect(conn.multiplexer).to.equal(muxerProtocol) }) - it('should upgrade with only crypto', async () => { - ({ dialer, listener } = await createPeers({ streamMuxers: [] }, { streamMuxers: [] })) + it('should upgrade outbound with only crypto', async () => { + const upgrader = new DefaultUpgrader(await createDefaultUpgraderComponents(), { + ...init, + streamMuxers: [] + }) - const connection = await dialer.dial(listener.getMultiaddrs()) + const connection = await upgrader.upgradeOutbound(maConn) 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 protector = stubInterface() - protector.protect.callsFake(async (conn) => conn) - const connectionProtector = (): ConnectionProtector => protector + it('should use a private connection protector when provided for inbound connections', async () => { + const connectionProtector = stubInterface() + connectionProtector.protect.callsFake(async (conn) => conn) - ;({ dialer, listener } = await createPeers({ connectionProtector }, { connectionProtector })) + const upgrader = new DefaultUpgrader(await createDefaultUpgraderComponents({ + connectionProtector + }), init) - const input = Uint8Array.from([0, 1, 2, 3, 4]) - const output = await dialer.services.echo.echo(listener.getMultiaddrs(), input) - expect(output).to.equalBytes(input) + await upgrader.upgradeInbound(maConn) - expect(protector.protect.callCount).to.equal(2) + expect(connectionProtector.protect.callCount).to.equal(1) }) - it('should fail if crypto fails', async () => { - class BoomCrypto implements ConnectionEncrypter { - static protocol = '/unstable' - public protocol = '/unstable' - async secureInbound (): Promise { throw new Error('Boom') } - async secureOutbound (): Promise { throw new Error('Boom') } - } + it('should use a private connection protector when provided for outbound connections', async () => { + const connectionProtector = stubInterface() + connectionProtector.protect.callsFake(async (conn) => conn) + + const upgrader = new DefaultUpgrader(await createDefaultUpgraderComponents({ + connectionProtector + }), init) + + await upgrader.upgradeOutbound(maConn) - ({ dialer, dialerComponents, listener, listenerComponents } = await createPeers({ + expect(connectionProtector.protect.callCount).to.equal(1) + }) + + it('should fail inbound if crypto fails', async () => { + const upgrader = new DefaultUpgrader(await createDefaultUpgraderComponents(), { + ...init, connectionEncrypters: [ - () => new BoomCrypto() + new BoomCrypto() ] - }, { + }) + + await expect(upgrader.upgradeInbound(maConn)).to.eventually.be.rejected + .with.property('name', 'EncryptionFailedError') + }) + + it('should fail outbound if crypto fails', async () => { + const upgrader = new DefaultUpgrader(await createDefaultUpgraderComponents(), { + ...init, connectionEncrypters: [ - () => new BoomCrypto() + new BoomCrypto() ] - })) - - const dialerUpgraderUpgradeOutboundSpy = Sinon.spy(dialerComponents.upgrader, 'upgradeOutbound') - const listenerUpgraderUpgradeInboundSpy = Sinon.spy(listenerComponents.upgrader, 'upgradeInbound') + }) - await expect(dialer.dial(listener.getMultiaddrs())).to.eventually.be.rejected + await expect(upgrader.upgradeOutbound(maConn)).to.eventually.be.rejected .with.property('name', 'EncryptionFailedError') + }) - // Ensure both sides fail - 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 abort if inbound upgrade is slow', async () => { + const upgrader = new DefaultUpgrader(await createDefaultUpgraderComponents(), { + ...init, + inboundUpgradeTimeout: 100 + }) + + maConn.source = map(maConn.source, async (buf) => { + await delay(2000) + return buf + }) + + await expect(upgrader.upgradeOutbound(maConn)).to.eventually.be.rejected + .with.property('message').that.include('aborted') }) - it('should clear timeout if upgrade is successful', async () => { - ({ dialer, dialerComponents, listener, listenerComponents } = await createPeers({ - connectionManager: { - inboundUpgradeTimeout: 100 - } - }, { - connectionManager: { - inboundUpgradeTimeout: 100 - } - })) + it('should abort if outbound upgrade is slow', async () => { + const upgrader = new DefaultUpgrader(await createDefaultUpgraderComponents(), { + ...init, + outboundUpgradeTimeout: 100 + }) - await dialer.dial(listener.getMultiaddrs()) + maConn.source = map(maConn.source, async (buf) => { + await delay(2000) + return buf + }) - await delay(1000) + await expect(upgrader.upgradeOutbound(maConn)).to.eventually.be.rejected + .with.property('message').that.include('aborted') + }) - // 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 abort by signal if inbound upgrade is slow', async () => { + const upgrader = new DefaultUpgrader(await createDefaultUpgraderComponents(), { + ...init, + inboundUpgradeTimeout: 10000 + }) + + maConn.source = map(maConn.source, async (buf) => { + await delay(2000) + return buf + }) + + await expect(upgrader.upgradeOutbound(maConn, { + signal: AbortSignal.timeout(100) + })).to.eventually.be.rejected + .with.property('message').that.include('aborted') }) - it('should not abort if upgrade is successful', async () => { - ({ dialer, dialerComponents, listener, listenerComponents } = await createPeers({ - connectionManager: { - inboundUpgradeTimeout: 10000 - } - }, { - connectionManager: { - inboundUpgradeTimeout: 10000 - } - })) + it('should abort by signal if outbound upgrade is slow', async () => { + const upgrader = new DefaultUpgrader(await createDefaultUpgraderComponents(), { + ...init, + outboundUpgradeTimeout: 10000 + }) + + maConn.source = map(maConn.source, async (buf) => { + await delay(2000) + return buf + }) + + await expect(upgrader.upgradeOutbound(maConn, { + signal: AbortSignal.timeout(100) + })).to.eventually.be.rejected + .with.property('message').that.include('aborted') + }) - await dialer.dial(listener.getMultiaddrs(), { - signal: AbortSignal.timeout(500) + it('should not abort if inbound upgrade is successful', async () => { + const upgrader = new DefaultUpgrader(await createDefaultUpgraderComponents(), { + ...init, + inboundUpgradeTimeout: 100 }) + const conn = await upgrader.upgradeInbound(maConn) await delay(1000) // 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) + expect(conn.status).to.equal('open') }) - 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' - }) - ] - })) - - const dialerUpgraderUpgradeOutboundSpy = Sinon.spy(dialerComponents.upgrader, 'upgradeOutbound') - const listenerUpgraderUpgradeInboundSpy = Sinon.spy(listenerComponents.upgrader, 'upgradeInbound') + it('should not abort if outbound upgrade is successful', async () => { + const upgrader = new DefaultUpgrader(await createDefaultUpgraderComponents(), { + ...init, + inboundUpgradeTimeout: 100 + }) + const conn = await upgrader.upgradeOutbound(maConn) - await expect(dialer.dial(listener.getMultiaddrs())).to.eventually.be.rejected - .with.property('name', 'MuxerUnavailableError') + await delay(1000) - // 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') + // connections should still be open after timeout + expect(conn.status).to.equal('open') }) - 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() - - dialerComponents.events.addEventListener('connection:open', (event) => { - expect(event.detail.remotePeer.equals(listener.peerId)).to.be.true() - localConnectionEventReceived.resolve() - }) - dialerComponents.events.addEventListener('connection:close', (event) => { - expect(event.detail.remotePeer.equals(listener.peerId)).to.be.true() - localConnectionEndEventReceived.resolve() - }) - 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() - }) - 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([ - ...dialer.getConnections(listener.peerId), - ...listener.getConnections(dialer.peerId) - ]) - expect(connections).to.have.lengthOf(2) - - await Promise.all([ - localConnectionEventReceived.promise, - localPeerConnectEventReceived.promise, - remoteConnectionEventReceived.promise, - remotePeerConnectEventReceived.promise - ]) - - // Verify onConnectionEnd is called with the connection - await Promise.all(connections.map(async conn => { await conn.close() })) - - await Promise.all([ - localConnectionEndEventReceived.promise, - localPeerDisconnectEventReceived.promise, - remoteConnectionEndEventReceived.promise, - remotePeerDisconnectEventReceived.promise - ]) - }) + it('should not abort by signal if inbound upgrade is successful', async () => { + const upgrader = new DefaultUpgrader(await createDefaultUpgraderComponents(), { + ...init, + inboundUpgradeTimeout: 10000 + }) + const conn = await upgrader.upgradeInbound(maConn, { + signal: AbortSignal.timeout(100) + }) + + await delay(1000) - it('should fail to create a stream for an unsupported protocol', async () => { - ({ dialer, listener } = await createPeers()) + // connections should still be open after timeout + expect(conn.status).to.equal('open') + }) - await dialer.dial(listener.getMultiaddrs()) + it('should not abort by signal if outbound upgrade is successful', async () => { + const upgrader = new DefaultUpgrader(await createDefaultUpgraderComponents(), { + ...init, + inboundUpgradeTimeout: 10000 + }) + const conn = await upgrader.upgradeOutbound(maConn, { + signal: AbortSignal.timeout(100) + }) - const connections = await Promise.all([ - ...dialer.getConnections(listener.peerId), - ...listener.getConnections(dialer.peerId) - ]) - expect(connections).to.have.lengthOf(2) + await delay(1000) - 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') + // connections should still be open after timeout + expect(conn.status).to.equal('open') }) - it('should abort protocol selection for slow stream creation', async () => { - ({ dialer, listener } = await createPeers({ + it('should abort protocol selection for slow outbound stream creation', async () => { + const upgrader = new DefaultUpgrader(await createDefaultUpgraderComponents(), { + ...init, streamMuxers: [ - slowMuxer(1000) + stubInterface({ + protocol: muxerProtocol, + createStreamMuxer: () => stubInterface({ + protocol: muxerProtocol, + sink: async (source) => drain(source), + source: (async function * () {})(), + newStream: () => stubInterface({ + id: 'stream-id', + log: logger('test-stream'), + sink: async (source) => drain(source), + source: (async function * (): any { + await delay(2000) + yield Uint8Array.from([0, 1, 2, 3, 4]) + })() + }) + }) + }) ] - })) - - const connection = await dialer.dial(listener.getMultiaddrs()) + }) + const conn = await upgrader.upgradeOutbound(maConn) - await expect(connection.newStream('/echo/1.0.0', { + await expect(conn.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 () => { - ({ dialer, listener } = await createPeers()) + it('should abort stream when protocol negotiation fails on outbound stream', async () => { + let stream: Stream | undefined - await dialer.dial(listener.getMultiaddrs()) - - const connections = await Promise.all([ - ...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) + const upgrader = new DefaultUpgrader(await createDefaultUpgraderComponents(), { + ...init, + streamMuxers: [ + stubInterface({ + protocol: muxerProtocol, + createStreamMuxer: () => stubInterface({ + protocol: muxerProtocol, + sink: async (source) => drain(source), + source: (async function * () { + await delay(2000) + yield Uint8Array.from([0, 1, 2, 3, 4]) + })(), + newStream: () => { + stream = stubInterface({ + id: 'stream-id', + log: logger('test-stream'), + sink: async (source) => drain(source), + source: map((async function * () { + yield '/multistream/1.0.0\n' + yield '/different/protocol\n' + })(), str => encode.single(uint8ArrayFromString(str))) + }) + + return stream + } + }) + }) + ] + }) + const conn = await upgrader.upgradeOutbound(maConn) - await expect(connections[0].newStream('/foo/1.0.0')) + await expect(conn.newStream('/foo/1.0.0')) .to.eventually.be.rejected.with.property('name', 'UnsupportedProtocolError') // wait for remote to close await delay(100) - expect(connections[0].streams).to.have.lengthOf(0) - expect(connections[1].streams).to.have.lengthOf(0) + expect(stream?.abort).to.have.property('called', true) }) - it('should allow skipping encryption and protection', async () => { - const protector = stubInterface() - const encrypter = stubInterface() + it('should allow skipping outbound encryption and protection', async () => { + const connectionProtector = stubInterface() + const connectionEncrypter = stubInterface({ + protocol: encrypterProtocol + }) - ;({ dialer, listener } = await createPeers({ - transports: [ - memory({ - upgraderOptions: { - skipEncryption: true, - skipProtection: true - } - }) - ], + const upgrader = new DefaultUpgrader(await createDefaultUpgraderComponents({ + connectionProtector + }), { + ...init, connectionEncrypters: [ - () => encrypter - ], - connectionProtector: () => protector - }, { - transports: [ - memory({ - upgraderOptions: { - skipEncryption: true, - skipProtection: true - } + connectionEncrypter + ] + }) + await upgrader.upgradeOutbound(maConn, { + skipEncryption: true, + skipProtection: true, + muxerFactory: stubInterface({ + createStreamMuxer: () => stubInterface({ + protocol: muxerProtocol, + sink: async (source) => drain(source), + source: (async function * () {})() }) - ], - connectionEncrypters: [ - () => encrypter - ], - connectionProtector: () => protector - })) + }) + }) + expect(connectionProtector.protect).to.have.property('called', false) + expect(connectionEncrypter.secureOutbound).to.have.property('called', false) + }) - 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 allow skipping inbound encryption and protection', async () => { + const connectionProtector = stubInterface() + const connectionEncrypter = stubInterface({ + protocol: encrypterProtocol + }) - 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') + const upgrader = new DefaultUpgrader(await createDefaultUpgraderComponents({ + connectionProtector + }), { + ...init, + connectionEncrypters: [ + connectionEncrypter + ] + }) + await upgrader.upgradeInbound(maConn, { + skipEncryption: true, + skipProtection: true, + muxerFactory: stubInterface({ + createStreamMuxer: () => stubInterface({ + protocol: muxerProtocol, + sink: async (source) => drain(source), + source: (async function * () {})() + }) + }) + }) + expect(connectionProtector.protect).to.have.property('called', false) + expect(connectionEncrypter.secureOutbound).to.have.property('called', false) }) it('should not decrement inbound pending connection count if the connection is denied', async () => { - ({ dialer, dialerComponents, listener, listenerComponents } = await createPeers()) - - listenerComponents.connectionManager.acceptIncomingConnection = async () => false - const afterUpgradeInboundSpy = Sinon.spy(listenerComponents.connectionManager, 'afterUpgradeInbound') - - await expect(dialer.dial(listener.getMultiaddrs())).to.eventually.be.rejected - .with.property('message', 'Connection denied') + const components = await createDefaultUpgraderComponents({ + connectionManager: stubInterface({ + acceptIncomingConnection: async () => false + }) + }) + const upgrader = new DefaultUpgrader(components, init) + await expect(upgrader.upgradeInbound(maConn)).to.eventually.be.rejected + .with.property('name', 'ConnectionDeniedError') - expect(afterUpgradeInboundSpy.called).to.be.false() + expect(components.connectionManager.afterUpgradeInbound).to.have.property('called', false) }) it('should limit the number of incoming streams that can be opened using a protocol', async () => { - ({ dialer, listener } = await createPeers()) - - const protocol = '/a-test-protocol/1.0.0' - - await listener.handle(protocol, () => {}, { - maxInboundStreams: 2, - maxOutboundStreams: 2 + const protocol = '/test/protocol' + const maxInboundStreams = 2 + let streamMuxerInit: StreamMuxerInit | undefined + let streamMuxer: StreamMuxer | undefined + const components = await createDefaultUpgraderComponents({ + registrar: stubInterface({ + getHandler: () => ({ + options: { + maxInboundStreams + }, + handler: Sinon.stub() + }), + getProtocols: () => [protocol] + }) + }) + const upgrader = new DefaultUpgrader(components, { + ...init, + streamMuxers: [ + stubInterface({ + protocol: muxerProtocol, + createStreamMuxer: (init) => { + streamMuxerInit = init + streamMuxer = stubInterface({ + protocol: muxerProtocol, + sink: async (source) => drain(source), + source: (async function * () {})(), + streams: [] + }) + return streamMuxer + } + }) + ] }) - const connection = await dialer.dial(listener.getMultiaddrs()) - expect(connection.streams).to.have.lengthOf(0) - - await connection.newStream(protocol) - await connection.newStream(protocol) - - expect(connection.streams).to.have.lengthOf(2) + const conn = await upgrader.upgradeInbound(maConn) + expect(conn.streams).to.have.lengthOf(0) + + for (let i = 0; i < (maxInboundStreams + 1); i++) { + const incomingStream = stubInterface({ + id: `stream-id-${i}`, + log: logger('test-stream'), + direction: 'inbound', + sink: async (source) => drain(source), + source: map((async function * () { + yield '/multistream/1.0.0\n' + yield `${protocol}\n` + })(), str => encode.single(uint8ArrayFromString(str))) + }) + + streamMuxer?.streams.push(incomingStream) + streamMuxerInit?.onIncomingStream?.(incomingStream) + } - const stream = await connection.newStream(protocol) + await delay(100) - await expect(drain(stream.source)).to.eventually.be.rejected() - .with.property('name', 'StreamResetError') + expect(streamMuxer?.streams).to.have.lengthOf(3) + expect(streamMuxer?.streams[0]).to.have.nested.property('abort.called', false) + expect(streamMuxer?.streams[1]).to.have.nested.property('abort.called', false) + expect(streamMuxer?.streams[2]).to.have.nested.property('abort.called', true) }) it('should limit the number of outgoing streams that can be opened using a protocol', async () => { - ({ dialer, listener } = await createPeers()) - - const protocol = '/a-test-protocol/1.0.0' - - await listener.handle(protocol, () => {}, { - maxInboundStreams: 20, - maxOutboundStreams: 20 + const protocol = '/test/protocol' + const maxOutboundStreams = 2 + let streamMuxer: StreamMuxer | undefined + const components = await createDefaultUpgraderComponents({ + registrar: stubInterface({ + getHandler: () => ({ + options: { + maxOutboundStreams + }, + handler: Sinon.stub() + }), + getProtocols: () => [protocol] + }) }) - - await dialer.handle(protocol, () => {}, { - maxInboundStreams: 2, - maxOutboundStreams: 2 + const upgrader = new DefaultUpgrader(components, { + ...init, + streamMuxers: [ + stubInterface({ + protocol: muxerProtocol, + createStreamMuxer: () => { + streamMuxer = stubInterface({ + protocol: muxerProtocol, + sink: async (source) => drain(source), + source: (async function * () {})(), + streams: [], + newStream: () => { + const outgoingStream = stubInterface({ + id: 'stream-id', + log: logger('test-stream'), + direction: 'outbound', + sink: async (source) => drain(source), + source: map((async function * () { + yield '/multistream/1.0.0\n' + yield `${protocol}\n` + })(), str => encode.single(uint8ArrayFromString(str))) + }) + + streamMuxer?.streams.push(outgoingStream) + return outgoingStream + } + }) + return streamMuxer + } + }) + ] }) - const connection = await dialer.dial(listener.getMultiaddrs()) - expect(connection.streams).to.have.lengthOf(0) - - await connection.newStream(protocol) - await connection.newStream(protocol) + const conn = await upgrader.upgradeInbound(maConn) + expect(conn.streams).to.have.lengthOf(0) - expect(connection.streams).to.have.lengthOf(2) + await conn.newStream(protocol) + await conn.newStream(protocol) - await expect(connection.newStream(protocol)).to.eventually.be.rejected() + await expect(conn.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 () => { - ({ dialer, listener } = await createPeers()) - - const protocol = '/a-test-protocol/1.0.0' - - await listener.handle(protocol, () => {}, { - maxInboundStreams: 20, - maxOutboundStreams: 20 + const protocol = '/test/protocol' + let streamMuxer: StreamMuxer | undefined + const components = await createDefaultUpgraderComponents({ + registrar: stubInterface({ + getHandler: () => ({ + options: {}, + handler: Sinon.stub() + }), + getProtocols: () => [protocol] + }) + }) + const upgrader = new DefaultUpgrader(components, { + ...init, + streamMuxers: [ + stubInterface({ + protocol: muxerProtocol, + createStreamMuxer: () => { + streamMuxer = stubInterface({ + protocol: muxerProtocol, + sink: async (source) => drain(source), + source: (async function * () {})(), + streams: [], + newStream: () => { + const outgoingStream = stubInterface({ + id: 'stream-id', + log: logger('test-stream'), + direction: 'outbound', + sink: async (source) => drain(source), + source: map((async function * () { + yield '/multistream/1.0.0\n' + yield `${protocol}\n` + })(), str => encode.single(uint8ArrayFromString(str))) + }) + + streamMuxer?.streams.push(outgoingStream) + return outgoingStream + } + }) + return streamMuxer + } + }) + ] }) - const connection = await dialer.dial(listener.getMultiaddrs()) - expect(connection.streams).to.have.lengthOf(0) + const conn = await upgrader.upgradeInbound(maConn) + expect(conn.streams).to.have.lengthOf(0) const opts = { - maxOutboundStreams: 2 + maxOutboundStreams: 3 } - await connection.newStream(protocol, opts) - await connection.newStream(protocol, opts) - - expect(connection.streams).to.have.lengthOf(2) + await conn.newStream(protocol, opts) + await conn.newStream(protocol, opts) + await conn.newStream(protocol, opts) - await expect(connection.newStream(protocol, opts)).to.eventually.be.rejected() + await expect(conn.newStream(protocol, opts)).to.eventually.be.rejected .with.property('name', 'TooManyOutboundProtocolStreamsError') }) }) diff --git a/packages/libp2p/test/upgrading/utils.ts b/packages/libp2p/test/upgrading/utils.ts new file mode 100644 index 0000000000..88b0c23bf9 --- /dev/null +++ b/packages/libp2p/test/upgrading/utils.ts @@ -0,0 +1,39 @@ +/* eslint-env mocha */ + +import { generateKeyPair } from '@libp2p/crypto/keys' +import { TypedEventEmitter } from '@libp2p/interface' +import { defaultLogger } from '@libp2p/logger' +import { peerIdFromPrivateKey } from '@libp2p/peer-id' +import { stubInterface, type StubbedInstance } from 'sinon-ts' +import type { DefaultUpgraderComponents } from '../../src/upgrader.js' +import type { ConnectionGater, PeerId, PeerStore, TypedEventTarget, Libp2pEvents, ComponentLogger, Metrics, ConnectionProtector } from '@libp2p/interface' +import type { ConnectionManager, Registrar } from '@libp2p/interface-internal' + +export interface StubbedDefaultUpgraderComponents { + peerId: PeerId + metrics?: StubbedInstance + connectionManager: StubbedInstance + connectionGater: StubbedInstance + connectionProtector?: StubbedInstance + registrar: StubbedInstance + peerStore: StubbedInstance + events: TypedEventTarget + logger: ComponentLogger +} + +export async function createDefaultUpgraderComponents (options?: Partial): Promise { + return { + peerId: peerIdFromPrivateKey(await generateKeyPair('Ed25519')), + connectionManager: stubInterface({ + acceptIncomingConnection: async () => true + }), + connectionGater: stubInterface(), + registrar: stubInterface(), + peerStore: stubInterface({ + all: async () => [] + }), + events: new TypedEventEmitter(), + logger: defaultLogger(), + ...options + } as unknown as any +}