diff --git a/.release-please.json b/.release-please.json index cda47e94f2..baff6c7215 100644 --- a/.release-please.json +++ b/.release-please.json @@ -22,6 +22,7 @@ "packages/peer-store": {}, "packages/protocol-autonat": {}, "packages/protocol-perf": {}, + "packages/protocol-ping": {}, "packages/pubsub": {}, "packages/pubsub-floodsub": {}, "packages/stream-multiplexer-mplex": {}, diff --git a/doc/CONFIGURATION.md b/doc/CONFIGURATION.md index 934613242e..4bbeac8c13 100644 --- a/doc/CONFIGURATION.md +++ b/doc/CONFIGURATION.md @@ -987,14 +987,14 @@ Changing the protocol name prefix can isolate default public network (IPFS) for ```js import { createLibp2p } from 'libp2p' import { identifyService } from 'libp2p/identify' -import { pingService } from 'libp2p/ping' +import { ping } from 'libp2p/@ping' const node = await createLibp2p({ services: { identify: identifyService({ protocolPrefix: 'ipfs' // default }), - ping: pingService({ + ping: ping({ protocolPrefix: 'ipfs' // default }) } @@ -1003,7 +1003,7 @@ const node = await createLibp2p({ protocols: [ "/ipfs/id/1.0.0", // identify service protocol (if we have multiplexers) "/ipfs/id/push/1.0.0", // identify service push protocol (if we have multiplexers) - "/ipfs/ping/1.0.0", // built-in ping protocol + "/ipfs/ping/1.0.0", // ping protocol ] */ ``` diff --git a/doc/migrations/v0.46-v1.0.0.md b/doc/migrations/v0.46-v1.0.0.md index 0d76ab4305..fd7b14375a 100644 --- a/doc/migrations/v0.46-v1.0.0.md +++ b/doc/migrations/v0.46-v1.0.0.md @@ -6,6 +6,7 @@ A migration guide for refactoring your application code from libp2p `v0.46` to ` ## Table of Contents - [AutoNAT](#autonat) +- [Ping](#ping) - [KeyChain](#keychain) - [UPnPNat](#upnpnat) - [Pnet](#pnet) @@ -41,6 +42,36 @@ const node = await createLibp2p({ }) ``` +## Ping + +The Ping service is now published in its own package. + +**Before** + +```ts +import { createLibp2p } from 'libp2p' +import { pingService } from 'libp2p/ping' + +const node = await createLibp2p({ + services: { + ping: pingService() + } +}) +``` + +**After** + +```ts +import { createLibp2p } from 'libp2p' +import { ping } from '@libp2p/ping' + +const node = await createLibp2p({ + services: { + ping: ping() + } +}) +``` + ## KeyChain The KeyChain object is no longer included on Libp2p and must be instantiated explicitly if desired. diff --git a/interop/README.md b/interop/README.md index 8c59833650..209fa6bb64 100644 --- a/interop/README.md +++ b/interop/README.md @@ -7,7 +7,7 @@ > Multidimensional interop tests -# Install +# Install ```console $ npm i @libp2p/multidim-interop @@ -57,7 +57,7 @@ $ docker build . -f ./interop/BrowserDockerfile -t js-libp2p-browsers $ git clone https://github.com/libp2p/test-plans.git ``` 2. (Optional) If you are running an M1 Mac you may need to override the build platform. - - Edit `/multidim-interop/dockerBuildWrapper.sh` + - Edit `/transport-interop/dockerBuildWrapper.sh` - Add `--platform linux/arm64/v8` to the `docker buildx build` command ``` docker buildx build \ @@ -67,7 +67,7 @@ $ docker build . -f ./interop/BrowserDockerfile -t js-libp2p-browsers ``` 3. (Optional) Enable some sort of debug output - nim-libp2p - - edit `/multidim-interop/impl/nim/$VERSION/Dockerfile` + - edit `/transport-interop/impl/nim/$VERSION/Dockerfile` - Change `-d:chronicles_log_level=WARN` to `-d:chronicles_log_level=DEBUG` - rust-libp2p - When starting the docker container add `-e RUST_LOG=debug` diff --git a/interop/package.json b/interop/package.json index 979628ee10..a25b3f51da 100644 --- a/interop/package.json +++ b/interop/package.json @@ -54,6 +54,7 @@ "@chainsafe/libp2p-noise": "^13.0.0", "@chainsafe/libp2p-yamux": "^5.0.0", "@libp2p/mplex": "^9.0.12", + "@libp2p/ping": "^0.0.0", "@libp2p/tcp": "^8.0.13", "@libp2p/webrtc": "^3.2.10", "@libp2p/websockets": "^7.0.13", diff --git a/interop/test/ping.spec.ts b/interop/test/ping.spec.ts index 75cf38ca0d..5e65237fce 100644 --- a/interop/test/ping.spec.ts +++ b/interop/test/ping.spec.ts @@ -5,6 +5,7 @@ import { } from 'aegir/chai' import { noise } from '@chainsafe/libp2p-noise' import { yamux } from '@chainsafe/libp2p-yamux' import { mplex } from '@libp2p/mplex' +import { ping, type PingService } from '@libp2p/ping' import { tcp } from '@libp2p/tcp' import { webRTC, webRTCDirect } from '@libp2p/webrtc' import { webSockets } from '@libp2p/websockets' @@ -14,7 +15,6 @@ import { type Multiaddr, multiaddr } from '@multiformats/multiaddr' import { createLibp2p, type Libp2p, type Libp2pOptions } from 'libp2p' import { circuitRelayTransport } from 'libp2p/circuit-relay' import { type IdentifyService, identifyService } from 'libp2p/identify' -import { pingService, type PingService } from 'libp2p/ping' async function redisProxy (commands: any[]): Promise { const res = await fetch(`http://localhost:${process.env.proxyPort ?? ''}/`, { body: JSON.stringify(commands), method: 'POST' }) @@ -49,7 +49,7 @@ describe('ping test', function () { denyDialMultiaddr: async () => false }, services: { - ping: pingService(), + ping: ping(), identify: identifyService() } } diff --git a/interop/tsconfig.json b/interop/tsconfig.json index 5be3797bc7..b6bf4185ad 100644 --- a/interop/tsconfig.json +++ b/interop/tsconfig.json @@ -11,6 +11,9 @@ { "path": "../packages/libp2p" }, + { + "path": "../packages/protocol-ping" + }, { "path": "../packages/stream-multiplexer-mplex" }, diff --git a/packages/libp2p/.aegir.js b/packages/libp2p/.aegir.js index a0d8cbc706..ff622a2e8c 100644 --- a/packages/libp2p/.aegir.js +++ b/packages/libp2p/.aegir.js @@ -18,7 +18,6 @@ export default { const { plaintext } = await import('./dist/src/insecure/index.js') const { circuitRelayServer, circuitRelayTransport } = await import('./dist/src/circuit-relay/index.js') const { identifyService } = await import('./dist/src/identify/index.js') - const { pingService } = await import('./dist/src/ping/index.js') const { fetchService } = await import('./dist/src/fetch/index.js') const peerId = await createEd25519PeerId() @@ -47,7 +46,6 @@ export default { ], services: { identify: identifyService(), - ping: pingService(), fetch: fetchService(), relay: circuitRelayServer({ reservations: { diff --git a/packages/libp2p/package.json b/packages/libp2p/package.json index 3605fa17db..5b07feb654 100644 --- a/packages/libp2p/package.json +++ b/packages/libp2p/package.json @@ -67,10 +67,6 @@ "./insecure": { "types": "./dist/src/insecure/index.d.ts", "import": "./dist/src/insecure/index.js" - }, - "./ping": { - "types": "./dist/src/ping/index.d.ts", - "import": "./dist/src/ping/index.js" } }, "eslintConfig": { diff --git a/packages/libp2p/test/configuration/protocol-prefix.node.ts b/packages/libp2p/test/configuration/protocol-prefix.node.ts deleted file mode 100644 index db4ecea61c..0000000000 --- a/packages/libp2p/test/configuration/protocol-prefix.node.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* eslint-env mocha */ - -import { expect } from 'aegir/chai' -import { pEvent } from 'p-event' -import { type FetchService, fetchService } from '../../src/fetch/index.js' -import { identifyService } from '../../src/identify/index.js' -import { createLibp2p } from '../../src/index.js' -import { type PingService, pingService } from '../../src/ping/index.js' -import { createBaseOptions } from '../fixtures/base-options.js' -import type { Libp2p } from '@libp2p/interface' - -describe('Protocol prefix is configurable', () => { - let libp2p: Libp2p<{ identify: unknown, ping: PingService, fetch: FetchService }> - - afterEach(async () => { - if (libp2p != null) { - await libp2p.stop() - } - }) - - it('protocolPrefix is provided', async () => { - const testProtocol = 'test-protocol' - libp2p = await createLibp2p(createBaseOptions({ - services: { - identify: identifyService({ - protocolPrefix: testProtocol - }), - ping: pingService({ - protocolPrefix: testProtocol - }), - fetch: fetchService({ - protocolPrefix: testProtocol - }) - }, - start: false - })) - - const eventPromise = pEvent(libp2p, 'self:peer:update') - await libp2p.start() - await eventPromise - - const peer = await libp2p.peerStore.get(libp2p.peerId) - expect(peer.protocols).to.include.members([ - `/${testProtocol}/fetch/0.0.1`, - `/${testProtocol}/id/1.0.0`, - `/${testProtocol}/id/push/1.0.0`, - `/${testProtocol}/ping/1.0.0` - ]) - }) - - it('protocolPrefix is not provided', async () => { - libp2p = await createLibp2p(createBaseOptions({ - services: { - identify: identifyService(), - ping: pingService(), - fetch: fetchService() - }, - start: false - })) - - const eventPromise = pEvent(libp2p, 'self:peer:update') - await libp2p.start() - await eventPromise - - const peer = await libp2p.peerStore.get(libp2p.peerId) - expect(peer.protocols).to.include.members([ - '/ipfs/id/1.0.0', - '/ipfs/id/push/1.0.0', - '/ipfs/ping/1.0.0', - '/libp2p/fetch/0.0.1' - ]) - }) -}) diff --git a/packages/libp2p/test/ping/index.spec.ts b/packages/libp2p/test/ping/index.spec.ts deleted file mode 100644 index dd6c877f50..0000000000 --- a/packages/libp2p/test/ping/index.spec.ts +++ /dev/null @@ -1,136 +0,0 @@ -/* eslint-env mocha */ - -import { ERR_TIMEOUT } from '@libp2p/interface/errors' -import { TypedEventEmitter } from '@libp2p/interface/events' -import { start, stop } from '@libp2p/interface/startable' -import { mockRegistrar, mockUpgrader, connectionPair } from '@libp2p/interface-compliance-tests/mocks' -import { createEd25519PeerId } from '@libp2p/peer-id-factory' -import { PersistentPeerStore } from '@libp2p/peer-store' -import { expect } from 'aegir/chai' -import { MemoryDatastore } from 'datastore-core' -import delay from 'delay' -import { pipe } from 'it-pipe' -import sinon from 'sinon' -import { stubInterface } from 'sinon-ts' -import { defaultComponents, type Components } from '../../src/components.js' -import { DefaultConnectionManager } from '../../src/connection-manager/index.js' -import { PROTOCOL } from '../../src/ping/constants.js' -import { pingService, type PingServiceInit } from '../../src/ping/index.js' -import type { ConnectionGater } from '@libp2p/interface/connection-gater' -import type { TransportManager } from '@libp2p/interface-internal/transport-manager' - -const defaultInit: PingServiceInit = { - protocolPrefix: 'ipfs', - maxInboundStreams: 1, - maxOutboundStreams: 1, - timeout: 1000 -} - -async function createComponents (index: number): Promise { - const peerId = await createEd25519PeerId() - - const events = new TypedEventEmitter() - const components = defaultComponents({ - peerId, - registrar: mockRegistrar(), - upgrader: mockUpgrader({ events }), - datastore: new MemoryDatastore(), - transportManager: stubInterface(), - connectionGater: stubInterface(), - events - }) - components.peerStore = new PersistentPeerStore(components) - components.connectionManager = new DefaultConnectionManager(components, { - minConnections: 50, - maxConnections: 1000, - inboundUpgradeTimeout: 1000 - }) - - return components -} - -describe('ping', () => { - let localComponents: Components - let remoteComponents: Components - - beforeEach(async () => { - localComponents = await createComponents(0) - remoteComponents = await createComponents(1) - - await Promise.all([ - start(localComponents), - start(remoteComponents) - ]) - }) - - afterEach(async () => { - sinon.restore() - - await Promise.all([ - stop(localComponents), - stop(remoteComponents) - ]) - }) - - it('should be able to ping another peer', async () => { - const localPing = pingService(defaultInit)(localComponents) - const remotePing = pingService(defaultInit)(remoteComponents) - - await start(localPing) - await start(remotePing) - - // simulate connection between nodes - const [localToRemote, remoteToLocal] = connectionPair(localComponents, remoteComponents) - localComponents.events.safeDispatchEvent('connection:open', { detail: localToRemote }) - remoteComponents.events.safeDispatchEvent('connection:open', { detail: remoteToLocal }) - - // Run ping - await expect(localPing.ping(remoteComponents.peerId)).to.eventually.be.gte(0) - }) - - it('should time out pinging another peer when waiting for a pong', async () => { - const localPing = pingService(defaultInit)(localComponents) - const remotePing = pingService(defaultInit)(remoteComponents) - - await start(localPing) - await start(remotePing) - - // simulate connection between nodes - const [localToRemote, remoteToLocal] = connectionPair(localComponents, remoteComponents) - localComponents.events.safeDispatchEvent('connection:open', { detail: localToRemote }) - remoteComponents.events.safeDispatchEvent('connection:open', { detail: remoteToLocal }) - - // replace existing handler with a really slow one - await remoteComponents.registrar.unhandle(PROTOCOL) - await remoteComponents.registrar.handle(PROTOCOL, ({ stream }) => { - void pipe( - stream, - async function * (source) { - for await (const chunk of source) { - // longer than the timeout - await delay(1000) - - yield chunk - } - }, - stream - ) - }) - - const newStreamSpy = sinon.spy(localToRemote, 'newStream') - - // 10 ms timeout - const signal = AbortSignal.timeout(10) - - // Run ping, should time out - await expect(localPing.ping(remoteComponents.peerId, { - signal - })) - .to.eventually.be.rejected.with.property('code', ERR_TIMEOUT) - - // should have closed stream - expect(newStreamSpy).to.have.property('callCount', 1) - const stream = await newStreamSpy.getCall(0).returnValue - expect(stream).to.have.nested.property('timeline.close') - }) -}) diff --git a/packages/libp2p/test/ping/ping.node.ts b/packages/libp2p/test/ping/ping.node.ts deleted file mode 100644 index d10f355031..0000000000 --- a/packages/libp2p/test/ping/ping.node.ts +++ /dev/null @@ -1,152 +0,0 @@ -/* eslint-env mocha */ - -import { multiaddr } from '@multiformats/multiaddr' -import { expect } from 'aegir/chai' -import { pipe } from 'it-pipe' -import pDefer from 'p-defer' -import { PROTOCOL } from '../../src/ping/constants.js' -import { pingService, type PingService } from '../../src/ping/index.js' -import { createBaseOptions } from '../fixtures/base-options.js' -import { createNode, populateAddressBooks } from '../fixtures/creators/peer.js' -import type { Libp2p } from '@libp2p/interface' - -describe('ping', () => { - let nodes: Array> - - beforeEach(async () => { - nodes = await Promise.all([ - createNode({ - config: createBaseOptions({ - addresses: { - listen: [ - '/ip4/0.0.0.0/tcp/0' - ] - }, - services: { - ping: pingService() - } - }) - }), - createNode({ - config: createBaseOptions({ - addresses: { - listen: [ - '/ip4/0.0.0.0/tcp/0' - ] - }, - services: { - ping: pingService() - } - }) - }), - createNode({ - config: createBaseOptions({ - addresses: { - listen: [ - '/ip4/0.0.0.0/tcp/0' - ] - }, - services: { - ping: pingService() - } - }) - }) - ]) - await populateAddressBooks(nodes) - - await nodes[0].peerStore.patch(nodes[1].peerId, { - multiaddrs: nodes[1].getMultiaddrs() - }) - await nodes[1].peerStore.patch(nodes[0].peerId, { - multiaddrs: nodes[0].getMultiaddrs() - }) - }) - - afterEach(async () => Promise.all(nodes.map(async n => { await n.stop() }))) - - it('ping once from peer0 to peer1 using a multiaddr', async () => { - const ma = multiaddr(nodes[2].getMultiaddrs()[0]) - const latency = await nodes[0].services.ping.ping(ma) - - expect(latency).to.be.a('Number') - }) - - it('ping once from peer0 to peer1 using a peerId', async () => { - const latency = await nodes[0].services.ping.ping(nodes[1].peerId) - - expect(latency).to.be.a('Number') - }) - - it('ping several times for getting an average', async () => { - const latencies = [] - - for (let i = 0; i < 5; i++) { - latencies.push(await nodes[1].services.ping.ping(nodes[0].peerId)) - } - - const averageLatency = latencies.reduce((p, c) => p + c, 0) / latencies.length - expect(averageLatency).to.be.a('Number') - }) - - it('only waits for the first response to arrive', async () => { - const defer = pDefer() - - await nodes[1].unhandle(PROTOCOL) - await nodes[1].handle(PROTOCOL, ({ stream }) => { - void pipe( - stream, - async function * (stream) { - for await (const data of stream) { - yield data - - // something longer than the test timeout - await defer.promise - } - }, - stream - ) - }, { - runOnTransientConnection: true - }) - - const latency = await nodes[0].services.ping.ping(nodes[1].peerId) - - expect(latency).to.be.a('Number') - - defer.resolve() - }) - - it('allows two incoming streams from the same peer', async () => { - const remote = nodes[0] - const client = await createNode({ - config: createBaseOptions({ - services: { - ping: pingService({ - // Allow two outbound ping streams. - // It is not allowed by the spec, but this test needs to open two concurrent streams. - maxOutboundStreams: 2 - }) - } - }) - }) - await client.components.peerStore.patch(remote.peerId, { - multiaddrs: remote.getMultiaddrs() - }) - // register our new node for shutdown after the test finishes - // otherwise the Mocha/Node.js process never finishes - nodes.push(client) - - // Send two ping requests in parallel, this should open two concurrent streams - const results = await Promise.allSettled([ - client.services.ping.ping(remote.peerId), - client.services.ping.ping(remote.peerId) - ]) - - // Verify that the remote peer accepted both inbound streams - expect(results.map(describe)).to.deep.equal(['fulfilled', 'fulfilled']) - - function describe (result: PromiseSettledResult): string { - return result.status === 'fulfilled' ? result.status : result.reason ?? result.status - } - }) -}) diff --git a/packages/libp2p/typedoc.json b/packages/libp2p/typedoc.json index feca398eb2..322fa7cee9 100644 --- a/packages/libp2p/typedoc.json +++ b/packages/libp2p/typedoc.json @@ -5,7 +5,6 @@ "./src/dcutr/index.ts", "./src/fetch/index.ts", "./src/identify/index.ts", - "./src/insecure/index.ts", - "./src/ping/index.ts" + "./src/insecure/index.ts" ] } diff --git a/packages/protocol-ping/LICENSE b/packages/protocol-ping/LICENSE new file mode 100644 index 0000000000..20ce483c86 --- /dev/null +++ b/packages/protocol-ping/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/protocol-ping/LICENSE-APACHE b/packages/protocol-ping/LICENSE-APACHE new file mode 100644 index 0000000000..14478a3b60 --- /dev/null +++ b/packages/protocol-ping/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/protocol-ping/LICENSE-MIT b/packages/protocol-ping/LICENSE-MIT new file mode 100644 index 0000000000..72dc60d84b --- /dev/null +++ b/packages/protocol-ping/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/protocol-ping/README.md b/packages/protocol-ping/README.md new file mode 100644 index 0000000000..e22c206cce --- /dev/null +++ b/packages/protocol-ping/README.md @@ -0,0 +1,56 @@ +[![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=master\&style=flat-square)](https://github.com/libp2p/js-libp2p/actions/workflows/main.yml?query=branch%3Amaster) + +> Implementation of Ping Protocol + +# About + +Use the `autoNATService` function to add support for the [AutoNAT protocol](https://docs.libp2p.io/concepts/nat/autonat/) +to libp2p. + +## Example + +```typescript +import { createLibp2p } from 'libp2p' +import { autoNATService } from '@libp2p/autonat' + +const node = await createLibp2p({ + // ...other options + services: { + autoNAT: autoNATService() + } +}) +``` + +# Install + +```console +$ npm i @libp2p/ping +``` + +## Browser ` +``` + +> Implementation of Autonat Protocol + +# API Docs + +- + +# License + +Licensed under either of + +- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) +- MIT ([LICENSE-MIT](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/protocol-ping/package.json b/packages/protocol-ping/package.json new file mode 100644 index 0000000000..09dbc95e52 --- /dev/null +++ b/packages/protocol-ping/package.json @@ -0,0 +1,65 @@ +{ + "name": "@libp2p/ping", + "version": "0.0.0", + "description": "Implementation of Ping Protocol", + "license": "Apache-2.0 OR MIT", + "homepage": "https://github.com/libp2p/js-libp2p/tree/master/packages/protocol-ping#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/libp2p/js-libp2p.git" + }, + "bugs": { + "url": "https://github.com/libp2p/js-libp2p/issues" + }, + "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": { + "build": "aegir build", + "test": "aegir test", + "clean": "aegir clean", + "lint": "aegir lint", + "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:node": "aegir test -t node --cov", + "dep-check": "aegir dep-check" + }, + "dependencies": { + "@libp2p/crypto": "^2.0.8", + "@libp2p/interface": "^0.1.2", + "@libp2p/interface-internal": "^0.1.5", + "@libp2p/logger": "^3.1.0", + "@libp2p/peer-id-factory": "^3.0.8", + "@multiformats/multiaddr": "^12.1.5", + "it-first": "^3.0.3", + "it-pipe": "^3.0.1", + "uint8arrays": "^4.0.6" + }, + "devDependencies": { + "aegir": "^41.0.2", + "it-byte-stream": "^1.0.1", + "it-pair": "^2.0.6", + "p-defer": "^4.0.0", + "sinon-ts": "^2.0.0" + } +} diff --git a/packages/libp2p/src/ping/constants.ts b/packages/protocol-ping/src/constants.ts similarity index 93% rename from packages/libp2p/src/ping/constants.ts rename to packages/protocol-ping/src/constants.ts index a7561558e1..73c08d3081 100644 --- a/packages/libp2p/src/ping/constants.ts +++ b/packages/protocol-ping/src/constants.ts @@ -13,3 +13,5 @@ export const TIMEOUT = 10000 // opening stream A even though the dialing peer is opening stream B and closing stream A). export const MAX_INBOUND_STREAMS = 2 export const MAX_OUTBOUND_STREAMS = 1 + +export const ERR_WRONG_PING_ACK = 'ERR_WRONG_PING_ACK' diff --git a/packages/protocol-ping/src/index.ts b/packages/protocol-ping/src/index.ts new file mode 100644 index 0000000000..3c3dbfbe5c --- /dev/null +++ b/packages/protocol-ping/src/index.ts @@ -0,0 +1,56 @@ +/** + * @packageDocumentation + * + * The ping service implements the [libp2p ping spec](https://github.com/libp2p/specs/blob/master/ping/ping.md) allowing you to make a latency measurement to a remote peer. + * + * @example + * + * ```typescript + * import { createLibp2p } from 'libp2p' + * import { ping } from '@libp2p/ping' + * import { multiaddr } from '@multiformats/multiaddr' + * + * const node = await createLibp2p({ + * services: { + * ping: ping() + * } + * }) + * + * const rtt = await node.services.ping.ping(multiaddr('/ip4/...')) + * + * console.info(rtt) + * ``` + */ + +import { PingService as PingServiceClass } from './ping.js' +import type { AbortOptions, ComponentLogger } from '@libp2p/interface' +import type { PeerId } from '@libp2p/interface/peer-id' +import type { ConnectionManager } from '@libp2p/interface-internal/connection-manager' +import type { Registrar } from '@libp2p/interface-internal/registrar' +import type { Multiaddr } from '@multiformats/multiaddr' + +export interface PingService { + ping(peer: PeerId | Multiaddr | Multiaddr[], options?: AbortOptions): Promise +} + +export interface PingServiceInit { + protocolPrefix?: string + maxInboundStreams?: number + maxOutboundStreams?: number + runOnTransientConnection?: boolean + + /** + * How long we should wait for a ping response + */ + timeout?: number +} + +export interface PingServiceComponents { + registrar: Registrar + connectionManager: ConnectionManager + logger: ComponentLogger +} + +export function ping (init: PingServiceInit = {}): (components: PingServiceComponents) => PingService { + return (components) => new PingServiceClass(components, init) +} diff --git a/packages/libp2p/src/ping/index.ts b/packages/protocol-ping/src/ping.ts similarity index 68% rename from packages/libp2p/src/ping/index.ts rename to packages/protocol-ping/src/ping.ts index 88e0cc39c1..b150f528c6 100644 --- a/packages/libp2p/src/ping/index.ts +++ b/packages/protocol-ping/src/ping.ts @@ -1,43 +1,18 @@ import { randomBytes } from '@libp2p/crypto' import { CodeError, ERR_TIMEOUT } from '@libp2p/interface/errors' -import { logger } from '@libp2p/logger' import first from 'it-first' import { pipe } from 'it-pipe' import { equals as uint8ArrayEquals } from 'uint8arrays/equals' -import { codes } from '../errors.js' -import { PROTOCOL_PREFIX, PROTOCOL_NAME, PING_LENGTH, PROTOCOL_VERSION, TIMEOUT, MAX_INBOUND_STREAMS, MAX_OUTBOUND_STREAMS } from './constants.js' -import type { AbortOptions } from '@libp2p/interface' +import { PROTOCOL_PREFIX, PROTOCOL_NAME, PING_LENGTH, PROTOCOL_VERSION, TIMEOUT, MAX_INBOUND_STREAMS, MAX_OUTBOUND_STREAMS, ERR_WRONG_PING_ACK } from './constants.js' +import type { PingServiceComponents, PingServiceInit, PingService as PingServiceInterface } from './index.js' +import type { AbortOptions, Logger } from '@libp2p/interface' import type { Stream } from '@libp2p/interface/connection' import type { PeerId } from '@libp2p/interface/peer-id' import type { Startable } from '@libp2p/interface/startable' -import type { ConnectionManager } from '@libp2p/interface-internal/connection-manager' -import type { IncomingStreamData, Registrar } from '@libp2p/interface-internal/registrar' +import type { IncomingStreamData } from '@libp2p/interface-internal/registrar' import type { Multiaddr } from '@multiformats/multiaddr' -const log = logger('libp2p:ping') - -export interface PingService { - ping(peer: PeerId | Multiaddr | Multiaddr[], options?: AbortOptions): Promise -} - -export interface PingServiceInit { - protocolPrefix?: string - maxInboundStreams?: number - maxOutboundStreams?: number - runOnTransientConnection?: boolean - - /** - * How long we should wait for a ping response - */ - timeout?: number -} - -export interface PingServiceComponents { - registrar: Registrar - connectionManager: ConnectionManager -} - -class DefaultPingService implements Startable, PingService { +export class PingService implements Startable, PingServiceInterface { public readonly protocol: string private readonly components: PingServiceComponents private started: boolean @@ -45,15 +20,19 @@ class DefaultPingService implements Startable, PingService { private readonly maxInboundStreams: number private readonly maxOutboundStreams: number private readonly runOnTransientConnection: boolean + readonly #log: Logger - constructor (components: PingServiceComponents, init: PingServiceInit) { + constructor (components: PingServiceComponents, init: PingServiceInit = {}) { this.components = components + this.#log = components.logger.forComponent('libp2p:ping') this.started = false this.protocol = `/${init.protocolPrefix ?? PROTOCOL_PREFIX}/${PROTOCOL_NAME}/${PROTOCOL_VERSION}` this.timeout = init.timeout ?? TIMEOUT this.maxInboundStreams = init.maxInboundStreams ?? MAX_INBOUND_STREAMS this.maxOutboundStreams = init.maxOutboundStreams ?? MAX_OUTBOUND_STREAMS this.runOnTransientConnection = init.runOnTransientConnection ?? true + + this.handleMessage = this.handleMessage.bind(this) } async start (): Promise { @@ -78,30 +57,27 @@ class DefaultPingService implements Startable, PingService { * A handler to register with Libp2p to process ping messages */ handleMessage (data: IncomingStreamData): void { - log('incoming ping from %p', data.connection.remotePeer) + this.#log('incoming ping from %p', data.connection.remotePeer) const { stream } = data const start = Date.now() void pipe(stream, stream) .catch(err => { - log.error('incoming ping from %p failed with error', data.connection.remotePeer, err) + this.#log.error('incoming ping from %p failed with error', data.connection.remotePeer, err) }) .finally(() => { const ms = Date.now() - start - log('incoming ping from %p complete in %dms', data.connection.remotePeer, ms) + this.#log('incoming ping from %p complete in %dms', data.connection.remotePeer, ms) }) } /** * Ping a given peer and wait for its response, getting the operation latency. - * - * @param {PeerId|Multiaddr} peer - * @returns {Promise} */ async ping (peer: PeerId | Multiaddr | Multiaddr[], options: AbortOptions = {}): Promise { - log('pinging %p', peer) + this.#log('pinging %p', peer) const start = Date.now() const data = randomBytes(PING_LENGTH) @@ -140,18 +116,18 @@ class DefaultPingService implements Startable, PingService { const ms = Date.now() - start if (result == null) { - throw new CodeError(`Did not receive a ping ack after ${ms}ms`, codes.ERR_WRONG_PING_ACK) + throw new CodeError(`Did not receive a ping ack after ${ms}ms`, ERR_WRONG_PING_ACK) } if (!uint8ArrayEquals(data, result.subarray())) { - throw new CodeError(`Received wrong ping ack after ${ms}ms`, codes.ERR_WRONG_PING_ACK) + throw new CodeError(`Received wrong ping ack after ${ms}ms`, ERR_WRONG_PING_ACK) } - log('ping %p complete in %dms', connection.remotePeer, ms) + this.#log('ping %p complete in %dms', connection.remotePeer, ms) return ms } catch (err: any) { - log.error('error while pinging %p', connection.remotePeer, err) + this.#log.error('error while pinging %p', connection.remotePeer, err) stream?.abort(err) @@ -164,7 +140,3 @@ class DefaultPingService implements Startable, PingService { } } } - -export function pingService (init: PingServiceInit = {}): (components: PingServiceComponents) => PingService { - return (components) => new DefaultPingService(components, init) -} diff --git a/packages/protocol-ping/test/index.spec.ts b/packages/protocol-ping/test/index.spec.ts new file mode 100644 index 0000000000..102b00d128 --- /dev/null +++ b/packages/protocol-ping/test/index.spec.ts @@ -0,0 +1,134 @@ +/* eslint-env mocha */ + +import { ERR_TIMEOUT } from '@libp2p/interface/errors' +import { start } from '@libp2p/interface/startable' +import { defaultLogger } from '@libp2p/logger' +import { createEd25519PeerId } from '@libp2p/peer-id-factory' +import { expect } from 'aegir/chai' +import { byteStream } from 'it-byte-stream' +import { pair } from 'it-pair' +import { duplexPair } from 'it-pair/duplex' +import pDefer from 'p-defer' +import { stubInterface, type StubbedInstance } from 'sinon-ts' +import { PROTOCOL } from '../src/constants.js' +import { PingService } from '../src/ping.js' +import type { ComponentLogger } from '@libp2p/interface' +import type { Stream, Connection } from '@libp2p/interface/connection' +import type { ConnectionManager } from '@libp2p/interface-internal/connection-manager' +import type { Registrar } from '@libp2p/interface-internal/registrar' + +interface StubbedPingServiceComponents { + registrar: StubbedInstance + connectionManager: StubbedInstance + logger: ComponentLogger +} + +function echoStream (): StubbedInstance { + const stream = stubInterface() + + // make stream input echo to stream output + const duplex: any = pair() + stream.source = duplex.source + stream.sink = duplex.sink + + return stream +} + +describe('ping', () => { + let components: StubbedPingServiceComponents + + beforeEach(async () => { + components = { + registrar: stubInterface(), + connectionManager: stubInterface(), + logger: defaultLogger() + } + }) + + it('should be able to ping another peer', async () => { + const ping = new PingService(components) + + await start(ping) + + const remotePeer = await createEd25519PeerId() + + const connection = stubInterface() + components.connectionManager.openConnection.withArgs(remotePeer).resolves(connection) + + const stream = echoStream() + connection.newStream.withArgs(PROTOCOL).resolves(stream) + + // Run ping + await expect(ping.ping(remotePeer)).to.eventually.be.gte(0) + }) + + it('should time out pinging another peer when waiting for a pong', async () => { + const timeout = 10 + const ping = new PingService(components) + + await start(ping) + + const remotePeer = await createEd25519PeerId() + + const connection = stubInterface() + components.connectionManager.openConnection.withArgs(remotePeer).resolves(connection) + + const stream = echoStream() + const deferred = pDefer() + // eslint-disable-next-line require-yield + stream.source = (async function * () { + await deferred.promise + })() + stream.abort.callsFake((err) => { + deferred.reject(err) + }) + connection.newStream.withArgs(PROTOCOL).resolves(stream) + + // 10 ms timeout + const signal = AbortSignal.timeout(timeout) + + // Run ping, should time out + await expect(ping.ping(remotePeer, { + signal + })) + .to.eventually.be.rejected.with.property('code', ERR_TIMEOUT) + + // should have aborted stream + expect(stream.abort).to.have.property('called', true) + }) + + it('should handle incoming ping', async () => { + const ping = new PingService(components) + + await start(ping) + + const remotePeer = await createEd25519PeerId() + + const connection = stubInterface() + components.connectionManager.openConnection.withArgs(remotePeer).resolves(connection) + + const stream = echoStream() + connection.newStream.withArgs(PROTOCOL).resolves(stream) + + const duplex = duplexPair() + const incomingStream = stubInterface(duplex[0]) + const outgoingStream = stubInterface(duplex[1]) + + const handler = components.registrar.handle.getCall(0).args[1] + + // handle incoming ping stream + handler({ + stream: incomingStream, + connection: stubInterface() + }) + + const input = Uint8Array.from([0, 1, 2, 3, 4]) + + const b = byteStream(outgoingStream) + await b.write(input) + + const output = await b.read() + + expect(output).to.equalBytes(input) + }) +}) diff --git a/packages/protocol-ping/tsconfig.json b/packages/protocol-ping/tsconfig.json new file mode 100644 index 0000000000..f2ddf6c667 --- /dev/null +++ b/packages/protocol-ping/tsconfig.json @@ -0,0 +1,27 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": [ + "src", + "test" + ], + "references": [ + { + "path": "../interface" + }, + { + "path": "../interface-internal" + }, + { + "path": "../logger" + }, + { + "path": "../peer-id" + }, + { + "path": "../peer-id-factory" + } + ] +} diff --git a/packages/protocol-ping/typedoc.json b/packages/protocol-ping/typedoc.json new file mode 100644 index 0000000000..f599dc728d --- /dev/null +++ b/packages/protocol-ping/typedoc.json @@ -0,0 +1,5 @@ +{ + "entryPoints": [ + "./src/index.ts" + ] +}