diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000000..4bf8b2bd06 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,28 @@ +## Title + + +## Description + + + +## Notes & open questions + + + +## Change checklist + +- [ ] I have performed a self-review of my own code +- [ ] I have made corresponding changes to the documentation if necessary (this includes comments as well) +- [ ] I have added tests that prove my fix is effective or that my feature works \ No newline at end of file diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4917feab55..a354658173 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -179,7 +179,7 @@ jobs: - uses: ipfs/aegir/actions/cache-node-modules@master - run: npm run --if-present test:example - multidim-interop: + transport-interop: needs: build runs-on: ${{ fromJSON(github.repository == 'libp2p/js-libp2p' && '["self-hosted", "linux", "x64", "4xlarge"]' || '"ubuntu-latest"') }} steps: @@ -219,7 +219,7 @@ jobs: test-electron-renderer, test-interop, test-examples, - multidim-interop + transport-interop ] if: github.event_name == 'push' && github.ref == 'refs/heads/master' steps: @@ -237,7 +237,8 @@ jobs: { "type": "fix", "section": "Bug Fixes", "hidden": false }, { "type": "chore", "section": "Trivial Changes", "hidden": false }, { "type": "docs", "section": "Documentation", "hidden": false }, - { "type": "deps", "section": "Dependencies", "hidden": false } + { "type": "deps", "section": "Dependencies", "hidden": false }, + { "type": "refactor", "section": "Refactors", "hidden": false } ] - uses: actions/checkout@v3 with: diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 50bb7fccf6..71b4d4c4f6 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1 +1 @@ -{"packages/crypto":"2.0.3","packages/interface":"0.1.2","packages/interface-compliance-tests":"4.0.4","packages/interface-internal":"0.1.4","packages/kad-dht":"10.0.4","packages/keychain":"3.0.3","packages/libp2p":"0.46.5","packages/logger":"3.0.2","packages/metrics-prometheus":"2.0.4","packages/multistream-select":"4.0.2","packages/peer-collections":"4.0.3","packages/peer-discovery-bootstrap":"9.0.4","packages/peer-discovery-mdns":"9.0.4","packages/peer-id":"3.0.2","packages/peer-id-factory":"3.0.3","packages/peer-record":"6.0.3","packages/peer-store":"9.0.3","packages/protocol-perf":"1.1.2","packages/pubsub":"8.0.5","packages/pubsub-floodsub":"8.0.5","packages/stream-multiplexer-mplex":"9.0.4","packages/transport-tcp":"8.0.4","packages/transport-webrtc":"3.1.5","packages/transport-websockets":"7.0.4","packages/transport-webtransport":"3.0.5","packages/utils":"4.0.2"} \ No newline at end of file +{"interop":"1.0.6","packages/crypto":"2.0.5","packages/interface":"0.1.3","packages/interface-compliance-tests":"4.1.1","packages/interface-internal":"0.1.6","packages/kad-dht":"10.0.9","packages/keychain":"3.0.5","packages/libp2p":"0.46.14","packages/logger":"3.0.3","packages/metrics-prometheus":"2.0.8","packages/multistream-select":"4.0.3","packages/peer-collections":"4.0.5","packages/peer-discovery-bootstrap":"9.0.8","packages/peer-discovery-mdns":"9.0.10","packages/peer-id":"3.0.3","packages/peer-id-factory":"3.0.5","packages/peer-record":"6.0.6","packages/peer-store":"9.0.6","packages/protocol-perf":"1.1.11","packages/pubsub":"8.0.7","packages/pubsub-floodsub":"8.0.9","packages/stream-multiplexer-mplex":"9.0.8","packages/transport-tcp":"8.0.9","packages/transport-webrtc":"3.2.3","packages/transport-websockets":"7.0.9","packages/transport-webtransport":"3.1.3","packages/utils":"4.0.4"} \ No newline at end of file diff --git a/.release-please.json b/.release-please.json index b2951dc98e..3a33d3ae02 100644 --- a/.release-please.json +++ b/.release-please.json @@ -4,6 +4,7 @@ "bump-patch-for-minor-pre-major": true, "group-pull-request-title-pattern": "chore: release ${component}", "packages": { + "interop": {}, "packages/crypto": {}, "packages/interface": {}, "packages/interface-compliance-tests": {}, diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000000..e851c09801 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1 @@ +* @libp2p/js-libp2p-dev diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2a101e077b..428ae1d725 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,9 +1,11 @@ # Contributing guidelines -libp2p as a project, including js-libp2p and all of its modules, follows the [standard IPFS Community contributing guidelines](https://github.com/ipfs/community/blob/master/CONTRIBUTING.md). +👋 Contributions are welcome! -We also adhere to the [IPFS JavaScript Community contributing guidelines](https://github.com/ipfs/community/blob/master/CONTRIBUTING_JS.md) which provide additional information of how to collaborate and contribute in the JavaScript implementation of libp2p. +👉 Please see the [IPFS JavaScript Community contributing guidelines](https://github.com/ipfs/community/blob/master/CONTRIBUTING_JS.md) for how to collaborate and contribute to js-libp2p. -We appreciate your time and attention for going over these. Please open an issue on [ipfs/community](https://github.com/ipfs/community) if you have any question. +💪 The [core maintainers](./CODEOWNERS) hang out in #libp2p-implementers in ipfs.io Matrix, FIL Slack, and IPFS Discord. They perform weekly triage of issues and PRs per https://lu.ma/js-libp2p -Thank you. +🤲 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 in [README.md](./README.md), without any additional terms or conditions. + +🙏 Thank you! diff --git a/README.md b/README.md index db1bfdf952..06b53a0e6b 100644 --- a/README.md +++ b/README.md @@ -1,76 +1,3 @@ -# js-libp2p-monorepo - -[![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) - -> JavaScript implementation of libp2p, a modular peer to peer network stack - -## Table of contents - -- [Structure](#structure) - - [Project status](#project-status) -- [Background](#background) -- [Roadmap](#roadmap) -- [Install](#install) -- [Usage](#usage) - - [Configuration](#configuration) - - [Limits](#limits) - - [Getting started](#getting-started) - - [Tutorials and Examples](#tutorials-and-examples) -- [Development](#development) - - [Tests](#tests) - - [Run unit tests](#run-unit-tests) - - [Packages](#packages) -- [Used by](#used-by) -- [Contribute](#contribute) -- [API Docs](#api-docs) -- [License](#license) -- [Contribution](#contribution) - -## Structure - -- [`/doc`](./doc) Docs for libp2p -- [`/examples/auto-relay`](./examples/auto-relay) Shows how to configure relayed connections -- [`/examples/chat`](./examples/chat) An example chat app using libp2p -- [`/examples/connection-encryption`](./examples/connection-encryption) An example of how to configure connection encrypters -- [`/examples/delegated-routing`](./examples/delegated-routing) How to configure libp2p delegated routers -- [`/examples/discovery-mechanisms`](./examples/discovery-mechanisms) How to configure peer discovery mechanisms -- [`/examples/echo`](./examples/echo) An example echo app -- [`/examples/peer-and-content-routing`](./examples/peer-and-content-routing) How to use peer and content routing -- [`/examples/pnet`](./examples/pnet) How to configure a libp2p private network -- [`/examples/protocol-and-stream-muxing`](./examples/protocol-and-stream-muxing) How to use multiplex protocols streams -- [`/examples/pubsub`](./examples/pubsub) An example using libp2p pubsub -- [`/examples/transports`](./examples/transports) An example using different types of libp2p transport -- [`/interop`](./interop) Multidimension Interop Test -- [`/packages/crypto`](./packages/crypto) Crypto primitives for libp2p -- [`/packages/interface`](./packages/interface) The interface implemented by a libp2p node -- [`/packages/interface-compliance-tests`](./packages/interface-compliance-tests) Compliance tests for JS libp2p interfaces -- [`/packages/interface-internal`](./packages/interface-internal) Interfaces implemented by internal libp2p components -- [`/packages/kad-dht`](./packages/kad-dht) JavaScript implementation of the Kad-DHT for libp2p -- [`/packages/keychain`](./packages/keychain) Key management and cryptographically protected messages -- [`/packages/libp2p`](./packages/libp2p) JavaScript implementation of libp2p, a modular peer to peer network stack -- [`/packages/logger`](./packages/logger) A logging component for use in js-libp2p modules -- [`/packages/metrics-prometheus`](./packages/metrics-prometheus) Collect libp2p metrics for scraping by Prometheus or Graphana -- [`/packages/multistream-select`](./packages/multistream-select) JavaScript implementation of multistream-select -- [`/packages/peer-collections`](./packages/peer-collections) Stores values against a peer id -- [`/packages/peer-discovery-bootstrap`](./packages/peer-discovery-bootstrap) Peer discovery via a list of bootstrap peers -- [`/packages/peer-discovery-mdns`](./packages/peer-discovery-mdns) Node.js libp2p mDNS discovery implementation for peer discovery -- [`/packages/peer-id`](./packages/peer-id) Implementation of @libp2p/interface-peer-id -- [`/packages/peer-id-factory`](./packages/peer-id-factory) Create PeerId instances -- [`/packages/peer-record`](./packages/peer-record) Used to transfer signed peer data across the network -- [`/packages/peer-store`](./packages/peer-store) Stores information about peers libp2p knows on the network -- [`/packages/protocol-perf`](./packages/protocol-perf) Implementation of Perf Protocol -- [`/packages/pubsub`](./packages/pubsub) libp2p pubsub base class -- [`/packages/pubsub-floodsub`](./packages/pubsub-floodsub) libp2p-floodsub, also known as pubsub-flood or just dumbsub, this implementation of pubsub focused on delivering an API for Publish/Subscribe, but with no CastTree Forming (it just floods the network). -- [`/packages/stream-multiplexer-mplex`](./packages/stream-multiplexer-mplex) JavaScript implementation of -- [`/packages/transport-tcp`](./packages/transport-tcp) A TCP transport for libp2p -- [`/packages/transport-webrtc`](./packages/transport-webrtc) A libp2p transport using WebRTC connections -- [`/packages/transport-websockets`](./packages/transport-websockets) JavaScript implementation of the WebSockets module that libp2p uses and that implements the interface-transport spec -- [`/packages/transport-webtransport`](./packages/transport-webtransport) JavaScript implementation of the WebTransport module that libp2p uses and that implements the interface-transport spec -- [`/packages/utils`](./packages/utils) Package to aggregate shared logic and dependencies for the libp2p ecosystem -

libp2p hex logo

@@ -99,9 +26,19 @@

+# js-libp2p-monorepo + +[![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) + +> JavaScript implementation of libp2p, a modular peer to peer network stack + + ### Project status -We've come a long way, but this project is still in Alpha, lots of development is happening, API might change, beware of the Dragons 🐉.. +This project has been used in production for years in Ethereum, IPFS, and more. It is actively maintained by multiple organizations and continues to be improved! The API might change, but we strictly follow semver. The documentation in the master branch may contain changes from a pre-release. If you are looking for the documentation of the latest release, you can view the latest release on [**npm**](https://www.npmjs.com/package/libp2p), or select the tag in github that matches the version you are looking for. @@ -110,6 +47,26 @@ If you are looking for the documentation of the latest release, you can view the **Want to update libp2p in your project?** Check our [migrations folder](./doc/migrations). +## Table of contents + +- [Background](#background) +- [Roadmap](#roadmap) +- [Install](#install) +- [Usage](#usage) + - [Configuration](#configuration) + - [Limits](#limits) + - [Getting started](#getting-started) + - [Tutorials and Examples](#tutorials-and-examples) +- [Structure](#structure) +- [Development](#development) + - [Tests](#tests) + - [Run unit tests](#run-unit-tests) + - [Packages](#packages) +- [Used by](#used-by) +- [Contribute](#contribute) +- [API Docs](#api-docs) +- [License](#license) + ## Background libp2p is the product of a long and arduous quest to understand the evolution of the Internet networking stack. In order to build P2P applications, devs have long had to make custom ad-hoc solutions to fit their needs, sometimes making some hard assumptions about their runtimes and the state of the network at the time of their development. Today, looking back more than 20 years, we see a clear pattern in the types of mechanisms built around the Internet Protocol, IP, which can be found throughout many layers of the OSI layer system, libp2p distils these mechanisms into flat categories and defines clear interfaces that once exposed, enable other protocols and applications to use and swap them, enabling upgradability and adaptability for the runtime, without breaking the API. @@ -159,6 +116,48 @@ If you are starting your journey with `js-libp2p`, read the [GETTING\_STARTED.md You can find multiple examples on the [examples folder](./examples) that will guide you through using libp2p for several scenarios. +## Structure + +- [`/doc`](./doc) Docs for libp2p +- [`/examples/auto-relay`](./examples/auto-relay) Shows how to configure relayed connections +- [`/examples/chat`](./examples/chat) An example chat app using libp2p +- [`/examples/connection-encryption`](./examples/connection-encryption) An example of how to configure connection encrypters +- [`/examples/delegated-routing`](./examples/delegated-routing) How to configure libp2p delegated routers +- [`/examples/discovery-mechanisms`](./examples/discovery-mechanisms) How to configure peer discovery mechanisms +- [`/examples/echo`](./examples/echo) An example echo app +- [`/examples/peer-and-content-routing`](./examples/peer-and-content-routing) How to use peer and content routing +- [`/examples/pnet`](./examples/pnet) How to configure a libp2p private network +- [`/examples/protocol-and-stream-muxing`](./examples/protocol-and-stream-muxing) How to use multiplex protocols streams +- [`/examples/pubsub`](./examples/pubsub) An example using libp2p pubsub +- [`/examples/transports`](./examples/transports) An example using different types of libp2p transport +- [`/interop`](./interop) Multidimension Interop Test +- [`/packages/crypto`](./packages/crypto) Crypto primitives for libp2p +- [`/packages/interface`](./packages/interface) The interface implemented by a libp2p node +- [`/packages/interface-compliance-tests`](./packages/interface-compliance-tests) Compliance tests for JS libp2p interfaces +- [`/packages/interface-internal`](./packages/interface-internal) Interfaces implemented by internal libp2p components +- [`/packages/kad-dht`](./packages/kad-dht) JavaScript implementation of the Kad-DHT for libp2p +- [`/packages/keychain`](./packages/keychain) Key management and cryptographically protected messages +- [`/packages/libp2p`](./packages/libp2p) JavaScript implementation of libp2p, a modular peer to peer network stack +- [`/packages/logger`](./packages/logger) A logging component for use in js-libp2p modules +- [`/packages/metrics-prometheus`](./packages/metrics-prometheus) Collect libp2p metrics for scraping by Prometheus or Graphana +- [`/packages/multistream-select`](./packages/multistream-select) JavaScript implementation of multistream-select +- [`/packages/peer-collections`](./packages/peer-collections) Stores values against a peer id +- [`/packages/peer-discovery-bootstrap`](./packages/peer-discovery-bootstrap) Peer discovery via a list of bootstrap peers +- [`/packages/peer-discovery-mdns`](./packages/peer-discovery-mdns) Node.js libp2p mDNS discovery implementation for peer discovery +- [`/packages/peer-id`](./packages/peer-id) Implementation of @libp2p/interface-peer-id +- [`/packages/peer-id-factory`](./packages/peer-id-factory) Create PeerId instances +- [`/packages/peer-record`](./packages/peer-record) Used to transfer signed peer data across the network +- [`/packages/peer-store`](./packages/peer-store) Stores information about peers libp2p knows on the network +- [`/packages/protocol-perf`](./packages/protocol-perf) Implementation of Perf Protocol +- [`/packages/pubsub`](./packages/pubsub) libp2p pubsub base class +- [`/packages/pubsub-floodsub`](./packages/pubsub-floodsub) libp2p-floodsub, also known as pubsub-flood or just dumbsub, this implementation of pubsub focused on delivering an API for Publish/Subscribe, but with no CastTree Forming (it just floods the network). +- [`/packages/stream-multiplexer-mplex`](./packages/stream-multiplexer-mplex) JavaScript implementation of +- [`/packages/transport-tcp`](./packages/transport-tcp) A TCP transport for libp2p +- [`/packages/transport-webrtc`](./packages/transport-webrtc) A libp2p transport using WebRTC connections +- [`/packages/transport-websockets`](./packages/transport-websockets) JavaScript implementation of the WebSockets module that libp2p uses and that implements the interface-transport spec +- [`/packages/transport-webtransport`](./packages/transport-webtransport) JavaScript implementation of the WebTransport module that libp2p uses and that implements the interface-transport spec +- [`/packages/utils`](./packages/utils) Package to aggregate shared logic and dependencies for the libp2p ecosystem + ## Development **Clone and install dependencies:** @@ -235,7 +234,7 @@ List of packages currently in existence for libp2p

HOPR Logo - IPFS in JavaScript logo + Helia (IPFS in JavaScript) logo

@@ -243,11 +242,7 @@ And [many others...](https://github.com/libp2p/js-libp2p/network/dependents) ## Contribute -The libp2p implementation in JavaScript is a work in progress. As such, there are a few things you can do right now to help out: - -- Go through the modules and **check out existing issues**. This would be especially useful for modules in active development. Some knowledge of IPFS/libp2p may be required, as well as the infrastructure behind it - for instance, you may need to read up on p2p and more complex operations like muxing to be able to help technically. -- **Perform code reviews**. Most of this has been developed by @diasdavid, which means that more eyes will help a) speed the project along b) ensure quality and c) reduce possible future bugs. -- **Add tests**. There can never be enough tests. +See [CONTRIBUTING.md](./CONTRIBUTING.md). ## API Docs @@ -259,7 +254,3 @@ 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/doc/ARCHITECTURE.md b/doc/ARCHITECTURE.md new file mode 100644 index 0000000000..59489f3f42 --- /dev/null +++ b/doc/ARCHITECTURE.md @@ -0,0 +1,217 @@ +# Libp2p Architecture + +js-libp2p is comprised of a number of components that work together to provide functionality such as dailling peers, managing connections, registering protocols, storing information about peers and much more. This document aims to provide a high level overview of the components and how they interact with each other. + +- [Libp2p Architecture](#libp2p-architecture) + - [Component Diagram](#component-diagram) + - [Sequence Diagrams](#sequence-diagrams) + - [Dialing a Peer](#dialing-a-peer) + - [Opening a stream on a connection](#opening-a-stream-on-a-connection) + +## Component Diagram + +```mermaid +flowchart TB + direction TB + subgraph Components + direction TB + PeerId ~~~ + Events ~~~ + ConnectionGater ~~~ + Upgrader + AddressManager ~~~ + ConnectionManager ~~~ + TransportManager ~~~ + Registrar + PeerStore ~~~ + Datastore ~~~ + PeerRouting ~~~ + _xx[ ] + ContentRouting ~~~ + Metrics ~~~ + ConnectionProtector ~~~ + _x[ ] + + style _x opacity:0; + style _xx opacity:0; + + end + + subgraph Connections[Connection Configuration] + direction TB + + subgraph Transports + direction TB + TCP + WebRTC + Websocket + Webtransport + end + + subgraph Encryption[Connection Encryptions] + direction TB + Noise + Plaintext + end + + subgraph Multiplexer[Stream Multiplexers] + direction TB + Yamux + Mplex + end + + Multiplexer ~~~ Encryption ~~~ Transports + + end +``` + +```mermaid +--- +title: Components Dependency Graph +--- +flowchart TD + PeerId + Events + ConnectionGater + Upgrader + AddressManager + ConnectionManager + TransportManager + Registrar + PeerStore + Datastore + PeerRouting + ContentRouting + Metrics + ConnectionProtector + + %% AddressManager + PeerId --> AddressManager + TransportManager --> AddressManager + PeerStore --> AddressManager + Events --> AddressManager + + %% ConnectionManager + PeerId --> ConnectionManager + Metrics --> ConnectionManager + PeerStore --> ConnectionManager + TransportManager --> ConnectionManager + ConnectionGater --> ConnectionManager + Events --> ConnectionManager + + %% TransportManager + Metrics --> TransportManager + AddressManager --> TransportManager + Upgrader --> TransportManager + Events --> TransportManager + + %% Upgrader + PeerId --> Upgrader + Metrics --> Upgrader + ConnectionManager --> Upgrader + ConnectionGater --> Upgrader + ConnectionProtector --> Upgrader + Registrar --> Upgrader + PeerStore --> Upgrader + Events --> Upgrader + + %% Registrar + PeerId --> Registrar + ConnectionManager --> Registrar + PeerStore --> Registrar + Events --> Registrar + + %% PeerStore + PeerId --> PeerStore + Datastore --> PeerStore + Events --> PeerStore + + %% PeerRouting + PeerId --> PeerRouting + PeerStore --> PeerRouting + + %% ContentRouting + PeerStore --> ContentRouting +``` + +## Sequence Diagrams + +These diagrams show the interactions between the components in common scenarios. They are not exhaustive and are intended to provide a high level overview of the interactions between the components. + +### Dialing a Peer + +This illustrates an outbound connection being established to a peer. + +```mermaid +%% how an outbound connection is opened when a user calls .dial(), +%% assuming user is not connected to the PeerId for the +%% Multiaddr that was dialed. +%% +%% This is +%% +sequenceDiagram + User->>+libp2p: dial a multiaddr `.dial()` + libp2p->>+Connection Manager: open a connection for me to MA `.openConnection()` + %% obfuscating the dial queue. + %% Connection Manager->>+Transport Manager: Choose transport to use for Multiaddr + Connection Manager->>+Transport Manager: Network level reach out `.dial()` + Transport Manager->>+Transport: Get MultiaddrConn `socket + multiaddr` + %% Transport->>+Transport Manager: Return MultiaddrConn `socket + multiaddr` + %% how the upgrade happens is transport specific, so transports directly call upgrader + Transport-->>+Upgrader: upgrade my connection?? + Upgrader-->>+Upgrader: Perform upgrade (see other diagram) + Upgrader->>+Connection Manager: Connection (link to interface) + %% Connection Manager->>+Connection Manager: Connection (link to interface) + Connection Manager->>+User: Connection (link to interface) +``` + + +### Opening a stream on a connection + +This illustrates a stream being opened on an existing connection that will echo a message back to the sender. This assumes that a stable connection has been established between the two peers. + +```mermaid +%% pushing data over stream +%% register stream handler, local opens a stream for proto, send data, +%% remote receives data and sends data back +%% local receives data +%% stream may or may not then be closed. +%% Local is the node sending data, Remote is other peer the conn is with +%% Echo protocol +sequenceDiagram + box Local side + participant Local + participant Connection + participant LocalMuxer + end + participant Stream + box pink Connection + end + box Remote side + participant Remote + participant RemoteMuxer + participant RemoteUpgrader + participant RemoteRegistrar + participant RemoteStreamHandler + end + + Remote->>RemoteRegistrar: Register Stream Handler `libp2p.handle` + %% only register stream handlers when you want to listen for protocols. SENDERs do not need to listen + Local->>Connection: Open outbound stream + Connection->>LocalMuxer: Open stream + LocalMuxer->>RemoteMuxer: Open stream + RemoteMuxer->>RemoteUpgrader: notify Stream created + Note over Connection,RemoteUpgrader: multi stream select handles protocol negotiation + Connection->>Local: return Stream + RemoteUpgrader->>RemoteRegistrar: select stream handler + RemoteRegistrar->>RemoteStreamHandler: handle stream + Note over RemoteStreamHandler,Local: Stream data flow & control is dictated by protocol, below is example of "echo" + activate Stream + Local->>Stream: send bytes "hello world" + Stream->>RemoteStreamHandler: receive bytes "hello world" + %% RemoteStreamHandler->>+RemoteStreamHandler: [echo] pipe back received bytes + RemoteStreamHandler->>Stream: echo bytes back to sender + Stream->>Local: receive echoed bytes + deactivate Stream + +``` \ No newline at end of file diff --git a/doc/CONFIGURATION.md b/doc/CONFIGURATION.md index c49d4ff4ca..dcba663f72 100644 --- a/doc/CONFIGURATION.md +++ b/doc/CONFIGURATION.md @@ -44,6 +44,8 @@ libp2p is a modular networking stack. It's designed to be able to suit a variety Regardless of how you configure libp2p, the top level [API](./API.md) will always remain the same. **Note**: if some modules are not configured, like Content Routing, using those methods will throw errors. +To get a high-level overview of the js-libp2p architecture, please read the [Architecture](./ARCHITECTURE.md) document. + ## Modules `js-libp2p` acts as the composer for this modular p2p networking stack using libp2p compatible modules as its subsystems. For getting an instance of `js-libp2p` compliant with all types of networking requirements, it is possible to specify the following subsystems: @@ -126,7 +128,7 @@ If you want to know more about libp2p connection encryption, you should read the Some available peer discovery modules are: -- [@libp2p/mdns](https://github.com/libp2p/js-libp2ptree/master/packages/peer-discovery-mdns) +- [@libp2p/mdns](https://github.com/libp2p/js-libp2p/tree/master/packages/peer-discovery-mdns) - [@libp2p/bootstrap](https://github.com/libp2p/js-libp2p/tree/master/packages/peer-discovery-bootstrap) - [@libp2p/kad-dht](https://github.com/libp2p/js-libp2p/tree/master/packages/kad-dht) - [@chainsafe/discv5](https://github.com/chainsafe/discv5) diff --git a/doc/CONNECTION_MANAGER.md b/doc/CONNECTION_MANAGER.md deleted file mode 100644 index a22a14fcb1..0000000000 --- a/doc/CONNECTION_MANAGER.md +++ /dev/null @@ -1,3 +0,0 @@ -# Connection Manager - -The documentation here has moved to https://libp2p.github.io/js-libp2p-interfaces/modules/_libp2p_interface_connection_manager.html - please update your bookmarks! diff --git a/doc/DIALER.md b/doc/DIALER.md deleted file mode 100644 index bba39d5b70..0000000000 --- a/doc/DIALER.md +++ /dev/null @@ -1,32 +0,0 @@ -# js-libp2p Dialer - -**Synopsis** -* Parallel dials to the same peer will yield the same connection/error when the first dial settles. -* All Dial Requests in js-libp2p must request a token(s) from the Dialer. - * The number of tokens requested should be between 1 and the MAX_PER_PEER_DIALS max set in the Dialer. - * If the number of available tokens is less than requested, the Dialer may return less than requested. -* The number of tokens a DialRequest obtains reflects the maximum number of parallel Multiaddr Dials it can make. -* If no tokens are available a DialRequest should immediately end and throw. -* As tokens are limited, DialRequests should be given a prioritized list of Multiaddrs to minimize the potential request time. -* Once a Multiaddr Dial has succeeded, all pending dials in that Dial Request should be aborted. -* If DIAL_TIMEOUT time has elapsed before any one Multiaddr Dial succeeds, all remaining dials in the DialRequest should be aborted. -* When a Multiaddr Dial is settled, if there are no more addresses to dial, its token should be released back to the dialer. -* Once the DialRequest is settled, any remaining tokens should be released to the dialer. - -## Multiaddr Confidence - -An effective dialing system should involve the inclusion of a Confidence system for Multiaddrs. This enables ranking of Multiaddrs so that a prioritized list can be passed to DialRequests to maximize usage of Dialer Tokens, and minimize connection times. - -**Not Yet Implemented**: This system will be designed and implemented in a future update. - -## Notes - -* A DialRequest gets a set of tokens from the Dialer, up to the MAX_PER_PEER_DIALS max. -* A DialRequest SHOULD fail if no dial tokens are available. The DialRequest MAY proceed without tokens, but this should be reserved for High Priority actions and should be rare. -* A DialRequest MUST NOT request more tokens than it has addresses to dial. Example: If the MAX_PER_PEER_DIALS is 4 and a DialRequest has 1 address, it should only request 1 token. -* A DialRequest SHOULD execute parallel dials for each of its addresses up the total number of tokens it has. -* On a successful dial, the DialRequest MUST abort any other in progress dials, return the successful connection and release all tokens. -* A new DialRequest SHOULD be given a descending list of prioritized Multiaddrs, based on their confidence. Having higher confidence Multiaddrs first can help minimize the time a DialRequest is active. -* A failed dial to a Multiaddr SHOULD add that Multiaddr to a temporary denyList. -* A failed dial to a Multiaddr SHOULD lower the confidence of that Multiaddr. -* A successful dial to a Multiaddr SHOULD increase the confidence of that Multiaddr. diff --git a/doc/GETTING_STARTED.md b/doc/GETTING_STARTED.md index 50b37076c6..49db548f8f 100644 --- a/doc/GETTING_STARTED.md +++ b/doc/GETTING_STARTED.md @@ -134,7 +134,7 @@ import { createLibp2p } from 'libp2p' import { webSockets } from '@libp2p/websockets' import { noise } from '@chainsafe/libp2p-noise' import { mplex } from '@libp2p/mplex' -import { yamux } from '@chainsafe/libp2p-yamux', +import { yamux } from '@chainsafe/libp2p-yamux' const node = await createLibp2p({ transports: [webSockets()], @@ -214,7 +214,7 @@ import { createLibp2p } from 'libp2p' import { webSockets } from '@libp2p/websockets' import { noise } from '@chainsafe/libp2p-noise' import { mplex } from '@libp2p/mplex' -import { yamux } from '@chainsafe/libp2p-yamux', +import { yamux } from '@chainsafe/libp2p-yamux' import { bootstrap } from '@libp2p/bootstrap' diff --git a/doc/package.json b/doc/package.json index 34c831f289..a1b9c91f2d 100644 --- a/doc/package.json +++ b/doc/package.json @@ -15,6 +15,7 @@ "eslintConfig": { "extends": "ipfs", "parserOptions": { + "project": true, "sourceType": "module" } }, @@ -27,7 +28,7 @@ "@libp2p/mplex": "^9.0.0", "@libp2p/prometheus-metrics": "^2.0.0", "@libp2p/tcp": "^8.0.0", - "aegir": "^40.0.1", + "aegir": "^41.0.2", "libp2p": "^0.46.0", "prom-client": "^14.2.0" }, diff --git a/examples/auto-relay/package.json b/examples/auto-relay/package.json index 50b2c71e44..fbef4c84c4 100644 --- a/examples/auto-relay/package.json +++ b/examples/auto-relay/package.json @@ -21,6 +21,7 @@ "eslintConfig": { "extends": "ipfs", "parserOptions": { + "project": true, "sourceType": "module" } }, @@ -37,8 +38,8 @@ "libp2p": "^0.46.0" }, "devDependencies": { - "aegir": "^40.0.8", - "execa": "^7.1.1", + "aegir": "^41.0.2", + "execa": "^8.0.1", "p-defer": "^4.0.0", "uint8arrays": "^4.0.4" }, diff --git a/examples/chat/package.json b/examples/chat/package.json index 4f5d8c603e..4c9ca59231 100644 --- a/examples/chat/package.json +++ b/examples/chat/package.json @@ -21,6 +21,7 @@ "eslintConfig": { "extends": "ipfs", "parserOptions": { + "project": true, "sourceType": "module" } }, @@ -44,8 +45,8 @@ "uint8arrays": "^4.0.6" }, "devDependencies": { - "aegir": "^40.0.8", - "execa": "^7.1.1", + "aegir": "^41.0.2", + "execa": "^8.0.1", "p-defer": "^4.0.0" }, "private": true diff --git a/examples/connection-encryption/package.json b/examples/connection-encryption/package.json index 99da5daf14..d4c652bc1b 100644 --- a/examples/connection-encryption/package.json +++ b/examples/connection-encryption/package.json @@ -21,6 +21,7 @@ "eslintConfig": { "extends": "ipfs", "parserOptions": { + "project": true, "sourceType": "module" } }, @@ -38,7 +39,7 @@ "uint8arrays": "^4.0.6" }, "devDependencies": { - "aegir": "^40.0.8", + "aegir": "^41.0.2", "test-ipfs-example": "^1.0.0" }, "private": true diff --git a/examples/delegated-routing/package.json b/examples/delegated-routing/package.json index ffa79e94d2..1dc0953e35 100644 --- a/examples/delegated-routing/package.json +++ b/examples/delegated-routing/package.json @@ -46,7 +46,7 @@ "react-scripts": "^5.0.1" }, "devDependencies": { - "aegir": "^40.0.8" + "aegir": "^41.0.2" }, "browserslist": [ ">0.2%", diff --git a/examples/discovery-mechanisms/package.json b/examples/discovery-mechanisms/package.json index 3c4ade5594..934710d4a3 100644 --- a/examples/discovery-mechanisms/package.json +++ b/examples/discovery-mechanisms/package.json @@ -21,6 +21,7 @@ "eslintConfig": { "extends": "ipfs", "parserOptions": { + "project": true, "sourceType": "module" } }, @@ -36,13 +37,13 @@ "@libp2p/floodsub": "^8.0.0", "@libp2p/mdns": "^9.0.0", "@libp2p/mplex": "^9.0.0", - "@libp2p/pubsub-peer-discovery": "^8.0.4", + "@libp2p/pubsub-peer-discovery": "^9.0.0", "@libp2p/tcp": "^8.0.0", "libp2p": "^0.46.0" }, "devDependencies": { - "aegir": "^40.0.8", - "execa": "^7.1.1", + "aegir": "^41.0.2", + "execa": "^8.0.1", "p-wait-for": "^5.0.2", "uint8arrays": "^4.0.4" }, diff --git a/examples/echo/package.json b/examples/echo/package.json index 4a38ffea9c..fe6e07a54b 100644 --- a/examples/echo/package.json +++ b/examples/echo/package.json @@ -21,6 +21,7 @@ "eslintConfig": { "extends": "ipfs", "parserOptions": { + "project": true, "sourceType": "module" } }, @@ -42,8 +43,8 @@ "uint8arrays": "^4.0.6" }, "devDependencies": { - "aegir": "^40.0.8", - "execa": "^7.1.1", + "aegir": "^41.0.2", + "execa": "^8.0.1", "p-defer": "^4.0.0" }, "private": true diff --git a/examples/libp2p-in-the-browser/websockets/package.json b/examples/libp2p-in-the-browser/websockets/package.json index aaf2a8bbd4..e42fa7b487 100644 --- a/examples/libp2p-in-the-browser/websockets/package.json +++ b/examples/libp2p-in-the-browser/websockets/package.json @@ -21,6 +21,7 @@ "eslintConfig": { "extends": "ipfs", "parserOptions": { + "project": true, "sourceType": "module" } }, diff --git a/examples/nat-traversal/README.md b/examples/nat-traversal/README.md deleted file mode 100644 index 38f33ceff9..0000000000 --- a/examples/nat-traversal/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# WIP - This example is still in the works -![](http://1.bp.blogspot.com/-tNvSnCW0KlQ/U-KOKGVoJkI/AAAAAAAAA3Q/aiSLMeSJFtw/s1600/WIP-sign.jpg) diff --git a/examples/peer-and-content-routing/package.json b/examples/peer-and-content-routing/package.json index 1581c20778..0218be3139 100644 --- a/examples/peer-and-content-routing/package.json +++ b/examples/peer-and-content-routing/package.json @@ -21,6 +21,7 @@ "eslintConfig": { "extends": "ipfs", "parserOptions": { + "project": true, "sourceType": "module" } }, @@ -40,7 +41,7 @@ "multiformats": "^12.0.1" }, "devDependencies": { - "aegir": "^40.0.8", + "aegir": "^41.0.2", "test-ipfs-example": "^1.0.0" }, "private": true diff --git a/examples/pnet/package.json b/examples/pnet/package.json index 47b7cab334..e619f3fe9a 100644 --- a/examples/pnet/package.json +++ b/examples/pnet/package.json @@ -21,6 +21,7 @@ "eslintConfig": { "extends": "ipfs", "parserOptions": { + "project": true, "sourceType": "module" } }, @@ -38,7 +39,7 @@ "uint8arrays": "^4.0.6" }, "devDependencies": { - "aegir": "^40.0.8", + "aegir": "^41.0.2", "test-ipfs-example": "^1.0.0" }, "private": true diff --git a/examples/protocol-and-stream-muxing/package.json b/examples/protocol-and-stream-muxing/package.json index af5eae622c..d8d8086336 100644 --- a/examples/protocol-and-stream-muxing/package.json +++ b/examples/protocol-and-stream-muxing/package.json @@ -21,6 +21,7 @@ "eslintConfig": { "extends": "ipfs", "parserOptions": { + "project": true, "sourceType": "module" } }, @@ -38,7 +39,7 @@ "uint8arrays": "^4.0.6" }, "devDependencies": { - "aegir": "^40.0.8", + "aegir": "^41.0.2", "test-ipfs-example": "^1.0.0" }, "private": true diff --git a/examples/pubsub/message-filtering/README.md b/examples/pubsub/message-filtering/README.md index b6f11d55cb..7b9dba1309 100644 --- a/examples/pubsub/message-filtering/README.md +++ b/examples/pubsub/message-filtering/README.md @@ -8,7 +8,7 @@ First, let's update our libp2p configuration with a pubsub implementation. ```JavaScript import { createLibp2p } from 'libp2p' -import { GossipSub } from '@chainsafe/libp2p-gossipsub' +import { gossipsub } from '@chainsafe/libp2p-gossipsub' import { tcp } from '@libp2p/tcp' import { mplex } from '@libp2p/mplex' import { yamux } from '@chainsafe/libp2p-yamux' @@ -23,7 +23,9 @@ const createNode = async () => { streamMuxers: [yamux(), mplex()], connectionEncryption: [noise()], // we add the Pubsub module we want - pubsub: gossipsub({ allowPublishToZeroPeers: true }) + services: { + pubsub: gossipsub({ allowPublishToZeroPeers: true }) + } }) return node @@ -88,13 +90,17 @@ await node3.services.pubsub.subscribe(topic) Finally, let's define the additional filter in the fruit topic. ```JavaScript +import { TopicValidatorResult } from '@libp2p/interface/pubsub' + const validateFruit = (msgTopic, msg) => { const fruit = uint8ArrayToString(msg.data) const validFruit = ['banana', 'apple', 'orange'] + // car is not a fruit ! if (!validFruit.includes(fruit)) { throw new Error('no valid fruit received') } + return TopicValidatorResult.Accept } node1.services.pubsub.topicValidators.set(topic, validateFruit) diff --git a/examples/pubsub/package.json b/examples/pubsub/package.json index 56607e076e..97c909fc67 100644 --- a/examples/pubsub/package.json +++ b/examples/pubsub/package.json @@ -21,6 +21,7 @@ "eslintConfig": { "extends": "ipfs", "parserOptions": { + "project": true, "sourceType": "module" } }, @@ -38,8 +39,8 @@ "uint8arrays": "^4.0.6" }, "devDependencies": { - "aegir": "^40.0.8", - "execa": "^7.1.1", + "aegir": "^41.0.2", + "execa": "^8.0.1", "p-defer": "^4.0.0" }, "private": true diff --git a/examples/transports/package.json b/examples/transports/package.json index 4ebd75fd0c..c5c06b6f19 100644 --- a/examples/transports/package.json +++ b/examples/transports/package.json @@ -21,6 +21,7 @@ "eslintConfig": { "extends": "ipfs", "parserOptions": { + "project": true, "sourceType": "module" } }, @@ -40,7 +41,7 @@ "uint8arrays": "^4.0.6" }, "devDependencies": { - "aegir": "^40.0.8", + "aegir": "^41.0.2", "test-ipfs-example": "^1.0.0" }, "private": true diff --git a/interop/CHANGELOG.md b/interop/CHANGELOG.md new file mode 100644 index 0000000000..fb517accc5 --- /dev/null +++ b/interop/CHANGELOG.md @@ -0,0 +1,77 @@ +# Changelog + +### [1.0.6](https://www.github.com/libp2p/js-libp2p/compare/multidim-interop-v1.0.5...multidim-interop-v1.0.6) (2023-10-10) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @libp2p/webrtc bumped from ^3.2.2 to ^3.2.3 + * @libp2p/webtransport bumped from ^3.1.2 to ^3.1.3 + * libp2p bumped from ^0.46.13 to ^0.46.14 + +### [1.0.5](https://www.github.com/libp2p/js-libp2p/compare/multidim-interop-v1.0.4...multidim-interop-v1.0.5) (2023-10-06) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @libp2p/mplex bumped from ^9.0.7 to ^9.0.8 + * @libp2p/tcp bumped from ^8.0.8 to ^8.0.9 + * @libp2p/webrtc bumped from ^3.2.1 to ^3.2.2 + * @libp2p/websockets bumped from ^7.0.8 to ^7.0.9 + * @libp2p/webtransport bumped from ^3.1.1 to ^3.1.2 + * libp2p bumped from ^0.46.12 to ^0.46.13 + +### [1.0.4](https://www.github.com/libp2p/js-libp2p/compare/multidim-interop-v1.0.3...multidim-interop-v1.0.4) (2023-10-01) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @libp2p/mplex bumped from ^9.0.6 to ^9.0.7 + * @libp2p/tcp bumped from ^8.0.7 to ^8.0.8 + * @libp2p/webrtc bumped from ^3.2.0 to ^3.2.1 + * @libp2p/websockets bumped from ^7.0.7 to ^7.0.8 + * @libp2p/webtransport bumped from ^3.1.0 to ^3.1.1 + * libp2p bumped from ^0.46.11 to ^0.46.12 + +### [1.0.3](https://www.github.com/libp2p/js-libp2p/compare/multidim-interop-v1.0.2...multidim-interop-v1.0.3) (2023-09-20) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @libp2p/webrtc bumped from ^3.1.11 to ^3.2.0 + * @libp2p/webtransport bumped from ^3.0.11 to ^3.1.0 + +### [1.0.2](https://www.github.com/libp2p/js-libp2p/compare/multidim-interop-v1.0.1...multidim-interop-v1.0.2) (2023-09-15) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @libp2p/mplex bumped from ^9.0.0 to ^9.0.6 + * @libp2p/tcp bumped from ^8.0.6 to ^8.0.7 + * @libp2p/webrtc bumped from ^3.1.10 to ^3.1.11 + * @libp2p/websockets bumped from ^7.0.6 to ^7.0.7 + * @libp2p/webtransport bumped from ^3.0.10 to ^3.0.11 + * libp2p bumped from ^0.46.10 to ^0.46.11 + +### [1.0.1](https://www.github.com/libp2p/js-libp2p/compare/multidim-interop-v1.0.0...multidim-interop-v1.0.1) (2023-09-10) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @libp2p/tcp bumped from ^8.0.0 to ^8.0.6 + * @libp2p/webrtc bumped from ^3.0.0 to ^3.1.10 + * @libp2p/websockets bumped from ^7.0.0 to ^7.0.6 + * @libp2p/webtransport bumped from ^3.0.0 to ^3.0.10 + * libp2p bumped from ^0.46.0 to ^0.46.10 diff --git a/interop/README.md b/interop/README.md index 2b6b88967e..9f77324dac 100644 --- a/interop/README.md +++ b/interop/README.md @@ -1,11 +1,11 @@ -# multidim-interop +# @libp2p/multidim-interop [![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) -> Multidimension Interop Test +> Multidimensional interop tests ## Table of contents diff --git a/interop/package.json b/interop/package.json index fe9df02039..3ab015c03a 100644 --- a/interop/package.json +++ b/interop/package.json @@ -1,7 +1,7 @@ { - "name": "multidim-interop", - "version": "1.0.0", - "description": "Multidimension Interop Test", + "name": "@libp2p/multidim-interop", + "version": "1.0.6", + "description": "Multidimensional interop tests", "author": "Glen De Cauwsemaecker / @marcopolo", "license": "Apache-2.0 OR MIT", "homepage": "https://github.com/libp2p/js-libp2p/tree/master/interop#readme", @@ -16,8 +16,18 @@ "types": "./dist/src/index.d.ts", "files": [ "src", + "test", "dist", - "!dist/test", + ".aegir.js", + "BrowserDockerfile", + "chromium-version.json", + "Dockerfile", + "firefox-version.json", + "Makefile", + "node-version.json", + "relay.js", + "tsconfig.json", + "webkit-version.json", "!**/*.tsbuildinfo" ], "exports": { @@ -29,6 +39,7 @@ "eslintConfig": { "extends": "ipfs", "parserOptions": { + "project": true, "sourceType": "module" } }, @@ -41,21 +52,18 @@ "dependencies": { "@chainsafe/libp2p-noise": "^13.0.0", "@chainsafe/libp2p-yamux": "^5.0.0", - "@libp2p/mplex": "^9.0.0", - "@libp2p/tcp": "^8.0.0", - "@libp2p/webrtc": "^3.0.0", - "@libp2p/websockets": "^7.0.0", - "@libp2p/webtransport": "^3.0.0", + "@libp2p/mplex": "^9.0.8", + "@libp2p/tcp": "^8.0.9", + "@libp2p/webrtc": "^3.2.3", + "@libp2p/websockets": "^7.0.9", + "@libp2p/webtransport": "^3.1.3", "@multiformats/mafmt": "^12.1.2", "@multiformats/multiaddr": "^12.1.5", - "libp2p": "^0.46.0", + "aegir": "^41.0.2", + "libp2p": "^0.46.14", "redis": "^4.5.1" }, - "devDependencies": { - "aegir": "^40.0.8" - }, "browser": { "@libp2p/tcp": false - }, - "private": true + } } diff --git a/package.json b/package.json index b36a7e5759..bb6e033168 100644 --- a/package.json +++ b/package.json @@ -33,11 +33,11 @@ "release": "run-s build docs:no-publish npm:release docs", "npm:release": "aegir exec --bail false npm -- publish", "release:rc": "aegir release-rc", - "docs": "NODE_OPTIONS=--max_old_space_size=8192 aegir docs -- --exclude interop --exclude examples --exclude doc", - "docs:no-publish": "NODE_OPTIONS=--max_old_space_size=8192 aegir docs --publish false -- --exclude interop --exclude examples --exclude doc" + "docs": "NODE_OPTIONS=--max_old_space_size=8192 aegir docs -- --exclude interop --exclude examples/auto-relay --exclude examples/chat --exclude examples/connection-encryption --exclude examples/delegated-routing --exclude examples/discovery-mechanisms --exclude examples/echo --exclude examples/peer-and-content-routing --exclude examples/pnet --exclude examples/protocol-and-stream-muxing --exclude examples/pubsub --exclude examples/transports --exclude doc", + "docs:no-publish": "NODE_OPTIONS=--max_old_space_size=8192 aegir docs --publish false -- --exclude interop --exclude examples/auto-relay --exclude examples/chat --exclude examples/connection-encryption --exclude examples/delegated-routing --exclude examples/discovery-mechanisms --exclude examples/echo --exclude examples/peer-and-content-routing --exclude examples/pnet --exclude examples/protocol-and-stream-muxing --exclude examples/pubsub --exclude examples/transports --exclude doc" }, "devDependencies": { - "aegir": "^40.0.1" + "aegir": "^41.0.2" }, "eslintConfig": { "extends": "ipfs", diff --git a/packages/crypto/CHANGELOG.md b/packages/crypto/CHANGELOG.md index f62cdad420..4f89fc5f0e 100644 --- a/packages/crypto/CHANGELOG.md +++ b/packages/crypto/CHANGELOG.md @@ -5,6 +5,22 @@ * **dev:** bump aegir from 38.1.8 to 39.0.5 ([#320](https://github.com/libp2p/js-libp2p-crypto/issues/320)) ([f0b4c06](https://github.com/libp2p/js-libp2p-crypto/commit/f0b4c068a23d78b1376865c6adf6cce21ab91196)) +### [2.0.5](https://www.github.com/libp2p/js-libp2p/compare/crypto-v2.0.4...crypto-v2.0.5) (2023-10-06) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @libp2p/interface bumped from ^0.1.2 to ^0.1.3 + +### [2.0.4](https://www.github.com/libp2p/js-libp2p/compare/crypto-v2.0.3...crypto-v2.0.4) (2023-09-15) + + +### Bug Fixes + +* **@libp2p/crypto:** improve unsupported key type message ([#2051](https://www.github.com/libp2p/js-libp2p/issues/2051)) ([d9159dd](https://www.github.com/libp2p/js-libp2p/commit/d9159dd5985241160f791acda164bb2e6408dd90)) + ### [2.0.3](https://www.github.com/libp2p/js-libp2p/compare/crypto-v2.0.2...crypto-v2.0.3) (2023-08-14) @@ -789,4 +805,4 @@ chore: update deps ### Features -* **keys:** implement generateKeyPairFromSeed for ed25519 ([e5b7c1f](https://github.com/libp2p/js-libp2p-crypto/commit/e5b7c1f)) \ No newline at end of file +* **keys:** implement generateKeyPairFromSeed for ed25519 ([e5b7c1f](https://github.com/libp2p/js-libp2p-crypto/commit/e5b7c1f)) diff --git a/packages/crypto/package.json b/packages/crypto/package.json index d359659805..785f3f720d 100644 --- a/packages/crypto/package.json +++ b/packages/crypto/package.json @@ -1,6 +1,6 @@ { "name": "@libp2p/crypto", - "version": "2.0.3", + "version": "2.0.5", "description": "Crypto primitives for libp2p", "license": "Apache-2.0 OR MIT", "homepage": "https://github.com/libp2p/js-libp2p/tree/master/packages/crypto#readme", @@ -63,6 +63,7 @@ "eslintConfig": { "extends": "ipfs", "parserOptions": { + "project": true, "sourceType": "module" }, "ignorePatterns": [ @@ -85,7 +86,7 @@ "generate": "protons ./src/keys/keys.proto" }, "dependencies": { - "@libp2p/interface": "^0.1.2", + "@libp2p/interface": "^0.1.3", "@noble/curves": "^1.1.0", "@noble/hashes": "^1.3.1", "multiformats": "^12.0.1", @@ -96,7 +97,7 @@ }, "devDependencies": { "@types/mocha": "^10.0.0", - "aegir": "^40.0.8", + "aegir": "^41.0.2", "benchmark": "^2.1.4", "protons": "^7.0.2" }, diff --git a/packages/crypto/src/aes/ciphers-browser.ts b/packages/crypto/src/aes/ciphers-browser.ts index f78388f721..da94b50857 100644 --- a/packages/crypto/src/aes/ciphers-browser.ts +++ b/packages/crypto/src/aes/ciphers-browser.ts @@ -5,7 +5,7 @@ import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' import { toString as uint8ArrayToString } from 'uint8arrays/to-string' export interface Cipher { - update: (data: Uint8Array) => Uint8Array + update(data: Uint8Array): Uint8Array } export function createCipheriv (mode: any, key: Uint8Array, iv: Uint8Array): Cipher { diff --git a/packages/crypto/src/aes/index.ts b/packages/crypto/src/aes/index.ts index b10b108747..cf1dd8b875 100644 --- a/packages/crypto/src/aes/index.ts +++ b/packages/crypto/src/aes/index.ts @@ -2,8 +2,8 @@ import { cipherMode } from './cipher-mode.js' import * as ciphers from './ciphers.js' export interface AESCipher { - encrypt: (data: Uint8Array) => Promise - decrypt: (data: Uint8Array) => Promise + encrypt(data: Uint8Array): Promise + decrypt(data: Uint8Array): Promise } export async function create (key: Uint8Array, iv: Uint8Array): Promise { diff --git a/packages/crypto/src/ciphers/interface.ts b/packages/crypto/src/ciphers/interface.ts index f4258e11db..9f30045bf6 100644 --- a/packages/crypto/src/ciphers/interface.ts +++ b/packages/crypto/src/ciphers/interface.ts @@ -9,6 +9,6 @@ export interface CreateOptions { } export interface AESCipher { - encrypt: (data: Uint8Array, password: string | Uint8Array) => Promise - decrypt: (data: Uint8Array, password: string | Uint8Array) => Promise + encrypt(data: Uint8Array, password: string | Uint8Array): Promise + decrypt(data: Uint8Array, password: string | Uint8Array): Promise } diff --git a/packages/crypto/src/hmac/index-browser.ts b/packages/crypto/src/hmac/index-browser.ts index 683d130ae1..69d7947d9e 100644 --- a/packages/crypto/src/hmac/index-browser.ts +++ b/packages/crypto/src/hmac/index-browser.ts @@ -12,7 +12,7 @@ const sign = async (key: CryptoKey, data: Uint8Array): Promise => { return new Uint8Array(buf, 0, buf.byteLength) } -export async function create (hashType: 'SHA1' | 'SHA256' | 'SHA512', secret: Uint8Array): Promise<{ digest: (data: Uint8Array) => Promise, length: number }> { +export async function create (hashType: 'SHA1' | 'SHA256' | 'SHA512', secret: Uint8Array): Promise<{ digest(data: Uint8Array): Promise, length: number }> { const hash = hashTypes[hashType] const key = await webcrypto.get().subtle.importKey( diff --git a/packages/crypto/src/hmac/index.ts b/packages/crypto/src/hmac/index.ts index b3d7689168..dc2ec68894 100644 --- a/packages/crypto/src/hmac/index.ts +++ b/packages/crypto/src/hmac/index.ts @@ -2,7 +2,7 @@ import crypto from 'crypto' import lengths from './lengths.js' export interface HMAC { - digest: (data: Uint8Array) => Promise + digest(data: Uint8Array): Promise length: number } diff --git a/packages/crypto/src/keys/index.ts b/packages/crypto/src/keys/index.ts index 3b50a33b2e..d8192d5abb 100644 --- a/packages/crypto/src/keys/index.ts +++ b/packages/crypto/src/keys/index.ts @@ -69,7 +69,7 @@ export function unmarshalPublicKey (buf: Uint8Array): PublicKey { case keysPBM.KeyType.Secp256k1: return supportedKeys.secp256k1.unmarshalSecp256k1PublicKey(data) default: - throw unsupportedKey(decoded.Type ?? 'RSA') + throw unsupportedKey(decoded.Type ?? 'unknown') } } diff --git a/packages/crypto/src/keys/interface.ts b/packages/crypto/src/keys/interface.ts index 4630df6a49..2cbfd5a12b 100644 --- a/packages/crypto/src/keys/interface.ts +++ b/packages/crypto/src/keys/interface.ts @@ -15,7 +15,7 @@ export interface ECDHKeyPair { export interface ECDHKey { key: Uint8Array - genSharedKey: (theirPub: Uint8Array, forcePrivate?: ECDHKeyPair) => Promise + genSharedKey(theirPub: Uint8Array, forcePrivate?: ECDHKeyPair): Promise } export interface JWKEncodedPublicKey { kty: string, crv: 'P-256' | 'P-384' | 'P-521', x: string, y: string, ext: boolean } diff --git a/packages/crypto/src/keys/jwk2pem.ts b/packages/crypto/src/keys/jwk2pem.ts index 64feebc188..d827f282d9 100644 --- a/packages/crypto/src/keys/jwk2pem.ts +++ b/packages/crypto/src/keys/jwk2pem.ts @@ -4,8 +4,8 @@ import forge from 'node-forge/lib/forge.js' import { base64urlToBigInteger } from '../util.js' export interface JWK { - encrypt: (msg: string) => string - decrypt: (msg: string) => string + encrypt(msg: string): string + decrypt(msg: string): string } function convert (key: any, types: string[]): Array { diff --git a/packages/crypto/src/keys/rsa-browser.ts b/packages/crypto/src/keys/rsa-browser.ts index 2f0e0c04b1..0e88e75c55 100644 --- a/packages/crypto/src/keys/rsa-browser.ts +++ b/packages/crypto/src/keys/rsa-browser.ts @@ -141,7 +141,7 @@ Explanation: */ -function convertKey (key: JsonWebKey, pub: boolean, msg: Uint8Array, handle: (msg: string, key: { encrypt: (msg: string) => string, decrypt: (msg: string) => string }) => string): Uint8Array { +function convertKey (key: JsonWebKey, pub: boolean, msg: Uint8Array, handle: (msg: string, key: { encrypt(msg: string): string, decrypt(msg: string): string }) => string): Uint8Array { const fkey = pub ? jwk2pub(key) : jwk2priv(key) const fmsg = uint8ArrayToString(Uint8Array.from(msg), 'ascii') const fomsg = handle(fmsg, fkey) diff --git a/packages/crypto/src/util.ts b/packages/crypto/src/util.ts index e0bab8c5ff..0dab953f8a 100644 --- a/packages/crypto/src/util.ts +++ b/packages/crypto/src/util.ts @@ -6,7 +6,7 @@ import { concat as uint8ArrayConcat } from 'uint8arrays/concat' import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' import { toString as uint8ArrayToString } from 'uint8arrays/to-string' -export function bigIntegerToUintBase64url (num: { abs: () => any }, len?: number): string { +export function bigIntegerToUintBase64url (num: { abs(): any }, len?: number): string { // Call `.abs()` to convert to unsigned let buf = Uint8Array.from(num.abs().toByteArray()) // toByteArray converts to big endian diff --git a/packages/interface-compliance-tests/CHANGELOG.md b/packages/interface-compliance-tests/CHANGELOG.md index e27331502b..a9ef37cb25 100644 --- a/packages/interface-compliance-tests/CHANGELOG.md +++ b/packages/interface-compliance-tests/CHANGELOG.md @@ -5,6 +5,46 @@ * bump aegir from 38.1.8 to 39.0.5 ([#393](https://github.com/libp2p/js-libp2p-interfaces/issues/393)) ([31f3797](https://github.com/libp2p/js-libp2p-interfaces/commit/31f3797b24f7c23f3f16e9db3a230bd5f7cd5175)) +### [4.1.1](https://www.github.com/libp2p/js-libp2p/compare/interface-compliance-tests-v4.1.0...interface-compliance-tests-v4.1.1) (2023-10-06) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @libp2p/interface bumped from ^0.1.2 to ^0.1.3 + * @libp2p/interface-internal bumped from ^0.1.5 to ^0.1.6 + * @libp2p/logger bumped from ^3.0.2 to ^3.0.3 + * @libp2p/multistream-select bumped from ^4.0.2 to ^4.0.3 + * @libp2p/peer-collections bumped from ^4.0.4 to ^4.0.5 + * @libp2p/peer-id bumped from ^3.0.2 to ^3.0.3 + * @libp2p/peer-id-factory bumped from ^3.0.4 to ^3.0.5 + +## [4.1.0](https://www.github.com/libp2p/js-libp2p/compare/interface-compliance-tests-v4.0.6...interface-compliance-tests-v4.1.0) (2023-10-01) + + +### Features + +* add mock stream pair ([#2069](https://www.github.com/libp2p/js-libp2p/issues/2069)) ([e3ab192](https://www.github.com/libp2p/js-libp2p/commit/e3ab1929b505df6d50b5a6ddc50cd2669f54b894)) + +### [4.0.6](https://www.github.com/libp2p/js-libp2p/compare/interface-compliance-tests-v4.0.5...interface-compliance-tests-v4.0.6) (2023-09-15) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @libp2p/interface-internal bumped from ^0.1.4 to ^0.1.5 + * @libp2p/peer-collections bumped from ^4.0.3 to ^4.0.4 + * @libp2p/peer-id-factory bumped from ^3.0.3 to ^3.0.4 + +### [4.0.5](https://www.github.com/libp2p/js-libp2p/compare/interface-compliance-tests-v4.0.4...interface-compliance-tests-v4.0.5) (2023-08-25) + + +### Bug Fixes + +* **@libp2p/interface-compliance-tests:** add aegir to deps ([#1983](https://www.github.com/libp2p/js-libp2p/issues/1983)) ([8977862](https://www.github.com/libp2p/js-libp2p/commit/89778624908a536e3253ee4fe1a0d287e1aad2e9)), closes [#1974](https://www.github.com/libp2p/js-libp2p/issues/1974) + ### [4.0.4](https://www.github.com/libp2p/js-libp2p/compare/interface-compliance-tests-v4.0.3...interface-compliance-tests-v4.0.4) (2023-08-16) diff --git a/packages/interface-compliance-tests/package.json b/packages/interface-compliance-tests/package.json index 0864500c2d..f467311297 100644 --- a/packages/interface-compliance-tests/package.json +++ b/packages/interface-compliance-tests/package.json @@ -1,6 +1,6 @@ { "name": "@libp2p/interface-compliance-tests", - "version": "4.0.4", + "version": "4.1.1", "description": "Compliance tests for JS libp2p interfaces", "license": "Apache-2.0 OR MIT", "homepage": "https://github.com/libp2p/js-libp2p/tree/master/packages/interface-compliance-tests#readme", @@ -84,6 +84,7 @@ "eslintConfig": { "extends": "ipfs", "parserOptions": { + "project": true, "sourceType": "module" } }, @@ -102,15 +103,16 @@ "test:electron-main": "aegir test -t electron-main" }, "dependencies": { - "@libp2p/interface": "^0.1.2", - "@libp2p/interface-internal": "^0.1.4", - "@libp2p/logger": "^3.0.2", - "@libp2p/multistream-select": "^4.0.2", - "@libp2p/peer-collections": "^4.0.3", - "@libp2p/peer-id": "^3.0.2", - "@libp2p/peer-id-factory": "^3.0.3", + "@libp2p/interface": "^0.1.3", + "@libp2p/interface-internal": "^0.1.6", + "@libp2p/logger": "^3.0.3", + "@libp2p/multistream-select": "^4.0.3", + "@libp2p/peer-collections": "^4.0.5", + "@libp2p/peer-id": "^3.0.3", + "@libp2p/peer-id-factory": "^3.0.5", "@multiformats/multiaddr": "^12.1.5", "abortable-iterator": "^5.0.1", + "aegir": "^41.0.2", "delay": "^6.0.0", "it-all": "^3.0.2", "it-drain": "^3.0.2", @@ -129,12 +131,11 @@ "p-limit": "^4.0.0", "p-wait-for": "^5.0.2", "protons-runtime": "^5.0.0", - "sinon": "^15.1.2", + "sinon": "^16.0.0", "uint8arraylist": "^2.4.3", "uint8arrays": "^4.0.6" }, "devDependencies": { - "aegir": "^40.0.8", "protons": "^7.0.2" } } diff --git a/packages/interface-compliance-tests/src/index.ts b/packages/interface-compliance-tests/src/index.ts index e9f9db76b3..fec38b0660 100644 --- a/packages/interface-compliance-tests/src/index.ts +++ b/packages/interface-compliance-tests/src/index.ts @@ -1,4 +1,4 @@ export interface TestSetup> { - setup: (args?: SetupArgs) => Promise - teardown: () => Promise + setup(args?: SetupArgs): Promise + teardown(): Promise } diff --git a/packages/interface-compliance-tests/src/mocks/connection.ts b/packages/interface-compliance-tests/src/mocks/connection.ts index 566de6d05c..8b1607d2ad 100644 --- a/packages/interface-compliance-tests/src/mocks/connection.ts +++ b/packages/interface-compliance-tests/src/mocks/connection.ts @@ -170,7 +170,13 @@ export function mockConnection (maConn: MultiaddrConnection, opts: MockConnectio return connection } -export function mockStream (stream: Duplex, Source, Promise>): Stream { +export interface StreamInit { + direction?: Direction + protocol?: string + id?: string +} + +export function mockStream (stream: Duplex, Source, Promise>, init: StreamInit = {}): Stream { return { ...stream, close: async () => {}, @@ -186,10 +192,31 @@ export function mockStream (stream: Duplex, Sourc id: `stream-${Date.now()}`, status: 'open', readStatus: 'ready', - writeStatus: 'ready' + writeStatus: 'ready', + ...init } } +export interface StreamPairInit { + duplex: Duplex, Source, Promise> + init?: StreamInit +} + +export function streamPair (a: StreamPairInit, b: StreamPairInit, init: StreamInit = {}): [Stream, Stream] { + return [ + mockStream(a.duplex, { + direction: 'outbound', + ...init, + ...(a.init ?? {}) + }), + mockStream(b.duplex, { + direction: 'inbound', + ...init, + ...(b.init ?? {}) + }) + ] +} + export interface Peer { peerId: PeerId registrar: Registrar diff --git a/packages/interface-compliance-tests/src/mocks/index.ts b/packages/interface-compliance-tests/src/mocks/index.ts index 8b78f1f7f3..ffdef08ac3 100644 --- a/packages/interface-compliance-tests/src/mocks/index.ts +++ b/packages/interface-compliance-tests/src/mocks/index.ts @@ -1,7 +1,7 @@ export { mockConnectionEncrypter } from './connection-encrypter.js' export { mockConnectionGater } from './connection-gater.js' export { mockConnectionManager, mockNetwork } from './connection-manager.js' -export { mockConnection, mockStream, connectionPair } from './connection.js' +export { mockConnection, mockStream, streamPair, connectionPair } from './connection.js' export { mockMultiaddrConnection, mockMultiaddrConnPair } from './multiaddr-connection.js' export { mockMuxer } from './muxer.js' export { mockRegistrar } from './registrar.js' diff --git a/packages/interface-compliance-tests/src/transport/index.ts b/packages/interface-compliance-tests/src/transport/index.ts index d2b9525687..c0a5a6ed11 100644 --- a/packages/interface-compliance-tests/src/transport/index.ts +++ b/packages/interface-compliance-tests/src/transport/index.ts @@ -6,8 +6,8 @@ import type { Transport } from '@libp2p/interface/transport' import type { Multiaddr } from '@multiformats/multiaddr' export interface Connector { - delay: (ms: number) => void - restore: () => void + delay(ms: number): void + restore(): void } export interface TransportTestFixtures { diff --git a/packages/interface-internal/CHANGELOG.md b/packages/interface-internal/CHANGELOG.md index 94ee19579a..4d159c1220 100644 --- a/packages/interface-internal/CHANGELOG.md +++ b/packages/interface-internal/CHANGELOG.md @@ -1,5 +1,24 @@ # Changelog +### [0.1.6](https://www.github.com/libp2p/js-libp2p/compare/interface-internal-v0.1.5...interface-internal-v0.1.6) (2023-10-06) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @libp2p/interface bumped from ^0.1.2 to ^0.1.3 + * @libp2p/peer-collections bumped from ^4.0.4 to ^4.0.5 + +### [0.1.5](https://www.github.com/libp2p/js-libp2p/compare/interface-internal-v0.1.4...interface-internal-v0.1.5) (2023-09-15) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @libp2p/peer-collections bumped from ^4.0.3 to ^4.0.4 + ### [0.1.4](https://www.github.com/libp2p/js-libp2p/compare/interface-internal-v0.1.3...interface-internal-v0.1.4) (2023-08-16) diff --git a/packages/interface-internal/package.json b/packages/interface-internal/package.json index 8774b5c944..f8ea476d52 100644 --- a/packages/interface-internal/package.json +++ b/packages/interface-internal/package.json @@ -1,6 +1,6 @@ { "name": "@libp2p/interface-internal", - "version": "0.1.4", + "version": "0.1.6", "description": "Interfaces implemented by internal libp2p components", "license": "Apache-2.0 OR MIT", "homepage": "https://github.com/libp2p/js-libp2p/tree/master/packages/interface-internal#readme", @@ -68,6 +68,7 @@ "eslintConfig": { "extends": "ipfs", "parserOptions": { + "project": true, "sourceType": "module" } }, @@ -78,12 +79,12 @@ "build": "aegir build" }, "dependencies": { - "@libp2p/interface": "^0.1.2", - "@libp2p/peer-collections": "^4.0.3", + "@libp2p/interface": "^0.1.3", + "@libp2p/peer-collections": "^4.0.5", "@multiformats/multiaddr": "^12.1.5", "uint8arraylist": "^2.4.3" }, "devDependencies": { - "aegir": "^40.0.8" + "aegir": "^41.0.2" } } diff --git a/packages/interface-internal/src/address-manager/index.ts b/packages/interface-internal/src/address-manager/index.ts index 622e0cb315..89b4231cca 100644 --- a/packages/interface-internal/src/address-manager/index.ts +++ b/packages/interface-internal/src/address-manager/index.ts @@ -4,40 +4,40 @@ export interface AddressManager { /** * Get peer listen multiaddrs */ - getListenAddrs: () => Multiaddr[] + getListenAddrs(): Multiaddr[] /** * Get peer announcing multiaddrs */ - getAnnounceAddrs: () => Multiaddr[] + getAnnounceAddrs(): Multiaddr[] /** * Get observed multiaddrs - these addresses may not have been confirmed as * publicly dialable yet */ - getObservedAddrs: () => Multiaddr[] + getObservedAddrs(): Multiaddr[] /** * Signal that we have confidence an observed multiaddr is publicly dialable - * this will make it appear in the output of getAddresses() */ - confirmObservedAddr: (addr: Multiaddr) => void + confirmObservedAddr(addr: Multiaddr): void /** * Signal that we do not have confidence an observed multiaddr is publicly dialable - * this will remove it from the output of getObservedAddrs() */ - removeObservedAddr: (addr: Multiaddr) => void + removeObservedAddr(addr: Multiaddr): void /** * Add peer observed addresses. These will then appear in the output of getObservedAddrs * but not getAddresses() until their dialability has been confirmed via a call to * confirmObservedAddr. */ - addObservedAddr: (addr: Multiaddr) => void + addObservedAddr(addr: Multiaddr): void /** * Get the current node's addresses */ - getAddresses: () => Multiaddr[] + getAddresses(): Multiaddr[] } diff --git a/packages/interface-internal/src/connection-manager/index.ts b/packages/interface-internal/src/connection-manager/index.ts index 74d7119179..66051b4e41 100644 --- a/packages/interface-internal/src/connection-manager/index.ts +++ b/packages/interface-internal/src/connection-manager/index.ts @@ -30,7 +30,7 @@ export interface ConnectionManager { * // [] * ``` */ - getConnections: (peerId?: PeerId) => Connection[] + getConnections(peerId?: PeerId): Connection[] /** * Return a map of all connections with their associated PeerIds @@ -41,7 +41,7 @@ export interface ConnectionManager { * const connectionsMap = libp2p.connectionManager.getConnectionsMap() * ``` */ - getConnectionsMap: () => PeerMap + getConnectionsMap(): PeerMap /** * Open a connection to a remote peer @@ -52,12 +52,12 @@ export interface ConnectionManager { * const connection = await libp2p.connectionManager.openConnection(peerId) * ``` */ - openConnection: (peer: PeerId | Multiaddr | Multiaddr[], options?: OpenConnectionOptions) => Promise + openConnection(peer: PeerId | Multiaddr | Multiaddr[], options?: OpenConnectionOptions): Promise /** * Close our connections to a peer */ - closeConnections: (peer: PeerId, options?: AbortOptions) => Promise + closeConnections(peer: PeerId, options?: AbortOptions): Promise /** * Invoked after an incoming connection is opened but before PeerIds are @@ -65,12 +65,12 @@ export interface ConnectionManager { * resources to accept the connection in which case it will return true, * otherwise it will return false. */ - acceptIncomingConnection: (maConn: MultiaddrConnection) => Promise + acceptIncomingConnection(maConn: MultiaddrConnection): Promise /** * Invoked after upgrading a multiaddr connection has finished */ - afterUpgradeInbound: () => void + afterUpgradeInbound(): void /** * Return the list of in-progress or queued dials @@ -81,5 +81,5 @@ export interface ConnectionManager { * const dials = libp2p.connectionManager.getDialQueue() * ``` */ - getDialQueue: () => PendingDial[] + getDialQueue(): PendingDial[] } diff --git a/packages/interface-internal/src/record/index.ts b/packages/interface-internal/src/record/index.ts index 1005ba0d02..ff27b0460b 100644 --- a/packages/interface-internal/src/record/index.ts +++ b/packages/interface-internal/src/record/index.ts @@ -16,11 +16,11 @@ export interface Record { /** * Marshal a record to be used in an envelope. */ - marshal: () => Uint8Array + marshal(): Uint8Array /** * Verifies if the other provided Record is identical to this one. */ - equals: (other: Record) => boolean + equals(other: Record): boolean } export interface Envelope { @@ -29,7 +29,7 @@ export interface Envelope { payload: Uint8Array signature: Uint8Array | Uint8ArrayList - marshal: () => Uint8Array - validate: (domain: string) => Promise - equals: (other: Envelope) => boolean + marshal(): Uint8Array + validate(domain: string): Promise + equals(other: Envelope): boolean } diff --git a/packages/interface-internal/src/registrar/index.ts b/packages/interface-internal/src/registrar/index.ts index a833cf7940..52db9d5b3e 100644 --- a/packages/interface-internal/src/registrar/index.ts +++ b/packages/interface-internal/src/registrar/index.ts @@ -38,22 +38,22 @@ export interface Registrar { /** * Return the list of protocols with registered handlers */ - getProtocols: () => string[] + getProtocols(): string[] /** * Add a protocol handler */ - handle: (protocol: string, handler: StreamHandler, options?: StreamHandlerOptions) => Promise + handle(protocol: string, handler: StreamHandler, options?: StreamHandlerOptions): Promise /** * Remove a protocol handler */ - unhandle: (protocol: string) => Promise + unhandle(protocol: string): Promise /** * Return the handler for the passed protocol */ - getHandler: (protocol: string) => StreamHandlerRecord + getHandler(protocol: string): StreamHandlerRecord /** * Register a topology handler for a protocol - the topology will be @@ -63,16 +63,16 @@ export interface Registrar { * An id will be returned that can later be used to unregister the * topology. */ - register: (protocol: string, topology: Topology) => Promise + register(protocol: string, topology: Topology): Promise /** * Remove the topology handler with the passed id. */ - unregister: (id: string) => void + unregister(id: string): void /** * Return all topology handlers that wish to be informed about peers * that support the passed protocol. */ - getTopologies: (protocol: string) => Topology[] + getTopologies(protocol: string): Topology[] } diff --git a/packages/interface-internal/src/transport-manager/index.ts b/packages/interface-internal/src/transport-manager/index.ts index b649969be2..a905d2a14b 100644 --- a/packages/interface-internal/src/transport-manager/index.ts +++ b/packages/interface-internal/src/transport-manager/index.ts @@ -3,13 +3,13 @@ import type { Listener, Transport } from '@libp2p/interface/transport' import type { Multiaddr } from '@multiformats/multiaddr' export interface TransportManager { - add: (transport: Transport) => void - dial: (ma: Multiaddr, options?: any) => Promise - getAddrs: () => Multiaddr[] - getTransports: () => Transport[] - getListeners: () => Listener[] - transportForMultiaddr: (ma: Multiaddr) => Transport | undefined - listen: (addrs: Multiaddr[]) => Promise - remove: (key: string) => Promise - removeAll: () => Promise + add(transport: Transport): void + dial(ma: Multiaddr, options?: any): Promise + getAddrs(): Multiaddr[] + getTransports(): Transport[] + getListeners(): Listener[] + transportForMultiaddr(ma: Multiaddr): Transport | undefined + listen(addrs: Multiaddr[]): Promise + remove(key: string): Promise + removeAll(): Promise } diff --git a/packages/interface/CHANGELOG.md b/packages/interface/CHANGELOG.md index 5173192b24..145ba5c8bc 100644 --- a/packages/interface/CHANGELOG.md +++ b/packages/interface/CHANGELOG.md @@ -5,6 +5,13 @@ * add start/stop events to libp2p interface ([#407](https://github.com/libp2p/js-libp2p-interfaces/issues/407)) ([016c1e8](https://github.com/libp2p/js-libp2p-interfaces/commit/016c1e82b060c93c80546cd8c493ec6e6c97cbec)) +### [0.1.3](https://www.github.com/libp2p/js-libp2p/compare/interface-v0.1.2...interface-v0.1.3) (2023-10-06) + + +### Bug Fixes + +* close webrtc streams without data loss ([#2073](https://www.github.com/libp2p/js-libp2p/issues/2073)) ([7d8b155](https://www.github.com/libp2p/js-libp2p/commit/7d8b15517a480e01a8ebd427ab0093509b78d5b0)) + ### [0.1.2](https://www.github.com/libp2p/js-libp2p/compare/interface-v0.1.1...interface-v0.1.2) (2023-08-14) diff --git a/packages/interface/package.json b/packages/interface/package.json index 194b1e40f4..801c1023a2 100644 --- a/packages/interface/package.json +++ b/packages/interface/package.json @@ -1,6 +1,6 @@ { "name": "@libp2p/interface", - "version": "0.1.2", + "version": "0.1.3", "description": "The interface implemented by a libp2p node", "license": "Apache-2.0 OR MIT", "homepage": "https://github.com/libp2p/js-libp2p/tree/master/packages/interface#readme", @@ -140,6 +140,7 @@ "eslintConfig": { "extends": "ipfs", "parserOptions": { + "project": true, "sourceType": "module" } }, @@ -164,12 +165,16 @@ "multiformats": "^12.0.1", "p-defer": "^4.0.0", "progress-events": "^1.0.0", + "race-signal": "^1.0.0", "uint8arraylist": "^2.4.3" }, "devDependencies": { "@types/sinon": "^10.0.15", - "aegir": "^40.0.8", - "sinon": "^15.1.2", + "aegir": "^41.0.2", + "delay": "^6.0.0", + "it-all": "^3.0.3", + "it-drain": "^3.0.3", + "sinon": "^16.0.0", "sinon-ts": "^1.0.0" } } diff --git a/packages/interface/src/connection-encrypter/index.ts b/packages/interface/src/connection-encrypter/index.ts index 0a4245db33..7faf799953 100644 --- a/packages/interface/src/connection-encrypter/index.ts +++ b/packages/interface/src/connection-encrypter/index.ts @@ -13,14 +13,14 @@ export interface ConnectionEncrypter { * pass it for extra verification, otherwise it will be determined during * the handshake. */ - secureOutbound: (localPeer: PeerId, connection: Duplex, Source, Promise>, remotePeer?: PeerId) => Promise> + secureOutbound(localPeer: PeerId, connection: Duplex, Source, Promise>, remotePeer?: PeerId): Promise> /** * Decrypt incoming data. If the remote PeerId is known, * pass it for extra verification, otherwise it will be determined during * the handshake */ - secureInbound: (localPeer: PeerId, connection: Duplex, Source, Promise>, remotePeer?: PeerId) => Promise> + secureInbound(localPeer: PeerId, connection: Duplex, Source, Promise>, remotePeer?: PeerId): Promise> } export interface SecuredConnection { diff --git a/packages/interface/src/connection-gater/index.ts b/packages/interface/src/connection-gater/index.ts index 4d85f5806a..1b59de4f38 100644 --- a/packages/interface/src/connection-gater/index.ts +++ b/packages/interface/src/connection-gater/index.ts @@ -12,7 +12,7 @@ export interface ConnectionGater { * * Return true to prevent dialing the passed peer. */ - denyDialPeer?: (peerId: PeerId) => Promise + denyDialPeer?(peerId: PeerId): Promise /** * denyDialMultiaddr tests whether we're permitted to dial the specified @@ -23,7 +23,7 @@ export interface ConnectionGater { * * Return true to prevent dialing the passed peer on the passed multiaddr. */ - denyDialMultiaddr?: (multiaddr: Multiaddr) => Promise + denyDialMultiaddr?(multiaddr: Multiaddr): Promise /** * denyInboundConnection tests whether an incipient inbound connection is allowed. @@ -33,7 +33,7 @@ export interface ConnectionGater { * * Return true to deny the incoming passed connection. */ - denyInboundConnection?: (maConn: MultiaddrConnection) => Promise + denyInboundConnection?(maConn: MultiaddrConnection): Promise /** * denyOutboundConnection tests whether an incipient outbound connection is allowed. @@ -43,7 +43,7 @@ export interface ConnectionGater { * * Return true to deny the incoming passed connection. */ - denyOutboundConnection?: (peerId: PeerId, maConn: MultiaddrConnection) => Promise + denyOutboundConnection?(peerId: PeerId, maConn: MultiaddrConnection): Promise /** * denyInboundEncryptedConnection tests whether a given connection, now encrypted, @@ -55,7 +55,7 @@ export interface ConnectionGater { * * Return true to deny the passed secured connection. */ - denyInboundEncryptedConnection?: (peerId: PeerId, maConn: MultiaddrConnection) => Promise + denyInboundEncryptedConnection?(peerId: PeerId, maConn: MultiaddrConnection): Promise /** * denyOutboundEncryptedConnection tests whether a given connection, now encrypted, @@ -67,7 +67,7 @@ export interface ConnectionGater { * * Return true to deny the passed secured connection. */ - denyOutboundEncryptedConnection?: (peerId: PeerId, maConn: MultiaddrConnection) => Promise + denyOutboundEncryptedConnection?(peerId: PeerId, maConn: MultiaddrConnection): Promise /** * denyInboundUpgradedConnection tests whether a fully capable connection is allowed. @@ -77,7 +77,7 @@ export interface ConnectionGater { * * Return true to deny the passed upgraded connection. */ - denyInboundUpgradedConnection?: (peerId: PeerId, maConn: MultiaddrConnection) => Promise + denyInboundUpgradedConnection?(peerId: PeerId, maConn: MultiaddrConnection): Promise /** * denyOutboundUpgradedConnection tests whether a fully capable connection is allowed. @@ -87,7 +87,7 @@ export interface ConnectionGater { * * Return true to deny the passed upgraded connection. */ - denyOutboundUpgradedConnection?: (peerId: PeerId, maConn: MultiaddrConnection) => Promise + denyOutboundUpgradedConnection?(peerId: PeerId, maConn: MultiaddrConnection): Promise /** * denyInboundRelayReservation tests whether a remote peer is allowed make a @@ -95,7 +95,7 @@ export interface ConnectionGater { * * Return true to deny the relay reservation. */ - denyInboundRelayReservation?: (source: PeerId) => Promise + denyInboundRelayReservation?(source: PeerId): Promise /** * denyOutboundRelayedConnection tests whether a remote peer is allowed to open a relayed @@ -106,7 +106,7 @@ export interface ConnectionGater { * * Return true to deny the relayed connection. */ - denyOutboundRelayedConnection?: (source: PeerId, destination: PeerId) => Promise + denyOutboundRelayedConnection?(source: PeerId, destination: PeerId): Promise /** * denyInboundRelayedConnection tests whether a remote peer is allowed to open a relayed @@ -117,12 +117,12 @@ export interface ConnectionGater { * * Return true to deny the relayed connection. */ - denyInboundRelayedConnection?: (relay: PeerId, remotePeer: PeerId) => Promise + denyInboundRelayedConnection?(relay: PeerId, remotePeer: PeerId): Promise /** * Used by the address book to filter passed addresses. * * Return true to allow storing the passed multiaddr for the passed peer. */ - filterMultiaddrForPeer?: (peer: PeerId, multiaddr: Multiaddr) => Promise + filterMultiaddrForPeer?(peer: PeerId, multiaddr: Multiaddr): Promise } diff --git a/packages/interface/src/connection/index.ts b/packages/interface/src/connection/index.ts index 2bed6736a1..684a2b9adb 100644 --- a/packages/interface/src/connection/index.ts +++ b/packages/interface/src/connection/index.ts @@ -101,7 +101,7 @@ export interface Stream extends Duplex, Source Promise + close(options?: AbortOptions): Promise /** * Closes the stream for **reading**. If iterating over the source of this stream in a `for await of` loop, it will return (exit the loop) after any buffered data has been consumed. @@ -110,14 +110,14 @@ export interface Stream extends Duplex, Source Promise + closeRead(options?: AbortOptions): Promise /** * Closes the stream for **writing**. If iterating over the source of this stream in a `for await of` loop, it will return (exit the loop) after any buffered data has been consumed. * * The source will return normally, the sink will continue to consume. */ - closeWrite: (options?: AbortOptions) => Promise + closeWrite(options?: AbortOptions): Promise /** * Closes the stream for **reading** *and* **writing**. This should be called when a *local error* has occurred. @@ -128,7 +128,7 @@ export interface Stream extends Duplex, Source void + abort(err: Error): void /** * Unique identifier for a stream. Identifiers are not unique across muxers. @@ -146,7 +146,7 @@ export interface Stream extends Duplex, Source Promise + newStream(protocols: string | string[], options?: NewStreamOptions): Promise /** * Gracefully close the connection. All queued data will be written to the * underlying transport. */ - close: (options?: AbortOptions) => Promise + close(options?: AbortOptions): Promise /** * Immediately close the connection, any queued data will be discarded */ - abort: (err: Error) => void + abort(err: Error): void } export const symbol = Symbol.for('@libp2p/connection') @@ -282,7 +282,7 @@ export interface ConnectionProtector { * between its two peers from the PSK the Protector instance was * created with. */ - protect: (connection: MultiaddrConnection) => Promise + protect(connection: MultiaddrConnection): Promise } export interface MultiaddrConnectionTimeline { @@ -313,12 +313,12 @@ export interface MultiaddrConnection extends Duplex, * Gracefully close the connection. All queued data will be written to the * underlying transport. */ - close: (options?: AbortOptions) => Promise + close(options?: AbortOptions): Promise /** * Immediately close the connection, any queued data will be discarded */ - abort: (err: Error) => void + abort(err: Error): void /** * The address of the remote end of the connection diff --git a/packages/interface/src/content-routing/index.ts b/packages/interface/src/content-routing/index.ts index 7f094d3fab..0cfa498605 100644 --- a/packages/interface/src/content-routing/index.ts +++ b/packages/interface/src/content-routing/index.ts @@ -41,7 +41,7 @@ export interface ContentRouting< * await contentRouting.provide(cid) * ``` */ - provide: (cid: CID, options?: AbortOptions & ProgressOptions) => Promise + provide(cid: CID, options?: AbortOptions & ProgressOptions): Promise /** * Find the providers of the passed CID. @@ -55,7 +55,7 @@ export interface ContentRouting< * } * ``` */ - findProviders: (cid: CID, options?: AbortOptions & ProgressOptions) => AsyncIterable + findProviders(cid: CID, options?: AbortOptions & ProgressOptions): AsyncIterable /** * Puts a value corresponding to the passed key in a way that can later be @@ -71,7 +71,7 @@ export interface ContentRouting< * await contentRouting.put(key, value) * ``` */ - put: (key: Uint8Array, value: Uint8Array, options?: AbortOptions & ProgressOptions) => Promise + put(key: Uint8Array, value: Uint8Array, options?: AbortOptions & ProgressOptions): Promise /** * Retrieves a value from the network corresponding to the passed key. @@ -85,5 +85,5 @@ export interface ContentRouting< * const value = await contentRouting.get(key) * ``` */ - get: (key: Uint8Array, options?: AbortOptions & ProgressOptions) => Promise + get(key: Uint8Array, options?: AbortOptions & ProgressOptions): Promise } diff --git a/packages/interface/src/index.ts b/packages/interface/src/index.ts index 5e5fb9b48d..8a59407c39 100644 --- a/packages/interface/src/index.ts +++ b/packages/interface/src/index.ts @@ -459,7 +459,7 @@ export interface Libp2p< * // [ ] * ``` */ - getMultiaddrs: () => Multiaddr[] + getMultiaddrs(): Multiaddr[] /** * Returns a list of supported protocols @@ -471,7 +471,7 @@ export interface Libp2p< * // [ '/ipfs/ping/1.0.0', '/ipfs/id/1.0.0' ] * ``` */ - getProtocols: () => string[] + getProtocols(): string[] /** * Return a list of all connections this node has open, optionally filtering @@ -486,7 +486,7 @@ export interface Libp2p< * } * ``` */ - getConnections: (peerId?: PeerId) => Connection[] + getConnections(peerId?: PeerId): Connection[] /** * Return the list of dials currently in progress or queued to start @@ -499,12 +499,12 @@ export interface Libp2p< * } * ``` */ - getDialQueue: () => PendingDial[] + getDialQueue(): PendingDial[] /** * Return a list of all peers we currently have a connection open to */ - getPeers: () => PeerId[] + getPeers(): PeerId[] /** * Dials to the provided peer. If successful, the known metadata of the @@ -526,7 +526,7 @@ export interface Libp2p< * await conn.close() * ``` */ - dial: (peer: PeerId | Multiaddr | Multiaddr[], options?: AbortOptions) => Promise + dial(peer: PeerId | Multiaddr | Multiaddr[], options?: AbortOptions): Promise /** * Dials to the provided peer and tries to handshake with the given protocols in order. @@ -544,7 +544,7 @@ export interface Libp2p< * pipe([1, 2, 3], stream, consume) * ``` */ - dialProtocol: (peer: PeerId | Multiaddr | Multiaddr[], protocols: string | string[], options?: NewStreamOptions) => Promise + dialProtocol(peer: PeerId | Multiaddr | Multiaddr[], protocols: string | string[], options?: NewStreamOptions): Promise /** * Attempts to gracefully close an open connection to the given peer. If the @@ -559,7 +559,7 @@ export interface Libp2p< * await libp2p.hangUp(remotePeerId) * ``` */ - hangUp: (peer: PeerId | Multiaddr, options?: AbortOptions) => Promise + hangUp(peer: PeerId | Multiaddr, options?: AbortOptions): Promise /** * Sets up [multistream-select routing](https://github.com/multiformats/multistream-select) of protocols to their application handlers. Whenever a stream is opened on one of the provided protocols, the handler will be called. `handle` must be called in order to register a handler and support for a given protocol. This also informs other peers of the protocols you support. @@ -581,7 +581,7 @@ export interface Libp2p< * }) * ``` */ - handle: (protocol: string | string[], handler: StreamHandler, options?: StreamHandlerOptions) => Promise + handle(protocol: string | string[], handler: StreamHandler, options?: StreamHandlerOptions): Promise /** * Removes the handler for each protocol. The protocol @@ -593,7 +593,7 @@ export interface Libp2p< * libp2p.unhandle(['/echo/1.0.0']) * ``` */ - unhandle: (protocols: string[] | string) => Promise + unhandle(protocols: string[] | string): Promise /** * Register a topology to be informed when peers are encountered that @@ -612,7 +612,7 @@ export interface Libp2p< * }) * ``` */ - register: (protocol: string, topology: Topology) => Promise + register(protocol: string, topology: Topology): Promise /** * Unregister topology to no longer be informed when peers connect or @@ -626,14 +626,14 @@ export interface Libp2p< * libp2p.unregister(id) * ``` */ - unregister: (id: string) => void + unregister(id: string): void /** * Returns the public key for the passed PeerId. If the PeerId is of the 'RSA' type * this may mean searching the DHT if the key is not present in the KeyStore. * A set of user defined services */ - getPublicKey: (peer: PeerId, options?: AbortOptions) => Promise + getPublicKey(peer: PeerId, options?: AbortOptions): Promise /** * A set of user defined services diff --git a/packages/interface/src/keychain/index.ts b/packages/interface/src/keychain/index.ts index 7632041dab..0812ff1cd8 100644 --- a/packages/interface/src/keychain/index.ts +++ b/packages/interface/src/keychain/index.ts @@ -47,7 +47,7 @@ export interface KeyChain { * const pemKey = await libp2p.keychain.exportKey('keyTest', 'password123') * ``` */ - exportKey: (name: string, password: string) => Promise> + exportKey(name: string, password: string): Promise> /** * Import a new key from a PEM encoded PKCS #8 string. @@ -60,7 +60,7 @@ export interface KeyChain { * const keyInfo = await libp2p.keychain.importKey('keyTestImport', pemKey, 'password123') * ``` */ - importKey: (name: string, pem: string, password: string) => Promise + importKey(name: string, pem: string, password: string): Promise /** * Import a new key from a PeerId with a private key component @@ -71,7 +71,7 @@ export interface KeyChain { * const keyInfo = await libp2p.keychain.importPeer('keyTestImport', peerIdFromString('12D3Foo...')) * ``` */ - importPeer: (name: string, peerId: PeerId) => Promise + importPeer(name: string, peerId: PeerId): Promise /** * Export an existing key as a PeerId @@ -82,7 +82,7 @@ export interface KeyChain { * const peerId = await libp2p.keychain.exportPeerId('key-name') * ``` */ - exportPeerId: (name: string) => Promise + exportPeerId(name: string): Promise /** * Create a key in the keychain. @@ -93,7 +93,7 @@ export interface KeyChain { * const keyInfo = await libp2p.keychain.createKey('keyTest', 'RSA', 4096) * ``` */ - createKey: (name: string, type: KeyType, size?: number) => Promise + createKey(name: string, type: KeyType, size?: number): Promise /** * List all the keys. @@ -104,7 +104,7 @@ export interface KeyChain { * const keyInfos = await libp2p.keychain.listKeys() * ``` */ - listKeys: () => Promise + listKeys(): Promise /** * Removes a key from the keychain. @@ -116,7 +116,7 @@ export interface KeyChain { * const keyInfo = await libp2p.keychain.removeKey('keyTest') * ``` */ - removeKey: (name: string) => Promise + removeKey(name: string): Promise /** * Rename a key in the keychain. @@ -128,7 +128,7 @@ export interface KeyChain { * const keyInfo = await libp2p.keychain.renameKey('keyTest', 'keyNewNtest') * ``` */ - renameKey: (oldName: string, newName: string) => Promise + renameKey(oldName: string, newName: string): Promise /** * Find a key by it's id. @@ -140,7 +140,7 @@ export interface KeyChain { * const keyInfo2 = await libp2p.keychain.findKeyById(keyInfo.id) * ``` */ - findKeyById: (id: string) => Promise + findKeyById(id: string): Promise /** * Find a key by it's name. @@ -152,7 +152,7 @@ export interface KeyChain { * const keyInfo2 = await libp2p.keychain.findKeyByName('keyTest') * ``` */ - findKeyByName: (name: string) => Promise + findKeyByName(name: string): Promise /** * Rotate keychain password and re-encrypt all associated keys @@ -163,5 +163,5 @@ export interface KeyChain { * await libp2p.keychain.rotateKeychainPass('oldPassword', 'newPassword') * ``` */ - rotateKeychainPass: (oldPass: string, newPass: string) => Promise + rotateKeychainPass(oldPass: string, newPass: string): Promise } diff --git a/packages/interface/src/keys/index.ts b/packages/interface/src/keys/index.ts index 62fac882c3..49c3782234 100644 --- a/packages/interface/src/keys/index.ts +++ b/packages/interface/src/keys/index.ts @@ -1,9 +1,9 @@ export interface PublicKey { readonly bytes: Uint8Array - verify: (data: Uint8Array, sig: Uint8Array) => Promise - marshal: () => Uint8Array - equals: (key: PublicKey) => boolean - hash: () => Promise + verify(data: Uint8Array, sig: Uint8Array): Promise + marshal(): Uint8Array + equals(key: PublicKey): boolean + hash(): Promise } /** @@ -12,10 +12,10 @@ export interface PublicKey { export interface PrivateKey { readonly public: PublicKey readonly bytes: Uint8Array - sign: (data: Uint8Array) => Promise - marshal: () => Uint8Array - equals: (key: PrivateKey) => boolean - hash: () => Promise + sign(data: Uint8Array): Promise + marshal(): Uint8Array + equals(key: PrivateKey): boolean + hash(): Promise /** * Gets the ID of the key. * @@ -23,11 +23,11 @@ export interface PrivateKey { * The public key is a protobuf encoding containing a type and the DER encoding * of the PKCS SubjectPublicKeyInfo. */ - id: () => Promise + id(): Promise /** * Exports the password protected key in the format specified. */ - export: (password: string, format?: 'pkcs-8' | string) => Promise + export(password: string, format?: 'pkcs-8' | string): Promise } export const Ed25519 = 'Ed25519' diff --git a/packages/interface/src/metrics/index.ts b/packages/interface/src/metrics/index.ts index 2d08189d30..930eaecc9e 100644 --- a/packages/interface/src/metrics/index.ts +++ b/packages/interface/src/metrics/index.ts @@ -48,28 +48,28 @@ export interface Metric { /** * Update the stored metric to the passed value */ - update: (value: number) => void + update(value: number): void /** * Increment the metric by the passed value or 1 */ - increment: (value?: number) => void + increment(value?: number): void /** * Decrement the metric by the passed value or 1 */ - decrement: (value?: number) => void + decrement(value?: number): void /** * Reset this metric to its default value */ - reset: () => void + reset(): void /** * Start a timed metric, call the returned function to * stop the timer */ - timer: () => StopTimer + timer(): StopTimer } /** @@ -80,31 +80,31 @@ export interface MetricGroup { /** * Update the stored metric group to the passed value */ - update: (values: Record) => void + update(values: Record): void /** * Increment the metric group keys by the passed number or * any non-numeric value to increment by 1 */ - increment: (values: Record) => void + increment(values: Record): void /** * Decrement the metric group keys by the passed number or * any non-numeric value to decrement by 1 */ - decrement: (values: Record) => void + decrement(values: Record): void /** * Reset the passed key in this metric group to its default value * or all keys if no key is passed */ - reset: () => void + reset(): void /** * Start a timed metric for the named key in the group, call * the returned function to stop the timer */ - timer: (key: string) => StopTimer + timer(key: string): StopTimer } /** @@ -115,12 +115,12 @@ export interface Counter { /** * Increment the metric by the passed value or 1 */ - increment: (value?: number) => void + increment(value?: number): void /** * Reset this metric to its default value */ - reset: () => void + reset(): void } /** @@ -133,13 +133,13 @@ export interface CounterGroup { * Increment the metric group keys by the passed number or * any non-numeric value to increment by 1 */ - increment: (values: Record) => void + increment(values: Record): void /** * Reset the passed key in this metric group to its default value * or all keys if no key is passed */ - reset: () => void + reset(): void } /** @@ -151,12 +151,12 @@ export interface Metrics { /** * Track a newly opened multiaddr connection */ - trackMultiaddrConnection: (maConn: MultiaddrConnection) => void + trackMultiaddrConnection(maConn: MultiaddrConnection): void /** * Track a newly opened protocol stream */ - trackProtocolStream: (stream: Stream, connection: Connection) => void + trackProtocolStream(stream: Stream, connection: Connection): void /** * Register an arbitrary metric. Call this to set help/labels for metrics diff --git a/packages/interface/src/peer-id/index.ts b/packages/interface/src/peer-id/index.ts index 39b629141d..931c34bc87 100644 --- a/packages/interface/src/peer-id/index.ts +++ b/packages/interface/src/peer-id/index.ts @@ -9,10 +9,10 @@ interface BasePeerId { readonly privateKey?: Uint8Array readonly publicKey?: Uint8Array - toString: () => string - toCID: () => CID - toBytes: () => Uint8Array - equals: (other: PeerId | Uint8Array | string) => boolean + toString(): string + toCID(): CID + toBytes(): Uint8Array + equals(other: PeerId | Uint8Array | string): boolean } export interface RSAPeerId extends BasePeerId { diff --git a/packages/interface/src/peer-routing/index.ts b/packages/interface/src/peer-routing/index.ts index c7823b9f9b..abe1710f09 100644 --- a/packages/interface/src/peer-routing/index.ts +++ b/packages/interface/src/peer-routing/index.ts @@ -38,7 +38,7 @@ export interface PeerRouting< * const peer = await peerRouting.findPeer(peerId, options) * ``` */ - findPeer: (peerId: PeerId, options?: AbortOptions & ProgressOptions) => Promise + findPeer(peerId: PeerId, options?: AbortOptions & ProgressOptions): Promise /** * Search the network for peers that are closer to the passed key. Peer @@ -53,5 +53,5 @@ export interface PeerRouting< * } * ``` */ - getClosestPeers: (key: Uint8Array, options?: AbortOptions & ProgressOptions) => AsyncIterable + getClosestPeers(key: Uint8Array, options?: AbortOptions & ProgressOptions): AsyncIterable } diff --git a/packages/interface/src/peer-store/index.ts b/packages/interface/src/peer-store/index.ts index 93ad16e103..ba0a683c57 100644 --- a/packages/interface/src/peer-store/index.ts +++ b/packages/interface/src/peer-store/index.ts @@ -156,7 +156,7 @@ export interface PeerStore { * }) * ``` */ - forEach: (fn: (peer: Peer) => void, query?: PeerQuery) => Promise + forEach(fn: (peer: Peer) => void, query?: PeerQuery): Promise /** * Returns all peers in the peer store. @@ -169,7 +169,7 @@ export interface PeerStore { * } * ``` */ - all: (query?: PeerQuery) => Promise + all(query?: PeerQuery): Promise /** * Delete all data stored for the passed peer @@ -187,7 +187,7 @@ export interface PeerStore { * // [] * ``` */ - delete: (peerId: PeerId) => Promise + delete(peerId: PeerId): Promise /** * Returns true if the passed PeerId is in the peer store @@ -202,7 +202,7 @@ export interface PeerStore { * // true * ``` */ - has: (peerId: PeerId) => Promise + has(peerId: PeerId): Promise /** * Returns all data stored for the passed PeerId @@ -214,7 +214,7 @@ export interface PeerStore { * // { .. } * ``` */ - get: (peerId: PeerId) => Promise + get(peerId: PeerId): Promise /** * Adds a peer to the peer store, overwriting any existing data @@ -227,7 +227,7 @@ export interface PeerStore { * }) * ``` */ - save: (id: PeerId, data: PeerData) => Promise + save(id: PeerId, data: PeerData): Promise /** * Adds a peer to the peer store, overwriting only the passed fields @@ -240,7 +240,7 @@ export interface PeerStore { * }) * ``` */ - patch: (id: PeerId, data: PeerData) => Promise + patch(id: PeerId, data: PeerData): Promise /** * Adds a peer to the peer store, deeply merging any existing data. @@ -253,7 +253,7 @@ export interface PeerStore { * }) * ``` */ - merge: (id: PeerId, data: PeerData) => Promise + merge(id: PeerId, data: PeerData): Promise /** * Unmarshal and verify a signed peer record, extract the multiaddrs and @@ -268,5 +268,5 @@ export interface PeerStore { * await peerStore.consumePeerRecord(buf, expectedPeer) * ``` */ - consumePeerRecord: (buf: Uint8Array, expectedPeer?: PeerId) => Promise + consumePeerRecord(buf: Uint8Array, expectedPeer?: PeerId): Promise } diff --git a/packages/interface/src/pubsub/index.ts b/packages/interface/src/pubsub/index.ts index f17641419d..6fd94382a1 100644 --- a/packages/interface/src/pubsub/index.ts +++ b/packages/interface/src/pubsub/index.ts @@ -72,10 +72,10 @@ export interface PeerStreams extends EventEmitter { inboundStream?: AsyncIterable isWritable: boolean - close: () => void - write: (buf: Uint8Array | Uint8ArrayList) => void - attachInboundStream: (stream: Stream) => AsyncIterable - attachOutboundStream: (stream: Stream) => Promise> + close(): void + write(buf: Uint8Array | Uint8ArrayList): void + attachInboundStream(stream: Stream): AsyncIterable + attachOutboundStream(stream: Stream): Promise> } export interface PubSubInit { @@ -189,7 +189,7 @@ export interface PubSub = PubSubEvents> exten */ topicValidators: Map - getPeers: () => PeerId[] + getPeers(): PeerId[] /** * Gets a list of topics the node is subscribed to. @@ -198,7 +198,7 @@ export interface PubSub = PubSubEvents> exten * const topics = libp2p.pubsub.getTopics() * ``` */ - getTopics: () => string[] + getTopics(): string[] /** * Subscribes to a pubsub topic. @@ -217,7 +217,7 @@ export interface PubSub = PubSubEvents> exten * libp2p.pubsub.subscribe(topic) * ``` */ - subscribe: (topic: string) => void + subscribe(topic: string): void /** * Unsubscribes from a pubsub topic. @@ -234,7 +234,7 @@ export interface PubSub = PubSubEvents> exten * libp2p.pubsub.unsubscribe(topic) * ``` */ - unsubscribe: (topic: string) => void + unsubscribe(topic: string): void /** * Gets a list of the PeerIds that are subscribed to one topic. @@ -245,7 +245,7 @@ export interface PubSub = PubSubEvents> exten * const peerIds = libp2p.pubsub.getSubscribers(topic) * ``` */ - getSubscribers: (topic: string) => PeerId[] + getSubscribers(topic: string): PeerId[] /** * Publishes messages to the given topic. @@ -259,7 +259,7 @@ export interface PubSub = PubSubEvents> exten * await libp2p.pubsub.publish(topic, data) * ``` */ - publish: (topic: string, data: Uint8Array) => Promise + publish(topic: string, data: Uint8Array): Promise } export interface PeerStreamEvents { diff --git a/packages/interface/src/record/index.ts b/packages/interface/src/record/index.ts index cb68e14206..5698d546b5 100644 --- a/packages/interface/src/record/index.ts +++ b/packages/interface/src/record/index.ts @@ -16,11 +16,11 @@ export interface Record { /** * Marshal a record to be used in an envelope. */ - marshal: () => Uint8Array + marshal(): Uint8Array /** * Verifies if the other provided Record is identical to this one. */ - equals: (other: Record) => boolean + equals(other: Record): boolean } export interface Envelope { @@ -29,7 +29,7 @@ export interface Envelope { payload: Uint8Array signature: Uint8Array | Uint8ArrayList - marshal: () => Uint8Array - validate: (domain: string) => Promise - equals: (other: Envelope) => boolean + marshal(): Uint8Array + validate(domain: string): Promise + equals(other: Envelope): boolean } diff --git a/packages/interface/src/startable.ts b/packages/interface/src/startable.ts index fed543ee59..8393abc489 100644 --- a/packages/interface/src/startable.ts +++ b/packages/interface/src/startable.ts @@ -2,49 +2,49 @@ * Implemented by components that have a lifecycle */ export interface Startable { - isStarted: () => boolean + isStarted(): boolean /** * If implemented, this method will be invoked before the start method. * * It should not assume any other components have been started. */ - beforeStart?: () => void | Promise + beforeStart?(): void | Promise /** * This method will be invoked to start the component. * * It should not assume that any other components have been started. */ - start: () => void | Promise + start(): void | Promise /** * If implemented, this method will be invoked after the start method. * * All other components will have had their start method invoked before this method is called. */ - afterStart?: () => void | Promise + afterStart?(): void | Promise /** * If implemented, this method will be invoked before the stop method. * * Any other components will still be running when this method is called. */ - beforeStop?: () => void | Promise + beforeStop?(): void | Promise /** * This method will be invoked to stop the component. * * It should not assume any other components are running when it is called. */ - stop: () => void | Promise + stop(): void | Promise /** * If implemented, this method will be invoked after the stop method. * * All other components will have had their stop method invoked before this method is called. */ - afterStop?: () => void | Promise + afterStop?(): void | Promise } export function isStartable (obj: any): obj is Startable { diff --git a/packages/interface/src/stream-muxer/index.ts b/packages/interface/src/stream-muxer/index.ts index fba7b9da34..1e656d2f8e 100644 --- a/packages/interface/src/stream-muxer/index.ts +++ b/packages/interface/src/stream-muxer/index.ts @@ -12,7 +12,7 @@ export interface StreamMuxerFactory { /** * Creates a new stream muxer to be used with a new connection */ - createStreamMuxer: (init?: StreamMuxerInit) => StreamMuxer + createStreamMuxer(init?: StreamMuxerInit): StreamMuxer } /** @@ -32,29 +32,29 @@ export interface StreamMuxer extends Duplex, Source Stream | Promise + newStream(name?: string): Stream | Promise /** * Close or abort all tracked streams and stop the muxer */ - close: (options?: AbortOptions) => Promise + close(options?: AbortOptions): Promise /** * Close or abort all tracked streams and stop the muxer */ - abort: (err: Error) => void + abort(err: Error): void } export interface StreamMuxerInit { /** * A callback function invoked every time an incoming stream is opened */ - onIncomingStream?: (stream: Stream) => void + onIncomingStream?(stream: Stream): void /** * A callback function invoke every time a stream ends */ - onStreamEnd?: (stream: Stream) => void + onStreamEnd?(stream: Stream): void /** * Outbound stream muxers are opened by the local node, inbound stream muxers are opened by the remote diff --git a/packages/interface/src/stream-muxer/stream.ts b/packages/interface/src/stream-muxer/stream.ts index bac8c7f729..1580dc529c 100644 --- a/packages/interface/src/stream-muxer/stream.ts +++ b/packages/interface/src/stream-muxer/stream.ts @@ -1,21 +1,24 @@ import { abortableSource } from 'abortable-iterator' import { type Pushable, pushable } from 'it-pushable' import defer, { type DeferredPromise } from 'p-defer' +import { raceSignal } from 'race-signal' import { Uint8ArrayList } from 'uint8arraylist' import { CodeError } from '../errors.js' import type { Direction, ReadStatus, Stream, StreamStatus, StreamTimeline, WriteStatus } from '../connection/index.js' import type { AbortOptions } from '../index.js' import type { Source } from 'it-stream-types' +// copied from @libp2p/logger to break a circular dependency interface Logger { (formatter: any, ...args: any[]): void - error: (formatter: any, ...args: any[]) => void - trace: (formatter: any, ...args: any[]) => void + error(formatter: any, ...args: any[]): void + trace(formatter: any, ...args: any[]): void enabled: boolean } const ERR_STREAM_RESET = 'ERR_STREAM_RESET' const ERR_SINK_INVALID_STATE = 'ERR_SINK_INVALID_STATE' +const DEFAULT_SEND_CLOSE_WRITE_TIMEOUT = 5000 export interface AbstractStreamInit { /** @@ -41,33 +44,39 @@ export interface AbstractStreamInit { /** * Invoked when the stream ends */ - onEnd?: (err?: Error | undefined) => void + onEnd?(err?: Error | undefined): void /** * Invoked when the readable end of the stream is closed */ - onCloseRead?: () => void + onCloseRead?(): void /** * Invoked when the writable end of the stream is closed */ - onCloseWrite?: () => void + onCloseWrite?(): void /** * Invoked when the the stream has been reset by the remote */ - onReset?: () => void + onReset?(): void /** * Invoked when the the stream has errored */ - onAbort?: (err: Error) => void + onAbort?(err: Error): void /** * How long to wait in ms for stream data to be written to the underlying * connection when closing the writable end of the stream. (default: 500) */ closeTimeout?: number + + /** + * After the stream sink has closed, a limit on how long it takes to send + * a close-write message to the remote peer. + */ + sendCloseWriteTimeout?: number } function isPromise (res?: any): res is Promise { @@ -94,6 +103,7 @@ export abstract class AbstractStream implements Stream { private readonly onCloseWrite?: () => void private readonly onReset?: () => void private readonly onAbort?: (err: Error) => void + private readonly sendCloseWriteTimeout: number protected readonly log: Logger @@ -113,6 +123,7 @@ export abstract class AbstractStream implements Stream { this.timeline = { open: Date.now() } + this.sendCloseWriteTimeout = init.sendCloseWriteTimeout ?? DEFAULT_SEND_CLOSE_WRITE_TIMEOUT this.onEnd = init.onEnd this.onCloseRead = init?.onCloseRead @@ -128,7 +139,6 @@ export abstract class AbstractStream implements Stream { this.log.trace('source ended') } - this.readStatus = 'closed' this.onSourceEnd(err) } }) @@ -173,11 +183,19 @@ export abstract class AbstractStream implements Stream { } } - this.log.trace('sink finished reading from source') - this.writeStatus = 'done' + this.log.trace('sink finished reading from source, write status is "%s"', this.writeStatus) + + if (this.writeStatus === 'writing') { + this.writeStatus = 'closing' + + this.log.trace('send close write to remote') + await this.sendCloseWrite({ + signal: AbortSignal.timeout(this.sendCloseWriteTimeout) + }) + + this.writeStatus = 'closed' + } - this.log.trace('sink calling closeWrite') - await this.closeWrite(options) this.onSinkEnd() } catch (err: any) { this.log.trace('sink ended with error, calling abort with error', err) @@ -196,6 +214,7 @@ export abstract class AbstractStream implements Stream { } this.timeline.closeRead = Date.now() + this.readStatus = 'closed' if (err != null && this.endErr == null) { this.endErr = err @@ -207,6 +226,10 @@ export abstract class AbstractStream implements Stream { this.log.trace('source and sink ended') this.timeline.close = Date.now() + if (this.status !== 'aborted' && this.status !== 'reset') { + this.status = 'closed' + } + if (this.onEnd != null) { this.onEnd(this.endErr) } @@ -221,6 +244,7 @@ export abstract class AbstractStream implements Stream { } this.timeline.closeWrite = Date.now() + this.writeStatus = 'closed' if (err != null && this.endErr == null) { this.endErr = err @@ -232,6 +256,10 @@ export abstract class AbstractStream implements Stream { this.log.trace('sink and source ended') this.timeline.close = Date.now() + if (this.status !== 'aborted' && this.status !== 'reset') { + this.status = 'closed' + } + if (this.onEnd != null) { this.onEnd(this.endErr) } @@ -266,16 +294,16 @@ export abstract class AbstractStream implements Stream { const readStatus = this.readStatus this.readStatus = 'closing' - if (readStatus === 'ready') { - this.log.trace('ending internal source queue') - this.streamSource.end() - } - if (this.status !== 'reset' && this.status !== 'aborted' && this.timeline.closeRead == null) { this.log.trace('send close read to remote') await this.sendCloseRead(options) } + if (readStatus === 'ready') { + this.log.trace('ending internal source queue') + this.streamSource.end() + } + this.log.trace('closed readable end of stream') } @@ -286,16 +314,13 @@ export abstract class AbstractStream implements Stream { this.log.trace('closing writable end of stream with starting write status "%s"', this.writeStatus) - const writeStatus = this.writeStatus - if (this.writeStatus === 'ready') { this.log.trace('sink was never sunk, sink an empty array') - await this.sink([]) - } - this.writeStatus = 'closing' + await raceSignal(this.sink([]), options.signal) + } - if (writeStatus === 'writing') { + if (this.writeStatus === 'writing') { // stop reading from the source passed to `.sink` in the microtask queue // - this lets any data queued by the user in the current tick get read // before we exit @@ -303,16 +328,12 @@ export abstract class AbstractStream implements Stream { queueMicrotask(() => { this.log.trace('aborting source passed to .sink') this.sinkController.abort() - this.sinkEnd.promise.then(resolve, reject) + raceSignal(this.sinkEnd.promise, options.signal) + .then(resolve, reject) }) }) } - if (this.status !== 'reset' && this.status !== 'aborted' && this.timeline.closeWrite == null) { - this.log.trace('send close write to remote') - await this.sendCloseWrite(options) - } - this.writeStatus = 'closed' this.log.trace('closed writable end of stream') @@ -357,6 +378,7 @@ export abstract class AbstractStream implements Stream { const err = new CodeError('stream reset', ERR_STREAM_RESET) this.status = 'reset' + this.timeline.reset = Date.now() this._closeSinkAndSource(err) this.onReset?.() } @@ -423,7 +445,7 @@ export abstract class AbstractStream implements Stream { return } - this.log.trace('muxer destroyed') + this.log.trace('stream destroyed') this._closeSinkAndSource() } diff --git a/packages/interface/src/topology/index.ts b/packages/interface/src/topology/index.ts index 697c598494..0772df3b38 100644 --- a/packages/interface/src/topology/index.ts +++ b/packages/interface/src/topology/index.ts @@ -5,6 +5,6 @@ export interface Topology { min?: number max?: number - onConnect?: (peerId: PeerId, conn: Connection) => void - onDisconnect?: (peerId: PeerId) => void + onConnect?(peerId: PeerId, conn: Connection): void + onDisconnect?(peerId: PeerId): void } diff --git a/packages/interface/src/transport/index.ts b/packages/interface/src/transport/index.ts index 203b1bcf5a..5acbfcc50e 100644 --- a/packages/interface/src/transport/index.ts +++ b/packages/interface/src/transport/index.ts @@ -15,17 +15,17 @@ export interface Listener extends EventEmitter { /** * Start a listener */ - listen: (multiaddr: Multiaddr) => Promise + listen(multiaddr: Multiaddr): Promise /** * Get listen addresses */ - getAddrs: () => Multiaddr[] + getAddrs(): Multiaddr[] /** * Close listener * * @returns {Promise} */ - close: () => Promise + close(): Promise } export const symbol = Symbol.for('@libp2p/transport') @@ -60,12 +60,12 @@ export interface Transport { /** * Dial a given multiaddr. */ - dial: (ma: Multiaddr, options: DialOptions) => Promise + dial(ma: Multiaddr, options: DialOptions): Promise /** * Create transport listeners. */ - createListener: (options: CreateListenerOptions) => Listener + createListener(options: CreateListenerOptions): Listener /** * Takes a list of `Multiaddr`s and returns only valid addresses for the transport @@ -108,10 +108,10 @@ export interface Upgrader { /** * Upgrades an outbound connection on `transport.dial`. */ - upgradeOutbound: (maConn: MultiaddrConnection, opts?: UpgraderOptions) => Promise + upgradeOutbound(maConn: MultiaddrConnection, opts?: UpgraderOptions): Promise /** * Upgrades an inbound connection on transport listener. */ - upgradeInbound: (maConn: MultiaddrConnection, opts?: UpgraderOptions) => Promise + upgradeInbound(maConn: MultiaddrConnection, opts?: UpgraderOptions): Promise } diff --git a/packages/interface/test/fixtures/logger.ts b/packages/interface/test/fixtures/logger.ts new file mode 100644 index 0000000000..4248f5c530 --- /dev/null +++ b/packages/interface/test/fixtures/logger.ts @@ -0,0 +1,16 @@ +// copied from @libp2p/logger to break a circular dependency +interface Logger { + (): void + error(): void + trace(): void + enabled: boolean +} + +export function logger (): Logger { + const output = (): void => {} + output.trace = (): void => {} + output.error = (): void => {} + output.enabled = false + + return output +} diff --git a/packages/interface/test/stream-muxer/stream.spec.ts b/packages/interface/test/stream-muxer/stream.spec.ts new file mode 100644 index 0000000000..eaeb33f04a --- /dev/null +++ b/packages/interface/test/stream-muxer/stream.spec.ts @@ -0,0 +1,196 @@ +import { expect } from 'aegir/chai' +import delay from 'delay' +import all from 'it-all' +import drain from 'it-drain' +import Sinon from 'sinon' +import { Uint8ArrayList } from 'uint8arraylist' +import { AbstractStream } from '../../src/stream-muxer/stream.js' +import { logger } from '../fixtures/logger.js' +import type { AbortOptions } from '../../src/index.js' + +class TestStream extends AbstractStream { + async sendNewStream (options?: AbortOptions): Promise { + + } + + async sendData (buf: Uint8ArrayList, options?: AbortOptions): Promise { + + } + + async sendReset (options?: AbortOptions): Promise { + + } + + async sendCloseWrite (options?: AbortOptions): Promise { + + } + + async sendCloseRead (options?: AbortOptions): Promise { + + } +} + +describe('abstract stream', () => { + let stream: TestStream + + beforeEach(() => { + stream = new TestStream({ + id: 'test', + direction: 'outbound', + log: logger() + }) + }) + + it('sends data', async () => { + const sendSpy = Sinon.spy(stream, 'sendData') + const data = [ + Uint8Array.from([0, 1, 2, 3, 4]) + ] + + await stream.sink(data) + + const call = sendSpy.getCall(0) + expect(call.args[0].subarray()).to.equalBytes(data[0]) + }) + + it('receives data', async () => { + const data = new Uint8ArrayList( + Uint8Array.from([0, 1, 2, 3, 4]) + ) + + stream.sourcePush(data) + stream.remoteCloseWrite() + + const output = await all(stream.source) + expect(output[0].subarray()).to.equalBytes(data.subarray()) + }) + + it('closes', async () => { + const sendCloseReadSpy = Sinon.spy(stream, 'sendCloseRead') + const sendCloseWriteSpy = Sinon.spy(stream, 'sendCloseWrite') + + await stream.close() + + expect(sendCloseReadSpy.calledOnce).to.be.true() + expect(sendCloseWriteSpy.calledOnce).to.be.true() + + expect(stream).to.have.property('status', 'closed') + expect(stream).to.have.property('writeStatus', 'closed') + expect(stream).to.have.property('readStatus', 'closed') + expect(stream).to.have.nested.property('timeline.close').that.is.a('number') + expect(stream).to.have.nested.property('timeline.closeRead').that.is.a('number') + expect(stream).to.have.nested.property('timeline.closeWrite').that.is.a('number') + expect(stream).to.not.have.nested.property('timeline.reset') + expect(stream).to.not.have.nested.property('timeline.abort') + }) + + it('closes for reading', async () => { + const sendCloseReadSpy = Sinon.spy(stream, 'sendCloseRead') + const sendCloseWriteSpy = Sinon.spy(stream, 'sendCloseWrite') + + await stream.closeRead() + + expect(sendCloseReadSpy.calledOnce).to.be.true() + expect(sendCloseWriteSpy.called).to.be.false() + + expect(stream).to.have.property('status', 'open') + expect(stream).to.have.property('writeStatus', 'ready') + expect(stream).to.have.property('readStatus', 'closed') + expect(stream).to.not.have.nested.property('timeline.close') + expect(stream).to.have.nested.property('timeline.closeRead').that.is.a('number') + expect(stream).to.not.have.nested.property('timeline.closeWrite') + expect(stream).to.not.have.nested.property('timeline.reset') + expect(stream).to.not.have.nested.property('timeline.abort') + }) + + it('closes for writing', async () => { + const sendCloseReadSpy = Sinon.spy(stream, 'sendCloseRead') + const sendCloseWriteSpy = Sinon.spy(stream, 'sendCloseWrite') + + await stream.closeWrite() + + expect(sendCloseReadSpy.called).to.be.false() + expect(sendCloseWriteSpy.calledOnce).to.be.true() + + expect(stream).to.have.property('status', 'open') + expect(stream).to.have.property('writeStatus', 'closed') + expect(stream).to.have.property('readStatus', 'ready') + expect(stream).to.not.have.nested.property('timeline.close') + expect(stream).to.not.have.nested.property('timeline.closeRead') + expect(stream).to.have.nested.property('timeline.closeWrite').that.is.a('number') + expect(stream).to.not.have.nested.property('timeline.reset') + expect(stream).to.not.have.nested.property('timeline.abort') + }) + + it('aborts', async () => { + const sendResetSpy = Sinon.spy(stream, 'sendReset') + + stream.abort(new Error('Urk!')) + + expect(sendResetSpy.calledOnce).to.be.true() + + expect(stream).to.have.property('status', 'aborted') + expect(stream).to.have.property('writeStatus', 'closed') + expect(stream).to.have.property('readStatus', 'closed') + expect(stream).to.have.nested.property('timeline.close').that.is.a('number') + expect(stream).to.have.nested.property('timeline.closeRead').that.is.a('number') + expect(stream).to.have.nested.property('timeline.closeWrite').that.is.a('number') + expect(stream).to.not.have.nested.property('timeline.reset') + expect(stream).to.have.nested.property('timeline.abort').that.is.a('number') + + await expect(stream.sink([])).to.eventually.be.rejected + .with.property('code', 'ERR_SINK_INVALID_STATE') + await expect(drain(stream.source)).to.eventually.be.rejected + .with('Urk!') + }) + + it('gets reset remotely', async () => { + stream.reset() + + expect(stream).to.have.property('status', 'reset') + expect(stream).to.have.property('writeStatus', 'closed') + expect(stream).to.have.property('readStatus', 'closed') + expect(stream).to.have.nested.property('timeline.close').that.is.a('number') + expect(stream).to.have.nested.property('timeline.closeRead').that.is.a('number') + expect(stream).to.have.nested.property('timeline.closeWrite').that.is.a('number') + expect(stream).to.have.nested.property('timeline.reset').that.is.a('number') + expect(stream).to.not.have.nested.property('timeline.abort') + + await expect(stream.sink([])).to.eventually.be.rejected + .with.property('code', 'ERR_SINK_INVALID_STATE') + await expect(drain(stream.source)).to.eventually.be.rejected + .with.property('code', 'ERR_STREAM_RESET') + }) + + it('does not send close read when remote closes write', async () => { + const sendCloseReadSpy = Sinon.spy(stream, 'sendCloseRead') + + stream.remoteCloseWrite() + + await delay(100) + + expect(sendCloseReadSpy.called).to.be.false() + }) + + it('does not send close write when remote closes read', async () => { + const sendCloseWriteSpy = Sinon.spy(stream, 'sendCloseWrite') + + stream.remoteCloseRead() + + await delay(100) + + expect(sendCloseWriteSpy.called).to.be.false() + }) + + it('does not send close read or write when remote resets', async () => { + const sendCloseReadSpy = Sinon.spy(stream, 'sendCloseRead') + const sendCloseWriteSpy = Sinon.spy(stream, 'sendCloseWrite') + + stream.reset() + + await delay(100) + + expect(sendCloseReadSpy.called).to.be.false() + expect(sendCloseWriteSpy.called).to.be.false() + }) +}) diff --git a/packages/kad-dht/CHANGELOG.md b/packages/kad-dht/CHANGELOG.md index 1ce303073a..9cbeb5a94c 100644 --- a/packages/kad-dht/CHANGELOG.md +++ b/packages/kad-dht/CHANGELOG.md @@ -5,6 +5,66 @@ * skip self-query if not running ([#479](https://github.com/libp2p/js-libp2p-kad-dht/issues/479)) ([7095290](https://github.com/libp2p/js-libp2p-kad-dht/commit/70952907a27fd8778773172059879656b4f08855)) +### [10.0.9](https://www.github.com/libp2p/js-libp2p/compare/kad-dht-v10.0.8...kad-dht-v10.0.9) (2023-10-06) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @libp2p/crypto bumped from ^2.0.4 to ^2.0.5 + * @libp2p/interface bumped from ^0.1.2 to ^0.1.3 + * @libp2p/interface-internal bumped from ^0.1.5 to ^0.1.6 + * @libp2p/logger bumped from ^3.0.2 to ^3.0.3 + * @libp2p/peer-collections bumped from ^4.0.4 to ^4.0.5 + * @libp2p/peer-id bumped from ^3.0.2 to ^3.0.3 + * devDependencies + * @libp2p/interface-compliance-tests bumped from ^4.1.0 to ^4.1.1 + * @libp2p/peer-id-factory bumped from ^3.0.4 to ^3.0.5 + * @libp2p/peer-store bumped from ^9.0.5 to ^9.0.6 + +### [10.0.8](https://www.github.com/libp2p/js-libp2p/compare/kad-dht-v10.0.7...kad-dht-v10.0.8) (2023-10-01) + + +### Dependencies + +* The following workspace dependencies were updated + * devDependencies + * @libp2p/interface-compliance-tests bumped from ^4.0.6 to ^4.1.0 + +### [10.0.7](https://www.github.com/libp2p/js-libp2p/compare/kad-dht-v10.0.6...kad-dht-v10.0.7) (2023-09-15) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @libp2p/crypto bumped from ^2.0.3 to ^2.0.4 + * @libp2p/interface-internal bumped from ^0.1.4 to ^0.1.5 + * @libp2p/peer-collections bumped from ^4.0.3 to ^4.0.4 + * devDependencies + * @libp2p/interface-compliance-tests bumped from ^4.0.5 to ^4.0.6 + * @libp2p/peer-id-factory bumped from ^3.0.3 to ^3.0.4 + * @libp2p/peer-store bumped from ^9.0.4 to ^9.0.5 + +### [10.0.6](https://www.github.com/libp2p/js-libp2p/compare/kad-dht-v10.0.5...kad-dht-v10.0.6) (2023-09-10) + + +### Dependencies + +* The following workspace dependencies were updated + * devDependencies + * @libp2p/peer-store bumped from ^9.0.3 to ^9.0.4 + +### [10.0.5](https://www.github.com/libp2p/js-libp2p/compare/kad-dht-v10.0.4...kad-dht-v10.0.5) (2023-08-25) + + +### Dependencies + +* The following workspace dependencies were updated + * devDependencies + * @libp2p/interface-compliance-tests bumped from ^4.0.4 to ^4.0.5 + ### [10.0.4](https://www.github.com/libp2p/js-libp2p/compare/kad-dht-v10.0.3...kad-dht-v10.0.4) (2023-08-16) diff --git a/packages/kad-dht/package.json b/packages/kad-dht/package.json index 813a9a9e46..4a215a7bfc 100644 --- a/packages/kad-dht/package.json +++ b/packages/kad-dht/package.json @@ -1,6 +1,6 @@ { "name": "@libp2p/kad-dht", - "version": "10.0.4", + "version": "10.0.9", "description": "JavaScript implementation of the Kad-DHT for libp2p", "license": "Apache-2.0 OR MIT", "homepage": "https://github.com/libp2p/js-libp2p/tree/master/packages/kad-dht#readme", @@ -31,6 +31,7 @@ "eslintConfig": { "extends": "ipfs", "parserOptions": { + "project": true, "sourceType": "module" }, "ignorePatterns": [ @@ -51,12 +52,12 @@ "dep-check": "aegir dep-check -i events" }, "dependencies": { - "@libp2p/crypto": "^2.0.3", - "@libp2p/interface": "^0.1.2", - "@libp2p/interface-internal": "^0.1.4", - "@libp2p/logger": "^3.0.2", - "@libp2p/peer-collections": "^4.0.3", - "@libp2p/peer-id": "^3.0.2", + "@libp2p/crypto": "^2.0.5", + "@libp2p/interface": "^0.1.3", + "@libp2p/interface-internal": "^0.1.6", + "@libp2p/logger": "^3.0.3", + "@libp2p/peer-collections": "^4.0.5", + "@libp2p/peer-id": "^3.0.3", "@multiformats/multiaddr": "^12.1.5", "@types/sinon": "^10.0.15", "abortable-iterator": "^5.0.1", @@ -88,26 +89,26 @@ "uint8arrays": "^4.0.6" }, "devDependencies": { - "@libp2p/interface-compliance-tests": "^4.0.4", - "@libp2p/peer-id-factory": "^3.0.3", - "@libp2p/peer-store": "^9.0.3", + "@libp2p/interface-compliance-tests": "^4.1.1", + "@libp2p/peer-id-factory": "^3.0.5", + "@libp2p/peer-store": "^9.0.6", "@types/lodash.random": "^3.2.6", "@types/lodash.range": "^3.2.6", "@types/which": "^3.0.0", - "aegir": "^40.0.8", + "aegir": "^41.0.2", "datastore-level": "^10.0.0", "delay": "^6.0.0", - "execa": "^7.1.1", + "execa": "^8.0.1", "it-filter": "^3.0.1", "it-last": "^3.0.1", "lodash.random": "^3.2.0", "lodash.range": "^3.2.0", - "p-retry": "^5.1.2", + "p-retry": "^6.0.0", "p-wait-for": "^5.0.2", "protons": "^7.0.2", - "sinon": "^15.1.2", + "sinon": "^16.0.0", "ts-sinon": "^2.0.2", - "which": "^3.0.0" + "which": "^4.0.0" }, "browser": { "./dist/src/routing-table/generated-prefix-list.js": "./dist/src/routing-table/generated-prefix-list-browser.js" diff --git a/packages/kad-dht/src/index.ts b/packages/kad-dht/src/index.ts index 2c2230ac90..742f7cf456 100644 --- a/packages/kad-dht/src/index.ts +++ b/packages/kad-dht/src/index.ts @@ -155,47 +155,47 @@ export interface KadDHT { /** * Get a value from the DHT, the final ValueEvent will be the best value */ - get: (key: Uint8Array, options?: QueryOptions) => AsyncIterable + get(key: Uint8Array, options?: QueryOptions): AsyncIterable /** * Find providers of a given CID */ - findProviders: (key: CID, options?: QueryOptions) => AsyncIterable + findProviders(key: CID, options?: QueryOptions): AsyncIterable /** * Find a peer on the DHT */ - findPeer: (id: PeerId, options?: QueryOptions) => AsyncIterable + findPeer(id: PeerId, options?: QueryOptions): AsyncIterable /** * Find the closest peers to the passed key */ - getClosestPeers: (key: Uint8Array, options?: QueryOptions) => AsyncIterable + getClosestPeers(key: Uint8Array, options?: QueryOptions): AsyncIterable /** * Store provider records for the passed CID on the DHT pointing to us */ - provide: (key: CID, options?: QueryOptions) => AsyncIterable + provide(key: CID, options?: QueryOptions): AsyncIterable /** * Store the passed value under the passed key on the DHT */ - put: (key: Uint8Array, value: Uint8Array, options?: QueryOptions) => AsyncIterable + put(key: Uint8Array, value: Uint8Array, options?: QueryOptions): AsyncIterable /** * Returns the mode this node is in */ - getMode: () => Promise<'client' | 'server'> + getMode(): Promise<'client' | 'server'> /** * If 'server' this node will respond to DHT queries, if 'client' this node will not */ - setMode: (mode: 'client' | 'server') => Promise + setMode(mode: 'client' | 'server'): Promise /** * Force a routing table refresh */ - refreshRoutingTable: () => Promise + refreshRoutingTable(): Promise } export interface SingleKadDHT extends KadDHT { diff --git a/packages/kad-dht/src/routing-table/k-bucket.ts b/packages/kad-dht/src/routing-table/k-bucket.ts index fb8bb12c79..b8e510d0a2 100644 --- a/packages/kad-dht/src/routing-table/k-bucket.ts +++ b/packages/kad-dht/src/routing-table/k-bucket.ts @@ -95,14 +95,14 @@ export interface KBucketOptions { * An optional `distance` function that gets two `id` Uint8Arrays and return * distance (as number) between them. */ - distance?: (a: Uint8Array, b: Uint8Array) => number + distance?(a: Uint8Array, b: Uint8Array): number /** * An optional `arbiter` function that given two `contact` objects with the * same `id` returns the desired object to be used for updating the k-bucket. * For more details, see [arbiter function](#arbiter-function). */ - arbiter?: (incumbent: Contact, candidate: Contact) => Contact + arbiter?(incumbent: Contact, candidate: Contact): Contact } export interface Contact { diff --git a/packages/kad-dht/src/rpc/index.ts b/packages/kad-dht/src/rpc/index.ts index b063687e2d..e003d7ba0b 100644 --- a/packages/kad-dht/src/rpc/index.ts +++ b/packages/kad-dht/src/rpc/index.ts @@ -16,7 +16,7 @@ import type { PeerId } from '@libp2p/interface/peer-id' import type { IncomingStreamData } from '@libp2p/interface-internal/registrar' export interface DHTMessageHandler { - handle: (peerId: PeerId, msg: Message) => Promise + handle(peerId: PeerId, msg: Message): Promise } export interface RPCInit { diff --git a/packages/keychain/CHANGELOG.md b/packages/keychain/CHANGELOG.md index d31fedaa5c..f1c352f145 100644 --- a/packages/keychain/CHANGELOG.md +++ b/packages/keychain/CHANGELOG.md @@ -11,6 +11,31 @@ * **dev:** bump aegir from 38.1.8 to 39.0.10 ([#70](https://github.com/libp2p/js-libp2p-keychain/issues/70)) ([4da4a08](https://github.com/libp2p/js-libp2p-keychain/commit/4da4a08b86f436c36e2fae48ecc48817e9b8066f)) +### [3.0.5](https://www.github.com/libp2p/js-libp2p/compare/keychain-v3.0.4...keychain-v3.0.5) (2023-10-06) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @libp2p/crypto bumped from ^2.0.4 to ^2.0.5 + * @libp2p/interface bumped from ^0.1.2 to ^0.1.3 + * @libp2p/logger bumped from ^3.0.2 to ^3.0.3 + * @libp2p/peer-id bumped from ^3.0.2 to ^3.0.3 + * devDependencies + * @libp2p/peer-id-factory bumped from ^3.0.4 to ^3.0.5 + +### [3.0.4](https://www.github.com/libp2p/js-libp2p/compare/keychain-v3.0.3...keychain-v3.0.4) (2023-09-15) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @libp2p/crypto bumped from ^2.0.3 to ^2.0.4 + * devDependencies + * @libp2p/peer-id-factory bumped from ^3.0.3 to ^3.0.4 + ### [3.0.3](https://www.github.com/libp2p/js-libp2p/compare/keychain-v3.0.2...keychain-v3.0.3) (2023-08-14) @@ -274,4 +299,4 @@ Co-Authored-By: Vasco Santos ### Features * move bits from https://github.com/richardschneider/ipfs-encryption ([1a96ae8](https://github.com/libp2p/js-libp2p-keychain/commit/1a96ae8)) -* use libp2p-crypto ([#18](https://github.com/libp2p/js-libp2p-keychain/issues/18)) ([c1627a9](https://github.com/libp2p/js-libp2p-keychain/commit/c1627a9)) \ No newline at end of file +* use libp2p-crypto ([#18](https://github.com/libp2p/js-libp2p-keychain/issues/18)) ([c1627a9](https://github.com/libp2p/js-libp2p-keychain/commit/c1627a9)) diff --git a/packages/keychain/package.json b/packages/keychain/package.json index d458b38a7e..0ac5a359f4 100644 --- a/packages/keychain/package.json +++ b/packages/keychain/package.json @@ -1,6 +1,6 @@ { "name": "@libp2p/keychain", - "version": "3.0.3", + "version": "3.0.5", "description": "Key management and cryptographically protected messages", "license": "Apache-2.0 OR MIT", "homepage": "https://github.com/libp2p/js-libp2p/tree/master/packages/keychain#readme", @@ -36,6 +36,7 @@ "eslintConfig": { "extends": "ipfs", "parserOptions": { + "project": true, "sourceType": "module" } }, @@ -53,18 +54,18 @@ "test:electron-main": "aegir test -t electron-main" }, "dependencies": { - "@libp2p/crypto": "^2.0.3", - "@libp2p/interface": "^0.1.2", - "@libp2p/logger": "^3.0.2", - "@libp2p/peer-id": "^3.0.2", + "@libp2p/crypto": "^2.0.5", + "@libp2p/interface": "^0.1.3", + "@libp2p/logger": "^3.0.3", + "@libp2p/peer-id": "^3.0.3", "interface-datastore": "^8.2.0", "merge-options": "^3.0.4", "sanitize-filename": "^1.6.3", "uint8arrays": "^4.0.6" }, "devDependencies": { - "@libp2p/peer-id-factory": "^3.0.3", - "aegir": "^40.0.8", + "@libp2p/peer-id-factory": "^3.0.5", + "aegir": "^41.0.2", "datastore-core": "^9.1.1", "multiformats": "^12.0.1" } diff --git a/packages/libp2p/CHANGELOG.md b/packages/libp2p/CHANGELOG.md index 72629b3f12..36d4003014 100644 --- a/packages/libp2p/CHANGELOG.md +++ b/packages/libp2p/CHANGELOG.md @@ -1,9 +1,157 @@ -### [0.45.9](https://www.github.com/libp2p/js-libp2p/compare/libp2p-v0.45.8...libp2p-v0.45.9) (2023-06-14) +### [0.46.10](https://www.github.com/libp2p/js-libp2p/compare/libp2p-v0.46.9...libp2p-v0.46.10) (2023-09-10) ### Bug Fixes -* allow specifiying maxOutboundStreams in connection.newStream ([#1817](https://www.github.com/libp2p/js-libp2p/issues/1817)) ([b348fba](https://www.github.com/libp2p/js-libp2p/commit/b348fbaa7e16fd40f9a93e83a92c8152ad9e97e9)) +* **libp2p:** only dial one address at a time for peers ([#2028](https://www.github.com/libp2p/js-libp2p/issues/2028)) ([73b87c5](https://www.github.com/libp2p/js-libp2p/commit/73b87c5a1474f9acd47989b675724ea64d02c7b9)) +* **libp2p:** sort addresses to dial as public, then relay ([#2031](https://www.github.com/libp2p/js-libp2p/issues/2031)) ([5294f14](https://www.github.com/libp2p/js-libp2p/commit/5294f14caa314bb150554afff3a7ff45d2bf17ba)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @libp2p/peer-record bumped from ^6.0.3 to ^6.0.4 + * @libp2p/peer-store bumped from ^9.0.3 to ^9.0.4 + * @libp2p/utils bumped from ^4.0.2 to ^4.0.3 + * devDependencies + * @libp2p/kad-dht bumped from ^10.0.5 to ^10.0.6 + * @libp2p/mdns bumped from ^9.0.6 to ^9.0.7 + * @libp2p/tcp bumped from ^8.0.5 to ^8.0.6 + * @libp2p/websockets bumped from ^7.0.5 to ^7.0.6 + +### [0.46.14](https://www.github.com/libp2p/js-libp2p/compare/libp2p-v0.46.13...libp2p-v0.46.14) (2023-10-10) + + +### Bug Fixes + +* **circuit-relay:** respect applyDefaultLimit when it is false ([#2139](https://www.github.com/libp2p/js-libp2p/issues/2139)) ([df2153e](https://www.github.com/libp2p/js-libp2p/commit/df2153e268a72edd00c7663ce9d196d5547e994d)) + +### [0.46.13](https://www.github.com/libp2p/js-libp2p/compare/libp2p-v0.46.12...libp2p-v0.46.13) (2023-10-06) + + +### Bug Fixes + +* add missing events dep to fix browser bundlers ([#2134](https://www.github.com/libp2p/js-libp2p/issues/2134)) ([f670307](https://www.github.com/libp2p/js-libp2p/commit/f670307a90fe6665f10630823dd7058aab2a1c2f)), closes [#2110](https://www.github.com/libp2p/js-libp2p/issues/2110) +* close webrtc streams without data loss ([#2073](https://www.github.com/libp2p/js-libp2p/issues/2073)) ([7d8b155](https://www.github.com/libp2p/js-libp2p/commit/7d8b15517a480e01a8ebd427ab0093509b78d5b0)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @libp2p/crypto bumped from ^2.0.4 to ^2.0.5 + * @libp2p/interface bumped from ^0.1.2 to ^0.1.3 + * @libp2p/interface-internal bumped from ^0.1.5 to ^0.1.6 + * @libp2p/keychain bumped from ^3.0.4 to ^3.0.5 + * @libp2p/logger bumped from ^3.0.2 to ^3.0.3 + * @libp2p/multistream-select bumped from ^4.0.2 to ^4.0.3 + * @libp2p/peer-collections bumped from ^4.0.4 to ^4.0.5 + * @libp2p/peer-id bumped from ^3.0.2 to ^3.0.3 + * @libp2p/peer-id-factory bumped from ^3.0.4 to ^3.0.5 + * @libp2p/peer-record bumped from ^6.0.5 to ^6.0.6 + * @libp2p/peer-store bumped from ^9.0.5 to ^9.0.6 + * @libp2p/utils bumped from ^4.0.3 to ^4.0.4 + * devDependencies + * @libp2p/bootstrap bumped from ^9.0.7 to ^9.0.8 + * @libp2p/floodsub bumped from ^8.0.8 to ^8.0.9 + * @libp2p/interface-compliance-tests bumped from ^4.1.0 to ^4.1.1 + * @libp2p/kad-dht bumped from ^10.0.8 to ^10.0.9 + * @libp2p/mdns bumped from ^9.0.9 to ^9.0.10 + * @libp2p/mplex bumped from ^9.0.7 to ^9.0.8 + * @libp2p/tcp bumped from ^8.0.8 to ^8.0.9 + * @libp2p/websockets bumped from ^7.0.8 to ^7.0.9 + +### [0.46.12](https://www.github.com/libp2p/js-libp2p/compare/libp2p-v0.46.11...libp2p-v0.46.12) (2023-10-01) + + +### Bug Fixes + +* ensure all listeners are properly closed on tcp shutdown ([#2058](https://www.github.com/libp2p/js-libp2p/issues/2058)) ([b57bca4](https://www.github.com/libp2p/js-libp2p/commit/b57bca4493e1634108fe187466024e374b76c114)) +* include peer id in autodial log message ([#2075](https://www.github.com/libp2p/js-libp2p/issues/2075)) ([368ee26](https://www.github.com/libp2p/js-libp2p/commit/368ee26dbea5de8fb67d9a4596a169f327e73145)) +* **libp2p:** update circuit relay and upgrader logs ([#2071](https://www.github.com/libp2p/js-libp2p/issues/2071)) ([f09ac4a](https://www.github.com/libp2p/js-libp2p/commit/f09ac4a7704070fd92bae8d4482d06eac45ddd2c)) + + +### Dependencies + +* The following workspace dependencies were updated + * devDependencies + * @libp2p/bootstrap bumped from ^9.0.6 to ^9.0.7 + * @libp2p/floodsub bumped from ^8.0.7 to ^8.0.8 + * @libp2p/interface-compliance-tests bumped from ^4.0.6 to ^4.1.0 + * @libp2p/kad-dht bumped from ^10.0.7 to ^10.0.8 + * @libp2p/mdns bumped from ^9.0.8 to ^9.0.9 + * @libp2p/mplex bumped from ^9.0.6 to ^9.0.7 + * @libp2p/tcp bumped from ^8.0.7 to ^8.0.8 + * @libp2p/websockets bumped from ^7.0.7 to ^7.0.8 + +### [0.46.11](https://www.github.com/libp2p/js-libp2p/compare/libp2p-v0.46.10...libp2p-v0.46.11) (2023-09-15) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @libp2p/crypto bumped from ^2.0.3 to ^2.0.4 + * @libp2p/interface-internal bumped from ^0.1.4 to ^0.1.5 + * @libp2p/keychain bumped from ^3.0.3 to ^3.0.4 + * @libp2p/peer-collections bumped from ^4.0.3 to ^4.0.4 + * @libp2p/peer-id-factory bumped from ^3.0.3 to ^3.0.4 + * @libp2p/peer-record bumped from ^6.0.4 to ^6.0.5 + * @libp2p/peer-store bumped from ^9.0.4 to ^9.0.5 + * devDependencies + * @libp2p/bootstrap bumped from ^9.0.5 to ^9.0.6 + * @libp2p/floodsub bumped from ^8.0.6 to ^8.0.7 + * @libp2p/interface-compliance-tests bumped from ^4.0.5 to ^4.0.6 + * @libp2p/kad-dht bumped from ^10.0.6 to ^10.0.7 + * @libp2p/mdns bumped from ^9.0.7 to ^9.0.8 + * @libp2p/mplex bumped from ^9.0.5 to ^9.0.6 + * @libp2p/tcp bumped from ^8.0.6 to ^8.0.7 + * @libp2p/websockets bumped from ^7.0.6 to ^7.0.7 + +### [0.46.9](https://www.github.com/libp2p/js-libp2p/compare/libp2p-v0.46.8...libp2p-v0.46.9) (2023-09-05) + + +### Bug Fixes + +* **libp2p:** emit peer:discovered event on internal event bus ([#2019](https://www.github.com/libp2p/js-libp2p/issues/2019)) ([a6be8f0](https://www.github.com/libp2p/js-libp2p/commit/a6be8f0f4bbd81826c2ca5d48ea6175b1fdf3ab9)) + +### [0.46.8](https://www.github.com/libp2p/js-libp2p/compare/libp2p-v0.46.7...libp2p-v0.46.8) (2023-09-01) + + +### Bug Fixes + +* **libp2p:** update peer store with supported protocols after unhandle ([#2013](https://www.github.com/libp2p/js-libp2p/issues/2013)) ([63041af](https://www.github.com/libp2p/js-libp2p/commit/63041afefbefd246ee1d6d6a4958b1999076dc17)) + + +### Dependencies + +* The following workspace dependencies were updated + * devDependencies + * @libp2p/mdns bumped from ^9.0.5 to ^9.0.6 + +### [0.46.7](https://www.github.com/libp2p/js-libp2p/compare/libp2p-v0.46.6...libp2p-v0.46.7) (2023-08-25) + + +### Dependencies + +* The following workspace dependencies were updated + * devDependencies + * @libp2p/bootstrap bumped from ^9.0.4 to ^9.0.5 + * @libp2p/floodsub bumped from ^8.0.5 to ^8.0.6 + * @libp2p/interface-compliance-tests bumped from ^4.0.4 to ^4.0.5 + * @libp2p/kad-dht bumped from ^10.0.4 to ^10.0.5 + * @libp2p/mdns bumped from ^9.0.4 to ^9.0.5 + * @libp2p/mplex bumped from ^9.0.4 to ^9.0.5 + * @libp2p/tcp bumped from ^8.0.4 to ^8.0.5 + * @libp2p/websockets bumped from ^7.0.4 to ^7.0.5 + +### [0.46.6](https://www.github.com/libp2p/js-libp2p/compare/libp2p-v0.46.5...libp2p-v0.46.6) (2023-08-16) + + +### Bug Fixes + +* **libp2p:** move delay dep to production dependencies ([#1977](https://www.github.com/libp2p/js-libp2p/issues/1977)) ([725f5df](https://www.github.com/libp2p/js-libp2p/commit/725f5df1782a200cf1d12e6d03a164d028a7cc3e)) ### [0.46.5](https://www.github.com/libp2p/js-libp2p/compare/libp2p-v0.46.4...libp2p-v0.46.5) (2023-08-16) @@ -182,6 +330,13 @@ * @libp2p/tcp bumped from ^7.0.0 to ^8.0.0 * @libp2p/websockets bumped from ^6.0.0 to ^7.0.0 +### [0.45.9](https://www.github.com/libp2p/js-libp2p/compare/libp2p-v0.45.8...libp2p-v0.45.9) (2023-06-14) + + +### Bug Fixes + +* allow specifiying maxOutboundStreams in connection.newStream ([#1817](https://www.github.com/libp2p/js-libp2p/issues/1817)) ([b348fba](https://www.github.com/libp2p/js-libp2p/commit/b348fbaa7e16fd40f9a93e83a92c8152ad9e97e9)) + ### [0.45.8](https://www.github.com/libp2p/js-libp2p/compare/libp2p-v0.45.7...libp2p-v0.45.8) (2023-06-14) @@ -2391,4 +2546,4 @@ for subscribe to see how it should be used. -## [0.5.5](https://github.com/libp2p/js-libp2p/compare/v0.5.4...v0.5.5) (2017-03-21) \ No newline at end of file +## [0.5.5](https://github.com/libp2p/js-libp2p/compare/v0.5.4...v0.5.5) (2017-03-21) diff --git a/packages/libp2p/package.json b/packages/libp2p/package.json index 0a7f5434b0..9cb6a68fd3 100644 --- a/packages/libp2p/package.json +++ b/packages/libp2p/package.json @@ -1,6 +1,6 @@ { "name": "libp2p", - "version": "0.46.5", + "version": "0.46.14", "description": "JavaScript implementation of libp2p, a modular peer to peer network stack", "license": "Apache-2.0 OR MIT", "homepage": "https://github.com/libp2p/js-libp2p/tree/master/packages/libp2p#readme", @@ -88,6 +88,7 @@ "eslintConfig": { "extends": "ipfs", "parserOptions": { + "project": true, "sourceType": "module" }, "ignorePatterns": [ @@ -99,7 +100,7 @@ "scripts": { "clean": "aegir clean", "lint": "aegir lint", - "dep-check": "aegir dep-check", + "dep-check": "aegir dep-check -i events", "prepublishOnly": "node scripts/update-version.js && npm run build", "build": "aegir build", "generate": "run-s generate:proto:*", @@ -120,24 +121,25 @@ }, "dependencies": { "@achingbrain/nat-port-mapper": "^1.0.9", - "@libp2p/crypto": "^2.0.3", - "@libp2p/interface": "^0.1.2", - "@libp2p/interface-internal": "^0.1.4", - "@libp2p/keychain": "^3.0.3", - "@libp2p/logger": "^3.0.2", - "@libp2p/multistream-select": "^4.0.2", - "@libp2p/peer-collections": "^4.0.3", - "@libp2p/peer-id": "^3.0.2", - "@libp2p/peer-id-factory": "^3.0.3", - "@libp2p/peer-record": "^6.0.3", - "@libp2p/peer-store": "^9.0.3", - "@libp2p/utils": "^4.0.2", + "@libp2p/crypto": "^2.0.5", + "@libp2p/interface": "^0.1.3", + "@libp2p/interface-internal": "^0.1.6", + "@libp2p/keychain": "^3.0.5", + "@libp2p/logger": "^3.0.3", + "@libp2p/multistream-select": "^4.0.3", + "@libp2p/peer-collections": "^4.0.5", + "@libp2p/peer-id": "^3.0.3", + "@libp2p/peer-id-factory": "^3.0.5", + "@libp2p/peer-record": "^6.0.6", + "@libp2p/peer-store": "^9.0.6", + "@libp2p/utils": "^4.0.4", "@multiformats/mafmt": "^12.1.2", "@multiformats/multiaddr": "^12.1.5", "@multiformats/multiaddr-matcher": "^1.0.0", - "abortable-iterator": "^5.0.1", "any-signal": "^4.1.1", "datastore-core": "^9.0.1", + "delay": "^6.0.0", + "events": "^3.3.0", "interface-datastore": "^8.2.0", "it-all": "^3.0.2", "it-drain": "^3.0.2", @@ -156,11 +158,11 @@ "multiformats": "^12.0.1", "p-defer": "^4.0.0", "p-queue": "^7.3.4", - "p-retry": "^5.0.0", + "p-retry": "^6.0.0", "private-ip": "^3.0.0", "progress-events": "^1.0.0", "protons-runtime": "^5.0.0", - "rate-limiter-flexible": "^2.3.11", + "rate-limiter-flexible": "^3.0.0", "uint8arraylist": "^2.4.3", "uint8arrays": "^4.0.6", "wherearewe": "^2.0.1", @@ -170,21 +172,20 @@ "@chainsafe/libp2p-gossipsub": "^10.0.0", "@chainsafe/libp2p-noise": "^13.0.0", "@chainsafe/libp2p-yamux": "^5.0.0", - "@libp2p/bootstrap": "^9.0.4", + "@libp2p/bootstrap": "^9.0.8", "@libp2p/daemon-client": "^7.0.0", "@libp2p/daemon-server": "^6.0.0", - "@libp2p/floodsub": "^8.0.5", - "@libp2p/interface-compliance-tests": "^4.0.4", + "@libp2p/floodsub": "^8.0.9", + "@libp2p/interface-compliance-tests": "^4.1.1", "@libp2p/interop": "^9.0.0", - "@libp2p/kad-dht": "^10.0.4", - "@libp2p/mdns": "^9.0.4", - "@libp2p/mplex": "^9.0.4", - "@libp2p/tcp": "^8.0.4", - "@libp2p/websockets": "^7.0.4", + "@libp2p/kad-dht": "^10.0.9", + "@libp2p/mdns": "^9.0.10", + "@libp2p/mplex": "^9.0.8", + "@libp2p/tcp": "^8.0.9", + "@libp2p/websockets": "^7.0.9", "@types/xsalsa20": "^1.1.0", - "aegir": "^40.0.8", - "delay": "^6.0.0", - "execa": "^7.1.1", + "aegir": "^41.0.2", + "execa": "^8.0.1", "go-libp2p": "^1.1.1", "it-pushable": "^3.2.0", "it-to-buffer": "^4.0.1", @@ -193,7 +194,7 @@ "p-times": "^4.0.0", "p-wait-for": "^5.0.2", "protons": "^7.0.2", - "sinon": "^15.1.2", + "sinon": "^16.0.0", "sinon-ts": "^1.0.0" }, "browser": { diff --git a/packages/libp2p/src/autonat/index.ts b/packages/libp2p/src/autonat/index.ts index 63b4d6f849..3f70e6a850 100644 --- a/packages/libp2p/src/autonat/index.ts +++ b/packages/libp2p/src/autonat/index.ts @@ -20,18 +20,18 @@ */ import { setMaxListeners } from 'events' +import { CodeError } from '@libp2p/interface/errors' import { logger } from '@libp2p/logger' import { peerIdFromBytes } from '@libp2p/peer-id' import { createEd25519PeerId } from '@libp2p/peer-id-factory' import { multiaddr, protocols } from '@multiformats/multiaddr' -import { abortableDuplex } from 'abortable-iterator' -import { anySignal } from 'any-signal' import first from 'it-first' import * as lp from 'it-length-prefixed' import map from 'it-map' import parallel from 'it-parallel' import { pipe } from 'it-pipe' import isPrivateIp from 'private-ip' +import { codes } from '../errors.js' import { MAX_INBOUND_STREAMS, MAX_OUTBOUND_STREAMS, @@ -154,7 +154,13 @@ class DefaultAutoNATService implements Startable { * Handle an incoming AutoNAT request */ async handleIncomingAutonatStream (data: IncomingStreamData): Promise { - const signal = anySignal([AbortSignal.timeout(this.timeout)]) + const signal = AbortSignal.timeout(this.timeout) + + const onAbort = (): void => { + data.stream.abort(new CodeError('handleIncomingAutonatStream timeout', codes.ERR_TIMEOUT)) + } + + signal.addEventListener('abort', onAbort, { once: true }) // this controller may be used while dialing lots of peers so prevent MaxListenersExceededWarning // appearing in the console @@ -167,11 +173,10 @@ class DefaultAutoNATService implements Startable { .map(ma => ma.toOptions().host) try { - const source = abortableDuplex(data.stream, signal) const self = this await pipe( - source, + data.stream, (source) => lp.decode(source), async function * (stream) { const buf = await first(stream) @@ -380,14 +385,12 @@ class DefaultAutoNATService implements Startable { }) }, (source) => lp.encode(source), - // pipe to the stream, not the abortable source other wise we - // can't tell the remote when a dial timed out.. data.stream ) } catch (err) { log.error('error handling incoming autonat stream', err) } finally { - signal.clear() + signal.removeEventListener('abort', onAbort) } } @@ -456,6 +459,8 @@ class DefaultAutoNATService implements Startable { const networkSegments: string[] = [] const verifyAddress = async (peer: PeerInfo): Promise => { + let onAbort = (): void => {} + try { log('asking %p to verify multiaddr', peer.id) @@ -466,12 +471,15 @@ class DefaultAutoNATService implements Startable { const stream = await connection.newStream(this.protocol, { signal }) - const source = abortableDuplex(stream, signal) + + onAbort = () => { stream.abort(new CodeError('verifyAddress timeout', codes.ERR_TIMEOUT)) } + + signal.addEventListener('abort', onAbort, { once: true }) const buf = await pipe( [request], (source) => lp.encode(source), - source, + stream, (source) => lp.decode(source), async (stream) => first(stream) ) @@ -513,6 +521,8 @@ class DefaultAutoNATService implements Startable { return response.dialResponse } catch (err) { log.error('error asking remote to verify multiaddr', err) + } finally { + signal.removeEventListener('abort', onAbort) } } diff --git a/packages/libp2p/src/circuit-relay/server/index.ts b/packages/libp2p/src/circuit-relay/server/index.ts index 42f931ca16..aed3d5be8b 100644 --- a/packages/libp2p/src/circuit-relay/server/index.ts +++ b/packages/libp2p/src/circuit-relay/server/index.ts @@ -391,7 +391,7 @@ class CircuitRelayServer extends EventEmitter implements Star await hopstr.write({ type: HopMessage.Type.STATUS, status: Status.OK }) const sourceStream = stream.unwrap() - log('connection from %p to %p established - merging streans', connection.remotePeer, dstPeer) + log('connection from %p to %p established - merging streams', connection.remotePeer, dstPeer) const limit = this.reservationStore.get(dstPeer)?.limit // Short circuit the two streams to create the relayed connection createLimitedRelay(sourceStream, destinationStream, this.shutdownController.signal, limit) diff --git a/packages/libp2p/src/circuit-relay/transport/index.ts b/packages/libp2p/src/circuit-relay/transport/index.ts index 139fb69d7c..483f9a0366 100644 --- a/packages/libp2p/src/circuit-relay/transport/index.ts +++ b/packages/libp2p/src/circuit-relay/transport/index.ts @@ -241,7 +241,7 @@ class CircuitRelayTransport implements Transport { disconnectOnFailure }) } catch (err: any) { - log.error(`Circuit relay dial to destination ${destinationPeer.toString()} via relay ${relayPeer.toString()} failed`, err) + log.error('circuit relay dial to destination %p via relay %p failed', destinationPeer, relayPeer, err) if (stream != null) { stream.abort(err) diff --git a/packages/libp2p/src/circuit-relay/transport/reservation-store.ts b/packages/libp2p/src/circuit-relay/transport/reservation-store.ts index 027fc1ea05..2edcd4f998 100644 --- a/packages/libp2p/src/circuit-relay/transport/reservation-store.ts +++ b/packages/libp2p/src/circuit-relay/transport/reservation-store.ts @@ -261,7 +261,7 @@ export class ReservationStore extends EventEmitter imple options.signal?.throwIfAborted() log('requesting reservation from %p', connection.remotePeer) - const stream = await connection.newStream(RELAY_V2_HOP_CODEC) + const stream = await connection.newStream(RELAY_V2_HOP_CODEC, options) const pbstr = pbStream(stream) const hopstr = pbstr.pb(HopMessage) await hopstr.write({ type: HopMessage.Type.RESERVE }, options) diff --git a/packages/libp2p/src/circuit-relay/utils.ts b/packages/libp2p/src/circuit-relay/utils.ts index e37b020d71..550c80a177 100644 --- a/packages/libp2p/src/circuit-relay/utils.ts +++ b/packages/libp2p/src/circuit-relay/utils.ts @@ -1,9 +1,9 @@ +import { CodeError } from '@libp2p/interface/errors' import { logger } from '@libp2p/logger' -import { abortableSource } from 'abortable-iterator' import { anySignal } from 'any-signal' import { CID } from 'multiformats/cid' import { sha256 } from 'multiformats/hashes/sha2' -import { DEFAULT_DATA_LIMIT, DEFAULT_DURATION_LIMIT } from './constants.js' +import { codes } from '../errors.js' import type { Limit } from './pb/index.js' import type { Stream } from '@libp2p/interface/connection' import type { Source } from 'it-stream-types' @@ -12,6 +12,8 @@ import type { Uint8ArrayList } from 'uint8arraylist' const log = logger('libp2p:circuit-relay:utils') async function * countStreamBytes (source: Source, limit: { remaining: bigint }): AsyncGenerator { + const limitBytes = limit.remaining + for await (const buf of source) { const len = BigInt(buf.byteLength) @@ -28,7 +30,7 @@ async function * countStreamBytes (source: Source, log.error(err) } - throw new Error('data limit exceeded') + throw new CodeError(`data limit of ${limitBytes} bytes exceeded`, codes.ERR_TRANSFER_LIMIT_EXCEEDED) } limit.remaining -= len @@ -36,7 +38,7 @@ async function * countStreamBytes (source: Source, } } -const doRelay = (src: Stream, dst: Stream, abortSignal: AbortSignal, limit: Required): void => { +export function createLimitedRelay (src: Stream, dst: Stream, abortSignal: AbortSignal, limit?: Limit): void { function abortStreams (err: Error): void { src.abort(err) dst.abort(err) @@ -46,21 +48,33 @@ const doRelay = (src: Stream, dst: Stream, abortSignal: AbortSignal, limit: Requ const abortController = new AbortController() const signal = anySignal([abortSignal, abortController.signal]) - const timeout = setTimeout(() => { - abortController.abort() - }, limit.duration) + let timeout: ReturnType | undefined + + if (limit?.duration != null) { + timeout = setTimeout(() => { + abortController.abort() + }, limit.duration) + } let srcDstFinished = false let dstSrcFinished = false - const dataLimit = { - remaining: limit.data + let dataLimit: { remaining: bigint } | undefined + + if (limit?.data != null) { + dataLimit = { + remaining: limit.data + } } queueMicrotask(() => { - void dst.sink(countStreamBytes(abortableSource(src.source, signal, { - abortMessage: 'duration limit exceeded' - }), dataLimit)) + const onAbort = (): void => { + dst.abort(new CodeError(`duration limit of ${limit?.duration} ms exceeded`, codes.ERR_TRANSFER_LIMIT_EXCEEDED)) + } + + signal.addEventListener('abort', onAbort, { once: true }) + + void dst.sink(dataLimit == null ? src.source : countStreamBytes(src.source, dataLimit)) .catch(err => { log.error('error while relaying streams src -> dst', err) abortStreams(err) @@ -69,6 +83,7 @@ const doRelay = (src: Stream, dst: Stream, abortSignal: AbortSignal, limit: Requ srcDstFinished = true if (dstSrcFinished) { + signal.removeEventListener('abort', onAbort) signal.clear() clearTimeout(timeout) } @@ -76,9 +91,13 @@ const doRelay = (src: Stream, dst: Stream, abortSignal: AbortSignal, limit: Requ }) queueMicrotask(() => { - void src.sink(countStreamBytes(abortableSource(dst.source, signal, { - abortMessage: 'duration limit exceeded' - }), dataLimit)) + const onAbort = (): void => { + src.abort(new CodeError(`duration limit of ${limit?.duration} ms exceeded`, codes.ERR_TRANSFER_LIMIT_EXCEEDED)) + } + + signal.addEventListener('abort', onAbort, { once: true }) + + void src.sink(dataLimit == null ? dst.source : countStreamBytes(dst.source, dataLimit)) .catch(err => { log.error('error while relaying streams dst -> src', err) abortStreams(err) @@ -87,6 +106,7 @@ const doRelay = (src: Stream, dst: Stream, abortSignal: AbortSignal, limit: Requ dstSrcFinished = true if (srcDstFinished) { + signal.removeEventListener('abort', onAbort) signal.clear() clearTimeout(timeout) } @@ -94,16 +114,6 @@ const doRelay = (src: Stream, dst: Stream, abortSignal: AbortSignal, limit: Requ }) } -export function createLimitedRelay (source: Stream, destination: Stream, abortSignal: AbortSignal, limit?: Limit): void { - const dataLimit = limit?.data ?? BigInt(DEFAULT_DATA_LIMIT) - const durationLimit = limit?.duration ?? DEFAULT_DURATION_LIMIT - - doRelay(source, destination, abortSignal, { - data: dataLimit, - duration: durationLimit - }) -} - /** * Convert a namespace string into a cid */ diff --git a/packages/libp2p/src/config.ts b/packages/libp2p/src/config.ts index fd4d3f8f63..a9b83fb532 100644 --- a/packages/libp2p/src/config.ts +++ b/packages/libp2p/src/config.ts @@ -1,6 +1,6 @@ import { CodeError } from '@libp2p/interface/errors' import { FaultTolerance } from '@libp2p/interface/transport' -import { publicAddressesFirst } from '@libp2p/utils/address-sort' +import { defaultAddressSort } from '@libp2p/utils/address-sort' import { dnsaddrResolver } from '@multiformats/multiaddr/resolvers' import mergeOptions from 'merge-options' import { codes, messages } from './errors.js' @@ -19,7 +19,7 @@ const DefaultConfig: Partial = { resolvers: { dnsaddr: dnsaddrResolver }, - addressSorter: publicAddressesFirst + addressSorter: defaultAddressSort }, transportManager: { faultTolerance: FaultTolerance.FATAL_ALL diff --git a/packages/libp2p/src/connection-manager/auto-dial.ts b/packages/libp2p/src/connection-manager/auto-dial.ts index 7ba2247a8f..6d27e6f42b 100644 --- a/packages/libp2p/src/connection-manager/auto-dial.ts +++ b/packages/libp2p/src/connection-manager/auto-dial.ts @@ -2,7 +2,7 @@ import { logger } from '@libp2p/logger' import { PeerMap, PeerSet } from '@libp2p/peer-collections' import { toString as uint8ArrayToString } from 'uint8arrays/to-string' import { PeerJobQueue } from '../utils/peer-job-queue.js' -import { AUTO_DIAL_CONCURRENCY, AUTO_DIAL_INTERVAL, AUTO_DIAL_MAX_QUEUE_LENGTH, AUTO_DIAL_PEER_RETRY_THRESHOLD, AUTO_DIAL_PRIORITY, LAST_DIAL_FAILURE_KEY, MIN_CONNECTIONS } from './constants.js' +import { AUTO_DIAL_CONCURRENCY, AUTO_DIAL_DISCOVERED_PEERS_DEBOUNCE, AUTO_DIAL_INTERVAL, AUTO_DIAL_MAX_QUEUE_LENGTH, AUTO_DIAL_PEER_RETRY_THRESHOLD, AUTO_DIAL_PRIORITY, LAST_DIAL_FAILURE_KEY, MIN_CONNECTIONS } from './constants.js' import type { Libp2pEvents } from '@libp2p/interface' import type { EventEmitter } from '@libp2p/interface/events' import type { PeerStore } from '@libp2p/interface/peer-store' @@ -18,6 +18,7 @@ interface AutoDialInit { autoDialPriority?: number autoDialInterval?: number autoDialPeerRetryThreshold?: number + autoDialDiscoveredPeersDebounce?: number } interface AutoDialComponents { @@ -32,7 +33,8 @@ const defaultOptions = { autoDialConcurrency: AUTO_DIAL_CONCURRENCY, autoDialPriority: AUTO_DIAL_PRIORITY, autoDialInterval: AUTO_DIAL_INTERVAL, - autoDialPeerRetryThreshold: AUTO_DIAL_PEER_RETRY_THRESHOLD + autoDialPeerRetryThreshold: AUTO_DIAL_PEER_RETRY_THRESHOLD, + autoDialDiscoveredPeersDebounce: AUTO_DIAL_DISCOVERED_PEERS_DEBOUNCE } export class AutoDial implements Startable { @@ -44,6 +46,7 @@ export class AutoDial implements Startable { private readonly autoDialIntervalMs: number private readonly autoDialMaxQueueLength: number private readonly autoDialPeerRetryThresholdMs: number + private readonly autoDialDiscoveredPeersDebounce: number private autoDialInterval?: ReturnType private started: boolean private running: boolean @@ -61,6 +64,7 @@ export class AutoDial implements Startable { this.autoDialIntervalMs = init.autoDialInterval ?? defaultOptions.autoDialInterval this.autoDialMaxQueueLength = init.maxQueueLength ?? defaultOptions.maxQueueLength this.autoDialPeerRetryThresholdMs = init.autoDialPeerRetryThreshold ?? defaultOptions.autoDialPeerRetryThreshold + this.autoDialDiscoveredPeersDebounce = init.autoDialDiscoveredPeersDebounce ?? defaultOptions.autoDialDiscoveredPeersDebounce this.started = false this.running = false this.queue = new PeerJobQueue({ @@ -77,6 +81,22 @@ export class AutoDial implements Startable { log.error(err) }) }) + + // sometimes peers are discovered in quick succession so add a small + // debounce to ensure all eligible peers are autodialed + let debounce: ReturnType + + // when new peers are discovered, dial them if we don't have + // enough connections + components.events.addEventListener('peer:discovery', () => { + clearTimeout(debounce) + debounce = setTimeout(() => { + this.autoDial() + .catch(err => { + log.error(err) + }) + }, this.autoDialDiscoveredPeersDebounce) + }) } isStarted (): boolean { @@ -152,25 +172,25 @@ export class AutoDial implements Startable { (peer) => { // Remove peers without addresses if (peer.addresses.length === 0) { - log.trace('not autodialing %p because they have no addresses') + log.trace('not autodialing %p because they have no addresses', peer.id) return false } // remove peers we are already connected to if (connections.has(peer.id)) { - log.trace('not autodialing %p because they are already connected') + log.trace('not autodialing %p because they are already connected', peer.id) return false } // remove peers we are already dialling if (dialQueue.has(peer.id)) { - log.trace('not autodialing %p because they are already being dialed') + log.trace('not autodialing %p because they are already being dialed', peer.id) return false } // remove peers already in the autodial queue if (this.queue.hasJob(peer.id)) { - log.trace('not autodialing %p because they are already being autodialed') + log.trace('not autodialing %p because they are already being autodialed', peer.id) return false } diff --git a/packages/libp2p/src/connection-manager/constants.browser.ts b/packages/libp2p/src/connection-manager/constants.browser.ts index 8db6c9ca58..2c369c1245 100644 --- a/packages/libp2p/src/connection-manager/constants.browser.ts +++ b/packages/libp2p/src/connection-manager/constants.browser.ts @@ -1,10 +1,5 @@ export * from './constants.defaults.js' -/** - * @see https://libp2p.github.io/js-libp2p/interfaces/index._internal_.ConnectionManagerConfig.html#maxParallelDials - */ -export const MAX_PARALLEL_DIALS = 10 - /** * @see https://libp2p.github.io/js-libp2p/interfaces/index._internal_.ConnectionManagerConfig.html#minConnections */ @@ -16,6 +11,11 @@ export const MIN_CONNECTIONS = 5 export const MAX_CONNECTIONS = 100 /** - * @see https://libp2p.github.io/js-libp2p/interfaces/index._internal_.ConnectionManagerConfig.html#autoDialConcurrency + * @see https://libp2p.github.io/js-libp2p/interfaces/index._internal_.ConnectionManagerConfig.html#maxParallelDials + */ +export const MAX_PARALLEL_DIALS = 50 + +/** + * @see https://libp2p.github.io/js-libp2p/interfaces/libp2p.index.unknown.ConnectionManagerInit.html#autoDialPeerRetryThreshold */ -export const AUTO_DIAL_CONCURRENCY = 10 +export const AUTO_DIAL_PEER_RETRY_THRESHOLD = 1000 * 60 * 7 diff --git a/packages/libp2p/src/connection-manager/constants.defaults.ts b/packages/libp2p/src/connection-manager/constants.defaults.ts index 60e7e5c430..13f1f18957 100644 --- a/packages/libp2p/src/connection-manager/constants.defaults.ts +++ b/packages/libp2p/src/connection-manager/constants.defaults.ts @@ -16,13 +16,18 @@ export const MAX_PEER_ADDRS_TO_DIAL = 25 /** * @see https://libp2p.github.io/js-libp2p/interfaces/index._internal_.ConnectionManagerConfig.html#maxParallelDialsPerPeer */ -export const MAX_PARALLEL_DIALS_PER_PEER = 10 +export const MAX_PARALLEL_DIALS_PER_PEER = 1 /** * @see https://libp2p.github.io/js-libp2p/interfaces/index._internal_.ConnectionManagerConfig.html#autoDialInterval */ export const AUTO_DIAL_INTERVAL = 5000 +/** + * @see https://libp2p.github.io/js-libp2p/interfaces/index._internal_.ConnectionManagerConfig.html#autoDialConcurrency + */ +export const AUTO_DIAL_CONCURRENCY = 25 + /** * @see https://libp2p.github.io/js-libp2p/interfaces/index._internal_.ConnectionManagerConfig.html#autoDialPriority */ @@ -34,9 +39,9 @@ export const AUTO_DIAL_PRIORITY = 0 export const AUTO_DIAL_MAX_QUEUE_LENGTH = 100 /** - * @see https://libp2p.github.io/js-libp2p/interfaces/libp2p.index.unknown.ConnectionManagerInit.html#autoDialPeerRetryThreshold + * @see https://libp2p.github.io/js-libp2p/interfaces/libp2p.index.unknown.ConnectionManagerInit.html#autoDialDiscoveredPeersDebounce */ -export const AUTO_DIAL_PEER_RETRY_THRESHOLD = 1000 * 60 +export const AUTO_DIAL_DISCOVERED_PEERS_DEBOUNCE = 10 /** * @see https://libp2p.github.io/js-libp2p/interfaces/index._internal_.ConnectionManagerConfig.html#inboundConnectionThreshold diff --git a/packages/libp2p/src/connection-manager/constants.ts b/packages/libp2p/src/connection-manager/constants.ts index 170351fd77..a6a6c486f4 100644 --- a/packages/libp2p/src/connection-manager/constants.ts +++ b/packages/libp2p/src/connection-manager/constants.ts @@ -1,10 +1,5 @@ export * from './constants.defaults.js' -/** - * @see https://libp2p.github.io/js-libp2p/interfaces/index._internal_.ConnectionManagerConfig.html#maxParallelDials - */ -export const MAX_PARALLEL_DIALS = 100 - /** * @see https://libp2p.github.io/js-libp2p/interfaces/index._internal_.ConnectionManagerConfig.html#minConnections */ @@ -16,6 +11,11 @@ export const MIN_CONNECTIONS = 50 export const MAX_CONNECTIONS = 300 /** - * @see https://libp2p.github.io/js-libp2p/interfaces/index._internal_.ConnectionManagerConfig.html#autoDialConcurrency + * @see https://libp2p.github.io/js-libp2p/interfaces/index._internal_.ConnectionManagerConfig.html#maxParallelDials + */ +export const MAX_PARALLEL_DIALS = 100 + +/** + * @see https://libp2p.github.io/js-libp2p/interfaces/libp2p.index.unknown.ConnectionManagerInit.html#autoDialPeerRetryThreshold */ -export const AUTO_DIAL_CONCURRENCY = 25 +export const AUTO_DIAL_PEER_RETRY_THRESHOLD = 1000 * 60 diff --git a/packages/libp2p/src/connection-manager/dial-queue.ts b/packages/libp2p/src/connection-manager/dial-queue.ts index 1e6bb8b6ba..3ba2b956bb 100644 --- a/packages/libp2p/src/connection-manager/dial-queue.ts +++ b/packages/libp2p/src/connection-manager/dial-queue.ts @@ -1,7 +1,7 @@ import { setMaxListeners } from 'events' import { AbortError, CodeError } from '@libp2p/interface/errors' import { logger } from '@libp2p/logger' -import { publicAddressesFirst } from '@libp2p/utils/address-sort' +import { defaultAddressSort } from '@libp2p/utils/address-sort' import { type Multiaddr, type Resolver, resolvers } from '@multiformats/multiaddr' import { dnsaddrResolver } from '@multiformats/multiaddr/resolvers' import { type ClearableSignal, anySignal } from 'any-signal' @@ -29,8 +29,8 @@ import type { TransportManager } from '@libp2p/interface-internal/transport-mana const log = logger('libp2p:connection-manager:dial-queue') export interface PendingDialTarget { - resolve: (value: any) => void - reject: (err: Error) => void + resolve(value: any): void + reject(err: Error): void } export interface DialOptions extends AbortOptions { @@ -51,7 +51,7 @@ interface DialerInit { } const defaultOptions = { - addressSorter: publicAddressesFirst, + addressSorter: defaultAddressSort, maxParallelDials: MAX_PARALLEL_DIALS, maxPeerAddrsToDial: MAX_PEER_ADDRS_TO_DIAL, maxParallelDialsPerPeer: MAX_PARALLEL_DIALS_PER_PEER, diff --git a/packages/libp2p/src/connection-manager/index.ts b/packages/libp2p/src/connection-manager/index.ts index 4258bd4c87..d94612fd9f 100644 --- a/packages/libp2p/src/connection-manager/index.ts +++ b/packages/libp2p/src/connection-manager/index.ts @@ -2,7 +2,7 @@ import { CodeError } from '@libp2p/interface/errors' import { KEEP_ALIVE } from '@libp2p/interface/peer-store/tags' import { logger } from '@libp2p/logger' import { PeerMap } from '@libp2p/peer-collections' -import { publicAddressesFirst } from '@libp2p/utils/address-sort' +import { defaultAddressSort } from '@libp2p/utils/address-sort' import { type Multiaddr, type Resolver, multiaddr } from '@multiformats/multiaddr' import { dnsaddrResolver } from '@multiformats/multiaddr/resolvers' import { RateLimiterMemory } from 'rate-limiter-flexible' @@ -10,7 +10,7 @@ import { codes } from '../errors.js' import { getPeerAddress } from '../get-peer.js' import { AutoDial } from './auto-dial.js' import { ConnectionPruner } from './connection-pruner.js' -import { AUTO_DIAL_CONCURRENCY, AUTO_DIAL_MAX_QUEUE_LENGTH, AUTO_DIAL_PRIORITY, DIAL_TIMEOUT, INBOUND_CONNECTION_THRESHOLD, MAX_CONNECTIONS, MAX_INCOMING_PENDING_CONNECTIONS, MAX_PARALLEL_DIALS, MAX_PEER_ADDRS_TO_DIAL, MIN_CONNECTIONS } from './constants.js' +import { AUTO_DIAL_CONCURRENCY, AUTO_DIAL_MAX_QUEUE_LENGTH, AUTO_DIAL_PRIORITY, DIAL_TIMEOUT, INBOUND_CONNECTION_THRESHOLD, MAX_CONNECTIONS, MAX_INCOMING_PENDING_CONNECTIONS, MAX_PARALLEL_DIALS, MAX_PARALLEL_DIALS_PER_PEER, MAX_PEER_ADDRS_TO_DIAL, MIN_CONNECTIONS } from './constants.js' import { DialQueue } from './dial-queue.js' import type { PendingDial, AddressSorter, Libp2pEvents, AbortOptions } from '@libp2p/interface' import type { Connection, MultiaddrConnection } from '@libp2p/interface/connection' @@ -30,14 +30,14 @@ const DEFAULT_DIAL_PRIORITY = 50 export interface ConnectionManagerInit { /** * The maximum number of connections libp2p is willing to have before it starts - * pruning connections to reduce resource usage. (default: 300) + * pruning connections to reduce resource usage. (default: 300, 100 in browsers) */ maxConnections?: number /** * The minimum number of connections below which libp2p will start to dial peers * from the peer book. Setting this to 0 effectively disables this behaviour. - * (default: 50) + * (default: 50, 5 in browsers) */ minConnections?: number @@ -68,10 +68,18 @@ export interface ConnectionManagerInit { /** * When we've failed to dial a peer, do not autodial them again within this - * number of ms. (default: 1 minute) + * number of ms. (default: 1 minute, 7 minutes in browsers) */ autoDialPeerRetryThreshold?: number + /** + * Newly discovered peers may be auto-dialed to increase the number of open + * connections, but they can be discovered in quick succession so add a small + * delay before attempting to dial them in case more peers have been + * discovered. (default: 10ms) + */ + autoDialDiscoveredPeersDebounce?: number + /** * Sort the known addresses of a peer before trying to dial, By default public * addresses will be dialled before private (e.g. loopback or LAN) addresses. @@ -80,7 +88,7 @@ export interface ConnectionManagerInit { /** * The maximum number of dials across all peers to execute in parallel. - * (default: 100) + * (default: 100, 50 in browsers) */ maxParallelDials?: number @@ -88,7 +96,7 @@ export interface ConnectionManagerInit { * To prevent individual peers with large amounts of multiaddrs swamping the * dial queue, this value controls how many addresses to dial in parallel per * peer. So for example if two peers have 10 addresses and this value is set - * at 5, we will dial 5 addresses from each at a time. (default: 10) + * at 5, we will dial 5 addresses from each at a time. (default: 1) */ maxParallelDialsPerPeer?: number @@ -246,9 +254,10 @@ export class DefaultConnectionManager implements ConnectionManager, Startable { transportManager: components.transportManager, connectionGater: components.connectionGater }, { - addressSorter: init.addressSorter ?? publicAddressesFirst, + addressSorter: init.addressSorter ?? defaultAddressSort, maxParallelDials: init.maxParallelDials ?? MAX_PARALLEL_DIALS, maxPeerAddrsToDial: init.maxPeerAddrsToDial ?? MAX_PEER_ADDRS_TO_DIAL, + maxParallelDialsPerPeer: init.maxParallelDialsPerPeer ?? MAX_PARALLEL_DIALS_PER_PEER, dialTimeout: init.dialTimeout ?? DIAL_TIMEOUT, resolvers: init.resolvers ?? { dnsaddr: dnsaddrResolver diff --git a/packages/libp2p/src/connection/index.ts b/packages/libp2p/src/connection/index.ts index 8509b8d70a..4066e78c93 100644 --- a/packages/libp2p/src/connection/index.ts +++ b/packages/libp2p/src/connection/index.ts @@ -14,10 +14,10 @@ const CLOSE_TIMEOUT = 500 interface ConnectionInit { remoteAddr: Multiaddr remotePeer: PeerId - newStream: (protocols: string[], options?: AbortOptions) => Promise - close: (options?: AbortOptions) => Promise - abort: (err: Error) => void - getStreams: () => Stream[] + newStream(protocols: string[], options?: AbortOptions): Promise + close(options?: AbortOptions): Promise + abort(err: Error): void + getStreams(): Stream[] status: ConnectionStatus direction: Direction timeline: ConnectionTimeline @@ -158,16 +158,22 @@ export class ConnectionImpl implements Connection { } catch { } try { + log.trace('closing all streams') + // close all streams gracefully - this can throw if we're not multiplexed await Promise.all( this.streams.map(async s => s.close(options)) ) - // Close raw connection + log.trace('closing underlying transport') + + // close raw connection await this._close(options) - this.timeline.close = Date.now() + log.trace('updating timeline with close time') + this.status = 'closed' + this.timeline.close = Date.now() } catch (err: any) { log.error('error encountered during graceful close of connection to %a', this.remoteAddr, err) this.abort(err) diff --git a/packages/libp2p/src/dcutr/dcutr.ts b/packages/libp2p/src/dcutr/dcutr.ts index 190766bfa2..e6852b6d03 100644 --- a/packages/libp2p/src/dcutr/dcutr.ts +++ b/packages/libp2p/src/dcutr/dcutr.ts @@ -262,7 +262,10 @@ export class DefaultDCUtRService implements Startable { } log('unilateral connection upgrade to %p succeeded via %a, closing relayed connection', relayedConnection.remotePeer, connection.remoteAddr) - await relayedConnection.close() + + await relayedConnection.close({ + signal + }) return true } catch (err) { diff --git a/packages/libp2p/src/errors.ts b/packages/libp2p/src/errors.ts index 7fc428b6d4..a2d2ffb6c6 100644 --- a/packages/libp2p/src/errors.ts +++ b/packages/libp2p/src/errors.ts @@ -75,5 +75,5 @@ export enum codes { ERR_TOO_MANY_OUTBOUND_PROTOCOL_STREAMS = 'ERR_TOO_MANY_OUTBOUND_PROTOCOL_STREAMS', ERR_TOO_MANY_INBOUND_PROTOCOL_STREAMS = 'ERR_TOO_MANY_INBOUND_PROTOCOL_STREAMS', ERR_CONNECTION_DENIED = 'ERR_CONNECTION_DENIED', - ERR_TRANSFER_LIMIT_EXCEEDED = 'ERR_TRANSFER_LIMIT_EXCEEDED', + ERR_TRANSFER_LIMIT_EXCEEDED = 'ERR_TRANSFER_LIMIT_EXCEEDED' } diff --git a/packages/libp2p/src/fetch/index.ts b/packages/libp2p/src/fetch/index.ts index f72e1e7b4a..3523b2b17d 100644 --- a/packages/libp2p/src/fetch/index.ts +++ b/packages/libp2p/src/fetch/index.ts @@ -1,7 +1,6 @@ import { setMaxListeners } from 'events' import { CodeError } from '@libp2p/interface/errors' import { logger } from '@libp2p/logger' -import { abortableDuplex } from 'abortable-iterator' import first from 'it-first' import * as lp from 'it-length-prefixed' import { pipe } from 'it-pipe' @@ -55,7 +54,7 @@ export interface FetchService { /** * Sends a request to fetch the value associated with the given key from the given peer */ - fetch: (peer: PeerId, key: string, options?: AbortOptions) => Promise + fetch(peer: PeerId, key: string, options?: AbortOptions): Promise /** * Registers a new lookup callback that can map keys to values, for a given set of keys that @@ -68,7 +67,7 @@ export interface FetchService { * libp2p.fetchService.registerLookupFunction('/prefix', (key) => { ... }) * ``` */ - registerLookupFunction: (prefix: string, lookup: LookupFunction) => void + registerLookupFunction(prefix: string, lookup: LookupFunction): void /** * Registers a new lookup callback that can map keys to values, for a given set of keys that @@ -81,7 +80,7 @@ export interface FetchService { * libp2p.fetchService.unregisterLookupFunction('/prefix') * ``` */ - unregisterLookupFunction: (prefix: string, lookup?: LookupFunction) => void + unregisterLookupFunction(prefix: string, lookup?: LookupFunction): void } /** @@ -140,6 +139,7 @@ class DefaultFetchService implements Startable, FetchService { const connection = await this.components.connectionManager.openConnection(peer, options) let signal = options.signal let stream: Stream | undefined + let onAbort = (): void => {} // create a timeout if no abort signal passed if (signal == null) { @@ -157,15 +157,19 @@ class DefaultFetchService implements Startable, FetchService { signal }) + onAbort = () => { + stream?.abort(new CodeError('fetch timeout', codes.ERR_TIMEOUT)) + } + // make stream abortable - const source = abortableDuplex(stream, signal) + signal.addEventListener('abort', onAbort, { once: true }) log('fetch %s', key) const result = await pipe( [FetchRequest.encode({ identifier: key })], (source) => lp.encode(source), - source, + stream, (source) => lp.decode(source), async function (source) { const buf = await first(source) @@ -200,6 +204,7 @@ class DefaultFetchService implements Startable, FetchService { return result ?? null } finally { + signal.removeEventListener('abort', onAbort) if (stream != null) { await stream.close() } diff --git a/packages/libp2p/src/identify/index.ts b/packages/libp2p/src/identify/index.ts index a61309b3d3..c74238e832 100644 --- a/packages/libp2p/src/identify/index.ts +++ b/packages/libp2p/src/identify/index.ts @@ -79,9 +79,9 @@ export interface IdentifyService { * Please use with caution. If you find yourself needing to call this method to discover other peers that support your protocol, * you may be better off configuring a topology to be notified instead. */ - identify: (connection: Connection, options?: AbortOptions) => Promise + identify(connection: Connection, options?: AbortOptions): Promise - push: () => Promise + push(): Promise } export function identifyService (init: IdentifyServiceInit = {}): (components: IdentifyServiceComponents) => IdentifyService { diff --git a/packages/libp2p/src/index.ts b/packages/libp2p/src/index.ts index 6a5cda3192..8fa5882135 100644 --- a/packages/libp2p/src/index.ts +++ b/packages/libp2p/src/index.ts @@ -97,12 +97,12 @@ export interface Libp2pInit /** * A Metrics implementation can be supplied to collect metrics on this node */ - metrics?: (components: Components) => Metrics + metrics?(components: Components): Metrics /** * A ConnectionProtector can be used to create a secure overlay on top of the network using pre-shared keys */ - connectionProtector?: (components: Components) => ConnectionProtector + connectionProtector?(components: Components): ConnectionProtector /** * Arbitrary libp2p modules diff --git a/packages/libp2p/src/libp2p.ts b/packages/libp2p/src/libp2p.ts index 1b884efcdd..e115e90c8c 100644 --- a/packages/libp2p/src/libp2p.ts +++ b/packages/libp2p/src/libp2p.ts @@ -114,7 +114,7 @@ export class Libp2pNode< protocols: evt.detail.peer.protocols } - this.safeDispatchEvent('peer:discovery', { detail: peerInfo }) + components.events.safeDispatchEvent('peer:discovery', { detail: peerInfo }) } }) diff --git a/packages/libp2p/src/ping/index.ts b/packages/libp2p/src/ping/index.ts index 82c36c3aab..ea8d23db5e 100644 --- a/packages/libp2p/src/ping/index.ts +++ b/packages/libp2p/src/ping/index.ts @@ -1,7 +1,6 @@ import { randomBytes } from '@libp2p/crypto' import { CodeError } from '@libp2p/interface/errors' import { logger } from '@libp2p/logger' -import { abortableDuplex } from 'abortable-iterator' import first from 'it-first' import { pipe } from 'it-pipe' import { equals as uint8ArrayEquals } from 'uint8arrays/equals' @@ -18,7 +17,7 @@ import type { Multiaddr } from '@multiformats/multiaddr' const log = logger('libp2p:ping') export interface PingService { - ping: (peer: PeerId | Multiaddr | Multiaddr[], options?: AbortOptions) => Promise + ping(peer: PeerId | Multiaddr | Multiaddr[], options?: AbortOptions): Promise } export interface PingServiceInit { @@ -108,6 +107,7 @@ class DefaultPingService implements Startable, PingService { const data = randomBytes(PING_LENGTH) const connection = await this.components.connectionManager.openConnection(peer, options) let stream: Stream | undefined + let onAbort = (): void => {} options.signal = options.signal ?? AbortSignal.timeout(this.timeout) @@ -117,12 +117,16 @@ class DefaultPingService implements Startable, PingService { runOnTransientConnection: this.runOnTransientConnection }) + onAbort = () => { + stream?.abort(new CodeError('ping timeout', codes.ERR_TIMEOUT)) + } + // make stream abortable - const source = abortableDuplex(stream, options.signal) + options.signal.addEventListener('abort', onAbort, { once: true }) const result = await pipe( [data], - source, + stream, async (source) => first(source) ) @@ -146,6 +150,7 @@ class DefaultPingService implements Startable, PingService { throw err } finally { + options.signal.removeEventListener('abort', onAbort) if (stream != null) { await stream.close() } diff --git a/packages/libp2p/src/registrar.ts b/packages/libp2p/src/registrar.ts index f42ac0afeb..c679888f4f 100644 --- a/packages/libp2p/src/registrar.ts +++ b/packages/libp2p/src/registrar.ts @@ -109,7 +109,7 @@ export class DefaultRegistrar implements Registrar { // Update self protocols in the peer store await this.components.peerStore.patch(this.components.peerId, { - protocols: protocolList + protocols: this.getProtocols() }) } diff --git a/packages/libp2p/src/transport-manager.ts b/packages/libp2p/src/transport-manager.ts index 070503764a..38f5359616 100644 --- a/packages/libp2p/src/transport-manager.ts +++ b/packages/libp2p/src/transport-manager.ts @@ -262,12 +262,22 @@ export class DefaultTransportManager implements TransportManager, Startable { * If a transport has any running listeners, they will be closed. */ async remove (key: string): Promise { - log('removing %s', key) + const listeners = this.listeners.get(key) ?? [] + log.trace('removing transport %s', key) // Close any running listeners - for (const listener of this.listeners.get(key) ?? []) { - await listener.close() + const tasks = [] + log.trace('closing listeners for %s', key) + while (listeners.length > 0) { + const listener = listeners.pop() + + if (listener == null) { + continue + } + + tasks.push(listener.close()) } + await Promise.all(tasks) this.transports.delete(key) this.listeners.delete(key) diff --git a/packages/libp2p/src/upgrader.ts b/packages/libp2p/src/upgrader.ts index adf079b49f..18934e3ad0 100644 --- a/packages/libp2p/src/upgrader.ts +++ b/packages/libp2p/src/upgrader.ts @@ -3,8 +3,6 @@ import { CodeError } from '@libp2p/interface/errors' import { logger } from '@libp2p/logger' import * as mss from '@libp2p/multistream-select' import { peerIdFromString } from '@libp2p/peer-id' -import { abortableDuplex } from 'abortable-iterator' -import { anySignal } from 'any-signal' import { createConnection } from './connection/index.js' import { INBOUND_UPGRADE_TIMEOUT } from './connection-manager/constants.js' import { codes } from './errors.js' @@ -164,7 +162,13 @@ export class DefaultUpgrader implements Upgrader { let muxerFactory: StreamMuxerFactory | undefined let cryptoProtocol - const signal = anySignal([AbortSignal.timeout(this.inboundUpgradeTimeout)]) + const signal = AbortSignal.timeout(this.inboundUpgradeTimeout) + + const onAbort = (): void => { + maConn.abort(new CodeError('inbound upgrade timeout', codes.ERR_TIMEOUT)) + } + + signal.addEventListener('abort', onAbort, { once: true }) try { // fails on node < 15.4 @@ -172,10 +176,6 @@ export class DefaultUpgrader implements Upgrader { } catch { } try { - const abortableStream = abortableDuplex(maConn, signal) - maConn.source = abortableStream.source - maConn.sink = abortableStream.sink - if ((await this.components.connectionGater.denyInboundConnection?.(maConn)) === true) { throw new CodeError('The multiaddr connection is blocked by gater.acceptConnection', codes.ERR_CONNECTION_INTERCEPTED) } @@ -256,8 +256,9 @@ export class DefaultUpgrader implements Upgrader { transient: opts?.transient }) } finally { + signal.removeEventListener('abort', onAbort) + this.components.connectionManager.afterUpgradeInbound() - signal.clear() } } @@ -477,7 +478,7 @@ export class DefaultUpgrader implements Upgrader { return muxedStream } catch (err: any) { - log.error('could not create new stream', err) + log.error('could not create new stream for protocols %s on connection with address %a', protocols, connection.remoteAddr, err) if (muxedStream.timeline.close == null) { muxedStream.abort(err) @@ -544,11 +545,16 @@ export class DefaultUpgrader implements Upgrader { newStream: newStream ?? errConnectionNotMultiplexed, getStreams: () => { if (muxer != null) { return muxer.streams } else { return [] } }, close: async (options?: AbortOptions) => { - await maConn.close(options) // Ensure remaining streams are closed gracefully if (muxer != null) { + log.trace('close muxer') await muxer.close(options) } + + log.trace('close maconn') + // close the underlying transport + await maConn.close(options) + log.trace('closed maconn') }, abort: (err) => { maConn.abort(err) diff --git a/packages/libp2p/test/autonat/index.spec.ts b/packages/libp2p/test/autonat/index.spec.ts index 75e37df817..250033f7b3 100644 --- a/packages/libp2p/test/autonat/index.spec.ts +++ b/packages/libp2p/test/autonat/index.spec.ts @@ -436,6 +436,9 @@ describe('autonat', () => { } sink.end() + }, + abort: (err) => { + void stream.source.throw(err) } } const connection = { diff --git a/packages/libp2p/test/circuit-relay/relay.node.ts b/packages/libp2p/test/circuit-relay/relay.node.ts index 24b843bd3d..936dfbb869 100644 --- a/packages/libp2p/test/circuit-relay/relay.node.ts +++ b/packages/libp2p/test/circuit-relay/relay.node.ts @@ -8,22 +8,91 @@ import { Circuit } from '@multiformats/mafmt' import { multiaddr } from '@multiformats/multiaddr' import { expect } from 'aegir/chai' import delay from 'delay' +import all from 'it-all' import { pipe } from 'it-pipe' import { pbStream } from 'it-protobuf-stream' import defer from 'p-defer' import pWaitFor from 'p-wait-for' import sinon from 'sinon' import { Uint8ArrayList } from 'uint8arraylist' -import { RELAY_V2_HOP_CODEC } from '../../src/circuit-relay/constants.js' +import { DEFAULT_DATA_LIMIT, RELAY_V2_HOP_CODEC } from '../../src/circuit-relay/constants.js' import { circuitRelayServer, type CircuitRelayService, circuitRelayTransport } from '../../src/circuit-relay/index.js' import { HopMessage, Status } from '../../src/circuit-relay/pb/index.js' import { identifyService } from '../../src/identify/index.js' -import { createLibp2p } from '../../src/index.js' +import { createLibp2p, type Libp2pOptions } from '../../src/index.js' import { plaintext } from '../../src/insecure/index.js' import { discoveredRelayConfig, doesNotHaveRelay, getRelayAddress, hasRelay, usingAsRelay } from './utils.js' +import type { Components } from '../../src/components.js' import type { Libp2p } from '@libp2p/interface' import type { Connection } from '@libp2p/interface/connection' +async function createClient (options: Libp2pOptions = {}): Promise { + return createLibp2p({ + addresses: { + listen: ['/ip4/127.0.0.1/tcp/0'] + }, + transports: [ + tcp(), + circuitRelayTransport() + ], + streamMuxers: [ + yamux(), + mplex() + ], + connectionEncryption: [ + plaintext() + ], + connectionManager: { + minConnections: 0 + }, + services: { + identify: identifyService() + }, + ...options + }) +} + +async function createRelay (options: Libp2pOptions = {}): Promise> { + return createLibp2p({ + addresses: { + listen: ['/ip4/127.0.0.1/tcp/0'] + }, + transports: [ + tcp(), + circuitRelayTransport() + ], + streamMuxers: [ + yamux(), + mplex() + ], + connectionEncryption: [ + plaintext() + ], + ...options, + services: { + relay: circuitRelayServer(), + identify: identifyService(), + ...(options.services ?? {}) + } + }) +} + +const ECHO_PROTOCOL = '/test/echo/1.0.0' +const echoService = (components: Components): unknown => { + return { + async start () { + await components.registrar.handle(ECHO_PROTOCOL, ({ stream }) => { + void pipe( + stream, stream + ) + }, { + runOnTransientConnection: true + }) + }, + stop () {} + } +} + describe('circuit-relay', () => { describe('flows with 1 listener', () => { let local: Libp2p @@ -34,104 +103,37 @@ describe('circuit-relay', () => { beforeEach(async () => { // create 1 node and 3 relays [local, relay1, relay2, relay3] = await Promise.all([ - createLibp2p({ - connectionManager: { - minConnections: 0 - }, - addresses: { - listen: ['/ip4/127.0.0.1/tcp/0'] - }, + createClient({ transports: [ tcp(), circuitRelayTransport({ discoverRelays: 1 }) - ], - streamMuxers: [ - yamux(), - mplex() - ], - connectionEncryption: [ - plaintext() - ], - services: { - identify: identifyService() - } + ] }), - createLibp2p({ - connectionManager: { - minConnections: 0 - }, - addresses: { - listen: ['/ip4/127.0.0.1/tcp/0'] - }, + createRelay({ transports: [ tcp(), circuitRelayTransport({ discoverRelays: 1 }) - ], - streamMuxers: [ - yamux(), - mplex() - ], - connectionEncryption: [ - plaintext() - ], - services: { - relay: circuitRelayServer(), - identify: identifyService() - } + ] }), - createLibp2p({ - connectionManager: { - minConnections: 0 - }, - addresses: { - listen: ['/ip4/127.0.0.1/tcp/0'] - }, + createRelay({ transports: [ tcp(), circuitRelayTransport({ discoverRelays: 1 }) - ], - streamMuxers: [ - yamux(), - mplex() - ], - connectionEncryption: [ - plaintext() - ], - services: { - relay: circuitRelayServer(), - identify: identifyService() - } + ] }), - createLibp2p({ - connectionManager: { - minConnections: 0 - }, - addresses: { - listen: ['/ip4/127.0.0.1/tcp/0'] - }, + createRelay({ transports: [ tcp(), circuitRelayTransport({ discoverRelays: 1 }) - ], - streamMuxers: [ - yamux(), - mplex() - ], - connectionEncryption: [ - plaintext() - ], - services: { - relay: circuitRelayServer(), - identify: identifyService() - } + ] }) ]) }) @@ -348,113 +350,45 @@ describe('circuit-relay', () => { beforeEach(async () => { [local, remote, relay1, relay2, relay3] = await Promise.all([ - createLibp2p({ - addresses: { - listen: ['/ip4/127.0.0.1/tcp/0'] - }, + createClient({ transports: [ tcp(), circuitRelayTransport({ discoverRelays: 1 }) - ], - streamMuxers: [ - yamux(), - mplex() - ], - connectionEncryption: [ - plaintext() - ], - services: { - identify: identifyService() - } + ] }), - createLibp2p({ - addresses: { - listen: ['/ip4/127.0.0.1/tcp/0'] - }, + createClient({ transports: [ tcp(), circuitRelayTransport({ discoverRelays: 1 }) - ], - streamMuxers: [ - yamux(), - mplex() - ], - connectionEncryption: [ - plaintext() - ], - services: { - identify: identifyService() - } + ] }), - createLibp2p({ - addresses: { - listen: ['/ip4/127.0.0.1/tcp/0'] - }, + createRelay({ transports: [ tcp(), circuitRelayTransport({ discoverRelays: 1 }) - ], - streamMuxers: [ - yamux(), - mplex() - ], - connectionEncryption: [ - plaintext() - ], - services: { - relay: circuitRelayServer(), - identify: identifyService() - } + ] }), - createLibp2p({ - addresses: { - listen: ['/ip4/127.0.0.1/tcp/0'] - }, + createRelay({ transports: [ tcp(), circuitRelayTransport({ discoverRelays: 1 }) - ], - streamMuxers: [ - yamux(), - mplex() - ], - connectionEncryption: [ - plaintext() - ], - services: { - relay: circuitRelayServer(), - identify: identifyService() - } + ] }), - createLibp2p({ - addresses: { - listen: ['/ip4/127.0.0.1/tcp/0'] - }, + createRelay({ transports: [ tcp(), circuitRelayTransport({ discoverRelays: 1 }) - ], - streamMuxers: [ - yamux(), - mplex() - ], - connectionEncryption: [ - plaintext() - ], - services: { - relay: circuitRelayServer(), - identify: identifyService() - } + ] }) ]) }) @@ -470,17 +404,11 @@ describe('circuit-relay', () => { it('should not add listener to a already relayed connection', async () => { // Relay 1 discovers Relay 3 and connect - await relay1.peerStore.merge(relay3.peerId, { - multiaddrs: relay3.getMultiaddrs() - }) - await relay1.dial(relay3.peerId) + await relay1.dial(relay3.getMultiaddrs()) await usingAsRelay(relay1, relay3) // Relay 2 discovers Relay 3 and connect - await relay2.peerStore.merge(relay3.peerId, { - multiaddrs: relay3.getMultiaddrs() - }) - await relay2.dial(relay3.peerId) + await relay2.dial(relay3.getMultiaddrs()) await usingAsRelay(relay2, relay3) // Relay 1 discovers Relay 2 relayed multiaddr via Relay 3 @@ -770,72 +698,29 @@ describe('circuit-relay', () => { beforeEach(async () => { [local, remote, relay] = await Promise.all([ - createLibp2p({ - addresses: { - listen: ['/ip4/127.0.0.1/tcp/0'] - }, + createClient({ transports: [ tcp(), circuitRelayTransport({ discoverRelays: 1 }) - ], - streamMuxers: [ - yamux(), - mplex() - ], - connectionEncryption: [ - plaintext() - ], - services: { - identify: identifyService() - } + ] }), - createLibp2p({ - addresses: { - listen: ['/ip4/127.0.0.1/tcp/0'] - }, + createClient({ transports: [ tcp(), circuitRelayTransport({ discoverRelays: 1 }) - ], - streamMuxers: [ - yamux(), - mplex() - ], - connectionEncryption: [ - plaintext() - ], - services: { - identify: identifyService() - } + ] }), - createLibp2p({ - addresses: { - listen: ['/ip4/127.0.0.1/tcp/0'] - }, - transports: [ - tcp(), - circuitRelayTransport({ - discoverRelays: 1 - }) - ], - streamMuxers: [ - yamux(), - mplex() - ], - connectionEncryption: [ - plaintext() - ], + createRelay({ services: { relay: circuitRelayServer({ reservations: { defaultDataLimit: 1024n } - }), - identify: identifyService() + }) } }) ]) @@ -901,72 +786,29 @@ describe('circuit-relay', () => { beforeEach(async () => { [local, remote, relay] = await Promise.all([ - createLibp2p({ - addresses: { - listen: ['/ip4/127.0.0.1/tcp/0'] - }, + createClient({ transports: [ tcp(), circuitRelayTransport({ discoverRelays: 1 }) - ], - streamMuxers: [ - yamux(), - mplex() - ], - connectionEncryption: [ - plaintext() - ], - services: { - identify: identifyService() - } + ] }), - createLibp2p({ - addresses: { - listen: ['/ip4/127.0.0.1/tcp/0'] - }, + createClient({ transports: [ tcp(), circuitRelayTransport({ discoverRelays: 1 }) - ], - streamMuxers: [ - yamux(), - mplex() - ], - connectionEncryption: [ - plaintext() - ], - services: { - identify: identifyService() - } + ] }), - createLibp2p({ - addresses: { - listen: ['/ip4/127.0.0.1/tcp/0'] - }, - transports: [ - tcp(), - circuitRelayTransport({ - discoverRelays: 1 - }) - ], - streamMuxers: [ - yamux(), - mplex() - ], - connectionEncryption: [ - plaintext() - ], + createRelay({ services: { relay: circuitRelayServer({ reservations: { defaultDurationLimit: 1000 } - }), - identify: identifyService() + }) } }) ]) @@ -1034,66 +876,15 @@ describe('circuit-relay', () => { let relay: Libp2p<{ relay: CircuitRelayService }> beforeEach(async () => { - relay = await createLibp2p({ - addresses: { - listen: ['/ip4/127.0.0.1/tcp/0'] - }, - transports: [ - tcp(), - circuitRelayTransport() - ], - streamMuxers: [ - yamux(), - mplex() - ], - connectionEncryption: [ - plaintext() - ], - services: { - relay: circuitRelayServer(), - identify: identifyService() - } - }) + relay = await createRelay() ;[local, remote] = await Promise.all([ - createLibp2p({ - addresses: { - listen: ['/ip4/127.0.0.1/tcp/0'] - }, - transports: [ - tcp(), - circuitRelayTransport() - ], - streamMuxers: [ - yamux(), - mplex() - ], - connectionEncryption: [ - plaintext() - ], - services: { - identify: identifyService() - } - }), - createLibp2p({ + createClient(), + createClient({ addresses: { listen: [ `${relay.getMultiaddrs()[0].toString()}/p2p-circuit` ] - }, - transports: [ - tcp(), - circuitRelayTransport() - ], - streamMuxers: [ - yamux(), - mplex() - ], - connectionEncryption: [ - plaintext() - ], - services: { - identify: identifyService() } }) ]) @@ -1121,66 +912,64 @@ describe('circuit-relay', () => { let relay: Libp2p<{ relay: CircuitRelayService }> beforeEach(async () => { - relay = await createLibp2p({ - addresses: { - listen: ['/ip4/127.0.0.1/tcp/0'] - }, - transports: [ - tcp(), - circuitRelayTransport() - ], - streamMuxers: [ - yamux(), - mplex() - ], - connectionEncryption: [ - plaintext() - ], + relay = await createRelay() + + ;[local, remote] = await Promise.all([ + createClient(), + createClient({ + addresses: { + listen: [ + `${relay.getMultiaddrs()[0].toString().split('/p2p')[0]}/p2p-circuit` + ] + } + }) + ]) + }) + + afterEach(async () => { + // Stop each node + await Promise.all([local, remote, relay].map(async libp2p => { + if (libp2p != null) { + await libp2p.stop() + } + })) + }) + + it('should be able to dial remote on preconfigured relay address', async () => { + const ma = getRelayAddress(remote) + + await expect(local.dial(ma)).to.eventually.be.ok() + }) + }) + + describe('unlimited relay', () => { + let local: Libp2p + let remote: Libp2p + let relay: Libp2p<{ relay: CircuitRelayService }> + const defaultDurationLimit = 100 + + beforeEach(async () => { + relay = await createRelay({ services: { - relay: circuitRelayServer(), - identify: identifyService() + relay: circuitRelayServer({ + reservations: { + defaultDurationLimit, + applyDefaultLimit: false + } + }) } }) ;[local, remote] = await Promise.all([ - createLibp2p({ - addresses: { - listen: ['/ip4/127.0.0.1/tcp/0'] - }, - transports: [ - tcp(), - circuitRelayTransport() - ], - streamMuxers: [ - yamux(), - mplex() - ], - connectionEncryption: [ - plaintext() - ], - services: { - identify: identifyService() - } - }), - createLibp2p({ + createClient(), + createClient({ addresses: { listen: [ `${relay.getMultiaddrs()[0].toString().split('/p2p')[0]}/p2p-circuit` ] }, - transports: [ - tcp(), - circuitRelayTransport() - ], - streamMuxers: [ - yamux(), - mplex() - ], - connectionEncryption: [ - plaintext() - ], services: { - identify: identifyService() + echoService } }) ]) @@ -1195,10 +984,59 @@ describe('circuit-relay', () => { })) }) - it('should be able to dial remote on preconfigured relay address', async () => { + it('should not apply a data limit', async () => { const ma = getRelayAddress(remote) - await expect(local.dial(ma)).to.eventually.be.ok() + const stream = await local.dialProtocol(ma, ECHO_PROTOCOL, { + runOnTransientConnection: true + }) + + // write more than the default data limit + const data = new Uint8Array(Number(DEFAULT_DATA_LIMIT * 2n)) + + const result = await pipe( + [data], + stream, + async (source) => new Uint8ArrayList(...(await all(source))) + ) + + expect(result.subarray()).to.equalBytes(data) + }) + + it('should not apply a time limit', async () => { + const ma = getRelayAddress(remote) + + const stream = await local.dialProtocol(ma, ECHO_PROTOCOL, { + runOnTransientConnection: true + }) + + let finished = false + + setTimeout(() => { + finished = true + }, defaultDurationLimit * 5) + + const start = Date.now() + let finish = 0 + + await pipe( + async function * () { + while (true) { + yield new Uint8Array() + await delay(10) + + if (finished) { + finish = Date.now() + break + } + } + }, + stream + ) + + // default time limit is set to 100ms so the stream should have been open + // for longer than that + expect(finish - start).to.be.greaterThan(defaultDurationLimit) }) }) }) diff --git a/packages/libp2p/test/circuit-relay/utils.spec.ts b/packages/libp2p/test/circuit-relay/utils.spec.ts index b2d8cb436b..71e0b8b2e4 100644 --- a/packages/libp2p/test/circuit-relay/utils.spec.ts +++ b/packages/libp2p/test/circuit-relay/utils.spec.ts @@ -201,7 +201,7 @@ describe('circuit-relay utils', () => { createLimitedRelay(localStream, remoteStream, controller.signal, limit) - expect(await toBuffer(received)).to.have.property('byteLength', 8) + expect(await toBuffer(received)).to.have.property('byteLength', 12) expect(localStreamAbortSpy).to.have.property('called', true) expect(remoteStreamAbortSpy).to.have.property('called', true) }) diff --git a/packages/libp2p/test/connection-manager/dial-queue.spec.ts b/packages/libp2p/test/connection-manager/dial-queue.spec.ts index b2f44e7483..2505ac2950 100644 --- a/packages/libp2p/test/connection-manager/dial-queue.spec.ts +++ b/packages/libp2p/test/connection-manager/dial-queue.spec.ts @@ -199,7 +199,8 @@ describe('dial queue', () => { const controller = new AbortController() dialer = new DialQueue(components, { - maxParallelDials: 2 + maxParallelDials: 2, + maxParallelDialsPerPeer: 10 }) components.transportManager.transportForMultiaddr.returns(stubInterface()) @@ -268,7 +269,8 @@ describe('dial queue', () => { }) dialer = new DialQueue(components, { - maxParallelDials: 50 + maxParallelDials: 50, + maxParallelDialsPerPeer: 10 }) await expect(dialer.dial(Object.keys(actions).map(str => multiaddr(str)))).to.eventually.equal(connection2) diff --git a/packages/libp2p/test/connection-manager/direct.node.ts b/packages/libp2p/test/connection-manager/direct.node.ts index c18a5147f6..6da375ad6b 100644 --- a/packages/libp2p/test/connection-manager/direct.node.ts +++ b/packages/libp2p/test/connection-manager/direct.node.ts @@ -230,7 +230,8 @@ describe('dialing (direct, TCP)', () => { ] const dialer = new DialQueue(localComponents, { - maxParallelDials: 2 + maxParallelDials: 2, + maxParallelDialsPerPeer: 10 }) const deferredDial = pDefer() @@ -268,7 +269,9 @@ describe('dialing (direct, TCP)', () => { multiaddrs: addrs }) - const dialer = new DialQueue(localComponents) + const dialer = new DialQueue(localComponents, { + maxParallelDialsPerPeer: 10 + }) const transportManagerDialStub = sinon.stub(localTM, 'dial') transportManagerDialStub.callsFake(async (ma) => { diff --git a/packages/libp2p/test/connection-manager/direct.spec.ts b/packages/libp2p/test/connection-manager/direct.spec.ts index 70c2232b62..780c45f605 100644 --- a/packages/libp2p/test/connection-manager/direct.spec.ts +++ b/packages/libp2p/test/connection-manager/direct.spec.ts @@ -8,7 +8,7 @@ import { mplex } from '@libp2p/mplex' import { peerIdFromString } from '@libp2p/peer-id' import { createEd25519PeerId } from '@libp2p/peer-id-factory' import { PersistentPeerStore } from '@libp2p/peer-store' -import { publicAddressesFirst } from '@libp2p/utils/address-sort' +import { defaultAddressSort } from '@libp2p/utils/address-sort' import { webSockets } from '@libp2p/websockets' import * as filters from '@libp2p/websockets/filters' import { multiaddr } from '@multiformats/multiaddr' @@ -193,12 +193,13 @@ describe('dialing (direct, WebSockets)', () => { multiaddr('/ip4/30.0.0.1/tcp/15001/ws') ] - const publicAddressesFirstSpy = sinon.spy(publicAddressesFirst) + const addressesSorttSpy = sinon.spy(defaultAddressSort) const localTMDialStub = sinon.stub(localTM, 'dial').callsFake(async (ma) => mockConnection(mockMultiaddrConnection(mockDuplex(), remoteComponents.peerId))) connectionManager = new DefaultConnectionManager(localComponents, { - addressSorter: publicAddressesFirstSpy, - maxParallelDials: 3 + addressSorter: addressesSorttSpy, + maxParallelDials: 3, + maxParallelDialsPerPeer: 3 }) await connectionManager.start() @@ -212,7 +213,7 @@ describe('dialing (direct, WebSockets)', () => { const sortedAddresses = peerMultiaddrs .map((m) => ({ multiaddr: m, isCertified: false })) - .sort(publicAddressesFirst) + .sort(defaultAddressSort) expect(localTMDialStub.getCall(0).args[0].equals(sortedAddresses[0].multiaddr)) expect(localTMDialStub.getCall(1).args[0].equals(sortedAddresses[1].multiaddr)) @@ -272,7 +273,9 @@ describe('dialing (direct, WebSockets)', () => { multiaddrs: addrs }) - connectionManager = new DefaultConnectionManager(localComponents) + connectionManager = new DefaultConnectionManager(localComponents, { + maxParallelDialsPerPeer: 10 + }) await connectionManager.start() const transactionManagerDialStub = sinon.stub(localTM, 'dial') diff --git a/packages/libp2p/test/fetch/index.spec.ts b/packages/libp2p/test/fetch/index.spec.ts index 97126f138c..9ef87c88e5 100644 --- a/packages/libp2p/test/fetch/index.spec.ts +++ b/packages/libp2p/test/fetch/index.spec.ts @@ -135,7 +135,7 @@ describe('fetch', () => { await expect(localFetch.fetch(remoteComponents.peerId, key, { signal })) - .to.eventually.be.rejected.with.property('code', 'ABORT_ERR') + .to.eventually.be.rejected.with.property('code', 'ERR_TIMEOUT') // should have closed stream expect(newStreamSpy).to.have.property('callCount', 1) diff --git a/packages/libp2p/test/ping/index.spec.ts b/packages/libp2p/test/ping/index.spec.ts index 174f236f76..c37d5d0ebe 100644 --- a/packages/libp2p/test/ping/index.spec.ts +++ b/packages/libp2p/test/ping/index.spec.ts @@ -125,7 +125,7 @@ describe('ping', () => { await expect(localPing.ping(remoteComponents.peerId, { signal })) - .to.eventually.be.rejected.with.property('code', 'ABORT_ERR') + .to.eventually.be.rejected.with.property('code', 'ERR_TIMEOUT') // should have closed stream expect(newStreamSpy).to.have.property('callCount', 1) diff --git a/packages/libp2p/test/registrar/registrar.spec.ts b/packages/libp2p/test/registrar/registrar.spec.ts index 6dc954395f..0e5b585acf 100644 --- a/packages/libp2p/test/registrar/registrar.spec.ts +++ b/packages/libp2p/test/registrar/registrar.spec.ts @@ -297,6 +297,10 @@ describe('registrar', () => { await libp2p.unhandle(['/echo/1.0.0']) expect(registrar.getProtocols()).to.not.have.any.keys(['/echo/1.0.0']) expect(registrar.getHandler('/echo/1.0.1')).to.have.property('handler', echoHandler) + + await expect(libp2p.peerStore.get(libp2p.peerId)).to.eventually.have.deep.property('protocols', [ + '/echo/1.0.1' + ]) }) }) }) diff --git a/packages/libp2p/test/upgrading/upgrader.spec.ts b/packages/libp2p/test/upgrading/upgrader.spec.ts index 1ec2290f46..f1343f128e 100644 --- a/packages/libp2p/test/upgrading/upgrader.spec.ts +++ b/packages/libp2p/test/upgrading/upgrader.spec.ts @@ -279,6 +279,44 @@ describe('Upgrader', () => { }) }) + it('should clear timeout if upgrade is successful', async () => { + const { inbound, outbound } = mockMultiaddrConnPair({ addrs, remotePeer }) + + localUpgrader = new DefaultUpgrader(localComponents, { + connectionEncryption: [ + plaintext()() + ], + muxers: [ + yamux()() + ], + inboundUpgradeTimeout: 1000 + }) + remoteUpgrader = new DefaultUpgrader(remoteComponents, { + connectionEncryption: [ + plaintext()() + ], + muxers: [ + yamux()() + ], + 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 }) diff --git a/packages/logger/CHANGELOG.md b/packages/logger/CHANGELOG.md index 6b6ab8aab0..fbe63635f4 100644 --- a/packages/logger/CHANGELOG.md +++ b/packages/logger/CHANGELOG.md @@ -5,6 +5,17 @@ * specify updated formatter for multiaddrs ([#36](https://github.com/libp2p/js-libp2p-logger/issues/36)) ([abaefb4](https://github.com/libp2p/js-libp2p-logger/commit/abaefb490a0d9464a23b422d9fc5b80051532d10)) +### [3.0.3](https://www.github.com/libp2p/js-libp2p/compare/logger-v3.0.2...logger-v3.0.3) (2023-10-06) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @libp2p/interface bumped from ^0.1.2 to ^0.1.3 + * devDependencies + * @libp2p/peer-id bumped from ^3.0.2 to ^3.0.3 + ### [3.0.2](https://www.github.com/libp2p/js-libp2p/compare/logger-v3.0.1...logger-v3.0.2) (2023-08-14) @@ -234,4 +245,4 @@ ### Features -* add tracked-map ([#156](https://github.com/libp2p/js-libp2p-interfaces/issues/156)) ([c17730f](https://github.com/libp2p/js-libp2p-interfaces/commit/c17730f8bca172db85507740eaba81b3cf514d04)) \ No newline at end of file +* add tracked-map ([#156](https://github.com/libp2p/js-libp2p-interfaces/issues/156)) ([c17730f](https://github.com/libp2p/js-libp2p-interfaces/commit/c17730f8bca172db85507740eaba81b3cf514d04)) diff --git a/packages/logger/package.json b/packages/logger/package.json index aa89cb1307..38babeb080 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -1,6 +1,6 @@ { "name": "@libp2p/logger", - "version": "3.0.2", + "version": "3.0.3", "description": "A logging component for use in js-libp2p modules", "license": "Apache-2.0 OR MIT", "homepage": "https://github.com/libp2p/js-libp2p/tree/master/packages/logger#readme", @@ -31,6 +31,7 @@ "eslintConfig": { "extends": "ipfs", "parserOptions": { + "project": true, "sourceType": "module" } }, @@ -48,17 +49,17 @@ "test:electron-main": "aegir test -t electron-main" }, "dependencies": { - "@libp2p/interface": "^0.1.2", + "@libp2p/interface": "^0.1.3", "@multiformats/multiaddr": "^12.1.5", "debug": "^4.3.4", "interface-datastore": "^8.2.0", "multiformats": "^12.0.1" }, "devDependencies": { - "@libp2p/peer-id": "^3.0.2", + "@libp2p/peer-id": "^3.0.3", "@types/debug": "^4.1.7", - "aegir": "^40.0.8", - "sinon": "^15.1.2", + "aegir": "^41.0.2", + "sinon": "^16.0.0", "uint8arrays": "^4.0.4" } } diff --git a/packages/logger/src/index.ts b/packages/logger/src/index.ts index 9597aa813a..87f1354d50 100644 --- a/packages/logger/src/index.ts +++ b/packages/logger/src/index.ts @@ -44,8 +44,8 @@ debug.formatters.a = (v?: Multiaddr): string => { export interface Logger { (formatter: any, ...args: any[]): void - error: (formatter: any, ...args: any[]) => void - trace: (formatter: any, ...args: any[]) => void + error(formatter: any, ...args: any[]): void + trace(formatter: any, ...args: any[]): void enabled: boolean } diff --git a/packages/metrics-prometheus/CHANGELOG.md b/packages/metrics-prometheus/CHANGELOG.md index fba4b8bfd9..f8ec58da18 100644 --- a/packages/metrics-prometheus/CHANGELOG.md +++ b/packages/metrics-prometheus/CHANGELOG.md @@ -5,6 +5,47 @@ * move prom-client to deps ([#32](https://github.com/libp2p/js-libp2p-prometheus-metrics/issues/32)) ([73acad0](https://github.com/libp2p/js-libp2p-prometheus-metrics/commit/73acad0a20a9a0ad024cd47a53f154668dbae77b)) +### [2.0.8](https://www.github.com/libp2p/js-libp2p/compare/prometheus-metrics-v2.0.7...prometheus-metrics-v2.0.8) (2023-10-06) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @libp2p/interface bumped from ^0.1.2 to ^0.1.3 + * @libp2p/logger bumped from ^3.0.2 to ^3.0.3 + * devDependencies + * @libp2p/interface-compliance-tests bumped from ^4.1.0 to ^4.1.1 + * @libp2p/peer-id-factory bumped from ^3.0.4 to ^3.0.5 + +### [2.0.7](https://www.github.com/libp2p/js-libp2p/compare/prometheus-metrics-v2.0.6...prometheus-metrics-v2.0.7) (2023-10-01) + + +### Dependencies + +* The following workspace dependencies were updated + * devDependencies + * @libp2p/interface-compliance-tests bumped from ^4.0.6 to ^4.1.0 + +### [2.0.6](https://www.github.com/libp2p/js-libp2p/compare/prometheus-metrics-v2.0.5...prometheus-metrics-v2.0.6) (2023-09-15) + + +### Dependencies + +* The following workspace dependencies were updated + * devDependencies + * @libp2p/interface-compliance-tests bumped from ^4.0.5 to ^4.0.6 + * @libp2p/peer-id-factory bumped from ^3.0.3 to ^3.0.4 + +### [2.0.5](https://www.github.com/libp2p/js-libp2p/compare/prometheus-metrics-v2.0.4...prometheus-metrics-v2.0.5) (2023-08-25) + + +### Dependencies + +* The following workspace dependencies were updated + * devDependencies + * @libp2p/interface-compliance-tests bumped from ^4.0.4 to ^4.0.5 + ### [2.0.4](https://www.github.com/libp2p/js-libp2p/compare/prometheus-metrics-v2.0.3...prometheus-metrics-v2.0.4) (2023-08-16) diff --git a/packages/metrics-prometheus/package.json b/packages/metrics-prometheus/package.json index 62c85d22ab..87fd6acc4a 100644 --- a/packages/metrics-prometheus/package.json +++ b/packages/metrics-prometheus/package.json @@ -1,6 +1,6 @@ { "name": "@libp2p/prometheus-metrics", - "version": "2.0.4", + "version": "2.0.8", "description": "Collect libp2p metrics for scraping by Prometheus or Graphana", "author": "", "license": "Apache-2.0 OR MIT", @@ -29,6 +29,7 @@ "eslintConfig": { "extends": "ipfs", "parserOptions": { + "project": true, "sourceType": "module" } }, @@ -42,17 +43,17 @@ "test:electron-main": "aegir test -t electron-main --cov" }, "dependencies": { - "@libp2p/interface": "^0.1.2", - "@libp2p/logger": "^3.0.2", + "@libp2p/interface": "^0.1.3", + "@libp2p/logger": "^3.0.3", "it-foreach": "^2.0.3", "it-stream-types": "^2.0.1", "prom-client": "^14.2.0" }, "devDependencies": { - "@libp2p/interface-compliance-tests": "^4.0.4", - "@libp2p/peer-id-factory": "^3.0.3", + "@libp2p/interface-compliance-tests": "^4.1.1", + "@libp2p/peer-id-factory": "^3.0.5", "@multiformats/multiaddr": "^12.1.3", - "aegir": "^40.0.8", + "aegir": "^41.0.2", "it-drain": "^3.0.2", "it-pipe": "^3.0.1", "p-defer": "^4.0.0" diff --git a/packages/metrics-prometheus/src/utils.ts b/packages/metrics-prometheus/src/utils.ts index 4e70930df8..83e12cb52c 100644 --- a/packages/metrics-prometheus/src/utils.ts +++ b/packages/metrics-prometheus/src/utils.ts @@ -1,7 +1,7 @@ import type { CalculateMetric } from '@libp2p/interface/metrics' export interface CalculatedMetric { - addCalculator: (calculator: CalculateMetric) => void + addCalculator(calculator: CalculateMetric): void } export const ONE_SECOND = 1000 diff --git a/packages/multistream-select/CHANGELOG.md b/packages/multistream-select/CHANGELOG.md index bf3d3aed88..b15826cbc0 100644 --- a/packages/multistream-select/CHANGELOG.md +++ b/packages/multistream-select/CHANGELOG.md @@ -11,6 +11,16 @@ * **dev:** bump aegir from 38.1.8 to 39.0.10 ([#70](https://github.com/libp2p/js-libp2p-multistream-select/issues/70)) ([f87b1c3](https://github.com/libp2p/js-libp2p-multistream-select/commit/f87b1c3505934ebeed6eff018af8d3042e7e6e06)) +### [4.0.3](https://www.github.com/libp2p/js-libp2p/compare/multistream-select-v4.0.2...multistream-select-v4.0.3) (2023-10-06) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @libp2p/interface bumped from ^0.1.2 to ^0.1.3 + * @libp2p/logger bumped from ^3.0.2 to ^3.0.3 + ### [4.0.2](https://www.github.com/libp2p/js-libp2p/compare/multistream-select-v4.0.1...multistream-select-v4.0.2) (2023-08-14) @@ -249,4 +259,4 @@ ### Bug Fixes -* add multistream-select and update pubsub types ([#170](https://github.com/libp2p/js-libp2p-interfaces/issues/170)) ([b9ecb2b](https://github.com/libp2p/js-libp2p-interfaces/commit/b9ecb2bee8f2abc0c41bfcf7bf2025894e37ddc2)) \ No newline at end of file +* add multistream-select and update pubsub types ([#170](https://github.com/libp2p/js-libp2p-interfaces/issues/170)) ([b9ecb2b](https://github.com/libp2p/js-libp2p-interfaces/commit/b9ecb2bee8f2abc0c41bfcf7bf2025894e37ddc2)) diff --git a/packages/multistream-select/package.json b/packages/multistream-select/package.json index 90248337a5..0d095536ad 100644 --- a/packages/multistream-select/package.json +++ b/packages/multistream-select/package.json @@ -1,6 +1,6 @@ { "name": "@libp2p/multistream-select", - "version": "4.0.2", + "version": "4.0.3", "description": "JavaScript implementation of multistream-select", "license": "Apache-2.0 OR MIT", "homepage": "https://github.com/libp2p/js-libp2p/tree/master/packages/multistream-select#readme", @@ -35,6 +35,7 @@ "eslintConfig": { "extends": "ipfs", "parserOptions": { + "project": true, "sourceType": "module" } }, @@ -52,8 +53,8 @@ "test:electron-main": "aegir test -t electron-main" }, "dependencies": { - "@libp2p/interface": "^0.1.2", - "@libp2p/logger": "^3.0.2", + "@libp2p/interface": "^0.1.3", + "@libp2p/logger": "^3.0.3", "abortable-iterator": "^5.0.1", "it-first": "^3.0.1", "it-handshake": "^4.1.3", @@ -68,7 +69,7 @@ "uint8arrays": "^4.0.6" }, "devDependencies": { - "aegir": "^40.0.8", + "aegir": "^41.0.2", "iso-random-stream": "^2.0.2", "it-all": "^3.0.1", "it-map": "^3.0.3", diff --git a/packages/peer-collections/CHANGELOG.md b/packages/peer-collections/CHANGELOG.md index 747f876693..43cda9ddb9 100644 --- a/packages/peer-collections/CHANGELOG.md +++ b/packages/peer-collections/CHANGELOG.md @@ -11,6 +11,27 @@ * **dev:** bump aegir from 38.1.8 to 39.0.10 ([#36](https://github.com/libp2p/js-libp2p-peer-collections/issues/36)) ([9fa3de6](https://github.com/libp2p/js-libp2p-peer-collections/commit/9fa3de6d85dbe1ade54fda86b597ed9ffe6d71d5)) +### [4.0.5](https://www.github.com/libp2p/js-libp2p/compare/peer-collections-v4.0.4...peer-collections-v4.0.5) (2023-10-06) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @libp2p/interface bumped from ^0.1.2 to ^0.1.3 + * @libp2p/peer-id bumped from ^3.0.2 to ^3.0.3 + * devDependencies + * @libp2p/peer-id-factory bumped from ^3.0.4 to ^3.0.5 + +### [4.0.4](https://www.github.com/libp2p/js-libp2p/compare/peer-collections-v4.0.3...peer-collections-v4.0.4) (2023-09-15) + + +### Dependencies + +* The following workspace dependencies were updated + * devDependencies + * @libp2p/peer-id-factory bumped from ^3.0.3 to ^3.0.4 + ### [4.0.3](https://www.github.com/libp2p/js-libp2p/compare/peer-collections-v4.0.2...peer-collections-v4.0.3) (2023-08-14) @@ -176,4 +197,4 @@ ### Bug Fixes -* simplify transport interface, update interfaces for use with libp2p ([#180](https://github.com/libp2p/js-libp2p-interfaces/issues/180)) ([ec81622](https://github.com/libp2p/js-libp2p-interfaces/commit/ec81622e5b7c6d256e0f8aed6d3695642473293b)) \ No newline at end of file +* simplify transport interface, update interfaces for use with libp2p ([#180](https://github.com/libp2p/js-libp2p-interfaces/issues/180)) ([ec81622](https://github.com/libp2p/js-libp2p-interfaces/commit/ec81622e5b7c6d256e0f8aed6d3695642473293b)) diff --git a/packages/peer-collections/package.json b/packages/peer-collections/package.json index fde2ced12e..615fd30416 100644 --- a/packages/peer-collections/package.json +++ b/packages/peer-collections/package.json @@ -1,6 +1,6 @@ { "name": "@libp2p/peer-collections", - "version": "4.0.3", + "version": "4.0.5", "description": "Stores values against a peer id", "license": "Apache-2.0 OR MIT", "homepage": "https://github.com/libp2p/js-libp2p/tree/master/packages/peer-collections#readme", @@ -31,6 +31,7 @@ "eslintConfig": { "extends": "ipfs", "parserOptions": { + "project": true, "sourceType": "module" } }, @@ -48,11 +49,11 @@ "test:electron-main": "aegir test -t electron-main" }, "dependencies": { - "@libp2p/interface": "^0.1.2", - "@libp2p/peer-id": "^3.0.2" + "@libp2p/interface": "^0.1.3", + "@libp2p/peer-id": "^3.0.3" }, "devDependencies": { - "@libp2p/peer-id-factory": "^3.0.3", - "aegir": "^40.0.8" + "@libp2p/peer-id-factory": "^3.0.5", + "aegir": "^41.0.2" } } diff --git a/packages/peer-discovery-bootstrap/CHANGELOG.md b/packages/peer-discovery-bootstrap/CHANGELOG.md index 42ff497060..b2a127a3ef 100644 --- a/packages/peer-discovery-bootstrap/CHANGELOG.md +++ b/packages/peer-discovery-bootstrap/CHANGELOG.md @@ -9,6 +9,46 @@ * update @libp2p/interface-peer-discovery to 2.0.0 ([#176](https://github.com/libp2p/js-libp2p-bootstrap/issues/176)) ([1954e75](https://github.com/libp2p/js-libp2p-bootstrap/commit/1954e75fa4b1e6b3b42f885f663f989fd0e422ab)) +### [9.0.8](https://www.github.com/libp2p/js-libp2p/compare/bootstrap-v9.0.7...bootstrap-v9.0.8) (2023-10-06) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @libp2p/interface bumped from ^0.1.2 to ^0.1.3 + * @libp2p/logger bumped from ^3.0.2 to ^3.0.3 + * @libp2p/peer-id bumped from ^3.0.2 to ^3.0.3 + * devDependencies + * @libp2p/interface-compliance-tests bumped from ^4.1.0 to ^4.1.1 + +### [9.0.7](https://www.github.com/libp2p/js-libp2p/compare/bootstrap-v9.0.6...bootstrap-v9.0.7) (2023-10-01) + + +### Dependencies + +* The following workspace dependencies were updated + * devDependencies + * @libp2p/interface-compliance-tests bumped from ^4.0.6 to ^4.1.0 + +### [9.0.6](https://www.github.com/libp2p/js-libp2p/compare/bootstrap-v9.0.5...bootstrap-v9.0.6) (2023-09-15) + + +### Dependencies + +* The following workspace dependencies were updated + * devDependencies + * @libp2p/interface-compliance-tests bumped from ^4.0.5 to ^4.0.6 + +### [9.0.5](https://www.github.com/libp2p/js-libp2p/compare/bootstrap-v9.0.4...bootstrap-v9.0.5) (2023-08-25) + + +### Dependencies + +* The following workspace dependencies were updated + * devDependencies + * @libp2p/interface-compliance-tests bumped from ^4.0.4 to ^4.0.5 + ### [9.0.4](https://www.github.com/libp2p/js-libp2p/compare/bootstrap-v9.0.3...bootstrap-v9.0.4) (2023-08-16) diff --git a/packages/peer-discovery-bootstrap/package.json b/packages/peer-discovery-bootstrap/package.json index 192b4da7ff..7961f87940 100644 --- a/packages/peer-discovery-bootstrap/package.json +++ b/packages/peer-discovery-bootstrap/package.json @@ -1,6 +1,6 @@ { "name": "@libp2p/bootstrap", - "version": "9.0.4", + "version": "9.0.8", "description": "Peer discovery via a list of bootstrap peers", "license": "Apache-2.0 OR MIT", "homepage": "https://github.com/libp2p/js-libp2p/tree/master/packages/peer-discovery-bootstrap#readme", @@ -31,6 +31,7 @@ "eslintConfig": { "extends": "ipfs", "parserOptions": { + "project": true, "sourceType": "module" } }, @@ -48,15 +49,15 @@ "test:electron-main": "aegir test -t electron-main" }, "dependencies": { - "@libp2p/interface": "^0.1.2", - "@libp2p/logger": "^3.0.2", - "@libp2p/peer-id": "^3.0.2", + "@libp2p/interface": "^0.1.3", + "@libp2p/logger": "^3.0.3", + "@libp2p/peer-id": "^3.0.3", "@multiformats/mafmt": "^12.1.2", "@multiformats/multiaddr": "^12.1.5" }, "devDependencies": { - "@libp2p/interface-compliance-tests": "^4.0.4", - "aegir": "^40.0.8", + "@libp2p/interface-compliance-tests": "^4.1.1", + "aegir": "^41.0.2", "sinon-ts": "^1.0.0" } } diff --git a/packages/peer-discovery-mdns/CHANGELOG.md b/packages/peer-discovery-mdns/CHANGELOG.md index d1ac447d88..3eb7ebe38b 100644 --- a/packages/peer-discovery-mdns/CHANGELOG.md +++ b/packages/peer-discovery-mdns/CHANGELOG.md @@ -9,6 +9,67 @@ * update @libp2p/interface-peer-discovery to 2.0.0 ([#197](https://github.com/libp2p/js-libp2p-mdns/issues/197)) ([e8172af](https://github.com/libp2p/js-libp2p-mdns/commit/e8172af8b9856a934327195238b00e5fbba436a4)) +### [9.0.10](https://www.github.com/libp2p/js-libp2p/compare/mdns-v9.0.9...mdns-v9.0.10) (2023-10-06) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @libp2p/interface bumped from ^0.1.2 to ^0.1.3 + * @libp2p/logger bumped from ^3.0.2 to ^3.0.3 + * @libp2p/peer-id bumped from ^3.0.2 to ^3.0.3 + * @libp2p/utils bumped from ^4.0.3 to ^4.0.4 + * devDependencies + * @libp2p/interface-compliance-tests bumped from ^4.1.0 to ^4.1.1 + * @libp2p/interface-internal bumped from ^0.1.5 to ^0.1.6 + * @libp2p/peer-id-factory bumped from ^3.0.4 to ^3.0.5 + +### [9.0.9](https://www.github.com/libp2p/js-libp2p/compare/mdns-v9.0.8...mdns-v9.0.9) (2023-10-01) + + +### Dependencies + +* The following workspace dependencies were updated + * devDependencies + * @libp2p/interface-compliance-tests bumped from ^4.0.6 to ^4.1.0 + +### [9.0.8](https://www.github.com/libp2p/js-libp2p/compare/mdns-v9.0.7...mdns-v9.0.8) (2023-09-15) + + +### Dependencies + +* The following workspace dependencies were updated + * devDependencies + * @libp2p/interface-compliance-tests bumped from ^4.0.5 to ^4.0.6 + * @libp2p/interface-internal bumped from ^0.1.4 to ^0.1.5 + * @libp2p/peer-id-factory bumped from ^3.0.3 to ^3.0.4 + +### [9.0.7](https://www.github.com/libp2p/js-libp2p/compare/mdns-v9.0.6...mdns-v9.0.7) (2023-09-10) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @libp2p/utils bumped from ^4.0.2 to ^4.0.3 + +### [9.0.6](https://www.github.com/libp2p/js-libp2p/compare/mdns-v9.0.5...mdns-v9.0.6) (2023-09-01) + + +### Bug Fixes + +* **@libp2p/mdns:** do not send TXT records that are too long ([#2014](https://www.github.com/libp2p/js-libp2p/issues/2014)) ([4f19234](https://www.github.com/libp2p/js-libp2p/commit/4f19234ecd7701795543715dbadf537f5c2f1ccb)), closes [#2012](https://www.github.com/libp2p/js-libp2p/issues/2012) + +### [9.0.5](https://www.github.com/libp2p/js-libp2p/compare/mdns-v9.0.4...mdns-v9.0.5) (2023-08-25) + + +### Dependencies + +* The following workspace dependencies were updated + * devDependencies + * @libp2p/interface-compliance-tests bumped from ^4.0.4 to ^4.0.5 + ### [9.0.4](https://www.github.com/libp2p/js-libp2p/compare/mdns-v9.0.3...mdns-v9.0.4) (2023-08-16) diff --git a/packages/peer-discovery-mdns/README.md b/packages/peer-discovery-mdns/README.md index 69be7f9185..be587d8220 100644 --- a/packages/peer-discovery-mdns/README.md +++ b/packages/peer-discovery-mdns/README.md @@ -48,30 +48,30 @@ async function start () { - options - `peerName` - Peer name to announce (should not be peeer id), default random string - `multiaddrs` - multiaddrs to announce - - `broadcast` - (true/false) announce our presence through mDNS, default `false` + - `broadcast` - (true/false) announce our presence through mDNS, default `true` - `interval` - query interval, default 10 \* 1000 (10 seconds) - - `serviceTag` - name of the service announce , default 'ipfs.local\` + - `serviceTag` - name of the service announce , default '_p2p._udp.local\` ## MDNS messages -A query is sent to discover the IPFS nodes on the local network +A query is sent to discover the libp2p nodes on the local network ```js { type: 'query', - questions: [ { name: 'ipfs.local', type: 'PTR' } ] + questions: [ { name: '_p2p._udp.local', type: 'PTR' } ] } ``` -When a query is detected, each IPFS node sends an answer about itself +When a query is detected, each libp2p node sends an answer about itself ```js - [ { name: 'ipfs.local', + [ { name: '_p2p._udp.local', type: 'PTR', class: 'IN', ttl: 120, - data: 'QmNPubsDWATVngE3d5WDSNe7eVrFLuk38qb9t6vdLnu2aK.ipfs.local' }, - { name: 'QmNPubsDWATVngE3d5WDSNe7eVrFLuk38qb9t6vdLnu2aK.ipfs.local', + data: 'QmNPubsDWATVngE3d5WDSNe7eVrFLuk38qb9t6vdLnu2aK._p2p._udp.local' }, + { name: 'QmNPubsDWATVngE3d5WDSNe7eVrFLuk38qb9t6vdLnu2aK._p2p._udp.local', type: 'SRV', class: 'IN', ttl: 120, @@ -80,7 +80,7 @@ When a query is detected, each IPFS node sends an answer about itself weight: 1, port: '20002', target: 'LAPTOP-G5LJ7VN9' } }, - { name: 'QmNPubsDWATVngE3d5WDSNe7eVrFLuk38qb9t6vdLnu2aK.ipfs.local', + { name: 'QmNPubsDWATVngE3d5WDSNe7eVrFLuk38qb9t6vdLnu2aK._p2p._udp.local', type: 'TXT', class: 'IN', ttl: 120, diff --git a/packages/peer-discovery-mdns/package.json b/packages/peer-discovery-mdns/package.json index 6da5de78b3..f362c70f69 100644 --- a/packages/peer-discovery-mdns/package.json +++ b/packages/peer-discovery-mdns/package.json @@ -1,6 +1,6 @@ { "name": "@libp2p/mdns", - "version": "9.0.4", + "version": "9.0.10", "description": "Node.js libp2p mDNS discovery implementation for peer discovery", "license": "Apache-2.0 OR MIT", "homepage": "https://github.com/libp2p/js-libp2p/tree/master/packages/peer-discovery-mdns#readme", @@ -31,6 +31,7 @@ "eslintConfig": { "extends": "ipfs", "parserOptions": { + "project": true, "sourceType": "module" } }, @@ -44,19 +45,20 @@ "test:electron-main": "aegir test -t electron-main" }, "dependencies": { - "@libp2p/interface": "^0.1.2", - "@libp2p/logger": "^3.0.2", - "@libp2p/peer-id": "^3.0.2", + "@libp2p/interface": "^0.1.3", + "@libp2p/logger": "^3.0.3", + "@libp2p/peer-id": "^3.0.3", + "@libp2p/utils": "^4.0.4", "@multiformats/multiaddr": "^12.1.5", "@types/multicast-dns": "^7.2.1", "dns-packet": "^5.4.0", "multicast-dns": "^7.2.5" }, "devDependencies": { - "@libp2p/interface-compliance-tests": "^4.0.4", - "@libp2p/interface-internal": "^0.1.4", - "@libp2p/peer-id-factory": "^3.0.3", - "aegir": "^40.0.8", + "@libp2p/interface-compliance-tests": "^4.1.1", + "@libp2p/interface-internal": "^0.1.6", + "@libp2p/peer-id-factory": "^3.0.5", + "aegir": "^41.0.2", "p-wait-for": "^5.0.2", "ts-sinon": "^2.0.2" } diff --git a/packages/peer-discovery-mdns/src/index.ts b/packages/peer-discovery-mdns/src/index.ts index 4f2f36ab89..7424131c06 100644 --- a/packages/peer-discovery-mdns/src/index.ts +++ b/packages/peer-discovery-mdns/src/index.ts @@ -6,6 +6,7 @@ import * as query from './query.js' import { stringGen } from './utils.js' import type { PeerDiscovery, PeerDiscoveryEvents } from '@libp2p/interface/peer-discovery' import type { PeerInfo } from '@libp2p/interface/peer-info' +import type { Startable } from '@libp2p/interface/src/startable.js' import type { AddressManager } from '@libp2p/interface-internal/address-manager' const log = logger('libp2p:mdns') @@ -23,7 +24,7 @@ export interface MulticastDNSComponents { addressManager: AddressManager } -class MulticastDNS extends EventEmitter implements PeerDiscovery { +class MulticastDNS extends EventEmitter implements PeerDiscovery, Startable { public mdns?: multicastDNS.MulticastDNS private readonly broadcast: boolean @@ -50,9 +51,10 @@ class MulticastDNS extends EventEmitter implements PeerDisc this.port = init.port ?? 5353 this.components = components this._queryInterval = null - this._onPeer = this._onPeer.bind(this) this._onMdnsQuery = this._onMdnsQuery.bind(this) this._onMdnsResponse = this._onMdnsResponse.bind(this) + this._onMdnsWarning = this._onMdnsWarning.bind(this) + this._onMdnsError = this._onMdnsError.bind(this) } readonly [peerDiscovery] = this @@ -76,6 +78,8 @@ class MulticastDNS extends EventEmitter implements PeerDisc this.mdns = multicastDNS({ port: this.port, ip: this.ip }) this.mdns.on('query', this._onMdnsQuery) this.mdns.on('response', this._onMdnsResponse) + this.mdns.on('warning', this._onMdnsWarning) + this.mdns.on('error', this._onMdnsError) this._queryInterval = query.queryLAN(this.mdns, this.serviceTag, this.interval) } @@ -113,14 +117,12 @@ class MulticastDNS extends EventEmitter implements PeerDisc } } - _onPeer (evt: CustomEvent): void { - if (this.mdns == null) { - return - } + _onMdnsWarning (err: Error): void { + log.error('mdns warning', err) + } - this.dispatchEvent(new CustomEvent('peer', { - detail: evt.detail - })) + _onMdnsError (err: Error): void { + log.error('mdns error', err) } /** @@ -135,6 +137,8 @@ class MulticastDNS extends EventEmitter implements PeerDisc this.mdns.removeListener('query', this._onMdnsQuery) this.mdns.removeListener('response', this._onMdnsResponse) + this.mdns.removeListener('warning', this._onMdnsWarning) + this.mdns.removeListener('error', this._onMdnsError) if (this._queryInterval != null) { clearInterval(this._queryInterval) diff --git a/packages/peer-discovery-mdns/src/query.ts b/packages/peer-discovery-mdns/src/query.ts index 2bc97b262c..bb6aa8ba30 100644 --- a/packages/peer-discovery-mdns/src/query.ts +++ b/packages/peer-discovery-mdns/src/query.ts @@ -1,6 +1,7 @@ import { logger } from '@libp2p/logger' import { peerIdFromString } from '@libp2p/peer-id' -import { multiaddr, type Multiaddr } from '@multiformats/multiaddr' +import { isPrivate } from '@libp2p/utils/multiaddr/is-private' +import { multiaddr, type Multiaddr, protocols } from '@multiformats/multiaddr' import type { PeerInfo } from '@libp2p/interface/peer-info' import type { Answer, StringAnswer, TxtAnswer } from 'dns-packet' import type { MulticastDNS, QueryPacket, ResponsePacket } from 'multicast-dns' @@ -9,7 +10,7 @@ const log = logger('libp2p:mdns:query') export function queryLAN (mdns: MulticastDNS, serviceTag: string, interval: number): ReturnType { const query = (): void => { - log('query', serviceTag) + log.trace('query', serviceTag) mdns.query({ questions: [{ @@ -40,6 +41,16 @@ export function gotResponse (rsp: ResponsePacket, localPeerName: string, service } }) + // according to the spec, peer details should be in the additional records, + // not the answers though it seems go-libp2p at least ignores this? + // https://github.com/libp2p/specs/blob/master/discovery/mdns.md#response + rsp.additionals.forEach((answer) => { + switch (answer.type) { + case 'TXT': txtAnswers.push(answer); break + default: break + } + }) + if (answerPTR == null || answerPTR?.name !== serviceTag || txtAnswers.length === 0 || @@ -63,7 +74,7 @@ export function gotResponse (rsp: ResponsePacket, localPeerName: string, service return { id: peerIdFromString(peerId), - multiaddrs, + multiaddrs: multiaddrs.map(addr => addr.decapsulateCode(protocols('p2p').code)), protocols: [] } } catch (e) { @@ -92,20 +103,45 @@ export function gotQuery (qry: QueryPacket, mdns: MulticastDNS, peerName: string data: peerName + '.' + serviceTag }) - multiaddrs.forEach((addr) => { - // spec mandates multiaddr contains peer id - if (addr.getPeerId() != null) { + multiaddrs + // mDNS requires link-local addresses only + // https://github.com/libp2p/specs/blob/master/discovery/mdns.md#issues + .filter(isLinkLocal) + .forEach((addr) => { + const data = 'dnsaddr=' + addr.toString() + + // TXT record fields have a max data length of 255 bytes + // see 6.1 - https://www.ietf.org/rfc/rfc6763.txt + if (data.length > 255) { + log('multiaddr %a is too long to use in mDNS query response', addr) + return + } + + // spec mandates multiaddr contains peer id + if (addr.getPeerId() == null) { + log('multiaddr %a did not have a peer ID so cannot be used in mDNS query response', addr) + return + } + answers.push({ name: peerName + '.' + serviceTag, type: 'TXT', class: 'IN', ttl: 120, - data: 'dnsaddr=' + addr.toString() + data }) - } - }) + }) - log('responding to query') + log.trace('responding to query') mdns.respond(answers) } } + +function isLinkLocal (ma: Multiaddr): boolean { + // match private ip4/ip6 & loopback addresses + if (isPrivate(ma)) { + return true + } + + return false +} diff --git a/packages/peer-discovery-mdns/test/compliance.spec.ts b/packages/peer-discovery-mdns/test/compliance.spec.ts index a823f9ed35..b24d590d91 100644 --- a/packages/peer-discovery-mdns/test/compliance.spec.ts +++ b/packages/peer-discovery-mdns/test/compliance.spec.ts @@ -1,6 +1,7 @@ /* eslint-env mocha */ import { CustomEvent } from '@libp2p/interface/events' +import { isStartable } from '@libp2p/interface/startable' import tests from '@libp2p/interface-compliance-tests/peer-discovery' import { createEd25519PeerId } from '@libp2p/peer-id-factory' import { multiaddr } from '@multiformats/multiaddr' @@ -32,16 +33,21 @@ describe('compliance tests', () => { }) // Trigger discovery - const maStr = '/ip4/127.0.0.1/tcp/15555/ws/p2p-webrtc-star/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSooo2d' - - // @ts-expect-error not a PeerDiscovery field - intervalId = setInterval(() => discovery._onPeer(new CustomEvent('peer', { - detail: { - id: peerId2, - multiaddrs: [multiaddr(maStr)], - protocols: [] + const maStr = '/ip4/127.0.0.1/tcp/15555/ws/p2p-webrtc-star' + + intervalId = setInterval(() => { + if (isStartable(discovery) && !discovery.isStarted()) { + return } - })), 1000) + + discovery.dispatchEvent(new CustomEvent('peer', { + detail: { + id: peerId2, + multiaddrs: [multiaddr(maStr)], + protocols: [] + } + })) + }, 1000) return discovery }, diff --git a/packages/peer-discovery-mdns/test/multicast-dns.spec.ts b/packages/peer-discovery-mdns/test/multicast-dns.spec.ts index 3ddf5c8ea9..9955002dd7 100644 --- a/packages/peer-discovery-mdns/test/multicast-dns.spec.ts +++ b/packages/peer-discovery-mdns/test/multicast-dns.spec.ts @@ -38,25 +38,25 @@ describe('MulticastDNS', () => { ]) aMultiaddrs = [ - multiaddr('/ip4/127.0.0.1/tcp/20001'), + multiaddr('/ip4/192.168.1.142/tcp/20001'), multiaddr('/dns4/webrtc-star.discovery.libp2p.io/tcp/443/wss/p2p-webrtc-star'), multiaddr('/dns4/discovery.libp2p.io/tcp/8443') ] bMultiaddrs = [ - multiaddr('/ip4/127.0.0.1/tcp/20002'), - multiaddr('/ip6/::1/tcp/20002'), + multiaddr('/ip4/192.168.1.143/tcp/20002'), + multiaddr('/ip6/2604:1380:4602:5c00::3/tcp/20002'), multiaddr('/dnsaddr/discovery.libp2p.io') ] cMultiaddrs = [ - multiaddr('/ip4/127.0.0.1/tcp/20003'), - multiaddr('/ip4/127.0.0.1/tcp/30003/ws'), + multiaddr('/ip4/192.168.1.144/tcp/20003'), + multiaddr('/ip4/192.168.1.144/tcp/30003/ws'), multiaddr('/dns4/discovery.libp2p.io') ] dMultiaddrs = [ - multiaddr('/ip4/127.0.0.1/tcp/30003/ws') + multiaddr('/ip4/192.168.1.145/tcp/30003/ws') ] }) @@ -110,7 +110,8 @@ describe('MulticastDNS', () => { await pWaitFor(() => peers.has(expectedPeer)) mdnsA.removeEventListener('peer', foundPeer) - expect(peers.get(expectedPeer).multiaddrs.length).to.equal(3) + // everything except loopback + expect(peers.get(expectedPeer).multiaddrs.length).to.equal(2) await stop(mdnsA, mdnsB, mdnsD) }) @@ -141,15 +142,6 @@ describe('MulticastDNS', () => { await stop(mdnsC) }) - it('should start and stop with go-libp2p-mdns compat', async () => { - const mdnsA = mdns({ - port: 50004 - })(getComponents(pA, aMultiaddrs)) - - await start(mdnsA) - await stop(mdnsA) - }) - it('should not emit undefined peer ids', async () => { const mdnsA = mdns({ port: 50004 @@ -219,4 +211,80 @@ describe('MulticastDNS', () => { await stop(mdnsA, mdnsB) }) + + it('only includes link-local addresses', async function () { + this.timeout(40 * 1000) + + // these are not link-local addresses + const publicAddress = '/ip4/48.52.76.32/tcp/1234' + const relayDnsAddress = `/dnsaddr/example.org/tcp/1234/p2p/${pD.toString()}/p2p-circuit` + const dnsAddress = '/dns4/example.org/tcp/1234' + + // this address is too long to fit in a TXT record + const longAddress = `/ip4/192.168.1.142/udp/4001/quic-v1/webtransport/certhash/uEiDils3hWFJmsWOJIoMPxAcpzlyFNxTDZpklIoB8643ddw/certhash/uEiAM4BGr4OMK3O9cFGwfbNc4J7XYnsKE5wNPKKaTLa4fkw/p2p/${pD.toString()}/p2p-circuit` + + // these are link-local addresses + const relayAddress = `/ip4/192.168.1.142/tcp/1234/p2p/${pD.toString()}/p2p-circuit` + const localAddress = '/ip4/192.168.1.123/tcp/1234' + const localWsAddress = '/ip4/192.168.1.123/tcp/1234/ws' + + // these are not link-local but go-libp2p advertises loopback addresses even + // though you shouldn't for mDNS + const loopbackAddress = '/ip4/127.0.0.1/tcp/1234' + const loopbackAddress6 = '/ip6/::1/tcp/1234' + + const mdnsA = mdns({ + broadcast: false, // do not talk to ourself + port: 50005, + ip: '224.0.0.252' + })(getComponents(pA, aMultiaddrs)) + + const mdnsB = mdns({ + port: 50005, // port must be the same + ip: '224.0.0.252' // ip must be the same + })(getComponents(pB, [ + multiaddr(publicAddress), + multiaddr(relayAddress), + multiaddr(relayDnsAddress), + multiaddr(localAddress), + multiaddr(loopbackAddress), + multiaddr(loopbackAddress6), + multiaddr(dnsAddress), + multiaddr(longAddress), + multiaddr(localWsAddress) + ])) + + await start(mdnsA, mdnsB) + + const { detail: { id, multiaddrs } } = await new Promise>((resolve) => { + mdnsA.addEventListener('peer', resolve, { + once: true + }) + }) + + expect(pB.toString()).to.eql(id.toString()) + + ;[ + publicAddress, + relayDnsAddress, + dnsAddress, + longAddress + ].forEach(addr => { + expect(multiaddrs.map(ma => ma.toString())) + .to.not.include(addr) + }) + + ;[ + relayAddress, + localAddress, + localWsAddress, + loopbackAddress, + loopbackAddress6 + ].forEach(addr => { + expect(multiaddrs.map(ma => ma.toString())) + .to.include(addr) + }) + + await stop(mdnsA, mdnsB) + }) }) diff --git a/packages/peer-id-factory/CHANGELOG.md b/packages/peer-id-factory/CHANGELOG.md index d92fdc7ce0..dba517b519 100644 --- a/packages/peer-id-factory/CHANGELOG.md +++ b/packages/peer-id-factory/CHANGELOG.md @@ -5,6 +5,26 @@ * update README.md ([#59](https://github.com/libp2p/js-libp2p-peer-id/issues/59)) ([aba6483](https://github.com/libp2p/js-libp2p-peer-id/commit/aba6483dad028ee5c24bfc01135b77568666cfd3)) +### [3.0.5](https://www.github.com/libp2p/js-libp2p/compare/peer-id-factory-v3.0.4...peer-id-factory-v3.0.5) (2023-10-06) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @libp2p/crypto bumped from ^2.0.4 to ^2.0.5 + * @libp2p/interface bumped from ^0.1.2 to ^0.1.3 + * @libp2p/peer-id bumped from ^3.0.2 to ^3.0.3 + +### [3.0.4](https://www.github.com/libp2p/js-libp2p/compare/peer-id-factory-v3.0.3...peer-id-factory-v3.0.4) (2023-09-15) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @libp2p/crypto bumped from ^2.0.3 to ^2.0.4 + ### [3.0.3](https://www.github.com/libp2p/js-libp2p/compare/peer-id-factory-v3.0.2...peer-id-factory-v3.0.3) (2023-08-14) @@ -273,4 +293,4 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline ### Features -* simpler peer id ([#117](https://github.com/libp2p/js-libp2p-interfaces/issues/117)) ([fa2c4f5](https://github.com/libp2p/js-libp2p-interfaces/commit/fa2c4f5be74a5cfc11489771881e57b4e53bf174)) \ No newline at end of file +* simpler peer id ([#117](https://github.com/libp2p/js-libp2p-interfaces/issues/117)) ([fa2c4f5](https://github.com/libp2p/js-libp2p-interfaces/commit/fa2c4f5be74a5cfc11489771881e57b4e53bf174)) diff --git a/packages/peer-id-factory/package.json b/packages/peer-id-factory/package.json index bc5b4079df..d2ca5bdb2f 100644 --- a/packages/peer-id-factory/package.json +++ b/packages/peer-id-factory/package.json @@ -1,6 +1,6 @@ { "name": "@libp2p/peer-id-factory", - "version": "3.0.3", + "version": "3.0.5", "description": "Create PeerId instances", "license": "Apache-2.0 OR MIT", "homepage": "https://github.com/libp2p/js-libp2p/tree/master/packages/peer-id-factory#readme", @@ -31,6 +31,7 @@ "eslintConfig": { "extends": "ipfs", "parserOptions": { + "project": true, "sourceType": "module" }, "ignorePatterns": [ @@ -52,16 +53,16 @@ "test:electron-main": "aegir test -t electron-main" }, "dependencies": { - "@libp2p/crypto": "^2.0.3", - "@libp2p/interface": "^0.1.2", - "@libp2p/peer-id": "^3.0.2", + "@libp2p/crypto": "^2.0.5", + "@libp2p/interface": "^0.1.3", + "@libp2p/peer-id": "^3.0.3", "multiformats": "^12.0.1", "protons-runtime": "^5.0.0", "uint8arraylist": "^2.4.3", "uint8arrays": "^4.0.6" }, "devDependencies": { - "aegir": "^40.0.8", + "aegir": "^41.0.2", "protons": "^7.0.2" } } diff --git a/packages/peer-id/CHANGELOG.md b/packages/peer-id/CHANGELOG.md index 4de3584f15..986cefe7ae 100644 --- a/packages/peer-id/CHANGELOG.md +++ b/packages/peer-id/CHANGELOG.md @@ -5,6 +5,15 @@ * update README.md ([#59](https://github.com/libp2p/js-libp2p-peer-id/issues/59)) ([aba6483](https://github.com/libp2p/js-libp2p-peer-id/commit/aba6483dad028ee5c24bfc01135b77568666cfd3)) +### [3.0.3](https://www.github.com/libp2p/js-libp2p/compare/peer-id-v3.0.2...peer-id-v3.0.3) (2023-10-06) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @libp2p/interface bumped from ^0.1.2 to ^0.1.3 + ### [3.0.2](https://www.github.com/libp2p/js-libp2p/compare/peer-id-v3.0.1...peer-id-v3.0.2) (2023-08-14) @@ -280,4 +289,4 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline ### Features -* simpler peer id ([#117](https://github.com/libp2p/js-libp2p-interfaces/issues/117)) ([fa2c4f5](https://github.com/libp2p/js-libp2p-interfaces/commit/fa2c4f5be74a5cfc11489771881e57b4e53bf174)) \ No newline at end of file +* simpler peer id ([#117](https://github.com/libp2p/js-libp2p-interfaces/issues/117)) ([fa2c4f5](https://github.com/libp2p/js-libp2p-interfaces/commit/fa2c4f5be74a5cfc11489771881e57b4e53bf174)) diff --git a/packages/peer-id/package.json b/packages/peer-id/package.json index 41d3e7ab5b..bb910d9888 100644 --- a/packages/peer-id/package.json +++ b/packages/peer-id/package.json @@ -1,6 +1,6 @@ { "name": "@libp2p/peer-id", - "version": "3.0.2", + "version": "3.0.3", "description": "Implementation of @libp2p/interface-peer-id", "license": "Apache-2.0 OR MIT", "homepage": "https://github.com/libp2p/js-libp2p/tree/master/packages/peer-id#readme", @@ -31,6 +31,7 @@ "eslintConfig": { "extends": "ipfs", "parserOptions": { + "project": true, "sourceType": "module" } }, @@ -48,11 +49,11 @@ "test:electron-main": "aegir test -t electron-main" }, "dependencies": { - "@libp2p/interface": "^0.1.2", + "@libp2p/interface": "^0.1.3", "multiformats": "^12.0.1", "uint8arrays": "^4.0.6" }, "devDependencies": { - "aegir": "^40.0.8" + "aegir": "^41.0.2" } } diff --git a/packages/peer-record/CHANGELOG.md b/packages/peer-record/CHANGELOG.md index 205f4b052b..69281e709b 100644 --- a/packages/peer-record/CHANGELOG.md +++ b/packages/peer-record/CHANGELOG.md @@ -11,6 +11,40 @@ * **dev:** bump aegir from 38.1.8 to 39.0.10 ([#64](https://github.com/libp2p/js-libp2p-peer-record/issues/64)) ([ba3ac38](https://github.com/libp2p/js-libp2p-peer-record/commit/ba3ac38c79e9449a75c0a54fefe289ee9e2c78fb)) +### [6.0.6](https://www.github.com/libp2p/js-libp2p/compare/peer-record-v6.0.5...peer-record-v6.0.6) (2023-10-06) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @libp2p/crypto bumped from ^2.0.4 to ^2.0.5 + * @libp2p/interface bumped from ^0.1.2 to ^0.1.3 + * @libp2p/peer-id bumped from ^3.0.2 to ^3.0.3 + * @libp2p/utils bumped from ^4.0.3 to ^4.0.4 + * devDependencies + * @libp2p/peer-id-factory bumped from ^3.0.4 to ^3.0.5 + +### [6.0.5](https://www.github.com/libp2p/js-libp2p/compare/peer-record-v6.0.4...peer-record-v6.0.5) (2023-09-15) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @libp2p/crypto bumped from ^2.0.3 to ^2.0.4 + * devDependencies + * @libp2p/peer-id-factory bumped from ^3.0.3 to ^3.0.4 + +### [6.0.4](https://www.github.com/libp2p/js-libp2p/compare/peer-record-v6.0.3...peer-record-v6.0.4) (2023-09-10) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @libp2p/utils bumped from ^4.0.2 to ^4.0.3 + ### [6.0.3](https://www.github.com/libp2p/js-libp2p/compare/peer-record-v6.0.2...peer-record-v6.0.3) (2023-08-14) @@ -327,4 +361,4 @@ ### Features -* add peer store/records, and streams are just streams ([#160](https://github.com/libp2p/js-libp2p-interfaces/issues/160)) ([8860a0c](https://github.com/libp2p/js-libp2p-interfaces/commit/8860a0cd46b359a5648402d83870f7ff957222fe)) \ No newline at end of file +* add peer store/records, and streams are just streams ([#160](https://github.com/libp2p/js-libp2p-interfaces/issues/160)) ([8860a0c](https://github.com/libp2p/js-libp2p-interfaces/commit/8860a0cd46b359a5648402d83870f7ff957222fe)) diff --git a/packages/peer-record/package.json b/packages/peer-record/package.json index f1a3c3ce77..a7110a82b4 100644 --- a/packages/peer-record/package.json +++ b/packages/peer-record/package.json @@ -1,6 +1,6 @@ { "name": "@libp2p/peer-record", - "version": "6.0.3", + "version": "6.0.6", "description": "Used to transfer signed peer data across the network", "license": "Apache-2.0 OR MIT", "homepage": "https://github.com/libp2p/js-libp2p/tree/master/packages/peer-record#readme", @@ -31,6 +31,7 @@ "eslintConfig": { "extends": "ipfs", "parserOptions": { + "project": true, "sourceType": "module" }, "ignorePatterns": [ @@ -55,10 +56,10 @@ "test:electron-main": "aegir test -t electron-main" }, "dependencies": { - "@libp2p/crypto": "^2.0.3", - "@libp2p/interface": "^0.1.2", - "@libp2p/peer-id": "^3.0.2", - "@libp2p/utils": "^4.0.2", + "@libp2p/crypto": "^2.0.5", + "@libp2p/interface": "^0.1.3", + "@libp2p/peer-id": "^3.0.3", + "@libp2p/utils": "^4.0.4", "@multiformats/multiaddr": "^12.1.5", "protons-runtime": "^5.0.0", "uint8-varint": "^2.0.0", @@ -66,8 +67,8 @@ "uint8arrays": "^4.0.6" }, "devDependencies": { - "@libp2p/peer-id-factory": "^3.0.3", - "aegir": "^40.0.8", + "@libp2p/peer-id-factory": "^3.0.5", + "aegir": "^41.0.2", "protons": "^7.0.2" } } diff --git a/packages/peer-store/CHANGELOG.md b/packages/peer-store/CHANGELOG.md index e66347fd5c..0996044df8 100644 --- a/packages/peer-store/CHANGELOG.md +++ b/packages/peer-store/CHANGELOG.md @@ -11,6 +11,40 @@ * **dev:** bump p-event from 5.0.1 to 6.0.0 ([#89](https://github.com/libp2p/js-libp2p-peer-store/issues/89)) ([9d96700](https://github.com/libp2p/js-libp2p-peer-store/commit/9d9670048b5e8feeac656cba92cb2e513e4a77be)) +### [9.0.6](https://www.github.com/libp2p/js-libp2p/compare/peer-store-v9.0.5...peer-store-v9.0.6) (2023-10-06) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @libp2p/interface bumped from ^0.1.2 to ^0.1.3 + * @libp2p/logger bumped from ^3.0.2 to ^3.0.3 + * @libp2p/peer-collections bumped from ^4.0.4 to ^4.0.5 + * @libp2p/peer-id bumped from ^3.0.2 to ^3.0.3 + * @libp2p/peer-id-factory bumped from ^3.0.4 to ^3.0.5 + * @libp2p/peer-record bumped from ^6.0.5 to ^6.0.6 + +### [9.0.5](https://www.github.com/libp2p/js-libp2p/compare/peer-store-v9.0.4...peer-store-v9.0.5) (2023-09-15) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @libp2p/peer-collections bumped from ^4.0.3 to ^4.0.4 + * @libp2p/peer-id-factory bumped from ^3.0.3 to ^3.0.4 + * @libp2p/peer-record bumped from ^6.0.4 to ^6.0.5 + +### [9.0.4](https://www.github.com/libp2p/js-libp2p/compare/peer-store-v9.0.3...peer-store-v9.0.4) (2023-09-10) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @libp2p/peer-record bumped from ^6.0.3 to ^6.0.4 + ### [9.0.3](https://www.github.com/libp2p/js-libp2p/compare/peer-store-v9.0.2...peer-store-v9.0.3) (2023-08-14) @@ -464,4 +498,4 @@ Co-authored-by: Alex Potsides ### Features -* add peer store/records, and streams are just streams ([#160](https://github.com/libp2p/js-libp2p-interfaces/issues/160)) ([8860a0c](https://github.com/libp2p/js-libp2p-interfaces/commit/8860a0cd46b359a5648402d83870f7ff957222fe)) \ No newline at end of file +* add peer store/records, and streams are just streams ([#160](https://github.com/libp2p/js-libp2p-interfaces/issues/160)) ([8860a0c](https://github.com/libp2p/js-libp2p-interfaces/commit/8860a0cd46b359a5648402d83870f7ff957222fe)) diff --git a/packages/peer-store/package.json b/packages/peer-store/package.json index 2e6adc3692..74350542c4 100644 --- a/packages/peer-store/package.json +++ b/packages/peer-store/package.json @@ -1,6 +1,6 @@ { "name": "@libp2p/peer-store", - "version": "9.0.3", + "version": "9.0.6", "description": "Stores information about peers libp2p knows on the network", "license": "Apache-2.0 OR MIT", "homepage": "https://github.com/libp2p/js-libp2p/tree/master/packages/peer-store#readme", @@ -31,6 +31,7 @@ "eslintConfig": { "extends": "ipfs", "parserOptions": { + "project": true, "sourceType": "module" }, "ignorePatterns": [ @@ -53,12 +54,12 @@ "test:electron-main": "aegir test -t electron-main" }, "dependencies": { - "@libp2p/interface": "^0.1.2", - "@libp2p/logger": "^3.0.2", - "@libp2p/peer-collections": "^4.0.3", - "@libp2p/peer-id": "^3.0.2", - "@libp2p/peer-id-factory": "^3.0.3", - "@libp2p/peer-record": "^6.0.3", + "@libp2p/interface": "^0.1.3", + "@libp2p/logger": "^3.0.3", + "@libp2p/peer-collections": "^4.0.5", + "@libp2p/peer-id": "^3.0.3", + "@libp2p/peer-id-factory": "^3.0.5", + "@libp2p/peer-record": "^6.0.6", "@multiformats/multiaddr": "^12.1.5", "interface-datastore": "^8.2.0", "it-all": "^3.0.2", @@ -70,12 +71,12 @@ }, "devDependencies": { "@types/sinon": "^10.0.15", - "aegir": "^40.0.8", + "aegir": "^41.0.2", "datastore-core": "^9.1.1", "delay": "^6.0.0", "p-defer": "^4.0.0", "p-event": "^6.0.0", "protons": "^7.0.2", - "sinon": "^15.1.2" + "sinon": "^16.0.0" } } diff --git a/packages/peer-store/src/utils/to-peer-pb.ts b/packages/peer-store/src/utils/to-peer-pb.ts index a3a3c92763..7270ad6f40 100644 --- a/packages/peer-store/src/utils/to-peer-pb.ts +++ b/packages/peer-store/src/utils/to-peer-pb.ts @@ -153,8 +153,8 @@ export async function toPeerPB (peerId: PeerId, data: Partial, strateg } interface CreateSortedMapOptions { - validate: (key: string, value: V) => void - map?: (key: string, value: V) => R + validate(key: string, value: V): void + map?(key: string, value: V): R } /** diff --git a/packages/protocol-perf/CHANGELOG.md b/packages/protocol-perf/CHANGELOG.md index fb273e4880..edfb9ed294 100644 --- a/packages/protocol-perf/CHANGELOG.md +++ b/packages/protocol-perf/CHANGELOG.md @@ -1,5 +1,108 @@ # Changelog +### [1.1.11](https://www.github.com/libp2p/js-libp2p/compare/perf-v1.1.10...perf-v1.1.11) (2023-10-10) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * libp2p bumped from ^0.46.13 to ^0.46.14 + +### [1.1.10](https://www.github.com/libp2p/js-libp2p/compare/perf-v1.1.9...perf-v1.1.10) (2023-10-06) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @libp2p/crypto bumped from ^2.0.4 to ^2.0.5 + * @libp2p/interface bumped from ^0.1.2 to ^0.1.3 + * @libp2p/interface-compliance-tests bumped from ^4.1.0 to ^4.1.1 + * @libp2p/interface-internal bumped from ^0.1.5 to ^0.1.6 + * @libp2p/logger bumped from ^3.0.2 to ^3.0.3 + * @libp2p/peer-id-factory bumped from ^3.0.4 to ^3.0.5 + * @libp2p/tcp bumped from ^8.0.8 to ^8.0.9 + * libp2p bumped from ^0.46.12 to ^0.46.13 + +### [1.1.9](https://www.github.com/libp2p/js-libp2p/compare/perf-v1.1.8...perf-v1.1.9) (2023-10-01) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @libp2p/interface-compliance-tests bumped from ^4.0.6 to ^4.1.0 + * @libp2p/tcp bumped from ^8.0.7 to ^8.0.8 + * libp2p bumped from ^0.46.11 to ^0.46.12 + +### [1.1.8](https://www.github.com/libp2p/js-libp2p/compare/perf-v1.1.7...perf-v1.1.8) (2023-09-15) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @libp2p/crypto bumped from ^2.0.3 to ^2.0.4 + * @libp2p/interface-compliance-tests bumped from ^4.0.5 to ^4.0.6 + * @libp2p/interface-internal bumped from ^0.1.4 to ^0.1.5 + * @libp2p/peer-id-factory bumped from ^3.0.3 to ^3.0.4 + * @libp2p/tcp bumped from ^8.0.6 to ^8.0.7 + * libp2p bumped from ^0.46.10 to ^0.46.11 + +### [1.1.7](https://www.github.com/libp2p/js-libp2p/compare/perf-v1.1.6...perf-v1.1.7) (2023-09-10) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @libp2p/tcp bumped from ^8.0.5 to ^8.0.6 + * libp2p bumped from ^0.46.9 to ^0.46.10 + +### [1.1.6](https://www.github.com/libp2p/js-libp2p/compare/perf-v1.1.5...perf-v1.1.6) (2023-09-05) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * libp2p bumped from ^0.46.8 to ^0.46.9 + +### [1.1.5](https://www.github.com/libp2p/js-libp2p/compare/perf-v1.1.4...perf-v1.1.5) (2023-09-01) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * libp2p bumped from ^0.46.7 to ^0.46.8 + +### [1.1.4](https://www.github.com/libp2p/js-libp2p/compare/perf-v1.1.3...perf-v1.1.4) (2023-08-25) + + +### Bug Fixes + +* **@libp2p/protocol-perf:** use noise for encryption ([#1992](https://www.github.com/libp2p/js-libp2p/issues/1992)) ([24c1c24](https://www.github.com/libp2p/js-libp2p/commit/24c1c2489cd58397c4691d382d6260d56791dbce)), closes [#1991](https://www.github.com/libp2p/js-libp2p/issues/1991) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @libp2p/interface-compliance-tests bumped from ^4.0.4 to ^4.0.5 + * @libp2p/tcp bumped from ^8.0.4 to ^8.0.5 + * libp2p bumped from ^0.46.6 to ^0.46.7 + +### [1.1.3](https://www.github.com/libp2p/js-libp2p/compare/perf-v1.1.2...perf-v1.1.3) (2023-08-16) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * libp2p bumped from ^0.46.5 to ^0.46.6 + ### [1.1.2](https://www.github.com/libp2p/js-libp2p/compare/perf-v1.1.1...perf-v1.1.2) (2023-08-16) diff --git a/packages/protocol-perf/package.json b/packages/protocol-perf/package.json index fc92acba0f..6902f8d8f0 100644 --- a/packages/protocol-perf/package.json +++ b/packages/protocol-perf/package.json @@ -1,6 +1,6 @@ { "name": "@libp2p/perf", - "version": "1.1.2", + "version": "1.1.11", "description": "Implementation of Perf Protocol", "author": "@maschad / @marcopolo", "license": "Apache-2.0 OR MIT", @@ -29,6 +29,7 @@ "eslintConfig": { "extends": "ipfs", "parserOptions": { + "project": true, "sourceType": "module" } }, @@ -47,21 +48,22 @@ "renderResults": "node dist/src/renderResults.js" }, "dependencies": { + "@chainsafe/libp2p-noise": "^13.0.0", "@chainsafe/libp2p-yamux": "^5.0.0", - "@libp2p/crypto": "^2.0.3", - "@libp2p/interface": "^0.1.2", - "@libp2p/interface-compliance-tests": "^4.0.4", - "@libp2p/interface-internal": "^0.1.4", + "@libp2p/crypto": "^2.0.5", + "@libp2p/interface": "^0.1.3", + "@libp2p/interface-compliance-tests": "^4.1.1", + "@libp2p/interface-internal": "^0.1.6", "@libp2p/interfaces": "3.3.2", - "@libp2p/logger": "^3.0.2", - "@libp2p/peer-id-factory": "^3.0.3", - "@libp2p/tcp": "^8.0.4", + "@libp2p/logger": "^3.0.3", + "@libp2p/peer-id-factory": "^3.0.5", + "@libp2p/tcp": "^8.0.9", "@multiformats/multiaddr": "^12.1.5", - "libp2p": "^0.46.5", + "libp2p": "^0.46.14", "uint8arrays": "^4.0.6", "yargs": "^17.7.2" }, "devDependencies": { - "aegir": "^40.0.8" + "aegir": "^41.0.2" } } diff --git a/packages/protocol-perf/src/index.ts b/packages/protocol-perf/src/index.ts index 15713cd6ec..c078fd9e23 100644 --- a/packages/protocol-perf/src/index.ts +++ b/packages/protocol-perf/src/index.ts @@ -57,7 +57,7 @@ export const defaultInit: PerfServiceInit = { } export interface PerfService { - measurePerformance: (startTime: number, connection: Connection, sendBytes: bigint, recvBytes: bigint, options?: AbortOptions) => Promise + measurePerformance(startTime: number, connection: Connection, sendBytes: bigint, recvBytes: bigint, options?: AbortOptions): Promise } export interface PerfServiceInit { @@ -145,7 +145,7 @@ class DefaultPerfService implements Startable, PerfService { const writeBlockSize = this.writeBlockSize - const stream = await connection.newStream([this.protocol]) + const stream = await connection.newStream([this.protocol], options) // Convert sendBytes to uint64 big endian buffer const view = new DataView(this.databuf) diff --git a/packages/protocol-perf/src/main.ts b/packages/protocol-perf/src/main.ts index f994fdc015..9033b8896e 100644 --- a/packages/protocol-perf/src/main.ts +++ b/packages/protocol-perf/src/main.ts @@ -1,10 +1,10 @@ +import { noise } from '@chainsafe/libp2p-noise' import { yamux } from '@chainsafe/libp2p-yamux' import { unmarshalPrivateKey } from '@libp2p/crypto/keys' import { createFromPrivKey } from '@libp2p/peer-id-factory' import { tcp } from '@libp2p/tcp' import { multiaddr } from '@multiformats/multiaddr' import { createLibp2p } from 'libp2p' -import { plaintext } from 'libp2p/insecure' import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' import yargs from 'yargs' import { hideBin } from 'yargs/helpers' @@ -57,7 +57,7 @@ export async function main (runServer: boolean, serverIpAddress: string, transpo transports: [tcp()], streamMuxers: [yamux()], connectionEncryption: [ - plaintext() + noise() ], services: { perf: perfService(defaultInit) diff --git a/packages/pubsub-floodsub/CHANGELOG.md b/packages/pubsub-floodsub/CHANGELOG.md index b15d804cb3..178ead29e1 100644 --- a/packages/pubsub-floodsub/CHANGELOG.md +++ b/packages/pubsub-floodsub/CHANGELOG.md @@ -11,6 +11,52 @@ * **dev:** bump aegir from 38.1.8 to 39.0.10 ([#233](https://github.com/libp2p/js-libp2p-floodsub/issues/233)) ([e073298](https://github.com/libp2p/js-libp2p-floodsub/commit/e073298f324a89656b0ca6d9a629e60eaedc7873)) +### [8.0.9](https://www.github.com/libp2p/js-libp2p/compare/floodsub-v8.0.8...floodsub-v8.0.9) (2023-10-06) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @libp2p/interface bumped from ^0.1.2 to ^0.1.3 + * @libp2p/logger bumped from ^3.0.2 to ^3.0.3 + * @libp2p/pubsub bumped from ^8.0.6 to ^8.0.7 + * devDependencies + * @libp2p/interface-compliance-tests bumped from ^4.1.0 to ^4.1.1 + * @libp2p/peer-collections bumped from ^4.0.4 to ^4.0.5 + * @libp2p/peer-id-factory bumped from ^3.0.4 to ^3.0.5 + +### [8.0.8](https://www.github.com/libp2p/js-libp2p/compare/floodsub-v8.0.7...floodsub-v8.0.8) (2023-10-01) + + +### Dependencies + +* The following workspace dependencies were updated + * devDependencies + * @libp2p/interface-compliance-tests bumped from ^4.0.6 to ^4.1.0 + +### [8.0.7](https://www.github.com/libp2p/js-libp2p/compare/floodsub-v8.0.6...floodsub-v8.0.7) (2023-09-15) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @libp2p/pubsub bumped from ^8.0.5 to ^8.0.6 + * devDependencies + * @libp2p/interface-compliance-tests bumped from ^4.0.5 to ^4.0.6 + * @libp2p/peer-collections bumped from ^4.0.3 to ^4.0.4 + * @libp2p/peer-id-factory bumped from ^3.0.3 to ^3.0.4 + +### [8.0.6](https://www.github.com/libp2p/js-libp2p/compare/floodsub-v8.0.5...floodsub-v8.0.6) (2023-08-25) + + +### Dependencies + +* The following workspace dependencies were updated + * devDependencies + * @libp2p/interface-compliance-tests bumped from ^4.0.4 to ^4.0.5 + ### [8.0.5](https://www.github.com/libp2p/js-libp2p/compare/floodsub-v8.0.4...floodsub-v8.0.5) (2023-08-16) diff --git a/packages/pubsub-floodsub/package.json b/packages/pubsub-floodsub/package.json index 8a4303ea24..0099535d9b 100644 --- a/packages/pubsub-floodsub/package.json +++ b/packages/pubsub-floodsub/package.json @@ -1,6 +1,6 @@ { "name": "@libp2p/floodsub", - "version": "8.0.5", + "version": "8.0.9", "description": "libp2p-floodsub, also known as pubsub-flood or just dumbsub, this implementation of pubsub focused on delivering an API for Publish/Subscribe, but with no CastTree Forming (it just floods the network).", "license": "Apache-2.0 OR MIT", "homepage": "https://github.com/libp2p/js-libp2p/tree/master/packages/pubsub-floodsub#readme", @@ -36,6 +36,7 @@ "eslintConfig": { "extends": "ipfs", "parserOptions": { + "project": true, "sourceType": "module" } }, @@ -54,23 +55,23 @@ "test:electron-main": "aegir test -t electron-main" }, "dependencies": { - "@libp2p/interface": "^0.1.2", - "@libp2p/logger": "^3.0.2", - "@libp2p/pubsub": "^8.0.5", + "@libp2p/interface": "^0.1.3", + "@libp2p/logger": "^3.0.3", + "@libp2p/pubsub": "^8.0.7", "protons-runtime": "^5.0.0", "uint8arraylist": "^2.4.3", "uint8arrays": "^4.0.6" }, "devDependencies": { - "@libp2p/interface-compliance-tests": "^4.0.4", - "@libp2p/peer-collections": "^4.0.3", - "@libp2p/peer-id-factory": "^3.0.3", + "@libp2p/interface-compliance-tests": "^4.1.1", + "@libp2p/peer-collections": "^4.0.5", + "@libp2p/peer-id-factory": "^3.0.5", "@multiformats/multiaddr": "^12.1.3", "@types/sinon": "^10.0.15", - "aegir": "^40.0.8", + "aegir": "^41.0.2", "multiformats": "^12.0.1", "p-wait-for": "^5.0.2", "protons": "^7.0.2", - "sinon": "^15.1.2" + "sinon": "^16.0.0" } } diff --git a/packages/pubsub/CHANGELOG.md b/packages/pubsub/CHANGELOG.md index 4692c14d6c..b4847adc19 100644 --- a/packages/pubsub/CHANGELOG.md +++ b/packages/pubsub/CHANGELOG.md @@ -5,6 +5,35 @@ * **dev:** bump delay from 5.0.0 to 6.0.0 ([#144](https://github.com/libp2p/js-libp2p-pubsub/issues/144)) ([1364ce4](https://github.com/libp2p/js-libp2p-pubsub/commit/1364ce41815d3392cfca61169e113cc5414ac2d9)) +### [8.0.7](https://www.github.com/libp2p/js-libp2p/compare/pubsub-v8.0.6...pubsub-v8.0.7) (2023-10-06) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @libp2p/crypto bumped from ^2.0.4 to ^2.0.5 + * @libp2p/interface bumped from ^0.1.2 to ^0.1.3 + * @libp2p/interface-internal bumped from ^0.1.5 to ^0.1.6 + * @libp2p/logger bumped from ^3.0.2 to ^3.0.3 + * @libp2p/peer-collections bumped from ^4.0.4 to ^4.0.5 + * @libp2p/peer-id bumped from ^3.0.2 to ^3.0.3 + * devDependencies + * @libp2p/peer-id-factory bumped from ^3.0.4 to ^3.0.5 + +### [8.0.6](https://www.github.com/libp2p/js-libp2p/compare/pubsub-v8.0.5...pubsub-v8.0.6) (2023-09-15) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @libp2p/crypto bumped from ^2.0.3 to ^2.0.4 + * @libp2p/interface-internal bumped from ^0.1.4 to ^0.1.5 + * @libp2p/peer-collections bumped from ^4.0.3 to ^4.0.4 + * devDependencies + * @libp2p/peer-id-factory bumped from ^3.0.3 to ^3.0.4 + ### [8.0.5](https://www.github.com/libp2p/js-libp2p/compare/pubsub-v8.0.4...pubsub-v8.0.5) (2023-08-16) diff --git a/packages/pubsub/package.json b/packages/pubsub/package.json index 9bb39c291e..baf14e94f9 100644 --- a/packages/pubsub/package.json +++ b/packages/pubsub/package.json @@ -1,6 +1,6 @@ { "name": "@libp2p/pubsub", - "version": "8.0.5", + "version": "8.0.7", "description": "libp2p pubsub base class", "license": "Apache-2.0 OR MIT", "homepage": "https://github.com/libp2p/js-libp2p/tree/master/packages/pubsub#readme", @@ -60,6 +60,7 @@ "eslintConfig": { "extends": "ipfs", "parserOptions": { + "project": true, "sourceType": "module" } }, @@ -78,12 +79,12 @@ "test:electron-main": "aegir test -t electron-main" }, "dependencies": { - "@libp2p/crypto": "^2.0.3", - "@libp2p/interface": "^0.1.2", - "@libp2p/interface-internal": "^0.1.4", - "@libp2p/logger": "^3.0.2", - "@libp2p/peer-collections": "^4.0.3", - "@libp2p/peer-id": "^3.0.2", + "@libp2p/crypto": "^2.0.5", + "@libp2p/interface": "^0.1.3", + "@libp2p/interface-internal": "^0.1.6", + "@libp2p/logger": "^3.0.3", + "@libp2p/peer-collections": "^4.0.5", + "@libp2p/peer-id": "^3.0.3", "abortable-iterator": "^5.0.1", "it-length-prefixed": "^9.0.1", "it-pipe": "^3.0.1", @@ -94,15 +95,15 @@ "uint8arrays": "^4.0.6" }, "devDependencies": { - "@libp2p/peer-id-factory": "^3.0.3", + "@libp2p/peer-id-factory": "^3.0.5", "@types/sinon": "^10.0.15", - "aegir": "^40.0.8", + "aegir": "^41.0.2", "delay": "^6.0.0", "it-pair": "^2.0.6", "p-defer": "^4.0.0", "p-wait-for": "^5.0.2", "protons": "^7.0.2", "protons-runtime": "^5.0.0", - "sinon": "^15.1.2" + "sinon": "^16.0.0" } } diff --git a/packages/stream-multiplexer-mplex/CHANGELOG.md b/packages/stream-multiplexer-mplex/CHANGELOG.md index 42cc084395..5f4a08412a 100644 --- a/packages/stream-multiplexer-mplex/CHANGELOG.md +++ b/packages/stream-multiplexer-mplex/CHANGELOG.md @@ -12,6 +12,45 @@ * **dev:** bump cborg from 1.10.2 to 2.0.1 ([#282](https://github.com/libp2p/js-libp2p-mplex/issues/282)) ([4dbc590](https://github.com/libp2p/js-libp2p-mplex/commit/4dbc590d1ac92581fe2e937757567eef3854acf4)) * **dev:** bump delay from 5.0.0 to 6.0.0 ([#281](https://github.com/libp2p/js-libp2p-mplex/issues/281)) ([1e03e75](https://github.com/libp2p/js-libp2p-mplex/commit/1e03e75369722be9872f747cd83f555bc08d49fe)) +### [9.0.8](https://www.github.com/libp2p/js-libp2p/compare/mplex-v9.0.7...mplex-v9.0.8) (2023-10-06) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @libp2p/interface bumped from ^0.1.2 to ^0.1.3 + * @libp2p/logger bumped from ^3.0.2 to ^3.0.3 + * devDependencies + * @libp2p/interface-compliance-tests bumped from ^4.1.0 to ^4.1.1 + +### [9.0.7](https://www.github.com/libp2p/js-libp2p/compare/mplex-v9.0.6...mplex-v9.0.7) (2023-10-01) + + +### Dependencies + +* The following workspace dependencies were updated + * devDependencies + * @libp2p/interface-compliance-tests bumped from ^4.0.6 to ^4.1.0 + +### [9.0.6](https://www.github.com/libp2p/js-libp2p/compare/mplex-v9.0.5...mplex-v9.0.6) (2023-09-15) + + +### Dependencies + +* The following workspace dependencies were updated + * devDependencies + * @libp2p/interface-compliance-tests bumped from ^4.0.5 to ^4.0.6 + +### [9.0.5](https://www.github.com/libp2p/js-libp2p/compare/mplex-v9.0.4...mplex-v9.0.5) (2023-08-25) + + +### Dependencies + +* The following workspace dependencies were updated + * devDependencies + * @libp2p/interface-compliance-tests bumped from ^4.0.4 to ^4.0.5 + ### [9.0.4](https://www.github.com/libp2p/js-libp2p/compare/mplex-v9.0.3...mplex-v9.0.4) (2023-08-16) diff --git a/packages/stream-multiplexer-mplex/package.json b/packages/stream-multiplexer-mplex/package.json index 8420da3a8a..67a1db3952 100644 --- a/packages/stream-multiplexer-mplex/package.json +++ b/packages/stream-multiplexer-mplex/package.json @@ -1,6 +1,6 @@ { "name": "@libp2p/mplex", - "version": "9.0.4", + "version": "9.0.8", "description": "JavaScript implementation of https://github.com/libp2p/mplex", "license": "Apache-2.0 OR MIT", "homepage": "https://github.com/libp2p/js-libp2p/tree/master/packages/stream-multiplexer-mplex#readme", @@ -38,6 +38,7 @@ "eslintConfig": { "extends": "ipfs", "parserOptions": { + "project": true, "sourceType": "module" } }, @@ -56,22 +57,22 @@ "test:electron-main": "aegir test -t electron-main" }, "dependencies": { - "@libp2p/interface": "^0.1.2", - "@libp2p/logger": "^3.0.2", + "@libp2p/interface": "^0.1.3", + "@libp2p/logger": "^3.0.3", "abortable-iterator": "^5.0.1", "benchmark": "^2.1.4", "it-batched-bytes": "^2.0.2", "it-pushable": "^3.2.0", "it-stream-types": "^2.0.1", - "rate-limiter-flexible": "^2.3.11", + "rate-limiter-flexible": "^3.0.0", "uint8-varint": "^2.0.0", "uint8arraylist": "^2.4.3", "uint8arrays": "^4.0.6" }, "devDependencies": { - "@libp2p/interface-compliance-tests": "^4.0.4", - "aegir": "^40.0.8", - "cborg": "^2.0.1", + "@libp2p/interface-compliance-tests": "^4.1.1", + "aegir": "^41.0.2", + "cborg": "^4.0.3", "delay": "^6.0.0", "iso-random-stream": "^2.0.2", "it-all": "^3.0.1", diff --git a/packages/stream-multiplexer-mplex/src/stream.ts b/packages/stream-multiplexer-mplex/src/stream.ts index cc8024e3cf..6ffa50e7ff 100644 --- a/packages/stream-multiplexer-mplex/src/stream.ts +++ b/packages/stream-multiplexer-mplex/src/stream.ts @@ -8,9 +8,9 @@ import type { Message } from './message-types.js' export interface Options { id: number - send: (msg: Message) => Promise + send(msg: Message): Promise name?: string - onEnd?: (err?: Error) => void + onEnd?(err?: Error): void type?: 'initiator' | 'receiver' maxMsgSize?: number } @@ -18,7 +18,7 @@ export interface Options { interface MplexStreamInit extends AbstractStreamInit { streamId: number name: string - send: (msg: Message) => Promise + send(msg: Message): Promise /** * The maximum allowable data size, any data larger than this will be diff --git a/packages/transport-tcp/CHANGELOG.md b/packages/transport-tcp/CHANGELOG.md index 14c36e4a52..58cedcc115 100644 --- a/packages/transport-tcp/CHANGELOG.md +++ b/packages/transport-tcp/CHANGELOG.md @@ -5,6 +5,60 @@ * **dev:** bump aegir from 38.1.8 to 39.0.10 ([#279](https://github.com/libp2p/js-libp2p-tcp/issues/279)) ([3ed1235](https://github.com/libp2p/js-libp2p-tcp/commit/3ed12353aa48b5a933f80042846a8f1c2337fa47)) +### [8.0.9](https://www.github.com/libp2p/js-libp2p/compare/tcp-v8.0.8...tcp-v8.0.9) (2023-10-06) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @libp2p/interface bumped from ^0.1.2 to ^0.1.3 + * @libp2p/logger bumped from ^3.0.2 to ^3.0.3 + * @libp2p/utils bumped from ^4.0.3 to ^4.0.4 + * devDependencies + * @libp2p/interface-compliance-tests bumped from ^4.1.0 to ^4.1.1 + +### [8.0.8](https://www.github.com/libp2p/js-libp2p/compare/tcp-v8.0.7...tcp-v8.0.8) (2023-10-01) + + +### Bug Fixes + +* ensure all listeners are properly closed on tcp shutdown ([#2058](https://www.github.com/libp2p/js-libp2p/issues/2058)) ([b57bca4](https://www.github.com/libp2p/js-libp2p/commit/b57bca4493e1634108fe187466024e374b76c114)) + + +### Dependencies + +* The following workspace dependencies were updated + * devDependencies + * @libp2p/interface-compliance-tests bumped from ^4.0.6 to ^4.1.0 + +### [8.0.7](https://www.github.com/libp2p/js-libp2p/compare/tcp-v8.0.6...tcp-v8.0.7) (2023-09-15) + + +### Dependencies + +* The following workspace dependencies were updated + * devDependencies + * @libp2p/interface-compliance-tests bumped from ^4.0.5 to ^4.0.6 + +### [8.0.6](https://www.github.com/libp2p/js-libp2p/compare/tcp-v8.0.5...tcp-v8.0.6) (2023-09-10) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @libp2p/utils bumped from ^4.0.2 to ^4.0.3 + +### [8.0.5](https://www.github.com/libp2p/js-libp2p/compare/tcp-v8.0.4...tcp-v8.0.5) (2023-08-25) + + +### Dependencies + +* The following workspace dependencies were updated + * devDependencies + * @libp2p/interface-compliance-tests bumped from ^4.0.4 to ^4.0.5 + ### [8.0.4](https://www.github.com/libp2p/js-libp2p/compare/tcp-v8.0.3...tcp-v8.0.4) (2023-08-16) diff --git a/packages/transport-tcp/package.json b/packages/transport-tcp/package.json index 36991a6100..7633c493bb 100644 --- a/packages/transport-tcp/package.json +++ b/packages/transport-tcp/package.json @@ -1,6 +1,6 @@ { "name": "@libp2p/tcp", - "version": "8.0.4", + "version": "8.0.9", "description": "A TCP transport for libp2p", "license": "Apache-2.0 OR MIT", "homepage": "https://github.com/libp2p/js-libp2p/tree/master/packages/transport-tcp#readme", @@ -37,6 +37,7 @@ "eslintConfig": { "extends": "ipfs", "parserOptions": { + "project": true, "sourceType": "module" } }, @@ -50,21 +51,21 @@ "test:electron-main": "aegir test -t electron-main" }, "dependencies": { - "@libp2p/interface": "^0.1.2", - "@libp2p/logger": "^3.0.2", - "@libp2p/utils": "^4.0.2", + "@libp2p/interface": "^0.1.3", + "@libp2p/logger": "^3.0.3", + "@libp2p/utils": "^4.0.4", "@multiformats/mafmt": "^12.1.2", "@multiformats/multiaddr": "^12.1.5", "@types/sinon": "^10.0.15", "stream-to-it": "^0.2.2" }, "devDependencies": { - "@libp2p/interface-compliance-tests": "^4.0.4", - "aegir": "^40.0.8", + "@libp2p/interface-compliance-tests": "^4.1.1", + "aegir": "^41.0.2", "it-all": "^3.0.1", "it-pipe": "^3.0.1", "p-defer": "^4.0.0", - "sinon": "^15.1.2", + "sinon": "^16.0.0", "uint8arrays": "^4.0.4" } } diff --git a/packages/transport-tcp/src/listener.ts b/packages/transport-tcp/src/listener.ts index c7de579471..95792ed137 100644 --- a/packages/transport-tcp/src/listener.ts +++ b/packages/transport-tcp/src/listener.ts @@ -1,4 +1,5 @@ import net from 'net' +import { CodeError } from '@libp2p/interface/errors' import { EventEmitter, CustomEvent } from '@libp2p/interface/events' import { logger } from '@libp2p/logger' import { CODE_P2P } from './constants.js' @@ -32,11 +33,11 @@ export interface CloseServerOnMaxConnectionsOpts { listenBelow: number /** Close server once connection count is greater than or equal to `closeAbove` */ closeAbove: number - onListenError?: (err: Error) => void + onListenError?(err: Error): void } interface Context extends TCPCreateListenerOptions { - handler?: (conn: Connection) => void + handler?(conn: Connection): void upgrader: Upgrader socketInactivityTimeout?: number socketCloseTimeout?: number @@ -46,17 +47,25 @@ interface Context extends TCPCreateListenerOptions { closeServerOnMaxConnections?: CloseServerOnMaxConnectionsOpts } -const SERVER_STATUS_UP = 1 -const SERVER_STATUS_DOWN = 0 - export interface TCPListenerMetrics { status: MetricGroup errors: CounterGroup events: CounterGroup } -type Status = { started: false } | { - started: true +enum TCPListenerStatusCode { + /** + * When server object is initialized but we don't know the listening address yet or + * the server object is stopped manually, can be resumed only by calling listen() + **/ + INACTIVE = 0, + ACTIVE = 1, + /* During the connection limits */ + PAUSED = 2, +} + +type Status = { code: TCPListenerStatusCode.INACTIVE } | { + code: Exclude listeningAddr: Multiaddr peerId: string | null netConfig: NetConfig @@ -66,7 +75,7 @@ export class TCPListener extends EventEmitter implements Listene private readonly server: net.Server /** Keep track of open connections to destroy in case of timeout */ private readonly connections = new Set() - private status: Status = { started: false } + private status: Status = { code: TCPListenerStatusCode.INACTIVE } private metrics?: TCPListenerMetrics private addr: string @@ -88,7 +97,7 @@ export class TCPListener extends EventEmitter implements Listene if (context.closeServerOnMaxConnections != null) { // Sanity check options if (context.closeServerOnMaxConnections.closeAbove < context.closeServerOnMaxConnections.listenBelow) { - throw Error('closeAbove must be >= listenBelow') + throw new CodeError('closeAbove must be >= listenBelow', 'ERROR_CONNECTION_LIMITS') } } @@ -133,7 +142,7 @@ export class TCPListener extends EventEmitter implements Listene } this.metrics?.status.update({ - [this.addr]: SERVER_STATUS_UP + [this.addr]: TCPListenerStatusCode.ACTIVE }) } @@ -145,13 +154,22 @@ export class TCPListener extends EventEmitter implements Listene }) .on('close', () => { this.metrics?.status.update({ - [this.addr]: SERVER_STATUS_DOWN + [this.addr]: this.status.code }) - this.dispatchEvent(new CustomEvent('close')) + + // If this event is emitted, the transport manager will remove the listener from it's cache + // in the meanwhile if the connections are dropped then listener will start listening again + // and the transport manager will not be able to close the server + if (this.status.code !== TCPListenerStatusCode.PAUSED) { + this.dispatchEvent(new CustomEvent('close')) + } }) } private onSocket (socket: net.Socket): void { + if (this.status.code !== TCPListenerStatusCode.ACTIVE) { + throw new CodeError('Server is is not listening yet', 'ERR_SERVER_NOT_RUNNING') + } // Avoid uncaught errors caused by unstable connections socket.on('error', err => { log('socket error', err) @@ -161,7 +179,7 @@ export class TCPListener extends EventEmitter implements Listene let maConn: MultiaddrConnection try { maConn = toMultiaddrConnection(socket, { - listeningAddr: this.status.started ? this.status.listeningAddr : undefined, + listeningAddr: this.status.listeningAddr, socketInactivityTimeout: this.context.socketInactivityTimeout, socketCloseTimeout: this.context.socketCloseTimeout, metrics: this.metrics?.events, @@ -189,9 +207,9 @@ export class TCPListener extends EventEmitter implements Listene ) { // The most likely case of error is if the port taken by this application is binded by // another process during the time the server if closed. In that case there's not much - // we can do. netListen() will be called again every time a connection is dropped, which + // we can do. resume() will be called again every time a connection is dropped, which // acts as an eventual retry mechanism. onListenError allows the consumer act on this. - this.netListen().catch(e => { + this.resume().catch(e => { log.error('error attempting to listen server once connection count under limit', e) this.context.closeServerOnMaxConnections?.onListenError?.(e as Error) }) @@ -206,7 +224,9 @@ export class TCPListener extends EventEmitter implements Listene this.context.closeServerOnMaxConnections != null && this.connections.size >= this.context.closeServerOnMaxConnections.closeAbove ) { - this.netClose() + this.pause(false).catch(e => { + log.error('error attempting to close server once connection count over limit', e) + }) } this.dispatchEvent(new CustomEvent('connection', { detail: conn })) @@ -232,7 +252,7 @@ export class TCPListener extends EventEmitter implements Listene } getAddrs (): Multiaddr[] { - if (!this.status.started) { + if (this.status.code === TCPListenerStatusCode.INACTIVE) { return [] } @@ -264,35 +284,44 @@ export class TCPListener extends EventEmitter implements Listene } async listen (ma: Multiaddr): Promise { - if (this.status.started) { - throw Error('server is already listening') + if (this.status.code === TCPListenerStatusCode.ACTIVE || this.status.code === TCPListenerStatusCode.PAUSED) { + throw new CodeError('server is already listening', 'ERR_SERVER_ALREADY_LISTENING') } const peerId = ma.getPeerId() const listeningAddr = peerId == null ? ma.decapsulateCode(CODE_P2P) : ma const { backlog } = this.context - this.status = { - started: true, - listeningAddr, - peerId, - netConfig: multiaddrToNetConfig(listeningAddr, { backlog }) - } + try { + this.status = { + code: TCPListenerStatusCode.ACTIVE, + listeningAddr, + peerId, + netConfig: multiaddrToNetConfig(listeningAddr, { backlog }) + } - await this.netListen() + await this.resume() + } catch (err) { + this.status = { code: TCPListenerStatusCode.INACTIVE } + throw err + } } async close (): Promise { - await Promise.all( - Array.from(this.connections.values()).map(async maConn => { await attemptClose(maConn) }) - ) - - // netClose already checks if server.listening - this.netClose() + // Close connections and server the same time to avoid any race condition + await Promise.all([ + Promise.all(Array.from(this.connections.values()).map(async maConn => attemptClose(maConn))), + this.pause(true).catch(e => { + log.error('error attempting to close server once connection count over limit', e) + }) + ]) } - private async netListen (): Promise { - if (!this.status.started || this.server.listening) { + /** + * Can resume a stopped or start an inert server + */ + private async resume (): Promise { + if (this.server.listening || this.status.code === TCPListenerStatusCode.INACTIVE) { return } @@ -304,11 +333,17 @@ export class TCPListener extends EventEmitter implements Listene this.server.listen(netConfig, resolve) }) + this.status = { ...this.status, code: TCPListenerStatusCode.ACTIVE } log('Listening on %s', this.server.address()) } - private netClose (): void { - if (!this.status.started || !this.server.listening) { + private async pause (permanent: boolean): Promise { + if (!this.server.listening && this.status.code === TCPListenerStatusCode.PAUSED && permanent) { + this.status = { code: TCPListenerStatusCode.INACTIVE } + return + } + + if (!this.server.listening || this.status.code !== TCPListenerStatusCode.ACTIVE) { return } @@ -326,9 +361,12 @@ export class TCPListener extends EventEmitter implements Listene // Stops the server from accepting new connections and keeps existing connections. // 'close' event is emitted only emitted when all connections are ended. // The optional callback will be called once the 'close' event occurs. - // - // NOTE: Since we want to keep existing connections and have checked `!this.server.listening` it's not necessary - // to pass a callback to close. - this.server.close() + + // We need to set this status before closing server, so other procedures are aware + // during the time the server is closing + this.status = permanent ? { code: TCPListenerStatusCode.INACTIVE } : { ...this.status, code: TCPListenerStatusCode.PAUSED } + await new Promise((resolve, reject) => { + this.server.close(err => { (err != null) ? reject(err) : resolve() }) + }) } } diff --git a/packages/transport-tcp/test/connection-limits.spec.ts b/packages/transport-tcp/test/connection-limits.spec.ts new file mode 100644 index 0000000000..30aea96ac7 --- /dev/null +++ b/packages/transport-tcp/test/connection-limits.spec.ts @@ -0,0 +1,217 @@ +import net from 'node:net' +import { promisify } from 'util' +import { EventEmitter } from '@libp2p/interface/events' +import { mockUpgrader } from '@libp2p/interface-compliance-tests/mocks' +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' +import { tcp } from '../src/index.js' +import type { TCPListener } from '../src/listener.js' + +const buildSocketAssertions = (port: number, closeCallbacks: Array<() => Promise | any>): { assertConnectedSocket(i: number): Promise, assertRefusedSocket(i: number): Promise } => { + function createSocket (i: number): net.Socket { + const socket = net.connect({ host: '127.0.0.1', port }) + + closeCallbacks.unshift(async function closeHandler (): Promise { + if (!socket.destroyed) { + socket.destroy() + await new Promise((resolve) => socket.on('close', resolve)) + } + }) + return socket + } + + async function assertConnectedSocket (i: number): Promise { + const socket = createSocket(i) + + await new Promise((resolve, reject) => { + socket.once('connect', () => { + resolve() + }) + socket.once('error', (err) => { + err.message = `Socket[${i}] ${err.message}` + reject(err) + }) + }) + + return socket + } + + async function assertRefusedSocket (i: number): Promise { + const socket = createSocket(i) + + await new Promise((resolve, reject) => { + socket.once('connect', () => { + reject(Error(`Socket[${i}] connected but was expected to reject`)) + }) + socket.once('error', (err) => { + if (err.message.includes('ECONNREFUSED')) { + resolve() + } else { + err.message = `Socket[${i}] unexpected error ${err.message}` + reject(err) + } + }) + }) + + return socket + } + + return { assertConnectedSocket, assertRefusedSocket } +} + +async function assertServerConnections (listener: TCPListener, connections: number): Promise { + // Expect server connections but allow time for sockets to connect or disconnect + for (let i = 0; i < 100; i++) { + // eslint-disable-next-line @typescript-eslint/dot-notation + if (listener['connections'].size === connections) { + return + } else { + await promisify(setTimeout)(10) + } + } + // eslint-disable-next-line @typescript-eslint/dot-notation + expect(listener['connections'].size).equals(connections, 'invalid amount of server connections') +} + +describe('closeAbove/listenBelow', () => { + const afterEachCallbacks: Array<() => Promise | any> = [] + afterEach(async () => { + await Promise.all(afterEachCallbacks.map(fn => fn())) + afterEachCallbacks.length = 0 + }) + + it('reject dial of connection above closeAbove', async () => { + const listenBelow = 2 + const closeAbove = 3 + const port = 9900 + + const trasnport = tcp({ closeServerOnMaxConnections: { listenBelow, closeAbove } })() + + const upgrader = mockUpgrader({ + events: new EventEmitter() + }) + const listener = trasnport.createListener({ upgrader }) as TCPListener + // eslint-disable-next-line @typescript-eslint/promise-function-async + afterEachCallbacks.push(() => listener.close()) + await listener.listen(multiaddr(`/ip4/127.0.0.1/tcp/${port}`)) + const { assertConnectedSocket, assertRefusedSocket } = buildSocketAssertions(port, afterEachCallbacks) + + await assertConnectedSocket(1) + await assertConnectedSocket(2) + await assertConnectedSocket(3) + await assertServerConnections(listener, 3) + + // Limit reached, server should be closed here + await assertRefusedSocket(4) + await assertRefusedSocket(5) + await assertServerConnections(listener, 3) + }) + + it('accepts dial of connection when connection drop listenBelow limit', async () => { + const listenBelow = 2 + const closeAbove = 3 + const port = 9900 + + const trasnport = tcp({ closeServerOnMaxConnections: { listenBelow, closeAbove } })() + + const upgrader = mockUpgrader({ + events: new EventEmitter() + }) + const listener = trasnport.createListener({ upgrader }) as TCPListener + // eslint-disable-next-line @typescript-eslint/promise-function-async + afterEachCallbacks.push(() => listener.close()) + await listener.listen(multiaddr(`/ip4/127.0.0.1/tcp/${port}`)) + const { assertConnectedSocket } = buildSocketAssertions(port, afterEachCallbacks) + + const socket1 = await assertConnectedSocket(1) + const socket2 = await assertConnectedSocket(2) + await assertConnectedSocket(3) + await assertServerConnections(listener, 3) + + // Destroy sockets to be have connections < listenBelow + socket1.destroy() + socket2.destroy() + // After destroying 2 sockets connections will be below "listenBelow" limit + await assertServerConnections(listener, 1) + + // Now it should be able to accept new connections + await assertConnectedSocket(4) + await assertConnectedSocket(5) + + // 2 connections dropped and 2 new connections accepted + await assertServerConnections(listener, 3) + }) + + it('should not emit "close" event when server is stopped due to "closeAbove" limit', async () => { + const listenBelow = 2 + const closeAbove = 3 + const port = 9900 + + const trasnport = tcp({ closeServerOnMaxConnections: { listenBelow, closeAbove } })() + + const upgrader = mockUpgrader({ + events: new EventEmitter() + }) + const listener = trasnport.createListener({ upgrader }) as TCPListener + // eslint-disable-next-line @typescript-eslint/promise-function-async + afterEachCallbacks.push(() => listener.close()) + + let closeEventCallCount = 0 + listener.addEventListener('close', () => { + closeEventCallCount += 1 + }) + + await listener.listen(multiaddr(`/ip4/127.0.0.1/tcp/${port}`)) + const { assertConnectedSocket } = buildSocketAssertions(port, afterEachCallbacks) + + await assertConnectedSocket(1) + await assertConnectedSocket(2) + await assertConnectedSocket(3) + await assertServerConnections(listener, 3) + + // Limit reached, server should be closed but should not emit "close" event + expect(closeEventCallCount).equals(0) + }) + + it('should emit "listening" event when server is resumed due to "listenBelow" limit', async () => { + const listenBelow = 2 + const closeAbove = 3 + const port = 9900 + + const trasnport = tcp({ closeServerOnMaxConnections: { listenBelow, closeAbove } })() + + const upgrader = mockUpgrader({ + events: new EventEmitter() + }) + const listener = trasnport.createListener({ upgrader }) as TCPListener + // eslint-disable-next-line @typescript-eslint/promise-function-async + afterEachCallbacks.push(() => listener.close()) + + let listeningEventCallCount = 0 + listener.addEventListener('listening', () => { + listeningEventCallCount += 1 + }) + + await listener.listen(multiaddr(`/ip4/127.0.0.1/tcp/${port}`)) + const { assertConnectedSocket } = buildSocketAssertions(port, afterEachCallbacks) + + // Server should be listening now + expect(listeningEventCallCount).equals(1) + + const socket1 = await assertConnectedSocket(1) + const socket2 = await assertConnectedSocket(2) + await assertConnectedSocket(3) + // Limit reached, server should be closed now + await assertServerConnections(listener, 3) + + // Close some sockets to resume listening + socket1.destroy() + socket2.destroy() + + // Wait for listener to emit event + await promisify(setTimeout)(50) + + // Server should emit the "listening" event again + expect(listeningEventCallCount).equals(2) + }) +}) diff --git a/packages/transport-tcp/test/max-connections-close.spec.ts b/packages/transport-tcp/test/max-connections-close.spec.ts deleted file mode 100644 index 177c71bbe9..0000000000 --- a/packages/transport-tcp/test/max-connections-close.spec.ts +++ /dev/null @@ -1,121 +0,0 @@ -import net from 'node:net' -import { promisify } from 'util' -import { EventEmitter } from '@libp2p/interface/events' -import { mockUpgrader } from '@libp2p/interface-compliance-tests/mocks' -import { multiaddr } from '@multiformats/multiaddr' -import { expect } from 'aegir/chai' -import { tcp } from '../src/index.js' -import type { TCPListener } from '../src/listener.js' - -describe('close server on maxConnections', () => { - const afterEachCallbacks: Array<() => Promise | any> = [] - afterEach(async () => { - await Promise.all(afterEachCallbacks.map(fn => fn())) - afterEachCallbacks.length = 0 - }) - - it('reject dial of connection above closeAbove', async () => { - const listenBelow = 2 - const closeAbove = 3 - const port = 9900 - - const seenRemoteConnections = new Set() - const trasnport = tcp({ closeServerOnMaxConnections: { listenBelow, closeAbove } })() - - const upgrader = mockUpgrader({ - events: new EventEmitter() - }) - const listener = trasnport.createListener({ upgrader }) as TCPListener - // eslint-disable-next-line @typescript-eslint/promise-function-async - afterEachCallbacks.push(() => listener.close()) - await listener.listen(multiaddr(`/ip4/127.0.0.1/tcp/${port}`)) - - listener.addEventListener('connection', (conn) => { - seenRemoteConnections.add(conn.detail.remoteAddr.toString()) - }) - - function createSocket (): net.Socket { - const socket = net.connect({ host: '127.0.0.1', port }) - - // eslint-disable-next-line @typescript-eslint/promise-function-async - afterEachCallbacks.unshift(async () => { - if (!socket.destroyed) { - socket.destroy() - await new Promise((resolve) => socket.on('close', resolve)) - } - }) - - return socket - } - - async function assertConnectedSocket (i: number): Promise { - const socket = createSocket() - - await new Promise((resolve, reject) => { - socket.once('connect', () => { - resolve() - }) - socket.once('error', (err) => { - err.message = `Socket[${i}] ${err.message}` - reject(err) - }) - }) - - return socket - } - - async function assertRefusedSocket (i: number): Promise { - const socket = createSocket() - - await new Promise((resolve, reject) => { - socket.once('connect', () => { - reject(Error(`Socket[${i}] connected but was expected to reject`)) - }) - socket.once('error', (err) => { - if (err.message.includes('ECONNREFUSED')) { - resolve() - } else { - err.message = `Socket[${i}] unexpected error ${err.message}` - reject(err) - } - }) - }) - } - - async function assertServerConnections (connections: number): Promise { - // Expect server connections but allow time for sockets to connect or disconnect - for (let i = 0; i < 100; i++) { - // eslint-disable-next-line @typescript-eslint/dot-notation - if (listener['connections'].size === connections) { - return - } else { - await promisify(setTimeout)(10) - } - } - // eslint-disable-next-line @typescript-eslint/dot-notation - expect(listener['connections'].size).equals(connections, 'Wrong server connections') - } - - const socket1 = await assertConnectedSocket(1) - const socket2 = await assertConnectedSocket(2) - const socket3 = await assertConnectedSocket(3) - await assertServerConnections(3) - // Limit reached, server should be closed here - await assertRefusedSocket(4) - await assertRefusedSocket(5) - // Destroy sockets to be have connections < listenBelow - socket1.destroy() - socket2.destroy() - await assertServerConnections(1) - // Attempt to connect more sockets - const socket6 = await assertConnectedSocket(6) - const socket7 = await assertConnectedSocket(7) - await assertServerConnections(3) - // Limit reached, server should be closed here - await assertRefusedSocket(8) - - expect(socket3.destroyed).equals(false, 'socket3 must not destroyed') - expect(socket6.destroyed).equals(false, 'socket6 must not destroyed') - expect(socket7.destroyed).equals(false, 'socket7 must not destroyed') - }) -}) diff --git a/packages/transport-webrtc/.aegir.js b/packages/transport-webrtc/.aegir.js index 491576df87..3b38600b7a 100644 --- a/packages/transport-webrtc/.aegir.js +++ b/packages/transport-webrtc/.aegir.js @@ -8,7 +8,6 @@ export default { before: async () => { const { createLibp2p } = await import('libp2p') const { circuitRelayServer } = await import('libp2p/circuit-relay') - const { identifyService } = await import('libp2p/identify') const { webSockets } = await import('@libp2p/websockets') const { noise } = await import('@chainsafe/libp2p-noise') const { yamux } = await import('@chainsafe/libp2p-yamux') @@ -34,11 +33,11 @@ export default { reservations: { maxReservations: Infinity } - }), - identify: identifyService() + }) }, connectionManager: { - minConnections: 0 + minConnections: 0, + inboundConnectionThreshold: Infinity } }) diff --git a/packages/transport-webrtc/CHANGELOG.md b/packages/transport-webrtc/CHANGELOG.md index 1b6d79093c..ca190b424c 100644 --- a/packages/transport-webrtc/CHANGELOG.md +++ b/packages/transport-webrtc/CHANGELOG.md @@ -5,6 +5,139 @@ * add browser-to-browser test for bi-directional communication ([#172](https://github.com/libp2p/js-libp2p-webrtc/issues/172)) ([1ec3d8a](https://github.com/libp2p/js-libp2p-webrtc/commit/1ec3d8a8b611d5227f430037e2547fd86d115eaa)) +### [3.2.3](https://www.github.com/libp2p/js-libp2p/compare/webrtc-v3.2.2...webrtc-v3.2.3) (2023-10-10) + + +### Dependencies + +* The following workspace dependencies were updated + * devDependencies + * libp2p bumped from ^0.46.13 to ^0.46.14 + +### [3.2.2](https://www.github.com/libp2p/js-libp2p/compare/webrtc-v3.2.1...webrtc-v3.2.2) (2023-10-06) + + +### Bug Fixes + +* close webrtc streams without data loss ([#2073](https://www.github.com/libp2p/js-libp2p/issues/2073)) ([7d8b155](https://www.github.com/libp2p/js-libp2p/commit/7d8b15517a480e01a8ebd427ab0093509b78d5b0)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @libp2p/interface bumped from ^0.1.2 to ^0.1.3 + * @libp2p/interface-internal bumped from ^0.1.5 to ^0.1.6 + * @libp2p/logger bumped from ^3.0.2 to ^3.0.3 + * @libp2p/peer-id bumped from ^3.0.2 to ^3.0.3 + * devDependencies + * @libp2p/interface-compliance-tests bumped from ^4.1.0 to ^4.1.1 + * @libp2p/peer-id-factory bumped from ^3.0.4 to ^3.0.5 + * @libp2p/websockets bumped from ^7.0.8 to ^7.0.9 + * libp2p bumped from ^0.46.12 to ^0.46.13 + +### [3.2.1](https://www.github.com/libp2p/js-libp2p/compare/webrtc-v3.2.0...webrtc-v3.2.1) (2023-10-01) + + +### Bug Fixes + +* **transports:** filter circuit addresses ([#2060](https://www.github.com/libp2p/js-libp2p/issues/2060)) ([972b10a](https://www.github.com/libp2p/js-libp2p/commit/972b10a967653f60666a061bddfa46c0decfcc70)) + + +### Dependencies + +* The following workspace dependencies were updated + * devDependencies + * @libp2p/interface-compliance-tests bumped from ^4.0.6 to ^4.1.0 + * @libp2p/websockets bumped from ^7.0.7 to ^7.0.8 + * libp2p bumped from ^0.46.11 to ^0.46.12 + +## [3.2.0](https://www.github.com/libp2p/js-libp2p/compare/webrtc-v3.1.11...webrtc-v3.2.0) (2023-09-20) + + +### Features + +* collect dial/listen metrics in webrtc and webtransport ([#2061](https://www.github.com/libp2p/js-libp2p/issues/2061)) ([6cb80f7](https://www.github.com/libp2p/js-libp2p/commit/6cb80f7d3b308aff955f4de247680a3c9c26993b)) + +### [3.1.11](https://www.github.com/libp2p/js-libp2p/compare/webrtc-v3.1.10...webrtc-v3.1.11) (2023-09-15) + + +### Bug Fixes + +* **@libp2p/webrtc:** set max message size in alignment with spec ([#2050](https://www.github.com/libp2p/js-libp2p/issues/2050)) ([122f1e6](https://www.github.com/libp2p/js-libp2p/commit/122f1e67d4c0aa8c4c8f50aa24a0c0dbe00411fa)) +* **@libp2p/webrtc:** use correct udp port in remote address ([#2055](https://www.github.com/libp2p/js-libp2p/issues/2055)) ([0ce318e](https://www.github.com/libp2p/js-libp2p/commit/0ce318ecea222dc01776a3534d96351675ba9e0d)) +* **@libp2p/webrtc:** use stream logger instead of global logger ([#2042](https://www.github.com/libp2p/js-libp2p/issues/2042)) ([88c47f5](https://www.github.com/libp2p/js-libp2p/commit/88c47f51f9d67a6261e4ac65c494cd1e6e4ed8dd)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @libp2p/interface-internal bumped from ^0.1.4 to ^0.1.5 + * devDependencies + * @libp2p/interface-compliance-tests bumped from ^4.0.5 to ^4.0.6 + * @libp2p/peer-id-factory bumped from ^3.0.3 to ^3.0.4 + * @libp2p/websockets bumped from ^7.0.6 to ^7.0.7 + * libp2p bumped from ^0.46.10 to ^0.46.11 + +### [3.1.10](https://www.github.com/libp2p/js-libp2p/compare/webrtc-v3.1.9...webrtc-v3.1.10) (2023-09-10) + + +### Bug Fixes + +* **@libp2p/webrtc:** update stream logger name to webrtc ([#2035](https://www.github.com/libp2p/js-libp2p/issues/2035)) ([0d228f9](https://www.github.com/libp2p/js-libp2p/commit/0d228f9f078b65fd5aa48ec644946e5c74ed2741)) + + +### Dependencies + +* The following workspace dependencies were updated + * devDependencies + * @libp2p/websockets bumped from ^7.0.5 to ^7.0.6 + * libp2p bumped from ^0.46.9 to ^0.46.10 + +### [3.1.9](https://www.github.com/libp2p/js-libp2p/compare/webrtc-v3.1.8...webrtc-v3.1.9) (2023-09-05) + + +### Bug Fixes + +* **@libp2p/webrtc:** close data-channel on muxer stream end ([#1976](https://www.github.com/libp2p/js-libp2p/issues/1976)) ([7517082](https://www.github.com/libp2p/js-libp2p/commit/7517082d0ae5dcd8f3f2d13aee2a13067836a2be)) + + +### Dependencies + +* The following workspace dependencies were updated + * devDependencies + * libp2p bumped from ^0.46.8 to ^0.46.9 + +### [3.1.8](https://www.github.com/libp2p/js-libp2p/compare/webrtc-v3.1.7...webrtc-v3.1.8) (2023-09-01) + + +### Dependencies + +* The following workspace dependencies were updated + * devDependencies + * libp2p bumped from ^0.46.7 to ^0.46.8 + +### [3.1.7](https://www.github.com/libp2p/js-libp2p/compare/webrtc-v3.1.6...webrtc-v3.1.7) (2023-08-25) + + +### Dependencies + +* The following workspace dependencies were updated + * devDependencies + * @libp2p/interface-compliance-tests bumped from ^4.0.4 to ^4.0.5 + * @libp2p/websockets bumped from ^7.0.4 to ^7.0.5 + * libp2p bumped from ^0.46.6 to ^0.46.7 + +### [3.1.6](https://www.github.com/libp2p/js-libp2p/compare/webrtc-v3.1.5...webrtc-v3.1.6) (2023-08-16) + + +### Dependencies + +* The following workspace dependencies were updated + * devDependencies + * libp2p bumped from ^0.46.5 to ^0.46.6 + ### [3.1.5](https://www.github.com/libp2p/js-libp2p/compare/webrtc-v3.1.4...webrtc-v3.1.5) (2023-08-16) diff --git a/packages/transport-webrtc/package.json b/packages/transport-webrtc/package.json index 78d2971c42..21254dbdea 100644 --- a/packages/transport-webrtc/package.json +++ b/packages/transport-webrtc/package.json @@ -1,6 +1,6 @@ { "name": "@libp2p/webrtc", - "version": "3.1.5", + "version": "3.2.3", "description": "A libp2p transport using WebRTC connections", "license": "Apache-2.0 OR MIT", "homepage": "https://github.com/libp2p/js-libp2p/tree/master/packages/transport-webrtc#readme", @@ -28,6 +28,7 @@ "eslintConfig": { "extends": "ipfs", "parserOptions": { + "project": true, "sourceType": "module" } }, @@ -45,13 +46,15 @@ }, "dependencies": { "@chainsafe/libp2p-noise": "^13.0.0", - "@libp2p/interface": "^0.1.2", - "@libp2p/interface-internal": "^0.1.4", - "@libp2p/logger": "^3.0.2", - "@libp2p/peer-id": "^3.0.2", + "@libp2p/interface": "^0.1.3", + "@libp2p/interface-internal": "^0.1.6", + "@libp2p/logger": "^3.0.3", + "@libp2p/peer-id": "^3.0.3", "@multiformats/mafmt": "^12.1.2", "@multiformats/multiaddr": "^12.1.5", + "@multiformats/multiaddr-matcher": "^1.0.1", "abortable-iterator": "^5.0.1", + "any-signal": "^4.1.1", "detect-browser": "^5.3.0", "it-length-prefixed": "^9.0.1", "it-pipe": "^3.0.1", @@ -61,27 +64,31 @@ "it-to-buffer": "^4.0.2", "multiformats": "^12.0.1", "multihashes": "^4.0.3", - "node-datachannel": "^0.4.3", + "node-datachannel": "^0.5.0-dev", "p-defer": "^4.0.0", "p-event": "^6.0.0", + "p-timeout": "^6.1.2", "protons-runtime": "^5.0.0", + "race-signal": "^1.0.0", "uint8arraylist": "^2.4.3", "uint8arrays": "^4.0.6" }, "devDependencies": { "@chainsafe/libp2p-yamux": "^5.0.0", - "@libp2p/interface-compliance-tests": "^4.0.4", - "@libp2p/peer-id-factory": "^3.0.3", - "@libp2p/websockets": "^7.0.4", + "@libp2p/interface-compliance-tests": "^4.1.1", + "@libp2p/peer-id-factory": "^3.0.5", + "@libp2p/websockets": "^7.0.9", "@types/sinon": "^10.0.15", - "aegir": "^40.0.8", + "aegir": "^41.0.2", "delay": "^6.0.0", + "it-drain": "^3.0.3", "it-length": "^3.0.2", "it-map": "^3.0.3", "it-pair": "^2.0.6", - "libp2p": "^0.46.5", + "libp2p": "^0.46.14", + "p-retry": "^6.1.0", "protons": "^7.0.2", - "sinon": "^15.1.2", + "sinon": "^16.0.0", "sinon-ts": "^1.0.0" }, "browser": { diff --git a/packages/transport-webrtc/src/index.ts b/packages/transport-webrtc/src/index.ts index 0245aefcc9..a8bbf37e75 100644 --- a/packages/transport-webrtc/src/index.ts +++ b/packages/transport-webrtc/src/index.ts @@ -3,6 +3,40 @@ import { WebRTCDirectTransport, type WebRTCTransportDirectInit, type WebRTCDirec import type { WebRTCTransportComponents, WebRTCTransportInit } from './private-to-private/transport.js' import type { Transport } from '@libp2p/interface/transport' +export interface DataChannelOptions { + /** + * The maximum message size sendable over the channel in bytes (default 16KB) + */ + maxMessageSize?: number + + /** + * If the channel's `bufferedAmount` grows over this amount in bytes, wait + * for it to drain before sending more data (default: 16MB) + */ + maxBufferedAmount?: number + + /** + * When `bufferedAmount` is above `maxBufferedAmount`, we pause sending until + * the `bufferedAmountLow` event fires - this controls how long we wait for + * that event in ms (default: 30s) + */ + bufferedAmountLowEventTimeout?: number + + /** + * When closing a stream, we wait for `bufferedAmount` to become 0 before + * closing the underlying RTCDataChannel - this controls how long we wait + * in ms (default: 30s) + */ + drainTimeout?: number + + /** + * When closing a stream we first send a FIN flag to the remote and wait + * for a FIN_ACK reply before closing the underlying RTCDataChannel - this + * controls how long we wait for the acknowledgement in ms (default: 5s) + */ + closeTimeout?: number +} + /** * @param {WebRTCTransportDirectInit} init - WebRTC direct transport configuration * @param init.dataChannel - DataChannel configurations diff --git a/packages/transport-webrtc/src/maconn.ts b/packages/transport-webrtc/src/maconn.ts index c32d88760c..04318aa549 100644 --- a/packages/transport-webrtc/src/maconn.ts +++ b/packages/transport-webrtc/src/maconn.ts @@ -5,7 +5,7 @@ import type { CounterGroup } from '@libp2p/interface/metrics' import type { AbortOptions, Multiaddr } from '@multiformats/multiaddr' import type { Source, Sink } from 'it-stream-types' -const log = logger('libp2p:webrtc:connection') +const log = logger('libp2p:webrtc:maconn') interface WebRTCMultiaddrConnectionInit { /** @@ -65,8 +65,13 @@ export class WebRTCMultiaddrConnection implements MultiaddrConnection { this.timeline = init.timeline this.peerConnection = init.peerConnection + const initialState = this.peerConnection.connectionState + this.peerConnection.onconnectionstatechange = () => { - if (this.peerConnection.connectionState === 'closed' || this.peerConnection.connectionState === 'disconnected' || this.peerConnection.connectionState === 'failed') { + log.trace('peer connection state change', this.peerConnection.connectionState, 'initial state', initialState) + + if (this.peerConnection.connectionState === 'disconnected' || this.peerConnection.connectionState === 'failed' || this.peerConnection.connectionState === 'closed') { + // nothing else to do but close the connection this.timeline.close = Date.now() } } diff --git a/packages/transport-webrtc/src/muxer.ts b/packages/transport-webrtc/src/muxer.ts index 7e5b655a3c..fae5f15011 100644 --- a/packages/transport-webrtc/src/muxer.ts +++ b/packages/transport-webrtc/src/muxer.ts @@ -1,6 +1,7 @@ +import { logger } from '@libp2p/logger' import { createStream } from './stream.js' -import { nopSink, nopSource } from './util.js' -import type { DataChannelOpts } from './stream.js' +import { drainAndClose, nopSink, nopSource } from './util.js' +import type { DataChannelOptions } from './index.js' import type { Stream } from '@libp2p/interface/connection' import type { CounterGroup } from '@libp2p/interface/metrics' import type { StreamMuxer, StreamMuxerFactory, StreamMuxerInit } from '@libp2p/interface/stream-muxer' @@ -8,6 +9,8 @@ import type { AbortOptions } from '@multiformats/multiaddr' import type { Source, Sink } from 'it-stream-types' import type { Uint8ArrayList } from 'uint8arraylist' +const log = logger('libp2p:webrtc:muxer') + const PROTOCOL = '/webrtc' export interface DataChannelMuxerFactoryInit { @@ -17,19 +20,16 @@ export interface DataChannelMuxerFactoryInit { peerConnection: RTCPeerConnection /** - * Optional metrics for this data channel muxer + * The protocol to use */ - metrics?: CounterGroup + protocol?: string /** - * Data channel options + * Optional metrics for this data channel muxer */ - dataChannelOptions?: Partial + metrics?: CounterGroup - /** - * The protocol to use - */ - protocol?: string + dataChannelOptions?: DataChannelOptions } export class DataChannelMuxerFactory implements StreamMuxerFactory { @@ -41,23 +41,23 @@ export class DataChannelMuxerFactory implements StreamMuxerFactory { private readonly peerConnection: RTCPeerConnection private streamBuffer: Stream[] = [] private readonly metrics?: CounterGroup - private readonly dataChannelOptions?: Partial + private readonly dataChannelOptions?: DataChannelOptions constructor (init: DataChannelMuxerFactoryInit) { this.peerConnection = init.peerConnection this.metrics = init.metrics this.protocol = init.protocol ?? PROTOCOL - this.dataChannelOptions = init.dataChannelOptions + this.dataChannelOptions = init.dataChannelOptions ?? {} // store any datachannels opened before upgrade has been completed this.peerConnection.ondatachannel = ({ channel }) => { const stream = createStream({ channel, direction: 'inbound', - dataChannelOptions: init.dataChannelOptions, onEnd: () => { this.streamBuffer = this.streamBuffer.filter(s => s.id !== stream.id) - } + }, + ...this.dataChannelOptions }) this.streamBuffer.push(stream) } @@ -90,34 +90,15 @@ export class DataChannelMuxer implements StreamMuxer { public protocol: string private readonly peerConnection: RTCPeerConnection - private readonly dataChannelOptions?: DataChannelOpts + private readonly dataChannelOptions: DataChannelOptions private readonly metrics?: CounterGroup - /** - * Gracefully close all tracked streams and stop the muxer - */ - close: (options?: AbortOptions) => Promise = async () => { } - - /** - * Abort all tracked streams and stop the muxer - */ - abort: (err: Error) => void = () => { } - - /** - * The stream source, a no-op as the transport natively supports multiplexing - */ - source: AsyncGenerator = nopSource() - - /** - * The stream destination, a no-op as the transport natively supports multiplexing - */ - sink: Sink, Promise> = nopSink - constructor (readonly init: DataChannelMuxerInit) { this.streams = init.streams this.peerConnection = init.peerConnection this.protocol = init.protocol ?? PROTOCOL this.metrics = init.metrics + this.dataChannelOptions = init.dataChannelOptions ?? {} /** * Fired when a data channel has been added to the connection has been @@ -129,19 +110,19 @@ export class DataChannelMuxer implements StreamMuxer { const stream = createStream({ channel, direction: 'inbound', - dataChannelOptions: this.dataChannelOptions, onEnd: () => { + log.trace('stream %s %s %s onEnd', stream.direction, stream.id, stream.protocol) + drainAndClose(channel, `inbound ${stream.id} ${stream.protocol}`, this.dataChannelOptions.drainTimeout) this.streams = this.streams.filter(s => s.id !== stream.id) this.metrics?.increment({ stream_end: true }) init?.onStreamEnd?.(stream) - } + }, + ...this.dataChannelOptions }) this.streams.push(stream) - if ((init?.onIncomingStream) != null) { - this.metrics?.increment({ incoming_stream: true }) - init.onIncomingStream(stream) - } + this.metrics?.increment({ incoming_stream: true }) + init?.onIncomingStream?.(stream) } const onIncomingStream = init?.onIncomingStream @@ -150,18 +131,52 @@ export class DataChannelMuxer implements StreamMuxer { } } + /** + * Gracefully close all tracked streams and stop the muxer + */ + async close (options?: AbortOptions): Promise { + try { + await Promise.all( + this.streams.map(async stream => stream.close(options)) + ) + } catch (err: any) { + this.abort(err) + } + } + + /** + * Abort all tracked streams and stop the muxer + */ + abort (err: Error): void { + for (const stream of this.streams) { + stream.abort(err) + } + } + + /** + * The stream source, a no-op as the transport natively supports multiplexing + */ + source: AsyncGenerator = nopSource() + + /** + * The stream destination, a no-op as the transport natively supports multiplexing + */ + sink: Sink, Promise> = nopSink + newStream (): Stream { // The spec says the label SHOULD be an empty string: https://github.com/libp2p/specs/blob/master/webrtc/README.md#rtcdatachannel-label const channel = this.peerConnection.createDataChannel('') const stream = createStream({ channel, direction: 'outbound', - dataChannelOptions: this.dataChannelOptions, onEnd: () => { + log.trace('stream %s %s %s onEnd', stream.direction, stream.id, stream.protocol) + drainAndClose(channel, `outbound ${stream.id} ${stream.protocol}`, this.dataChannelOptions.drainTimeout) this.streams = this.streams.filter(s => s.id !== stream.id) this.metrics?.increment({ stream_end: true }) this.init?.onStreamEnd?.(stream) - } + }, + ...this.dataChannelOptions }) this.streams.push(stream) this.metrics?.increment({ outgoing_stream: true }) diff --git a/packages/transport-webrtc/src/pb/message.proto b/packages/transport-webrtc/src/pb/message.proto index 9301bd802b..ea1ae55b99 100644 --- a/packages/transport-webrtc/src/pb/message.proto +++ b/packages/transport-webrtc/src/pb/message.proto @@ -2,7 +2,8 @@ syntax = "proto3"; message Message { enum Flag { - // The sender will no longer send messages on the stream. + // The sender will no longer send messages on the stream. The recipient + // should send a FIN_ACK back to the sender. FIN = 0; // The sender will no longer read messages on the stream. Incoming data is @@ -12,6 +13,10 @@ message Message { // The sender abruptly terminates the sending part of the stream. The // receiver can discard any data that it already received on that stream. RESET = 2; + + // The sender previously received a FIN. + // Workaround for https://bugs.chromium.org/p/chromium/issues/detail?id=1484907 + FIN_ACK = 3; } optional Flag flag = 1; diff --git a/packages/transport-webrtc/src/pb/message.ts b/packages/transport-webrtc/src/pb/message.ts index a74ca6dd06..f8abb7a4a9 100644 --- a/packages/transport-webrtc/src/pb/message.ts +++ b/packages/transport-webrtc/src/pb/message.ts @@ -17,13 +17,15 @@ export namespace Message { export enum Flag { FIN = 'FIN', STOP_SENDING = 'STOP_SENDING', - RESET = 'RESET' + RESET = 'RESET', + FIN_ACK = 'FIN_ACK' } enum __FlagValues { FIN = 0, STOP_SENDING = 1, - RESET = 2 + RESET = 2, + FIN_ACK = 3 } export namespace Flag { diff --git a/packages/transport-webrtc/src/private-to-private/handler.ts b/packages/transport-webrtc/src/private-to-private/handler.ts deleted file mode 100644 index 0f4f055b33..0000000000 --- a/packages/transport-webrtc/src/private-to-private/handler.ts +++ /dev/null @@ -1,177 +0,0 @@ -import { CodeError } from '@libp2p/interface/errors' -import { logger } from '@libp2p/logger' -import { abortableDuplex } from 'abortable-iterator' -import { pbStream } from 'it-protobuf-stream' -import pDefer, { type DeferredPromise } from 'p-defer' -import { DataChannelMuxerFactory } from '../muxer.js' -import { RTCPeerConnection, RTCSessionDescription } from '../webrtc/index.js' -import { Message } from './pb/message.js' -import { readCandidatesUntilConnected, resolveOnConnected } from './util.js' -import type { DataChannelOpts } from '../stream.js' -import type { Stream } from '@libp2p/interface/connection' -import type { StreamMuxerFactory } from '@libp2p/interface/stream-muxer' -import type { IncomingStreamData } from '@libp2p/interface-internal/registrar' - -const DEFAULT_TIMEOUT = 30 * 1000 - -const log = logger('libp2p:webrtc:peer') - -export type IncomingStreamOpts = { rtcConfiguration?: RTCConfiguration, dataChannelOptions?: Partial } & IncomingStreamData - -export async function handleIncomingStream ({ rtcConfiguration, dataChannelOptions, stream: rawStream }: IncomingStreamOpts): Promise<{ pc: RTCPeerConnection, muxerFactory: StreamMuxerFactory, remoteAddress: string }> { - const signal = AbortSignal.timeout(DEFAULT_TIMEOUT) - const stream = pbStream(abortableDuplex(rawStream, signal)).pb(Message) - const pc = new RTCPeerConnection(rtcConfiguration) - - try { - const muxerFactory = new DataChannelMuxerFactory({ peerConnection: pc, dataChannelOptions }) - const connectedPromise: DeferredPromise = pDefer() - const answerSentPromise: DeferredPromise = pDefer() - - signal.onabort = () => { - connectedPromise.reject(new CodeError('Timed out while trying to connect', 'ERR_TIMEOUT')) - } - // candidate callbacks - pc.onicecandidate = ({ candidate }) => { - answerSentPromise.promise.then( - async () => { - await stream.write({ - type: Message.Type.ICE_CANDIDATE, - data: (candidate != null) ? JSON.stringify(candidate.toJSON()) : '' - }) - }, - (err) => { - log.error('cannot set candidate since sending answer failed', err) - connectedPromise.reject(err) - } - ) - } - - resolveOnConnected(pc, connectedPromise) - - // read an SDP offer - const pbOffer = await stream.read() - if (pbOffer.type !== Message.Type.SDP_OFFER) { - throw new Error(`expected message type SDP_OFFER, received: ${pbOffer.type ?? 'undefined'} `) - } - const offer = new RTCSessionDescription({ - type: 'offer', - sdp: pbOffer.data - }) - - await pc.setRemoteDescription(offer).catch(err => { - log.error('could not execute setRemoteDescription', err) - throw new Error('Failed to set remoteDescription') - }) - - // create and write an SDP answer - const answer = await pc.createAnswer().catch(err => { - log.error('could not execute createAnswer', err) - answerSentPromise.reject(err) - throw new Error('Failed to create answer') - }) - // write the answer to the remote - await stream.write({ type: Message.Type.SDP_ANSWER, data: answer.sdp }) - - await pc.setLocalDescription(answer).catch(err => { - log.error('could not execute setLocalDescription', err) - answerSentPromise.reject(err) - throw new Error('Failed to set localDescription') - }) - - answerSentPromise.resolve() - - // wait until candidates are connected - await readCandidatesUntilConnected(connectedPromise, pc, stream) - - const remoteAddress = parseRemoteAddress(pc.currentRemoteDescription?.sdp ?? '') - - return { pc, muxerFactory, remoteAddress } - } catch (err) { - pc.close() - throw err - } -} - -export interface ConnectOptions { - stream: Stream - signal: AbortSignal - rtcConfiguration?: RTCConfiguration - dataChannelOptions?: Partial -} - -export async function initiateConnection ({ rtcConfiguration, dataChannelOptions, signal, stream: rawStream }: ConnectOptions): Promise<{ pc: RTCPeerConnection, muxerFactory: StreamMuxerFactory, remoteAddress: string }> { - const stream = pbStream(abortableDuplex(rawStream, signal)).pb(Message) - // setup peer connection - const pc = new RTCPeerConnection(rtcConfiguration) - - try { - const muxerFactory = new DataChannelMuxerFactory({ peerConnection: pc, dataChannelOptions }) - - const connectedPromise: DeferredPromise = pDefer() - resolveOnConnected(pc, connectedPromise) - - // reject the connectedPromise if the signal aborts - signal.onabort = connectedPromise.reject - // we create the channel so that the peerconnection has a component for which - // to collect candidates. The label is not relevant to connection initiation - // but can be useful for debugging - const channel = pc.createDataChannel('init') - // setup callback to write ICE candidates to the remote - // peer - pc.onicecandidate = ({ candidate }) => { - void stream.write({ - type: Message.Type.ICE_CANDIDATE, - data: (candidate != null) ? JSON.stringify(candidate.toJSON()) : '' - }) - .catch(err => { - log.error('error sending ICE candidate', err) - }) - } - - // create an offer - const offerSdp = await pc.createOffer() - // write the offer to the stream - await stream.write({ type: Message.Type.SDP_OFFER, data: offerSdp.sdp }) - // set offer as local description - await pc.setLocalDescription(offerSdp).catch(err => { - log.error('could not execute setLocalDescription', err) - throw new Error('Failed to set localDescription') - }) - - // read answer - const answerMessage = await stream.read() - if (answerMessage.type !== Message.Type.SDP_ANSWER) { - throw new Error('remote should send an SDP answer') - } - - const answerSdp = new RTCSessionDescription({ type: 'answer', sdp: answerMessage.data }) - await pc.setRemoteDescription(answerSdp).catch(err => { - log.error('could not execute setRemoteDescription', err) - throw new Error('Failed to set remoteDescription') - }) - - await readCandidatesUntilConnected(connectedPromise, pc, stream) - channel.close() - - const remoteAddress = parseRemoteAddress(pc.currentRemoteDescription?.sdp ?? '') - - return { pc, muxerFactory, remoteAddress } - } catch (err) { - pc.close() - throw err - } -} - -function parseRemoteAddress (sdp: string): string { - // 'a=candidate:1746876089 1 udp 2113937151 0614fbad-b...ocal 54882 typ host generation 0 network-cost 999' - const candidateLine = sdp.split('\r\n').filter(line => line.startsWith('a=candidate')).pop() - const candidateParts = candidateLine?.split(' ') - - if (candidateLine == null || candidateParts == null || candidateParts.length < 5) { - log('could not parse remote address from', candidateLine) - return '/webrtc' - } - - return `/dnsaddr/${candidateParts[4]}/${candidateParts[2].toLowerCase()}/${candidateParts[3]}/webrtc` -} diff --git a/packages/transport-webrtc/src/private-to-private/initiate-connection.ts b/packages/transport-webrtc/src/private-to-private/initiate-connection.ts new file mode 100644 index 0000000000..408bef0ac1 --- /dev/null +++ b/packages/transport-webrtc/src/private-to-private/initiate-connection.ts @@ -0,0 +1,191 @@ +import { CodeError } from '@libp2p/interface/errors' +import { logger } from '@libp2p/logger' +import { peerIdFromString } from '@libp2p/peer-id' +import { multiaddr, type Multiaddr } from '@multiformats/multiaddr' +import { pbStream } from 'it-protobuf-stream' +import pDefer, { type DeferredPromise } from 'p-defer' +import { type RTCPeerConnection, RTCSessionDescription } from '../webrtc/index.js' +import { Message } from './pb/message.js' +import { SIGNALING_PROTO_ID, splitAddr, type WebRTCTransportMetrics } from './transport.js' +import { parseRemoteAddress, readCandidatesUntilConnected, resolveOnConnected } from './util.js' +import type { DataChannelOptions } from '../index.js' +import type { Connection } from '@libp2p/interface/connection' +import type { ConnectionManager } from '@libp2p/interface-internal/connection-manager' +import type { IncomingStreamData } from '@libp2p/interface-internal/registrar' +import type { TransportManager } from '@libp2p/interface-internal/transport-manager' + +const log = logger('libp2p:webrtc:initiate-connection') + +export interface IncomingStreamOpts extends IncomingStreamData { + rtcConfiguration?: RTCConfiguration + dataChannelOptions?: Partial + signal: AbortSignal +} + +export interface ConnectOptions { + peerConnection: RTCPeerConnection + multiaddr: Multiaddr + connectionManager: ConnectionManager + transportManager: TransportManager + dataChannelOptions?: Partial + signal?: AbortSignal + metrics?: WebRTCTransportMetrics +} + +export async function initiateConnection ({ peerConnection, signal, metrics, multiaddr: ma, connectionManager, transportManager }: ConnectOptions): Promise<{ remoteAddress: Multiaddr }> { + const { baseAddr, peerId } = splitAddr(ma) + + metrics?.dialerEvents.increment({ open: true }) + + log.trace('dialing base address: %a', baseAddr) + + const relayPeer = baseAddr.getPeerId() + + if (relayPeer == null) { + throw new CodeError('Relay peer was missing', 'ERR_INVALID_ADDRESS') + } + + const connections = connectionManager.getConnections(peerIdFromString(relayPeer)) + let connection: Connection + let shouldCloseConnection = false + + if (connections.length === 0) { + // use the transport manager to open a connection. Initiating a WebRTC + // connection takes place in the context of a dial - if we use the + // connection manager instead we can end up joining our own dial context + connection = await transportManager.dial(baseAddr, { + signal + }) + // this connection is unmanaged by the connection manager so we should + // close it when we are done + shouldCloseConnection = true + } else { + connection = connections[0] + } + + try { + const stream = await connection.newStream(SIGNALING_PROTO_ID, { + signal, + runOnTransientConnection: true + }) + + const messageStream = pbStream(stream).pb(Message) + const connectedPromise: DeferredPromise = pDefer() + const sdpAbortedListener = (): void => { + connectedPromise.reject(new CodeError('SDP handshake aborted', 'ERR_SDP_HANDSHAKE_ABORTED')) + } + + try { + resolveOnConnected(peerConnection, connectedPromise) + + // reject the connectedPromise if the signal aborts + signal?.addEventListener('abort', sdpAbortedListener) + + // we create the channel so that the RTCPeerConnection has a component for + // which to collect candidates. The label is not relevant to connection + // initiation but can be useful for debugging + const channel = peerConnection.createDataChannel('init') + + // setup callback to write ICE candidates to the remote peer + peerConnection.onicecandidate = ({ candidate }) => { + // a null candidate means end-of-candidates, an empty string candidate + // means end-of-candidates for this generation, otherwise this should + // be a valid candidate object + // see - https://www.w3.org/TR/webrtc/#rtcpeerconnectioniceevent + const data = JSON.stringify(candidate?.toJSON() ?? null) + + log.trace('initiator sending ICE candidate %s', data) + + void messageStream.write({ + type: Message.Type.ICE_CANDIDATE, + data + }, { + signal + }) + .catch(err => { + log.error('error sending ICE candidate', err) + }) + } + peerConnection.onicecandidateerror = (event) => { + log('initiator ICE candidate error', event) + } + + // create an offer + const offerSdp = await peerConnection.createOffer() + + log.trace('initiator send SDP offer %s', offerSdp.sdp) + + // write the offer to the stream + await messageStream.write({ type: Message.Type.SDP_OFFER, data: offerSdp.sdp }, { + signal + }) + + // set offer as local description + await peerConnection.setLocalDescription(offerSdp).catch(err => { + log.error('could not execute setLocalDescription', err) + throw new CodeError('Failed to set localDescription', 'ERR_SDP_HANDSHAKE_FAILED') + }) + + // read answer + const answerMessage = await messageStream.read({ + signal + }) + + if (answerMessage.type !== Message.Type.SDP_ANSWER) { + throw new CodeError('remote should send an SDP answer', 'ERR_SDP_HANDSHAKE_FAILED') + } + + log.trace('initiator receive SDP answer %s', answerMessage.data) + + const answerSdp = new RTCSessionDescription({ type: 'answer', sdp: answerMessage.data }) + await peerConnection.setRemoteDescription(answerSdp).catch(err => { + log.error('could not execute setRemoteDescription', err) + throw new CodeError('Failed to set remoteDescription', 'ERR_SDP_HANDSHAKE_FAILED') + }) + + log.trace('initiator read candidates until connected') + + await readCandidatesUntilConnected(connectedPromise, peerConnection, messageStream, { + direction: 'initiator', + signal + }) + + log.trace('initiator connected, closing init channel') + channel.close() + + log.trace('initiator closing signalling stream') + await messageStream.unwrap().unwrap().close({ + signal + }) + + const remoteAddress = parseRemoteAddress(peerConnection.currentRemoteDescription?.sdp ?? '') + + log.trace('initiator connected to remote address %s', remoteAddress) + + return { + remoteAddress: multiaddr(remoteAddress).encapsulate(`/p2p/${peerId.toString()}`) + } + } catch (err: any) { + peerConnection.close() + stream.abort(err) + throw err + } finally { + // remove event listeners + signal?.removeEventListener('abort', sdpAbortedListener) + peerConnection.onicecandidate = null + peerConnection.onicecandidateerror = null + } + } finally { + // if we had to open a connection to perform the SDP handshake + // close it because it's not tracked by the connection manager + if (shouldCloseConnection) { + try { + await connection.close({ + signal + }) + } catch (err: any) { + connection.abort(err) + } + } + } +} diff --git a/packages/transport-webrtc/src/private-to-private/listener.ts b/packages/transport-webrtc/src/private-to-private/listener.ts index 1dccac6e25..53a3d299c6 100644 --- a/packages/transport-webrtc/src/private-to-private/listener.ts +++ b/packages/transport-webrtc/src/private-to-private/listener.ts @@ -5,20 +5,27 @@ import type { ListenerEvents, Listener } from '@libp2p/interface/transport' import type { TransportManager } from '@libp2p/interface-internal/transport-manager' import type { Multiaddr } from '@multiformats/multiaddr' -export interface ListenerOptions { +export interface WebRTCPeerListenerComponents { peerId: PeerId transportManager: TransportManager } +export interface WebRTCPeerListenerInit { + shutdownController: AbortController +} + export class WebRTCPeerListener extends EventEmitter implements Listener { private readonly peerId: PeerId private readonly transportManager: TransportManager + private readonly shutdownController: AbortController - constructor (opts: ListenerOptions) { + constructor (components: WebRTCPeerListenerComponents, init: WebRTCPeerListenerInit) { super() - this.peerId = opts.peerId - this.transportManager = opts.transportManager + this.peerId = components.peerId + this.transportManager = components.transportManager + + this.shutdownController = init.shutdownController } async listen (): Promise { @@ -39,6 +46,7 @@ export class WebRTCPeerListener extends EventEmitter implements } async close (): Promise { + this.shutdownController.abort() this.safeDispatchEvent('close', {}) } } diff --git a/packages/transport-webrtc/src/private-to-private/signaling-stream-handler.ts b/packages/transport-webrtc/src/private-to-private/signaling-stream-handler.ts new file mode 100644 index 0000000000..69db46361c --- /dev/null +++ b/packages/transport-webrtc/src/private-to-private/signaling-stream-handler.ts @@ -0,0 +1,129 @@ +import { CodeError } from '@libp2p/interface/errors' +import { logger } from '@libp2p/logger' +import { pbStream } from 'it-protobuf-stream' +import pDefer, { type DeferredPromise } from 'p-defer' +import { type RTCPeerConnection, RTCSessionDescription } from '../webrtc/index.js' +import { Message } from './pb/message.js' +import { parseRemoteAddress, readCandidatesUntilConnected, resolveOnConnected } from './util.js' +import type { IncomingStreamData } from '@libp2p/interface-internal/registrar' + +const log = logger('libp2p:webrtc:signaling-stream-handler') + +export interface IncomingStreamOpts extends IncomingStreamData { + peerConnection: RTCPeerConnection + signal: AbortSignal +} + +export async function handleIncomingStream ({ peerConnection, stream, signal, connection }: IncomingStreamOpts): Promise<{ remoteAddress: string }> { + log.trace('new inbound signaling stream') + + const messageStream = pbStream(stream).pb(Message) + + try { + const connectedPromise: DeferredPromise = pDefer() + const answerSentPromise: DeferredPromise = pDefer() + + signal.onabort = () => { + connectedPromise.reject(new CodeError('Timed out while trying to connect', 'ERR_TIMEOUT')) + } + + // candidate callbacks + peerConnection.onicecandidate = ({ candidate }) => { + answerSentPromise.promise.then( + async () => { + // a null candidate means end-of-candidates, an empty string candidate + // means end-of-candidates for this generation, otherwise this should + // be a valid candidate object + // see - https://www.w3.org/TR/webrtc/#rtcpeerconnectioniceevent + const data = JSON.stringify(candidate?.toJSON() ?? null) + + log.trace('recipient sending ICE candidate %s', data) + + await messageStream.write({ + type: Message.Type.ICE_CANDIDATE, + data + }, { + signal + }) + }, + (err) => { + log.error('cannot set candidate since sending answer failed', err) + connectedPromise.reject(err) + } + ) + } + + resolveOnConnected(peerConnection, connectedPromise) + + // read an SDP offer + const pbOffer = await messageStream.read({ + signal + }) + + if (pbOffer.type !== Message.Type.SDP_OFFER) { + throw new CodeError(`expected message type SDP_OFFER, received: ${pbOffer.type ?? 'undefined'} `, 'ERR_SDP_HANDSHAKE_FAILED') + } + + log.trace('recipient receive SDP offer %s', pbOffer.data) + + const offer = new RTCSessionDescription({ + type: 'offer', + sdp: pbOffer.data + }) + + await peerConnection.setRemoteDescription(offer).catch(err => { + log.error('could not execute setRemoteDescription', err) + throw new CodeError('Failed to set remoteDescription', 'ERR_SDP_HANDSHAKE_FAILED') + }) + + // create and write an SDP answer + const answer = await peerConnection.createAnswer().catch(err => { + log.error('could not execute createAnswer', err) + answerSentPromise.reject(err) + throw new CodeError('Failed to create answer', 'ERR_SDP_HANDSHAKE_FAILED') + }) + + log.trace('recipient send SDP answer %s', answer.sdp) + + // write the answer to the remote + await messageStream.write({ type: Message.Type.SDP_ANSWER, data: answer.sdp }, { + signal + }) + + await peerConnection.setLocalDescription(answer).catch(err => { + log.error('could not execute setLocalDescription', err) + answerSentPromise.reject(err) + throw new CodeError('Failed to set localDescription', 'ERR_SDP_HANDSHAKE_FAILED') + }) + + answerSentPromise.resolve() + + log.trace('recipient read candidates until connected') + + // wait until candidates are connected + await readCandidatesUntilConnected(connectedPromise, peerConnection, messageStream, { + direction: 'recipient', + signal + }) + + log.trace('recipient connected, closing signaling stream') + await messageStream.unwrap().unwrap().close({ + signal + }) + } catch (err: any) { + if (peerConnection.connectionState !== 'connected') { + log.error('error while handling signaling stream from peer %a', connection.remoteAddr, err) + + peerConnection.close() + throw err + } else { + log('error while handling signaling stream from peer %a, ignoring as the RTCPeerConnection is already connected', connection.remoteAddr, err) + } + } + + const remoteAddress = parseRemoteAddress(peerConnection.currentRemoteDescription?.sdp ?? '') + + log.trace('recipient connected to remote address %s', remoteAddress) + + return { remoteAddress } +} diff --git a/packages/transport-webrtc/src/private-to-private/transport.ts b/packages/transport-webrtc/src/private-to-private/transport.ts index 7f93ac13e8..af5c3ea284 100644 --- a/packages/transport-webrtc/src/private-to-private/transport.ts +++ b/packages/transport-webrtc/src/private-to-private/transport.ts @@ -2,29 +2,40 @@ import { CodeError } from '@libp2p/interface/errors' import { type CreateListenerOptions, type DialOptions, symbol, type Transport, type Listener, type Upgrader } from '@libp2p/interface/transport' import { logger } from '@libp2p/logger' import { peerIdFromString } from '@libp2p/peer-id' -import { multiaddr, type Multiaddr, protocols } from '@multiformats/multiaddr' +import { multiaddr, type Multiaddr } from '@multiformats/multiaddr' +import { WebRTC } from '@multiformats/multiaddr-matcher' import { codes } from '../error.js' import { WebRTCMultiaddrConnection } from '../maconn.js' -import { cleanup } from '../webrtc/index.js' -import { initiateConnection, handleIncomingStream } from './handler.js' +import { DataChannelMuxerFactory } from '../muxer.js' +import { cleanup, RTCPeerConnection } from '../webrtc/index.js' +import { initiateConnection } from './initiate-connection.js' import { WebRTCPeerListener } from './listener.js' -import type { DataChannelOpts } from '../stream.js' +import { handleIncomingStream } from './signaling-stream-handler.js' +import type { DataChannelOptions } from '../index.js' import type { Connection } from '@libp2p/interface/connection' import type { PeerId } from '@libp2p/interface/peer-id' +import type { CounterGroup, Metrics } from '@libp2p/interface/src/metrics/index.js' import type { Startable } from '@libp2p/interface/startable' import type { IncomingStreamData, Registrar } from '@libp2p/interface-internal/registrar' +import type { ConnectionManager } from '@libp2p/interface-internal/src/connection-manager/index.js' import type { TransportManager } from '@libp2p/interface-internal/transport-manager' const log = logger('libp2p:webrtc:peer') const WEBRTC_TRANSPORT = '/webrtc' const CIRCUIT_RELAY_TRANSPORT = '/p2p-circuit' -const SIGNALING_PROTO_ID = '/webrtc-signaling/0.0.1' -const WEBRTC_CODE = protocols('webrtc').code +export const SIGNALING_PROTO_ID = '/webrtc-signaling/0.0.1' +const INBOUND_CONNECTION_TIMEOUT = 30 * 1000 export interface WebRTCTransportInit { rtcConfiguration?: RTCConfiguration - dataChannel?: Partial + dataChannel?: DataChannelOptions + + /** + * Inbound connections must complete the upgrade within this many ms + * (default: 30s) + */ + inboundConnectionTimeout?: number } export interface WebRTCTransportComponents { @@ -32,15 +43,38 @@ export interface WebRTCTransportComponents { registrar: Registrar upgrader: Upgrader transportManager: TransportManager + connectionManager: ConnectionManager + metrics?: Metrics +} + +export interface WebRTCTransportMetrics { + dialerEvents: CounterGroup + listenerEvents: CounterGroup } export class WebRTCTransport implements Transport, Startable { private _started = false + private readonly metrics?: WebRTCTransportMetrics + private readonly shutdownController: AbortController constructor ( private readonly components: WebRTCTransportComponents, private readonly init: WebRTCTransportInit = {} ) { + this.shutdownController = new AbortController() + + if (components.metrics != null) { + this.metrics = { + dialerEvents: components.metrics.registerCounterGroup('libp2p_webrtc_dialer_events_total', { + label: 'event', + help: 'Total count of WebRTC dialer events by type' + }), + listenerEvents: components.metrics.registerCounterGroup('libp2p_webrtc_listener_events_total', { + label: 'event', + help: 'Total count of WebRTC listener events by type' + }) + } + } } isStarted (): boolean { @@ -63,7 +97,9 @@ export class WebRTCTransport implements Transport, Startable { } createListener (options: CreateListenerOptions): Listener { - return new WebRTCPeerListener(this.components) + return new WebRTCPeerListener(this.components, { + shutdownController: this.shutdownController + }) } readonly [Symbol.toStringTag] = '@libp2p/webrtc' @@ -71,10 +107,7 @@ export class WebRTCTransport implements Transport, Startable { readonly [symbol] = true filter (multiaddrs: Multiaddr[]): Multiaddr[] { - return multiaddrs.filter((ma) => { - const codes = ma.protoCodes() - return codes.includes(WEBRTC_CODE) - }) + return multiaddrs.filter(WebRTC.exactMatch) } /* @@ -85,80 +118,96 @@ export class WebRTCTransport implements Transport, Startable { * /p2p//p2p-circuit/webrtc/p2p/ */ async dial (ma: Multiaddr, options: DialOptions): Promise { - log.trace('dialing address: ', ma) - const { baseAddr, peerId } = splitAddr(ma) + log.trace('dialing address: %a', ma) - if (options.signal == null) { - const controller = new AbortController() - options.signal = controller.signal - } + const peerConnection = new RTCPeerConnection(this.init.rtcConfiguration) + const muxerFactory = new DataChannelMuxerFactory({ + peerConnection, + dataChannelOptions: this.init.dataChannel + }) - const connection = await this.components.transportManager.dial(baseAddr, options) - const signalingStream = await connection.newStream(SIGNALING_PROTO_ID, { - ...options, - runOnTransientConnection: true + const { remoteAddress } = await initiateConnection({ + peerConnection, + multiaddr: ma, + dataChannelOptions: this.init.dataChannel, + signal: options.signal, + connectionManager: this.components.connectionManager, + transportManager: this.components.transportManager }) - try { - const { pc, muxerFactory, remoteAddress } = await initiateConnection({ - stream: signalingStream, - rtcConfiguration: this.init.rtcConfiguration, - dataChannelOptions: this.init.dataChannel, - signal: options.signal - }) + const webRTCConn = new WebRTCMultiaddrConnection({ + peerConnection, + timeline: { open: Date.now() }, + remoteAddr: remoteAddress, + metrics: this.metrics?.dialerEvents + }) - const result = await options.upgrader.upgradeOutbound( - new WebRTCMultiaddrConnection({ - peerConnection: pc, - timeline: { open: Date.now() }, - remoteAddr: multiaddr(remoteAddress).encapsulate(`/p2p/${peerId.toString()}`) - }), - { - skipProtection: true, - skipEncryption: true, - muxerFactory - } - ) - - // close the stream if SDP has been exchanged successfully - await signalingStream.close() - return result - } catch (err: any) { - // reset the stream in case of any error - signalingStream.abort(err) - throw err - } finally { - // Close the signaling connection - await connection.close() - } + const connection = await options.upgrader.upgradeOutbound(webRTCConn, { + skipProtection: true, + skipEncryption: true, + muxerFactory + }) + + // close the connection on shut down + this._closeOnShutdown(peerConnection, webRTCConn) + + return connection } async _onProtocol ({ connection, stream }: IncomingStreamData): Promise { + const signal = AbortSignal.timeout(this.init.inboundConnectionTimeout ?? INBOUND_CONNECTION_TIMEOUT) + const peerConnection = new RTCPeerConnection(this.init.rtcConfiguration) + const muxerFactory = new DataChannelMuxerFactory({ peerConnection, dataChannelOptions: this.init.dataChannel }) + try { - const { pc, muxerFactory, remoteAddress } = await handleIncomingStream({ - rtcConfiguration: this.init.rtcConfiguration, + const { remoteAddress } = await handleIncomingStream({ + peerConnection, connection, stream, - dataChannelOptions: this.init.dataChannel + signal }) - await this.components.upgrader.upgradeInbound(new WebRTCMultiaddrConnection({ - peerConnection: pc, + const webRTCConn = new WebRTCMultiaddrConnection({ + peerConnection, timeline: { open: (new Date()).getTime() }, - remoteAddr: multiaddr(remoteAddress).encapsulate(`/p2p/${connection.remotePeer.toString()}`) - }), { + remoteAddr: multiaddr(remoteAddress).encapsulate(`/p2p/${connection.remotePeer.toString()}`), + metrics: this.metrics?.listenerEvents + }) + + // close the connection on shut down + this._closeOnShutdown(peerConnection, webRTCConn) + + await this.components.upgrader.upgradeInbound(webRTCConn, { skipEncryption: true, skipProtection: true, muxerFactory }) + + // close the stream if SDP messages have been exchanged successfully + await stream.close({ + signal + }) } catch (err: any) { stream.abort(err) throw err - } finally { - // Close the signaling connection - await connection.close() } } + + private _closeOnShutdown (pc: RTCPeerConnection, webRTCConn: WebRTCMultiaddrConnection): void { + // close the connection on shut down + const shutDownListener = (): void => { + webRTCConn.close() + .catch(err => { + log.error('could not close WebRTCMultiaddrConnection', err) + }) + } + + this.shutdownController.signal.addEventListener('abort', shutDownListener) + + pc.addEventListener('close', () => { + this.shutdownController.signal.removeEventListener('abort', shutDownListener) + }) + } } export function splitAddr (ma: Multiaddr): { baseAddr: Multiaddr, peerId: PeerId } { diff --git a/packages/transport-webrtc/src/private-to-private/util.ts b/packages/transport-webrtc/src/private-to-private/util.ts index 6d2b97898d..b892e1d4b6 100644 --- a/packages/transport-webrtc/src/private-to-private/util.ts +++ b/packages/transport-webrtc/src/private-to-private/util.ts @@ -1,49 +1,101 @@ +import { CodeError } from '@libp2p/interface/errors' import { logger } from '@libp2p/logger' +import { abortableSource } from 'abortable-iterator' +import { anySignal } from 'any-signal' +import * as lp from 'it-length-prefixed' +import { AbortError, raceSignal } from 'race-signal' import { isFirefox } from '../util.js' import { RTCIceCandidate } from '../webrtc/index.js' import { Message } from './pb/message.js' +import type { Stream } from '@libp2p/interface/connection' +import type { AbortOptions, MessageStream } from 'it-protobuf-stream' import type { DeferredPromise } from 'p-defer' -interface MessageStream { - read: () => Promise - write: (d: Message) => void | Promise +const log = logger('libp2p:webrtc:peer:util') + +export interface ReadCandidatesOptions extends AbortOptions { + direction: string } -const log = logger('libp2p:webrtc:peer:util') +export const readCandidatesUntilConnected = async (connectedPromise: DeferredPromise, pc: RTCPeerConnection, stream: MessageStream, options: ReadCandidatesOptions): Promise => { + // if we connect, stop trying to read from the stream + const controller = new AbortController() + connectedPromise.promise.then(() => { + controller.abort() + }, () => { + controller.abort() + }) + + const signal = anySignal([ + controller.signal, + options.signal + ]) + + const source = abortableSource(stream.unwrap().unwrap().source, signal, { + returnOnAbort: true + }) + + try { + // read candidates until we are connected or we reach the end of the stream + for await (const buf of lp.decode(source)) { + const message = Message.decode(buf) -export const readCandidatesUntilConnected = async (connectedPromise: DeferredPromise, pc: RTCPeerConnection, stream: MessageStream): Promise => { - while (true) { - const readResult = await Promise.race([connectedPromise.promise, stream.read()]) - // check if readResult is a message - if (readResult instanceof Object) { - const message = readResult if (message.type !== Message.Type.ICE_CANDIDATE) { - throw new Error('expected only ice candidates') + throw new CodeError('ICE candidate message expected', 'ERR_NOT_ICE_CANDIDATE') } - // end of candidates has been signalled - if (message.data == null || message.data === '') { + + let candidateInit = JSON.parse(message.data ?? 'null') + + if (candidateInit === '') { + log.trace('end-of-candidates for this generation received') + candidateInit = { + candidate: '', + sdpMid: '0', + sdpMLineIndex: 0 + } + } + + if (candidateInit === null) { log.trace('end-of-candidates received') - break + candidateInit = { + candidate: null, + sdpMid: '0', + sdpMLineIndex: 0 + } } - log.trace('received new ICE candidate: %s', message.data) + // a null candidate means end-of-candidates + // see - https://www.w3.org/TR/webrtc/#rtcpeerconnectioniceevent + const candidate = new RTCIceCandidate(candidateInit) + + log.trace('%s received new ICE candidate', options.direction, candidate) + try { - await pc.addIceCandidate(new RTCIceCandidate(JSON.parse(message.data))) + await pc.addIceCandidate(candidate) } catch (err) { - log.error('bad candidate received: ', err) - throw new Error('bad candidate received') + log.error('%s bad candidate received', options.direction, err) } - } else { - // connected promise resolved - break } + } catch (err) { + log.error('%s error parsing ICE candidate', options.direction, err) + } finally { + signal.clear() } - await connectedPromise.promise + + if (options.signal?.aborted === true) { + throw new AbortError('Aborted while reading ICE candidates', 'ERR_ICE_CANDIDATES_READ_ABORTED') + } + + // read all available ICE candidates, wait for connection state change + await raceSignal(connectedPromise.promise, options.signal, { + errorMessage: 'Aborted before connected', + errorCode: 'ERR_ABORTED_BEFORE_CONNECTED' + }) } export function resolveOnConnected (pc: RTCPeerConnection, promise: DeferredPromise): void { pc[isFirefox ? 'oniceconnectionstatechange' : 'onconnectionstatechange'] = (_) => { - log.trace('receiver peerConnectionState state: ', pc.connectionState) + log.trace('receiver peerConnectionState state change: %s', pc.connectionState) switch (isFirefox ? pc.iceConnectionState : pc.connectionState) { case 'connected': promise.resolve() @@ -51,10 +103,23 @@ export function resolveOnConnected (pc: RTCPeerConnection, promise: DeferredProm case 'failed': case 'disconnected': case 'closed': - promise.reject(new Error('RTCPeerConnection was closed')) + promise.reject(new CodeError('RTCPeerConnection was closed', 'ERR_CONNECTION_CLOSED_BEFORE_CONNECTED')) break default: break } } } + +export function parseRemoteAddress (sdp: string): string { + // 'a=candidate:1746876089 1 udp 2113937151 0614fbad-b...ocal 54882 typ host generation 0 network-cost 999' + const candidateLine = sdp.split('\r\n').filter(line => line.startsWith('a=candidate')).pop() + const candidateParts = candidateLine?.split(' ') + + if (candidateLine == null || candidateParts == null || candidateParts.length < 5) { + log('could not parse remote address from', candidateLine) + return '/webrtc' + } + + return `/dnsaddr/${candidateParts[4]}/${candidateParts[2].toLowerCase()}/${candidateParts[5]}/webrtc` +} diff --git a/packages/transport-webrtc/src/private-to-public/transport.ts b/packages/transport-webrtc/src/private-to-public/transport.ts index f824197378..a82f84a2b5 100644 --- a/packages/transport-webrtc/src/private-to-public/transport.ts +++ b/packages/transport-webrtc/src/private-to-public/transport.ts @@ -3,6 +3,7 @@ import { type CreateListenerOptions, symbol, type Transport, type Listener } fro import { logger } from '@libp2p/logger' import * as p from '@libp2p/peer-id' import { protocols } from '@multiformats/multiaddr' +import { WebRTCDirect } from '@multiformats/multiaddr-matcher' import * as multihashes from 'multihashes' import { concat } from 'uint8arrays/concat' import { fromString as uint8arrayFromString } from 'uint8arrays/from-string' @@ -15,7 +16,7 @@ import { RTCPeerConnection } from '../webrtc/index.js' import * as sdp from './sdp.js' import { genUfrag } from './util.js' import type { WebRTCDialOptions } from './options.js' -import type { DataChannelOpts } from '../stream.js' +import type { DataChannelOptions } from '../index.js' import type { Connection } from '@libp2p/interface/connection' import type { CounterGroup, Metrics } from '@libp2p/interface/metrics' import type { PeerId } from '@libp2p/interface/peer-id' @@ -55,7 +56,7 @@ export interface WebRTCMetrics { } export interface WebRTCTransportDirectInit { - dataChannel?: Partial + dataChannel?: DataChannelOptions } export class WebRTCDirectTransport implements Transport { @@ -67,9 +68,9 @@ export class WebRTCDirectTransport implements Transport { this.init = init if (components.metrics != null) { this.metrics = { - dialerEvents: components.metrics.registerCounterGroup('libp2p_webrtc_dialer_events_total', { + dialerEvents: components.metrics.registerCounterGroup('libp2p_webrtc-direct_dialer_events_total', { label: 'event', - help: 'Total count of WebRTC dial events by type' + help: 'Total count of WebRTC-direct dial events by type' }) } } @@ -80,7 +81,7 @@ export class WebRTCDirectTransport implements Transport { */ async dial (ma: Multiaddr, options: WebRTCDialOptions): Promise { const rawConn = await this._connect(ma, options) - log(`dialing address - ${ma.toString()}`) + log('dialing address: %a', ma) return rawConn } @@ -95,7 +96,7 @@ export class WebRTCDirectTransport implements Transport { * Takes a list of `Multiaddr`s and returns only valid addresses for the transport */ filter (multiaddrs: Multiaddr[]): Multiaddr[] { - return multiaddrs.filter(validMa) + return multiaddrs.filter(WebRTCDirect.exactMatch) } /** @@ -193,7 +194,7 @@ export class WebRTCDirectTransport implements Transport { // we pass in undefined for these parameters. const noise = Noise({ prologueBytes: fingerprintsPrologue })() - const wrappedChannel = createStream({ channel: handshakeDataChannel, direction: 'inbound', dataChannelOptions: this.init.dataChannel }) + const wrappedChannel = createStream({ channel: handshakeDataChannel, direction: 'inbound', ...(this.init.dataChannel ?? {}) }) const wrappedDuplex = { ...wrappedChannel, sink: wrappedChannel.sink.bind(wrappedChannel), @@ -275,12 +276,3 @@ export class WebRTCDirectTransport implements Transport { return concat([prefix, local, remote]) } } - -/** - * Determine if a given multiaddr contains a WebRTC Code (280), - * a Certhash Code (466) and a PeerId - */ -function validMa (ma: Multiaddr): boolean { - const codes = ma.protoCodes() - return codes.includes(WEBRTC_CODE) && codes.includes(CERTHASH_CODE) && ma.getPeerId() != null && !codes.includes(protocols('p2p-circuit').code) -} diff --git a/packages/transport-webrtc/src/stream.ts b/packages/transport-webrtc/src/stream.ts index 7b26bf7ac1..788fbf0dd4 100644 --- a/packages/transport-webrtc/src/stream.ts +++ b/packages/transport-webrtc/src/stream.ts @@ -3,20 +3,18 @@ import { AbstractStream, type AbstractStreamInit } from '@libp2p/interface/strea import { logger } from '@libp2p/logger' import * as lengthPrefixed from 'it-length-prefixed' import { type Pushable, pushable } from 'it-pushable' +import pDefer from 'p-defer' import { pEvent, TimeoutError } from 'p-event' +import pTimeout from 'p-timeout' +import { raceSignal } from 'race-signal' import { Uint8ArrayList } from 'uint8arraylist' import { Message } from './pb/message.js' +import type { DataChannelOptions } from './index.js' +import type { AbortOptions } from '@libp2p/interface' import type { Direction } from '@libp2p/interface/connection' +import type { DeferredPromise } from 'p-defer' -const log = logger('libp2p:webrtc:stream') - -export interface DataChannelOpts { - maxMessageSize: number - maxBufferedAmount: number - bufferedAmountLowEventTimeout: number -} - -export interface WebRTCStreamInit extends AbstractStreamInit { +export interface WebRTCStreamInit extends AbstractStreamInit, DataChannelOptions { /** * The network channel used for bidirectional peer-to-peer transfers of * arbitrary data @@ -24,23 +22,39 @@ export interface WebRTCStreamInit extends AbstractStreamInit { * {@link https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel} */ channel: RTCDataChannel - - dataChannelOptions?: Partial - - maxDataSize: number } -// Max message size that can be sent to the DataChannel -const MAX_MESSAGE_SIZE = 16 * 1024 - -// How much can be buffered to the DataChannel at once -const MAX_BUFFERED_AMOUNT = 16 * 1024 * 1024 - -// How long time we wait for the 'bufferedamountlow' event to be emitted -const BUFFERED_AMOUNT_LOW_TIMEOUT = 30 * 1000 - -// protobuf field definition overhead -const PROTOBUF_OVERHEAD = 3 +/** + * How much can be buffered to the DataChannel at once + */ +export const MAX_BUFFERED_AMOUNT = 16 * 1024 * 1024 + +/** + * How long time we wait for the 'bufferedamountlow' event to be emitted + */ +export const BUFFERED_AMOUNT_LOW_TIMEOUT = 30 * 1000 + +/** + * protobuf field definition overhead + */ +export const PROTOBUF_OVERHEAD = 5 + +/** + * Length of varint, in bytes + */ +export const VARINT_LENGTH = 2 + +/** + * Max message size that can be sent to the DataChannel + */ +export const MAX_MESSAGE_SIZE = 16 * 1024 + +/** + * When closing streams we send a FIN then wait for the remote to + * reply with a FIN_ACK. If that does not happen within this timeout + * we close the stream anyway. + */ +export const FIN_ACK_TIMEOUT = 5000 export class WebRTCStream extends AbstractStream { /** @@ -48,11 +62,6 @@ export class WebRTCStream extends AbstractStream { */ private readonly channel: RTCDataChannel - /** - * Data channel options - */ - private readonly dataChannelOptions: DataChannelOpts - /** * push data from the underlying datachannel to the length prefix decoder * and then the protobuf decoder. @@ -60,21 +69,66 @@ export class WebRTCStream extends AbstractStream { private readonly incomingData: Pushable private messageQueue?: Uint8ArrayList - private readonly maxDataSize: number + + private readonly maxBufferedAmount: number + + private readonly bufferedAmountLowEventTimeout: number + + /** + * The maximum size of a message in bytes + */ + private readonly maxMessageSize: number + + /** + * When this promise is resolved, the remote has sent us a FIN flag + */ + private readonly receiveFinAck: DeferredPromise + private readonly finAckTimeout: number + // private sentFinAck: boolean constructor (init: WebRTCStreamInit) { + // override onEnd to send/receive FIN_ACK before closing the stream + const originalOnEnd = init.onEnd + init.onEnd = (err?: Error): void => { + this.log.trace('readable and writeable ends closed', this.status) + + void Promise.resolve(async () => { + if (this.timeline.abort != null || this.timeline.reset !== null) { + return + } + + // wait for FIN_ACK if we haven't received it already + try { + await pTimeout(this.receiveFinAck.promise, { + milliseconds: this.finAckTimeout + }) + } catch (err) { + this.log.error('error receiving FIN_ACK', err) + } + }) + .then(() => { + // stop processing incoming messages + this.incomingData.end() + + // final cleanup + originalOnEnd?.(err) + }) + .catch(err => { + this.log.error('error ending stream', err) + }) + } + super(init) this.channel = init.channel this.channel.binaryType = 'arraybuffer' this.incomingData = pushable() this.messageQueue = new Uint8ArrayList() - this.dataChannelOptions = { - bufferedAmountLowEventTimeout: init.dataChannelOptions?.bufferedAmountLowEventTimeout ?? BUFFERED_AMOUNT_LOW_TIMEOUT, - maxBufferedAmount: init.dataChannelOptions?.maxBufferedAmount ?? MAX_BUFFERED_AMOUNT, - maxMessageSize: init.dataChannelOptions?.maxMessageSize ?? MAX_MESSAGE_SIZE - } - this.maxDataSize = init.maxDataSize + this.bufferedAmountLowEventTimeout = init.bufferedAmountLowEventTimeout ?? BUFFERED_AMOUNT_LOW_TIMEOUT + this.maxBufferedAmount = init.maxBufferedAmount ?? MAX_BUFFERED_AMOUNT + this.maxMessageSize = (init.maxMessageSize ?? MAX_MESSAGE_SIZE) - PROTOBUF_OVERHEAD - VARINT_LENGTH + this.receiveFinAck = pDefer() + this.finAckTimeout = init.closeTimeout ?? FIN_ACK_TIMEOUT // set up initial state switch (this.channel.readyState) { @@ -92,7 +146,7 @@ export class WebRTCStream extends AbstractStream { break default: - log.error('unknown datachannel state %s', this.channel.readyState) + this.log.error('unknown datachannel state %s', this.channel.readyState) throw new CodeError('Unknown datachannel state', 'ERR_INVALID_STATE') } @@ -100,19 +154,27 @@ export class WebRTCStream extends AbstractStream { this.channel.onopen = (_evt) => { this.timeline.open = new Date().getTime() - if (this.messageQueue != null) { + if (this.messageQueue != null && this.messageQueue.byteLength > 0) { + this.log.trace('dataChannel opened, sending queued messages', this.messageQueue.byteLength, this.channel.readyState) + // send any queued messages this._sendMessage(this.messageQueue) .catch(err => { + this.log.error('error sending queued messages', err) this.abort(err) }) - this.messageQueue = undefined } + + this.messageQueue = undefined } this.channel.onclose = (_evt) => { + // if the channel has closed we'll never receive a FIN_ACK so resolve the + // promise so we don't try to wait later + this.receiveFinAck.resolve() + void this.close().catch(err => { - log.error('error closing stream after channel closed', err) + this.log.error('error closing stream after channel closed', err) }) } @@ -121,8 +183,6 @@ export class WebRTCStream extends AbstractStream { this.abort(err) } - const self = this - this.channel.onmessage = async (event: MessageEvent) => { const { data } = event @@ -133,11 +193,13 @@ export class WebRTCStream extends AbstractStream { this.incomingData.push(new Uint8Array(data, 0, data.byteLength)) } + const self = this + // pipe framed protobuf messages through a length prefixed decoder, and // surface data from the `Message.message` field through a source. Promise.resolve().then(async () => { for await (const buf of lengthPrefixed.decode(this.incomingData)) { - const message = self.processIncomingProtobuf(buf.subarray()) + const message = self.processIncomingProtobuf(buf) if (message != null) { self.sourcePush(new Uint8ArrayList(message)) @@ -145,7 +207,7 @@ export class WebRTCStream extends AbstractStream { } }) .catch(err => { - log.error('error processing incoming data channel messages', err) + this.log.error('error processing incoming data channel messages', err) }) } @@ -154,12 +216,12 @@ export class WebRTCStream extends AbstractStream { } async _sendMessage (data: Uint8ArrayList, checkBuffer: boolean = true): Promise { - if (checkBuffer && this.channel.bufferedAmount > this.dataChannelOptions.maxBufferedAmount) { + if (checkBuffer && this.channel.bufferedAmount > this.maxBufferedAmount) { try { - await pEvent(this.channel, 'bufferedamountlow', { timeout: this.dataChannelOptions.bufferedAmountLowEventTimeout }) + await pEvent(this.channel, 'bufferedamountlow', { timeout: this.bufferedAmountLowEventTimeout }) } catch (err: any) { if (err instanceof TimeoutError) { - throw new Error('Timed out waiting for DataChannel buffer to clear') + throw new CodeError(`Timed out waiting for DataChannel buffer to clear after ${this.bufferedAmountLowEventTimeout}ms`, 'ERR_BUFFER_CLEAR_TIMEOUT') } throw err @@ -167,7 +229,7 @@ export class WebRTCStream extends AbstractStream { } if (this.channel.readyState === 'closed' || this.channel.readyState === 'closing') { - throw new CodeError('Invalid datachannel state - closed or closing', 'ERR_INVALID_STATE') + throw new CodeError(`Invalid datachannel state - ${this.channel.readyState}`, 'ERR_INVALID_STATE') } if (this.channel.readyState === 'open') { @@ -183,16 +245,18 @@ export class WebRTCStream extends AbstractStream { this.messageQueue.append(data) } else { - log.error('unknown datachannel state %s', this.channel.readyState) + this.log.error('unknown datachannel state %s', this.channel.readyState) throw new CodeError('Unknown datachannel state', 'ERR_INVALID_STATE') } } async sendData (data: Uint8ArrayList): Promise { + // sending messages is an async operation so use a copy of the list as it + // may be changed beneath us data = data.sublist() while (data.byteLength > 0) { - const toSend = Math.min(data.byteLength, this.maxDataSize) + const toSend = Math.min(data.byteLength, this.maxMessageSize) const buf = data.subarray(0, toSend) const msgbuf = Message.encode({ message: buf }) const sendbuf = lengthPrefixed.encode.single(msgbuf) @@ -206,8 +270,25 @@ export class WebRTCStream extends AbstractStream { await this._sendFlag(Message.Flag.RESET) } - async sendCloseWrite (): Promise { - await this._sendFlag(Message.Flag.FIN) + async sendCloseWrite (options: AbortOptions): Promise { + const sent = await this._sendFlag(Message.Flag.FIN) + + if (sent) { + this.log.trace('awaiting FIN_ACK') + try { + await raceSignal(this.receiveFinAck.promise, options?.signal, { + errorMessage: 'sending close-write was aborted before FIN_ACK was received', + errorCode: 'ERR_FIN_ACK_NOT_RECEIVED' + }) + } catch (err) { + this.log.error('failed to await FIN_ACK', err) + } + } else { + this.log.trace('sending FIN failed, not awaiting FIN_ACK') + } + + // if we've attempted to receive a FIN_ACK, do not try again + this.receiveFinAck.resolve() } async sendCloseRead (): Promise { @@ -217,14 +298,21 @@ export class WebRTCStream extends AbstractStream { /** * Handle incoming */ - private processIncomingProtobuf (buffer: Uint8Array): Uint8Array | undefined { + private processIncomingProtobuf (buffer: Uint8ArrayList): Uint8Array | undefined { const message = Message.decode(buffer) if (message.flag !== undefined) { + this.log.trace('incoming flag %s, write status "%s", read status "%s"', message.flag, this.writeStatus, this.readStatus) + if (message.flag === Message.Flag.FIN) { // We should expect no more data from the remote, stop reading - this.incomingData.end() this.remoteCloseWrite() + + this.log.trace('sending FIN_ACK') + void this._sendFlag(Message.Flag.FIN_ACK) + .catch(err => { + this.log.error('error sending FIN_ACK immediately', err) + }) } if (message.flag === Message.Flag.RESET) { @@ -236,21 +324,45 @@ export class WebRTCStream extends AbstractStream { // The remote has stopped reading this.remoteCloseRead() } + + if (message.flag === Message.Flag.FIN_ACK) { + this.log.trace('received FIN_ACK') + this.receiveFinAck.resolve() + } } - return message.message + // ignore data messages if we've closed the readable end already + if (this.readStatus === 'ready') { + return message.message + } } - private async _sendFlag (flag: Message.Flag): Promise { - log.trace('Sending flag: %s', flag.toString()) + private async _sendFlag (flag: Message.Flag): Promise { + if (this.channel.readyState !== 'open') { + // flags can be sent while we or the remote are closing the datachannel so + // if the channel isn't open, don't try to send it but return false to let + // the caller know and act if they need to + this.log.trace('not sending flag %s because channel is not open', flag.toString()) + return false + } + + this.log.trace('sending flag %s', flag.toString()) const msgbuf = Message.encode({ flag }) const prefixedBuf = lengthPrefixed.encode.single(msgbuf) - await this._sendMessage(prefixedBuf, false) + try { + await this._sendMessage(prefixedBuf, false) + + return true + } catch (err: any) { + this.log.error('could not send flag %s', flag.toString(), err) + } + + return false } } -export interface WebRTCStreamOptions { +export interface WebRTCStreamOptions extends DataChannelOptions { /** * The network channel used for bidirectional peer-to-peer transfers of * arbitrary data @@ -264,23 +376,18 @@ export interface WebRTCStreamOptions { */ direction: Direction - dataChannelOptions?: Partial - - maxMsgSize?: number - - onEnd?: (err?: Error | undefined) => void + /** + * A callback invoked when the channel ends + */ + onEnd?(err?: Error | undefined): void } export function createStream (options: WebRTCStreamOptions): WebRTCStream { - const { channel, direction, onEnd, dataChannelOptions } = options + const { channel, direction } = options return new WebRTCStream({ id: direction === 'inbound' ? (`i${channel.id}`) : `r${channel.id}`, - direction, - maxDataSize: (dataChannelOptions?.maxMessageSize ?? MAX_MESSAGE_SIZE) - PROTOBUF_OVERHEAD, - dataChannelOptions, - onEnd, - channel, - log: logger(`libp2p:mplex:stream:${direction}:${channel.id}`) + log: logger(`libp2p:webrtc:stream:${direction}:${channel.id}`), + ...options }) } diff --git a/packages/transport-webrtc/src/util.ts b/packages/transport-webrtc/src/util.ts index e26e64dd5f..e35b90ca43 100644 --- a/packages/transport-webrtc/src/util.ts +++ b/packages/transport-webrtc/src/util.ts @@ -1,4 +1,9 @@ +import { logger } from '@libp2p/logger' import { detect } from 'detect-browser' +import pDefer from 'p-defer' +import pTimeout from 'p-timeout' + +const log = logger('libp2p:webrtc:utils') const browser = detect() export const isFirefox = ((browser != null) && browser.name === 'firefox') @@ -6,3 +11,58 @@ export const isFirefox = ((browser != null) && browser.name === 'firefox') export const nopSource = async function * nop (): AsyncGenerator {} export const nopSink = async (_: any): Promise => {} + +export const DATA_CHANNEL_DRAIN_TIMEOUT = 30 * 1000 + +export function drainAndClose (channel: RTCDataChannel, direction: string, drainTimeout: number = DATA_CHANNEL_DRAIN_TIMEOUT): void { + if (channel.readyState !== 'open') { + return + } + + void Promise.resolve() + .then(async () => { + // wait for bufferedAmount to become zero + if (channel.bufferedAmount > 0) { + log('%s drain channel with %d buffered bytes', direction, channel.bufferedAmount) + const deferred = pDefer() + let drained = false + + channel.bufferedAmountLowThreshold = 0 + + const closeListener = (): void => { + if (!drained) { + log('%s drain channel closed before drain', direction) + deferred.resolve() + } + } + + channel.addEventListener('close', closeListener, { + once: true + }) + + channel.addEventListener('bufferedamountlow', () => { + drained = true + channel.removeEventListener('close', closeListener) + deferred.resolve() + }) + + await pTimeout(deferred.promise, { + milliseconds: drainTimeout + }) + } + }) + .then(async () => { + // only close if the channel is still open + if (channel.readyState === 'open') { + channel.close() + } + }) + .catch(err => { + log.error('error closing outbound stream', err) + }) +} + +export interface AbortPromiseOptions { + signal?: AbortSignal + message?: string +} diff --git a/packages/transport-webrtc/test/basics.spec.ts b/packages/transport-webrtc/test/basics.spec.ts index 03d89a1c8b..f7170c6126 100644 --- a/packages/transport-webrtc/test/basics.spec.ts +++ b/packages/transport-webrtc/test/basics.spec.ts @@ -7,15 +7,19 @@ import * as filter from '@libp2p/websockets/filters' import { WebRTC } from '@multiformats/mafmt' import { multiaddr } from '@multiformats/multiaddr' import { expect } from 'aegir/chai' +import drain from 'it-drain' import map from 'it-map' import { pipe } from 'it-pipe' +import { pushable } from 'it-pushable' import toBuffer from 'it-to-buffer' import { createLibp2p } from 'libp2p' import { circuitRelayTransport } from 'libp2p/circuit-relay' -import { identifyService } from 'libp2p/identify' +import pDefer from 'p-defer' +import pRetry from 'p-retry' import { webRTC } from '../src/index.js' import type { Libp2p } from '@libp2p/interface' -import type { Connection } from '@libp2p/interface/connection' +import type { Connection, Stream } from '@libp2p/interface/connection' +import type { StreamHandler } from '@libp2p/interface/stream-handler' async function createNode (): Promise { return createLibp2p({ @@ -38,9 +42,6 @@ async function createNode (): Promise { streamMuxers: [ yamux() ], - services: { - identify: identifyService() - }, connectionGater: { denyDialMultiaddr: () => false }, @@ -55,6 +56,7 @@ describe('basics', () => { let localNode: Libp2p let remoteNode: Libp2p + let streamHandler: StreamHandler async function connectNodes (): Promise { const remoteAddr = remoteNode.getMultiaddrs() @@ -64,11 +66,8 @@ describe('basics', () => { throw new Error('Remote peer could not listen on relay') } - await remoteNode.handle(echo, ({ stream }) => { - void pipe( - stream, - stream - ) + await remoteNode.handle(echo, (info) => { + streamHandler(info) }, { runOnTransientConnection: true }) @@ -83,6 +82,13 @@ describe('basics', () => { } beforeEach(async () => { + streamHandler = ({ stream }) => { + void pipe( + stream, + stream + ) + } + localNode = await createNode() remoteNode = await createNode() }) @@ -101,9 +107,7 @@ describe('basics', () => { const connection = await connectNodes() // open a stream on the echo protocol - const stream = await connection.newStream(echo, { - runOnTransientConnection: true - }) + const stream = await connection.newStream(echo) // send and receive some data const input = new Array(5).fill(0).map(() => new Uint8Array(10)) @@ -138,4 +142,204 @@ describe('basics', () => { // asset that we got the right data expect(output).to.equalBytes(toBuffer(input)) }) + + it('can close local stream for reading but send a large file', async () => { + let output: Uint8Array = new Uint8Array(0) + const streamClosed = pDefer() + + streamHandler = ({ stream }) => { + void Promise.resolve().then(async () => { + output = await toBuffer(map(stream.source, (buf) => buf.subarray())) + await stream.close() + streamClosed.resolve() + }) + } + + const connection = await connectNodes() + + // open a stream on the echo protocol + const stream = await connection.newStream(echo, { + runOnTransientConnection: true + }) + + // close for reading + await stream.closeRead() + + // send some data + const input = new Array(5).fill(0).map(() => new Uint8Array(1024 * 1024)) + + await stream.sink(input) + await stream.close() + + // wait for remote to receive all data + await streamClosed.promise + + // asset that we got the right data + expect(output).to.equalBytes(toBuffer(input)) + }) + + it('can close local stream for writing but receive a large file', async () => { + const input = new Array(5).fill(0).map(() => new Uint8Array(1024 * 1024)) + + streamHandler = ({ stream }) => { + void Promise.resolve().then(async () => { + // send some data + await stream.sink(input) + await stream.close() + }) + } + + const connection = await connectNodes() + + // open a stream on the echo protocol + const stream = await connection.newStream(echo, { + runOnTransientConnection: true + }) + + // close for reading + await stream.closeWrite() + + // receive some data + const output = await toBuffer(map(stream.source, (buf) => buf.subarray())) + + await stream.close() + + // asset that we got the right data + expect(output).to.equalBytes(toBuffer(input)) + }) + + it('can close local stream for writing and reading while a remote stream is writing', async () => { + /** + * NodeA NodeB + * | <--- STOP_SENDING | + * | FIN ---> | + * | <--- FIN | + * | FIN_ACK ---> | + * | <--- FIN_ACK | + */ + + const getRemoteStream = pDefer() + + streamHandler = ({ stream }) => { + void Promise.resolve().then(async () => { + getRemoteStream.resolve(stream) + }) + } + + const connection = await connectNodes() + + // open a stream on the echo protocol + const stream = await connection.newStream(echo, { + runOnTransientConnection: true + }) + + const remoteStream = await getRemoteStream.promise + // close the readable end of the remote stream + await remoteStream.closeRead() + + // keep the remote write end open, this should delay the FIN_ACK reply to the local stream + const remoteInputStream = pushable() + void remoteStream.sink(remoteInputStream) + + const p = stream.closeWrite() + + // wait for remote to receive local close-write + await pRetry(() => { + if (remoteStream.readStatus !== 'closed') { + throw new Error('Remote stream read status ' + remoteStream.readStatus) + } + }, { + minTimeout: 100 + }) + + // remote closes write + remoteInputStream.end() + + // wait to receive FIN_ACK + await p + + // wait for remote to notice closure + await pRetry(() => { + if (remoteStream.status !== 'closed') { + throw new Error('Remote stream not closed') + } + }) + + assertStreamClosed(stream) + assertStreamClosed(remoteStream) + }) + + it('can close local stream for writing and reading while a remote stream is writing using source/sink', async () => { + /** + * NodeA NodeB + * | FIN ---> | + * | <--- FIN | + * | FIN_ACK ---> | + * | <--- FIN_ACK | + */ + + const getRemoteStream = pDefer() + + streamHandler = ({ stream }) => { + void Promise.resolve().then(async () => { + getRemoteStream.resolve(stream) + }) + } + + const connection = await connectNodes() + + // open a stream on the echo protocol + const stream = await connection.newStream(echo, { + runOnTransientConnection: true + }) + + const remoteStream = await getRemoteStream.promise + // close the readable end of the remote stream + await remoteStream.closeRead() + // readable end should finish + await drain(remoteStream.source) + + // keep the remote write end open, this should delay the FIN_ACK reply to the local stream + const p = stream.sink([]) + + // wait for remote to receive local close-write + await pRetry(() => { + if (remoteStream.readStatus !== 'closed') { + throw new Error('Remote stream read status ' + remoteStream.readStatus) + } + }, { + minTimeout: 100 + }) + + // remote closes write + await remoteStream.sink([]) + + // wait to receive FIN_ACK + await p + + // close read end of stream + await stream.closeRead() + // readable end should finish + await drain(stream.source) + + // wait for remote to notice closure + await pRetry(() => { + if (remoteStream.status !== 'closed') { + throw new Error('Remote stream not closed') + } + }) + + assertStreamClosed(stream) + assertStreamClosed(remoteStream) + }) }) + +function assertStreamClosed (stream: Stream): void { + expect(stream.status).to.equal('closed') + expect(stream.readStatus).to.equal('closed') + expect(stream.writeStatus).to.equal('closed') + + expect(stream.timeline.close).to.be.a('number') + expect(stream.timeline.closeRead).to.be.a('number') + expect(stream.timeline.closeWrite).to.be.a('number') +} diff --git a/packages/transport-webrtc/test/listener.spec.ts b/packages/transport-webrtc/test/listener.spec.ts index 34feedb859..036e727d7c 100644 --- a/packages/transport-webrtc/test/listener.spec.ts +++ b/packages/transport-webrtc/test/listener.spec.ts @@ -16,6 +16,8 @@ describe('webrtc private-to-private listener', () => { const listener = new WebRTCPeerListener({ peerId, transportManager + }, { + shutdownController: new AbortController() }) const otherListener = stubInterface({ diff --git a/packages/transport-webrtc/test/peer.browser.spec.ts b/packages/transport-webrtc/test/peer.browser.spec.ts index 8d490ff967..5e98c1078a 100644 --- a/packages/transport-webrtc/test/peer.browser.spec.ts +++ b/packages/transport-webrtc/test/peer.browser.spec.ts @@ -1,56 +1,119 @@ -import { mockConnection, mockMultiaddrConnection, mockRegistrar, mockStream, mockUpgrader } from '@libp2p/interface-compliance-tests/mocks' +import { mockRegistrar, mockUpgrader, streamPair } from '@libp2p/interface-compliance-tests/mocks' import { createEd25519PeerId } from '@libp2p/peer-id-factory' -import { multiaddr } from '@multiformats/multiaddr' +import { multiaddr, type Multiaddr } from '@multiformats/multiaddr' import { expect } from 'aegir/chai' import { detect } from 'detect-browser' -import { pair } from 'it-pair' import { duplexPair } from 'it-pair/duplex' import { pbStream } from 'it-protobuf-stream' import Sinon from 'sinon' -import { initiateConnection, handleIncomingStream } from '../src/private-to-private/handler.js' +import { stubInterface, type StubbedInstance } from 'sinon-ts' +import { initiateConnection } from '../src/private-to-private/initiate-connection.js' import { Message } from '../src/private-to-private/pb/message.js' -import { WebRTCTransport, splitAddr } from '../src/private-to-private/transport.js' +import { handleIncomingStream } from '../src/private-to-private/signaling-stream-handler.js' +import { SIGNALING_PROTO_ID, WebRTCTransport, splitAddr } from '../src/private-to-private/transport.js' import { RTCPeerConnection, RTCSessionDescription } from '../src/webrtc/index.js' +import type { Connection, Stream } from '@libp2p/interface/connection' +import type { ConnectionManager } from '@libp2p/interface-internal/connection-manager' +import type { TransportManager } from '@libp2p/interface-internal/transport-manager' const browser = detect() +interface PrivateToPrivateComponents { + initiator: { + multiaddr: Multiaddr + peerConnection: RTCPeerConnection + connectionManager: StubbedInstance + transportManager: StubbedInstance + connection: StubbedInstance + stream: Stream + } + recipient: { + peerConnection: RTCPeerConnection + connection: StubbedInstance + abortController: AbortController + signal: AbortSignal + stream: Stream + } +} + +async function getComponents (): Promise { + const relayPeerId = await createEd25519PeerId() + const receiverPeerId = await createEd25519PeerId() + const receiverMultiaddr = multiaddr(`/ip4/123.123.123.123/tcp/123/p2p/${relayPeerId}/p2p-circuit/webrtc/p2p/${receiverPeerId}`) + const [initiatorToReceiver, receiverToInitiator] = duplexPair() + const [initiatorStream, receiverStream] = streamPair({ + duplex: initiatorToReceiver, + init: { + protocol: SIGNALING_PROTO_ID + } + }, { + duplex: receiverToInitiator, + init: { + protocol: SIGNALING_PROTO_ID + } + }) + + const recipientAbortController = new AbortController() + + return { + initiator: { + multiaddr: receiverMultiaddr, + peerConnection: new RTCPeerConnection(), + connectionManager: stubInterface(), + transportManager: stubInterface(), + connection: stubInterface(), + stream: initiatorStream + }, + recipient: { + peerConnection: new RTCPeerConnection(), + connection: stubInterface(), + abortController: recipientAbortController, + signal: recipientAbortController.signal, + stream: receiverStream + } + } +} + describe('webrtc basic', () => { const isFirefox = ((browser != null) && browser.name === 'firefox') it('should connect', async () => { - const [receiver, initiator] = duplexPair() - const dstPeerId = await createEd25519PeerId() - const connection = mockConnection( - mockMultiaddrConnection(pair(), dstPeerId) - ) - const controller = new AbortController() - const initiatorPeerConnectionPromise = initiateConnection({ stream: mockStream(initiator), signal: controller.signal }) - const receiverPeerConnectionPromise = handleIncomingStream({ stream: mockStream(receiver), connection }) - await expect(initiatorPeerConnectionPromise).to.be.fulfilled() - await expect(receiverPeerConnectionPromise).to.be.fulfilled() - const [{ pc: pc0 }, { pc: pc1 }] = await Promise.all([initiatorPeerConnectionPromise, receiverPeerConnectionPromise]) + const { initiator, recipient } = await getComponents() + + // no existing connection + initiator.connectionManager.getConnections.returns([]) + + // transport manager dials recipient + initiator.transportManager.dial.resolves(initiator.connection) + + // signalling stream opens successfully + initiator.connection.newStream.withArgs(SIGNALING_PROTO_ID).resolves(initiator.stream) + + await expect( + Promise.all([ + initiateConnection(initiator), + handleIncomingStream(recipient) + ]) + ).to.eventually.be.fulfilled() + if (isFirefox) { - expect(pc0.iceConnectionState).eq('connected') - expect(pc1.iceConnectionState).eq('connected') + expect(initiator.peerConnection.iceConnectionState).eq('connected') + expect(recipient.peerConnection.iceConnectionState).eq('connected') return } - expect(pc0.connectionState).eq('connected') - expect(pc1.connectionState).eq('connected') + expect(initiator.peerConnection.connectionState).eq('connected') + expect(recipient.peerConnection.connectionState).eq('connected') - pc0.close() - pc1.close() + initiator.peerConnection.close() + recipient.peerConnection.close() }) }) describe('webrtc receiver', () => { it('should fail receiving on invalid sdp offer', async () => { - const [receiver, initiator] = duplexPair() - const dstPeerId = await createEd25519PeerId() - const connection = mockConnection( - mockMultiaddrConnection(pair(), dstPeerId) - ) - const receiverPeerConnectionPromise = handleIncomingStream({ stream: mockStream(receiver), connection }) - const stream = pbStream(initiator).pb(Message) + const { initiator, recipient } = await getComponents() + const receiverPeerConnectionPromise = handleIncomingStream(recipient) + const stream = pbStream(initiator.stream).pb(Message) await stream.write({ type: Message.Type.SDP_OFFER, data: 'bad' }) await expect(receiverPeerConnectionPromise).to.be.rejectedWith(/Failed to set remoteDescription/) @@ -59,10 +122,18 @@ describe('webrtc receiver', () => { describe('webrtc dialer', () => { it('should fail receiving on invalid sdp answer', async () => { - const [receiver, initiator] = duplexPair() - const controller = new AbortController() - const initiatorPeerConnectionPromise = initiateConnection({ signal: controller.signal, stream: mockStream(initiator) }) - const stream = pbStream(receiver).pb(Message) + const { initiator, recipient } = await getComponents() + + // existing connection already exists + initiator.connectionManager.getConnections.returns([ + initiator.connection + ]) + + // signalling stream opens successfully + initiator.connection.newStream.withArgs(SIGNALING_PROTO_ID).resolves(initiator.stream) + + const initiatorPeerConnectionPromise = initiateConnection(initiator) + const stream = pbStream(recipient.stream).pb(Message) const offerMessage = await stream.read() expect(offerMessage.type).to.eq(Message.Type.SDP_OFFER) @@ -72,10 +143,19 @@ describe('webrtc dialer', () => { }) it('should fail on receiving a candidate before an answer', async () => { - const [receiver, initiator] = duplexPair() - const controller = new AbortController() - const initiatorPeerConnectionPromise = initiateConnection({ signal: controller.signal, stream: mockStream(initiator) }) - const stream = pbStream(receiver).pb(Message) + const { initiator, recipient } = await getComponents() + + // existing connection already exists + initiator.connectionManager.getConnections.returns([ + initiator.connection + ]) + + // signalling stream opens successfully + initiator.connection.newStream.withArgs(SIGNALING_PROTO_ID).resolves(initiator.stream) + + const initiatorPeerConnectionPromise = initiateConnection(initiator) + + const stream = pbStream(recipient.stream).pb(Message) const pc = new RTCPeerConnection() pc.onicecandidate = ({ candidate }) => { @@ -99,14 +179,15 @@ describe('webrtc dialer', () => { describe('webrtc filter', () => { it('can filter multiaddrs to dial', async () => { const transport = new WebRTCTransport({ - transportManager: Sinon.stub() as any, + transportManager: stubInterface(), + connectionManager: stubInterface(), peerId: Sinon.stub() as any, registrar: mockRegistrar(), upgrader: mockUpgrader({}) }, {}) const valid = [ - multiaddr('/ip4/127.0.0.1/tcp/1234/ws/p2p-circuit/webrtc') + multiaddr('/ip4/127.0.0.1/tcp/1234/ws/p2p/12D3KooWFqpHsdZaL4NW6eVE3yjhoSDNv7HJehPZqj17kjKntAh2/p2p-circuit/webrtc/p2p/12D3KooWF2P1k8SVRL1cV1Z9aNM8EVRwbrMESyRf58ceQkaht4AF') ] expect(transport.filter(valid)).length(1) diff --git a/packages/transport-webrtc/test/stream.browser.spec.ts b/packages/transport-webrtc/test/stream.browser.spec.ts index 457f95317d..3e72b51e63 100644 --- a/packages/transport-webrtc/test/stream.browser.spec.ts +++ b/packages/transport-webrtc/test/stream.browser.spec.ts @@ -5,13 +5,14 @@ import { bytes } from 'multiformats' import { Message } from '../src/pb/message.js' import { createStream, type WebRTCStream } from '../src/stream.js' import { RTCPeerConnection } from '../src/webrtc/index.js' +import { receiveFinAck } from './util.js' import type { Stream } from '@libp2p/interface/connection' const TEST_MESSAGE = 'test_message' function setup (): { peerConnection: RTCPeerConnection, dataChannel: RTCDataChannel, stream: WebRTCStream } { const peerConnection = new RTCPeerConnection() const dataChannel = peerConnection.createDataChannel('whatever', { negotiated: true, id: 91 }) - const stream = createStream({ channel: dataChannel, direction: 'outbound' }) + const stream = createStream({ channel: dataChannel, direction: 'outbound', closeTimeout: 1 }) return { peerConnection, dataChannel, stream } } @@ -28,9 +29,10 @@ function generatePbByFlag (flag?: Message.Flag): Uint8Array { describe('Stream Stats', () => { let stream: WebRTCStream let peerConnection: RTCPeerConnection + let dataChannel: RTCDataChannel beforeEach(async () => { - ({ stream, peerConnection } = setup()) + ({ stream, peerConnection, dataChannel } = setup()) }) afterEach(() => { @@ -45,7 +47,10 @@ describe('Stream Stats', () => { it('close marks it closed', async () => { expect(stream.timeline.close).to.not.exist() + + receiveFinAck(dataChannel) await stream.close() + expect(stream.timeline.close).to.be.a('number') }) @@ -58,15 +63,23 @@ describe('Stream Stats', () => { it('closeWrite marks it write-closed only', async () => { expect(stream.timeline.close).to.not.exist() + + receiveFinAck(dataChannel) await stream.closeWrite() + expect(stream.timeline.close).to.not.exist() expect(stream.timeline.closeWrite).to.be.greaterThanOrEqual(stream.timeline.open) }) it('closeWrite AND closeRead = close', async () => { expect(stream.timeline.close).to.not.exist() - await stream.closeWrite() - await stream.closeRead() + + receiveFinAck(dataChannel) + await Promise.all([ + stream.closeRead(), + stream.closeWrite() + ]) + expect(stream.timeline.close).to.be.a('number') expect(stream.timeline.closeWrite).to.be.greaterThanOrEqual(stream.timeline.open) expect(stream.timeline.closeRead).to.be.greaterThanOrEqual(stream.timeline.open) diff --git a/packages/transport-webrtc/test/stream.spec.ts b/packages/transport-webrtc/test/stream.spec.ts index 1812ca2c7a..6a80366373 100644 --- a/packages/transport-webrtc/test/stream.spec.ts +++ b/packages/transport-webrtc/test/stream.spec.ts @@ -4,46 +4,38 @@ import { expect } from 'aegir/chai' import length from 'it-length' import * as lengthPrefixed from 'it-length-prefixed' import { pushable } from 'it-pushable' +import pDefer from 'p-defer' import { Uint8ArrayList } from 'uint8arraylist' import { Message } from '../src/pb/message.js' -import { createStream } from '../src/stream.js' - -const mockDataChannel = (opts: { send: (bytes: Uint8Array) => void, bufferedAmount?: number }): RTCDataChannel => { - return { - readyState: 'open', - close: () => {}, - addEventListener: (_type: string, _listener: () => void) => {}, - removeEventListener: (_type: string, _listener: () => void) => {}, - ...opts - } as RTCDataChannel -} - -const MAX_MESSAGE_SIZE = 16 * 1024 +import { MAX_BUFFERED_AMOUNT, MAX_MESSAGE_SIZE, PROTOBUF_OVERHEAD, createStream } from '../src/stream.js' +import { mockDataChannel, receiveFinAck } from './util.js' describe('Max message size', () => { it(`sends messages smaller or equal to ${MAX_MESSAGE_SIZE} bytes in one`, async () => { const sent: Uint8ArrayList = new Uint8ArrayList() - const data = new Uint8Array(MAX_MESSAGE_SIZE - 5) + const data = new Uint8Array(MAX_MESSAGE_SIZE - PROTOBUF_OVERHEAD) const p = pushable() + const channel = mockDataChannel({ + send: (bytes) => { + sent.append(bytes) + } + }) // Make sure that the data that ought to be sent will result in a message with exactly MAX_MESSAGE_SIZE const messageLengthEncoded = lengthPrefixed.encode.single(Message.encode({ message: data })) expect(messageLengthEncoded.length).eq(MAX_MESSAGE_SIZE) const webrtcStream = createStream({ - channel: mockDataChannel({ - send: (bytes) => { - sent.append(bytes) - } - }), - direction: 'outbound' + channel, + direction: 'outbound', + closeTimeout: 1 }) p.push(data) p.end() + receiveFinAck(channel) await webrtcStream.sink(p) - // length(message) + message + length(FIN) + FIN - expect(length(sent)).to.equal(4) + expect(length(sent)).to.equal(6) for (const buf of sent) { expect(buf.byteLength).to.be.lessThanOrEqual(MAX_MESSAGE_SIZE) @@ -54,22 +46,24 @@ describe('Max message size', () => { const sent: Uint8ArrayList = new Uint8ArrayList() const data = new Uint8Array(MAX_MESSAGE_SIZE) const p = pushable() + const channel = mockDataChannel({ + send: (bytes) => { + sent.append(bytes) + } + }) // Make sure that the data that ought to be sent will result in a message with exactly MAX_MESSAGE_SIZE + 1 // const messageLengthEncoded = lengthPrefixed.encode.single(Message.encode({ message: data })).subarray() // expect(messageLengthEncoded.length).eq(MAX_MESSAGE_SIZE + 1) const webrtcStream = createStream({ - channel: mockDataChannel({ - send: (bytes) => { - sent.append(bytes) - } - }), + channel, direction: 'outbound' }) p.push(data) p.end() + receiveFinAck(channel) await webrtcStream.sink(p) expect(length(sent)).to.equal(6) @@ -80,33 +74,32 @@ describe('Max message size', () => { }) it('closes the stream if bufferamountlow timeout', async () => { - const MAX_BUFFERED_AMOUNT = 16 * 1024 * 1024 + 1 const timeout = 100 - let closed = false - const webrtcStream = createStream({ - dataChannelOptions: { - bufferedAmountLowEventTimeout: timeout + const closed = pDefer() + const channel = mockDataChannel({ + send: () => { + throw new Error('Expected to not send') }, - channel: mockDataChannel({ - send: () => { - throw new Error('Expected to not send') - }, - bufferedAmount: MAX_BUFFERED_AMOUNT - }), + bufferedAmount: MAX_BUFFERED_AMOUNT + 1 + }) + const webrtcStream = createStream({ + bufferedAmountLowEventTimeout: timeout, + closeTimeout: 1, + channel, direction: 'outbound', onEnd: () => { - closed = true + closed.resolve() } }) const t0 = Date.now() await expect(webrtcStream.sink([new Uint8Array(1)])).to.eventually.be.rejected - .with.property('message', 'Timed out waiting for DataChannel buffer to clear') + .with.property('code', 'ERR_BUFFER_CLEAR_TIMEOUT') const t1 = Date.now() expect(t1 - t0).greaterThan(timeout) expect(t1 - t0).lessThan(timeout + 1000) // Some upper bound - expect(closed).true() + await closed.promise expect(webrtcStream.timeline.close).to.be.greaterThan(webrtcStream.timeline.open) expect(webrtcStream.timeline.abort).to.be.greaterThan(webrtcStream.timeline.open) }) diff --git a/packages/transport-webrtc/test/transport.browser.spec.ts b/packages/transport-webrtc/test/transport.browser.spec.ts index ea7b56d545..8ba736a0d0 100644 --- a/packages/transport-webrtc/test/transport.browser.spec.ts +++ b/packages/transport-webrtc/test/transport.browser.spec.ts @@ -3,10 +3,10 @@ import { type CreateListenerOptions, symbol } from '@libp2p/interface/transport' import { mockMetrics, mockUpgrader } from '@libp2p/interface-compliance-tests/mocks' import { createEd25519PeerId } from '@libp2p/peer-id-factory' -import { multiaddr, type Multiaddr } from '@multiformats/multiaddr' -import { expect, assert } from 'aegir/chai' +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' import { UnimplementedError } from '../src/error.js' -import * as underTest from '../src/private-to-public/transport.js' +import { WebRTCDirectTransport, type WebRTCDirectTransportComponents } from '../src/private-to-public/transport.js' import { expectError } from './util.js' import type { Metrics } from '@libp2p/interface/metrics' @@ -15,9 +15,9 @@ function ignoredDialOption (): CreateListenerOptions { return { upgrader } } -describe('WebRTC Transport', () => { +describe('WebRTCDirect Transport', () => { let metrics: Metrics - let components: underTest.WebRTCDirectTransportComponents + let components: WebRTCDirectTransportComponents before(async () => { metrics = mockMetrics()() @@ -28,13 +28,13 @@ describe('WebRTC Transport', () => { }) it('can construct', () => { - const t = new underTest.WebRTCDirectTransport(components) + const t = new WebRTCDirectTransport(components) expect(t.constructor.name).to.equal('WebRTCDirectTransport') }) it('can dial', async () => { const ma = multiaddr('/ip4/1.2.3.4/udp/1234/webrtc-direct/certhash/uEiAUqV7kzvM1wI5DYDc1RbcekYVmXli_Qprlw3IkiEg6tQ/p2p/12D3KooWGDMwwqrpcYKpKCgxuKT2NfqPqa94QnkoBBpqvCaiCzWd') - const transport = new underTest.WebRTCDirectTransport(components) + const transport = new WebRTCDirectTransport(components) const options = ignoredDialOption() // don't await as this isn't an e2e test @@ -42,7 +42,7 @@ describe('WebRTC Transport', () => { }) it('createListner throws', () => { - const t = new underTest.WebRTCDirectTransport(components) + const t = new WebRTCDirectTransport(components) try { t.createListener(ignoredDialOption()) expect('Should have thrown').to.equal('but did not') @@ -52,38 +52,38 @@ describe('WebRTC Transport', () => { }) it('toString property getter', () => { - const t = new underTest.WebRTCDirectTransport(components) + const t = new WebRTCDirectTransport(components) const s = t[Symbol.toStringTag] expect(s).to.equal('@libp2p/webrtc-direct') }) it('symbol property getter', () => { - const t = new underTest.WebRTCDirectTransport(components) + const t = new WebRTCDirectTransport(components) const s = t[symbol] expect(s).to.equal(true) }) it('transport filter filters out invalid multiaddrs', async () => { - const mas: Multiaddr[] = [ - '/ip4/1.2.3.4/udp/1234/webrtc/certhash/uEiAUqV7kzvM1wI5DYDc1RbcekYVmXli_Qprlw3IkiEg6tQ', - '/ip4/1.2.3.4/udp/1234/webrtc-direct/certhash/uEiAUqV7kzvM1wI5DYDc1RbcekYVmXli_Qprlw3IkiEg6tQ/p2p/12D3KooWGDMwwqrpcYKpKCgxuKT2NfqPqa94QnkoBBpqvCaiCzWd', - '/ip4/1.2.3.4/udp/1234/webrtc-direct/p2p/12D3KooWGDMwwqrpcYKpKCgxuKT2NfqPqa94QnkoBBpqvCaiCzWd', - '/ip4/1.2.3.4/udp/1234/certhash/uEiAUqV7kzvM1wI5DYDc1RbcekYVmXli_Qprlw3IkiEg6tQ/p2p/12D3KooWGDMwwqrpcYKpKCgxuKT2NfqPqa94QnkoBBpqvCaiCzWd' - ].map((s) => multiaddr(s)) - const t = new underTest.WebRTCDirectTransport(components) - const result = t.filter(mas) - const expected = + const valid = [ multiaddr('/ip4/1.2.3.4/udp/1234/webrtc-direct/certhash/uEiAUqV7kzvM1wI5DYDc1RbcekYVmXli_Qprlw3IkiEg6tQ/p2p/12D3KooWGDMwwqrpcYKpKCgxuKT2NfqPqa94QnkoBBpqvCaiCzWd') + ] + const invalid = [ + multiaddr('/ip4/1.2.3.4/udp/1234/webrtc/certhash/uEiAUqV7kzvM1wI5DYDc1RbcekYVmXli_Qprlw3IkiEg6tQ'), + multiaddr('/ip4/1.2.3.4/udp/1234/webrtc-direct/p2p/12D3KooWGDMwwqrpcYKpKCgxuKT2NfqPqa94QnkoBBpqvCaiCzWd'), + multiaddr('/ip4/1.2.3.4/udp/1234/certhash/uEiAUqV7kzvM1wI5DYDc1RbcekYVmXli_Qprlw3IkiEg6tQ/p2p/12D3KooWGDMwwqrpcYKpKCgxuKT2NfqPqa94QnkoBBpqvCaiCzWd') + ] - assert.isNotNull(result) - expect(result.constructor.name).to.equal('Array') - expect(result).to.have.length(1) - expect(result[0].equals(expected)).to.be.true() + const t = new WebRTCDirectTransport(components) + + expect(t.filter([ + ...valid, + ...invalid + ])).to.deep.equal(valid) }) it('throws WebRTC transport error when dialing a multiaddr without a PeerId', async () => { const ma = multiaddr('/ip4/1.2.3.4/udp/1234/webrtc-direct/certhash/uEiAUqV7kzvM1wI5DYDc1RbcekYVmXli_Qprlw3IkiEg6tQ') - const transport = new underTest.WebRTCDirectTransport(components) + const transport = new WebRTCDirectTransport(components) try { await transport.dial(ma, ignoredDialOption()) diff --git a/packages/transport-webrtc/test/util.ts b/packages/transport-webrtc/test/util.ts index 70c492b6a7..fe3b3b42d2 100644 --- a/packages/transport-webrtc/test/util.ts +++ b/packages/transport-webrtc/test/util.ts @@ -1,4 +1,6 @@ import { expect } from 'aegir/chai' +import * as lengthPrefixed from 'it-length-prefixed' +import { Message } from '../src/pb/message.js' export const expectError = (error: unknown, message: string): void => { if (error instanceof Error) { @@ -7,3 +9,28 @@ export const expectError = (error: unknown, message: string): void => { expect('Did not throw error:').to.equal(message) } } + +/** + * simulates receiving a FIN_ACK on the passed datachannel + */ +export function receiveFinAck (channel: RTCDataChannel): void { + const msgbuf = Message.encode({ flag: Message.Flag.FIN_ACK }) + const data = lengthPrefixed.encode.single(msgbuf).subarray() + channel.onmessage?.(new MessageEvent('message', { data })) +} + +let mockDataChannelId = 0 + +export const mockDataChannel = (opts: { send(bytes: Uint8Array): void, bufferedAmount?: number }): RTCDataChannel => { + // @ts-expect-error incomplete implementation + const channel: RTCDataChannel = { + readyState: 'open', + close: () => { }, + addEventListener: (_type: string, _listener: (_: any) => void) => { }, + removeEventListener: (_type: string, _listener: (_: any) => void) => { }, + id: mockDataChannelId++, + ...opts + } + + return channel +} diff --git a/packages/transport-websockets/CHANGELOG.md b/packages/transport-websockets/CHANGELOG.md index 2ee4c06bc3..96384c9937 100644 --- a/packages/transport-websockets/CHANGELOG.md +++ b/packages/transport-websockets/CHANGELOG.md @@ -6,6 +6,65 @@ * **dev:** bump @libp2p/interface-mocks from 11.0.3 to 12.0.1 ([#241](https://github.com/libp2p/js-libp2p-websockets/issues/241)) ([f956836](https://github.com/libp2p/js-libp2p-websockets/commit/f95683641bda2f9b250768768451e0c121afc2a0)) * **dev:** bump aegir from 38.1.8 to 39.0.9 ([#245](https://github.com/libp2p/js-libp2p-websockets/issues/245)) ([4a35f6b](https://github.com/libp2p/js-libp2p-websockets/commit/4a35f6b39a918fb7ef779292553cb452a543afb0)) +### [7.0.9](https://www.github.com/libp2p/js-libp2p/compare/websockets-v7.0.8...websockets-v7.0.9) (2023-10-06) + + +### Bug Fixes + +* close webrtc streams without data loss ([#2073](https://www.github.com/libp2p/js-libp2p/issues/2073)) ([7d8b155](https://www.github.com/libp2p/js-libp2p/commit/7d8b15517a480e01a8ebd427ab0093509b78d5b0)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @libp2p/interface bumped from ^0.1.2 to ^0.1.3 + * @libp2p/logger bumped from ^3.0.2 to ^3.0.3 + * @libp2p/utils bumped from ^4.0.3 to ^4.0.4 + * devDependencies + * @libp2p/interface-compliance-tests bumped from ^4.1.0 to ^4.1.1 + +### [7.0.8](https://www.github.com/libp2p/js-libp2p/compare/websockets-v7.0.7...websockets-v7.0.8) (2023-10-01) + + +### Bug Fixes + +* log websocket error on graceful close failure ([#2072](https://www.github.com/libp2p/js-libp2p/issues/2072)) ([72319fe](https://www.github.com/libp2p/js-libp2p/commit/72319fe6d3b6402a92788c4c4e52eb7e0e477b3d)) + + +### Dependencies + +* The following workspace dependencies were updated + * devDependencies + * @libp2p/interface-compliance-tests bumped from ^4.0.6 to ^4.1.0 + +### [7.0.7](https://www.github.com/libp2p/js-libp2p/compare/websockets-v7.0.6...websockets-v7.0.7) (2023-09-15) + + +### Dependencies + +* The following workspace dependencies were updated + * devDependencies + * @libp2p/interface-compliance-tests bumped from ^4.0.5 to ^4.0.6 + +### [7.0.6](https://www.github.com/libp2p/js-libp2p/compare/websockets-v7.0.5...websockets-v7.0.6) (2023-09-10) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @libp2p/utils bumped from ^4.0.2 to ^4.0.3 + +### [7.0.5](https://www.github.com/libp2p/js-libp2p/compare/websockets-v7.0.4...websockets-v7.0.5) (2023-08-25) + + +### Dependencies + +* The following workspace dependencies were updated + * devDependencies + * @libp2p/interface-compliance-tests bumped from ^4.0.4 to ^4.0.5 + ### [7.0.4](https://www.github.com/libp2p/js-libp2p/compare/websockets-v7.0.3...websockets-v7.0.4) (2023-08-16) @@ -790,4 +849,4 @@ -# 0.1.0 (2016-02-26) +# 0.1.0 (2016-02-26) \ No newline at end of file diff --git a/packages/transport-websockets/package.json b/packages/transport-websockets/package.json index c9342b0c4c..df58354214 100644 --- a/packages/transport-websockets/package.json +++ b/packages/transport-websockets/package.json @@ -1,6 +1,6 @@ { "name": "@libp2p/websockets", - "version": "7.0.4", + "version": "7.0.9", "description": "JavaScript implementation of the WebSockets module that libp2p uses and that implements the interface-transport spec", "license": "Apache-2.0 OR MIT", "homepage": "https://github.com/libp2p/js-libp2p/tree/master/packages/transport-websockets#readme", @@ -51,6 +51,7 @@ "eslintConfig": { "extends": "ipfs", "parserOptions": { + "project": true, "sourceType": "module" } }, @@ -68,9 +69,9 @@ "test:electron-main": "aegir test -t electron-main -f ./dist/test/node.js --cov" }, "dependencies": { - "@libp2p/interface": "^0.1.2", - "@libp2p/logger": "^3.0.2", - "@libp2p/utils": "^4.0.2", + "@libp2p/interface": "^0.1.3", + "@libp2p/logger": "^3.0.3", + "@libp2p/utils": "^4.0.4", "@multiformats/mafmt": "^12.1.2", "@multiformats/multiaddr": "^12.1.5", "@multiformats/multiaddr-to-uri": "^9.0.2", @@ -82,8 +83,8 @@ "ws": "^8.12.1" }, "devDependencies": { - "@libp2p/interface-compliance-tests": "^4.0.4", - "aegir": "^40.0.8", + "@libp2p/interface-compliance-tests": "^4.1.1", + "aegir": "^41.0.2", "is-loopback-addr": "^2.0.1", "it-all": "^3.0.1", "it-drain": "^3.0.2", diff --git a/packages/transport-websockets/src/socket-to-conn.ts b/packages/transport-websockets/src/socket-to-conn.ts index 206ad7459b..91a44a5a44 100644 --- a/packages/transport-websockets/src/socket-to-conn.ts +++ b/packages/transport-websockets/src/socket-to-conn.ts @@ -56,6 +56,7 @@ export function socketToMaConn (stream: DuplexWebSocket, remoteAddr: Multiaddr, try { await stream.close() } catch (err: any) { + log.error('error closing WebSocket gracefully', err) this.abort(err) } finally { options.signal.removeEventListener('abort', listener) diff --git a/packages/transport-websockets/test/node.ts b/packages/transport-websockets/test/node.ts index 3526129a71..32aa1b02ae 100644 --- a/packages/transport-websockets/test/node.ts +++ b/packages/transport-websockets/test/node.ts @@ -332,6 +332,10 @@ describe('dial', () => { return !isLoopbackAddr(address) }) + if (addrs.length === 0) { + return + } + // Dial first no loopback address const conn = await ws.dial(addrs[0], { upgrader }) const s = goodbye({ source: [uint8ArrayFromString('hey')], sink: all }) diff --git a/packages/transport-webtransport/CHANGELOG.md b/packages/transport-webtransport/CHANGELOG.md index 3e5015d228..84afa61866 100644 --- a/packages/transport-webtransport/CHANGELOG.md +++ b/packages/transport-webtransport/CHANGELOG.md @@ -11,6 +11,119 @@ * bump @chainsafe/libp2p-noise from 11.0.4 to 12.0.1 ([#80](https://github.com/libp2p/js-libp2p-webtransport/issues/80)) ([599dab1](https://github.com/libp2p/js-libp2p-webtransport/commit/599dab1b4f6ae816b0c0feefc926c1b38d24b676)) +### [3.1.3](https://www.github.com/libp2p/js-libp2p/compare/webtransport-v3.1.2...webtransport-v3.1.3) (2023-10-10) + + +### Dependencies + +* The following workspace dependencies were updated + * devDependencies + * libp2p bumped from ^0.46.13 to ^0.46.14 + +### [3.1.2](https://www.github.com/libp2p/js-libp2p/compare/webtransport-v3.1.1...webtransport-v3.1.2) (2023-10-06) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @libp2p/interface bumped from ^0.1.2 to ^0.1.3 + * @libp2p/logger bumped from ^3.0.2 to ^3.0.3 + * @libp2p/peer-id bumped from ^3.0.2 to ^3.0.3 + * devDependencies + * @libp2p/peer-id-factory bumped from ^3.0.3 to ^3.0.5 + * libp2p bumped from ^0.46.12 to ^0.46.13 + +### [3.1.1](https://www.github.com/libp2p/js-libp2p/compare/webtransport-v3.1.0...webtransport-v3.1.1) (2023-10-01) + + +### Bug Fixes + +* **transports:** filter circuit addresses ([#2060](https://www.github.com/libp2p/js-libp2p/issues/2060)) ([972b10a](https://www.github.com/libp2p/js-libp2p/commit/972b10a967653f60666a061bddfa46c0decfcc70)) + + +### Dependencies + +* The following workspace dependencies were updated + * devDependencies + * libp2p bumped from ^0.46.11 to ^0.46.12 + +## [3.1.0](https://www.github.com/libp2p/js-libp2p/compare/webtransport-v3.0.11...webtransport-v3.1.0) (2023-09-20) + + +### Features + +* collect dial/listen metrics in webrtc and webtransport ([#2061](https://www.github.com/libp2p/js-libp2p/issues/2061)) ([6cb80f7](https://www.github.com/libp2p/js-libp2p/commit/6cb80f7d3b308aff955f4de247680a3c9c26993b)) + +### [3.0.11](https://www.github.com/libp2p/js-libp2p/compare/webtransport-v3.0.10...webtransport-v3.0.11) (2023-09-15) + + +### Bug Fixes + +* **@libp2p/webtransport:** handle dialing circuit addresses ([#2054](https://www.github.com/libp2p/js-libp2p/issues/2054)) ([20d5f22](https://www.github.com/libp2p/js-libp2p/commit/20d5f2200ee2a538a923f9e1df517c2bffad9105)) + + +### Dependencies + +* The following workspace dependencies were updated + * devDependencies + * libp2p bumped from ^0.46.10 to ^0.46.11 + +### [3.0.10](https://www.github.com/libp2p/js-libp2p/compare/webtransport-v3.0.9...webtransport-v3.0.10) (2023-09-10) + + +### Dependencies + +* The following workspace dependencies were updated + * devDependencies + * libp2p bumped from ^0.46.9 to ^0.46.10 + +### [3.0.9](https://www.github.com/libp2p/js-libp2p/compare/webtransport-v3.0.8...webtransport-v3.0.9) (2023-09-05) + + +### Bug Fixes + +* **@libp2p/webtransport:** remove custom WebTransport types ([#2022](https://www.github.com/libp2p/js-libp2p/issues/2022)) ([0634e3b](https://www.github.com/libp2p/js-libp2p/commit/0634e3b704e98892bd55dfd1506963d31ad4fd0b)) + + +### Dependencies + +* The following workspace dependencies were updated + * devDependencies + * libp2p bumped from ^0.46.8 to ^0.46.9 + +### [3.0.8](https://www.github.com/libp2p/js-libp2p/compare/webtransport-v3.0.7...webtransport-v3.0.8) (2023-09-01) + + +### Bug Fixes + +* **@libp2p/webtransport:** remove filters export ([#2018](https://www.github.com/libp2p/js-libp2p/issues/2018)) ([3282563](https://www.github.com/libp2p/js-libp2p/commit/328256339b1539bb048f41cd22542234b2b7a44f)) + + +### Dependencies + +* The following workspace dependencies were updated + * devDependencies + * libp2p bumped from ^0.46.7 to ^0.46.8 + +### [3.0.7](https://www.github.com/libp2p/js-libp2p/compare/webtransport-v3.0.6...webtransport-v3.0.7) (2023-08-25) + + +### Dependencies + +* The following workspace dependencies were updated + * devDependencies + * libp2p bumped from ^0.46.6 to ^0.46.7 + +### [3.0.6](https://www.github.com/libp2p/js-libp2p/compare/webtransport-v3.0.5...webtransport-v3.0.6) (2023-08-16) + + +### Dependencies + +* The following workspace dependencies were updated + * devDependencies + * libp2p bumped from ^0.46.5 to ^0.46.6 + ### [3.0.5](https://www.github.com/libp2p/js-libp2p/compare/webtransport-v3.0.4...webtransport-v3.0.5) (2023-08-16) @@ -233,4 +346,4 @@ * bump protons-runtime from 3.1.0 to 4.0.1 ([a7ef395](https://github.com/libp2p/js-libp2p-webtransport/commit/a7ef3959d024813caa327afdd502d5bcb91a15e3)) * **dev:** bump @libp2p/interface-mocks from 4.0.3 to 7.0.1 ([85a492d](https://github.com/libp2p/js-libp2p-webtransport/commit/85a492da5b8df76d710dd21dd4b8bf59df4e1184)) -* **dev:** bump uint8arrays from 3.1.1 to 4.0.2 ([cb554e8](https://github.com/libp2p/js-libp2p-webtransport/commit/cb554e8dbb19a6ec5b085307f4c04c04ae313d2d)) \ No newline at end of file +* **dev:** bump uint8arrays from 3.1.1 to 4.0.2 ([cb554e8](https://github.com/libp2p/js-libp2p-webtransport/commit/cb554e8dbb19a6ec5b085307f4c04c04ae313d2d)) diff --git a/packages/transport-webtransport/package.json b/packages/transport-webtransport/package.json index 6b7999bc06..e0442f0712 100644 --- a/packages/transport-webtransport/package.json +++ b/packages/transport-webtransport/package.json @@ -1,6 +1,6 @@ { "name": "@libp2p/webtransport", - "version": "3.0.5", + "version": "3.1.3", "description": "JavaScript implementation of the WebTransport module that libp2p uses and that implements the interface-transport spec", "license": "Apache-2.0 OR MIT", "homepage": "https://github.com/libp2p/js-libp2p/tree/master/packages/transport-webtransport#readme", @@ -42,15 +42,12 @@ ".": { "types": "./src/index.d.ts", "import": "./dist/src/index.js" - }, - "./filters": { - "types": "./dist/src/filters.d.ts", - "import": "./dist/src/filters.js" } }, "eslintConfig": { "extends": "ipfs", "parserOptions": { + "project": true, "sourceType": "module" } }, @@ -65,18 +62,20 @@ }, "dependencies": { "@chainsafe/libp2p-noise": "^13.0.0", - "@libp2p/interface": "^0.1.2", - "@libp2p/logger": "^3.0.2", - "@libp2p/peer-id": "^3.0.2", + "@libp2p/interface": "^0.1.3", + "@libp2p/logger": "^3.0.3", + "@libp2p/peer-id": "^3.0.3", "@multiformats/multiaddr": "^12.1.5", + "@multiformats/multiaddr-matcher": "^1.0.1", "it-stream-types": "^2.0.1", "multiformats": "^12.0.1", "uint8arraylist": "^2.4.3", "uint8arrays": "^4.0.6" }, "devDependencies": { - "aegir": "^40.0.8", - "libp2p": "^0.46.5", + "@libp2p/peer-id-factory": "^3.0.5", + "aegir": "^41.0.2", + "libp2p": "^0.46.14", "p-defer": "^4.0.0" }, "browser": { diff --git a/packages/transport-webtransport/src/index.ts b/packages/transport-webtransport/src/index.ts index e7d37dbf43..d37e5a6564 100644 --- a/packages/transport-webtransport/src/index.ts +++ b/packages/transport-webtransport/src/index.ts @@ -2,28 +2,20 @@ import { noise } from '@chainsafe/libp2p-noise' import { type Transport, symbol, type CreateListenerOptions, type DialOptions, type Listener } from '@libp2p/interface/transport' import { logger } from '@libp2p/logger' import { type Multiaddr, type AbortOptions } from '@multiformats/multiaddr' +import { WebTransport as WebTransportMatcher } from '@multiformats/multiaddr-matcher' import { webtransportBiDiStreamToStream } from './stream.js' import { inertDuplex } from './utils/inert-duplex.js' import { isSubset } from './utils/is-subset.js' import { parseMultiaddr } from './utils/parse-multiaddr.js' import type { Connection, MultiaddrConnection, Stream } from '@libp2p/interface/connection' +import type { CounterGroup, Metrics } from '@libp2p/interface/metrics' import type { PeerId } from '@libp2p/interface/peer-id' import type { StreamMuxerFactory, StreamMuxerInit, StreamMuxer } from '@libp2p/interface/stream-muxer' import type { Source } from 'it-stream-types' import type { MultihashDigest } from 'multiformats/hashes/interface' -declare global { - var WebTransport: any -} - -// https://www.w3.org/TR/webtransport/#web-transport-close-info -interface WebTransportCloseInfo { - closeCode: number - reason: string -} - interface WebTransportSessionCleanup { - (closeInfo?: WebTransportCloseInfo): void + (metric: string): void } const log = logger('libp2p:webtransport') @@ -34,17 +26,32 @@ export interface WebTransportInit { export interface WebTransportComponents { peerId: PeerId + metrics?: Metrics +} + +export interface WebTransportMetrics { + dialerEvents: CounterGroup } class WebTransportTransport implements Transport { private readonly components: WebTransportComponents private readonly config: Required + private readonly metrics?: WebTransportMetrics constructor (components: WebTransportComponents, init: WebTransportInit = {}) { this.components = components this.config = { maxInboundStreams: init.maxInboundStreams ?? 1000 } + + if (components.metrics != null) { + this.metrics = { + dialerEvents: components.metrics.registerCounterGroup('libp2p_webtransport_dialer_events_total', { + label: 'event', + help: 'Total count of WebTransport dialer events by type' + }) + } + } } readonly [Symbol.toStringTag] = '@libp2p/webtransport' @@ -75,10 +82,15 @@ class WebTransportTransport implements Transport { let abortListener: (() => void) | undefined let maConn: MultiaddrConnection | undefined - let cleanUpWTSession: (closeInfo?: WebTransportCloseInfo) => void = () => {} + let cleanUpWTSession: WebTransportSessionCleanup = () => {} + + let closed = false + let ready = false + let authenticated = false try { - let closed = false + this.metrics?.dialerEvents.increment({ pending: true }) + const wt = new WebTransport(`${url}/.well-known/libp2p-webtransport?type=noise`, { serverCertificateHashes: certhashes.map(certhash => ({ algorithm: 'sha-256', @@ -86,74 +98,69 @@ class WebTransportTransport implements Transport { })) }) - cleanUpWTSession = (closeInfo?: WebTransportCloseInfo) => { + cleanUpWTSession = (metric: string) => { + if (closed) { + // already closed session + return + } + try { + this.metrics?.dialerEvents.increment({ [metric]: true }) + wt.close() + } catch (err) { + log.error('error closing wt session', err) + } finally { + // This is how we specify the connection is closed and shouldn't be used. if (maConn != null) { - if (maConn.timeline.close != null) { - // already closed session - return - } - - // This is how we specify the connection is closed and shouldn't be used. maConn.timeline.close = Date.now() } - if (closed) { - // already closed session - return - } - - wt.close(closeInfo) - } catch (err) { - log.error('error closing wt session', err) - } finally { closed = true } } - // this promise resolves/throws when the session is closed or failed to init - wt.closed - .then(async () => { - await maConn?.close() - }) - .catch((err: Error) => { - log.error('error on remote wt session close', err) - maConn?.abort(err) - }) - .finally(() => { - // if we never got as far as creating the maConn, just clean up the session - if (maConn == null) { - cleanUpWTSession() - } - }) - // if the dial is aborted before we are ready, close the WebTransport session abortListener = () => { - if (abortListener != null) { - options.signal?.removeEventListener('abort', abortListener) + if (ready) { + cleanUpWTSession('noise_timeout') + } else { + cleanUpWTSession('ready_timeout') } - - cleanUpWTSession() } - options.signal?.addEventListener('abort', abortListener) + options.signal?.addEventListener('abort', abortListener, { + once: true + }) + + await Promise.race([ + wt.closed, + wt.ready + ]) - await wt.ready + ready = true + this.metrics?.dialerEvents.increment({ ready: true }) + + // this promise resolves/throws when the session is closed + wt.closed.catch((err: Error) => { + log.error('error on remote wt session close', err) + }) + .finally(() => { + cleanUpWTSession('remote_close') + }) if (!await this.authenticateWebTransport(wt, localPeer, remotePeer, certhashes)) { throw new Error('Failed to authenticate webtransport') } + this.metrics?.dialerEvents.increment({ open: true }) + maConn = { - close: async (options?: AbortOptions) => { + close: async () => { log('Closing webtransport') - cleanUpWTSession() + cleanUpWTSession('close') }, abort: (err: Error) => { log('aborting webtransport due to passed err', err) - cleanUpWTSession({ - closeCode: 0, - reason: err.message - }) + cleanUpWTSession('abort') }, remoteAddr: ma, timeline: { @@ -163,16 +170,19 @@ class WebTransportTransport implements Transport { ...inertDuplex() } - options?.signal?.throwIfAborted() + authenticated = true - return await options.upgrader.upgradeOutbound(maConn, { skipEncryption: true, muxerFactory: this.webtransportMuxer(wt, cleanUpWTSession), skipProtection: true }) + return await options.upgrader.upgradeOutbound(maConn, { skipEncryption: true, muxerFactory: this.webtransportMuxer(wt), skipProtection: true }) } catch (err: any) { log.error('caught wt session err', err) - cleanUpWTSession({ - closeCode: 0, - reason: err.message - }) + if (authenticated) { + cleanUpWTSession('upgrade_error') + } else if (ready) { + cleanUpWTSession('noise_error') + } else { + cleanUpWTSession('ready_error') + } throw err } finally { @@ -197,7 +207,7 @@ class WebTransportTransport implements Transport { yield val.value } - if (val.done === true) { + if (val.done) { break } } @@ -230,7 +240,7 @@ class WebTransportTransport implements Transport { return true } - webtransportMuxer (wt: InstanceType, cleanUpWTSession: WebTransportSessionCleanup): StreamMuxerFactory { + webtransportMuxer (wt: WebTransport): StreamMuxerFactory { let streamIDCounter = 0 const config = this.config return { @@ -252,7 +262,7 @@ class WebTransportTransport implements Transport { while (true) { const { done, value: wtStream } = await reader.read() - if (done === true) { + if (done) { break } @@ -291,14 +301,17 @@ class WebTransportTransport implements Transport { */ close: async (options?: AbortOptions) => { log('Closing webtransport muxer') - cleanUpWTSession() + + await Promise.all( + activeStreams.map(async s => s.close(options)) + ) }, abort: (err: Error) => { log('Aborting webtransport muxer with err:', err) - cleanUpWTSession({ - closeCode: 0, - reason: err.message - }) + + for (const stream of activeStreams) { + stream.abort(err) + } }, // This stream muxer is webtransport native. Therefore it doesn't plug in with any other duplex. ...inertDuplex() @@ -317,7 +330,7 @@ class WebTransportTransport implements Transport { * Takes a list of `Multiaddr`s and returns only valid webtransport addresses. */ filter (multiaddrs: Multiaddr[]): Multiaddr[] { - return multiaddrs.filter(ma => ma.protoNames().includes('webtransport')) + return multiaddrs.filter(WebTransportMatcher.exactMatch) } } diff --git a/packages/transport-webtransport/src/stream.ts b/packages/transport-webtransport/src/stream.ts index b0fb99e32d..8d9a1e8a3a 100644 --- a/packages/transport-webtransport/src/stream.ts +++ b/packages/transport-webtransport/src/stream.ts @@ -6,7 +6,7 @@ import type { Source } from 'it-stream-types' const log = logger('libp2p:webtransport:stream') -export async function webtransportBiDiStreamToStream (bidiStream: any, streamId: string, direction: Direction, activeStreams: Stream[], onStreamEnd: undefined | ((s: Stream) => void)): Promise { +export async function webtransportBiDiStreamToStream (bidiStream: WebTransportBidirectionalStream, streamId: string, direction: Direction, activeStreams: Stream[], onStreamEnd: undefined | ((s: Stream) => void)): Promise { const writer = bidiStream.writable.getWriter() const reader = bidiStream.readable.getReader() await writer.ready @@ -60,6 +60,9 @@ export async function webtransportBiDiStreamToStream (bidiStream: any, streamId: abort (err: Error) { if (!writerClosed) { writer.abort(err) + .catch(err => { + log.error('could not abort stream', err) + }) writerClosed = true } readerClosed = true @@ -140,7 +143,7 @@ export async function webtransportBiDiStreamToStream (bidiStream: any, streamId: source: (async function * () { while (true) { const val = await reader.read() - if (val.done === true) { + if (val.done) { readerClosed = true if (writerClosed) { cleanupStreamFromActiveStreams() diff --git a/packages/transport-webtransport/src/utils/parse-multiaddr.ts b/packages/transport-webtransport/src/utils/parse-multiaddr.ts index 22a51ffe55..1592119aad 100644 --- a/packages/transport-webtransport/src/utils/parse-multiaddr.ts +++ b/packages/transport-webtransport/src/utils/parse-multiaddr.ts @@ -1,5 +1,7 @@ +import { CodeError } from '@libp2p/interface/errors' import { peerIdFromString } from '@libp2p/peer-id' import { type Multiaddr, protocols } from '@multiformats/multiaddr' +import { WebTransport } from '@multiformats/multiaddr-matcher' import { bases, digest } from 'multiformats/basics' import type { PeerId } from '@libp2p/interface/peer-id' import type { MultihashDigest } from 'multiformats/hashes/interface' @@ -11,73 +13,46 @@ function decodeCerthashStr (s: string): MultihashDigest { return digest.decode(multibaseDecoder.decode(s)) } -export function parseMultiaddr (ma: Multiaddr): { url: string, certhashes: MultihashDigest[], remotePeer?: PeerId } { +export interface ParsedMultiaddr { + url: string + certhashes: MultihashDigest[] + remotePeer?: PeerId +} + +export function parseMultiaddr (ma: Multiaddr): ParsedMultiaddr { + if (!WebTransport.matches(ma)) { + throw new CodeError('Invalid multiaddr, was not a WebTransport address', 'ERR_INVALID_MULTIADDR') + } + const parts = ma.stringTuples() + const certhashes = parts + .filter(([name, _]) => name === protocols('certhash').code) + .map(([_, value]) => decodeCerthashStr(value ?? '')) + + // only take the first peer id in the multiaddr as it may be a relay + const remotePeer = parts + .filter(([name, _]) => name === protocols('p2p').code) + .map(([_, value]) => peerIdFromString(value ?? ''))[0] + + const opts = ma.toOptions() + let host = opts.host - // This is simpler to have inline than extract into a separate function - // eslint-disable-next-line complexity - const { url, certhashes, remotePeer } = parts.reduce((state: { url: string, certhashes: MultihashDigest[], seenHost: boolean, seenPort: boolean, remotePeer?: PeerId }, [proto, value]) => { - switch (proto) { - case protocols('ip6').code: - // @ts-expect-error - ts error on switch fallthrough - case protocols('dns6').code: - if (value?.includes(':') === true) { - /** - * This resolves cases where `new globalThis.WebTransport` fails to construct because of an invalid URL being passed. - * - * `new URL('https://::1:4001/blah')` will throw a `TypeError: Failed to construct 'URL': Invalid URL` - * `new URL('https://[::1]:4001/blah')` is valid and will not. - * - * @see https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2 - */ - value = `[${value}]` - } - // eslint-disable-next-line no-fallthrough - case protocols('ip4').code: - case protocols('dns4').code: - if (state.seenHost || state.seenPort) { - throw new Error('Invalid multiaddr, saw host and already saw the host or port') - } - return { - ...state, - url: `${state.url}${value ?? ''}`, - seenHost: true - } - case protocols('quic').code: - case protocols('quic-v1').code: - case protocols('webtransport').code: - if (!state.seenHost || !state.seenPort) { - throw new Error("Invalid multiaddr, Didn't see host and port, but saw quic/webtransport") - } - return state - case protocols('udp').code: - if (state.seenPort) { - throw new Error('Invalid multiaddr, saw port but already saw the port') - } - return { - ...state, - url: `${state.url}:${value ?? ''}`, - seenPort: true - } - case protocols('certhash').code: - if (!state.seenHost || !state.seenPort) { - throw new Error('Invalid multiaddr, saw the certhash before seeing the host and port') - } - return { - ...state, - certhashes: state.certhashes.concat([decodeCerthashStr(value ?? '')]) - } - case protocols('p2p').code: - return { - ...state, - remotePeer: peerIdFromString(value ?? '') - } - default: - throw new Error(`unexpected component in multiaddr: ${proto} ${protocols(proto).name} ${value ?? ''} `) - } - }, - // All webtransport urls are https - { url: 'https://', seenHost: false, seenPort: false, certhashes: [] }) + if (opts.family === 6 && host?.includes(':')) { + /** + * This resolves cases where `new WebTransport()` fails to construct because of an invalid URL being passed. + * + * `new URL('https://::1:4001/blah')` will throw a `TypeError: Failed to construct 'URL': Invalid URL` + * `new URL('https://[::1]:4001/blah')` is valid and will not. + * + * @see https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2 + */ + host = `[${host}]` + } - return { url, certhashes, remotePeer } + return { + // All webtransport urls are https + url: `https://${host}:${opts.port}`, + certhashes, + remotePeer + } } diff --git a/packages/transport-webtransport/test/browser.ts b/packages/transport-webtransport/test/browser.ts index 0cf27d2ddf..3771545df4 100644 --- a/packages/transport-webtransport/test/browser.ts +++ b/packages/transport-webtransport/test/browser.ts @@ -7,12 +7,6 @@ import { expect } from 'aegir/chai' import { createLibp2p, type Libp2p } from 'libp2p' import { webTransport } from '../src/index.js' -declare global { - interface Window { - WebTransport: any - } -} - describe('libp2p-webtransport', () => { let node: Libp2p @@ -94,8 +88,8 @@ describe('libp2p-webtransport', () => { const maStrP2p = maStr.split('/p2p/')[1] const ma = multiaddr(maStrNoCerthash + '/p2p/' + maStrP2p) - const err = await expect(node.dial(ma)).to.eventually.be.rejected() - expect(err.toString()).to.contain('Expected multiaddr to contain certhashes') + await expect(node.dial(ma)).to.eventually.be.rejected() + .with.property('code', 'ERR_NO_VALID_ADDRESSES') }) it('fails to connect due to an aborted signal', async () => { diff --git a/packages/transport-webtransport/test/transport.spec.ts b/packages/transport-webtransport/test/transport.spec.ts new file mode 100644 index 0000000000..3a5c8c921b --- /dev/null +++ b/packages/transport-webtransport/test/transport.spec.ts @@ -0,0 +1,34 @@ +/* eslint-disable no-console */ +/* eslint-env mocha */ + +import { createEd25519PeerId } from '@libp2p/peer-id-factory' +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' +import { webTransport, type WebTransportComponents } from '../src/index.js' + +describe('WebTransport Transport', () => { + let components: WebTransportComponents + + beforeEach(async () => { + components = { + peerId: await createEd25519PeerId() + } + }) + + it('transport filter filters out invalid multiaddrs', async () => { + const valid = [ + multiaddr('/ip4/1.2.3.4/udp/1234/quic-v1/webtransport/certhash/uEiAUqV7kzvM1wI5DYDc1RbcekYVmXli_Qprlw3IkiEg6tQ/certhash/uEiAUqV7kzvM1wI5DYDc1RbcekYVmXli_Qprlw3IkiEg6tQ/p2p/12D3KooWGDMwwqrpcYKpKCgxuKT2NfqPqa94QnkoBBpqvCaiCzWd') + ] + const invalid = [ + multiaddr('/ip4/1.2.3.4/udp/1234/quic-v1/webtransport/certhash/uEiAUqV7kzvM1wI5DYDc1RbcekYVmXli_Qprlw3IkiEg6tQ/certhash/uEiAUqV7kzvM1wI5DYDc1RbcekYVmXli_Qprlw3IkiEg6tQ/p2p/12D3KooWGDMwwqrpcYKpKCgxuKT2NfqPqa94QnkoBBpqvCaiCzWd/p2p-circuit/p2p/12D3KooWGDMwwqrpcYKpKCgxuKT2NfqPqa94QnkoBBpqvCaiCzWd'), + multiaddr('/ip4/1.2.3.4/udp/1234/webrtc-direct/certhash/uEiAUqV7kzvM1wI5DYDc1RbcekYVmXli_Qprlw3IkiEg6tQ/p2p/12D3KooWGDMwwqrpcYKpKCgxuKT2NfqPqa94QnkoBBpqvCaiCzWd') + ] + + const t = webTransport()(components) + + expect(t.filter([ + ...valid, + ...invalid + ])).to.deep.equal(valid) + }) +}) diff --git a/packages/transport-webtransport/test/utils/parse-multiaddr.spec.ts b/packages/transport-webtransport/test/utils/parse-multiaddr.spec.ts new file mode 100644 index 0000000000..e390f80168 --- /dev/null +++ b/packages/transport-webtransport/test/utils/parse-multiaddr.spec.ts @@ -0,0 +1,73 @@ +import { multiaddr } from '@multiformats/multiaddr' +import { expect } from 'aegir/chai' +import { base64url } from 'multiformats/bases/base64' +import { parseMultiaddr } from '../../src/utils/parse-multiaddr.js' + +describe('parse multiaddr', () => { + describe('valid addresses', () => { + it('parses relay address', () => { + const relayPeer = '12D3KooWKtv8rpaXJkLCoH4C299wFCVBg1eMzZrPfaV37QVVJrhF' + const targetPeer = '12D3KooWCDt87xcGVJWmQpaXGTSaevbRpAoMJqvsVuETDrQJvSC5' + const certHashes = [ + 'uEiCvU3clCu16U6Xjh9dzH7yKE2bkGftZw404nYMR6ZXIyg', + 'uEiB8ZfHAe_lEBtxio0KQwmE8mFEesh3p_7-Ac5oOU7HhOw' + ] + + const ma = multiaddr(`/ip4/154.38.162.255/udp/4001/quic-v1/webtransport/${certHashes.map(c => `certhash/${c}`).join('/')}/p2p/${relayPeer}/p2p-circuit/p2p/${targetPeer}`) + const { url, certhashes, remotePeer } = parseMultiaddr(ma) + + expect(url).to.equal('https://154.38.162.255:4001') + expect(certhashes.map(hash => base64url.encode(hash.bytes))).to.deep.equal(certHashes) + expect(remotePeer?.toString()).to.equal(relayPeer.toString()) + }) + + it('parses WebRTC relay address', () => { + const relayPeer = '12D3KooWKtv8rpaXJkLCoH4C299wFCVBg1eMzZrPfaV37QVVJrhF' + const targetPeer = '12D3KooWCDt87xcGVJWmQpaXGTSaevbRpAoMJqvsVuETDrQJvSC5' + const certHashes = [ + 'uEiCvU3clCu16U6Xjh9dzH7yKE2bkGftZw404nYMR6ZXIyg', + 'uEiB8ZfHAe_lEBtxio0KQwmE8mFEesh3p_7-Ac5oOU7HhOw' + ] + + const ma = multiaddr(`/ip4/154.38.162.255/udp/4001/quic-v1/webtransport/${certHashes.map(c => `certhash/${c}`).join('/')}/p2p/${relayPeer}/p2p-circuit/webrtc/p2p/${targetPeer}`) + const { url, certhashes, remotePeer } = parseMultiaddr(ma) + + expect(url).to.equal('https://154.38.162.255:4001') + expect(certhashes.map(hash => base64url.encode(hash.bytes))).to.deep.equal(certHashes) + expect(remotePeer?.toString()).to.equal(relayPeer) + }) + + it('parses ip6 loopback address', () => { + const targetPeer = '12D3KooWCDt87xcGVJWmQpaXGTSaevbRpAoMJqvsVuETDrQJvSC5' + const certHashes = [ + 'uEiCvU3clCu16U6Xjh9dzH7yKE2bkGftZw404nYMR6ZXIyg', + 'uEiB8ZfHAe_lEBtxio0KQwmE8mFEesh3p_7-Ac5oOU7HhOw' + ] + + const ma = multiaddr(`/ip6/::1/udp/4001/quic-v1/webtransport/${certHashes.map(c => `certhash/${c}`).join('/')}/p2p/${targetPeer}`) + const { url, certhashes, remotePeer } = parseMultiaddr(ma) + + expect(url).to.equal('https://[::1]:4001') + expect(certhashes.map(hash => base64url.encode(hash.bytes))).to.deep.equal(certHashes) + expect(remotePeer?.toString()).to.equal(targetPeer) + }) + }) + + describe('invalid addresses', () => { + it('fails to parse a non-webtransport address', () => { + const targetPeer = '12D3KooWCDt87xcGVJWmQpaXGTSaevbRpAoMJqvsVuETDrQJvSC5' + const ma = multiaddr(`/ip4/123.123.123.123/udp/4001/p2p/${targetPeer}`) + + expect(() => parseMultiaddr(ma)).to.throw() + .with.property('code', 'ERR_INVALID_MULTIADDR') + }) + + it('fails to parse a webtransport address without certhashes', () => { + const targetPeer = '12D3KooWCDt87xcGVJWmQpaXGTSaevbRpAoMJqvsVuETDrQJvSC5' + const ma = multiaddr(`/ip4/123.123.123.123/udp/4001/webtransport/p2p/${targetPeer}`) + + expect(() => parseMultiaddr(ma)).to.throw() + .with.property('code', 'ERR_INVALID_MULTIADDR') + }) + }) +}) diff --git a/packages/transport-webtransport/typedoc.json b/packages/transport-webtransport/typedoc.json index 72ec6c791e..f599dc728d 100644 --- a/packages/transport-webtransport/typedoc.json +++ b/packages/transport-webtransport/typedoc.json @@ -1,6 +1,5 @@ { "entryPoints": [ - "./src/index.ts", - "./src/filters.ts" + "./src/index.ts" ] } diff --git a/packages/utils/CHANGELOG.md b/packages/utils/CHANGELOG.md index d872cf026c..b432d16d92 100644 --- a/packages/utils/CHANGELOG.md +++ b/packages/utils/CHANGELOG.md @@ -11,6 +11,23 @@ * **dev:** bump aegir from 38.1.8 to 39.0.10 ([#100](https://github.com/libp2p/js-libp2p-utils/issues/100)) ([da6547c](https://github.com/libp2p/js-libp2p-utils/commit/da6547cdd073ba1a4225be5a419c6776c4ebe6f1)) +### [4.0.4](https://www.github.com/libp2p/js-libp2p/compare/utils-v4.0.3...utils-v4.0.4) (2023-10-06) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @libp2p/interface bumped from ^0.1.2 to ^0.1.3 + * @libp2p/logger bumped from ^3.0.2 to ^3.0.3 + +### [4.0.3](https://www.github.com/libp2p/js-libp2p/compare/utils-v4.0.2...utils-v4.0.3) (2023-09-10) + + +### Bug Fixes + +* **libp2p:** sort addresses to dial as public, then relay ([#2031](https://www.github.com/libp2p/js-libp2p/issues/2031)) ([5294f14](https://www.github.com/libp2p/js-libp2p/commit/5294f14caa314bb150554afff3a7ff45d2bf17ba)) + ### [4.0.2](https://www.github.com/libp2p/js-libp2p/compare/utils-v4.0.1...utils-v4.0.2) (2023-08-14) @@ -374,4 +391,4 @@ ### Features -* ip port to multiaddr ([#1](https://github.com/libp2p/js-libp2p-utils/issues/1)) ([426b421](https://github.com/libp2p/js-libp2p-utils/commit/426b421)) \ No newline at end of file +* ip port to multiaddr ([#1](https://github.com/libp2p/js-libp2p-utils/issues/1)) ([426b421](https://github.com/libp2p/js-libp2p-utils/commit/426b421)) diff --git a/packages/utils/package.json b/packages/utils/package.json index cfd196230f..89b6c0f7e5 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -1,6 +1,6 @@ { "name": "@libp2p/utils", - "version": "4.0.2", + "version": "4.0.4", "description": "Package to aggregate shared logic and dependencies for the libp2p ecosystem", "license": "Apache-2.0 OR MIT", "homepage": "https://github.com/libp2p/js-libp2p/tree/master/packages/utils#readme", @@ -68,6 +68,7 @@ "eslintConfig": { "extends": "ipfs", "parserOptions": { + "project": true, "sourceType": "module" } }, @@ -86,16 +87,17 @@ }, "dependencies": { "@chainsafe/is-ip": "^2.0.2", - "@libp2p/interface": "^0.1.2", - "@libp2p/logger": "^3.0.2", + "@libp2p/interface": "^0.1.3", + "@libp2p/logger": "^3.0.3", "@multiformats/multiaddr": "^12.1.5", + "@multiformats/multiaddr-matcher": "^1.0.1", "is-loopback-addr": "^2.0.1", "it-stream-types": "^2.0.1", "private-ip": "^3.0.0", "uint8arraylist": "^2.4.3" }, "devDependencies": { - "aegir": "^40.0.8", + "aegir": "^41.0.2", "it-all": "^3.0.1", "it-pair": "^2.0.6", "it-pipe": "^3.0.1", diff --git a/packages/utils/src/address-sort.ts b/packages/utils/src/address-sort.ts index 7ab8128561..5773a2d90c 100644 --- a/packages/utils/src/address-sort.ts +++ b/packages/utils/src/address-sort.ts @@ -20,13 +20,13 @@ * ``` */ +import { Circuit } from '@multiformats/multiaddr-matcher' import { isPrivate } from './multiaddr/is-private.js' import type { Address } from '@libp2p/interface/peer-store' /** - * Compare function for array.sort(). - * This sort aims to move the private addresses to the end of the array. - * In case of equality, a certified address will come first. + * Compare function for array.sort() that moves public addresses to the start + * of the array. */ export function publicAddressesFirst (a: Address, b: Address): -1 | 0 | 1 { const isAPrivate = isPrivate(a.multiaddr) @@ -37,7 +37,15 @@ export function publicAddressesFirst (a: Address, b: Address): -1 | 0 | 1 { } else if (!isAPrivate && isBPrivate) { return -1 } - // Check certified? + + return 0 +} + +/** + * Compare function for array.sort() that moves certified addresses to the start + * of the array. + */ +export function certifiedAddressesFirst (a: Address, b: Address): -1 | 0 | 1 { if (a.isCertified && !b.isCertified) { return -1 } else if (!a.isCertified && b.isCertified) { @@ -48,8 +56,36 @@ export function publicAddressesFirst (a: Address, b: Address): -1 | 0 | 1 { } /** - * A test thing + * Compare function for array.sort() that moves circuit relay addresses to the + * start of the array. */ -export async function something (): Promise { - return Uint8Array.from([0, 1, 2]) +export function circuitRelayAddressesLast (a: Address, b: Address): -1 | 0 | 1 { + const isACircuit = Circuit.exactMatch(a.multiaddr) + const isBCircuit = Circuit.exactMatch(b.multiaddr) + + if (isACircuit && !isBCircuit) { + return 1 + } else if (!isACircuit && isBCircuit) { + return -1 + } + + return 0 +} + +export function defaultAddressSort (a: Address, b: Address): -1 | 0 | 1 { + const publicResult = publicAddressesFirst(a, b) + + if (publicResult !== 0) { + return publicResult + } + + const relayResult = circuitRelayAddressesLast(a, b) + + if (relayResult !== 0) { + return relayResult + } + + const certifiedResult = certifiedAddressesFirst(a, b) + + return certifiedResult } diff --git a/packages/utils/test/address-sort.spec.ts b/packages/utils/test/address-sort.spec.ts index 39ede62c9a..b520ccccb2 100644 --- a/packages/utils/test/address-sort.spec.ts +++ b/packages/utils/test/address-sort.spec.ts @@ -2,50 +2,176 @@ import { multiaddr } from '@multiformats/multiaddr' import { expect } from 'aegir/chai' -import { publicAddressesFirst } from '../src/address-sort.js' +import { publicAddressesFirst, certifiedAddressesFirst, circuitRelayAddressesLast, defaultAddressSort } from '../src/address-sort.js' describe('address-sort', () => { - it('should sort public addresses first', () => { - const addresses = [ - { + describe('public addresses first', () => { + it('should sort public addresses first', () => { + const publicAddress = { + multiaddr: multiaddr('/ip4/30.0.0.1/tcp/4000'), + isCertified: false + } + const privateAddress = { multiaddr: multiaddr('/ip4/127.0.0.1/tcp/4000'), isCertified: false - }, - { + } + + const addresses = [ + privateAddress, + publicAddress + ] + + const sortedAddresses = addresses.sort(publicAddressesFirst) + expect(sortedAddresses).to.deep.equal([ + publicAddress, + privateAddress + ]) + }) + }) + + describe('certified addresses first', () => { + it('should sort certified addresses first', () => { + const certifiedPublicAddress = { + multiaddr: multiaddr('/ip4/30.0.0.1/tcp/4001'), + isCertified: true + } + const publicAddress = { multiaddr: multiaddr('/ip4/30.0.0.1/tcp/4000'), isCertified: false - }, - { - multiaddr: multiaddr('/ip4/31.0.0.1/tcp/4000'), + } + const certifiedPrivateAddress = { + multiaddr: multiaddr('/ip4/127.0.0.1/tcp/4001'), + isCertified: true + } + const privateAddress = { + multiaddr: multiaddr('/ip4/127.0.0.1/tcp/4000'), isCertified: false } - ] - const sortedAddresses = addresses.sort(publicAddressesFirst) - expect(sortedAddresses[0].multiaddr.equals(multiaddr('/ip4/30.0.0.1/tcp/4000'))).to.eql(true) - expect(sortedAddresses[1].multiaddr.equals(multiaddr('/ip4/31.0.0.1/tcp/4000'))).to.eql(true) - expect(sortedAddresses[2].multiaddr.equals(multiaddr('/ip4/127.0.0.1/tcp/4000'))).to.eql(true) + const addresses = [ + publicAddress, + certifiedPublicAddress, + certifiedPrivateAddress, + privateAddress + ] + + const sortedAddresses = addresses.sort(certifiedAddressesFirst) + expect(sortedAddresses).to.deep.equal([ + certifiedPublicAddress, + certifiedPrivateAddress, + publicAddress, + privateAddress + ]) + }) }) - it('should sort public certified addresses first', () => { - const addresses = [ - { - multiaddr: multiaddr('/ip4/127.0.0.1/tcp/4000'), + describe('circuit relay addresses last', () => { + it('should sort circuit relay addresses last', () => { + const publicAddress = { + multiaddr: multiaddr('/ip4/30.0.0.1/tcp/4000'), isCertified: false - }, - { + } + const publicRelay = { + multiaddr: multiaddr('/ip4/30.0.0.1/tcp/4000/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN/p2p-circuit/p2p/QmcrQZ6RJdpYuGvZqD5QEHAv6qX4BrQLJLQPQUrTrzdcgm'), + isCertified: false + } + + const addresses = [ + publicRelay, + publicAddress + ] + + const sortedAddresses = addresses.sort(circuitRelayAddressesLast) + expect(sortedAddresses).to.deep.equal([ + publicAddress, + publicRelay + ]) + }) + }) + + describe('default address sort', () => { + it('should sort public, then public relay, then private, then private relay with certified addresses taking priority', () => { + const certifiedPublicAddress = { + multiaddr: multiaddr('/ip4/30.0.0.1/tcp/4001'), + isCertified: true + } + const publicAddress = { multiaddr: multiaddr('/ip4/30.0.0.1/tcp/4000'), isCertified: false - }, - { - multiaddr: multiaddr('/ip4/31.0.0.1/tcp/4000'), + } + const certifiedPublicRelay = { + multiaddr: multiaddr('/ip4/30.0.0.1/tcp/4001/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN/p2p-circuit/p2p/QmcrQZ6RJdpYuGvZqD5QEHAv6qX4BrQLJLQPQUrTrzdcgm'), + isCertified: true + } + const publicRelay = { + multiaddr: multiaddr('/ip4/30.0.0.1/tcp/4000/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN/p2p-circuit/p2p/QmcrQZ6RJdpYuGvZqD5QEHAv6qX4BrQLJLQPQUrTrzdcgm'), + isCertified: false + } + const certifiedPrivateAddress = { + multiaddr: multiaddr('/ip4/127.0.0.1/tcp/4001'), isCertified: true } - ] + const privateAddress = { + multiaddr: multiaddr('/ip4/127.0.0.1/tcp/4000'), + isCertified: false + } + const certifiedPrivateRelay = { + multiaddr: multiaddr('/ip4/127.0.0.1/tcp/4001/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN/p2p-circuit/p2p/QmcrQZ6RJdpYuGvZqD5QEHAv6qX4BrQLJLQPQUrTrzdcgm'), + isCertified: true + } + const privateRelay = { + multiaddr: multiaddr('/ip4/127.0.0.1/tcp/4000/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN/p2p-circuit/p2p/QmcrQZ6RJdpYuGvZqD5QEHAv6qX4BrQLJLQPQUrTrzdcgm'), + isCertified: false + } + + const addresses = [ + privateAddress, + certifiedPrivateAddress, + publicRelay, + certifiedPublicRelay, + privateRelay, + publicAddress, + certifiedPublicAddress, + certifiedPrivateRelay + ].sort(() => { + return Math.random() > 0.5 ? -1 : 1 + }) + + const sortedAddresses = addresses.sort(defaultAddressSort) + expect(sortedAddresses).to.deep.equal([ + certifiedPublicAddress, + publicAddress, + certifiedPublicRelay, + publicRelay, + certifiedPrivateAddress, + privateAddress, + certifiedPrivateRelay, + privateRelay + ]) + }) + + it('should sort WebRTC over relay addresses before relay addresses', () => { + const publicRelay = { + multiaddr: multiaddr('/ip4/30.0.0.1/tcp/4000/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN/p2p-circuit/p2p/QmcrQZ6RJdpYuGvZqD5QEHAv6qX4BrQLJLQPQUrTrzdcgm'), + isCertified: false + } + const webRTCOverRelay = { + multiaddr: multiaddr('/ip4/30.0.0.1/tcp/4000/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN/p2p-circuit/webrtc/p2p/QmcrQZ6RJdpYuGvZqD5QEHAv6qX4BrQLJLQPQUrTrzdcgm'), + isCertified: false + } + + const addresses = [ + publicRelay, + webRTCOverRelay + ].sort(() => { + return Math.random() > 0.5 ? -1 : 1 + }) - const sortedAddresses = addresses.sort(publicAddressesFirst) - expect(sortedAddresses[0].multiaddr.equals(multiaddr('/ip4/31.0.0.1/tcp/4000'))).to.eql(true) - expect(sortedAddresses[1].multiaddr.equals(multiaddr('/ip4/30.0.0.1/tcp/4000'))).to.eql(true) - expect(sortedAddresses[2].multiaddr.equals(multiaddr('/ip4/127.0.0.1/tcp/4000'))).to.eql(true) + const sortedAddresses = addresses.sort(defaultAddressSort) + expect(sortedAddresses).to.deep.equal([ + webRTCOverRelay, + publicRelay + ]) + }) }) }) diff --git a/typedoc.json b/typedoc.json new file mode 100644 index 0000000000..55b1f87a5f --- /dev/null +++ b/typedoc.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://typedoc.org/schema.json", + "name": "libp2p" +}